Solution de Rendu Léger pour Données Géospatiales Massives : Stratégie de Tuilage Efficace

Contexte du Défi Géospatial

Dans la plupart des projets SIG web, les fonds de carte sont généralement fournis par des services en ligne comme TianDiTu ou via des tuiles Google Maps hébergées localement (par exemple, avec Nginx). Cependant, les données métier géographiques, souvent stockées dans PostGIS, peuvent atteindre des volumes considérables, excédant parfois 100 Go. Une telle quantité de données entraîne des latences de chargement inacceptables côté client et une charge excessive sur la base de données, rendant le système impraticable. Des exemples typiques incluent les parcelles forestières, les empreintes de bâtiments pour l'urbanisme, ou les couches de pentes à l'échelle d'une province (pouvant dépasser 80 Go).

Approche et Implémentation Technique

Pour résoudre ces problèmes de performance, nous proposons une stratégie de tuilage cartographique. Cette solution est compatible avec le schéma de tuilage EPSG:4490 (standard national chinois) de TianDiTu, ainsi qu'avec le schéma Web Mercator (EPSG:3857, standard internet) utilisé par Google Maps. La génération de tuiles s'étend du niveau de zoom 13 à 18 pour les détails fins. Pour les niveaux de zoom inférieurs (1 à 12), où la quantité de données serait massive, une technique de généralisation ou d'échantillonnage est appliquée, limitant chaque tuile à un maximum de 2000 entités. Cette approche permet au frontend d'afficher dynamiquement les couches métier uniquement à partir du niveau de zoom 13, optimisant ainsi les performances.

De plus, un index R-tree est appliqué aux données géospatiales lors de leur insertion en base, garantissant des requêtes spatiales ultra-rapides. Lors d'un clic sur une entité (point, ligne ou polygone) sur la carte, le backend peut instantanément retourner toutes les informations attributaires associées. Des tests avec des jeux de données de 30 Go ont montré des temps de réponse quasi nuls.

Le backend de cette solution est développé en Java, s'appuyant sur le framework Spring Boot et la bibliothèque GeoTools. Côté client, l'intégration est facilitée et supporte les APIs JavaScript populaires telles qu'OpenLayers, Leaflet, Mapbox et ArcGIS JS API.

Évaluation des Performances

Des tests de charge ont été effectués pour valider l'efficacité de la solution :

  • Sur un ordinateur portable standard (Intel i5-8400H, 16 Go de RAM, SSD 256 Go), un test de stress de 30 minutes sur l'accès aux tuiles locales a révélé un temps de réponse moyen de 24 ms, avec un débit de requêtes (QPS) dépassant 3000.
  • La génération de tuiles pour 20 millions d'entités (niveaux de zoom 14-15) à l'échelle d'une province a été accomplie en environ 45 minutes sur une machine plus puissante (Intel i7-13600KF, 32 Go de RAM, configurée pour 16 threads).

Exemples de Code Frontend pour l'Intégration

Les sections suivantes présentent des extraits de code HTML/JavaScript génériques pour l'intégration des services de tuiles TianDiTu et locaux avec différentes APIs cartographiques JavaScript. Il suffit de sauvegarder le code dans un fichier HTML et de remplacer le jeton TianDiTu (YOUR_TIANDITU_TOKEN) par votre clé API valide.

4.1 Intégration avec OpenLayers

