diff --git a/examples/vector-layer.js b/examples/vector-layer.js index d830b2dc6a..8ce9b792bf 100644 --- a/examples/vector-layer.js +++ b/examples/vector-layer.js @@ -27,7 +27,7 @@ var vector = new ol.layer.Vector({ new ol.style.Rule({ symbolizers: [ new ol.style.Fill({ - color: '#ffffff', + color: 'white', opacity: 0.6 }), new ol.style.Stroke({ @@ -40,10 +40,14 @@ var vector = new ol.layer.Vector({ maxResolution: 5000, symbolizers: [ new ol.style.Text({ - color: '#000000', + color: 'black', text: ol.expr.parse('name'), fontFamily: 'Calibri,sans-serif', - fontSize: 12 + fontSize: 12, + stroke: new ol.style.Stroke({ + color: 'white', + width: 3 + }) }) ] }) diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index feb8364a1a..dd79015e38 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -762,6 +762,7 @@ * @property {number|ol.expr.Expression|undefined} fontSize Font size in pixels. * @property {string|ol.expr.Expression} text Text for the label. * @property {number|ol.expr.Expression|undefined} opacity Opacity (0-1). + * @property {ol.style.Stroke|undefined} stroke Stroke symbolizer for text. * @property {number|ol.expr.Expression|undefined} zIndex Stack order. */ diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index 409d95d534..7232ad8865 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -293,6 +293,15 @@ ol.renderer.canvas.Vector.prototype.renderText_ = context.textAlign = 'center'; context.textBaseline = 'middle'; + var stroke = false; + if (goog.isDef(text.strokeColor)) { + stroke = true; + goog.asserts.assertString(text.strokeColor); + context.strokeStyle = text.strokeColor; + goog.asserts.assertNumber(text.strokeWidth); + context.lineWidth = text.strokeWidth; + } + for (var i = 0, ii = features.length; i < ii; ++i) { feature = features[i]; if (feature.renderIntent === ol.layer.VectorLayerRenderIntent.HIDDEN) { @@ -303,6 +312,16 @@ ol.renderer.canvas.Vector.prototype.renderText_ = for (var j = 0, jj = vecs.length; j < jj; ++j) { vec = vecs[j]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); + if (stroke) { + if (text.strokeOpacity !== text.opacity) { + goog.asserts.assertNumber(text.strokeOpacity); + context.globalAlpha = text.strokeOpacity; + } + context.strokeText(texts[i], vec[0], vec[1]); + if (text.strokeOpacity !== text.opacity) { + context.globalAlpha = text.opacity; + } + } context.fillText(texts[i], vec[0], vec[1]); } } diff --git a/src/ol/style/textliteral.js b/src/ol/style/textliteral.js index ec3036bed3..23e06cad90 100644 --- a/src/ol/style/textliteral.js +++ b/src/ol/style/textliteral.js @@ -10,6 +10,9 @@ goog.require('ol.style.Literal'); * fontSize: number, * text: string, * opacity: number, + * strokeColor: (string|undefined), + * strokeOpacity: (number|undefined), + * strokeWidth: (number|undefined), * zIndex: number}} */ ol.style.TextLiteralOptions; @@ -43,6 +46,37 @@ ol.style.TextLiteral = function(options) { /** @type {number} */ this.opacity = options.opacity; + /** @type {string|undefined} */ + this.strokeColor = options.strokeColor; + if (goog.isDef(this.strokeColor)) { + goog.asserts.assertString( + this.strokeColor, 'strokeColor must be a string'); + } + + /** @type {number|undefined} */ + this.strokeOpacity = options.strokeOpacity; + if (goog.isDef(this.strokeOpacity)) { + goog.asserts.assertNumber( + this.strokeOpacity, 'strokeOpacity must be a number'); + } + + /** @type {number|undefined} */ + this.strokeWidth = options.strokeWidth; + if (goog.isDef(this.strokeWidth)) { + goog.asserts.assertNumber( + this.strokeWidth, 'strokeWidth must be a number'); + } + + // if any stroke property is defined, all must be defined + var strokeDef = goog.isDef(this.strokeColor) && + goog.isDef(this.strokeOpacity) && + goog.isDef(this.strokeWidth); + var strokeUndef = !goog.isDef(this.strokeColor) && + !goog.isDef(this.strokeOpacity) && + !goog.isDef(this.strokeWidth); + goog.asserts.assert(strokeDef || strokeUndef, + 'If any stroke property is defined, all must be defined'); + goog.asserts.assertNumber(options.zIndex, 'zIndex must be a number'); /** @type {number} */ this.zIndex = options.zIndex; @@ -59,5 +93,8 @@ ol.style.TextLiteral.prototype.equals = function(other) { this.fontFamily == other.fontFamily && this.fontSize == other.fontSize && this.opacity == other.opacity && + this.strokeColor == other.strokeColor && + this.strokeOpacity == other.strokeOpacity && + this.strokeWidth == other.strokeWidth && this.zIndex == other.zIndex; }; diff --git a/src/ol/style/textsymbolizer.js b/src/ol/style/textsymbolizer.js index 95e00864f0..a597fe27c2 100644 --- a/src/ol/style/textsymbolizer.js +++ b/src/ol/style/textsymbolizer.js @@ -60,6 +60,12 @@ ol.style.Text = function(options) { (options.opacity instanceof ol.expr.Expression) ? options.opacity : new ol.expr.Literal(options.opacity); + /** + * @type {ol.style.Stroke} + * @private + */ + this.stroke_ = goog.isDefAndNotNull(options.stroke) ? options.stroke : null; + /** * @type {ol.expr.Expression} * @private @@ -102,6 +108,20 @@ ol.style.Text.prototype.createLiteral = function(featureOrType) { var opacity = Number(ol.expr.evaluateFeature(this.opacity_, feature)); goog.asserts.assert(!isNaN(opacity), 'opacity must be a number'); + var strokeColor, strokeOpacity, strokeWidth; + if (!goog.isNull(this.stroke_)) { + strokeColor = ol.expr.evaluateFeature(this.stroke_.getColor(), feature); + goog.asserts.assertString( + strokeColor, 'strokeColor must be a string'); + strokeOpacity = Number(ol.expr.evaluateFeature( + this.stroke_.getOpacity(), feature)); + goog.asserts.assert(!isNaN(strokeOpacity), + 'strokeOpacity must be a number'); + strokeWidth = Number(ol.expr.evaluateFeature( + this.stroke_.getWidth(), feature)); + goog.asserts.assert(!isNaN(strokeWidth), 'strokeWidth must be a number'); + } + var zIndex = Number(ol.expr.evaluateFeature(this.zIndex_, feature)); goog.asserts.assert(!isNaN(zIndex), 'zIndex must be a number'); @@ -111,6 +131,9 @@ ol.style.Text.prototype.createLiteral = function(featureOrType) { fontSize: fontSize, text: text, opacity: opacity, + strokeColor: strokeColor, + strokeOpacity: strokeOpacity, + strokeWidth: strokeWidth, zIndex: zIndex }); }; diff --git a/test/spec/ol/style/textliteral.test.js b/test/spec/ol/style/textliteral.test.js index b3b945675a..0ad48b7835 100644 --- a/test/spec/ol/style/textliteral.test.js +++ b/test/spec/ol/style/textliteral.test.js @@ -2,6 +2,54 @@ goog.provide('ol.test.style.TextLiteral'); describe('ol.style.TextLiteral', function() { + describe('constructor', function() { + + it('creates a new literal', function() { + var literal = new ol.style.TextLiteral({ + color: '#ff0000', + fontFamily: 'Arial', + fontSize: 11, + text: 'Test', + opacity: 0.5, + zIndex: 0 + }); + expect(literal).to.be.a(ol.style.Literal); + expect(literal).to.be.a(ol.style.TextLiteral); + }); + + it('accepts stroke properties', function() { + var literal = new ol.style.TextLiteral({ + color: '#ff0000', + fontFamily: 'Arial', + fontSize: 11, + text: 'Test', + opacity: 0.5, + strokeColor: '#ff0000', + strokeWidth: 2, + strokeOpacity: 0.5, + zIndex: 0 + }); + expect(literal).to.be.a(ol.style.TextLiteral); + }); + + it('throws with incomplete stroke properties', function() { + expect(function() { + new ol.style.TextLiteral({ + color: '#ff0000', + fontFamily: 'Arial', + fontSize: 11, + text: 'Test', + opacity: 0.5, + strokeColor: '#ff0000', + zIndex: 0 + }); + }).throwException(function(err) { + expect(err).to.be.a(goog.asserts.AssertionError); + }); + }); + + }); + describe('#equals()', function() { it('identifies equal literals', function() { @@ -82,4 +130,6 @@ describe('ol.style.TextLiteral', function() { }); +goog.require('goog.asserts.AssertionError'); +goog.require('ol.style.Literal'); goog.require('ol.style.TextLiteral'); diff --git a/test/spec/ol/style/textsymbolizer.test.js b/test/spec/ol/style/textsymbolizer.test.js index 0be628317a..11ad31e8b8 100644 --- a/test/spec/ol/style/textsymbolizer.test.js +++ b/test/spec/ol/style/textsymbolizer.test.js @@ -38,6 +38,19 @@ describe('ol.style.Text', function() { expect(symbolizer).to.be.a(ol.style.Text); }); + it('accepts stroke', function() { + var symbolizer = new ol.style.Text({ + color: '#000000', + text: 'Test', + stroke: new ol.style.Stroke({ + color: '#ff0000', + width: 2, + opacity: 0.5 + }) + }); + expect(symbolizer).to.be.a(ol.style.Text); + }); + }); describe('#createLiteral()', function() { @@ -128,6 +141,36 @@ describe('ol.style.Text', function() { expect(literal.opacity).to.be(0.42); }); + it('evaluates stroke expressions', function() { + var symbolizer = new ol.style.Text({ + text: 'test', + stroke: new ol.style.Stroke({ + width: ol.expr.parse('strokeWidth') + }) + }); + + var feature = new ol.Feature({ + strokeWidth: '4.2' + }); + + var literal = symbolizer.createLiteral(feature); + expect(literal.strokeWidth).to.be(4.2); + }); + + it('applies stroke defaults', function() { + var symbolizer = new ol.style.Text({ + text: 'test', + stroke: new ol.style.Stroke({ + width: 2 + }) + }); + + var literal = symbolizer.createLiteral(); + expect(literal.strokeWidth).to.be(2); + expect(literal.strokeColor).to.be('#696969'); + expect(literal.strokeOpacity).to.be(0.75); + }); + }); describe('#getColor()', function() { @@ -338,5 +381,6 @@ goog.require('ol.expr'); goog.require('ol.expr.Literal'); goog.require('ol.expr.Literal'); goog.require('ol.geom.GeometryType'); +goog.require('ol.style.Stroke'); goog.require('ol.style.Text'); goog.require('ol.style.TextLiteral');