Skip to content

Latest commit

 

History

History
1728 lines (1286 loc) · 66.1 KB

File metadata and controls

1728 lines (1286 loc) · 66.1 KB

Guide de Référence sViewer

Guide technique complet pour développeurs et intégrateurs..


Sommaire

  1. Mode Simple : Paramètres KVP
  2. Mode WebComponent : API JavaScript
  3. Configuration Avancée
  4. Services OGC et Données
  5. Projections et Repères
  6. Requêtes Cartographiques
  7. Intégration geOrchestra
  8. Progressive Web App (PWA)
  9. Internationalization (i18n)
  10. Architecture et API Interne
  11. Dépannage

Mode Simple : Paramètres KVP

Syntaxe générale

https://my-sviewer.example.org/sviewer/?param1=valeur1&param2=valeur2

Les paramètres sont traités comme des chaînes de caractères. Les URL doivent être encodées (ex: espace = %20).

Paramètres de positionnement

x, y, z

Positionne la carte et définit le niveau de zoom.

  • x, y : Coordonnées en EPSG:3857 (Web Mercator) ou EPSG:4326 (longitude/latitude) — détection automatique
  • z : Niveau de zoom (0-18), entier
?x=-366959&y=2951352&z=5
?x=-3.36&y=48.11&z=5

Détection automatique EPSG:4326 / EPSG:3857 : si |x| ≤ 180 et |y| ≤ 180, les coordonnées sont interprétées comme longitude/latitude (EPSG:4326) et reprojetées automatiquement. Sinon, interprétées comme EPSG:3857. Les deux formats sont donc acceptés sans paramètre supplémentaire.

title

Affiche un titre personnalisé au-dessus de la carte.

?title=Carte%20d'exemple

Restrictions : Texte court (~30 chars) recommandé pour affichage mobile.

lb (layer background)

Sélectionne la fond de carte (background layer) par index.

?lb=0      # Première fond de carte
?lb=1      # Deuxième fond de carte

Configuration : Les données disponibles sont définies dans local/customConfig.jslayersBackground[]. L'index par défaut est 0.


Paramètres de données cartographiques

layers

Ajoute une ou plusieurs données WMS à la carte. Format : liste séparée par des virgules.

Syntaxe basique :

?layers=namespace:layername

Avec style personnalisé :

?layers=namespace:layername*stylename

Avec filtre CQL :

?layers=namespace:layername*stylename*CQL_FILTER

Exemples :

?layers=geor:ma_donnee
?layers=geor:commune*orange
?layers=geor:commune*orange*population>50000

Cas d'usage:

  • Les données doivent être publiées sur un serveur OGC (geOrchestra, GeoServer, etc.)
  • Si geOrchestraBaseUrl est configuré, sViewer construit automatiquement les URLs WMS
  • Les styles doivent exister sur le serveur pour être appliqués
  • Les filtres CQL utilisent la syntaxe OGC standard

md (metadata)

Charge automatiquement une ou plusieurs données WMS depuis des identifiants de fiches de métadonnées CSW (ISO 19139). Plusieurs identifiants séparés par des virgules chargent autant de données en parallèle.

?md=<identifiant-csw>
?md=<id1>,<id2>,<id3>
?md=<id>@<csw-endpoint>

