UNPKG

@geoext/geoext

Version:

GeoExt is Open Source and enables building desktop-like GIS applications through the web. It is a JavaScript framework that combines the GIS functionality of OpenLayers with the user interface savvy of the ExtJS library provided by Sencha.

672 lines (625 loc) 22.6 kB
/* Copyright (c) 2015-present The Open Source Geospatial Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /** * A serializer for layers that have an `ol.source.Vector` source. * * This class is heavily inspired by the excellent `ngeo` Print service class: * [camptocamp/ngeo](https://github.com/camptocamp/ngeo). * * Additionally some utility methods were borrowed from the color class of the * [google/closure-library](https://github.com/google/closure-library). * * @class GeoExt.data.serializer.Vector */ Ext.define( 'GeoExt.data.serializer.Vector', { extend: 'GeoExt.data.serializer.Base', mixins: ['GeoExt.mixin.SymbolCheck'], // <debug> symbols: [ 'ol.color.asArray', 'ol.Feature', 'ol.Feature#getGeometry', 'ol.Feature#getStyleFunction', 'ol.format.GeoJSON', 'ol.format.GeoJSON#writeFeatureObject', 'ol.geom.Geometry', 'ol.geom.LineString#getType', 'ol.geom.MultiLineString#getType', 'ol.geom.MultiPoint#getType', 'ol.geom.MultiPolygon#getType', 'ol.geom.Point#getType', 'ol.geom.Polygon#getType', 'ol.layer.Vector#getOpacity', 'ol.layer.Vector#getStyleFunction', 'ol.source.Vector', 'ol.source.Vector#getFeatures', 'ol.style.Circle', 'ol.style.Circle#getRadius', 'ol.style.Circle#getFill', 'ol.style.Fill', 'ol.style.Fill#getColor', 'ol.style.Icon', 'ol.style.Icon#getSrc', 'ol.style.Icon#getRotation', 'ol.style.Stroke', 'ol.style.Stroke#getColor', 'ol.style.Stroke#getWidth', 'ol.style.Style', 'ol.style.Style#getFill', 'ol.style.Style#getImage', 'ol.style.Style#getStroke', 'ol.style.Style#getText', 'ol.style.Text', 'ol.style.Text#getFont', 'ol.style.Text#getOffsetX', 'ol.style.Text#getOffsetY', 'ol.style.Text#getRotation', 'ol.style.Text#getText', 'ol.style.Text#getTextAlign', ], // </debug> inheritableStatics: { /** * The types of styles that mapfish supports. * * @private */ PRINTSTYLE_TYPES: { POINT: 'Point', LINE_STRING: 'LineString', POLYGON: 'Polygon', }, /** * An object that maps an `ol.geom.GeometryType` to a #PRINTSTYLE_TYPES. * * @private */ GEOMETRY_TYPE_TO_PRINTSTYLE_TYPE: {}, // filled once class is defined /** * A fallback serialization of a vector layer that will be used if * the given source e.g. doesn't have any features. * * @private */ FALLBACK_SERIALIZATION: { geoJson: { type: 'FeatureCollection', features: [], }, opacity: 1, style: { 'version': '2', '*': { symbolizers: [ { type: 'point', strokeColor: 'white', strokeOpacity: 1, strokeWidth: 4, strokeDashstyle: 'solid', fillColor: 'red', }, ], }, }, type: 'geojson', }, /** * The prefix we will give to the generated styles. Every feature will * -- once it is serialized -- have a property constructed with * the #FEAT_STYLE_PREFIX and a counter. For every unique combination * of #FEAT_STYLE_PREFIX + i with the value style uid (see #getUid * and #GX_UID_PROPERTY), the layer serialization will also have a * CQL entry with a matching symbolizer: * * { * // … * style: { * "[_gx3_style_0='ext-46']": { * symbolizer: { * // … * } * } * }, * geoJson: { * // … * features: [ * { * // … * properties: { * '_gx3_style_0': 'ext-46' * // … * } * } * ] * } * // … * } * * @private */ FEAT_STYLE_PREFIX: '_gx3_style_', /** * The name / identifier for the uid property that is assigned and read * out in #getUid * * @private */ GX_UID_PROPERTY: '__gx_uid__', /** * A shareable instance of ol.format.GeoJSON to serialize the features. * * @private */ format: new ol.format.GeoJSON(), /** * @inheritdoc */ sourceCls: ol.source.Vector, /** * @inheritdoc */ serialize: function (layer, source, viewRes, map) { const me = this; me.validateSource(source); let extent; if (map) { extent = map.getView().calculateExtent(); } const format = me.format; const geoJsonFeatures = []; const mapfishStyleObject = { version: 2, }; const processFeatures = function (feature) { const geometry = feature.getGeometry(); if (Ext.isEmpty(geometry)) { // no need to encode features with no geometry return; } const geometryType = geometry.getType(); const geojsonFeature = format.writeFeatureObject(feature); // remove parent feature references as they break serialization // later on if ( geojsonFeature.properties && geojsonFeature.properties.parentFeature ) { geojsonFeature.properties.parentFeature = undefined; } let styles = null; let styleFunction = feature.getStyleFunction(); if (Ext.isDefined(styleFunction)) { styles = styleFunction(feature, viewRes); } else { styleFunction = layer.getStyleFunction(); if (Ext.isDefined(styleFunction)) { styles = styleFunction(feature, viewRes); } } if (!Ext.isArray(styles)) { styles = [styles]; } if (!Ext.isEmpty(styles)) { geoJsonFeatures.push(geojsonFeature); if (Ext.isEmpty(geojsonFeature.properties)) { geojsonFeature.properties = {}; } Ext.each(styles, function (style, j) { const styleId = me.getUid(style, geometryType); const featureStyleProp = me.FEAT_STYLE_PREFIX + j; me.encodeVectorStyle( mapfishStyleObject, geometryType, style, styleId, featureStyleProp, ); geojsonFeature.properties[featureStyleProp] = styleId; }); } }; if (extent) { source.forEachFeatureInExtent(extent, processFeatures); } else { Ext.each(source.getFeatures(), processFeatures); } let serialized; // MapFish Print fails if there are no style rules, even if there // are no features either. To work around this, we add a basic // style in the else clause array of GeoJSON features is empty. if (geoJsonFeatures.length > 0) { const geojsonFeatureCollection = { type: 'FeatureCollection', features: geoJsonFeatures, }; serialized = { geoJson: geojsonFeatureCollection, opacity: layer.getOpacity(), style: mapfishStyleObject, type: 'geojson', }; } else { serialized = this.FALLBACK_SERIALIZATION; } return serialized; }, /** * Encodes an ol.style.Style into the passed MapFish style object. * * @param {Object} object The MapFish style object. * @param {ol.geom.GeometryType} geometryType The type of the GeoJSON * geometry * @param {ol.style.Style} style The style to encode. * @param {string} styleId The id of the style. * @param {string} featureStyleProp Feature style property name. * @private */ encodeVectorStyle: function ( object, geometryType, style, styleId, featureStyleProp, ) { const me = this; const printTypes = me.PRINTSTYLE_TYPES; const printStyleLookup = me.GEOMETRY_TYPE_TO_PRINTSTYLE_TYPE; if (!Ext.isDefined(printStyleLookup[geometryType])) { // unsupported geometry type return; } const styleType = printStyleLookup[geometryType]; const key = '[' + featureStyleProp + " = '" + styleId + "']"; if (Ext.isDefined(object[key])) { // do nothing if we already have a style object for this CQL // rule return; } const styleObject = { symbolizers: [], }; object[key] = styleObject; const fillStyle = style.getFill(); const imageStyle = style.getImage(); const strokeStyle = style.getStroke(); const textStyle = style.getText(); const hasFillStyle = !Ext.isEmpty(fillStyle); const hasImageStyle = !Ext.isEmpty(imageStyle); const hasStrokeStyle = !Ext.isEmpty(strokeStyle); const hasTextStyle = !Ext.isEmpty(textStyle); const POLYTYPE = printTypes.POLYGON; const LINETYPE = printTypes.LINE_STRING; const POINTTYPE = printTypes.POINT; if (styleType === POLYTYPE && hasFillStyle) { me.encodeVectorStylePolygon( styleObject.symbolizers, fillStyle, strokeStyle, ); } else if (styleType === LINETYPE && hasStrokeStyle) { me.encodeVectorStyleLine(styleObject.symbolizers, strokeStyle); } else if (styleType === POINTTYPE && hasImageStyle) { me.encodeVectorStylePoint(styleObject.symbolizers, imageStyle); } // this can be there regardless of type if (hasTextStyle) { me.encodeTextStyle(styleObject.symbolizers, textStyle); } }, /** * Encodes an `ol.style.Fill` and an optional `ol.style.Stroke` and adds * it to the passed symbolizers array. * * @param {Array<Object>} symbolizers Array of MapFish Print symbolizers. * @param {ol.style.Fill} fillStyle Fill style. * @param {ol.style.Stroke} strokeStyle Stroke style. May be null. * @private */ encodeVectorStylePolygon: function (symbolizers, fillStyle, strokeStyle) { const symbolizer = { type: 'polygon', }; this.encodeVectorStyleFill(symbolizer, fillStyle); if (strokeStyle !== null) { this.encodeVectorStyleStroke(symbolizer, strokeStyle); } symbolizers.push(symbolizer); }, /** * Encodes an `ol.style.Stroke` and adds it to the passed symbolizers * array. * * @param {Array<Object>} symbolizers Array of MapFish Print symbolizers. * @param {ol.style.Stroke} strokeStyle Stroke style. * @private */ encodeVectorStyleLine: function (symbolizers, strokeStyle) { const symbolizer = { type: 'line', }; this.encodeVectorStyleStroke(symbolizer, strokeStyle); symbolizers.push(symbolizer); }, /** * Encodes an `ol.style.Image` and adds it to the passed symbolizers * array. * * @param {Array<Object>} symbolizers Array of MapFish Print symbolizers. * @param {ol.style.Image} imageStyle Image style. * @private */ encodeVectorStylePoint: function (symbolizers, imageStyle) { let symbolizer; if (imageStyle instanceof ol.style.Circle) { symbolizer = { type: 'point', }; symbolizer.pointRadius = imageStyle.getRadius(); const fillStyle = imageStyle.getFill(); if (fillStyle !== null) { this.encodeVectorStyleFill(symbolizer, fillStyle); } const strokeStyle = imageStyle.getStroke(); if (strokeStyle !== null) { this.encodeVectorStyleStroke(symbolizer, strokeStyle); } } else if (imageStyle instanceof ol.style.Icon) { const src = imageStyle.getSrc(); if (Ext.isDefined(src)) { const img = imageStyle.getImage(window.devicePixelRatio || 1); const canvas = document.createElement('canvas'); canvas.width = img.naturalWidth || img.width; canvas.height = img.naturalHeight || img.height; canvas.getContext('2d').drawImage(img, 0, 0); const format = 'image/' + src.match(/\.(\w+)$/)[1]; symbolizer = { type: 'point', externalGraphic: canvas.toDataURL(), graphicFormat: format, }; const rotation = imageStyle.getRotation(); if (rotation !== 0) { const degreesRotation = (rotation * 180) / Math.PI; symbolizer.rotation = degreesRotation; } } } if (Ext.isDefined(symbolizer)) { symbolizers.push(symbolizer); } }, /** * Encodes an `ol.style.Text` and adds it to the passed symbolizers * array. * * @param {Array<Object>} symbolizers Array of MapFish Print symbolizers. * @param {ol.style.Text} textStyle Text style. * @private */ encodeTextStyle: function (symbolizers, textStyle) { const symbolizer = { type: 'Text', }; const label = textStyle.getText(); if (!Ext.isDefined(label)) { // do not encode undefined labels return; } symbolizer.label = label; const labelAlign = textStyle.getTextAlign(); if (Ext.isDefined(labelAlign)) { symbolizer.labelAlign = labelAlign; } const labelRotation = textStyle.getRotation(); if (Ext.isDefined(labelRotation)) { // Mapfish Print expects a string to rotate text const strRotationDeg = (labelRotation * 180) / Math.PI + ''; symbolizer.labelRotation = strRotationDeg; } const offsetX = textStyle.getOffsetX(); const offsetY = textStyle.getOffsetY(); if (offsetX) { symbolizer.labelXOffset = offsetX; } if (offsetY) { symbolizer.labelYOffset = -offsetY; } const fontStyle = textStyle.getFont(); if (Ext.isDefined(fontStyle)) { const el = document.createElement('span'); el.style.font = fontStyle; symbolizer.fontWeight = el.style.fontWeight; symbolizer.fontSize = el.style.fontSize; symbolizer.fontFamily = el.style.fontFamily; symbolizer.fontStyle = el.style.fontStyle; } const strokeStyle = textStyle.getStroke(); if (strokeStyle !== null && strokeStyle.getColor()) { const strokeColor = strokeStyle.getColor(); const strokeColorRgba = ol.color.asArray(strokeColor); symbolizer.haloColor = this.rgbArrayToHex(strokeColorRgba); symbolizer.haloOpacity = strokeColorRgba[3]; const width = strokeStyle.getWidth(); if (Ext.isDefined(width)) { symbolizer.haloRadius = width; } } const fillStyle = textStyle.getFill(); if (fillStyle !== null && fillStyle.getColor()) { const fillColorRgba = ol.color.asArray(fillStyle.getColor()); symbolizer.fontColor = this.rgbArrayToHex(fillColorRgba); } // Mapfish Print allows offset only if labelAlign is defined. if (Ext.isDefined(symbolizer.labelAlign)) { symbolizer.labelXOffset = textStyle.getOffsetX(); // Mapfish uses the opposite direction of OpenLayers for y // axis, so the minus sign is required for the y offset to // be identical. symbolizer.labelYOffset = -textStyle.getOffsetY(); } symbolizers.push(symbolizer); }, /** * Encode the passed `ol.style.Fill` into the passed symbolizer. * * @param {Object} symbolizer MapFish Print symbolizer. * @param {ol.style.Fill} fillStyle Fill style. * @private */ encodeVectorStyleFill: function (symbolizer, fillStyle) { const fillColor = fillStyle.getColor(); if (fillColor !== null) { const fillColorRgba = ol.color.asArray(fillColor); symbolizer.fillColor = this.rgbArrayToHex(fillColorRgba); symbolizer.fillOpacity = fillColorRgba[3]; } }, /** * Encode the passed `ol.style.Stroke` into the passed symbolizer. * * @param {Object} symbolizer MapFish Print symbolizer. * @param {ol.style.Stroke} strokeStyle Stroke style. * @private */ encodeVectorStyleStroke: function (symbolizer, strokeStyle) { const strokeColor = strokeStyle.getColor(); if (strokeColor !== null) { const strokeColorRgba = ol.color.asArray(strokeColor); symbolizer.strokeColor = this.rgbArrayToHex(strokeColorRgba); symbolizer.strokeOpacity = strokeColorRgba[3]; } const strokeWidth = strokeStyle.getWidth(); if (Ext.isDefined(strokeWidth)) { symbolizer.strokeWidth = strokeWidth; } }, /** * Takes a hex value and prepends a zero if it's a single digit. * Taken from https://github.com/google/closure-library color.js-file. * It is called `prependZeroIfNecessaryHelper` there. * * @param {string} hex Hex value to prepend if single digit. * @return {string} The hex value prepended with zero if it was single * digit, otherwise the same value that was passed in. * @private */ padHexValue: function (hex) { return hex.length === 1 ? '0' + hex : hex; }, /** * Converts a color from RGB to hex representation. * Taken from https://github.com/google/closure-library color.js-file. * * @param {number} r Amount of red, int between 0 and 255. * @param {number} g Amount of green, int between 0 and 255. * @param {number} b Amount of blue, int between 0 and 255. * @return {string} The passed color in hex representation. * @private */ rgbToHex: function (r, g, b) { r = Number(r); g = Number(g); b = Number(b); if ( isNaN(r) || r < 0 || r > 255 || isNaN(g) || g < 0 || g > 255 || isNaN(b) || b < 0 || b > 255 ) { Ext.raise( '"(' + r + ',' + g + ',' + b + '") is not a valid ' + ' RGB color', ); } const hexR = this.padHexValue(r.toString(16)); const hexG = this.padHexValue(g.toString(16)); const hexB = this.padHexValue(b.toString(16)); return '#' + hexR + hexG + hexB; }, /** * Converts a color from RGB to hex representation. * Taken from https://github.com/google/closure-library color.js-file * * @param {Array<number>} rgbArr An array with three numbers representing * red, green and blue. * @return {string} The passed color in hex representation. * @private */ rgbArrayToHex: function (rgbArr) { return this.rgbToHex(rgbArr[0], rgbArr[1], rgbArr[2]); }, /** * Returns a unique id for this object. The object is assigned a new * property #GX_UID_PROPERTY and modified in place if this hasn't * happened in a previous call. * * @param {Object} obj The object to get the uid of. * @param {string} geometryType The geometryType for the style. * @return {string} The uid of the object. * @private */ getUid: function (obj, geometryType) { if (!Ext.isObject(obj)) { Ext.raise('Cannot get uid of non-object.'); } let key = this.GX_UID_PROPERTY; if (geometryType) { key += '-' + geometryType; } if (!Ext.isDefined(obj[key])) { obj[key] = Ext.id(); } return obj[key]; }, }, }, function (cls) { // This is ol.geom.GeometryType, from // https://github.com/openlayers/ol3/blob/master/src/ol/geom/geometry.js const olGeomTypes = { POINT: 'Point', LINE_STRING: 'LineString', LINEAR_RING: 'LinearRing', POLYGON: 'Polygon', MULTI_POINT: 'MultiPoint', MULTI_LINE_STRING: 'MultiLineString', MULTI_POLYGON: 'MultiPolygon', GEOMETRY_COLLECTION: 'GeometryCollection', CIRCLE: 'Circle', }; // The supported types for the print const printStyleTypes = cls.PRINTSTYLE_TYPES; // a map that connect ol geometry types to their mapfish equivalent; // Please note that not all ol geometry types can be serialized. const geom2print = {}; geom2print[olGeomTypes.POINT] = printStyleTypes.POINT; geom2print[olGeomTypes.MULTI_POINT] = printStyleTypes.POINT; geom2print[olGeomTypes.LINE_STRING] = printStyleTypes.LINE_STRING; geom2print[olGeomTypes.MULTI_LINE_STRING] = printStyleTypes.LINE_STRING; geom2print[olGeomTypes.POLYGON] = printStyleTypes.POLYGON; geom2print[olGeomTypes.MULTI_POLYGON] = printStyleTypes.POLYGON; cls.GEOMETRY_TYPE_TO_PRINTSTYLE_TYPE = geom2print; // Register this serializer via the inherited method `register`. cls.register(cls); }, );