diff --git a/examples/vector-layer-sld.html b/examples/vector-layer-sld.html index eda42d859b..62900c20ac 100644 --- a/examples/vector-layer-sld.html +++ b/examples/vector-layer-sld.html @@ -38,7 +38,7 @@

Vector layer example

-

Example of a countries vector layer with country information on hover and country labels at higher zoom levels.

+

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.

diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index 37424a6108..545573f100 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -573,6 +573,12 @@ * 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. diff --git a/src/ol/parser/ogc/sldparser_v1.js b/src/ol/parser/ogc/sldparser_v1.js index 5708a207f0..93bf09eb44 100644 --- a/src/ol/parser/ogc/sldparser_v1.js +++ b/src/ol/parser/ogc/sldparser_v1.js @@ -1,9 +1,12 @@ 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'); @@ -76,11 +79,11 @@ ol.parser.ogc.SLD_v1 = function() { rule.elseFilter = true; }, 'MinScaleDenominator': function(node, rule) { - rule.minResolution = this.getResolutionFromScale_( + rule.minResolution = this.getResolutionFromScaleDenominator_( parseFloat(this.getChildValue(node))); }, 'MaxScaleDenominator': function(node, rule) { - rule.maxResolution = this.getResolutionFromScale_( + rule.maxResolution = this.getResolutionFromScaleDenominator_( parseFloat(this.getChildValue(node))); }, 'TextSymbolizer': function(node, rule) { @@ -230,53 +233,56 @@ ol.parser.ogc.SLD_v1 = function() { ); }, 'PolygonSymbolizer': function(node, rule) { - var config = { - fill: false, - stroke: false - }; + var config = {}; this.readChildNodes(node, config); config.zIndex = this.featureTypeCounter; - if (config.fill === true) { + if (goog.isDef(config.fill)) { var fill = { - color: config['fillColor'], - opacity: config['fillOpacity'] + color: config.fill.fillColor, + opacity: config.fill.fillOpacity }; rule.symbolizers.push( new ol.style.Fill(fill) ); + delete config.fill; } - if (config.stroke === true) { + if (goog.isDef(config.stroke)) { var stroke = { - color: config['strokeColor'], - opacity: config['strokeOpacity'], - width: config['strokeWidth'] + color: config.stroke.strokeColor, + opacity: config.stroke.strokeOpacity, + width: config.stroke.strokeWidth }; rule.symbolizers.push( new ol.style.Stroke(stroke) ); + delete config.stroke; } }, 'PointSymbolizer': function(node, rule) { - var config = { - fill: null, - stroke: null, - graphic: null - }; + var config = {}; this.readChildNodes(node, config); config.zIndex = this.featureTypeCounter; + if (config.fill) { + config.fill = new ol.style.Fill(config.fill); + } + if (config.stroke) { + config.stroke = new ol.style.Stroke(config.stroke); + } // TODO shape or icon? rule.symbolizers.push( new ol.style.Shape(config) ); }, 'Stroke': function(node, symbolizer) { - symbolizer.stroke = true; - this.readChildNodes(node, symbolizer); + var stroke = {}; + this.readChildNodes(node, stroke); + symbolizer.stroke = stroke; }, 'Fill': function(node, symbolizer) { - symbolizer.fill = true; - this.readChildNodes(node, symbolizer); + var fill = {}; + this.readChildNodes(node, fill); + symbolizer.fill = fill; }, 'CssParameter': function(node, symbolizer) { var cssProperty = node.getAttribute('name'); @@ -382,6 +388,353 @@ ol.parser.ogc.SLD_v1 = function() { } } }; + this.writers = { + 'http://www.opengis.net/sld': { + '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'); + if (style.name) { + this.writeNode('Name', style.name, null, node); + } + if (style.title) { + this.writeNode('Title', style.title, null, node); + } + if (style.description) { + this.writeNode('Abstract', style.description, null, node); + } + if (style.isDefault) { + this.writeNode('IsDefault', style.isDefault, null, node); + } + if (style.rules) { + // group style objects by symbolizer zIndex + var rulesByZ = { + 0: [] + }; + var zValues = [0]; + var rule, ruleMap, symbolizer, zIndex, clone; + for (var i = 0, ii = style.rules.length; i < ii; ++i) { + rule = style.rules[i]; + var symbolizers = rule.getSymbolizers(); + if (symbolizers) { + ruleMap = {}; + for (var j = 0, jj = symbolizers.length; j < jj; ++j) { + symbolizer = symbolizers[j]; + zIndex = symbolizer.zIndex; + if (!(zIndex in ruleMap)) { + // TODO check if clone works? + clone = goog.object.clone(rule); + clone.setSymbolizers([]); + ruleMap[zIndex] = clone; + } + // TODO check if clone works + ruleMap[zIndex].getSymbolizers().push( + goog.object.clone(symbolizer)); + } + for (zIndex in ruleMap) { + if (!(zIndex in rulesByZ)) { + zValues.push(zIndex); + rulesByZ[zIndex] = []; + } + rulesByZ[zIndex].push(ruleMap[zIndex]); + } + } else { + // no symbolizers in rule + rulesByZ[0].push(goog.object.clone(rule)); + } + } + // write one FeatureTypeStyle per zIndex + zValues.sort(); + var rules; + for (var i = 0, ii = zValues.length; i < ii; ++i) { + rules = rulesByZ[zValues[i]]; + if (rules.length > 0) { + clone = goog.object.clone(style); + clone.setRules(rulesByZ[zValues[i]]); + this.writeNode('FeatureTypeStyle', clone, null, node); + } + } + } else { + this.writeNode('FeatureTypeStyle', style, null, node); + } + return node; + }, + 'IsDefault': function(bool) { + var node = this.createElementNS('sld:IsDefault'); + node.appendChild(this.createTextNode((bool) ? '1' : '0')); + return node; + }, + 'FeatureTypeStyle': function(style) { + var node = this.createElementNS('sld:FeatureTypeStyle'); + // OpenLayers currently stores no Name, Title, Abstract, + // FeatureTypeName, or SemanticTypeIdentifier information + // related to FeatureTypeStyle + // add in rules + var rules = style.getRules(); + for (var i = 0, ii = rules.length; i < ii; ++i) { + this.writeNode('Rule', rules[i], null, node); + } + return node; + }, + 'Rule': function(rule) { + var node = this.createElementNS('sld:Rule'); + var filter = rule.getFilter(); + if (rule.name) { + this.writeNode('Name', rule.name, null, node); + } + if (rule.title) { + this.writeNode('Title', rule.title, null, node); + } + if (rule.description) { + this.writeNode('Abstract', rule.description, null, node); + } + if (rule.elseFilter) { + this.writeNode('ElseFilter', null, null, node); + } else if (filter) { + this.writeNode('Filter', filter, 'http://www.opengis.net/ogc', 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]; + // TODO other types of symbolizers + 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.assert(opacity instanceof 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(); + } + goog.asserts.assert(size instanceof ol.expr.Literal, + 'Only ol.expr.Literal supported for in Size'); + this.writeNode('Size', size.getValue(), null, node); + if (symbolizer instanceof ol.style.Icon) { + var rotation = symbolizer.getRotation(); + goog.asserts.assert(rotation instanceof 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.assert(fillColor instanceof ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: fillColor.getValue(), + key: 'fillColor' + }, null, node); + var fillOpacity = symbolizer.getOpacity(); + goog.asserts.assert(fillOpacity instanceof 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(); + // TODO in SLD optional, but in ol3 required? + this.writeNode('Label', text, null, node); + // TODO in SLD optional, but in ol3 required? + this.writeNode('Font', symbolizer, null, node); + // TODO map align to labelAnchorPoint etc. + var stroke = symbolizer.getStroke(); + if (!goog.isNull(stroke)) { + this.writeNode('Halo', stroke, null, node); + } + 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.assert(strokeColor instanceof ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: strokeColor.getValue(), + key: 'strokeColor' + }, null, node); + var strokeOpacity = symbolizer.getOpacity(); + goog.asserts.assert(strokeOpacity instanceof ol.expr.Literal, msg); + this.writeNode('CssParameter', { + value: strokeOpacity.getValue(), + key: 'strokeOpacity' + }, null, node); + var strokeWidth = symbolizer.getWidth(); + goog.asserts.assert(strokeWidth instanceof 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 node = this.createElementNS('sld:CssParameter'); + node.setAttribute('name', + ol.parser.ogc.SLD_v1.getCssProperty_(obj.key)); + 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]) { @@ -392,6 +745,15 @@ ol.parser.ogc.SLD_v1 = function() { 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); @@ -415,13 +777,28 @@ ol.parser.ogc.SLD_v1.cssMap_ = { }; +/** + * @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.getResolutionFromScale_ = +ol.parser.ogc.SLD_v1.prototype.getResolutionFromScaleDenominator_ = function(scaleDenominator) { var dpi = 25.4 / 0.28; var mpu = ol.METERS_PER_UNIT[this.units]; @@ -429,6 +806,19 @@ ol.parser.ogc.SLD_v1.prototype.getResolutionFromScale_ = }; +/** + * @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. @@ -451,3 +841,24 @@ ol.parser.ogc.SLD_v1.prototype.read = function(data, opt_options) { 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/style/rule.js b/src/ol/style/rule.js index b06e03cae9..25d342aa5e 100644 --- a/src/ol/style/rule.js +++ b/src/ol/style/rule.js @@ -78,3 +78,27 @@ 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_; +}; diff --git a/src/ol/style/style.js b/src/ol/style/style.js index 1a99081935..d7e0d38022 100644 --- a/src/ol/style/style.js +++ b/src/ol/style/style.js @@ -207,3 +207,19 @@ 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; +}; 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 index 057d322062..5a6b76ef0f 100644 --- a/test/spec/ol/parser/ogc/sld_v1_0_0.test.js +++ b/test/spec/ol/parser/ogc/sld_v1_0_0.test.js @@ -56,6 +56,7 @@ describe('ol.parser.ogc.SLD_v1_0_0', function() { 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); + window.console.log(parser.write(obj)); done(); }); });