diff --git a/examples/style-rules.js b/examples/style-rules.js index a7d3a708d2..b41f720a5a 100644 --- a/examples/style-rules.js +++ b/examples/style-rules.js @@ -4,13 +4,17 @@ goog.require('ol.RendererHint'); goog.require('ol.View2D'); goog.require('ol.control.defaults'); goog.require('ol.filter.Filter'); +goog.require('ol.filter.Geometry'); +goog.require('ol.geom.GeometryType'); goog.require('ol.layer.Vector'); goog.require('ol.parser.GeoJSON'); goog.require('ol.proj'); goog.require('ol.source.Vector'); goog.require('ol.style.Line'); goog.require('ol.style.Rule'); +goog.require('ol.style.Shape'); goog.require('ol.style.Style'); +goog.require('ol.style.Text'); var style = new ol.style.Style({rules: [ @@ -42,6 +46,21 @@ var style = new ol.style.Style({rules: [ opacity: 1 }) ] + }), + new ol.style.Rule({ + filter: new ol.filter.Geometry(ol.geom.GeometryType.POINT), + symbolizers: [ + new ol.style.Shape({ + size: 40, + fillColor: '#013' + }), + new ol.style.Text({ + color: '#bada55', + name: new ol.Expression('label'), + fontFamily: 'Calibri,sans-serif', + fontSize: 14 + }) + ] }) ]}); @@ -114,6 +133,42 @@ vector.parseFeatures({ 'type': 'LineString', 'coordinates': [[10000000, -10000000], [-10000000, -10000000]] } + }, { + 'type': 'Feature', + 'properties': { + 'label': 'South' + }, + 'geometry': { + 'type': 'Point', + 'coordinates': [0, -6000000] + } + }, { + 'type': 'Feature', + 'properties': { + 'label': 'West' + }, + 'geometry': { + 'type': 'Point', + 'coordinates': [-6000000, 0] + } + }, { + 'type': 'Feature', + 'properties': { + 'label': 'North' + }, + 'geometry': { + 'type': 'Point', + 'coordinates': [0, 6000000] + } + }, { + 'type': 'Feature', + 'properties': { + 'label': 'East' + }, + 'geometry': { + 'type': 'Point', + 'coordinates': [6000000, 0] + } }] }, new ol.parser.GeoJSON(), ol.proj.get('EPSG:3857')); diff --git a/examples/vector-layer.html b/examples/vector-layer.html index 6cea238384..2b8a08a591 100644 --- a/examples/vector-layer.html +++ b/examples/vector-layer.html @@ -37,7 +37,7 @@

Vector layer example

-

Example of a countries vector layer with country information on hover.

+

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

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