<html lang="fr">
<head>
    <meta charset="utf-8"/>
    <title>Visualiseur Web Mercator avec OpenLayers</title>
    <link rel="icon" href="data:image/ico;base64,aWNv">

    <link rel="stylesheet" href="../../../js/lib/openlayers.css" type="text/css">
    <style type="text/css">
        body, #conteneur-carte {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            font-size: 13px;
            border: 0;
        }

        .popup-entite {
            position: relative;
            font-size: 12px;
            max-width: 300px;
            background: white;
            padding: 10px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
        }
        #indicateur-chargement {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 15px;
            border-radius: 5px;
            z-index: 1000;
        }
    </style>
    <script src="../../../js/lib/openlayers.js"></script>
    <script type="text/javascript">
        let carteSIG;
        let sourceGeometrie; // Source pour les géométries des entités
        let sourceMarqueurClic; // Source pour le marqueur de clic
        let superpositionPopup; // Superposition pour la fenêtre pop-up
        const URL_BASE_API = 'http://localhost:8080/api/geospatial/requete/'; // URL de base du service backend

        function initialiserApplication() {
            // Définir une étendue par défaut (par ex. pour une zone spécifique)
            const etendueInitialeWGS84 = [108.935, 34.249, 108.952, 34.256];
            // Transformer l'étendue en Web Mercator (EPSG:3857)
            const etendueTransformee = ol.proj.transformExtent(etendueInitialeWGS84, 'EPSG:4326', 'EPSG:3857');

            // Utiliser la projection Web Mercator
            const projectionCarte = ol.proj.get('EPSG:3857');

            // Obtenir les couches TianDiTu en Web Mercator
            const coucheVectorielleTDT = creerCoucheTianDiTuWM("vec_w");  // Fond de carte vectoriel
            const coucheAnnotationTDT = creerCoucheTianDiTuWM("cva_w");  // Annotations vectorielles

            // Couche de données locale (assurez-vous qu'elle est en 3857)
            const coucheDonneesLocale = creerCoucheLocaleWM("t_bati_py-3857-c"); // Exemple: bâtiments OpenStreetMap

            // Créer une source et une couche pour les géométries des entités
            sourceGeometrie = new ol.source.Vector();
            const coucheGeometrie = new ol.layer.Vector({
                source: sourceGeometrie,
                style: new ol.style.Style({
                    fill: new ol.style.Fill({
                        color: 'rgba(255, 100, 100, 0.3)' // Remplissage translucide
                    }),
                    stroke: new ol.style.Stroke({
                        color: 'rgb(200, 0, 0)', // Contour rouge foncé
                        width: 2
                    }),
                    image: new ol.style.Circle({
                        radius: 7,
                        fill: new ol.style.Fill({
                            color: 'rgb(200, 0, 0)'
                        }),
                        stroke: new ol.style.Stroke({
                            color: 'white',
                            width: 1
                        })
                    })
                })
            });

            // Créer une source et une couche pour le marqueur de clic
            sourceMarqueurClic = new ol.source.Vector();
            const coucheMarqueurClic = new ol.layer.Vector({
                source: sourceMarqueurClic,
                style: new ol.style.Style({
                    image: new ol.style.Circle({
                        radius: 9,
                        fill: new ol.style.Fill({
                            color: 'rgb(0, 150, 0)' // Marqueur de clic vert
                        }),
                        stroke: new ol.style.Stroke({
                            color: 'white',
                            width: 2
                        })
                    })
                })
            });

            // Créer un conteneur pour la fenêtre pop-up
            const conteneurPopup = document.createElement('div');
            conteneurPopup.className = 'popup-entite';
            superpositionPopup = new ol.Overlay({
                element: conteneurPopup,
                autoPan: true,
                autoPanAnimation: {
                    duration: 250
                }
            });

            carteSIG = new ol.Map({
                controls: ol.control.defaults({
                    attribution: false // Masquer l'attribution par défaut pour TianDiTu
                }),
                target: 'conteneur-carte',
                layers: [coucheVectorielleTDT, coucheAnnotationTDT, coucheDonneesLocale, coucheGeometrie, coucheMarqueurClic],
                overlays: [superpositionPopup],
                view: new ol.View({
                    projection: projectionCarte,
                    minZoom: 2,
                    maxZoom: 18
                })
            });
            // Ajouter un écouteur d'événements pour le clic sur la carte
            carteSIG.on('click', gererClicCarte);
            carteSIG.getView().fit(etendueTransformee, { size: carteSIG.getSize() });
        }

        // Crée une couche TianDiTu en projection Web Mercator (EPSG:3857)
        function creerCoucheTianDiTuWM(typeCouche) {
            // Remplacez 'YOUR_TIANDITU_TOKEN' par votre jeton TianDiTu valide
            const urlServiceTDT = `https://t0.tianditu.gov.cn/DataServer?T=${typeCouche}&X={x}&Y={y}&L={z}&tk=YOUR_TIANDITU_TOKEN`;

            const projection = ol.proj.get('EPSG:3857');
            const etendueProjection = [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244];

            // Calcul des résolutions pour le schéma de tuilage Web Mercator standard
            const resolutionMax = etendueProjection[3] * 2 / 256;
            const resolutions = Array.from({ length: 19 }, (_, z) => resolutionMax / Math.pow(2, z));

            const origineTuiles = ol.extent.getTopLeft(etendueProjection);

            return new ol.layer.Tile({
                extent: etendueProjection,
                source: new ol.source.XYZ({
                    url: urlServiceTDT,
                    projection: projection,
                    tileGrid: new ol.tilegrid.TileGrid({
                        origin: origineTuiles,
                        resolutions: resolutions,
                        tileSize: 256
                    }),
                    crossOrigin: 'anonymous' // Important pour éviter les problèmes CORS
                })
            });
        }

        // Crée une couche de tuiles locale en projection Web Mercator (EPSG:3857)
        function creerCoucheLocaleWM(nomCouche) {
            // URL d'un service de tuiles local ou exemple. Remplacez par votre URL de service.
            const urlServiceLocal = `http://localhost:8080/mon-serveur-de-tuiles-api/cache-zxy/${nomCouche}/{z}/{x}/{y}`;

            const projection = ol.proj.get('EPSG:3857');
            const etendueProjection = [-20037508.342789244, -20037508.342789244, 20037508.342789244, 20037508.342789244];

            const resolutionMax = etendueProjection[3] * 2 / 256;
            const resolutions = Array.from({ length: 19 }, (_, z) => resolutionMax / Math.pow(2, z));

            const origineTuiles = ol.extent.getTopLeft(etendueProjection);

            return new ol.layer.Tile({
                extent: etendueProjection,
                source: new ol.source.XYZ({
                    url: urlServiceLocal,
                    projection: projection,
                    tileGrid: new ol.tilegrid.TileGrid({
                        origin: origineTuiles,
                        resolutions: resolutions,
                        tileSize: 256
                    })
                })
            });
        }

        // Gère l'événement de clic sur la carte
        function gererClicCarte(evenement) {
            sourceMarqueurClic.clear(); // Efface les marqueurs de clic précédents

            const coordonneeClic = evenement.coordinate;

            // Transforme la coordonnée du clic en WGS84 (lat/lon)
            const coordonneesLonLat = ol.proj.transform(
                coordonneeClic,
                carteSIG.getView().getProjection(),
                'EPSG:4326'
            );

            // Ajoute un nouveau marqueur au point cliqué
            const entiteMarqueur = new ol.Feature({
                geometry: new ol.geom.Point(coordonneeClic)
            });
            sourceMarqueurClic.addFeature(entiteMarqueur);

            // Exécute une requête spatiale avec les coordonnées lat/lon transformées
            effectuerRequeteSpatiale(coordonneesLonLat[1], coordonneesLonLat[0]); // lat, lon
        }

        // Récupère un paramètre de l'URL
        function obtenirParametreURL(nom) {
            const paramsURL = new URLSearchParams(window.location.search);
            return paramsURL.get(nom);
        }

        // Effectue une requête spatiale vers le backend
        function effectuerRequeteSpatiale(latitude, longitude) {
            const nomService = obtenirParametreURL('service') || 'entite_geologique-4490';
            const urlRequete = `${URL_BASE_API}${nomService}?lat=${latitude}&lon=${longitude}&tolerance=20`;

            afficherChargement(true);

            fetch(urlRequete)
                .then(reponse => {
                    if (!reponse.ok) throw new Error(`Réponse réseau anormale: ${reponse.statusText}`);
                    return reponse.json();
                })
                .then(donnees => {
                    sourceGeometrie.clear(); // Efface les géométries affichées précédemment
                    superpositionPopup.setPosition(undefined); // Ferme la fenêtre pop-up

                    traiterDonneesSpaciales(donnees);
                    afficherChargement(false);
                })
                .catch(erreur => {
                    console.error('Erreur lors de la requête spatiale:', erreur);
                    afficherChargement(false);
                    alert(`Échec de la requête: ${erreur.message}`);
                });
        }

        // Traite les données géospatiales reçues
        function traiterDonneesSpaciales(donnees) {
            const nomCoucheRetour = Object.keys(donnees)[0];
            let entites = donnees[nomCoucheRetour];

            if (!entites) {
                alert('Aucune donnée trouvée pour la requête.');
                return;
            }

            // S'assurer que les entités sont un tableau
            if (!Array.isArray(entites)) {
                entites = [entites];
            }

            if (entites.length === 0) {
                alert('Aucune entité trouvée.');
                return;
            }

            let premiereEntiteTrouvee = null;
            entites.forEach((entite, index) => {
                try {
                    const geometrieJSON = JSON.parse(entite.geometry);
                    const entiteOL = creerEntiteGeometrique(geometrieJSON, entite);

                    if (entiteOL) {
                        sourceGeometrie.addFeature(entiteOL);
                        if (index === 0) premiereEntiteTrouvee = entiteOL;
                    }
                } catch (erreur) {
                    console.error('Échec de l\'analyse de la géométrie:', erreur);
                }
            });

            // Si des entités sont trouvées, centrer la vue sur la première et afficher la pop-up
            if (premiereEntiteTrouvee) {
                const etendueEntite = premiereEntiteTrouvee.getGeometry().getExtent();
                carteSIG.getView().fit(etendueEntite, { padding: [50, 50, 50, 50], duration: 500 }); // Ajuster le zoom

                // Afficher la fenêtre pop-up après un court délai pour que la carte ait fini de bouger
                setTimeout(() => {
                    afficherPopupPourEntite(premiereEntiteTrouvee, entites[0]);
                }, 500);
            }
        }

        // Crée un objet Feature OpenLayers à partir d'une géométrie GeoJSON et de propriétés
        function creerEntiteGeometrique(geometrieJSON, proprietes) {
            let olGeometrie;

            // Fonction de conversion de coordonnées de WGS84 (4326) vers Web Mercator (3857)
            function transformerCoordonnees(coords) {
                if (Array.isArray(coords[0]) && Array.isArray(coords[0][0])) {
                    // Cas des géométries multipartistes (MultiPolygon, MultiLineString, etc.)
                    return coords.map(transformerCoordonnees);
                } else if (Array.isArray(coords[0]) && typeof coords[0][0] === 'number') {
                    // Tableau de paires de coordonnées [longitude, latitude]
                    return coords.map(coord => ol.proj.transform(coord, 'EPSG:4326', 'EPSG:3857'));
                } else if (Array.isArray(coords) && typeof coords[0] === 'number') {
                    // Une seule paire de coordonnées [longitude, latitude]
                    return ol.proj.transform(coords, 'EPSG:4326', 'EPSG:3857');
                }
                return coords;
            }

            switch (geometrieJSON.type) {
                case 'Point':
                    olGeometrie = new ol.geom.Point(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                case 'LineString':
                    olGeometrie = new ol.geom.LineString(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                case 'Polygon':
                    olGeometrie = new ol.geom.Polygon(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                case 'MultiPoint':
                    olGeometrie = new ol.geom.MultiPoint(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                case 'MultiLineString':
                    olGeometrie = new ol.geom.MultiLineString(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                case 'MultiPolygon':
                    olGeometrie = new ol.geom.MultiPolygon(transformerCoordonnees(geometrieJSON.coordinates));
                    break;
                default:
                    console.warn('Type de géométrie inconnu:', geometrieJSON.type);
                    return null;
            }

            return new ol.Feature({
                geometry: olGeometrie,
                properties: proprietes // Stocke les propriétés dans l'entité
            });
        }

        // Affiche une fenêtre pop-up pour une entité donnée
        function afficherPopupPourEntite(entite, proprietes) {
            const contenuPopup = genererContenuPopup(proprietes);
            superpositionPopup.getElement().innerHTML = contenuPopup;

            const geometrie = entite.getGeometry();
            let coordonneePopup;

            if (geometrie.getType() === 'Point') {
                coordonneePopup = geometrie.getCoordinates();
            } else {
                // Pour les lignes et polygones, positionner la pop-up au centre de leur étendue
                coordonneePopup = ol.extent.getCenter(geometrie.getExtent());
            }
            superpositionPopup.setPosition(coordonneePopup);
        }

        // Génère le contenu HTML pour la fenêtre pop-up à partir des propriétés de l'entité
        function genererContenuPopup(proprietes) {
            let contenu = '<div class="popup-entite">';
            contenu += '<h4 style="margin-top: 0; margin-bottom: 10px;">Propriétés de l\'entité</h4>';
            contenu += '<table>';

            for (const cle in proprietes) {
                // Exclure la propriété 'geometry' qui est déjà traitée
                if (cle !== 'geometry' && proprietes.hasOwnProperty(cle)) {
                    contenu += `<tr><th>${cle}</th><td>${proprietes[cle] !== null ? proprietes[cle] : ''}</td></tr>`;
                }
            }

            contenu += '</table>';
            contenu += '</div>';
            return contenu;
        }

        // Affiche ou masque l'indicateur de chargement
        function afficherChargement(afficher) {
            let indicateurChargement = document.getElementById('indicateur-chargement');

            if (!indicateurChargement && afficher) {
                indicateurChargement = document.createElement('div');
                indicateurChargement.id = 'indicateur-chargement';
                indicateurChargement.innerHTML = 'Chargement en cours...';
                document.getElementById('conteneur-carte').appendChild(indicateurChargement);
            } else if (indicateurChargement && !afficher) {
                indicateurChargement.parentNode.removeChild(indicateurChargement);
            }
        }
    </script>
</head>
<body onload="initialiserApplication()">
<div id="conteneur-carte"></div>
</body>
</html>

4.2 Intégration avec Leaflet

<html lang="fr">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Projection Web Mercator avec Leaflet</title>
    <link rel="icon" href="data:image/ico;base64,aWNv">

    <!-- Leaflet CSS -->
    <link rel="stylesheet" href="../../../js/lib/leaflet.css"/>

    <style>
        body,
        html,
        #carte-leaflet {
            border: 0;
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            font-size: 13px;
            overflow: hidden;
            font-family: Arial, sans-serif;
        }

        .leaflet-control-attribution {
            font-size: 11px;
        }

        .controle-personnalise {
            background: white;
            padding: 5px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            cursor: pointer;
            margin: 10px;
        }

        .panneau-info {
            position: absolute;
            top: 10px;
            right: 10px;
            background: white;
            padding: 10px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            z-index: 1000;
            max-width: 300px;
            font-size: 12px;
        }

        .controle-couches {
            position: absolute;
            top: 10px;
            left: 50px;
            background: white;
            padding: 10px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            z-index: 1000;
        }

        /* Styles de la popup */
        .popup-geometrie {
            font-size: 12px;
            max-width: 300px;
        }

        .popup-geometrie table {
            width: 100%;
            border-collapse: collapse;
            margin-top: 5px;
        }

        .popup-geometrie th {
            text-align: left;
            background-color: #f2f2f2;
            padding: 4px;
            font-weight: bold;
            border: 1px solid #ddd;
        }

        .popup-geometrie td {
            padding: 4px;
            border: 1px solid #ddd;
            word-break: break-all;
        }

        #indicateur-chargement {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 20px;
            border-radius: 5px;
            z-index: 2000;
        }

        /* Marqueur de clic */
        .marqueur-clic {
            background-color: rgb(0, 150, 0); /* Vert */
            width: 20px;
            height: 20px;
            border-radius: 50%;
            border: 2px solid white;
            box-shadow: 0 0 10px rgba(0,0,0,0.5);
        }
    </style>
</head>

<body>
<div id="carte-leaflet"></div>

<!-- Panneau de contrôle des couches -->
<div class="controle-couches">
    <h4 style="margin-top: 0; margin-bottom: 8px;">Gestion des Couches</h4>
    <div>
        <label>
            <input type="checkbox" id="couche-vec-tdt" checked> TianDiTu Vectoriel
        </label>
    </div>
    <div>
        <label>
            <input type="checkbox" id="couche-cva-tdt" checked> TianDiTu Annotations
        </label>
    </div>
    <div>
        <label>
            <input type="checkbox" id="couche-locale" checked> Service Local
        </label>
    </div>
</div>

<!-- Panneau d'informations -->
<div class="panneau-info">
    <h4 style="margin-top: 0; margin-bottom: 8px;">Informations Cartographiques</h4>
    <div>Projection: EPSG:3857 (Web Mercator)</div>
    <div>Niveau de zoom: <span id="niveau-zoom">-</span></div>
    <div>Coordonnées centrales: <span id="coords-centre">-</span></div>
    <div>Étendue actuelle: <span id="etendue-carte">-</span></div>
    <div style="margin-top: 8px;">
        <button id="reinitialiser-vue" style="padding: 5px 10px; font-size: 12px;">Réinitialiser la Vue</button>
    </div>
</div>

<!-- Leaflet JS -->
<script src="../../../js/lib/leaflet.js"></script>

<script>
    let maCarte;
    let coucheVectorielleTDT, coucheAnnotationTDT, coucheServiceLocal;
    let marqueurClic; // Variable pour le marqueur de clic
    const groupeCouchesGeometrie = L.layerGroup(); // Groupe pour les géométries des requêtes
    const URL_API_SPATIALE = 'http://localhost:8080/api/geospatial/requete/'; // URL de base du service backend

    // Fonction d'initialisation
    function initialiserCarte() {
        maCarte = L.map('carte-leaflet', {
            minZoom: 2,
            maxZoom: 17,
            zoomControl: true,
            attributionControl: true,
            crs: L.CRS.EPSG3857 // Utilisation de la projection Web Mercator
        });

        // Étendue de vue par défaut (exemple: une zone spécifique)
        const etendueDefaut = [
            [34.249, 108.935], // Coin Sud-Ouest (lat, lon)
            [34.256, 108.952]  // Coin Nord-Est (lat, lon)
        ];

        // Créer les couches de base
        coucheVectorielleTDT = creerCoucheTianDiTu("vec_w");
        coucheAnnotationTDT = creerCoucheTianDiTu("cva_w");
        coucheServiceLocal = creerCoucheLocale();

        // Ajouter les couches à la carte
        coucheVectorielleTDT.addTo(maCarte);
        coucheAnnotationTDT.addTo(maCarte);
        coucheServiceLocal.addTo(maCarte);
        groupeCouchesGeometrie.addTo(maCarte); // Ajouter le groupe de couches de géométrie

        // Ajuster la vue de la carte à l'étendue par défaut
        maCarte.fitBounds(etendueDefaut);

        // Configurer les écouteurs d'événements
        configurerEvenementsCarte();

        // Mettre à jour le panneau d'information initial
        mettreAJourPanneauInfo();
    }

    // Crée une couche de tuiles TianDiTu
    function creerCoucheTianDiTu(typeCouche) {
        // Remplacez 'YOUR_TIANDITU_TOKEN' par votre jeton TianDiTu valide
        const urlTDT = `https://t0.tianditu.gov.cn/DataServer?T=${typeCouche}&X={x}&Y={y}&L={z}&tk=YOUR_TIANDITU_TOKEN`;

        const couche = L.tileLayer(urlTDT, {
            minZoom: 2,
            maxZoom: 17,
            tileSize: 256,
            noWrap: true, // Empêche la répétition des tuiles
            errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' // Tuile d'erreur
        });

        // La logique pour ajuster les niveaux de zoom si nécessaire, telle que dans l'exemple original,
        // n'est généralement pas nécessaire si le service TDT fournit des tuiles standard.
        // L'exemple original modifiait le niveau de zoom L={z}+0, ce qui est redondant.
        return couche;
    }

    // Crée une couche de tuiles locale
    function creerCoucheLocale() {
        // URL de votre service de tuiles local ou exemple
        const urlLocale = 'http://localhost:8080/mon-serveur-de-tuiles-api/cache-zxy/t_batiment-py-3857-c/{z}/{x}/{y}';

        const couche = L.tileLayer(urlLocale, {
            minZoom: 10, // Afficher à partir d'un niveau de zoom plus élevé
            maxZoom: 17,
            tileSize: 256,
            noWrap: true,
            errorTileUrl: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
        });

        return couche;
    }

    // Configure les écouteurs d'événements de la carte et des contrôles
    function configurerEvenementsCarte() {
        maCarte.on('moveend zoomend', mettreAJourPanneauInfo);
        maCarte.on('click', gererClicCarteLeaflet);

        document.getElementById('reinitialiser-vue').addEventListener('click', function () {
            const etendueReset = [
                [34.228888, 108.876331],
                [34.313841, 108.995664]
            ];
            maCarte.fitBounds(etendueReset);
        });

        document.getElementById('couche-vec-tdt').addEventListener('change', function (e) {
            if (e.target.checked) {
                maCarte.addLayer(coucheVectorielleTDT);
            } else {
                maCarte.removeLayer(coucheVectorielleTDT);
            }
        });

        document.getElementById('couche-cva-tdt').addEventListener('change', function (e) {
            if (e.target.checked) {
                maCarte.addLayer(coucheAnnotationTDT);
            } else {
                maCarte.removeLayer(coucheAnnotationTDT);
            }
        });

        document.getElementById('couche-locale').addEventListener('change', function (e) {
            if (e.target.checked) {
                maCarte.addLayer(coucheServiceLocal);
            } else {
                maCarte.removeLayer(coucheServiceLocal);
            }
        });

        L.control.scale({ position: 'bottomleft', imperial: false }).addTo(maCarte);

        // Contrôle plein écran personnalisé
        const controlePleinEcran = L.control({ position: 'topleft' });
        controlePleinEcran.onAdd = function () {
            const div = L.DomUtil.create('div', 'controle-personnalise');
            div.innerHTML = '&#x26F6;'; // Symbole de plein écran
            div.title = 'Basculer en plein écran';
            div.style.fontSize = '18px';
            div.style.padding = '5px 8px';

            div.onclick = function () {
                const elem = document.documentElement;
                if (!document.fullscreenElement) {
                    if (elem.requestFullscreen) {
                        elem.requestFullscreen();
                    } else if (elem.webkitRequestFullscreen) { /* Safari */
                        elem.webkitRequestFullscreen();
                    } else if (elem.msRequestFullscreen) { /* IE11 */
                        elem.msRequestFullscreen();
                    }
                    div.innerHTML = '&#x2715;'; // Symbole de fermeture
                } else {
                    if (document.exitFullscreen) {
                        document.exitFullscreen();
                    } else if (document.webkitExitFullscreen) { /* Safari */
                        document.webkitExitFullscreen();
                    } else if (document.msExitFullscreen) { /* IE11 */
                        document.msExitFullscreen();
                    }
                    div.innerHTML = '&#x26F6;';
                }
            };
            return div;
        };
        controlePleinEcran.addTo(maCarte);
    }

    // Met à jour le panneau d'informations de la carte
    function mettreAJourPanneauInfo() {
        document.getElementById('niveau-zoom').textContent = maCarte.getZoom();

        const centre = maCarte.getCenter();
        document.getElementById('coords-centre').textContent =
            `${centre.lat.toFixed(6)}, ${centre.lng.toFixed(6)}`;

        const etendue = maCarte.getBounds();
        document.getElementById('etendue-carte').textContent =
            `[${etendue.getSouth().toFixed(6)}, ${etendue.getWest().toFixed(6)}] à [${etendue.getNorth().toFixed(6)}, ${etendue.getEast().toFixed(6)}]`;
    }

    // Gère l'événement de clic sur la carte pour les requêtes spatiales
    function gererClicCarteLeaflet(e) {
        if (marqueurClic) {
            maCarte.removeLayer(marqueurClic);
        }

        marqueurClic = L.marker(e.latlng, {
            icon: L.divIcon({
                className: 'marqueur-clic',
                iconSize: [24, 24]
            })
        }).addTo(maCarte);

        effectuerRequeteSpatiale(e.latlng.lat, e.latlng.lng);
    }

    // Obtient un paramètre de requête de l'URL
    function obtenirParametreURL(nom) {
        const paramsURL = new URLSearchParams(window.location.search);
        return paramsURL.get(nom);
    }

    // Exécute une requête spatiale vers le backend
    function effectuerRequeteSpatiale(latitude, longitude) {
        const nomServiceRequete = obtenirParametreURL('service') || 'entite_geologique-4490';
        const urlRequete = `${URL_API_SPATIALE}${nomServiceRequete}?lat=${latitude}&lon=${longitude}&tolerance=20`;

        afficherChargement(true);

        fetch(urlRequete)
            .then(reponse => {
                if (!reponse.ok) throw new Error(`Réponse réseau non conforme: ${reponse.statusText}`);
                return reponse.json();
            })
            .then(donnees => {
                groupeCouchesGeometrie.clearLayers(); // Efface les géométries précédentes
                traiterDonneesSpaciales(donnees);
                afficherChargement(false);
            })
            .catch(erreur => {
                console.error('Erreur lors de la requête spatiale:', erreur);
                afficherChargement(false);
                alert(`Échec de la requête: ${erreur.message}`);
            });
    }

    // Traite les données géospatiales reçues et les affiche sur la carte
    function traiterDonneesSpaciales(donnees) {
        const cleService = Object.keys(donnees)[0];
        let entites = donnees[cleService];

        if (!entites || entites.length === 0) {
            alert('Aucune donnée géospatiale trouvée pour cette requête.');
            return;
        }

        entites.forEach((entite, index) => {
            try {
                const geometrieParsed = JSON.parse(entite.geometry);
                const coucheGeometrique = creerCoucheLeafletPourGeometrie(geometrieParsed, entite);

                if (coucheGeometrique) {
                    groupeCouchesGeometrie.addLayer(coucheGeometrique);

                    // Afficher la pop-up
                    setTimeout(() => {
                        ouvrirPopupPourCouche(coucheGeometrique, entite);
                    }, 200);

                    // Ajuster la vue pour la première entité trouvée
                    if (index === 0) {
                        const limites = coucheGeometrique.getBounds ? coucheGeometrique.getBounds() :
                                        coucheGeometrique.getLatLng ? L.latLngBounds(coucheGeometrique.getLatLng(), coucheGeometrique.getLatLng()) : null;
                        if (limites) {
                            maCarte.fitBounds(limites, { padding: [50, 50], maxZoom: 15 });
                        }
                    }
                }
            } catch (erreur) {
                console.error('Erreur lors du traitement d\'une géométrie:', erreur);
            }
        });
    }

    // Crée une couche Leaflet à partir d'une géométrie GeoJSON et de propriétés
    function creerCoucheLeafletPourGeometrie(geometrie, proprietes) {
        let couche = null;
        const stylePolygone = {
            color: 'rgb(200, 0, 0)', // Rouge
            weight: 2,
            opacity: 0.7,
            fillColor: 'rgba(255, 100, 100, 0.3)',
            fillOpacity: 0.3
        };
        const styleLigne = {
            color: 'rgb(0, 0, 200)', // Bleu
            weight: 3,
            opacity: 0.7
        };

        const coordonneesTransformees = extraireCoordonneesLeaflet(geometrie);

        switch (geometrie.type) {
            case 'Point':
                couche = L.marker(coordonneesTransformees);
                break;
            case 'MultiPoint':
                couche = L.layerGroup(coordonneesTransformees.map(coords => L.marker(coords)));
                break;
            case 'LineString':
                couche = L.polyline(coordonneesTransformees, styleLigne);
                break;
            case 'MultiLineString':
                couche = L.layerGroup(coordonneesTransformees.map(ligne => L.polyline(ligne, styleLigne)));
                break;
            case 'Polygon':
                couche = L.polygon(coordonneesTransformees, stylePolygone);
                break;
            case 'MultiPolygon':
                couche = L.layerGroup(coordonneesTransformees.map(poly => L.polygon(poly, stylePolygone)));
                break;
        }

        if (couche) {
            const contenuPopup = genererContenuPopupFeuille(proprietes);
            if (couche.bindPopup) {
                couche.bindPopup(contenuPopup, {
                    className: 'popup-geometrie',
                    maxWidth: 400,
                    autoClose: false,
                    closeOnClick: false
                });
            } else if (couche.eachLayer) {
                couche.eachLayer(sousCouche => {
                    if (sousCouche.bindPopup) {
                        sousCouche.bindPopup(contenuPopup, {
                            className: 'popup-geometrie',
                            maxWidth: 400,
                            autoClose: false,
                            closeOnClick: false
                        });
                    }
                });
            }
        }
        return couche;
    }

    // Détermine la position pour ouvrir la pop-up
    function obtenirLatLngPopup(couche) {
        if (!couche) return maCarte.getCenter();

        try {
            if (couche.getLatLng) { // Pour les points
                const latlng = couche.getLatLng();
                if (latlng && typeof latlng.lat === 'number') return latlng;
            } else if (couche.getBounds) { // Pour les lignes et polygones
                const limites = couche.getBounds();
                if (limites && limites.isValid()) return limites.getCenter();
            } else if (couche.eachLayer) { // Pour les groupes de couches
                let premiereLatLng = null;
                couche.eachLayer(sousCouche => {
                    if (!premiereLatLng) premiereLatLng = obtenirLatLngPopup(sousCouche);
                });
                if (premiereLatLng) return premiereLatLng;
            }
        } catch (erreur) {
            console.warn('Impossible de déterminer la position de la pop-up:', erreur);
        }
        return maCarte.getCenter();
    }

    // Ouvre la pop-up pour une couche donnée
    function ouvrirPopupPourCouche(couche, proprietes) {
        if (!couche) return;
        const positionPopup = obtenirLatLngPopup(couche);
        const contenuPopup = genererContenuPopupFeuille(proprietes);

        if (couche.bindPopup) {
            couche.bindPopup(contenuPopup, {
                className: 'popup-geometrie',
                maxWidth: 400,
                autoClose: false,
                closeOnClick: false
            });
            couche.openPopup(positionPopup);
        } else if (couche.eachLayer) {
            couche.eachLayer(sousCouche => {
                if (sousCouche.bindPopup) {
                    sousCouche.bindPopup(contenuPopup, {
                        className: 'popup-geometrie',
                        maxWidth: 400,
                        autoClose: false,
                        closeOnClick: false
                    });
                    sousCouche.openPopup(obtenirLatLngPopup(sousCouche) || positionPopup);
                    return false; // Ouvrir uniquement la première
                }
            });
        }
    }

    // Transforme les coordonnées GeoJSON ([lon, lat]) en format Leaflet ([lat, lon])
    function extraireCoordonneesLeaflet(geometrie) {
        function traiterListeCoords(listeCoords) {
            if (Array.isArray(listeCoords[0]) && typeof listeCoords[0][0] === 'number') {
                return listeCoords.map(coord => [coord[1], coord[0]]); // [lat, lon]
            } else {
                return listeCoords.map(traiterListeCoords);
            }
        }
        return traiterListeCoords(geometrie.coordinates);
    }

    // Génère le contenu HTML de la pop-up pour Leaflet
    function genererContenuPopupFeuille(proprietes) {
        let contenu = '<div class="popup-geometrie">';
        contenu += '<h4 style="margin-top: 0; margin-bottom: 10px;">Propriétés de l\'entité</h4>';
        contenu += '<table>';

        for (const cle in proprietes) {
            if (cle !== 'geometry' && proprietes.hasOwnProperty(cle)) {
                contenu += `<tr><th>${cle}</th><td>${proprietes[cle] !== null ? proprietes[cle] : ''}</td></tr>`;
            }
        }
        contenu += '</table>';
        contenu += '</div>';
        return contenu;
    }

    // Affiche ou masque l'indicateur de chargement
    function afficherChargement(afficher) {
        let indicateurChargement = document.getElementById('indicateur-chargement');

        if (!indicateurChargement && afficher) {
            indicateurChargement = document.createElement('div');
            indicateurChargement.id = 'indicateur-chargement';
            indicateurChargement.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.7); color: white; padding: 20px; border-radius: 5px; z-index: 2000;';
            indicateurChargement.innerHTML = 'Requête en cours...';
            document.getElementById('carte-leaflet').appendChild(indicateurChargement);
        } else if (indicateurChargement && !afficher) {
            indicateurChargement.parentNode.removeChild(indicateurChargement);
        }
    }

    document.addEventListener('DOMContentLoaded', initialiserCarte);
</script>
</body>
</html>

4.3 Intégration avec ArcGIS API for JavaScript

<html lang="fr">
<head>
    <meta charset="utf-8"/>
    <meta name="viewport" content="initial-scale=1, maximum-scale=1, user-scalable=no"/>
    <title>Projection Web Mercator avec ArcGIS API pour JavaScript</title>
    <link rel="icon" href="data:image/ico;base64,aWNv">
    <!-- ArcGIS API for JavaScript CSS -->
    <link rel="stylesheet" href="https://js.arcgis.com/3.46/esri/css/esri.css">
    <style>
        html, body, #vue-carte-sig {
            margin: 0;
            padding: 0;
            width: 100%;
            height: 100%;
            font-family: Arial, sans-serif;
            font-size: 13px;
            overflow: hidden;
        }

        /* Panneau de contrôle des couches */
        .controle-couches-ag {
            position: absolute;
            top: 15px;
            left: 15px;
            background: white;
            padding: 12px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            z-index: 100;
            max-width: 250px;
        }

        .controle-couches-ag h4 {
            margin: 0 0 10px 0;
            font-size: 14px;
        }

        .controle-couches-ag div {
            margin-bottom: 8px;
        }

        /* Panneau d'informations */
        .panneau-info-ag {
            position: absolute;
            top: 15px;
            right: 15px;
            background: white;
            padding: 12px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            z-index: 100;
            max-width: 300px;
        }

        .panneau-info-ag h4 {
            margin: 0 0 10px 0;
            font-size: 14px;
        }

        .panneau-info-ag div {
            margin-bottom: 5px;
        }

        .panneau-info-ag button {
            margin-top: 10px;
            padding: 5px 10px;
            font-size: 12px;
            cursor: pointer;
        }

        /* Bouton plein écran */
        .bouton-plein-ecran {
            position: absolute;
            top: 15px;
            left: 180px;
            background: white;
            width: 36px;
            height: 36px;
            border-radius: 4px;
            box-shadow: 0 1px 5px rgba(0, 0, 0, 0.4);
            z-index: 100;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            font-size: 18px;
        }

        .bouton-plein-ecran:hover {
            background: #f5f5f5;
        }

        /* Cacher le logo ArcGIS par défaut */
        .esriControlsBR {
            display: none !important;
        }

        /* Styles de marqueur et popup pour la requête */
        .marqueur-clic-ag {
            background-color: rgb(0, 150, 0); /* Vert */
            width: 20px;
            height: 20px;
            border-radius: 50%;
            border: 2px solid white;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
        }

        #indicateur-chargement-ag {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(0, 0, 0, 0.7);
            color: white;
            padding: 20px;
            border-radius: 5px;
            z-index: 2000;
        }

        .popup-contenu-ag {
            max-width: 400px;
        }

        .popup-contenu-ag table {
            width: 100%;
            border-collapse: collapse;
        }

        .popup-contenu-ag th, .popup-contenu-ag td {
            padding: 4px 8px;
            border: 1px solid #ddd;
            text-align: left;
            font-size: 12px;
        }

        .popup-contenu-ag th {
            background-color: #f5f5f5;
            font-weight: bold;
        }
    </style>
