UNPKG

geopf-extensions-openlayers

Version:

French Geoportal Extensions for OpenLayers libraries

506 lines (469 loc) 18.7 kB
// import openlayers import olGPX from "ol/format/GPX"; // import Geometry import MultiLineString from "ol/geom/MultiLineString"; import LineString from "ol/geom/LineString"; import Feature from "ol/Feature"; // import local import Styling from "./Styling"; import Parser from "../Utils/Parser"; /** * @classdesc * * Extended Styles GPX format to export (internal use only !) * * SPEC * cf. https://www.topografix.com/gpx.asp * * * @alias ol.format.GPXExtended * @module GPXExtended */ class GPX extends olGPX { /** * @constructor * @param {Object} options - Options * @param {Object} [options.defaultStyle] - Styles by default * @param {String} [options.orderBy] - Sort by key the feature before writing. By default, no sorting * @param {Object} [options.extensions] - Add properties to file root * @param {function} [options.readExtensions] - Reading extensions (native) */ constructor (options) { super(options); if (!(this instanceof GPX)) { throw new TypeError("ERROR CLASS_CONSTRUCTOR"); } this.options = options || {}; // INFO // surcharge de la callback : readExtensions if (this.options.readExtensions && typeof this.options.readExtensions === "function") { var clbk = this.options.readExtensions; // callback definie par l'utilisateur this.options.readExtensions = function (feature, node) { this.readExtensions(feature, node); clbk.call(this, feature, node); }; } else { this.options.readExtensions = this.readExtensions; } // INFO // defaultStyle est un objet de type Style if (this.options.defaultStyle === null || typeof this.options.defaultStyle === "undefined") { this.options.defaultStyle = {}; } this.source = null; return this; } /** * Read Extend Styles for Features. * This function overloads ol.format.GPX.readFeatures ... * * @see olGPX#readFeatures * @param {Document|Node} source - Source. * @param {Object} options - options. see olx.format.ReadOptions * @returns {Feature[]} Features. */ readFeatures (source, options) { // INFO // le travail de lecture des extensions du format est porté // par la callback des options : readExtensions var features = super.readFeatures(source, options); // String ou Dom if (typeof source === "string") { this.source = Parser.parse(source); } else if (source !== null) { this.source = source; } // INFO // on applique les styles par defaut definis avec l'option defaultStyle // sauf sur les features qui possèdent des extensions. // les features avec extensions sont traité au préalable // dans la callback des options : readExtensions var self = this; features.forEach(function (feature, index, array) { feature.setId(index + 1); // HACK : enregistrement de la description de la balise 'desc' du format GPX var value = feature.getProperties().desc; if (value) { feature.setProperties({ description : value }); } var featureStyleFunction = feature.getStyleFunction(); if (!featureStyleFunction) { var styleFunction = Styling.defineStyleFunctionByDefault(self.options.defaultStyle); if (styleFunction) { feature.setStyle(styleFunction); } } }); return features; } /** * Write Extend Styles for Features. * This function overloads ol.format.GPX.writeFeatures ... * * @see olGPX#writeFeatures * @param {Features[]} features - Features. * @param {Object} options - Options. * * @returns {String} Result or null. */ writeFeatures (features, options) { // INFO // il n'est pas possible de surcharger les parsers d'OpenLayers (private), // on decide de (re)parser la sortie d'OpenLayers afin d'y placer les balises // d'extensions // on met à jour les properties de styles features.forEach(function (feature, index, array) { // HACK : enregistrement de la description dans la balise 'desc' du format GPX var value = feature.getProperties().description; if (value) { feature.setProperties({ desc : value }); } Styling.definePropertiesFromStyle(feature); // HACK : Le type surfacique n'existe pas au format GPX, // on doit la transformer en un lineaire. // Par contre, on garde un trace de la transformation : // * le style surfacique // * le type de geometrie initiale var type = feature.getGeometry().getType(); if (type === "Polygon") { // creation d'une copie pour ne pas modifier les features de carte var fp = feature.clone(); fp.set("type", type); fp.setGeometry(new LineString(feature.getGeometry().getCoordinates())); features.push(fp); // feature à supprimer de l'export array.splice(index, 1); } else if (type === "MultiPolygon") { // creation d'une copie pour ne pas modifier les features de carte var fm = feature.clone(); fm.set("type", type); fm.setGeometry(new MultiLineString(feature.getGeometry().getCoordinates())); features.push(fm); // feature à supprimer de l'export array.splice(index, 1); } }); // tri des features en fonction de la balise "number" || "id" || "order" if (this.options.orderBy !== undefined) { var key = this.options.orderBy; if (key) { var sortFct = function (a, b) { var cmpA = a.get(key) || 0; var cmpB = b.get(key) || 0; return cmpA.toString().localeCompare(cmpB.toString(), undefined, { numeric : true }); }; features.sort(sortFct); } } // nodes var gpxNode = super.writeFeaturesNode(features, options); if (gpxNode === null) { return null; } // on ajoute les extensions à la racine pour les metadonnées de calcul if (this.options.hasOwnProperty("extensions")) { this.writeRootExtensions_(gpxNode, this.options.extensions); } // INFO // à chaque fois qu'un style est trouvé dans un feature, // on appelle la fonction d'insertion des balises extensions dans le DOM. this.processExtensions_(gpxNode, features, { extensions : this.writeExtensions_ }); // dom -> string var gpxStringExtended = Parser.toString(gpxNode); if (!gpxStringExtended) { return null; } // format string var gpxStringFormatted = Parser.format(gpxStringExtended); if (gpxStringFormatted === "") { return null; } return gpxStringFormatted; } /** * Callback to read extensions from options : readExtensions * * @param {Feature} feature - ... * @param {*} node - ... */ readExtensions (feature, node) { var _node = node; // recherche de la properties de type Node ou Element // si le node n'est pas renseigné... if (!node) { var props = feature.getProperties(); for (const key in props) { if (Object.hasOwnProperty.call(props, key)) { const element = props[key]; if (element instanceof Node) { _node = element; break; } } } } if (!_node) { // eslint-disable-next-line no-console console.warn("node not found !"); return; } // ex. de nodes : // <extensions> // <marker-size>medium</marker-size> // <marker-symbol></marker-symbol> // <marker-color>#ffffff</marker-color> // </extensions> for (var index = 0; index < _node.childNodes.length; index++) { var element = _node.childNodes[index]; if (element.nodeType === 1) { feature.set(element.nodeName, element.textContent); } } // cas particulier du format GPX : // il n'existe pas de surfacique sur ce format, mais il est possible de forcer // la transformation en polygone pour des besoins particuliers de visualisation Styling.APPLY_CONVERT_GEOM_GPX = true; var style = Styling.defineStyleFromProperties(feature); if (style) { feature.setStyle(style); } } /** * ... * @param {*} key ... * @returns {Object} json * @todo */ readRootExtensions (key) { var value = {}; // Rechercher : // <metadata> // <extensions xmlns="http://www.w3.org/1999/xhtml"> // <data name="geoportail:compute">{...}</data> // </extensions> // </metadata> var firstNodeLevelGpx = this.source.childNodes[0]; // gpx var searchChildNodesMeta = firstNodeLevelGpx.childNodes; // search metadata for (var k = 0; k < searchChildNodesMeta.length; k++) { var nodeMeta = searchChildNodesMeta[k]; if (nodeMeta.nodeName === "metadata") { var searchChildNodesExt = nodeMeta.childNodes; // search extensions for (var i = 0; i < searchChildNodesExt.length; i++) { var nodeExt = searchChildNodesExt[i]; if (nodeExt.nodeName === "extensions") { var searchChildNodesData = nodeExt.childNodes; // search data for (var j = 0; j < searchChildNodesData.length; j++) { var nodeData = searchChildNodesData[j]; if (nodeData.nodeName === "data") { var name = nodeData.attributes[0]; if (name && name.nodeName === "name") { if (name.nodeValue === key) { value = JSON.parse(nodeData.textContent); break; } } } } } } } } return value; } /** * ... * * @param {*} doc - ... * @param {*} extensions - ... * @param {Boolean} [xml=false] - write tag xml or json */ writeRootExtensions_ (doc, extensions, xml) { // TODO namespace ? var metadata = document.createElement("metadata"); var extensionsRoot = document.createElement("extensions"); // INFO // convert JSON to XML (dom) // * type string : // { typestring: "string" } -> <typestring>string</typestring> // // * type object : // { typeobject: { typestring1: "string", typestring2: "string" } } // -> <typeobject> // <typestring1>string</typestring1> // <typestring2>string</typestring2> // </typeobject> // // * type array : // { typearray : ["item1", "item2"] } // -> <typearray type="array" index=2> // <value>item1</value> // <value>item2</value> // </typearray> // // * type array of array // -> <typearray type="array" index=1> // <value type="array" index=2> // <value>1</value> // <value>2</value> // </value> // </typearray> // // * type array of object // -> <typearray type="array" index=2> // <value> // <typestring1>string</typestring1> // <typestring2>string</typestring2> // </value> // <value> // <typestring1>string</typestring1> // <typestring2>string</typestring2> // </value> // </typearray> function toDOM (node, json) { for (const key in json) { if (Object.hasOwnProperty.call(json, key)) { var element = json[key] || ""; // au cas où... var tag = document.createElement(key); // eslint-disable-next-line valid-typeof if (typeof element === "string" || typeof element === "number") { tag.innerHTML = element; node.appendChild(tag); } else if (element instanceof Array) { tag.setAttribute("type", "array"); tag.setAttribute("index", element.length); for (let index = 0; index < element.length; index++) { var item = element[index] || ""; // au cas où... var n = document.createElement("value"); if (typeof item === "string" || typeof item === "number") { n.innerHTML = item; tag.appendChild(n); } else if (item instanceof Array) { n.setAttribute("type", "array"); n.setAttribute("index", item.length); for (let i = 0; i < item.length; i++) { var value = item[i] || ""; // au cas où... var k = document.createElement("value"); if (typeof value === "string" || typeof value === "number") { k.innerHTML = value; n.appendChild(k); } } tag.appendChild(n); } else if (item instanceof Object) { tag.appendChild(toDOM(n, item)); } else { // "Unknown element !" } } node.appendChild(tag); } else if (element instanceof Object) { node.appendChild(toDOM(tag, element)); } else { // "Unknown element !" } } } return node; } if (xml) { // structure xml toDOM(extensionsRoot, extensions); } else { // structure json par defaut // ex. // <metadata> // <extensions xmlns="http://www.w3.org/1999/xhtml"> // <data name="geoportail:compute">{...}</data> // </extensions> // </metadata> for (const key in extensions) { if (Object.hasOwnProperty.call(extensions, key)) { const value = extensions[key]; var dataElement = document.createElement("data"); dataElement.setAttribute("name", key); var data = document.createTextNode(JSON.stringify(value)); dataElement.appendChild(data); extensionsRoot.appendChild(dataElement); } } } metadata.appendChild(extensionsRoot); // insertion en 1ere place ! var firstChild = doc.firstChild; doc.insertBefore(metadata, firstChild); } /** * ... * * @param {Feature} feature - ... * @param {HTMLElement} node - ... * @private */ writeExtensions_ (feature, node) { // creation du DOM var extensionsNode = document.createElementNS(node.parentNode.namespaceURI, "extensions"); Styling.getListTags().forEach(key => { if (feature.get(key)) { var extension = document.createElementNS(node.parentNode.namespaceURI, key); extension.innerHTML = feature.get(key); extensionsNode.appendChild(extension); } }); node.appendChild(extensionsNode); } /** * ... * * @param {HTMLElement} doc - ... * @param {Feature[]} features - ... * @param {Object} actions - ... * @private */ processExtensions_ (doc, features, actions) { // INFO // OpenLayers ne gère pas tous les tags du format GPX : ex. metadata // Liste des tags : // * wpt // * rte // * trk // On peut y placer nos balises extensions. var index = -1; var nodes = doc.childNodes; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; switch (node.nodeName) { case "wpt": case "rte": case "trk": index++; var feature = features[index]; var style = feature.getStyle(); if (style) { var fct = actions.extensions; if (fct && typeof fct === "function") { fct(feature, node); } } break; case "metadata": break; default: // on ne devrait jamais passer à ce niveau !? // eslint-disable-next-line no-console console.warn("nodename unknown :", node.nodeName); break; } } } }; export default GPX; // Expose GPX as ol.source.GPXExtended. (for a build bundle) if (window.ol && window.ol.format) { window.ol.format.GPXExtended = GPX; }