Comportement :

  • Interroge le CSW (geOrchestraBaseUrl/geonetwork/srv/eng/csw par défaut, ou l'endpoint précisé via id@url) pour résoudre l'URL WMS et le nom de chaque donnée
  • Affiche titre, résumé et légende depuis chaque fiche (un panneau par métadonnée)
  • Titre automatique de la carte : uniquement si un seul md= — ambigu avec plusieurs, utiliser &title= explicitement
  • Ignoré si layers= est aussi présent (layers= est prioritaire)
  • Inclus dans le permalien si layers= est absent (identifiants rejoints par virgule)

→ Voir Services OGC — CSW pour le détail.


Paramètres de requête et recherche

q (query)

Active une requête automatique au démarrage sur les données visibles.

?layers=geor:commune&q=1
?geojson=https://…/data.geojson&q=1

Comportement WMS :

  • Exécute un GetFeatureInfo au centre de la vue initiale
  • Affiche les résultats dans le panneau Résultats
  • Nécessite au moins une donnée queryable (attribut queryable="1" en WMS)

Comportement GeoJSON :

  • Après chargement des entités, recherche l'entité la plus proche du centre de la vue (hit-test pixel, tolérance 32 px, puis fallback getClosestFeatureToCoordinate)
  • Affiche ses propriétés dans le panneau Résultats
  • Fonctionne avec tout adaptateur (?geojson=, Grist, CSV…)

s (search)

Active la barre de recherche au démarrage.

?s=1
?geojson=https://…/data.geojson&s=1

Services interrogés :

  • IGN Géoplateforme (ou customConfig.openLSGeocodeUrl)
  • WFS de chaque donnée queryable (si disponible et CORS OK)
  • Entités GeoJSON chargées — recherche en mémoire sur toutes les propriétés scalaires (string, nombre, date), résultats limités à config.maxWfsSearchFeatures

CORS : Géoplateforme et services WFS doivent supporter CORS. La recherche GeoJSON est en mémoire — aucun appel réseau.

qcl_filters

Applique des filtres CQL aux données déjà chargées via layers=, sans les réencoder dans la chaîne layers. Format : liste séparée par des points-virgules, un filtre par donnée dans l'ordre de layers=.

?layers=ns:a,ns:b&qcl_filters=population>50000;commune='Strasbourg'

Filtre vide possible : qcl_filters=;commune='Strasbourg' (pas de filtre sur la première donnée).

Non-persistant (absent du permalien).


Paramètres d'affichage et partage

theme

Thème d'affichage. Sans paramètre, suit prefers-color-scheme du système.

Valeur Effet
theme=light Thème clair forcé
theme=dark Thème sombre forcé

Persistant dans le permalien si ≠ light.

opacity

Opacité initiale de toutes les données (hors fonds de carte). Plage : 01. Défaut : 1 (valeur layerOpacity de customConfig).

?opacity=0.6

Persistant dans le permalien si ≠ 1.

position

Active le suivi GPS au chargement.

?position=1

Persistant dans le permalien si GPS actif.

address

Géocode silencieusement une adresse au chargement et recentre la carte.

?address=4+rue+du+D%C3%B4me%2C+Strasbourg
  • Score IGN ≥ 0.8 → recentrage silencieux + marqueur
  • Score < 0.8 → ouvre le panneau de recherche avec les résultats pour sélection manuelle
  • Compatible embed : SViewer.init('#map', { address: 'Strasbourg' })

Non-persistant (absent du permalien).

geojson

Charge un fichier GeoJSON distant comme données vectorielles interactives.

?geojson=https://raw.githubusercontent.com/user/repo/main/data.geojson
?geojson=https://data.gouv.fr/.../dataset.geojson
  • Supporte points, lignes et polygones (collections mixtes comprises)
  • Clic sur un objet → affiche ses propriétés dans le panneau d'info
  • CORS requis sur le serveur source
  • Composable avec ?layers= : superpose les données GeoJSON à des données WMS
  • Compatible embed : SViewer.init('#map', { geojson: 'https://...' })
  • Si une entité possède une propriété _label, celle-ci est affichée comme étiquette texte sur la carte
  • Si l'URL source n'est pas un GeoJSON natif, les adaptateurs déclarés dans customConfig.adapters sont essayés dans l'ordre pour normaliser la réponse (voir Adaptateurs JSON)
  • Simplification géométrique automatique : si le nombre total de sommets dépasse le seuil (25 000 sur mobile, 100 000 sur desktop), les géométries lignes et polygones sont simplifiées à la volée (tolérance 10 m mobile / 20 m desktop). Les points ne sont pas affectés. Transparent pour l'utilisateur.

Persistant dans le permalien et le code embed.

label

Désigne la propriété GeoJSON à afficher comme étiquette sur chaque entité. Utile quand la source ne contient pas de propriété _label.

?geojson=https://…&label=nom
?geojson=https://…&label=numero
  • Copie la valeur de la propriété nommée vers _label sur chaque entité après chargement
  • Sans effet si la propriété n'existe pas sur une entité donnée
  • Persistant dans le permalien

debug (debug mode)

Valeur Effet
debug=true Active les logs console (diagnostic WMS, CORS, AJAX)
debug=1 Charge sviewer.js et sviewer.css non-minifiés (indépendant de debug=true)

Non-persistant (absent du permalien).

lang

Force la langue de l'interface (priorité sur customConfig.lang et la détection navigateur).

?lang=fr
?lang=en

Code ISO 639-1 à 2 lettres. Langues disponibles : voir hardConfig.i18n dans js/i18n.js. Non-persistant.

c (configuration)

Charge une configuration alternative au lieu de customConfig.js.

?c=ma_config

Mécanisme :

  • Charge dynamiquement local/customConfig_ma_config.js
  • Écrase les paramètres par défaut avec les valeurs de cette configuration
  • Restrictions de nom : [a-zA-Z0-9_-]+ uniquement

Paramètres persistants

Permalien (lien partageable) :

  • x, y, z
  • layers
  • md (si layers= absent)
  • q, s
  • c, lb, theme
  • opacity (si ≠ 1)
  • position (si GPS actif)
  • geojson (si présent)

Code d'intégration WebComponent :

  • x, y, z
  • layers
  • md (si layers= absent)
  • title
  • c, lb, theme
  • opacity (si ≠ 1)
  • position (si GPS actif)
  • geojson (si présent)

Le paramètre debug n'est pas persistant.


Mode WebComponent : API JavaScript

Initialisation

<div id="ma-carte"></div>

<script src="https://my-sviewer.example.org/sviewer/static/js/embed.min.js"></script>
<script>
  SViewer.init('#ma-carte', options);
</script>

Options d'initialisation

Les options passées à SViewer.init() utilisent exactement les mêmes noms que les paramètres KVP du mode simple. customConfig.js est chargé en premier, puis les options embed s'appliquent par-dessus.

Option Type Équivalent KVP Description
x number ?x= Longitude initiale (EPSG:3857 ou 4326 — détection auto)
y number ?y= Latitude initiale (EPSG:3857 ou 4326 — détection auto)
z number ?z= Niveau de zoom (0-18)
title string ?title= Titre de la carte
lb number ?lb= Index du fond de carte
layers string ?layers= Données à afficher (séparées par virgules)
c string ?c= Nom du profil de configuration alternatif
theme string ?theme= Thème d'affichage : light (défaut) ou dark. Sans paramètre : suit prefers-color-scheme
opacity number ?opacity= Opacité des données (0–1, défaut : 1)
position 1 ?position=1 Active le suivi GPS au chargement
geojson string ?geojson= URL d'un fichier GeoJSON à charger comme données vectorielles (CORS requis)

Le bouton HTML du panneau de partage génère automatiquement un fragment SViewer.init() pour la vue courante.

Exemples complets

Exemple minimal :

SViewer.init('#ma-carte', {
    x: -366959,
    y: 2951352,
    z: 5
});

Exemple avec données :

SViewer.init('#ma-carte', {
    x: 0,
    y: 2000000,
    z: 6,
    title: 'Ressources nationales',
    layers: 'geor:commune,geor:departement'
});

Exemple complet :

SViewer.init('#ma-carte', {
    x: -390192,
    y: 6122108,
    z: 10,
    lb: 1,
    layers: 'dreal_b:ae_casparcas',
    title: 'Evaluation Environnementale'
});

Configuration Avancée

Fichier customConfig.js

La configuration centralisée d'une instance sViewer se fait dans local/customConfig.js. Vous devez être familier avec OpenLayers pour la modifier.

Structure :

customConfig = {
    title: 'sViewer',
    lang: 'fr',
    geOrchestraBaseUrl: 'https://my-georchestra.example.org',
    initialExtent: [-600000, 6090000, -100000, 6100000],
    maxExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
    restrictedExtent: [-20037508.34, -20037508.34, 20037508.34, 20037508.34],
    center: [-350000, 6150000],   // vue initiale : centre (EPSG:3857), priorité sur initialExtent
    zoom: 10,                     // vue initiale : niveau de zoom, priorité sur initialExtent
    maxFeatures: 10,
    maxGeocodeResults: 5,
    maxWfsSearchFeatures: 8,
    nodata: '<!--nodatadetect-->\n<!--nodatadetect-->',
    openLSGeocodeUrl: "https://data.geopf.fr/geocodage/search",
    allowedDomains: [],              // optionnel — [] = tous les domaines autorisés
    geojsonStyle: {                  // style des données GeoJSON (?geojson=)
        color: '#ff6600',
        fillOpacity: 0.35,
        strokeWidth: 4,
        selectionColor: '#ee7733'
    },
    layerOpacity: 1,                  // opacité initiale des données WMS (0–1)
    searchPlaceholder: 'adresse, lieu-dit, commune...',
    gpsTrackingInterval: 5,           // secondes entre deux mises à jour GPS
    gpsTrackingTimeout: 300,          // arrêt auto GPS après N secondes (0 = jamais)
    extensions: ['grist', 'my-extension'],  // → voir ext/EXT_API.md — adapters register via SViewer.registerAdapter()
    layersBackground: [ /* ... */ ],
    backgroundPresets: [ /* ... */ ]  // → voir section Presets fonds de carte
};

layerOpacity

Opacité initiale de toutes les données WMS (hors fonds de carte). Plage : 01. Défaut : 1. Équivalent persistant du paramètre URL ?opacity=.

layerOpacity: 0.8

searchPlaceholder

Texte affiché dans la barre de recherche quand elle est vide.

searchPlaceholder: 'adresse, lieu-dit, commune...'

geocodeAdapter

Fonction de normalisation des réponses du service de géocodage. Obligatoire si openLSGeocodeUrl pointe vers un service non compatible IGN Géoplateforme (ex: Nominatim). Reçoit la réponse JSON parsée, retourne un tableau d'objets { label, coords, score, zoom }.

// IGN Géoplateforme (défaut — inutile de le déclarer)
geocodeAdapter: function(response) {
    return (response.features || []).map(function(f) {
        var zoomByType = { municipality: 13, street: 17, housenumber: 18 };
        return {
            label:  f.properties.label,
            coords: f.geometry.coordinates,  // [lon, lat]
            score:  f.properties.score || 0,
            zoom:   zoomByType[f.properties.type] || 16
        };
    });
},

// Nominatim (OpenStreetMap)
openLSGeocodeUrl: 'https://nominatim.openstreetmap.org/search',
geocodeParams: { format: 'json' },
geocodeAdapter: function(response) {
    return (response || []).map(function(r) {
        var zoomByType = { city: 12, town: 13, village: 14, road: 16, house: 18 };
        return {
            label:  r.display_name,
            coords: [parseFloat(r.lon), parseFloat(r.lat)],
            score:  0.5,
            zoom:   zoomByType[r.type] || 14
        };
    });
}

gpsTrackingInterval / gpsTrackingTimeout

Contrôle le suivi GPS (?position=1).

Clé Défaut Description
gpsTrackingInterval 5 Secondes entre deux mises à jour de position
gpsTrackingTimeout 0 Arrêt automatique après N secondes (0 = jamais)
gpsTrackingInterval: 5,
gpsTrackingTimeout: 300   // arrêt après 5 minutes

Étendues (Extents)

Trois étendues contrôlent le comportement de la carte :

  • initialExtent : Zone affichée au démarrage
  • maxExtent : Limite du panoramique (pan limits)
  • restrictedExtent : Limite du zoom (zoom limits)

Format : [minX, minY, maxX, maxY] en EPSG:3857.

Exemple :

initialExtent: [-600000, 6090000, -100000, 6100000],
maxExtent: [-20037508, -20037508, 20037508, 20037508],
restrictedExtent: [-20037508, -20037508, 20037508, 20037508]

Fonds de carte

Configuration des fonds de carte disponibles dans le sélecteur.

Utiliser ol.source.XYZ pour les services TMS/slippy map. Pour WMTS (meilleur rendu), utiliser un IIFE pour éviter les variables globales :

layersBackground: [
    // WMTS — meilleur rendu que TMS pour la Géoplateforme IGN
    new ol.layer.Tile({
        source: new ol.source.WMTS((function() {
            var proj = ol.proj.get('EPSG:3857');
            var ext = proj.getExtent();
            var res = [156543.03392811998,78271.51696419998,39135.758481959984,19567.879241008988,9783.939620504494,4891.969810252247,2445.9849051261233,1222.9924525765016,611.4962262882508,305.74811314412537,152.87405657206268,76.43702828603134,38.21851414301567,19.109257071507836,9.554628535753918,4.777314267876959,2.3886571339384795,1.1943285669692398,0.5971642834846199,0.29858214174231994];
            return {
                attributions: ['© IGNF BD ORTHO'],
                url: 'https://data.geopf.fr/wmts',
                layer: 'ORTHOIMAGERY.ORTHOPHOTOS',
                matrixSet: 'PM',
                format: 'image/jpeg',
                projection: proj,
                tileGrid: new ol.tilegrid.WMTS({
                    origin: ol.extent.getTopLeft(ext),
                    resolutions: res,
                    matrixIds: ['0','1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19']
                }),
                style: 'normal',
                crossOrigin: 'anonymous'
            };
        })()),
        title: 'Photos aériennes IGN'
    }),
    // TMS — utiliser ol.source.XYZ pour les services slippy map
    new ol.layer.Tile({
        source: new ol.source.XYZ({
            attributions: ['Contributeurs OpenStreetmap'],
            url: 'https://my-tileserver.example.org/osm/tms/osm:grey/EPSG3857/{z}/{x}/{-y}.png',
            maxResolution: 78271.51696402048,
            crossOrigin: 'anonymous'
        }),
        title: 'Carte OpenStreetmap'
    })
]

Notes :

  • Chaque donnée doit avoir un attribut title
  • Toutes les donnée doivent être en EPSG:3857
  • crossOrigin: 'anonymous' requis sur toutes les sources pour que la fonction snapshot fonctionne
  • WMTS Géoplateforme : utiliser un IIFE ((function(){ ... })()) pour éviter les variables globales
  • Services TMS geopf (data.geopf.fr/tms) : Y-axis = XYZ standard ({y}), malgré le profil TMS déclaré
  • Services TMS MapProxy : Y-axis = TMS inversé ({-y}), maxResolution requis car la grille démarre à zoom 0 = 78271 m/px (décalage d'un niveau vs grille OL standard)

Presets fonds de carte (backgroundPresets)

backgroundPresets remplace layersBackground seul quand on veut combiner fond de carte et données superposées en un seul clic. Chaque preset pilote atomiquement layersBackground[lb] et layersOverlay[lo].

backgroundPresets: [
    { lb: 0, lo: -1, title: 'Photo aérienne' },
    { lb: 0, lo: 0,  title: 'Photo aérienne + noms de lieux' },
    { lb: 1, lo: -1, title: 'OpenStreetMap' }
]
Clé Type Description
lb number Index dans layersBackground[]
lo number Index dans layersOverlay[]-1 = aucune donnée superposée
title string Libellé affiché dans le sélecteur de fond

Comportement : si backgroundPresets est défini et non vide, il prend la main sur layersBackground seul. Le paramètre ?lb= pointe alors sur l'index du preset, pas sur l'index du fond.

layersBackground seul (mode legacy) reste supporté mais déconseillé pour les nouvelles configs.

Service de géocodage

Configuration du service d'adresses (recherche de lieux) :

openLSGeocodeUrl: "https://data.geopf.fr/geocodage/search"

Le service doit :

  • Supporter l'API OpenLS (standard OGC)
  • Accepter les requêtes GET/POST JSON
  • Supporter CORS

Style des données GeoJSON

La clé geojsonStyle contrôle l'apparence des données vectorielles chargées via ?geojson=.

geojsonStyle: {
    color: '#ff6600',           // couleur contour + remplissage des points (CSS color)
    fillOpacity: 0.35,          // opacité du remplissage polygone (0–1)
    strokeWidth: 4,             // épaisseur du trait lignes/polygones en pixels
    selectionColor: '#ee7733'   // couleur de l'entité sélectionnée au clic
}
  • color s'applique au trait et au remplissage des points — les polygones utilisent color + fillOpacity pour le fond
  • Lignes et polygones ont un liseré blanc automatique pour la lisibilité sur tout fond de carte
  • Cliquer sur une entité l'affiche en selectionColor ; cliquer ailleurs désélectionne
  • Valeurs par défaut : orange #ff6600, opacité 0.35, trait 4 px, sélection #ee7733
  • Si une entité possède une propriété _label, elle est affichée comme étiquette texte au-dessus de l'entité

Adaptateurs JSON (extensions)

Les adaptateurs normalisent les réponses d'APIs non-GeoJSON (Grist, ArcGIS REST…) en FeatureCollection pour affichage via ?geojson=.

Activer un adaptateur — ajouter son nom dans customConfig.js :

extensions: ['grist']

Chaque nom correspond à ext/{nom}/extension.js. Seuls les adaptateurs fournis avec sViewer sont supportés (pas de chemin externe).

Adaptateurs disponibles

Nom API supportée
grist Grist /api/docs/…/tables/…/records
csv Fichiers CSV (, ou ;), colonnes lat/lon auto-détectées ou colonne géométrie GeoJSON/WKT. Extension .csv ou hint ?_format=csv.

Plusieurs adaptateurs — listés dans l'ordre de priorité. Chaque adaptateur déclare un match(url) qui limite son activation à certaines URLs source :

extensions: ['grist', 'arcgis']

Écrire son propre adaptateur — créer ext/monadaptateur/extension.js :

SViewer.registerAdapter('monadaptateur', {
    match: function(url) { return url.indexOf('monapi.example.com') !== -1; },
    convert: function(response, sourceUrl) {
        // retourner un GeoJSON FeatureCollection (EPSG:4326)
        return null;
    }
});

match est optionnel — sans match, l'adaptateur est appelé pour toutes les URLs (fallback). Utiliser SViewer.registerAdapter() et non window.SViewer.adapters[key] = directement.

Paramètres hint Grist — le widget Grist encode des hints géométriques dans l'URL source pour bypasser l'auto-détection :

Paramètre Rôle
_geommode Mode : geojson, latlon, latlon_str, lonlat_str, wkt
_geomcol Colonne géométrie (modes geojson, latlon_str, lonlat_str, wkt)
_collat Colonne latitude (mode latlon)
_collon Colonne longitude (mode latlon)
_labelcol Colonne étiquette → propriété _label sur chaque feature

En l'absence de hints, l'adaptateur Grist auto-détecte la géométrie (candidats : geometry, geom, geo, shape, wkb_geometry ; puis paire lat/lon).

Sécurité — liste blanche de domaines OGC

Par défaut, sViewer accepte toute URL HTTPS comme endpoint OGC (WMS, WFS, CSW). Pour restreindre les domaines autorisés :

allowedDomains: ['my-georchestra.example.org', 'data.geopf.fr']
  • Absent ou [] : tous les domaines autorisés (comportement par défaut, compatible avec les configs existantes)
  • La correspondance est exacte ou par sous-domaine : 'example.org' autorise aussi tiles.example.org
  • S'applique aux quatre points d'entrée : URL WMS personnalisée (?layers=ns:layer@url), URL WMS extraite d'un enregistrement CSW, URL WFS découverte via DescribeLayer, URL GeoJSON (?geojson=)
  • En cas de blocage, un avertissement est émis en console (sViewer: blocked … not in allowedDomains)

Chargement dynamique d'extension

Par défaut, les extensions sont chargées au démarrage depuis customConfig.extensions et le paramètre URL ?ext=. Depuis sViewer 0.14.1, l'API SViewer.loadExtension(name) permet de charger une extension après le boot.

Usages typiques :

  • sélecteur de profils (page d'accueil → clic carte → chargement des extensions du profil)
  • activation à la demande (extension lourde chargée seulement à la première utilisation)
  • composition depuis la page hôte (parent postMessage → sViewer charge l'extension demandée)
  • auto-installation à la restauration (carte enregistrée référence ?ext=printme charge print si absent)
SViewer.loadExtension('me')
    .then(function (name) { /* extension active */ })
    .catch(function (err) {
        // err.code : invalid-name | manifest-fetch | manifest-parse |
        //            version-mismatch | script-load
    });

SViewer.hasExtension('me');         // boolean
SViewer.loadedExtensions();         // ['me', 'print', …] — snapshot

Comportement :

  • Idempotent : si déjà chargée, résolution immédiate avec le nom.
  • Déduplication des appels concurrents : un seul fetch, Promise partagée.
  • Vérifie manifest.json.sviewer.minVersion contre SViewer.version avant de charger le script.
  • Les extensions UI utilisant SViewer.onMapReady(fn) fonctionnent telles quelles : le callback se déclenche synchroniquement pour les abonnés tardifs si la carte est déjà prête.
  • Les adaptateurs (SViewer.registerAdapter) s'enregistrent immédiatement et sont disponibles pour les prochains ?geojson= ; les données déjà chargées ne sont pas re-parsées rétroactivement (déclencher une actualisation manuellement).

Limites :

  • Une extension qui écoute des événements globaux dès le top scope (par exemple superset qui écoute les messages postMessage envoyés par la page parente) peut manquer les événements émis avant la fin du chargement. Documenter ces extensions comme « boot-only ».
  • Aucune API de déchargement (unloadExtension) : un bouton de barre d'outils injecté reste en place. Les extensions doivent gérer leur propre cycle d'activation/désactivation.

Sécurité — sanitisation HTML des panneaux d'extension

Le HTML passé à SViewer.panel.open() et SViewer.panel.update() est sanitisé avant injection dans le DOM. La sanitisation supprime :

  • les éléments <script>
  • les attributs de gestionnaire d'événements (on* : onerror, onclick, onload, etc.)

Pourquoi : les extensions sont du code déployeur, mais elles peuvent refléter des données d'API tierces non maîtrisées. La sanitisation coupe le vecteur XSS sans changer l'API.

Conséquence pour les extensions : ne pas utiliser de gestionnaires onclick= inline dans le HTML passé au panneau — utiliser addEventListener après rendu :

SViewer.panel.open('myext', 'Mon outil', '<button id="my-btn">Lancer</button>');
document.getElementById('my-btn').addEventListener('click', myHandler);

La sanitisation n'affecte pas le HTML statique injecté directement par l'extension dans le DOM via document.createElement().

Sécurité — déploiement serveur web

Attention : ne jamais servir le dossier sviewer/ sans configuration nginx (ou Apache) dédiée.

Le dossier contient des fichiers non destinés au public : outils de build (scripts/), sources OpenLayers (build/ol-custom-entry.js), manifestes npm (package.json, package-lock.json), configuration de postcss, etc. Un serveur configuré en mode "liste les fichiers du dossier" ou sans restriction de chemin expose ces fichiers à tout visiteur.

Règle : utiliser le snippet nginx fourni (deploy/nginx/nginx-server.conf), qui applique une liste blanche stricte — seuls les chemins explicitement autorisés sont servis, tout le reste retourne 404.

Chemins bloqués par défaut : node_modules/, scripts/, build/ol-custom-entry.js, package.json, postcss.config.js, tous les fichiers .md, .sh, .py, .bak, .log et les dotfiles.

Bloc ext/superset/dist/ : le bundle plugin Superset est servi avec Access-Control-Allow-Origin: * — requis pour que Superset puisse le charger depuis une origine différente. Ce bloc doit précéder le bloc ext/ générique dans la config nginx (ordre de priorité des location ~).

Cache static/ : depuis 0.13.0, les assets static/ (hors static/lib/) sont servis avec Cache-Control: no-cache, must-revalidate au lieu de max-age=3600 — le navigateur revalide via ETag à chaque visite, évitant les assets périmés après une mise à jour.

Si vous utilisez Apache, reproduisez la même logique avec Require all denied sur les répertoires sensibles et Require all granted uniquement sur les chemins publics.

Sécurité — HTTPS obligatoire en production

sViewer doit être déployé exclusivement en HTTPS.

Raisons :

  • Intégrité des extensionsSViewer.loadExtension(name) et le boot loader chargent ext/<name>/extension.js depuis le même origine que sViewer. Si sViewer est servi en HTTP, un attaquant en position d'homme du milieu (wifi public, FAI hostile, proxy d'entreprise compromis) peut substituer le code de n'importe quelle extension → exécution de code arbitraire dans l'origine sViewer = vol de session, persistance via localStorage, XSS persistant.
  • Cartes enregistrées (ext/me) — les URLs sauvegardées et exportées en JSON sont validées : seul HTTPS est accepté à l'ouverture (exception : HTTP même origine, pour le dev local). Un déploiement en HTTP ferait apparaître des messages « URL refusée » sur toute carte enregistrée pointant vers un autre serveur.
  • OGC — règle déjà imposée par le code (CLAUDE.md) : les services WMS/WFS/CSW doivent être HTTPS, comportement renforcé par la politique mixed-content du navigateur.
  • RGPD — pour le secteur public français (audience cible), HTTPS est obligatoire pour tout traitement de données personnelles.

Configuration minimale recommandée :

# Redirection HTTP → HTTPS au niveau du serveur global
server {
    listen 80;
    server_name votre-domaine.fr;
    return 301 https://$host$request_uri;
}

# Bloc HTTPS principal — applique le snippet sViewer
server {
    listen 443 ssl http2;
    server_name votre-domaine.fr;
    # … certificats Let's Encrypt ou équivalent …
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    # … inclure deploy/nginx/nginx-server.conf …
}

Strict-Transport-Security (HSTS) force le navigateur à utiliser HTTPS même si l'utilisateur tape http:// — protection supplémentaire contre les attaques de downgrade.

Développement local : http://localhost/sviewer/ est toléré par ext/me (même origine = exception). Aucune dérogation pour des origines tierces en HTTP.

Sécurité — durcissement de la CSP connect-src

La CSP par défaut (deploy/nginx/nginx-server.conf) inclut connect-src 'self' https: — sViewer et ses extensions peuvent appeler n'importe quelle URL HTTPS. Acceptable pour la majorité des déploiements, mais permet à une extension malveillante (ou compromise) d'exfiltrer des données vers un serveur tiers.

Pour les déploiements sensibles, restreindre connect-src à la liste explicite des origines attendues :

# Exemple : sViewer + IGN Géoplateforme + geOrchestra local + Nominatim
add_header Content-Security-Policy "default-src 'none'; \
    script-src 'self' 'sha256-…'; \
    style-src 'self' 'unsafe-inline'; \
    img-src 'self' data: blob: https:; \
    font-src 'self'; \
    connect-src 'self' https://data.geopf.fr https://wxs.ign.fr https://geo.votre-collectivite.fr https://nominatim.openstreetmap.org; \
    manifest-src 'self'; base-uri 'self'; form-action 'none'; frame-ancestors *; \
    upgrade-insecure-requests;" always;

Effet : si une extension tente fetch('https://malveillant.example.com/exfil'), le navigateur bloque la requête et émet une violation CSP dans la console. Combiné avec report-uri ou report-to, permet d'auditer les tentatives en production.

Compromis : maintenance de la liste à chaque ajout de source de données. Solution : générer la liste depuis customConfig.js (backends, allowedDomains, etc.) via un script de déploiement.

Mise à jour du hash CSP après modification de index.html

Le snippet nginx inclut un hash sha256-… dans la Content-Security-Policy pour autoriser le script inline de index.html. Si ce script est modifié, le hash doit être recalculé.

Chrome hash le contenu exact entre <script> (inclus, sans le tag) et </script> (exclu), en bytes bruts UTF-8. Formule :

python3 -c "
import hashlib, base64
with open('index.html', 'rb') as f:
    raw = f.read()
body = raw.index(b'<body>')
s = raw.index(b'<script>', body) + 8   # +8 = len('<script>'), le \\n initial est inclus
e = raw.index(b'</script>', s)
print('sha256-' + base64.b64encode(hashlib.sha256(raw[s:e]).digest()).decode())
"

Remplacer ensuite la valeur sha256-… dans deploy/nginx/nginx-server.conf et deploy/nginx/nginx-server-proxy.conf.


Services OGC et Données

Web Map Service (WMS)

sViewer supporte WMS 1.3.0 uniquement.

GetCapabilities

sViewer lit automatiquement les capacités WMS pour :

  • Obtenir les données disponibles
  • Lire les métadonnées (titre, résumé)
  • Déterminer si une donnée est queryable

GetFeatureInfo

Requête pour interroger une données à une position donnée.

URL générée :

https://serveur/geoserver/namespace/layername/wms
  ?REQUEST=GetFeatureInfo
  &SERVICE=WMS
  &VERSION=1.3.0
  &LAYERS=namespace:layername
  &QUERY_LAYERS=namespace:layername
  &STYLES=style_name
  &INFO_FORMAT=text/html
  &FEATURE_COUNT=10
  &CRS=EPSG:3857
  &BBOX=...
  &WIDTH=400&HEIGHT=300
  &I=200&J=150

Configuration :

  • maxFeatures : nombre de résultats (défaut 10)
  • FORMAT : toujours image/png avec TRANSPARENT=true

Propriétés requises pour les données

Chaque donnée WMS doit :

  • Supporter EPSG:3857 (Web Mercator)
  • Avoir un attribut queryable="1" si on souhaite l'interroger
  • Supporter HTTPS (pas d'URLs non chiffrées)
  • Supporter CORS (pas de proxy nécessaire)

Catalogue Service for the Web (CSW) — paramètre md=

Quand md=<identifiant> est passé dans l'URL (sans layers=), sViewer interroge le CSW pour charger automatiquement une ou plusieurs données WMS depuis des fiches de métadonnées ISO 19139. Plusieurs identifiants séparés par des virgules déclenchent autant de requêtes CSW en parallèle.

Flux d'exécution (par identifiant)

URL ?md=<id1>,<id2>
     │
     ├─ fetchCSWRecord(<id1>) ──────────────────────┐
     └─ fetchCSWRecord(<id2>) (parallèle)           │
                                                    ▼
  GET ${geOrchestraBaseUrl}/geonetwork/srv/eng/csw
      ?SERVICE=CSW&VERSION=2.0.2&REQUEST=GetRecordById
      &Id=<id>&ElementSetName=full
      &OutputSchema=http://www.isotc211.org/2005/gmd
     │
     ▼ ISO 19139 XML
parseCSWForWMS()
  XPath: //gmd:distributionInfo//gmd:CI_OnlineResource
  → trouve protocole OGC:WMS
  → extrait URL WMS (sans query string) + nom de donnée
     │
     ▼
LayerQueryable({ skipMetadataPanel: true })
  LAYERS = namespace:layername  (nom complet, pas virtuel)
  url    = wmsUrl depuis CSW
  → map.addLayer()
     │
     ▼
Panneau Documentation (un par métadonnée)
  titre + résumé (XPath gmd:identificationInfo)
  image légende (GetLegendGraphic)
  lien fiche catalogue (CI_OnlineResource WWW:LINK)
  tableau : date, producteur, contact, licence

Titre automatique

Avec un seul md= : le titre de la carte est initialisé depuis la fiche. Avec plusieurs md= : aucun titre automatique — utiliser &title= explicitement.

Priorité

layers= est toujours prioritaire sur md=. Si les deux sont présents, md= est ignoré (log console).

Persistance

md= est inclus dans le permalink et le code d'intégration généré, à condition que layers= soit absent. Plusieurs identifiants sont rejoints par virgule.

Prérequis

  • Par défaut, le CSW est interrogé à ${geOrchestraBaseUrl}/geonetwork/srv/eng/csw ; la syntaxe id@https://csw-endpoint permet d'utiliser n'importe quel catalogue CSW
  • Chaque fiche doit contenir un CI_OnlineResource avec protocol = OGC:WMS
  • Le serveur WMS doit supporter CORS

Connecteur Grist

Le connecteur Grist est un widget personnalisé Grist qui affiche les données d'une table sur une carte sViewer. La synchronisation tableau → carte est supportée (sélection d'une ligne → zoom carte). La direction inverse (clic carte → sélection dans le tableau) n'est pas possible : l'API Grist setSelectedRows provoque une erreur LinkConfig invalid cycle qui casse la synchronisation tableau → carte. Clic carte = surbrillance visuelle uniquement.

Documentation complète : ext/grist/README.md

Architecture

Grist document
  └─ Widget personnalisé → ext/grist/index.html
       ├─ embed.js        (charge OL, Bootstrap, crée le DOM sViewer dans #sv-map)
       ├─ widget.js       (logique widget : Grist API, colonnes, styles, panneau config)
       └─ grist-plugin-api.js (CDN docs.getgrist.com — obligatoire)

Persistance de la configuration

La configuration est stockée par instance via grist.widgetApi.setOptions() / getOptions(). Chaque vue (widget) d'un document Grist dispose de sa propre configuration indépendante.

Clés persistées :

Clé Type Rôle
geom_mode string Mode géométrie (auto, geojson, latlon, latlon_str, lonlat_str, wkt)
_colGeom string Colonne géométrie active
_colLat / _colLon string Colonnes latitude/longitude (mode latlon)
_colLabel string Colonne étiquette
fill_color / fill_opacity string/number Style remplissage entités
stroke_color / stroke_opacity / stroke_width string/number Style contour entités
sel_fill_color / sel_fill_opacity string/number Style remplissage sélection
sel_stroke_color / sel_stroke_opacity / sel_stroke_width string/number Style contour sélection
title string Titre affiché dans la barre sViewer
layers string Couche(s) WMS additionnelles
md string Identifiant CSW
lb number Index fond de carte initial
x / y / z number Centre et zoom initiaux (EPSG:3857)
sviewer_base string URL de base sViewer (pour le lien de partage)
grist_api_base string Hôte Grist (instances auto-hébergées)
georchestra_base string Hôte geOrchestra
fit_on_load boolean Recadrage automatique sur les données

Lien de partage et hints géométriques

Lorsque l'utilisateur clique sur Partager dans sViewer, l'URL générée inclut ?geojson= pointant vers l'API Grist avec des paramètres hint encodés par buildGristGeojsonUrl() dans widget.js :

?geojson=https://docs.getgrist.com/api/docs/{docId}/tables/{tableId}/records
         ?_geommode=latlon_str&_geomcol=geo_point_2d&_labelcol=nom

Ces hints sont lus par jsonLayerAdapter (voir section jsonLayerAdapter ci-dessus) pour reproduire exactement le même rendu dans sViewer standalone, sans auto-détection.

Styles par défaut

Les valeurs par défaut des contrôles couleur/opacité du panneau de configuration sont lues depuis customConfig.geojsonStyle (ou hardConfig.geojsonStyle). En l'absence de configuration, les valeurs codées en dur sont : #ff6600, opacité 0.35, épaisseur 2.5 px (identiques à geojsonStyle).

Migration depuis v0.4.0

La table Grist _sviewer_customConfig n'est plus utilisée. Les clés feature_color et feature_highlight_color sont migrées automatiquement vers fill_color et sel_fill_color lors du premier chargement.


Projections et Repères

Projections supportées

EPSG:3857 (Web Mercator) est l'unique projection supportée.

Toutes les données WMS doivent être dans cette projection ou seront reprojetées automatiquement par le serveur OGC.


Requêtes Cartographiques

GetFeatureInfo (Interroger la carte)

Cliquer sur la carte déclenche une requête WMS GetFeatureInfo si une donnée queryable est visibile.

Résultats :

  • Affichage en panneau latéral
  • Tableau HTML ou texte selon le serveur
  • Maximum maxFeatures résultats

Erreurs courantes :

  • « Aucun résultat » : pas de donnée queryable à cette position
  • « Interrogation a échoué » : erreur CORS, URL non accessible, ou serveur refuse la requête

Recherche de lieux

Saisie libre d'adresse/lieu → requête vers le service géoplateforme.

Résultats :

  • Liste avec score de pertinence
  • Clic sur un résultat = panoramique + zoom vers le lieu
  • Marqueur temporaire

Limitations :

  • Couverture variable selon région

Recherche WFS (paramètre s=1)

Activé par ?s=1. Interroge les données WFS associées aux données WMS queryables, en parallèle de la géoplateforme.

Découverte automatique WFS

Au démarrage (doConfiguration), pour chaque donnée queryable :

WMS DescribeLayer  →  découvre l'URL WFS + typeName
WFS DescribeFeatureType  →  découvre les champs et leurs types

Résultat stocké dans layer.wfs :

  • url : endpoint WFS
  • typeName : nom du type de feature
  • fields : tous les champs scalaires (string, int, date, etc.) — pour l'affichage
  • searchFields : champs xsd:string uniquement — pour le filtre PropertyIsLike
  • geomField : nom du champ géométrie (exclu de l'affichage)

Si DescribeLayer ou DescribeFeatureType échoue, layer.wfs.url reste null et la donnée est silencieusement ignorée.

Flux de recherche

keyup (debounce 350ms)
  → abortSearchXhrs()          annule les XHR WFS en cours
  → openLsRequest()            géoplateforme IGN (parallèle)
  → searchAllWFSLayers()       pour chaque donnée avec wfs.url valide :
      WFS GetFeature
        FILTER: OR(PropertyIsLike) sur searchFields
        BBOX: étendue courante de la carte
        maxFeatures: config.maxWfsSearchFeatures (défaut 8)
        propertyName: tous les fields
      → featuresToList()        rendu Mustache + ajout dans #searchResults

Clic sur un résultat WFS

onSearchItemClick avec data.queryGFI = true :

  1. Recentre la carte sur les coordonnées du feature
  2. Appelle queryMap(coordinates) → déclenche WMS GetFeatureInfo comme un clic sur la carte

Légendes et métadonnées

Pour chaque donnée publiée depuis geOrchestra :

  • Légende graphique (si disponible)
  • Titre et résumé
  • Lien vers métadonnées

Progressive Web App (PWA)

Installation

sViewer est configuré comme Progressive Web App. Sur navigateurs compatibles (Chrome Android, Edge, Firefox Android, etc.), un bouton "Installer" ou "Ajouter à l'écran d'accueil" apparaît.

Fichiers PWA :

  • manifest.json : Métadonnées (nom, icônes, thème, etc.)
  • sw.js : Service Worker pour offline + caching
  • static/img/icon-192.png + static/img/icon-512.png : Icônes application

Service Worker

Service Worker (sw.js) enregistré uniquement en mode simple (index.html).

Comportement :

  • Cache ressources sViewer au premier chargement
  • Support offline limité (ressources en cache)
  • Mise en cache automatique des resources
  • Scope limité à /sviewer/

Mode WebComponent : embed.js n'enregistre pas le SW pour ne pas affecter la page hôte.

Manifest

Configuration dans manifest.json :

  • name : Nom complet (installation)
  • short_name : Nom court ≤12 chars (écran d'accueil)
  • start_url : URL de démarrage
  • scope : Portée du SW
  • display : Mode standalone (app native)
  • icons : PNG 192x192 et 512x512
  • theme_color + background_color : Couleurs barre d'adresse/splash

Application installée et persistance par URL

La persistance de sViewer est l'URL. Mais Android lie l'installation (WebAPK) au start_url du manifeste (/sviewer/, nu) : une carte configurée par permalien s'installe nue, et l'app installée n'a pas de barre d'adresse pour en charger une autre. (Une réécriture du manifeste à l'exécution a été essayée puis abandonnée : le navigateur évalue le manifeste avant le script — non fiable.)

Réponse — l'extension me comme hub installé. Le cœur expose deux primitives, extension-agnostiques :

  • SViewer.isInstalled() — vrai uniquement en application installée autonome (PWA/WebAPK) au niveau supérieur ; faux en onglet, embed, iframe Grist/Superset. Fail-closed (toute incertitude → faux). Détection : matchMedia('(display-mode: standalone|minimal-ui| fullscreen)') + navigator.standalone (iOS) + window.self === window.top.
  • SViewer.getPermalink() — le permalien canonique de la carte (identique au panneau Partager).

À partir de là, embed.js charge automatiquement l'extension me quand isInstalled() est vrai. me devient le hub d'accueil et fournit, en mode installé seulement, de quoi charger une carte sans barre d'adresse : coller une URL ou scanner un QR code (caméra + BarcodeDetector natif, repli jsQR auto-hébergé). L'URL chargée passe par une validation même origine (isSafeUrl) — un lien tiers ou un QR malveillant est refusé.

Le QR code d'une carte se génère depuis le panneau Partager ; on le scanne depuis me installé → transfert bureau→terrain sans saisie.


Fonctionnalités mobiles

Export image (snapshot)

Bouton Image dans le panneau Configuration → télécharge la vue courante en PNG.

Implémentation :

  • Écoute map.once('rendercomplete') puis lit map.getViewport().querySelector('canvas')
  • canvas.toBlob()URL.createObjectURL()<a>.download → clic programmatique
  • Entièrement côté client, zéro backend

Contrainte cross-origin : les sources WMS sViewer ont crossOrigin: 'anonymous'. Les données de fond/superposition définies dans customConfig.js doivent aussi avoir crossOrigin: 'anonymous' sur leur source OL, sinon le canvas est « tainted » et toBlob() lève une SecurityError (silencieuse, log console uniquement). Les services IGN Géoplateforme supportent CORS.


Internationalization (i18n)

Langues supportées

Langue Code Status
Français fr Défaut, complet
Anglais en Complet
Espagnol es Complet
Allemand de Complet

Fichier i18n.js

Toutes les chaînes traduites sont centralisées dans static/js/i18n.js :

Object.assign(window.SViewer.hardConfig, {
    i18n: {
        fr: {
            'Query': 'Interroger',
            'Legend': 'Légende',
            'Close': 'Fermer'
        },
        en: {
            'Query': 'Query',
            'Legend': 'Legend',
            'Close': 'Close'
        },
        es: { /* ... */ },
        de: { /* ... */ }
    }
});

Ajout de nouvelles traductions

Étape 1 : Ajouter la clé dans static/js/i18n.js

Object.assign(window.SViewer.hardConfig, {
    i18n: {
        fr: { 'ma clé': 'Texte français' },
        en: { 'ma clé': 'English text' },
        es: { 'ma clé': 'Texto español' },
        de: { 'ma clé': 'Deutscher Text' }
    }
});

Étape 2 : Ajouter class="i18n" title="ma clé" au HTML

<button class="i18n" title="ma clé">Placeholder</button>

Étape 3 : Pour du texte JavaScript

var msg = window.SViewer.hardConfig.i18n[window.SViewer.config.lang]['ma clé'];
alert(msg);

Sélection de langue

Par ordre de priorité :

  1. Paramètre URL ?lang=fr (code ISO 639-1 à 2 lettres)
  2. customConfig.lang
  3. Détection navigateur (Accept-Language)
  4. Défaut : en

Architecture et API Interne

Arborescence

sviewer/
├── index.html              — point d'entrée mode simple
├── manifest.json           — PWA manifest
├── sw.js                   — Service Worker
├── ext/                    — extensions (Grist, CSV, élévation, isochrone, Panoramax…)
│   ├── grist/              — widget Grist (index.html, widget.js, adapter.js)
│   ├── csv/                — adaptateur CSV (adapter.js)
│   ├── altitude/           — outils altimétriques : profil en long, … (IGN Géoplateforme)
│   ├── isochrone/          — isochrones (IGN Géoplateforme navigation)
│   ├── panoramax/          — viewer Panoramax (street-level imagery)
│   └── sample/             — extension de référence commentée
├── deploy/                 — config infra (nginx, Docker) — non servi
├── local/                  — sandbox déployeur (optionnel, monté en volume Docker)
│   ├── customConfig.js     — configuration locale (optionnel, défauts intégrés)
│   ├── customConfig_xxx.js — profils nommés (?c=xxx)
│   └── data/               — données locales, assets, etc.
├── js/                     — sources non-minifiées
│   ├── embed.js
│   └── sviewer.js
└── static/                 — tout ce qui est servi au navigateur
    ├── css/
    │   └── sviewer.min.css
    ├── fonts/
    │   ├── bootstrap-icons.subset.css
    │   └── bootstrap-icons.subset.woff2
    ├── img/
    │   ├── icon-192.png / icon-512.png / icon.svg
    │   └── pin-red.png
    ├── js/
    │   ├── embed.min.js
    │   └── sviewer.min.js
    ├── lib/                — dépendances vendored
    │   ├── bootstrap/
    │   ├── bootstrap-icons/
    │   ├── mustache/
    │   ├── ol/             — OpenLayers + proj4
    │   └── qrcode/
    └── templates/          — templates Mustache (sv-*.html)

Fichiers clés

Fichier Responsabilité
js/embed.js Chargement des dépendances + création du DOM + API SViewer.init()
js/sviewer.js Logique métier : carte, données, requêtes, état
css/sviewer.css Styles sViewer + overrides Bootstrap/OpenLayers
local/customConfig.js Configuration déployeur (optionnel)
static/js/i18n.js Traductions UI
index.html Point d'entrée mode simple

Dépendances

  • Bootstrap 5 : composants UI, responsive
  • OpenLayers 10 : rendu de carte, interactions OGC
  • proj4.js : projections cartographiques
  • Mustache : rendu de templates HTML
  • qrcode.js : génération codes QR (chargement lazy)

Toutes les dépendances sont self-hosted (pas de CDN). Aucune dépendance jQuery.

Flux de chargement (Mode WebComponent)

embed.js
  ├── Crée le bus d'événements (_svBus) — exposé via window._SViewerInternals (figé)
  ├── Détecte baseUrl depuis l'URL du script (window.SViewer.baseUrl)
  ├── Stocke les options dans window.SViewer.embedOptions
  ├── Crée le DOM (.sv-scope container)
  ├── Charge en parallèle : proj4 → OpenLayers → customConfig.js
  │   et en parallèle : Bootstrap JS + CSS sViewer (révèle le container)
  ├── Charge i18n.js
  └── Charge sviewer.js
        ├── Merge SViewer.embedOptions dans qs (priorité sur la page hôte)
        ├── Charge customConfig.js via SViewer.baseUrl (chemin absolu)
        └── doConfiguration() → doMap() → doGUI()
              └── init() : s'abonne à sv:loadFeatures / sv:loadFeatureObjects / sv:selectFeature
                           émet sv:mapReady

Objet config (global)

Fusionné à partir de, dans cet ordre de priorité :

  1. hardConfig (défauts sViewer)
  2. customConfig (configuration locale, local/customConfig.js)
  3. Options embed SViewer.embedOptions (passées à SViewer.init(), écrasent customConfig)
config = {
    title, lang, geOrchestraBaseUrl,
    initialExtent, maxExtent, restrictedExtent,
    maxFeatures, nodata,
    openLSGeocodeUrl,
    layersBackground, layersQueryable, i18n,
    // ... etc
}

Objet state (global)

État mutable de l'application :

state = {
    activePanel: 'query',         // Panneau affiché
    gfiok: true,                  // GetFeatureInfo actif
    mapCenter: [x, y],            // Centre actuel
    mapZoom: 12,
    layersVisible: [],            // Données visibles
    // ... etc
}

API SViewer.app

SViewer.init() retourne une Promise résolue avec l'instance SViewer.app une fois sViewer prêt.

Méthodes publiques :

Méthode Retourne Description
SViewer.getMap() ol.Map Objet carte OpenLayers
SViewer.getView() ol.View Vue OpenLayers (centre, zoom, projection)
SViewer.getApp() SViewer.app Instance sViewer (même résultat que la Promise)
app.getMap() ol.Map Identique à SViewer.getMap()
app.getView() ol.View Identique à SViewer.getView()
app.getConfig() object Configuration fusionnée (lecture seule)
app.getState() object État interne courant (lecture seule)
SViewer.setGeojsonUrl(url) Met à jour l'URL GeoJSON dans l'état (permalien/partage) sans recharger les données
SViewer.onTitleChange callback Fonction appelée quand l'utilisateur modifie le titre via le panneau de partage. null par défaut. Non appelée lors des modifications programmatiques (init, chargement md). Exemple : SViewer.onTitleChange = function(title) { /* persister */ };
SViewer.loadFeatures(geojson) Charge un GeoJSON FeatureCollection (objet JS) comme données vectorielles. Équivalent à ?geojson= mais avec données déjà parsées.
SViewer.refreshWMS() Force le rechargement de toutes les sources tuiles WMS (vide le cache OL). Utile après une modification serveur (édition WFS-T, import planifié).
SViewer.loadFeatureObjects(features, options) Charge un tableau d'entités OpenLayers (ol.Feature[]) déjà en EPSG:3857. Zéro reprojection, zéro sérialisation — chemin haute performance pour les widgets. Voir options ci-dessous.
SViewer.selectFeature(id) Sélectionne une entité par son id OL (feature.getId() — champ GeoJSON id de premier niveau). Fallback sur properties.id si absent. Zoome, affiche propriétés dans le panneau. console.warn si aucune entité trouvée.
SViewer.clearSelection() Efface la sélection courante et ferme le panneau de propriétés.
SViewer.onMapReady(fn) Enregistre un callback appelé une fois la carte initialisée. Argument : { map, view }.
SViewer.onFeatureClick(fn) Enregistre un callback appelé à chaque clic sur une entité vectorielle. Argument : { feature, coordinate, properties }.
SViewer.onFeatureSelect(fn) Enregistre un callback appelé à chaque changement de sélection (clic ou selectFeature/clearSelection). Argument : { feature, properties }null si désélection.
SViewer.onFeaturesLoaded(fn) Enregistre un callback appelé après chaque chargement de données vectorielles. Argument : { features, count }.

Contrôle de la vue :

SViewer.init('#ma-carte', { x: -390192, y: 6122108, z: 10 })
    .then(function(app) {
        var view = app.getView();

        // Centrer et zoomer sans animation
        view.setCenter([-390192, 6122108]);
        view.setZoom(14);

        // Centrer avec animation fluide
        view.animate({
            center: ol.proj.fromLonLat([-4.49, 48.39]),
            zoom: 14,
            duration: 800
        });
    });

Ajouter des données OL sur la carte :

SViewer.init('#ma-carte', {}).then(function(app) {
    var map = app.getMap();

    var overlay = new ol.layer.Tile({
        source: new ol.source.TileWMS({
            url: 'https://my-geoserver.example.org/geoserver/wms',
            params: { LAYERS: 'geor:commune', VERSION: '1.3.0' },
            crossOrigin: 'anonymous'
        })
    });
    map.addLayer(overlay);
});

Réagir aux événements OL :

SViewer.init('#ma-carte', {}).then(function(app) {
    var map = app.getMap();

    // Clic sur la carte
    map.on('singleclick', function(e) {
        var lonlat = ol.proj.toLonLat(e.coordinate);
        console.log('Clic :', lonlat[0].toFixed(5), lonlat[1].toFixed(5));
    });

    // Fin de déplacement
    map.on('moveend', function() {
        var view = map.getView();
        console.log('Zoom :', view.getZoom());
        console.log('Centre :', ol.proj.toLonLat(view.getCenter()));
    });
});

Note : Ces exemples utilisent l'API OpenLayers directement. Consulter la documentation OpenLayers pour l'ensemble des méthodes disponibles sur ol.Map et ol.View.

Embed SDK — bus d'événements

Le bus d'événements permet aux widgets embarqués (Grist, futures intégrations) de réagir aux événements cartographiques et de piloter la carte sans accéder aux internals OpenLayers.

Canal de communicationwindow._SViewerInternals.bus (objet figé, non modifiable depuis l'extérieur). Créé par embed.js avant tout chargement de dépendance. Absent en mode simple (pas d'embed.js).

Options de loadFeatureObjects

Clé Type Défaut Description
styleOverride ol.style.Style ou fonction style OL null Style personnalisé. Si absent, utilise customConfig.geojsonStyle.
fitExtent boolean false Recadre la vue sur l'étendue des entités après chargement.

Exemple — widget qui charge des entités OL et écoute les clics

// Après SViewer.init(...)
SViewer.onMapReady(function() {
    // Charger des ol.Feature[] déjà en EPSG:3857
    SViewer.loadFeatureObjects(olFeatures, { fitExtent: true });
});

SViewer.onFeatureClick(function(e) {
    console.log('Clic sur :', e.feature.getId(), e.properties);
});

SViewer.onFeatureSelect(function(e) {
    if (!e.feature) { console.log('Désélection'); return; }
    console.log('Sélection :', e.properties);
});

Exemple — charger un GeoJSON parsé

fetch('https://example.com/data.geojson')
    .then(function(r) { return r.json(); })
    .then(function(geojson) {
        SViewer.loadFeatures(geojson);
    });

Exemple — sélectionner une entité par id

// Sélectionne l'entité dont feature.getId() === 'row_42'
SViewer.selectFeature('row_42');

// Efface la sélection
SViewer.clearSelection();

Sécurité — le bus est encapsulé dans la closure embed.js et exposé via Object.defineProperty avec writable: false, configurable: false. La page hôte ne peut ni remplacer ni supprimer window._SViewerInternals.

Usage multiple — plusieurs appels onFeatureClick(fn) enregistrent plusieurs callbacks, tous appelés dans l'ordre d'enregistrement. Utiliser SViewer.onFeatureClick uniquement après que embed.js est chargé (c'est toujours le cas en mode embed).

Minification

Les fichiers minifiés sont générés via npm :

npm run minify        # embed.js → embed.min.js, sviewer.js → sviewer.min.js, sviewer.css → sviewer.min.css
npm run build         # build OL custom bundle + minify

index.html charge embed.min.js. embed.min.js charge les fichiers minifiés par défaut. Avec ?debug=1, charge les sources non-minifiées.

Source Minifié Outil
js/embed.js static/js/embed.min.js terser
js/sviewer.js static/js/sviewer.min.js terser
css/sviewer.css static/css/sviewer.min.css postcss + cssnano

La configuration cssnano est dans postcss.config.js (preset default).

Procédure de release

Checklist pré-release

  • Toutes les fonctionnalités mergées et testées
  • static/js/i18n.js — toutes les clefs présentes dans les 4 langues
  • Aucune erreur console en navigation (?debug=true)
  • CHANGELOG.md mis à jour pour cette version

Build

  1. Mettre à jour la version dans package.json :

    # éditer manuellement "version": "X.Y.Z"
  2. Committer les changements de code, puis appliquer le stamp (injecte version + hash du dernier commit dans embed.js) :

    git add -p && git commit -m "..."
    npm run stamp
  3. Minifier :

    npm run minify
  4. Committer les artefacts générés (embed.js stampé, fichiers .min.*) :

    git add js/embed.js static/js/embed.min.js static/js/sviewer.min.js static/css/sviewer.min.css
    git commit -m "chore: stamp vX.Y.Z + minify"
  5. Tagger et pousser :

    git tag vX.Y.Z
    git push && git push --tags

Vérification post-release

  • SViewer.version correct dans la console navigateur
  • Version visible en bas du panneau de partage
  • Section [Unreleased] de CHANGELOG.md vide

La version et le hash de commit apparaissent :

  • En bas du panneau Configuration (discret, opacity 0.4)
  • Dans la console du navigateur au démarrage : sViewer X.Y.Z (abcd123)
  • Via l'API publique : SViewer.version, SViewer.commit

Suite de tests navigateur

La suite de tests est accessible à /sviewer/tests/ — aucune installation requise.

Lancer les tests :

  • Ouvrir /sviewer/tests/ dans le navigateur
  • Cliquer sur un test dans le panneau gauche → la carte se charge dans le panneau droit, résultat affiché (✓/✗ + durée)
  • Run all : lance tous les tests en séquence
  • Run group : lance uniquement le groupe du test sélectionné
  • ?autorun=1 : lance tous les tests au chargement (usage CI headless)

Groupes de tests :

Groupe Description CI
Params Paramètres KVP URL (?x= ?y= ?z= ?c= ?lang= ?lb=)
Config Fusion hardConfig / customConfig (via ?c=test)
i18n Couverture des clefs, 4 langues
Live Endpoints WMS réels (GeoBretagne, IGN GPF) manuel

Les tests du groupe Live font appel à des services externes — les exclure en CI.

Ajouter un test WMS — copier un bloc dans tests/suites/04-wms-services.js, changer id, label et les arguments de makeWmsTest(). Format du paramètre layers : nom_donnee@https://url/wms.

Scoping CSS

Pour éviter les collisions avec le CSS hôte, toutes les classes sViewer utilisent le préfixe .sv- et sont englobées dans .sv-scope :

<div class="sv-scope" id="sv-container">
    <div class="sv-header">
        <button class="sv-btn">...</button>
    </div>
</div>

CSS produit :

.sv-scope .sv-header { ... }
.sv-scope .sv-btn { ... }

Dépannage

La carte ne charge pas

Symptômes : Page vierge, pas de message d'erreur.

Diagnostic :

  • Ouvrir la console du navigateur (F12)
  • Chercher des erreurs JavaScript (red icons)
  • Vérifier l'onglet Réseau (Network) : tous les fichiers se chargent-ils ?

Causes courantes :

  1. customConfig.js manquant → Copier customConfig.DIST.js en customConfig.js
  2. CORS erreur → Services OGC doivent supporter CORS
  3. Syntaxe JSON invalide → Vérifier customConfig.js

Les données WMS ne s'affichent pas

Diagnostic :

  • Vérifier que layers=namespace:layername est correct
  • Vérifier que la donnée existe sur le serveur WMS
  • Ouvrir l'URL WMS directement dans le navigateur

Causes courantes :

  1. Erreur CORS → Le serveur WMS doit envoyer Access-Control-Allow-Origin: *
  2. URL WMS incorrecte → Vérifier geOrchestraBaseUrl
  3. Projection → La donnée doit être en EPSG:3857 ou reprojetable

Requête GetFeatureInfo échoue

Message : « L'interrogation a échoué »

Causes :

  1. Donnée non queryable → Vérifier queryable="1" en GetCapabilities WMS
  2. CORS → Erreur No 'Access-Control-Allow-Origin' header
  3. Erreur serveur → Code HTTP 500 du serveur WMS

Solution :

// Mode simple : ajouter &q=0 pour désactiver
?layers=geor:commune&q=0

// Mode WebComponent : omettre q pour ne pas déclencher GetFeatureInfo
SViewer.init('#map', { layers: 'geor:commune' });
``

### Traductions manquantes

**Symptôme :** Texte en anglais au lieu de traduction.

**Cause :** Clé manquante dans `hardConfig.i18n[lang]`.

**Solution :** Ajouter la traduction manquante dans `static/js/i18n.js`.

---

## Plugin Apache Superset

Plugin graphique natif (`sviewer_map`) pour Apache Superset 4.x. Pas de fork, pas de redémarrage — l'administrateur enregistre le bundle via l'interface Plugins.

### Installation

Activer le feature flag dans `superset_config.py` :

```python
FEATURE_FLAGS = {"DYNAMIC_PLUGINS": True}

Enregistrer le plugin : Paramètres → Plugins → Ajouter :

https://votre-serveur/sviewer/ext/superset/dist/superset-plugin-chart-sviewer.js

Clé : sviewer_map

Architecture

Le plugin construit un <iframe> pointant vers sViewer avec ?ext=superset. sViewer charge automatiquement ext/superset/extension.js (même origine).

Dataset Superset
  → buildQueryContext (filtres natifs mergés côté serveur)
    → FeatureCollection GeoJSON (transformProps)
      → postMessage sv:geojson → iframe sViewer
        → SViewer.loadFeatures() → carte redessinée

Passthrough de l'URL de partage

Le champ URL sViewer accepte une URL de partage complète. Tous les paramètres sont transmis intégralement à l'iframe — position, zoom, fond de carte, données WMS, thème, extensions (s=1, q=1, ext=print…). Le plugin ajoute uniquement ext=superset (s'il est absent) et title= (nom du graphique Superset).

Symbologie

La symbologie est calculée côté plugin, injectée dans les propriétés GeoJSON :

Propriété Rôle
_sv_color Couleur CSS de l'entité (fixe ou rampe graduée)
_sv_radius Rayon en pixels (symboles proportionnels, points uniquement)
_label Libellé affiché en infobulle

Modes de normalisation disponibles : racine carrée, linéaire, logarithmique, quantile, Jenks (coupures naturelles), rang.

Build

cd ext/superset
npm install
npm run build   # → dist/superset-plugin-chart-sviewer.js

Committer dist/ après chaque build.

Documentation utilisateur complète


Ressources supplémentaires