# Analytique - créer des tables attributaires

### <span style="color: rgb(186, 55, 42);">**C'est parti ! On va donner une dimension analytique à ton SIG. On procède en trois étapes rapides.**</span>

### Étape 1 : Préparer la Base de Données

On va ajouter la colonne `surface_m2` et mettre à jour tes données actuelles.

1. **Connecte-toi à la base** :
    
    <div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1248 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQmAo" decode-data-ved="1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_c7200c97fab093a9","c_5b7951c496f77574",null,"rc_57676a7f47115c67",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1248 ng-star-inserted"><span class="ng-tns-c2446650269-1248">Bash</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1248 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1248 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1248"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1248">  
    </div></div></div>```
    psql -U alteris_user -d alteris_db -h localhost
    
    ```
    
    <div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1248 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQmAo" decode-data-ved="1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_c7200c97fab093a9","c_5b7951c496f77574",null,"rc_57676a7f47115c67",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1248"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1248"></div></div></div>*(Entre ton mot de passe `Julien...`)*

- **Exécute ces deux commandes SQL** :
    
    <div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1249 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQmQo" decode-data-ved="1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_c7200c97fab093a9","c_5b7951c496f77574",null,"rc_57676a7f47115c67",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1249 ng-star-inserted"><span class="ng-tns-c2446650269-1249">SQL</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1249 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1249 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1249"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1249">  
    </div></div></div>```
    -- 1. Ajoute la colonne pour stocker le calcul
    ALTER TABLE zones_interet ADD COLUMN surface_m2 FLOAT;
    
    -- 2. Calcule la surface pour les zones que tu as déjà dessinées
    -- On utilise ::geography pour forcer le calcul en mètres (et pas en degrés)
    UPDATE zones_interet SET surface_m2 = ST_Area(geom::geography);
    
    ```
    
    <div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1249 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQmQo" decode-data-ved="1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_c7200c97fab093a9","c_5b7951c496f77574",null,"rc_57676a7f47115c67",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1249"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1249"></div></div></div>
- Quitte avec `\q`.

Voici le contenu intégral et optimisé pour ton fichier **`server.js`**. Il inclut la gestion des fichiers statiques, la route de récupération avec les surfaces, la vérification de clic et la nouvelle route de sauvegarde avec calcul PostGIS.

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1269 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQywo" decode-data-ved="1" id="bkmrk-javascript" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_274a500626680477","c_5b7951c496f77574",null,"rc_aab1d6978c0df5b4",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1269 ng-star-inserted"><span class="ng-tns-c2446650269-1269">JavaScript</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1269 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1269 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1269"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1269">  
</div></div></div>```
const express = require('express');
const bodyParser = require('body-parser');
const pool = require('./db'); 

const app = express();
const PORT = 3001;

// Middleware pour lire le JSON et servir le dossier public (HTML/JS)
app.use(bodyParser.json());
app.use(express.static('public'));

// --- 1. RÉCUPÉRER TOUTES LES ZONES (AVEC SURFACE) ---
app.get('/api/zones', async (req, res) => {
  try {
    // On récupère le nom, la surface et la géométrie convertie en GeoJSON
    const query = `
      SELECT id, nom, surface_m2, ST_AsGeoJSON(geom) as geometrie 
      FROM zones_interet;
    `;
    const result = await pool.query(query);

    const features = result.rows.map(row => ({
      type: "Feature",
      properties: { 
        nom: row.nom,
        surface: row.surface_m2 
      },
      geometry: JSON.parse(row.geometrie)
    }));

    res.json({ type: "FeatureCollection", features: features });
  } catch (err) {
    console.error("Erreur GET /api/zones :", err);
    res.status(500).json({ error: "Erreur lors de la récupération des zones" });
  }
});

// --- 2. VÉRIFIER SI UN POINT EST DANS UNE ZONE ---
app.post('/api/check-zone', async (req, res) => {
  try {
    const geoJsonInput = req.body;
    const query = `
      SELECT nom, surface_m2 
      FROM zones_interet 
      WHERE ST_Contains(geom, ST_SetSRID(ST_GeomFromGeoJSON($1), 4326));
    `;
    const result = await pool.query(query, [JSON.stringify(geoJsonInput)]);

    if (result.rows.length > 0) {
      const zone = result.rows[0];
      const s = zone.surface_m2;
      const surfTexte = s > 10000 ? (s/10000).toFixed(2) + " ha" : Math.round(s) + " m²";
      
      res.json({ 
        status: "success", 
        message: `📍 Dans la zone : ${zone.nom} (${surfTexte})` 
      });
    } else {
      res.json({ status: "success", message: "📍 Hors de toute zone connue." });
    }
  } catch (err) {
    console.error("Erreur POST /api/check-zone :", err);
    res.status(500).json({ error: "Erreur calcul spatial" });
  }
});

// --- 3. SAUVEGARDER UNE NOUVELLE ZONE AVEC CALCUL DE SURFACE ---
app.post('/api/save-zone', async (req, res) => {
  try {
    const { nom, geojson } = req.body;

    // L'astuce PostGIS : ::geography force le calcul en mètres carrés
    const query = `
      INSERT INTO zones_interet (nom, geom, surface_m2)
      VALUES (
        $1, 
        ST_SetSRID(ST_GeomFromGeoJSON($2), 4326),
        ST_Area(ST_SetSRID(ST_GeomFromGeoJSON($2), 4326)::geography)
      )
      RETURNING id, surface_m2;
    `;

    const result = await pool.query(query, [nom, JSON.stringify(geojson.geometry)]);
    const surface = Math.round(result.rows[0].surface_m2);

    res.json({ 
      status: "success", 
      message: "Zone enregistrée !", 
      surface: surface,
      id: result.rows[0].id 
    });
  } catch (err) {
    console.error("Erreur POST /api/save-zone :", err);
    res.status(500).json({ error: "Erreur lors de la sauvegarde" });
  }
});

// Lancement du serveur
app.listen(PORT, () => {
  console.log(`🚀 Serveur Alteris prêt sur le port ${PORT}`);
});

```

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1269 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQywo" decode-data-ved="1" id="bkmrk--1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_274a500626680477","c_5b7951c496f77574",null,"rc_aab1d6978c0df5b4",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1269"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1269"></div></div></div>1. **Relance PM2** :
    
    <div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1270 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQzAo" decode-data-ved="1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_274a500626680477","c_5b7951c496f77574",null,"rc_aab1d6978c0df5b4",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1270 ng-star-inserted"><span class="ng-tns-c2446650269-1270">Bash</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1270 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1270 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1270"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1270">  
    </div></div></div>```
    pm2 restart alteris-api
    ```