diff --git a/examples/vector-layer.js b/examples/vector-layer.js index 2a55cbba93..a4a00eeeaa 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -1,6 +1,8 @@ +goog.require('ol.Expression'); goog.require('ol.Map'); goog.require('ol.RendererHint'); goog.require('ol.View2D'); +goog.require('ol.filter.Filter'); goog.require('ol.layer.TileLayer'); goog.require('ol.layer.Vector'); goog.require('ol.parser.GeoJSON'); @@ -10,6 +12,7 @@ goog.require('ol.source.Vector'); goog.require('ol.style.Polygon'); goog.require('ol.style.Rule'); goog.require('ol.style.Style'); +goog.require('ol.style.Text'); var raster = new ol.layer.TileLayer({ @@ -27,6 +30,19 @@ var vector = new ol.layer.Vector({ strokeColor: '#bada55' }) ] + }), + new ol.style.Rule({ + filter: new ol.filter.Filter(function() { + return map.getView().getResolution() < 5000; + }), + symbolizers: [ + new ol.style.Text({ + color: '#bada55', + name: new ol.Expression('name'), + fontFamily: 'Calibri,sans-serif', + fontSize: 12 + }) + ] }) ]}), transformFeatureInfo: function(features) { diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index bc61df1fcb..f149bca03e 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -573,6 +573,15 @@ * @property {Array.} rules Rules. */ +/** + * @typedef {Object} ol.style.TextOptions + * @property {string|ol.Expression|undefined} color Color. + * @property {string|ol.Expression|undefined} fontFamily Font family. + * @property {number|ol.Expression|undefined} fontSize Font size in pixels. + * @property {string|ol.Expression} name Name (i.e. text content) of the label. + * @property {number|ol.Expression|undefined} opacity Opacity (0-1). + */ + /** * @typedef {Object} ol.tilegrid.TileGridOptions * @property {number|undefined} minZoom Minimum zoom. diff --git a/src/ol/layer/vectorlayer.js b/src/ol/layer/vectorlayer.js index 21b2f865b4..42f56eca70 100644 --- a/src/ol/layer/vectorlayer.js +++ b/src/ol/layer/vectorlayer.js @@ -11,6 +11,7 @@ goog.require('ol.proj'); goog.require('ol.source.Vector'); goog.require('ol.structs.RTree'); goog.require('ol.style.Style'); +goog.require('ol.style.TextLiteral'); @@ -276,14 +277,17 @@ ol.layer.Vector.prototype.getPolygonVertices = function() { /** * @param {Object.} features Features. - * @return {Array.} symbolizers for features. + * @return {Array.} symbolizers for features. Each array in this array + * contains 3 items: an array of features, the symbolizer literal, and + * an array with optional additional data for each feature. */ ol.layer.Vector.prototype.groupFeaturesBySymbolizerLiteral = function(features) { var uniqueLiterals = {}, featuresBySymbolizer = [], style = this.style_, - i, j, l, feature, literals, numLiterals, literal, uniqueLiteral, key; + i, j, l, feature, literals, numLiterals, literal, uniqueLiteral, key, + item; for (i in features) { feature = features[i]; literals = feature.getSymbolizerLiterals(); @@ -305,9 +309,17 @@ ol.layer.Vector.prototype.groupFeaturesBySymbolizerLiteral = key = goog.getUid(literal); if (!goog.object.containsKey(uniqueLiterals, key)) { uniqueLiterals[key] = featuresBySymbolizer.length; - featuresBySymbolizer.push([[], literal]); + featuresBySymbolizer.push([ + /** @type {Array.} */ ([]), + /** @type {ol.style.SymbolizerLiteral} */ (literal), + /** @type {Array} */ ([]) + ]); + } + item = featuresBySymbolizer[uniqueLiterals[key]]; + item[0].push(feature); + if (literal instanceof ol.style.TextLiteral) { + item[2].push(literals[j].name); } - featuresBySymbolizer[uniqueLiterals[key]][0].push(feature); } } return featuresBySymbolizer; diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 22a399fd4a..5d7b0d1081 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -446,7 +446,7 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = group = groups[j]; deferred = sketchCanvasRenderer.renderFeaturesByGeometryType( /** @type {ol.geom.GeometryType} */ (type), - group[0], group[1]); + group[0], group[1], group[2]); if (deferred) { break renderByGeometryType; } diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index cb46ae288d..a8f86d33e1 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -8,6 +8,8 @@ goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('goog.vec.Mat4'); goog.require('ol.Feature'); +goog.require('ol.extent'); +goog.require('ol.geom.AbstractCollection'); goog.require('ol.geom.Geometry'); goog.require('ol.geom.GeometryType'); goog.require('ol.geom.LineString'); @@ -23,6 +25,7 @@ goog.require('ol.style.PolygonLiteral'); goog.require('ol.style.ShapeLiteral'); goog.require('ol.style.ShapeType'); goog.require('ol.style.SymbolizerLiteral'); +goog.require('ol.style.TextLiteral'); @@ -100,35 +103,40 @@ ol.renderer.canvas.VectorRenderer.prototype.getMaxSymbolSize = function() { * @param {ol.geom.GeometryType} type Geometry type. * @param {Array.} features Array of features. * @param {ol.style.SymbolizerLiteral} symbolizer Symbolizer. + * @param {Array} data Additional data. * @return {boolean} true if deferred, false if rendered. */ ol.renderer.canvas.VectorRenderer.prototype.renderFeaturesByGeometryType = - function(type, features, symbolizer) { + function(type, features, symbolizer, data) { var deferred = false; - switch (type) { - case ol.geom.GeometryType.POINT: - case ol.geom.GeometryType.MULTIPOINT: - goog.asserts.assert(symbolizer instanceof ol.style.PointLiteral, - 'Expected point symbolizer: ' + symbolizer); - deferred = this.renderPointFeatures_( - features, /** @type {ol.style.PointLiteral} */ (symbolizer)); - break; - case ol.geom.GeometryType.LINESTRING: - case ol.geom.GeometryType.MULTILINESTRING: - goog.asserts.assert(symbolizer instanceof ol.style.LineLiteral, - 'Expected line symbolizer: ' + symbolizer); - this.renderLineStringFeatures_( - features, /** @type {ol.style.LineLiteral} */ (symbolizer)); - break; - case ol.geom.GeometryType.POLYGON: - case ol.geom.GeometryType.MULTIPOLYGON: - goog.asserts.assert(symbolizer instanceof ol.style.PolygonLiteral, - 'Expected polygon symbolizer: ' + symbolizer); - this.renderPolygonFeatures_( - features, /** @type {ol.style.PolygonLiteral} */ (symbolizer)); - break; - default: - throw new Error('Rendering not implemented for geometry type: ' + type); + if (!(symbolizer instanceof ol.style.TextLiteral)) { + switch (type) { + case ol.geom.GeometryType.POINT: + case ol.geom.GeometryType.MULTIPOINT: + goog.asserts.assert(symbolizer instanceof ol.style.PointLiteral, + 'Expected point symbolizer: ' + symbolizer); + deferred = this.renderPointFeatures_( + features, /** @type {ol.style.PointLiteral} */ (symbolizer)); + break; + case ol.geom.GeometryType.LINESTRING: + case ol.geom.GeometryType.MULTILINESTRING: + goog.asserts.assert(symbolizer instanceof ol.style.LineLiteral, + 'Expected line symbolizer: ' + symbolizer); + this.renderLineStringFeatures_( + features, /** @type {ol.style.LineLiteral} */ (symbolizer)); + break; + case ol.geom.GeometryType.POLYGON: + case ol.geom.GeometryType.MULTIPOLYGON: + goog.asserts.assert(symbolizer instanceof ol.style.PolygonLiteral, + 'Expected polygon symbolizer: ' + symbolizer); + this.renderPolygonFeatures_( + features, /** @type {ol.style.PolygonLiteral} */ (symbolizer)); + break; + default: + throw new Error('Rendering not implemented for geometry type: ' + type); + } + } else { + this.renderLabels_(features, symbolizer, data); } return deferred; }; @@ -255,6 +263,52 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = }; +/** + * @param {Array.} features Array of features. + * @param {ol.style.TextLiteral} text Text symbolizer. + * @param {Array} names Label text for each feature. + * @private + */ +ol.renderer.canvas.VectorRenderer.prototype.renderLabels_ = + function(features, text, names) { + var context = this.context_, + fontArray = [], + color = text.color, + vecs, vec; + + context.save(); + context.scale(1, -1); + + if (color) { + context.fillStyle = color; + } + if (goog.isDef(text.fontSize)) { + fontArray.push((text.fontSize * this.inverseScale_) + 'px'); + } + if (goog.isDef(text.fontFamily)) { + fontArray.push(text.fontFamily); + } + if (fontArray.length) { + context.font = fontArray.join(' '); + } + context.globalAlpha = text.opacity; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + + for (var i = 0, ii = features.length; i < ii; ++i) { + vecs = ol.renderer.canvas.VectorRenderer.getLabelVectors( + features[i].getGeometry()); + for (var j = 0, jj = vecs.length; j < jj; ++j) { + vec = vecs[j]; + goog.vec.Mat4.multVec3(this.transform_, vec, vec); + context.fillText(names[i], vec[0], vec[1]); + } + } + + context.restore(); +}; + + /** * @param {Array.} features Array of polygon features. * @param {ol.style.PolygonLiteral} symbolizer Polygon symbolizer. @@ -384,6 +438,35 @@ ol.renderer.canvas.VectorRenderer.renderCircle_ = function(circle) { }; +/** + * @param {ol.geom.Geometry} geometry Geometry. + * @return {Array.} Renderable geometry vectors. + */ +ol.renderer.canvas.VectorRenderer.getLabelVectors = function(geometry) { + if (geometry instanceof ol.geom.AbstractCollection) { + var components = geometry.components; + var numComponents = components.length; + var result = []; + for (var i = 0; i < numComponents; ++i) { + result.push.apply(result, + ol.renderer.canvas.VectorRenderer.getLabelVectors(components[i])); + } + return result; + } + var type = geometry.getType(); + if (type == ol.geom.GeometryType.POINT) { + return [[geometry.get(0), geometry.get(1), 0]]; + } + if (type == ol.geom.GeometryType.POLYGON) { + // TODO Better label placement + var coordinates = ol.extent.getCenter(geometry.getBounds()); + return [[coordinates[0], coordinates[1], 0]]; + } + throw new Error('Label rendering not implemented for geometry type: ' + + type); +}; + + /** * @param {ol.style.ShapeLiteral} shape Shape symbolizer. * @return {!HTMLCanvasElement} Canvas element. diff --git a/src/ol/style.exports b/src/ol/style.exports index 8df1a0a1bd..d2e83eb60b 100644 --- a/src/ol/style.exports +++ b/src/ol/style.exports @@ -4,6 +4,7 @@ @exportClass ol.style.Rule ol.style.RuleOptions @exportClass ol.style.Shape ol.style.ShapeOptions @exportClass ol.style.Style ol.style.StyleOptions +@exportClass ol.style.Text ol.style.TextOptions @exportSymbol ol.style.IconType @exportSymbol ol.style.ShapeType @exportProperty ol.style.ShapeType.CIRCLE diff --git a/src/ol/style/text.js b/src/ol/style/text.js new file mode 100644 index 0000000000..4a260f395a --- /dev/null +++ b/src/ol/style/text.js @@ -0,0 +1,171 @@ +goog.provide('ol.style.Text'); +goog.provide('ol.style.TextLiteral'); + +goog.require('goog.asserts'); +goog.require('ol.Expression'); +goog.require('ol.ExpressionLiteral'); +goog.require('ol.style.Symbolizer'); +goog.require('ol.style.SymbolizerLiteral'); + + +/** + * @typedef {{color: (string|undefined), + * fontFamily: (string|undefined), + * fontSize: number, + * name: string, + * opacity: number}} + */ +ol.style.TextLiteralOptions; + + + +/** + * @constructor + * @extends {ol.style.SymbolizerLiteral} + * @param {ol.style.TextLiteralOptions} options Text literal options. + */ +ol.style.TextLiteral = function(options) { + + /** @type {string|undefined} */ + this.color = options.color; + if (goog.isDef(options.color)) { + goog.asserts.assertString(options.color, 'color must be a string'); + } + + /** @type {string|undefined} */ + this.fontFamily = options.fontFamily; + if (goog.isDef(options.fontFamily)) { + goog.asserts.assertString(options.fontFamily, + 'fontFamily must be a string'); + } + + goog.asserts.assertNumber(options.fontSize, 'fontSize must be a number'); + /** @type {number} */ + this.fontSize = options.fontSize; + + goog.asserts.assertString(options.name, 'name must be a string'); + /** @type {string} */ + this.name = options.name; + + goog.asserts.assertNumber(options.opacity, 'opacity must be a number'); + /** @type {number} */ + this.opacity = options.opacity; + +}; +goog.inherits(ol.style.TextLiteral, ol.style.SymbolizerLiteral); + + +/** + * @inheritDoc + */ +ol.style.TextLiteral.prototype.equals = function(textLiteral) { + return this.color == textLiteral.color && + this.fontFamily == textLiteral.fontFamily && + this.fontSize == textLiteral.fontSize && + this.opacity == textLiteral.opacity; +}; + + + +/** + * @constructor + * @extends {ol.style.Symbolizer} + * @param {ol.style.TextOptions} options Text options. + */ +ol.style.Text = function(options) { + + /** + * @type {ol.Expression} + * @private + */ + this.color_ = !goog.isDef(options.color) ? + null : + (options.color instanceof ol.Expression) ? + options.color : new ol.ExpressionLiteral(options.color); + + /** + * @type {ol.Expression} + * @private + */ + this.fontFamily_ = !goog.isDefAndNotNull(options.fontFamily) ? + null : + (options.fontFamily instanceof ol.Expression) ? + options.fontFamily : new ol.ExpressionLiteral(options.fontFamily); + + /** + * @type {ol.Expression} + * @private + */ + this.fontSize_ = !goog.isDefAndNotNull(options.fontSize) ? + null : + (options.fontSize instanceof ol.Expression) ? + options.fontSize : new ol.ExpressionLiteral(options.fontSize); + + /** + * @type {ol.Expression} + * @private + */ + this.name_ = (options.name instanceof ol.Expression) ? + options.name : new ol.ExpressionLiteral(options.name); + + /** + * @type {ol.Expression} + * @private + */ + this.opacity_ = !goog.isDef(options.opacity) ? + new ol.ExpressionLiteral(ol.style.TextDefaults.opacity) : + (options.opacity instanceof ol.Expression) ? + options.opacity : new ol.ExpressionLiteral(options.opacity); + +}; +goog.inherits(ol.style.Text, ol.style.Symbolizer); + + +/** + * @inheritDoc + * @return {ol.style.TextLiteral} Literal text symbolizer. + */ +ol.style.Text.prototype.createLiteral = function(opt_feature) { + var attrs, + feature = opt_feature; + if (goog.isDef(feature)) { + attrs = feature.getAttributes(); + } + + var color = goog.isNull(this.color_) ? + undefined : + /** @type {string} */ (this.color_.evaluate(feature, attrs)); + goog.asserts.assert(!goog.isDef(color) || goog.isString(color)); + + var fontFamily = goog.isNull(this.fontFamily_) ? + undefined : + /** @type {string} */ (this.fontFamily_.evaluate(feature, attrs)); + goog.asserts.assert(!goog.isDef(fontFamily) || goog.isString(fontFamily)); + + var fontSize = this.fontSize_.evaluate(feature, attrs); + goog.asserts.assertNumber(fontSize, 'fontSize must be a number'); + + var name = this.name_.evaluate(feature, attrs); + goog.asserts.assertString(name, 'name must be a string'); + + var opacity = this.opacity_.evaluate(feature, attrs); + goog.asserts.assertNumber(opacity, 'opacity must be a number'); + + return new ol.style.TextLiteral({ + color: color, + fontFamily: fontFamily, + fontSize: fontSize, + name: name, + opacity: opacity + }); +}; + + +/** + * @type {ol.style.TextLiteral} + */ +ol.style.TextDefaults = new ol.style.TextLiteral({ + fontSize: 10, + name: '', + opacity: 1 +});