diff --git a/examples/data/countries.sld b/examples/data/countries.sld new file mode 100644 index 0000000000..9ae5eebea3 --- /dev/null +++ b/examples/data/countries.sld @@ -0,0 +1,43 @@ + + + + countries + + countries + A sample style for countries + 1 + A sample style for countries + + name + + Sample + Sample + + + #ff0000 + 0.6 + + + #00FF00 + 0.5 + 4 + + + + + 20000000 + + + name + + + Arial + 10 + Normal + + + + + + + diff --git a/examples/vector-layer-sld.html b/examples/vector-layer-sld.html new file mode 100644 index 0000000000..62900c20ac --- /dev/null +++ b/examples/vector-layer-sld.html @@ -0,0 +1,56 @@ + + + + + + + + + + + Vector layer with styling from SLD example + + + + + +
+ +
+
+
+
+
+ +
+ +
+

Vector layer example

+

Example of a countries vector layer with country labels at higher zoom levels, styling info coming from SLD.

+
+

See the vector-layer-sld.js source to see how this is done.

+
+
vector, geojson, style, SLD, Styled Layer Descriptor
+
+ +
+ +
+ + + + + + diff --git a/examples/vector-layer-sld.js b/examples/vector-layer-sld.js new file mode 100644 index 0000000000..585cc66c09 --- /dev/null +++ b/examples/vector-layer-sld.js @@ -0,0 +1,52 @@ +goog.require('ol.Map'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.control'); +goog.require('ol.control.ScaleLine'); +goog.require('ol.layer.Tile'); +goog.require('ol.layer.Vector'); +goog.require('ol.parser.GeoJSON'); +goog.require('ol.parser.ogc.SLD'); +goog.require('ol.source.MapQuestOpenAerial'); +goog.require('ol.source.Vector'); + + +var raster = new ol.layer.Tile({ + source: new ol.source.MapQuestOpenAerial() +}); + +var xhr = new XMLHttpRequest(); +xhr.open('GET', 'data/countries.sld', true); + + +/** + * onload handler for the XHR request. + */ +xhr.onload = function() { + if (xhr.status == 200) { + var map = new ol.Map({ + controls: ol.control.defaults().extend([ + new ol.control.ScaleLine() + ]), + layers: [raster], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [0, 0], + zoom: 1 + }) + }); + var units = map.getView().getProjection().getUnits(); + var sld = new ol.parser.ogc.SLD().read(xhr.responseText, units); + var style = sld.namedLayers['countries'].userStyles[0]; + var vector = new ol.layer.Vector({ + source: new ol.source.Vector({ + parser: new ol.parser.GeoJSON(), + url: 'data/countries.geojson' + }), + style: style + }); + map.getLayers().push(vector); + } +}; +xhr.send(); diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 5031d9aad0..d416c8b63e 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -583,6 +583,18 @@ * @todo stability experimental */ + /** + * @typedef {Object} ol.parser.SLDReadOptions + * @property {ol.proj.Units} units The units to use in scale to resolution + * calculations. + */ + + /** + * @typedef {Object} ol.parser.SLDWriteOptions + * @property {ol.proj.Units} units The units to use in resolution to scale + * calculations. + */ + /** * @typedef {Object} ol.source.BingMapsOptions * @property {string|undefined} culture Culture code. Default is `en-us`. @@ -803,6 +815,8 @@ * a value is provided, the rule will apply at resolutions greater than or * equal to this value. * @property {Array.|undefined} symbolizers Symbolizers. + * @property {string|undefined} name Name. + * @property {string|undefined} title Title. * @todo stability experimental */ @@ -832,6 +846,8 @@ * @property {Array.|undefined} symbolizers Symbolizers * (that apply if no rules are provided or where none of the provided rules * apply). + * @property {string|undefined} name Name. + * @property {string|undefined} title Title. * @todo stability experimental */ diff --git a/src/ol/parser/ogc/filterparser_v1.js b/src/ol/parser/ogc/filterparser_v1.js index a030b9bf78..f0a21995ab 100644 --- a/src/ol/parser/ogc/filterparser_v1.js +++ b/src/ol/parser/ogc/filterparser_v1.js @@ -508,7 +508,14 @@ ol.parser.ogc.Filter_v1.prototype.write = function(filter) { */ ol.parser.ogc.Filter_v1.prototype.writeOgcExpression = function(expr, node) { if (expr instanceof ol.expr.Call) { - this.writeNode('Function', expr, null, node); + if (ol.expr.isLibCall(expr) === ol.expr.functions.CONCAT) { + var args = expr.getArgs(); + for (var i = 0, ii = args.length; i < ii; ++i) { + this.writeOgcExpression(args[i], node); + } + } else { + this.writeNode('Function', expr, null, node); + } } else if (expr instanceof ol.expr.Literal) { this.writeNode('Literal', expr, null, node); } else if (expr instanceof ol.expr.Identifier) { diff --git a/src/ol/parser/ogc/sldparser.js b/src/ol/parser/ogc/sldparser.js new file mode 100644 index 0000000000..c9db020b75 --- /dev/null +++ b/src/ol/parser/ogc/sldparser.js @@ -0,0 +1,27 @@ +goog.provide('ol.parser.ogc.SLD'); +goog.require('ol.parser.ogc.SLD_v1_0_0'); +goog.require('ol.parser.ogc.Versioned'); + + +/** + * @define {boolean} Whether to enable SLD version 1.0.0. + */ +ol.ENABLE_SLD_1_0_0 = true; + + + +/** + * @constructor + * @param {Object=} opt_options Options which will be set on this object. + * @extends {ol.parser.ogc.Versioned} + */ +ol.parser.ogc.SLD = function(opt_options) { + opt_options = opt_options || {}; + opt_options['defaultVersion'] = '1.0.0'; + this.parsers = {}; + if (ol.ENABLE_SLD_1_0_0) { + this.parsers['v1_0_0'] = ol.parser.ogc.SLD_v1_0_0; + } + goog.base(this, opt_options); +}; +goog.inherits(ol.parser.ogc.SLD, ol.parser.ogc.Versioned); diff --git a/src/ol/parser/ogc/sldparser_v1.js b/src/ol/parser/ogc/sldparser_v1.js new file mode 100644 index 0000000000..a1516d8c5a --- /dev/null +++ b/src/ol/parser/ogc/sldparser_v1.js @@ -0,0 +1,732 @@ +goog.provide('ol.parser.ogc.SLD_v1'); +goog.require('goog.asserts'); +goog.require('goog.dom.xml'); +goog.require('goog.object'); +goog.require('ol.expr.Literal'); +goog.require('ol.parser.XML'); +goog.require('ol.parser.ogc.Filter_v1_0_0'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Icon'); +goog.require('ol.style.Rule'); +goog.require('ol.style.Shape'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); +goog.require('ol.style.Text'); + + + +/** + * Read Styled Layer Descriptor (SLD). + * + * @constructor + * @extends {ol.parser.XML} + */ +ol.parser.ogc.SLD_v1 = function() { + this.defaultNamespaceURI = 'http://www.opengis.net/sld'; + this.readers = {}; + this.readers[this.defaultNamespaceURI] = { + 'StyledLayerDescriptor': function(node, sld) { + sld.version = node.getAttribute('version'); + this.readChildNodes(node, sld); + }, + 'Name': function(node, obj) { + obj.name = this.getChildValue(node); + }, + 'Title': function(node, obj) { + obj.title = this.getChildValue(node); + }, + 'Abstract': function(node, obj) { + obj.description = this.getChildValue(node); + }, + 'NamedLayer': function(node, sld) { + var layer = { + userStyles: [], + namedStyles: [] + }; + this.readChildNodes(node, layer); + sld.namedLayers[layer.name] = layer; + }, + 'NamedStyle': function(node, layer) { + layer.namedStyles.push( + this.getChildValue(node.firstChild) + ); + }, + 'UserStyle': function(node, layer) { + var obj = {rules: []}; + this.featureTypeCounter = -1; + this.readChildNodes(node, obj); + layer.userStyles.push(new ol.style.Style(obj)); + }, + 'FeatureTypeStyle': function(node, style) { + ++this.featureTypeCounter; + var obj = { + rules: style.rules + }; + this.readChildNodes(node, obj); + }, + 'Rule': function(node, obj) { + var config = {symbolizers: []}; + this.readChildNodes(node, config); + var rule = new ol.style.Rule(config); + obj.rules.push(rule); + }, + 'ElseFilter': function(node, rule) { + rule.elseFilter = true; + }, + 'MinScaleDenominator': function(node, rule) { + rule.minResolution = this.getResolutionFromScaleDenominator_( + parseFloat(this.getChildValue(node))); + }, + 'MaxScaleDenominator': function(node, rule) { + rule.maxResolution = this.getResolutionFromScaleDenominator_( + parseFloat(this.getChildValue(node))); + }, + 'TextSymbolizer': function(node, rule) { + var config = {}; + this.readChildNodes(node, config); + config.color = goog.isDef(config.fill) ? config.fill.fillColor : + ol.parser.ogc.SLD_v1.defaults_.fontColor; + delete config.fill; + config.zIndex = this.featureTypeCounter; + rule.symbolizers.push( + new ol.style.Text(/** @type {ol.style.TextOptions} */(config)) + ); + }, + 'Label': function(node, symbolizer) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + var value = ogcreaders._expression.call(this, node); + if (value) { + symbolizer.text = value; + } + }, + 'Font': function(node, symbolizer) { + this.readChildNodes(node, symbolizer); + }, + 'Halo': function(node, symbolizer) { + var obj = {}; + this.readChildNodes(node, obj); + symbolizer.stroke = new ol.style.Stroke({ + color: goog.isDef(obj.fill.fillColor) ? obj.fill.fillColor : + ol.parser.ogc.SLD_v1.defaults_.haloColor, + width: goog.isDef(obj.haloRadius) ? obj.haloRadius * 2 : + ol.parser.ogc.SLD_v1.defaults_.haloRadius, + opacity: goog.isDef(obj.fill.fillOpacity) ? obj.fill.fillOpacity : + ol.parser.ogc.SLD_v1.defaults_.haloOpacity + }); + }, + 'Radius': function(node, symbolizer) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + var radius = ogcreaders._expression.call(this, node); + goog.asserts.assertInstanceof(radius, ol.expr.Literal, + 'radius expected to be an ol.expr.Literal'); + if (goog.isDef(radius)) { + symbolizer.haloRadius = radius.getValue(); + } + }, + 'LineSymbolizer': function(node, rule) { + var config = {}; + this.readChildNodes(node, config); + config.zIndex = this.featureTypeCounter; + rule.symbolizers.push( + new ol.style.Stroke(config) + ); + }, + 'PolygonSymbolizer': function(node, rule) { + var config = {}; + this.readChildNodes(node, config); + config.zIndex = this.featureTypeCounter; + if (goog.isDef(config.fill)) { + var fill = { + color: config.fill.fillColor.getValue(), + opacity: goog.isDef(config.fill.fillOpacity) ? + config.fill.fillOpacity : + ol.parser.ogc.SLD_v1.defaults_.fillOpacity + }; + rule.symbolizers.push( + new ol.style.Fill(fill) + ); + delete config.fill; + } + if (goog.isDef(config.stroke)) { + var stroke = { + color: config.stroke.strokeColor.getValue(), + opacity: goog.isDef(config.stroke.strokeOpacity) ? + config.stroke.strokeOpacity : + ol.parser.ogc.SLD_v1.defaults_.strokeOpacity, + width: goog.isDef(config.stroke.strokeWidth) ? + config.stroke.strokeWidth : + ol.parser.ogc.SLD_v1.defaults_.strokeWidth + }; + rule.symbolizers.push( + new ol.style.Stroke(stroke) + ); + delete config.stroke; + } + + }, + 'PointSymbolizer': function(node, rule) { + var config = {}; + this.readChildNodes(node, config); + config.zIndex = this.featureTypeCounter; + if (config.fill) { + var fillConfig = { + color: goog.isDef(config.fill.fillColor) ? + config.fill.fillColor : + ol.parser.ogc.SLD_v1.defaults_.fillColor, + opacity: goog.isDef(config.fill.fillOpacity) ? + config.fill.fillOpacity : + ol.parser.ogc.SLD_v1.defaults_.fillOpacity + }; + config.fill = new ol.style.Fill(fillConfig); + } + if (config.stroke) { + var strokeConfig = { + color: goog.isDef(config.stroke.strokeColor) ? + config.stroke.strokeColor : + ol.parser.ogc.SLD_v1.defaults_.strokeColor, + width: goog.isDef(config.stroke.strokeWidth) ? + config.stroke.strokeWidth : + ol.parser.ogc.SLD_v1.defaults_.strokeWidth, + opacity: goog.isDef(config.stroke.strokeOpacity) ? + config.stroke.strokeOpacity : + ol.parser.ogc.SLD_v1.defaults_.strokeOpacity + }; + config.stroke = new ol.style.Stroke(strokeConfig); + } + var symbolizer; + if (goog.isDef(config.externalGraphic)) { + config.width = config.height = config.size; + symbolizer = new ol.style.Icon( + /** @type {ol.style.IconOptions} */(config)); + } else { + symbolizer = new ol.style.Shape(config); + } + rule.symbolizers.push(symbolizer); + }, + 'Stroke': function(node, symbolizer) { + var stroke = {}; + this.readChildNodes(node, stroke); + symbolizer.stroke = stroke; + }, + 'Fill': function(node, symbolizer) { + var fill = {}; + this.readChildNodes(node, fill); + symbolizer.fill = fill; + }, + 'CssParameter': function(node, symbolizer) { + var cssProperty = node.getAttribute('name'); + var symProperty = ol.parser.ogc.SLD_v1.cssMap_[cssProperty]; + if (symProperty) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + symbolizer[symProperty] = ogcreaders._expression.call(this, node); + } + }, + 'Graphic': function(node, symbolizer) { + var graphic = {}; + // painter's order not respected here, clobber previous with next + this.readChildNodes(node, graphic); + // directly properties with names that match symbolizer properties + var properties = [ + 'stroke', 'fill', 'rotation', 'opacity' + ]; + var prop, value; + for (var i = 0, ii = properties.length; i < ii; ++i) { + prop = properties[i]; + value = graphic[prop]; + if (goog.isDef(value)) { + symbolizer[prop] = value; + } + } + // set other generic properties with specific graphic property names + if (goog.isDef(graphic.graphicName)) { + symbolizer.type = graphic.graphicName; + } + if (goog.isDef(graphic.size)) { + var pointRadius = graphic.size / 2; + if (isNaN(pointRadius)) { + // likely a property name + symbolizer.size = graphic.size; + } else { + symbolizer.size = graphic.size / 2; + } + } + if (goog.isDef(graphic.href)) { + symbolizer.url = graphic.href; + } + }, + 'ExternalGraphic': function(node, graphic) { + this.readChildNodes(node, graphic); + }, + 'Mark': function(node, graphic) { + this.readChildNodes(node, graphic); + }, + 'WellKnownName': function(node, graphic) { + graphic.graphicName = this.getChildValue(node); + }, + 'Opacity': function(node, obj) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + obj.opacity = ogcreaders._expression.call(this, node); + }, + 'Size': function(node, obj) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + obj.size = ogcreaders._expression.call(this, node); + }, + 'Rotation': function(node, obj) { + var ogcreaders = this.readers[this.filter_.defaultNamespaceURI]; + obj.rotation = ogcreaders._expression.call(this, node); + }, + 'OnlineResource': function(node, obj) { + obj.href = this.getAttributeNS( + node, 'http://www.w3.org/1999/xlink', 'href' + ); + }, + 'Format': function(node, graphic) { + graphic.graphicFormat = this.getChildValue(node); + } + }; + this.writers = {}; + this.writers[this.defaultNamespaceURI] = { + 'StyledLayerDescriptor': function(sld) { + var node = this.createElementNS('sld:StyledLayerDescriptor'); + node.setAttribute('version', this.version); + if (goog.isDef(sld.name)) { + this.writeNode('Name', sld.name, null, node); + } + if (goog.isDef(sld.title)) { + this.writeNode('Title', sld.title, null, node); + } + if (goog.isDef(sld.description)) { + this.writeNode('Abstract', sld.description, null, node); + } + goog.object.forEach(sld.namedLayers, function(layer) { + this.writeNode('NamedLayer', layer, null, node); + }, this); + return node; + }, + 'Name': function(name) { + var node = this.createElementNS('sld:Name'); + node.appendChild(this.createTextNode(name)); + return node; + }, + 'Title': function(title) { + var node = this.createElementNS('sld:Title'); + node.appendChild(this.createTextNode(title)); + return node; + }, + 'Abstract': function(description) { + var node = this.createElementNS('sld:Abstract'); + node.appendChild(this.createTextNode(description)); + return node; + }, + 'NamedLayer': function(layer) { + var node = this.createElementNS('sld:NamedLayer'); + this.writeNode('Name', layer.name, null, node); + var i, ii; + if (layer.namedStyles) { + for (i = 0, ii = layer.namedStyles.length; i < ii; ++i) { + this.writeNode('NamedStyle', layer.namedStyles[i], null, node); + } + } + if (layer.userStyles) { + for (i = 0, ii = layer.userStyles.length; i < ii; ++i) { + this.writeNode('UserStyle', layer.userStyles[i], null, node); + } + } + return node; + }, + 'NamedStyle': function(name) { + var node = this.createElementNS('sld:NamedStyle'); + this.writeNode('Name', name, null, node); + return node; + }, + 'UserStyle': function(style) { + var node = this.createElementNS('sld:UserStyle'); + var name = style.getName(), title = style.getTitle(); + if (goog.isDef(name)) { + this.writeNode('Name', name, null, node); + } + if (goog.isDef(title)) { + this.writeNode('Title', title, null, node); + } + // TODO sorting by zIndex + this.writeNode('FeatureTypeStyle', style, null, node); + return node; + }, + 'FeatureTypeStyle': function(style) { + var node = this.createElementNS('sld:FeatureTypeStyle'); + var rules = style.getRules(); + for (var i = 0, ii = rules.length; i < ii; ++i) { + this.writeNode('Rule', rules[i], null, node); + } + var symbolizers = style.getSymbolizers(); + if (symbolizers.length > 0) { + // wrap this in a Rule with an ElseFilter + var rule = new ol.style.Rule({symbolizers: symbolizers}); + rule.elseFilter = true; + this.writeNode('Rule', rule, null, node); + } + return node; + }, + 'Rule': function(rule) { + var node = this.createElementNS('sld:Rule'); + var filter = rule.getFilter(); + var name = rule.getName(), title = rule.getTitle(); + if (goog.isDef(name)) { + this.writeNode('Name', name, null, node); + } + if (goog.isDef(title)) { + this.writeNode('Title', title, null, node); + } + if (rule.elseFilter === true) { + this.writeNode('ElseFilter', null, null, node); + } else if (filter) { + this.writeNode('Filter', filter, this.filter_.defaultNamespaceURI, + node); + } + var minResolution = rule.getMinResolution(); + if (minResolution > 0) { + this.writeNode('MinScaleDenominator', + this.getScaleDenominatorFromResolution_(minResolution), + null, node); + } + var maxResolution = rule.getMaxResolution(); + if (maxResolution < Infinity) { + this.writeNode('MaxScaleDenominator', + this.getScaleDenominatorFromResolution_(maxResolution), + null, node); + } + var type, symbolizer, symbolizers = rule.getSymbolizers(); + if (symbolizers) { + for (var i = 0, ii = symbolizers.length; i < ii; ++i) { + symbolizer = symbolizers[i]; + if (symbolizer instanceof ol.style.Text) { + type = 'Text'; + } else if (symbolizer instanceof ol.style.Stroke) { + type = 'Line'; + } else if (symbolizer instanceof ol.style.Fill) { + type = 'Polygon'; + } else if (symbolizer instanceof ol.style.Shape || + symbolizer instanceof ol.style.Icon) { + type = 'Point'; + } + if (goog.isDef(type)) { + this.writeNode(type + 'Symbolizer', symbolizer, null, node); + } + } + } + return node; + }, + 'PointSymbolizer': function(symbolizer) { + var node = this.createElementNS('sld:PointSymbolizer'); + this.writeNode('Graphic', symbolizer, null, node); + return node; + }, + 'Mark': function(symbolizer) { + var node = this.createElementNS('sld:Mark'); + this.writeNode('WellKnownName', symbolizer.getType(), null, node); + var fill = symbolizer.getFill(); + if (!goog.isNull(fill)) { + this.writeNode('Fill', fill, null, node); + } + var stroke = symbolizer.getStroke(); + if (!goog.isNull(stroke)) { + this.writeNode('Stroke', stroke, null, node); + } + return node; + }, + 'WellKnownName': function(name) { + var node = this.createElementNS('sld:WellKnownName'); + node.appendChild(this.createTextNode(name)); + return node; + }, + 'Graphic': function(symbolizer) { + var node = this.createElementNS('sld:Graphic'); + var size; + if (symbolizer instanceof ol.style.Icon) { + this.writeNode('ExternalGraphic', symbolizer, null, node); + var opacity = symbolizer.getOpacity(); + goog.asserts.assertInstanceof(opacity, ol.expr.Literal, + 'Only ol.expr.Literal supported for graphicOpacity'); + this.writeNode('Opacity', opacity.getValue(), null, node); + size = symbolizer.getWidth(); + } else if (symbolizer instanceof ol.style.Shape) { + this.writeNode('Mark', symbolizer, null, node); + size = symbolizer.getSize(); + } + this.writeNode('Size', size, null, node); + if (symbolizer instanceof ol.style.Icon) { + var rotation = symbolizer.getRotation(); + goog.asserts.assertInstanceof(rotation, ol.expr.Literal, + 'Only ol.expr.Literal supported for rotation'); + this.writeNode('Rotation', rotation.getValue(), null, node); + } + return node; + }, + 'PolygonSymbolizer': function(symbolizer) { + var node = this.createElementNS('sld:PolygonSymbolizer'); + this.writeNode('Fill', symbolizer, null, node); + return node; + }, + 'Fill': function(symbolizer) { + var node = this.createElementNS('sld:Fill'); + var fillColor = symbolizer.getColor(); + var msg = 'Only ol.expr.Literal supported for Fill properties'; + goog.asserts.assertInstanceof(fillColor, ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: fillColor.getValue(), + key: 'fillColor' + }, null, node); + var fillOpacity = symbolizer.getOpacity(); + goog.asserts.assertInstanceof(fillOpacity, ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: fillOpacity.getValue(), + key: 'fillOpacity' + }, null, node); + return node; + }, + 'TextSymbolizer': function(symbolizer) { + var node = this.createElementNS('sld:TextSymbolizer'); + var text = symbolizer.getText(); + this.writeNode('Label', text, null, node); + this.writeNode('Font', symbolizer, null, node); + var stroke = symbolizer.getStroke(); + if (!goog.isNull(stroke)) { + this.writeNode('Halo', stroke, null, node); + } + var color = symbolizer.getColor(); + goog.asserts.assertInstanceof(color, ol.expr.Literal, + 'font color should be ol.expr.Literal'); + this.writeNode('Fill', symbolizer, null, node); + return node; + }, + 'Halo': function(symbolizer) { + var node = this.createElementNS('sld:Halo'); + goog.asserts.assertInstanceof(symbolizer.getWidth(), ol.expr.Literal, + 'Only ol.expr.Literal supported for haloRadius'); + this.writeNode('Radius', symbolizer.getWidth().getValue() / 2, null, + node); + this.writeNode('Fill', symbolizer, null, node); + return node; + }, + 'Radius': function(value) { + var node = this.createElementNS('sld:Radius'); + node.appendChild(this.createTextNode(value)); + return node; + }, + 'LineSymbolizer': function(symbolizer) { + var node = this.createElementNS('sld:LineSymbolizer'); + this.writeNode('Stroke', symbolizer, null, node); + return node; + }, + 'Stroke': function(symbolizer) { + var node = this.createElementNS('sld:Stroke'); + var strokeColor = symbolizer.getColor(); + var msg = 'SLD writing of stroke properties only supported ' + + 'for ol.expr.Literal'; + goog.asserts.assertInstanceof(strokeColor, ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: strokeColor.getValue(), + key: 'strokeColor' + }, null, node); + var strokeOpacity = symbolizer.getOpacity(); + goog.asserts.assertInstanceof(strokeOpacity, ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: strokeOpacity.getValue(), + key: 'strokeOpacity' + }, null, node); + var strokeWidth = symbolizer.getWidth(); + goog.asserts.assertInstanceof(strokeWidth, ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: strokeWidth.getValue(), + key: 'strokeWidth' + }, null, node); + // TODO strokeDashstyle and strokeLinecap + return node; + }, + 'CssParameter': function(obj) { + // not handling ogc:expressions for now + var name = ol.parser.ogc.SLD_v1.getCssProperty_(obj.key); + if (goog.isDef(name)) { + var node = this.createElementNS('sld:CssParameter'); + node.setAttribute('name', name); + node.appendChild(this.createTextNode(obj.value)); + return node; + } + }, + 'Label': function(label) { + var node = this.createElementNS('sld:Label'); + this.filter_.writeOgcExpression(label, node); + return node; + }, + 'Font': function(symbolizer) { + var node = this.createElementNS('sld:Font'); + this.writeNode('CssParameter', { + key: 'fontFamily', + value: symbolizer.getFontFamily().getValue() + }, null, node); + this.writeNode('CssParameter', { + key: 'fontSize', + value: symbolizer.getFontSize().getValue() + }, null, node); + // TODO fontWeight and fontStyle + return node; + }, + 'MinScaleDenominator': function(scale) { + var node = this.createElementNS('sld:MinScaleDenominator'); + node.appendChild(this.createTextNode(scale)); + return node; + }, + 'MaxScaleDenominator': function(scale) { + var node = this.createElementNS('sld:MaxScaleDenominator'); + node.appendChild(this.createTextNode(scale)); + return node; + }, + 'Size': function(value) { + var node = this.createElementNS('sld:Size'); + this.filter_.writeOgcExpression(value, node); + return node; + } + }; + this.filter_ = new ol.parser.ogc.Filter_v1_0_0(); + for (var uri in this.filter_.readers) { + for (var key in this.filter_.readers[uri]) { + if (!goog.isDef(this.readers[uri])) { + this.readers[uri] = {}; + } + this.readers[uri][key] = goog.bind(this.filter_.readers[uri][key], + this.filter_); + } + } + for (var uri in this.filter_.writers) { + for (var key in this.filter_.writers[uri]) { + if (!goog.isDef(this.writers[uri])) { + this.writers[uri] = {}; + } + this.writers[uri][key] = goog.bind(this.filter_.writers[uri][key], + this.filter_); + } + } + goog.base(this); +}; +goog.inherits(ol.parser.ogc.SLD_v1, ol.parser.XML); + + +/** + * @private + */ +ol.parser.ogc.SLD_v1.cssMap_ = { + 'stroke': 'strokeColor', + 'stroke-opacity': 'strokeOpacity', + 'stroke-width': 'strokeWidth', + 'stroke-linecap': 'strokeLinecap', + 'stroke-dasharray': 'strokeDashstyle', + 'fill': 'fillColor', + 'fill-opacity': 'fillOpacity', + 'font-family': 'fontFamily', + 'font-size': 'fontSize', + 'font-weight': 'fontWeight', + 'font-style': 'fontStyle' +}; + + +/** + * @private + */ +ol.parser.ogc.SLD_v1.defaults_ = { + fillOpacity: 1, + strokeOpacity: 1, + strokeWidth: 1, + strokeColor: '#000000', + haloColor: '#FFFFFF', + haloOpacity: 1, + haloRadius: 1, + fillColor: '#808080', + fontColor: '#000000' +}; + + +/** + * @private + * @param {string} sym Symbolizer property. + * @return {string|undefined} The css property that matches the symbolizer + * property. + */ +ol.parser.ogc.SLD_v1.getCssProperty_ = function(sym) { + return goog.object.findKey(ol.parser.ogc.SLD_v1.cssMap_, + function(value, key, obj) { + return (sym === value); + } + ); +}; + + +/** + * @private + * @param {number} scaleDenominator The scale denominator to convert to + * resolution. + * @return {number} resolution. + */ +ol.parser.ogc.SLD_v1.prototype.getResolutionFromScaleDenominator_ = + function(scaleDenominator) { + var dpi = 25.4 / 0.28; + var mpu = ol.METERS_PER_UNIT[this.units]; + return 1 / ((1 / scaleDenominator) * (mpu * 39.37) * dpi); +}; + + +/** + * @private + * @param {number} resolution The resolution to convert to scale denominator. + * @return {number} scale denominator. + */ +ol.parser.ogc.SLD_v1.prototype.getScaleDenominatorFromResolution_ = + function(resolution) { + var dpi = 25.4 / 0.28; + var mpu = ol.METERS_PER_UNIT[this.units]; + return resolution * mpu * 39.37 * dpi; +}; + + +/** + * @param {string|Document|Element} data Data to read. + * @param {ol.parser.SLDReadOptions=} opt_options Read options. + * @return {Object} An object representing the document. + */ +ol.parser.ogc.SLD_v1.prototype.read = function(data, opt_options) { + var units = 'm'; + if (goog.isDef(opt_options) && goog.isDef(opt_options.units)) { + units = opt_options.units; + } + this.units = units; + if (goog.isString(data)) { + data = goog.dom.xml.loadXml(data); + } + if (data && data.nodeType == 9) { + data = data.documentElement; + } + var obj = {namedLayers: {}}; + this.readNode(data, obj); + delete this.units; + return obj; +}; + + +/** + * @param {Object} style The style to write out. + * @param {ol.parser.SLDWriteOptions=} opt_options Write options. + * @return {string} The serialized SLD. + */ +ol.parser.ogc.SLD_v1.prototype.write = function(style, opt_options) { + var units = 'm'; + if (goog.isDef(opt_options) && goog.isDef(opt_options.units)) { + units = opt_options.units; + } + this.units = units; + var root = this.writeNode('StyledLayerDescriptor', style); + this.setAttributeNS( + root, 'http://www.w3.org/2001/XMLSchema-instance', + 'xsi:schemaLocation', this.schemaLocation); + var result = this.serialize(root); + delete this.units; + return result; +}; diff --git a/src/ol/parser/ogc/sldparser_v1_0_0.js b/src/ol/parser/ogc/sldparser_v1_0_0.js new file mode 100644 index 0000000000..d86f897f6a --- /dev/null +++ b/src/ol/parser/ogc/sldparser_v1_0_0.js @@ -0,0 +1,18 @@ +goog.provide('ol.parser.ogc.SLD_v1_0_0'); + +goog.require('ol.parser.ogc.SLD_v1'); + + + +/** + * @constructor + * @extends {ol.parser.ogc.SLD_v1} + */ +ol.parser.ogc.SLD_v1_0_0 = function() { + goog.base(this); + this.version = '1.0.0'; + this.schemaLocation = 'http://www.opengis.net/sld ' + + 'http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd'; +}; +goog.inherits(ol.parser.ogc.SLD_v1_0_0, + ol.parser.ogc.SLD_v1); diff --git a/src/ol/style/rule.js b/src/ol/style/rule.js index b06e03cae9..d8234de95b 100644 --- a/src/ol/style/rule.js +++ b/src/ol/style/rule.js @@ -54,6 +54,20 @@ ol.style.Rule = function(options) { this.maxResolution_ = goog.isDef(options.maxResolution) ? options.maxResolution : Infinity; + /** + * @type {string|undefined} + * @private + */ + this.name_ = goog.isDef(options.name) ? + options.name : undefined; + + /** + * @type {string|undefined} + * @private + */ + this.title_ = goog.isDef(options.title) ? + options.title : undefined; + }; @@ -78,3 +92,43 @@ ol.style.Rule.prototype.applies = function(feature, resolution) { ol.style.Rule.prototype.getSymbolizers = function() { return this.symbolizers_; }; + + +/** + * @return {ol.expr.Expression} + */ +ol.style.Rule.prototype.getFilter = function() { + return this.filter_; +}; + + +/** + * @return {number} + */ +ol.style.Rule.prototype.getMinResolution = function() { + return this.minResolution_; +}; + + +/** + * @return {number} + */ +ol.style.Rule.prototype.getMaxResolution = function() { + return this.maxResolution_; +}; + + +/** + * @return {string|undefined} + */ +ol.style.Rule.prototype.getName = function() { + return this.name_; +}; + + +/** + * @return {string|undefined} + */ +ol.style.Rule.prototype.getTitle = function() { + return this.title_; +}; diff --git a/src/ol/style/style.js b/src/ol/style/style.js index 1a99081935..c8d70ef635 100644 --- a/src/ol/style/style.js +++ b/src/ol/style/style.js @@ -40,6 +40,19 @@ ol.style.Style = function(options) { this.symbolizers_ = goog.isDef(options.symbolizers) ? options.symbolizers : []; + /** + * @type {string|undefined} + * @private + */ + this.name_ = goog.isDef(options.name) ? + options.name : undefined; + + /** + * @type {string|undefined} + * @private + */ + this.title_ = goog.isDef(options.title) ? + options.title : undefined; }; @@ -207,3 +220,43 @@ ol.style.Style.reduceLiterals_ = function(literals) { } return reduced; }; + + +/** + * @return {Array.} + */ +ol.style.Style.prototype.getRules = function() { + return this.rules_; +}; + + +/** + * @param {Array.} rules The rules to set. + */ +ol.style.Style.prototype.setRules = function(rules) { + this.rules_ = rules; +}; + + +/** + * @return {Array.} + */ +ol.style.Style.prototype.getSymbolizers = function() { + return this.symbolizers_; +}; + + +/** + * @return {string|undefined} + */ +ol.style.Style.prototype.getName = function() { + return this.name_; +}; + + +/** + * @return {string|undefined} + */ +ol.style.Style.prototype.getTitle = function() { + return this.title_; +}; diff --git a/src/ol/style/textsymbolizer.js b/src/ol/style/textsymbolizer.js index bc3e1d3ec7..7b2e8e92cd 100644 --- a/src/ol/style/textsymbolizer.js +++ b/src/ol/style/textsymbolizer.js @@ -215,6 +215,15 @@ ol.style.Text.prototype.getZIndex = function() { }; +/** + * Get the stroke. + * @return {ol.style.Stroke} Stroke. + */ +ol.style.Text.prototype.getStroke = function() { + return this.stroke_; +}; + + /** * Set the font color. * @param {ol.expr.Expression} color Font color. diff --git a/test/spec/ol/parser/ogc/sld_v1_0_0.test.js b/test/spec/ol/parser/ogc/sld_v1_0_0.test.js new file mode 100644 index 0000000000..e44fb45ac6 --- /dev/null +++ b/test/spec/ol/parser/ogc/sld_v1_0_0.test.js @@ -0,0 +1,90 @@ +goog.provide('ol.test.parser.ogc.SLD_v1_0_0'); + + +describe('ol.parser.ogc.SLD_v1_0_0', function() { + + var parser = new ol.parser.ogc.SLD(); + var obj; + + describe('reading and writing', function() { + it('Handles reading', function(done) { + var url = 'spec/ol/parser/ogc/xml/sld_v1_0_0.xml'; + afterLoadXml(url, function(xml) { + obj = parser.read(xml); + expect(obj.version).to.equal('1.0.0'); + var style = obj.namedLayers['AAA161'].userStyles[0]; + expect(style).to.be.a(ol.style.Style); + expect(style.rules_.length).to.equal(2); + var first = style.rules_[0]; + expect(first).to.be.a(ol.style.Rule); + expect(first.filter_).to.be.a(ol.expr.Comparison); + expect(first.filter_.getLeft()).to.be.a(ol.expr.Identifier); + expect(first.filter_.getLeft().getName()).to.equal('CTE'); + expect(first.filter_.getOperator()).to.equal(ol.expr.ComparisonOp.EQ); + expect(first.filter_.getRight()).to.be.a(ol.expr.Literal); + expect(first.filter_.getRight().getValue()).to.equal('V0305'); + expect(first.getSymbolizers().length).to.equal(3); + expect(first.getSymbolizers()[0]).to.be.a(ol.style.Fill); + expect(first.getSymbolizers()[0].getColor().getValue()).to.equal( + '#ffffff'); + expect(first.getSymbolizers()[0].getOpacity().getValue()).to.equal(1); + expect(first.getSymbolizers()[1]).to.be.a(ol.style.Stroke); + expect(first.getSymbolizers()[1].getColor().getValue()).to.equal( + '#000000'); + expect(first.getSymbolizers()[2]).to.be.a(ol.style.Text); + expect(first.getSymbolizers()[2].getText()).to.be.a(ol.expr.Call); + expect(first.getSymbolizers()[2].getText().getArgs().length).to.equal( + 3); + expect(first.getSymbolizers()[2].getText().getArgs()[0]).to.be.a( + ol.expr.Literal); + expect(first.getSymbolizers()[2].getText().getArgs()[0].getValue()). + to.equal('A'); + expect(first.getSymbolizers()[2].getText().getArgs()[1]).to.be.a( + ol.expr.Identifier); + expect(first.getSymbolizers()[2].getText().getArgs()[1].getName()). + to.equal('FOO'); + expect(first.getSymbolizers()[2].getText().getArgs()[2]).to.be.a( + ol.expr.Literal); + expect(first.getSymbolizers()[2].getText().getArgs()[2].getValue()). + to.equal('label'); + expect(first.getSymbolizers()[2].getColor().getValue()).to.equal( + '#000000'); + expect(first.getSymbolizers()[2].getFontFamily().getValue()).to.equal( + 'Arial'); + expect(first.getSymbolizers()[2].getStroke()).to.be.a(ol.style.Stroke); + expect(first.getSymbolizers()[2].getStroke().getColor().getValue()) + .to.equal('#ffffff'); + expect(first.getSymbolizers()[2].getStroke().getWidth().getValue()) + .to.equal(6); + var second = style.rules_[1]; + expect(second.filter_).to.be.a(ol.expr.Comparison); + expect(second.getSymbolizers().length).to.equal(2); + expect(second.getSymbolizers()[0]).to.be.a(ol.style.Fill); + expect(second.getSymbolizers()[1]).to.be.a(ol.style.Stroke); + done(); + }); + }); + it('Handles write', function(done) { + var url = 'spec/ol/parser/ogc/xml/sld_v1_0_0_write.xml'; + afterLoadXml(url, function(xml) { + expect(goog.dom.xml.loadXml(parser.write(obj))).to.xmleql(xml); + done(); + }); + }); + }); + +}); + +goog.require('goog.dom.xml'); +goog.require('ol.parser.ogc.SLD_v1_0_0'); +goog.require('ol.parser.ogc.SLD'); +goog.require('ol.expr.Call'); +goog.require('ol.expr.Comparison'); +goog.require('ol.expr.ComparisonOp'); +goog.require('ol.expr.Identifier'); +goog.require('ol.expr.Literal'); +goog.require('ol.style.Fill'); +goog.require('ol.style.Rule'); +goog.require('ol.style.Stroke'); +goog.require('ol.style.Style'); +goog.require('ol.style.Text'); diff --git a/test/spec/ol/parser/ogc/xml/sld_v1_0_0.xml b/test/spec/ol/parser/ogc/xml/sld_v1_0_0.xml new file mode 100644 index 0000000000..6bb9cc654f --- /dev/null +++ b/test/spec/ol/parser/ogc/xml/sld_v1_0_0.xml @@ -0,0 +1,129 @@ + + + + AAA161 + + + + stortsteen + + + CTE + V0305 + + + 50000 + + + #ffffff + + + #000000 + + + + + + Arial + 14 + bold + normal + + + + + 0.5 + 0.5 + + + 5 + 5 + + 45 + + + + 3 + + #ffffff + + + + #000000 + + + + + betonbekleding + + + CTE + 1000 + + + 50000 + + + #ffff00 + + + #0000ff + + + + + + + + Second Layer + + + + first rule second layer + + + + number + + 1064866676 + + + 1065512599 + + + + cat + *dog.food!*good + + + + FOO + 5000 + + + + + 10000 + + + + star + + lime + + + olive + 2 + + + SIZE + + + + + + + diff --git a/test/spec/ol/parser/ogc/xml/sld_v1_0_0_write.xml b/test/spec/ol/parser/ogc/xml/sld_v1_0_0_write.xml new file mode 100644 index 0000000000..eeff7933ac --- /dev/null +++ b/test/spec/ol/parser/ogc/xml/sld_v1_0_0_write.xml @@ -0,0 +1,133 @@ + + + + AAA161 + + + + stortsteen + + + CTE + V0305 + + + 49999.99999999999 + + + #ffffff + 1 + + + + + #000000 + 1 + 1 + + + + AFOOlabel + + Arial + 14 + + + 3 + + #ffffff + 1 + + + + #000000 + 1 + + + + + betonbekleding + + + CTE + 1000 + + + 49999.99999999999 + + + #ffff00 + 1 + + + + + #0000ff + 1 + 1 + + + + + + + + Second Layer + + + + first rule second layer + + + + + FOO + 5000 + + + + cat + *dog.food!*good + + + number + + 1064866676 + + + 1065512599 + + + + + 10000 + + + + star + + lime + 1 + + + olive + 1 + 2 + + + SIZE + + + + + + +