Voici le fichier **`index.html`** complet et optimisé. Il est synchronisé avec ton nouveau `server.js` pour afficher les surfaces (en <span class="math-inline" data-index-in-node="126" data-math="m^2">$m^2$</span> ou en hectares) et utilise les outils de dessin sur le fond de carte **Google Satellite**.

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1276 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQ2go" decode-data-ved="1" id="bkmrk-html" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_995f53d871dc1e7f","c_5b7951c496f77574",null,"rc_7d6472010308fc67",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1276 ng-star-inserted"><span class="ng-tns-c2446650269-1276">HTML</span></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1276"></div></div>```

<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Alteris - SIG Expert & Surfaces</title>
    
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
    
    <style>
        body { margin: 0; padding: 0; font-family: 'Segoe UI', Arial, sans-serif; }
        #maCarte { width: 100vw; height: 100vh; background: #222; }
        
        .info-panel {
            position: absolute; top: 10px; right: 10px; z-index: 1000;
            background: rgba(255, 255, 255, 0.9); padding: 15px; border-radius: 8px;
            box-shadow: 0 0 15px rgba(0,0,0,0.3); max-width: 250px;
            pointer-events: none; /* Laisse passer les clics vers la carte si besoin */
        }
        .info-panel h3 { margin: 0 0 5px 0; color: #e67e22; font-size: 1.2em; }
        .info-panel p { margin: 0; font-size: 0.85em; color: #333; line-height: 1.4; }
        
        /* Style des Popups */
        .leaflet-popup-content-wrapper { border-radius: 4px; }
        .popup-title { color: #e67e22; font-weight: bold; font-size: 1.1em; display: block; margin-bottom: 5px; }
    </style>
</head>
<body>

    <div class="info-panel">
        <h3>Alteris Geomatic</h3>
        <p><b>Dessiner :</b> Utilisez les outils à gauche.</p>
        <p><b>Mesurer :</b> Cliquez sur une zone pour voir sa surface calculée par PostGIS.</p>
    </div>

    <div id="maCarte"></div>

    <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
    
    <script>
        // 1. FONDS DE CARTE
        const osm = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
            attribution: '© OpenStreetMap'
        });

        const googleSat = L.tileLayer('http://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', {
            maxZoom: 20,
            subdomains:['mt0','mt1','mt2','mt3'],
            attribution: '© Google Satellite'
        });

        // 2. INITIALISATION
        const map = L.map('maCarte', {
            center: [48.85, 2.35],
            zoom: 13,
            layers: [googleSat]
        });

        const baseMaps = { "Satellite": googleSat, "Plan": osm };
        L.control.layers(baseMaps).addTo(map);

        // 3. COUCHE DE DESSIN ET BARRE D'OUTILS
        const drawnItems = new L.FeatureGroup();
        map.addLayer(drawnItems);

        const drawControl = new L.Control.Draw({
            edit: { featureGroup: drawnItems },
            draw: {
                polygon: { allowIntersection: false, showArea: true, shapeOptions: { color: '#3498db' } },
                rectangle: { shapeOptions: { color: '#3498db' } },
                polyline: false, circle: false, marker: false, circlemarker: false
            }
        });
        map.addControl(drawControl);

        // 4. CHARGEMENT DES ZONES EXISTANTES DEPUIS L'API
        async function chargerZones() {
            try {
                const reponse = await fetch('/api/zones');
                const data = await reponse.json();
                
                L.geoJSON(data, {
                    style: { color: '#e67e22', weight: 3, fillOpacity: 0.3 },
                    onEachFeature: function (feature, layer) {
                        const s = feature.properties.surface;
                        // Formatage : Hectares si > 10000m², sinon m²
                        const affichageSurface = s > 10000 
                            ? (s / 10000).toFixed(2) + " ha" 
                            : Math.round(s) + " m²";

                        layer.bindPopup(`
                            <span class="popup-title">${feature.properties.nom}</span>
                            <hr style="margin:5px 0; border:0; border-top:1px solid #eee;">
                            <b>Surface :</b> ${affichageSurface}
                        `);
                    }
                }).addTo(map);
            } catch (err) {
                console.error("Erreur chargement zones:", err);
            }
        }

        // 5. ENREGISTREMENT D'UN NOUVEAU DESSIN
        map.on(L.Draw.Event.CREATED, async function (e) {
            const layer = e.layer;
            const geojson = layer.toGeoJSON();
            
            const nomZone = prompt("Nom de la nouvelle zone ?");
            if (!nomZone) return;

            try {
                const reponse = await fetch('/api/save-zone', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ nom: nomZone, geojson: geojson })
                });

                const resultat = await reponse.json();
                if (reponse.ok) {
                    alert(`Succès : ${resultat.message}`);
                    location.reload(); 
                }
            } catch (err) {
                alert("Erreur lors de la sauvegarde.");
            }
        });

        // 6. CLIC POUR VÉRIFICATION SPATIALE (ST_CONTAINS)
        map.on('click', async function(e) {
            // Ne pas déclencher si on est en train de dessiner
            if (drawControl._toolbars.draw._activeMode) return;

            const point = {
                type: "Point",
                coordinates: [e.latlng.lng, e.latlng.lat]
            };

            try {
                const reponse = await fetch('/api/check-zone', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(point)
                });
                const res = await reponse.json();

                L.popup()
                    .setLatLng(e.latlng)
                    .setContent(res.message)
                    .openOn(map);
            } catch (err) {
                console.error("Erreur clic spatial:", err);
            }
        });

        // Lancer le chargement au démarrage
        chargerZones();

    </script>
</body>
</html>
```

