@ignf-geoportal/sdk-2d
Version:
French Geoportal SDK based on OpenLayers (2D)
758 lines (695 loc) • 30.2 kB
JavaScript
/* global false */
import Logger from "../Utils/LoggerByDefault";
import { transform as olTransformProj } from "ol/proj";
import {
Services,
ProxyUtils,
ColorUtils
} from "geoportal-extensions-openlayers";
import { MapLoader } from "../Utils/MapLoader";
/**
* Reloads the map with a new cartographic library. The current view options (camera position, layers, controls) will be conserved.
* This function only works with the GpSDK3D bundle (run the "build:3d" npm task to generate it)
*
* @param {String} viewMode - The cartographic view mode. "2d" (for a 2D map) or "3d" (for a 3D map).
*
* @return {Object} the new map
* @static
* @private
*/
var switch2D3D = function (viewMode) {
var oldMap = {};
oldMap.projection = this.getProjection();
oldMap.center = this.getCenter();
oldMap.tilt = this.getTilt();
oldMap.azimuth = this.getAzimuth();
oldMap.zoom = this.getZoom();
oldMap.layersOptions = this.getLayersOptions();
oldMap.markersOptions = this.getMarkersOptions();
oldMap.controlsOptions = this.getControlsOptions();
oldMap.mapDiv = this.div.id;
oldMap.apiKey = this.apiKey;
oldMap.extent = this.mapOptions.extent;
oldMap.isWebGL2 = this.mapOptions.isWebGL2;
oldMap.enableRotation = this.mapOptions.enableRotation !== undefined ? this.mapOptions.enableRotation : null;
oldMap.mapEventsOptions = this.mapOptions.mapEventsOptions !== undefined ? this.mapOptions.mapEventsOptions : null;
// add all compute layer to 3D map with layersOptions
if (viewMode === "3d") {
for (var index = 0; index < this._layers.length; index++) {
var layer = this._layers[index];
// traitement des couches de calcul
if (layer.options.format.toUpperCase() === "COMPUTE") {
// INFO :
// les styles pour la 2D et 3D sont ajoutés à la volée,
// une evolution est à mettre en place sur les contrôles,
// les contrôles doivent transmettre les styles à la couche 2D & 3D.
// ex. isocurve = fill-color : "rgba(0,183,152,0.7)"
// ex. itineraire = stroke-color : "rgba(0,183,152,0.9)",
// stroke-width : 12
// les controles fournissent leurs méta-informations utiles à leur reconstruction en 2D.
// les infos issues de la methode getData() :
// * point de départ
// * point intermédiares
// * point d'arrivée
// * mode de transport
// * mode de calcul
// * passage
// * résultats
// idem pour la couche..., on récupere uniquement le tracé au format GeoJSON.
// la couche au format natif n'est pas utile pour la switch 2D<->3D.
var geojsonStr = null;
var geojsonObj = null;
switch (layer.options.control.toUpperCase()) {
case "ROUTE":
case "ISOCURVE":
case "ELEVATIONPATH":
var control = this.getLibMapControl(layer.options.control.toLowerCase());
// on distingue le cas d'un import de calcul et un calcul en cours...
oldMap.layersOptions[layer.id].controlOptions = (Object.keys(layer.options.controlOptions).length === 0) ? control.getData() : layer.options.controlOptions;
geojsonStr = (Object.keys(layer.options.data).length === 0) ? control.getGeoJSON() : layer.options.data;
// on parse le geojson pour y ajouter des properties de styles (2D et 3D)
geojsonObj = JSON.parse(geojsonStr);
// transmettre les styles du controles 2D vers 3D
var styles = control.getStyle();
geojsonObj.features.forEach(feature => {
if (!feature.properties) {
feature.properties = {};
}
if (feature.geometry.type === "Point") {
// INFO
// le style est propre à la 3D
// les controles ne fournissent pas les styles des icones
// le style est present dans le format
feature.properties.icon = feature.properties["marker-symbol"];
// FIXME transmettre l'offset !
} else if (feature.geometry.type === "LineString") {
// style pour la 2D et 3D
var colorStroke = {};
if (ColorUtils.isRGB(styles.getStroke().getColor())) {
colorStroke = ColorUtils.rgbaToHex(styles.getStroke().getColor());
} else {
colorStroke = {
hex : styles.getStroke().getColor(),
opacity : 1
};
}
feature.properties["stroke"] = colorStroke.hex || "#00B798";
feature.properties["stroke-opacity"] = colorStroke.opacity || 0.9;
feature.properties["stroke-width"] = styles.getStroke().getWidth() || 12;
} else if (feature.geometry.type === "Polygon") {
// style pour la 2D et 3D
var colorFill = {};
if (ColorUtils.isRGB(styles.getFill().getColor())) {
colorFill = ColorUtils.rgbaToHex(styles.getFill().getColor());
} else {
colorFill = {
hex : styles.getFill().getColor(),
opacity : 1
};
}
feature.properties["fill"] = colorFill.hex || "#00B798";
feature.properties["fill-opacity"] = colorFill.opacity || 0.7;
} else {
// ...
}
});
oldMap.layersOptions[layer.id].data = JSON.stringify(geojsonObj);
break;
default:
// TODO other format...
break;
}
}
}
}
// remove old controls and associated listeners
for (var controlId in oldMap.controlsOptions) {
this.removeControls(controlId);
}
// récupération des couches 3D qui n'étaient pas affichées en 2D
if (viewMode === "3d") {
if (this._3Dlayers) {
for (var l = 0; l < this._3Dlayers.length; l++) {
oldMap.layersOptions[this._3Dlayers[l].id] = this._3Dlayers[l].options;
}
}
}
// récupération de l'azimut
if (viewMode === "3d") {
oldMap.azimuth = this.getAzimuth();
}
// gestion du centre
function switch4center (mode, center, projection) {
var _center = null;
if (mode === "3d") {
// transformation des coordonnées de planes en géographiques
// FIXME : ne devrait pas se faire avec ol.proj mais avec proj4 car dans IMap, ol n'est pas forcement chargée !
var lonlat = olTransformProj([center.x, center.y], projection, "EPSG:4326");
_center = {
x : lonlat[0],
y : lonlat[1]
};
}
if (mode === "2d") {
// transformation des coordonnées de géographiques en planes
// FIXME : ne devrait pas se faire avec ol.proj mais avec proj4 car dans IMap, ol n'est pas forcement chargée !
var xy = olTransformProj([center.lon, center.lat], "EPSG:4326", "EPSG:3857");
_center = {
x : xy[0],
y : xy[1]
};
}
return _center;
}
oldMap.center = switch4center(viewMode, oldMap.center, oldMap.projection);
// gestion du controle overview
function switch4overview (mode, overview) {
// 2d <-> 3d
// * maximised <-> maximised
// * layers <-> layerId ou layer (mapping)
// * minZoom <-> layer.source.zoom.min
// * maxZoom <-> layer.source.zoom.max
// * projection <-> layer.source.projection.toUpperCase()
// mapping layer 3d <-> 2d :
// *protocol : format.toLowerCase(),
// *version : version,
// *url : url,
// *projection : projection.toUpperCase(),
// *networkOptions : {crossOrigin : "omit"},
// *tileMatrixSet :tileMatrixSet,
// *tileMatrixSetLimits :tileMatrixSetLimits,
// *format :outputFormat,
// *name :layer,
// *style :styleName,
// *zoom.min : minZoom || 1,
// *zoom.max : maxZoom || 21
// *extent : bbox
var _overview = {};
if (mode === "3d") {
// 2d -> 3d
if (overview.layers) {
if (overview.layers.length) {
// on prend le 1er element car la 3d ne peut pas avoir
// plusieurs couches...
var l = overview.layers[0];
if (typeof l === "object") {
// conf d'une couche : mapping des params
_overview.layer = {
id : l.layer,
type : "color",
visible : l.visible,
opacity : l.opacity || 1,
source : {
protocol : (l.format) ? l.format.toLowerCase() : "wmts",
version : l.version || "1.0.0",
projection : (l.projection) ? l.projection.toUpperCase() : null,
url : l.url,
// networkOptions: {
// crossOrigin: "anonymous"
// },
format : l.outputFormat || "image/jpeg",
name : l.layer,
tileMatrixSet : l.tileMatrixSet || "PM",
style : l.styleName || "normal",
// tileMatrixSetLimits : [],
// extent : {},
zoom : {
min : l.minZoom || 1,
max : l.maxZoom || 21
}
}
};
} else {
// id de couche
_overview.layerId = l;
}
}
}
}
if (mode === "2d") {
// 3d -> 2d
_overview.layers = [];
if (overview.layerId) {
// id de couche
_overview.layers.push(overview.layerId);
}
if (overview.layer) {
var ly = overview.layer;
// conf d'une couche : mapping des params
_overview.layers.push({
format : (ly.source.protocol) ? ly.source.protocol.toLowerCase() : "wmts",
minZoom : ly.source.zoom.min || 1,
maxZoom : ly.source.zoom.max || 21,
visibility : ly.visibility,
opacity : ly.opacity || 1,
url : ly.source.url,
layer : ly.source.name,
tileMatrixSet : ly.source.tileMatrixSet || "PM",
version : ly.source.version || "1.0.0",
styleName : ly.source.style || "normal",
outputFormat : ly.source.format || "image/jpeg",
projection : (ly.source.projection) ? ly.source.projection.toUpperCase() : null
});
}
}
// options communes
_overview.maximised = overview.maximised;
// options à garder lors du switch :
// 2d
_overview.minZoom = overview.minZoom;
_overview.maxZoom = overview.maxZoom;
_overview.projection = overview.projection;
// 3d
_overview.width = overview.width;
_overview.height = overview.height;
_overview.x = overview.x;
_overview.y = overview.y;
_overview.position = overview.position;
return _overview;
}
if (oldMap.controlsOptions.overview) {
oldMap.controlsOptions.overview = switch4overview(viewMode, oldMap.controlsOptions.overview);
}
this.destroyMap();
// this.libMap = null;
var newMap = MapLoader.load(
// FIXME faut-il rajouter un acces aux clés API directement dans Map getApiKeys()
// this.libMap.getApiKeys(),
// FIXME faut-il rajouter un acces à la div directement dans Map getDiv()
// this.libMap.getDiv(),
oldMap.mapDiv,
// récupére le paramétrage courant de la carte (par les librairies) et pas le paramétrage initial (par this.mapOptions)
{
apiKey : oldMap.apiKey,
enableRotation : oldMap.enableRotation,
projection : oldMap.projection,
center : oldMap.center,
extent : oldMap.extent,
azimuth : oldMap.azimuth,
tilt : oldMap.tilt,
zoom : oldMap.zoom,
// maxZoom : this.
// minZoom : this.
markersOptions : oldMap.markersOptions,
viewMode : viewMode,
isWebGL2 : oldMap.isWebGL2,
// proxyUrl
// noProxyDomains
// reloadConfig
layersOptions : oldMap.layersOptions,
controlsOptions : oldMap.controlsOptions,
mapEventsOptions : oldMap.mapEventsOptions
}
);
// une fois la carte chargée en 2D, on reconstruit les controles avec les couches
// de calcul...
if (viewMode === "2d") {
newMap.listen("mapLoaded", function () {
for (var i = 0; i < this._layers.length; i++) {
var clayer = this._layers[i];
// traitement des couches de calcul
if (clayer.options.format.toUpperCase() === "COMPUTE") {
var control = null;
switch (clayer.options.control.toUpperCase()) {
case "ROUTE":
case "ISOCURVE":
case "ELEVATIONPATH":
control = this.getLibMapControl(clayer.options.control.toLowerCase());
break;
default:
// TODO other format...
break;
}
if (control) {
control.setData(clayer.options.controlOptions);
control.setLayer(clayer.obj);
// control.setGeoJSON(clayer.options.data); // inutile ?
control.init();
}
}
}
});
}
return newMap;
};
/**
* Map Object. Returned by {@link module:Map Gp.Map.load()} function. Provides methods to interact with map view.
*
* @param {Object} opts - map options
* @constructor
* @private
*/
function IMap (opts) {
if (!(this instanceof IMap)) {
throw new TypeError("IMap constructor cannot be called as a function.");
}
// sauvegarde des options du constructeur.
this._opts = opts;
// logger
this.logger = Logger.getLogger("IMap");
this.logger.trace("[Constructeur IMap (options)]");
/**
* Couches enregistrées par le SDK
* Structure des objets stockés :
* {
* id : layerId
* obj : objet couche géré par l'implémentation.
* options : propriétés de la couche (de type layerOptions)
* }
*/
this._layers = [];
/**
* Couches enregistrées par le SDK et supprimées
* Structure des objets stockés :
* {
* id : layerId
* obj : objet couche géré par l'implémentation.
* options : propriétés de la couche (de type layerOptions)
* }
*/
this._layersRemoved = [];
/**
* Controlss enregistrés par le SDK
* Structure des objets stockés :
* {
* id : controlId
* obj : objet control géré par l'implémentation.
* options : propriétés du controle (de type controlOptions)
* }
*/
this._controls = [];
/**
* object d'enriegistrements des evenements.
* Les propriétés sont les identifiants de l'evenement associées à un tableau :
* "eventId" : [{
* action : // function
* key : "clef retournée par OL pour l'enregistrement de l'evt"
* context : objet d'execution de l'action
* }]
*/
this._events = {};
/**
* object d'enriegistrements des overlays.
* Markers enregistrés par le SDK
*/
this._markers = [];
// la configuration est elle chargée ?
this._isConfLoaded = false;
if (this._opts.mapOptions) {
this.apiKey = this._opts.mapOptions.apiKey;
}
this.div = this._opts.div;
// récupère les options par défaut pour les paramètres optionnels
// /!\ on duplique l'objet pour ne pas partager les mêmes options entre
// differentes cartes.
this.mapOptions = Object.create(IMap.DEFAULTOPTIONS);
// et on ajoute les options en paramètres aux options par défaut
for (var opt in this._opts.mapOptions) {
if (this._opts.mapOptions.hasOwnProperty(opt)) {
this.mapOptions[opt] = this._opts.mapOptions[opt];
// FIXME : ne marche qu'au premier niveau...
this.logger.trace("option " + opt + " récupérée");
}
}
// intialisation de la carte (à vide)
this._initMap();
// Substitute global constants configured at compile time
// cf. webpack.config.js
if (false) {
this.switch2D3D = switch2D3D.bind(this);
}
};
/**
* Options par défaut du service
* (pour les options facultatives seulement)
* @private
*/
IMap.DEFAULTOPTIONS = {
projection : "EPSG:3857",
center : {
x : 0,
y : 0,
projection : "EPSG:4326",
geolocate : false,
location : null,
locationType : "location"
},
azimuth : 0,
enableRotation : true,
tilt : 90,
zoom : 10,
minZoom : 0,
maxZoom : 21,
reloadConfig : true,
controlsOptions : {
draggable : true,
keyboard : true,
selectable : false
},
mapEventsOptions : {},
noProxyDomains : ["wxs.ign.fr"]
};
/**
* Couche par défaut
* @param {String} apiKey - api key
* @returns {Object} - config
* @private
*/
IMap.DEFAULTLAYER = function (apiKey) {
return {
"ORTHOIMAGERY.ORTHOPHOTOS::GEOPORTAIL:OGC:WMTS" : {
minZoom : 0,
maxZoom : 19,
queryable : true,
opacity : 1,
format : "wmts",
position : 0,
title : "Photographies aériennes",
description : "Photographies aériennes",
url : "https://data.geopf.fr/wmts",
layer : "ORTHOIMAGERY.ORTHOPHOTOS",
projection : "EPSG:3857",
tileMatrixSet : "PM",
topLeftCorner : {
x : -20037508,
y : 20037508
},
resolutions : [156543.033928041, 78271.51696402048, 39135.758482010235, 19567.87924100512, 9783.93962050256, 4891.96981025128, 2445.98490512564, 1222.99245256282, 611.49622628141, 305.7481131407048, 152.8740565703525, 76.43702828517624, 38.21851414258813, 19.10925707129406, 9.554628535647032, 4.777314267823516, 2.388657133911758, 1.194328566955879, 0.5971642834779395, 0.2985821417389697, 0.1492910708694849, 0.0746455354347424],
matrixIds : ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "20", "21"],
styleName : "normal",
outputFormat : "image/jpeg",
originators : [{
name : "IGN",
attribution : "Institut National de l'Information Géographique et Forestière",
url : "https://www.ign.fr"
}],
quicklookUrl : "https://wxs.ign.fr/static/pictures/ign_ortho.jpg",
metadata : [
{ url : "" },
{ format : "xml", url : "https://wxs.ign.fr/geoportail/csw?service=CSW&version=2.0.2&request=GetRecordById&Id=IGNF_BDORTHOr_2-0.xml" },
{ format : "xml", url : "https://wxs.ign.fr/geoportail/csw?service=CSW&version=2.0.2&request=GetRecordById&Id=SPOT5.xml" },
{ format : "xml", url : "https://wxs.ign.fr/geoportail/csw?service=CSW&version=2.0.2&request=GetRecordById&Id=NCL-DITTT-ORTHO.xml" },
{ format : "xml", url : "https://wxs.ign.fr/geoportail/csw?service=CSW&version=2.0.2&request=GetRecordById&Id=IGNF_BDORTHOr_2-0.xml" },
{ format : "xml", url : "https://wxs.ign.fr/geoportail/csw?service=CSW&version=2.0.2&request=GetRecordById&Id=IGNF_ORTHOHR_1-0.xml" }
],
legends : [
{
format : "application/pdf",
url : "https://www.geoportail.gouv.fr/depot/fiches/photographiesaeriennes/geoportail_dates_des_prises_de_vues_aeriennes.pdf",
minScaleDenominator : null
}
]
}
};
};
IMap.prototype = {
/*
* Constructeur (alias)
*/
constructor : IMap,
/**
* Map initialisation with constructor parameters (after _initMap call)
* needed for asynchronous _initMap implementation.
*
* @private
*/
_afterInitMap : function () {
// abonnement aux evenements demandes
var meOptions = this.mapOptions.mapEventsOptions;
if (meOptions) {
for (var evtId in meOptions) {
this.listen(evtId, meOptions[evtId], this);
}
}
// abonnement perso à l'evnement layerChanged pour tenir à jour l'objet this._layers
this.listen("layerChanged", this._onLayerChanged, this);
// centrage avec les coordonnées x, y (s'il y en a)
if (this.getViewMode() === "2d") {
this.setXYCenter(this.mapOptions.center);
}
// FIXME Config est créé en runtime dans la variable globale Gp
var scope = typeof window !== "undefined" ? window : {};
var Config = scope.Gp ? scope.Gp.Config : undefined;
// Gestion du paramètre apiKeys
var needsGetConfig = false;
if (this.apiKey && !this.mapOptions.reloadConfig) { // une clef est fournie
// et l'utilisateur ne souhaite pas faire un appel à la configuration
needsGetConfig = false;
} else if (this.apiKey || this.mapOptions.customConfigFile) {
// TODO : this.apiKey.length > 1
needsGetConfig = (this.mapOptions.reloadConfig || !Config || !Config.isKeyConfLoaded((Array.isArray(this.apiKey) ? this.apiKey[0] : this.apiKey))
);
} else { // une clef n'est pas fournie
// on essaye de trouver une configuration existante
if (Config) {
this.apiKey = Object.keys(Config.generalOptions.apiKeys);
} else {
// clé full par défaut et on charge la config
this.apiKey = "full";
needsGetConfig = true;
}
}
// appel de la configuration si nécessaire
// Dans tous les cas, le reste s'exécute dans _afterGetConfig
var map = this;
if (needsGetConfig) {
// config locale ? on met par defaut un callbackSuffix à ""
// à moins qu'on ne le surcharge (non documenté).
var callbackSuffix = this.mapOptions.callbackSuffix;
if (this.mapOptions.customConfigFile) {
callbackSuffix = callbackSuffix || "";
}
Services.getConfig({
apiKey : this.apiKey,
customConfigFile : this.mapOptions.customConfigFile,
callbackSuffix : callbackSuffix,
// fonction de rappel onSuccess
onSuccess : function (configResponse) {
map._afterGetConfig(configResponse);
},
// fonction de rappel onFailure
onFailure : function (error) {
this.logger.error(error);
map._afterGetConfig(null);
}
});
} else {
// HACK : je mets cette fonction dans la pile des callbacks afin que son
// execution soit differée par le moteur javascript !
// ceci afin qu'un abonnement à l'event "mapLoaded" puisse se faire...
setTimeout(function () {
map._afterGetConfig(Config);
});
}
},
/**
* callback d'appel à la configuration (ou non).
*
* @param {Object} configResponse - configuration associée à apiKey.
* @fires mapLoaded
* @fires configured
* @private
*/
_afterGetConfig : function (configResponse) {
this.logger.trace("[IMap] : Configuration chargée ... ou pas");
// TODO : detecter si on a le bon objet (error ou success)
this._isConfLoaded = !(typeof configResponse === "undefined");
// declenchement de l'evenement "configured"
var e = IMap.CustomEvent("configured", {
detail : {
config : configResponse
}
});
this.div.dispatchEvent(e);
// recuperation couche par defaut si aucune specifiee
if (!this.mapOptions.hasOwnProperty("layersOptions")) {
if (this._isConfLoaded) {
this.mapOptions.layersOptions = {
"ORTHOIMAGERY.ORTHOPHOTOS" : {}
};
} else {
this.mapOptions.layersOptions = IMap.DEFAULTLAYER(this.apiKey);
}
}
// recuperation du centre par défaut si aucun spécifié
if (this._isConfLoaded &&
(!this.mapOptions.center.hasOwnProperty("x") || this.mapOptions.center.x === 0) &&
(!this.mapOptions.center.hasOwnProperty("y") || this.mapOptions.center.y === 0)) {
// default autoconf "FXX" territory from the old autoconf
this.logger.trace("[IMap] : _afterGetConfig : setting default map center to (2.345274398, 48.860832558)");
this.mapOptions.center.x = "2.345274398";
this.mapOptions.center.y = "48.860832558";
this.mapOptions.center.projection = "EPSG:4326";
}
// centrage par location ou geolocalisation (s'il y en a). Centrage en x,y réalisé au chargement du globe (3d:initMap / 2d:_afterInitMap)
if (this.mapOptions.center.location || this.mapOptions.center.geolocate) {
this.setCenter(this.mapOptions.center);
}
// ajout des markers : en 3D, uniquement un enregistrement dans this._markers
if (this.mapOptions.hasOwnProperty("markersOptions")) {
this.setMarkersOptions(this.mapOptions.markersOptions);
}
// ajout des couches
this.addLayers(this.mapOptions.layersOptions);
// ajout des controles
// ... par defaut
this.addControls(this._getDefaultControls());
// ceux demandés par l'utilisateur
this.addControls(this.mapOptions.controlsOptions);
// declenchement de l'evenement "mapLoaded"
var eMapLoaded = IMap.CustomEvent("mapLoaded", {
detail : {
map : this
}
});
this.div.dispatchEvent(eMapLoaded);
},
/**
* Empty Map initialization : abstract
* Implementation has to call _afterInitMap() function.
*
* @private
*/
_initMap : function () {
this.logger.trace("[IMap] : _initMap");
},
/**
* ajoute un proxy aux url des couches vecteurs si besoin.
*
* @alias Gp.Map.setProxy
* @param {String} url - url
*
* @returns {String} - proxified url
*/
setProxy : function (url) {
return ProxyUtils.proxifyUrl(url, this.mapOptions);
},
/**
* Returns the current viewing mode (either 2D or 3d).
*
* @alias Gp.Map.getViewMode
* @returns {String} - The current viewing mode
*/
getViewMode : function () {
return this.mapOptions.viewMode;
},
/**
* Returns wrapped map object implemented by the underlying library. Can be :
* - an [ol.Map](https://openlayers.org/en/latest/apidoc/module-ol_Map-Map.html) object with current OpenLayers 3 implementation if the current map is in 2D.
* - an [Itowns.GlobeView](http://www.itowns-project.org/itowns/docs/#api/View/GlobeView) object, ie the iTowns GlobeView object overloaded by Geoportal Extension for iTowns, if the current map is in 3D.
*
* @alias Gp.Map.getLibMap
* @returns {Object} - wrapped map object.
*/
getLibMap : function () {
// TO BE OVERRIDDEN
return {};
},
/**
* Destroy map by canceling all events listening and removing DOM elements
*/
destroyMap : function () {
// TO BE OVERRIDDEN
}
};
export { IMap };