</head>

<body class="claro">
<div id="vue-carte-sig"></div>

<!-- Panneau de contrôle des couches -->
<div class="controle-couches-ag">
    <h4>Gestion des Couches</h4>
    <div>
        <label>
            <input type="checkbox" id="couche-vec-tdt-ag" checked> TianDiTu Vectoriel
        </label>
    </div>
    <div>
        <label>
            <input type="checkbox" id="couche-cva-tdt-ag" checked> TianDiTu Annotations
        </label>
    </div>
    <div>
        <label>
            <input type="checkbox" id="couche-locale-ag" checked> Service Local
        </label>
    </div>
</div>

<!-- Panneau d'informations -->
<div class="panneau-info-ag">
    <h4>Informations Cartographiques</h4>
    <div>Projection: EPSG:3857 (Web Mercator)</div>
    <div>Niveau de zoom: <span id="niveau-zoom-ag">-</span></div>
    <div>Coordonnées centrales: <span id="coords-centre-ag">-</span></div>
    <div>Étendue actuelle: <span id="etendue-carte-ag">-</span></div>
    <button id="reinitialiser-vue-ag">Réinitialiser la Vue</button>
</div>

<!-- Bouton plein écran -->
<div class="bouton-plein-ecran" id="bouton-plein-ecran-ag" title="Basculer en plein écran">&#x26F6;</div>