Et l'ajout du module suppression !

Voici donc la mise à jour pour la **fonctionnalité de suppression**, présentée selon ta nouvelle règle.

### 1. Les blocs qui évoluent (Le "Pourquoi")

#### Dans `server.js` :

On ajoute une route `DELETE`. Note l'utilisation de `:id` dans l'URL, qui permet de passer l'identifiant de la zone dynamiquement.

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1383 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQpQw" decode-data-ved="1" id="bkmrk-javascript-1" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_6d40951b88560703","c_5b7951c496f77574",null,"rc_6457309602cd457a",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1383 ng-star-inserted"><span class="ng-tns-c2446650269-1383">JavaScript</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1383 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1383 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1383"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1383"></div></div></div>```
app.delete('/api/zones/:id', async (req, res) => {
  const { id } = req.params;
  await pool.query('DELETE FROM zones_interet WHERE id = $1', [id]);
  res.json({ status: "success" });
});

```

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1383 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQpQw" decode-data-ved="1" id="bkmrk--5" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_6d40951b88560703","c_5b7951c496f77574",null,"rc_6457309602cd457a",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1383"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1383"></div></div></div>#### Dans `index.html` :

On modifie le popup pour inclure un bouton qui appelle une nouvelle fonction `supprimerZone(id)`.

<div _ngcontent-ng-c2446650269="" class="code-block ng-tns-c2446650269-1384 ng-animate-disabled ng-trigger ng-trigger-codeBlockRevealAnimation" data-hveid="0" data-ved="0CAAQhtANahgKEwjS_fmv4oCTAxUAAAAAHQAAAAAQpgw" decode-data-ved="1" id="bkmrk-javascript-2" jslog="223238;track:impression,attention;BardVeMetadataKey:[["r_6d40951b88560703","c_5b7951c496f77574",null,"rc_6457309602cd457a",null,null,"fr",null,1,null,null,1,0]]"><div _ngcontent-ng-c2446650269="" class="code-block-decoration header-formatted gds-title-s ng-tns-c2446650269-1384 ng-star-inserted"><span class="ng-tns-c2446650269-1384">JavaScript</span><div _ngcontent-ng-c2446650269="" class="buttons ng-tns-c2446650269-1384 ng-star-inserted"><button aria-label="Copier le code" class="mdc-icon-button mat-mdc-icon-button mat-mdc-button-base mat-mdc-tooltip-trigger copy-button ng-tns-c2446650269-1384 mat-unthemed ng-star-inserted"></button></div></div><div _ngcontent-ng-c2446650269="" class="formatted-code-block-internal-container ng-tns-c2446650269-1384"><div _ngcontent-ng-c2446650269="" class="animated-opacity ng-tns-c2446650269-1384"></div></div></div>```
// Dans la fonction chargerZones, le popup devient :
layer.bindPopup(`... <button onclick="supprimerZone(${feature.id})">Supprimer</button> ...`);

// Et on ajoute la fonction de communication avec l'API :
async function supprimerZone(id) {
    await fetch(`/api/zones/${id}`, { method: 'DELETE' });
    location.reload();
}
```

</body></html>