/* 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);
},
);