<!-- ArcGIS API for JavaScript -->
<script src="https://js.arcgis.com/3.46/"></script>

<script>
    require([
        'dojo/_base/declare',
        'esri/layers/TiledMapServiceLayer',
        'esri/map',
        'esri/layers/TileInfo',
        'esri/layers/LOD',
        'esri/SpatialReference',
        'esri/geometry/Extent',
        'esri/dijit/Scalebar',
        'esri/graphic',
        'esri/geometry/Point',
        'esri/geometry/Polyline',
        'esri/geometry/Polygon',
        'esri/symbols/SimpleMarkerSymbol',
        'esri/symbols/SimpleLineSymbol',
        'esri/symbols/SimpleFillSymbol',
        'esri/InfoTemplate',
        'esri/layers/GraphicsLayer',
        'esri/geometry/webMercatorUtils',
        'dojo/_base/Color',
        'dojo/dom',
        'dojo/on',
        'dojo/domReady!'
    ], function (declare, TiledMapServiceLayer, Map, TileInfo, LOD, SpatialReference, Extent, Scalebar,
                 Graphic, Point, Polyline, Polygon, SimpleMarkerSymbol, SimpleLineSymbol, SimpleFillSymbol,
                 InfoTemplate, GraphicsLayer, webMercatorUtils, Color, dom, on) {

        let maCarteAGIS;
        let coucheVecTDT, coucheCvaTDT, coucheLocaleAGIS;
        let gestionnaireChangementEtendue;

        let marqueurGraphiqueClic;
        let coucheGraphiquesGeometrie;
        const URL_API_GEOSPATIALE = 'http://localhost:8080/api/geospatial/requete/';

        // Étendue initiale pour la zone d'exemple (Web Mercator)
        const etendueInitialeWM = {
            xmin: 12122870.91,
            ymin: 4069681.55,
            xmax: 12134804.24,
            ymax: 4078174.76
        };

        // Classe de couche personnalisée pour TianDiTu (Web Mercator 3857)
        const CoucheTianDiTuWM = declare('CoucheTianDiTuWM', TiledMapServiceLayer, {
            constructor: function (typeCouche) {
                this.typeCouche = typeCouche || 'vec';
                this.spatialReference = new SpatialReference({ wkid: 3857 });
                this.initialExtent = this.fullExtent = new Extent(
                    -20037508.342789244, -20037508.342789244,
                    20037508.342789244, 20037508.342789244,
                    this.spatialReference
                );

                // Informations de tuilage standard Web Mercator
                this.tileInfo = new TileInfo({
                    'rows': 256,
                    'cols': 256,
                    'compressionQuality': 0,
                    'origin': { 'x': -20037508.342789244, 'y': 20037508.342789244 },
                    'spatialReference': { 'wkid': 3857 },
                    'lods': [
                        new LOD({ 'level': 0, 'resolution': 156543.033928, 'scale': 591657527.59 }),
                        new LOD({ 'level': 1, 'resolution': 78271.516964, 'scale': 295828763.79 }),
                        new LOD({ 'level': 2, 'resolution': 39135.758482, 'scale': 147914381.89 }),
                        new LOD({ 'level': 3, 'resolution': 19567.879241, 'scale': 73957190.94 }),
                        new LOD({ 'level': 4, 'resolution': 9783.939620, 'scale': 36978595.47 }),
                        new LOD({ 'level': 5, 'resolution': 4891.969810, 'scale': 18489297.73 }),
                        new LOD({ 'level': 6, 'resolution': 2445.984905, 'scale': 9244648.86 }),
                        new LOD({ 'level': 7, 'resolution': 1222.992453, 'scale': 4622324.43 }),
                        new LOD({ 'level': 8, 'resolution': 611.496226, 'scale': 2311162.21 }),
                        new LOD({ 'level': 9, 'resolution': 305.748113, 'scale': 1155581.10 }),
                        new LOD({ 'level': 10, 'resolution': 152.874057, 'scale': 577790.55 }),
                        new LOD({ 'level': 11, 'resolution': 76.437028, 'scale': 288895.27 }),
                        new LOD({ 'level': 12, 'resolution': 38.218514, 'scale': 144447.63 }),
                        new LOD({ 'level': 13, 'resolution': 19.109257, 'scale': 72223.81 }),
                        new LOD({ 'level': 14, 'resolution': 9.554629, 'scale': 36111.90 }),
                        new LOD({ 'level': 15, 'resolution': 4.777314, 'scale': 18055.95 }),
                        new LOD({ 'level': 16, 'resolution': 2.388657, 'scale': 9027.97 }),
                        new LOD({ 'level': 17, 'resolution': 1.194329, 'scale': 4513.98 }),
                        new LOD({ 'level': 18, 'resolution': 0.597164, 'scale': 2256.99 }),
                        new LOD({ 'level': 19, 'resolution': 0.298582, 'scale': 1128.49 })
                    ]
                });

                this.loaded = true;
                this.onLoad(this);
            },

            getTileUrl: function (niveau, ligne, colonne) {
                const jetonTDT = 'YOUR_TIANDITU_TOKEN'; // Remplacez par votre jeton TianDiTu
                if (this.typeCouche === 'vec') {
                    return `https://t0.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${niveau}&TILEROW=${ligne}&TILECOL=${colonne}&tk=${jetonTDT}`;
                } else if (this.typeCouche === 'cva') {
                    return `https://t0.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX=${niveau}&TILEROW=${ligne}&TILECOL=${colonne}&tk=${jetonTDT}`;
                }
                return '';
            }
        });

        // Classe de couche personnalisée pour le service local (Web Mercator 3857)
        const CoucheLocaleWM = declare('CoucheLocaleWM', TiledMapServiceLayer, {
            constructor: function () {
                this.spatialReference = new SpatialReference({ wkid: 3857 });
                this.initialExtent = this.fullExtent = new Extent(
                    -20037508.342789244, -20037508.342789244,
                    20037508.342789244, 20037508.342789244,
                    this.spatialReference
                );

                this.tileInfo = new TileInfo({
                    'rows': 256,
                    'cols': 256,
                    'compressionQuality': 0,
                    'origin': { 'x': -20037508.342789244, 'y': 20037508.342789244 },
                    'spatialReference': { 'wkid': 3857 },
                    'lods': [
                        new LOD({ 'level': 0, 'resolution': 156543.033928, 'scale': 591657527.59 }),
                        new LOD({ 'level': 1, 'resolution': 78271.516964, 'scale': 295828763.79 }),
                        new LOD({ 'level': 2, 'resolution': 39135.758482, 'scale': 147914381.89 }),
                        new LOD({ 'level': 3, 'resolution': 19567.879241, 'scale': 73957190.94 }),
                        new LOD({ 'level': 4, 'resolution': 9783.939620, 'scale': 36978595.47 }),
                        new LOD({ 'level': 5, 'resolution': 4891.969810, 'scale': 18489297.73 }),
                        new LOD({ 'level': 6, 'resolution': 2445.984905, 'scale': 9244648.86 }),
                        new LOD({ 'level': 7, 'resolution': 1222.992453, 'scale': 4622324.43 }),
                        new LOD({ 'level': 8, 'resolution': 611.496226, 'scale': 2311162.21 }),
                        new LOD({ 'level': 9, 'resolution': 305.748113, 'scale': 1155581.10 }),
                        new LOD({ 'level': 10, 'resolution': 152.874057, 'scale': 577790.55 }),
                        new LOD({ 'level': 11, 'resolution': 76.437028, 'scale': 288895.27 }),
                        new LOD({ 'level': 12, 'resolution': 38.218514, 'scale': 144447.63 }),
                        new LOD({ 'level': 13, 'resolution': 19.109257, 'scale': 72223.81 }),
                        new LOD({ 'level': 14, 'resolution': 9.554629, 'scale': 36111.90 }),
                        new LOD({ 'level': 15, 'resolution': 4.777314, 'scale': 18055.95 }),
                        new LOD({ 'level': 16, 'resolution': 2.388657, 'scale': 9027.97 }),
                        new LOD({ 'level': 17, 'resolution': 1.194329, 'scale': 4513.98 }),
                        new LOD({ 'level': 18, 'resolution': 0.597164, 'scale': 2256.99 }),
                        new LOD({ 'level': 19, 'resolution': 0.298582, 'scale': 1128.49 })
                    ]
                });

                this.loaded = true;
                this.onLoad(this);
            },

            getTileUrl: function (niveau, ligne, colonne) {
                // URL d'un service de tuiles local ou exemple. Remplacez par votre URL.
                return `http://localhost:8080/mon-serveur-de-tuiles-api/cache-zxy/t_batiment-py-3857-c/${niveau}/${colonne}/${ligne}`;
            }
        });

        // Initialiser la carte ArcGIS
        function initialiserCarteAGIS() {
            // Créer l'étendue par défaut en Web Mercator
            const etendueDefaut = new Extent(
                etendueInitialeWM.xmin, etendueInitialeWM.ymin,
                etendueInitialeWM.xmax, etendueInitialeWM.ymax,
                new SpatialReference({ wkid: 4326 }) // L'API convertira de 4326 à 3857 si nécessaire
            );

            maCarteAGIS = new Map('vue-carte-sig', {
                extent: etendueDefaut,
                zoom: 12,
                logo: false,
                slider: true,
                sliderStyle: 'small'
            });

            // Créer les couches
            coucheVecTDT = new CoucheTianDiTuWM('vec');
            coucheCvaTDT = new CoucheTianDiTuWM('cva');
            coucheLocaleAGIS = new CoucheLocaleWM();

            // Ajouter les couches à la carte
            maCarteAGIS.addLayer(coucheVecTDT);
            maCarteAGIS.addLayer(coucheCvaTDT);
            maCarteAGIS.addLayer(coucheLocaleAGIS);

            // Couche pour afficher les graphiques des requêtes spatiales
            coucheGraphiquesGeometrie = new GraphicsLayer();
            maCarteAGIS.addLayer(coucheGraphiquesGeometrie);

            // Ajouter la barre d'échelle
            const barreEchelle = new Scalebar({
                map: maCarteAGIS,
                scalebarUnit: 'metric',
                attachTo: 'bottom-left'
            });

            // Configurer les écouteurs d'événements
            configurerEvenementsAGIS();

            // Mettre à jour le panneau d'information initial
            mettreAJourPanneauInfoAGIS();
        }

        // Configurer les écouteurs d'événements pour la carte et les contrôles
        function configurerEvenementsAGIS() {
            gestionnaireChangementEtendue = maCarteAGIS.on('extent-change', mettreAJourPanneauInfoAGIS);
            maCarteAGIS.on('click', gererClicCarteAGIS);

            on(dom.byId('reinitialiser-vue-ag'), 'click', function () {
                const etendueReset = new Extent(
                    etendueInitialeWM.xmin, etendueInitialeWM.ymin,
                    etendueInitialeWM.xmax, etendueInitialeWM.ymax,
                    new SpatialReference({ wkid: 3857 }) // L'extent doit être en 3857 pour setExtent
                );
                maCarteAGIS.setExtent(etendueReset);
            });

            on(dom.byId('couche-vec-tdt-ag'), 'change', function (e) {
                if (e.target.checked) {
                    maCarteAGIS.addLayer(coucheVecTDT);
                } else {
                    maCarteAGIS.removeLayer(coucheVecTDT);
                }
            });

            on(dom.byId('couche-cva-tdt-ag'), 'change', function (e) {
                if (e.target.checked) {
                    maCarteAGIS.addLayer(coucheCvaTDT);
                } else {
                    maCarteAGIS.removeLayer(coucheCvaTDT);
                }
            });

            on(dom.byId('couche-locale-ag'), 'change', function (e) {
                if (e.target.checked) {
                    maCarteAGIS.addLayer(coucheLocaleAGIS);
                } else {
                    maCarteAGIS.removeLayer(coucheLocaleAGIS);
                }
            });

            // Fonctionnalité plein écran
            on(dom.byId('bouton-plein-ecran-ag'), 'click', function () {
                const elem = document.documentElement;
                const btn = dom.byId('bouton-plein-ecran-ag');

                if (!document.fullscreenElement &&
                    !document.mozFullScreenElement &&
                    !document.webkitFullscreenElement &&
                    !document.msFullscreenElement) {

                    if (elem.requestFullscreen) {
                        elem.requestFullscreen();
                    } else if (elem.msRequestFullscreen) {
                        elem.msRequestFullscreen();
                    } else if (elem.mozRequestFullScreen) {
                        elem.mozRequestFullScreen();
                    } else if (elem.webkitRequestFullscreen) {
                        elem.webkitRequestFullscreen();
                    }
                    btn.innerHTML = '&#x2715;'; // Symbole de fermeture
                } else {
                    if (document.exitFullscreen) {
                        document.exitFullscreen();
                    } else if (document.msExitFullscreen) {
                        document.msExitFullscreen();
                    } else if (document.mozCancelFullScreen) {
                        document.mozCancelFullScreen();
                    } else if (document.webkitExitFullscreen) {
                        document.webkitExitFullscreen();
                    }
                    btn.innerHTML = '&#x26F6;'; // Symbole de plein écran
                }
            });

            // Mettre à jour le bouton de plein écran lors de la sortie
            document.addEventListener('fullscreenchange', gererSortiePleinEcran);
            document.addEventListener('webkitfullscreenchange', gererSortiePleinEcran);
            document.addEventListener('mozfullscreenchange', gererSortiePleinEcran);
            document.addEventListener('MSFullscreenChange', gererSortiePleinEcran);

            function gererSortiePleinEcran() {
                const btn = dom.byId('bouton-plein-ecran-ag');
                if (!document.fullscreenElement &&
                    !document.mozFullScreenElement &&
                    !document.webkitFullscreenElement &&
                    !document.msFullscreenElement) {
                    btn.innerHTML = '&#x26F6;';
                }
            }
        }

        // Gère les clics sur la carte pour les requêtes spatiales
        function gererClicCarteAGIS(evt) {
            if (marqueurGraphiqueClic) {
                maCarteAGIS.graphics.remove(marqueurGraphiqueClic);
            }

            const pointClic = evt.mapPoint;
            const symboleMarqueur = new SimpleMarkerSymbol(
                SimpleMarkerSymbol.STYLE_CIRCLE,
                20,
                new SimpleLineSymbol(
                    SimpleLineSymbol.STYLE_SOLID,
                    new Color([255, 255, 255]),
                    2
                ),
                new Color([0, 150, 0, 0.8]) // Vert
            );

            marqueurGraphiqueClic = new Graphic(pointClic, symboleMarqueur);
            maCarteAGIS.graphics.add(marqueurGraphiqueClic);

            // Convertir le point cliqué en coordonnées géographiques (WGS84)
            const pointGeographique = webMercatorUtils.webMercatorToGeographic(pointClic);

            // Exécuter la requête spatiale avec les coordonnées lat/lon
            effectuerRequeteSpatialeAGIS(pointGeographique.y, pointGeographique.x);
        }

        // Obtient un paramètre de requête de l'URL
        function obtenirParametreURLAGIS(nom) {
            const paramsURL = new URLSearchParams(window.location.search);
            return paramsURL.get(nom);
        }

        // Effectue une requête spatiale vers le backend
        function effectuerRequeteSpatialeAGIS(latitude, longitude) {
            const nomServiceRequete = obtenirParametreURLAGIS('service') || 'entite_geologique-4490';
            const urlRequete = `${URL_API_GEOSPATIALE}${nomServiceRequete}?lat=${latitude}&lon=${longitude}&tolerance=20`;

            afficherChargementAGIS(true);
            coucheGraphiquesGeometrie.clear(); // Efface les graphiques précédents

            fetch(urlRequete)
                .then(reponse => {
                    if (!reponse.ok) throw new Error(`Réponse réseau non conforme: ${reponse.statusText}`);
                    return reponse.json();
                })
                .then(donnees => {
                    traiterDonneesSpacialesAGIS(donnees);
                    afficherChargementAGIS(false);
                })
                .catch(erreur => {
                    console.error('Erreur lors de la requête spatiale:', erreur);
                    afficherChargementAGIS(false);
                    alert(`Échec de la requête: ${erreur.message}`);
                });
        }

        // Traite les données géospatiales reçues et les affiche
        function traiterDonneesSpacialesAGIS(donnees) {
            let entites = [];

            if (Array.isArray(donnees)) {
                entites = donnees;
            } else if (donnees && typeof donnees === 'object') {
                const cle = Object.keys(donnees)[0];
                if (cle && Array.isArray(donnees[cle])) {
                    entites = donnees[cle];
                } else if (donnees.type === 'FeatureCollection' && Array.isArray(donnees.features)) {
                    entites = donnees.features;
                } else if (Array.isArray(donnees.features)) {
                    entites = donnees.features;
                } else if (Array.isArray(donnees.records)) {
                    entites = donnees.records;
                } else if (Array.isArray(donnees.results)) {
                    entites = donnees.results;
                } else if (Array.isArray(donnees.data)) {
                    entites = donnees.data;
                }
            }

            if (!entites || entites.length === 0) {
                console.warn('Aucune donnée valide trouvée pour l\'affichage.', donnees);
                alert('Aucune donnée géospatiale trouvée ou structure de données incorrecte.');
                return;
            }

            let premierGraphique = null;
            entites.forEach((entite, index) => {
                try {
                    let geometrieObjet = null;
                    let proprietesEntite = {};

                    if (entite.geometry && typeof entite.geometry === 'string') {
                        geometrieObjet = JSON.parse(entite.geometry);
                        proprietesEntite = entite;
                    } else if (entite.geometry && typeof entite.geometry === 'object') {
                        geometrieObjet = entite.geometry;
                        proprietesEntite = entite.properties || entite;
                    } else if (entite.shape && typeof entite.shape === 'string') {
                        geometrieObjet = JSON.parse(entite.shape);
                        proprietesEntite = entite;
                    } else if (entite.coordinates) {
                        geometrieObjet = entite;
                        proprietesEntite = entite.properties || {};
                    }

                    if (!geometrieObjet) {
                        console.warn('L\'entité ne contient pas de géométrie valide:', entite);
                        return;
                    }

                    const graphique = creerGraphiqueArcGIS(geometrieObjet, proprietesEntite);
                    if (graphique) {
                        coucheGraphiquesGeometrie.add(graphique);
                        if (index === 0 && graphique.geometry && graphique.geometry.getExtent) {
                            premierGraphique = graphique;
                            maCarteAGIS.setExtent(graphique.geometry.getExtent().expand(1.5)).catch(e => console.warn('Erreur lors du centrage de la carte:', e));
                        }
                    }
                } catch (erreur) {
                    console.error('Échec du traitement d\'une entité:', erreur, entite);
                }
            });

            if (premierGraphique) {
                setTimeout(() => {
                    maCarteAGIS.infoWindow.setFeatures([premierGraphique]);
                    maCarteAGIS.infoWindow.show(premierGraphique.geometry.getExtent().getCenter());
                }, 500);
            }
        }

        // Crée un graphique ArcGIS à partir d'une géométrie GeoJSON et de propriétés
        function creerGraphiqueArcGIS(geometrie, proprietes) {
            try {
                let geometrieEsri = null;
                let symbole = null;

                switch (geometrie.type) {
                    case 'Point':
                        geometrieEsri = new Point(geometrie.coordinates[0], geometrie.coordinates[1], new SpatialReference({ wkid: 4326 }));
                        symbole = new SimpleMarkerSymbol(
                            SimpleMarkerSymbol.STYLE_CIRCLE, 12,
                            new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([0, 0, 255]), 2), // Bleu
                            new Color([0, 0, 255, 0.7])
                        );
                        break;
                    case 'LineString':
                        geometrieEsri = new Polyline(new SpatialReference({ wkid: 4326 }));
                        geometrieEsri.addPath(geometrie.coordinates.map(coord => [coord[0], coord[1]]));
                        symbole = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([0, 0, 255]), 3); // Bleu
                        break;
                    case 'Polygon':
                        geometrieEsri = new Polygon(new SpatialReference({ wkid: 4326 }));
                        geometrieEsri.addRing(geometrie.coordinates[0].map(coord => [coord[0], coord[1]]));
                        symbole = new SimpleFillSymbol(
                            SimpleFillSymbol.STYLE_SOLID,
                            new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([255, 0, 0]), 2), // Rouge
                            new Color([255, 0, 0, 0.3])
                        );
                        break;
                    case 'MultiPoint':
                        // Simplification: Afficher uniquement le premier point
                        if (geometrie.coordinates && geometrie.coordinates.length > 0) {
                            geometrieEsri = new Point(geometrie.coordinates[0][0], geometrie.coordinates[0][1], new SpatialReference({ wkid: 4326 }));
                            symbole = new SimpleMarkerSymbol(
                                SimpleMarkerSymbol.STYLE_CIRCLE, 10,
                                new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([0, 255, 0]), 2), // Vert
                                new Color([0, 255, 0, 0.7])
                            );
                        }
                        break;
                    case 'MultiLineString':
                        // Simplification: Afficher uniquement la première ligne
                        if (geometrie.coordinates && geometrie.coordinates.length > 0) {
                            geometrieEsri = new Polyline(new SpatialReference({ wkid: 4326 }));
                            geometrieEsri.addPath(geometrie.coordinates[0].map(coord => [coord[0], coord[1]]));
                            symbole = new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([255, 165, 0]), 3); // Orange
                        }
                        break;
                    case 'MultiPolygon':
                        // Simplification: Afficher uniquement le premier anneau du premier polygone
                        if (geometrie.coordinates && geometrie.coordinates.length > 0 && geometrie.coordinates[0].length > 0) {
                            geometrieEsri = new Polygon(new SpatialReference({ wkid: 4326 }));
                            geometrieEsri.addRing(geometrie.coordinates[0][0].map(coord => [coord[0], coord[1]]));
                            symbole = new SimpleFillSymbol(
                                SimpleFillSymbol.STYLE_SOLID,
                                new SimpleLineSymbol(SimpleLineSymbol.STYLE_SOLID, new Color([255, 165, 0]), 2), // Orange
                                new Color([255, 165, 0, 0.3])
                            );
                        }
                        break;
                    default:
                        console.warn('Type de géométrie ArcGIS inconnu:', geometrie.type);
                        return null;
                }

                if (geometrieEsri && symbole) {
                    const graphique = new Graphic(geometrieEsri, symbole);
                    const contenuPopup = genererContenuPopupAGIS(proprietes);
                    graphique.setInfoTemplate(new InfoTemplate("Propriétés de l'entité", contenuPopup));
                    graphique.setAttributes(proprietes);
                    return graphique;
                }
                return null;
            } catch (erreur) {
                console.error('Échec de la création du graphique ArcGIS:', erreur, geometrie);
                return null;
            }
        }

        // Génère le contenu HTML de la pop-up pour ArcGIS
        function genererContenuPopupAGIS(proprietes) {
            let contenu = '<div class="popup-contenu-ag">';
            contenu += '<table>';

            for (const cle in proprietes) {
                if (cle !== 'geometry' && cle !== 'shape' && proprietes.hasOwnProperty(cle)) {
                    contenu += `<tr><th>${cle}</th><td>${proprietes[cle] !== null ? proprietes[cle] : ''}</td></tr>`;
                }
            }
            contenu += '</table>';
            contenu += '</div>';
            return contenu;
        }

        // Affiche ou masque l'indicateur de chargement
        function afficherChargementAGIS(afficher) {
            let indicateurChargement = document.getElementById('indicateur-chargement-ag');

            if (!indicateurChargement && afficher) {
                indicateurChargement = document.createElement('div');
                indicateurChargement.id = 'indicateur-chargement-ag';
                indicateurChargement.style.cssText = 'position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.7); color: white; padding: 20px; border-radius: 5px; z-index: 2000;';
                indicateurChargement.innerHTML = 'Requête en cours...';
                document.getElementById('vue-carte-sig').appendChild(indicateurChargement);
            } else if (indicateurChargement && !afficher) {
                indicateurChargement.parentNode.removeChild(indicateurChargement);
            }
        }

        // Met à jour le panneau d'informations de la carte
        function mettreAJourPanneauInfoAGIS() {
            if (!maCarteAGIS || !maCarteAGIS.extent) return;

            const etendue = maCarteAGIS.extent;
            const centre = etendue.getCenter();
            const niveau = maCarteAGIS.getLevel();

            dom.byId('niveau-zoom-ag').textContent = niveau !== -1 ? niveau : '-'; // -1 si le niveau n'est pas encore défini
            dom.byId('coords-centre-ag').textContent = `${centre.y.toFixed(2)}, ${centre.x.toFixed(2)}`;
            dom.byId('etendue-carte-ag').textContent = `[${etendue.ymin.toFixed(2)}, ${etendue.xmin.toFixed(2)}] à [${etendue.ymax.toFixed(2)}, ${etendue.xmax.toFixed(2)}]`;
        }

        // Initialiser la carte après le chargement du DOM
        initialiserCarteAGIS();
    });
</script>
</body>
</html>

Étiquettes: GIS Géospatial Tuilage Cartographique OpenLayers Leaflet

Publié le 22 juin à 19h11