diff --git a/externs/olx.js b/externs/olx.js index cf29fa0d01..404a6eba61 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -275,7 +275,7 @@ olx.interaction.InteractionOptions; * Method called by the map to notify the interaction that a browser event was * dispatched to the map. If the function returns a falsy value, * propagation of the event to other interactions in the map's interactions - * chain will be prevented (this includes functions with no explicit return). See + * chain will be prevented (this includes functions with no explicit return). See * {@link https://developer.mozilla.org/en-US/docs/Glossary/Falsy} * @type {function(ol.MapBrowserEvent):boolean} * @api @@ -7861,7 +7861,10 @@ olx.style.StrokeOptions.prototype.width; * textAlign: (string|undefined), * textBaseline: (string|undefined), * fill: (ol.style.Fill|undefined), - * stroke: (ol.style.Stroke|undefined)}} + * stroke: (ol.style.Stroke|undefined), + * backgroundFill: (ol.style.Fill|undefined), + * backgroundStroke: (ol.style.Stroke|undefined), + * padding: (Array.|undefined)}} */ olx.style.TextOptions; @@ -7990,6 +7993,34 @@ olx.style.TextOptions.prototype.fill; olx.style.TextOptions.prototype.stroke; +/** + * Fill style for the text background when `placement` is `'point'`. Default is + * no fill. + * @type {ol.style.Fill|undefined} + * @api + */ +olx.style.TextOptions.prototype.backgroundFill; + + +/** + * Stroke style for the text background when `placement` is `'point'`. Default + * is no stroke. + * @type {ol.style.Stroke|undefined} + * @api + */ +olx.style.TextOptions.prototype.backgroundStroke; + + +/** + * Padding in pixels around the text for decluttering and background. The order + * of values in the array is `[top, right, bottom, left]`. Default is + * `[0, 0, 0, 0]`. + * @type {Array.|undefined} + * @api + */ +olx.style.TextOptions.prototype.padding; + + /** * @typedef {{geometry: (undefined|string|ol.geom.Geometry|ol.StyleGeometryFunction), * fill: (ol.style.Fill|undefined), diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 2680f08077..710519d128 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -77,6 +77,13 @@ ol.render.canvas.defaultTextAlign = 'center'; ol.render.canvas.defaultTextBaseline = 'middle'; +/** + * @const + * @type {Array.} + */ +ol.render.canvas.defaultPadding = [0, 0, 0, 0]; + + /** * @const * @type {number} diff --git a/src/ol/render/canvas/linestringreplay.js b/src/ol/render/canvas/linestringreplay.js index 7741236a44..04035b33d9 100644 --- a/src/ol/render/canvas/linestringreplay.js +++ b/src/ol/render/canvas/linestringreplay.js @@ -54,7 +54,7 @@ ol.render.canvas.LineStringReplay.prototype.drawLineString = function(lineString if (strokeStyle === undefined || lineWidth === undefined) { return; } - this.updateStrokeStyle(state, true); + this.updateStrokeStyle(state, this.applyStroke); this.beginGeometry(lineStringGeometry, feature); this.hitDetectionInstructions.push([ ol.render.canvas.Instruction.SET_STROKE_STYLE, @@ -81,7 +81,7 @@ ol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multi if (strokeStyle === undefined || lineWidth === undefined) { return; } - this.updateStrokeStyle(state, true); + this.updateStrokeStyle(state, this.applyStroke); this.beginGeometry(multiLineStringGeometry, feature); this.hitDetectionInstructions.push([ ol.render.canvas.Instruction.SET_STROKE_STYLE, @@ -115,3 +115,17 @@ ol.render.canvas.LineStringReplay.prototype.finish = function() { this.reverseHitDetectionInstructions(); this.state = null; }; + + +/** + * @inheritDoc. + */ +ol.render.canvas.LineStringReplay.prototype.applyStroke = function(state) { + if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { + this.instructions.push([ol.render.canvas.Instruction.STROKE]); + state.lastStroke = this.coordinates.length; + } + state.lastStroke = 0; + ol.render.canvas.Replay.prototype.applyStroke.call(this, state); + this.instructions.push([ol.render.canvas.Instruction.BEGIN_PATH]); +}; diff --git a/src/ol/render/canvas/polygonreplay.js b/src/ol/render/canvas/polygonreplay.js index c3ea0a763e..8589850203 100644 --- a/src/ol/render/canvas/polygonreplay.js +++ b/src/ol/render/canvas/polygonreplay.js @@ -214,16 +214,10 @@ ol.render.canvas.PolygonReplay.prototype.finish = function() { ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyles_ = function(geometry) { var state = this.state; var fillStyle = state.fillStyle; - if (fillStyle !== undefined && (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle)) { - var fillInstruction = [ol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle]; - if (typeof fillStyle !== 'string') { - var fillExtent = geometry.getExtent(); - fillInstruction.push([fillExtent[0], fillExtent[3]]); - } - this.instructions.push(fillInstruction); - state.currentFillStyle = state.fillStyle; + if (fillStyle !== undefined) { + this.updateFillStyle(state, this.applyFill, geometry); } if (state.strokeStyle !== undefined) { - this.updateStrokeStyle(state, false); + this.updateStrokeStyle(state, this.applyStroke); } }; diff --git a/src/ol/render/canvas/replay.js b/src/ol/render/canvas/replay.js index b7159d814e..77175d4890 100644 --- a/src/ol/render/canvas/replay.js +++ b/src/ol/render/canvas/replay.js @@ -178,9 +178,14 @@ ol.inherits(ol.render.canvas.Replay, ol.render.VectorContext); * @param {number} scale Scale. * @param {boolean} snapToPixel Snap to pixel. * @param {number} width Width. + * @param {Array.} padding Padding. + * @param {boolean} fill Background fill. + * @param {boolean} stroke Backgroud stroke. */ -ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, anchorX, anchorY, - declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width) { +ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, + anchorX, anchorY, declutterGroup, height, opacity, originX, originY, + rotation, scale, snapToPixel, width, padding, fill, stroke) { + var fillStroke = fill || stroke; var localTransform = this.tmpLocalTransform_; anchorX *= scale; anchorY *= scale; @@ -194,6 +199,18 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, var w = (width + originX > image.width) ? image.width - originX : width; var h = (height + originY > image.height) ? image.height - originY : height; var box = this.tmpExtent_; + var boxW = padding[3] + w * scale + padding[1]; + var boxH = padding[0] + h * scale + padding[2]; + var boxX = x - padding[3]; + var boxY = y - padding[0]; + + var p1, p2, p3, p4; + if (fillStroke || rotation !== 0) { + p1 = [boxX, boxY]; + p2 = [boxX + boxW, boxY]; + p3 = [boxX + boxW, boxY + boxH]; + p4 = [boxX, boxY + boxH]; + } var transform = null; if (rotation !== 0) { @@ -201,13 +218,14 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, var centerY = y + anchorY; transform = ol.transform.compose(localTransform, centerX, centerY, 1, 1, rotation, -centerX, -centerY); + ol.extent.createOrUpdateEmpty(box); - ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y])); - ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y])); - ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x + w, y + h])); - ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, [x, y + w])); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, /** @type {Array.} */ (p1))); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, /** @type {Array.} */ (p2))); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, /** @type {Array.} */ (p3))); + ol.extent.extendCoordinate(box, ol.transform.apply(localTransform, /** @type {Array.} */ (p4))); } else { - ol.extent.createOrUpdate(x, y, x + w * scale, y + h * scale, box); + ol.extent.createOrUpdate(boxX, boxY, boxX + boxW, boxY + boxH, box); } var canvas = context.canvas; var intersects = box[0] <= canvas.width && box[2] >= 0 && box[1] <= canvas.height && box[3] >= 0; @@ -215,11 +233,48 @@ ol.render.canvas.Replay.prototype.replayImage_ = function(context, x, y, image, if (!intersects && declutterGroup[4] == 1) { return; } + /** @type {ol.CanvasFillStrokeState} */ + var state; + if (intersects && fillStroke) { + state = /** @type {ol.CanvasFillStrokeState} */ ({}); + if (fill) { + state.fillStyle = context.fillStyle; + } + if (stroke) { + state.strokeStyle = context.strokeStyle; + state.lineWidth = context.lineWidth; + state.lineCap = context.lineCap; + state.lineJoin = context.lineJoin; + state.miterLimit = context.miterLimit; + if (ol.has.CANVAS_LINE_DASH) { + state.lineDashOffset = context.lineDashOffset; + state.lineDash = context.getLineDash(); + } + } + } ol.extent.extend(declutterGroup, box); - declutterGroup.push(intersects ? + var declutterArgs = intersects ? [context, transform ? transform.slice(0) : null, opacity, image, originX, originY, w, h, x, y, scale] : - null); + null; + if (declutterArgs && fillStroke) { + declutterArgs.push(state, p1, p2, p3, p4); + } + declutterGroup.push(declutterArgs); } else if (intersects) { + if (fillStroke) { + context.beginPath(); + context.moveTo.apply(context, p1); + context.lineTo.apply(context, p2); + context.lineTo.apply(context, p3); + context.lineTo.apply(context, p4); + context.lineTo.apply(context, p1); + if (fill) { + context.fill(); + } + if (stroke) { + context.stroke(); + } + } ol.render.canvas.drawImage(context, transform, opacity, image, originX, originY, w, h, x, y, scale); } }; @@ -413,8 +468,35 @@ ol.render.canvas.Replay.prototype.renderDeclutter_ = function(declutterGroup) { this.declutterTree.insert(box); var drawImage = ol.render.canvas.drawImage; for (var j = 5, jj = declutterGroup.length; j < jj; ++j) { - if (declutterGroup[j]) { - drawImage.apply(undefined, /** @type {Array} */ (declutterGroup[j])); + var declutterData = /** @type {Array} */ (declutterGroup[j]); + if (declutterData) { + if (declutterData.length > 11) { + var context = /** @type {CanvasRenderingContext2D} */ (declutterData[0]); + context.beginPath(); + context.moveTo.apply(context, declutterData[12]); + context.lineTo.apply(context, declutterData[13]); + context.lineTo.apply(context, declutterData[14]); + context.lineTo.apply(context, declutterData[15]); + context.lineTo.apply(context, declutterData[12]); + var state = /** @type {ol.CanvasFillStrokeState} */ (declutterData[11]); + if (state.fillStyle) { + context.fillStyle = state.fillStyle; + context.fill(); + } + if (state.strokeStyle) { + context.strokeStyle = state.strokeStyle; + context.lineWidth = /** @type {number} */ (state.lineWidth); + context.lineCap = /** @type {string} */ (state.lineCap); + context.lineJoin = /** @type {string} */ (state.lineJoin); + context.miterLimit = /** @type {number} */ (state.miterLimit); + if (ol.has.CANVAS_LINE_DASH) { + context.lineDashOffset = /** @type {number} */ (state.lineDashOffset); + context.setLineDash(state.lineDash); + } + context.stroke(); + } + } + drawImage.apply(undefined, declutterData); } } } @@ -461,7 +543,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( var ii = instructions.length; // end of instructions var d = 0; // data index var dd; // end of per-instruction data - var anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup; + var anchorX, anchorY, prevX, prevY, roundX, roundY, declutterGroup, image; var pendingFill = 0; var pendingStroke = 0; var coordinateCache = this.coordinateCache_; @@ -552,7 +634,7 @@ ol.render.canvas.Replay.prototype.replay_ = function( case ol.render.canvas.Instruction.DRAW_IMAGE: d = /** @type {number} */ (instruction[1]); dd = /** @type {number} */ (instruction[2]); - var image = /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */ + image = /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */ (instruction[3]); // Remaining arguments in DRAW_IMAGE are in alphabetical order anchorX = /** @type {number} */ (instruction[4]); @@ -568,13 +650,24 @@ ol.render.canvas.Replay.prototype.replay_ = function( var snapToPixel = /** @type {boolean} */ (instruction[14]); var width = /** @type {number} */ (instruction[15]); + var padding, backgroundFill, backgroundStroke; + if (instruction.length > 16) { + padding = /** @type {Array.} */ (instruction[16]); + backgroundFill = /** @type {boolean} */ (instruction[17]); + backgroundStroke = /** @type {boolean} */ (instruction[18]); + } else { + padding = ol.render.canvas.defaultPadding; + backgroundFill = backgroundStroke = false; + } + if (rotateWithView) { rotation += viewRotation; } for (; d < dd; d += 2) { this.replayImage_(context, pixelCoordinates[d], pixelCoordinates[d + 1], image, anchorX, anchorY, - declutterGroup, height, opacity, originX, originY, rotation, scale, snapToPixel, width); + declutterGroup, height, opacity, originX, originY, rotation, scale, + snapToPixel, width, padding, backgroundFill, backgroundStroke); } this.renderDeclutter_(declutterGroup); ++i; @@ -613,7 +706,8 @@ ol.render.canvas.Replay.prototype.replay_ = function( this.replayImage_(context, /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width); + /** @type {number} */ (part[3]), textScale, false, label.width, + ol.render.canvas.defaultPadding, false, false); } } if (fill) { @@ -626,7 +720,8 @@ ol.render.canvas.Replay.prototype.replay_ = function( this.replayImage_(context, /** @type {number} */ (part[0]), /** @type {number} */ (part[1]), label, anchorX, anchorY, declutterGroup, label.height, 1, 0, 0, - /** @type {number} */ (part[3]), textScale, false, label.width); + /** @type {number} */ (part[3]), textScale, false, label.width, + ol.render.canvas.defaultPadding, false, false); } } } @@ -849,9 +944,51 @@ ol.render.canvas.Replay.prototype.setFillStrokeStyle = function(fillStyle, strok /** * @param {ol.CanvasFillStrokeState} state State. - * @param {boolean} managePath Manage stoke() - beginPath() for linestrings. + * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry. */ -ol.render.canvas.Replay.prototype.updateStrokeStyle = function(state, managePath) { +ol.render.canvas.Replay.prototype.applyFill = function(state, geometry) { + var fillStyle = state.fillStyle; + var fillInstruction = [ol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle]; + if (typeof fillStyle !== 'string') { + var fillExtent = geometry.getExtent(); + fillInstruction.push([fillExtent[0], fillExtent[3]]); + } + this.instructions.push(fillInstruction); +}; + + +/** + * @param {ol.CanvasFillStrokeState} state State. + */ +ol.render.canvas.Replay.prototype.applyStroke = function(state) { + this.instructions.push([ + ol.render.canvas.Instruction.SET_STROKE_STYLE, + state.strokeStyle, state.lineWidth * this.pixelRatio, state.lineCap, + state.lineJoin, state.miterLimit, + this.applyPixelRatio(state.lineDash), state.lineDashOffset * this.pixelRatio + ]); +}; + + +/** + * @param {ol.CanvasFillStrokeState} state State. + * @param {function(this:ol.render.canvas.Replay, ol.CanvasFillStrokeState, (ol.geom.Geometry|ol.render.Feature))} applyFill Apply fill. + * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry. + */ +ol.render.canvas.Replay.prototype.updateFillStyle = function(state, applyFill, geometry) { + var fillStyle = state.fillStyle; + if (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle) { + applyFill.call(this, state, geometry); + state.currentFillStyle = fillStyle; + } +}; + + +/** + * @param {ol.CanvasFillStrokeState} state State. + * @param {function(this:ol.render.canvas.Replay, ol.CanvasFillStrokeState)} applyStroke Apply stroke. + */ +ol.render.canvas.Replay.prototype.updateStrokeStyle = function(state, applyStroke) { var strokeStyle = state.strokeStyle; var lineCap = state.lineCap; var lineDash = state.lineDash; @@ -866,21 +1003,7 @@ ol.render.canvas.Replay.prototype.updateStrokeStyle = function(state, managePath state.currentLineJoin != lineJoin || state.currentLineWidth != lineWidth || state.currentMiterLimit != miterLimit) { - if (managePath) { - if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) { - this.instructions.push([ol.render.canvas.Instruction.STROKE]); - state.lastStroke = this.coordinates.length; - } - state.lastStroke = 0; - } - this.instructions.push([ - ol.render.canvas.Instruction.SET_STROKE_STYLE, - strokeStyle, lineWidth * this.pixelRatio, lineCap, lineJoin, miterLimit, - this.applyPixelRatio(lineDash), lineDashOffset * this.pixelRatio - ]); - if (managePath) { - this.instructions.push([ol.render.canvas.Instruction.BEGIN_PATH]); - } + applyStroke.call(this, state); state.currentStrokeStyle = strokeStyle; state.currentLineCap = lineCap; state.currentLineDash = lineDash; diff --git a/src/ol/render/canvas/textreplay.js b/src/ol/render/canvas/textreplay.js index 2588bc52fb..12a258908e 100644 --- a/src/ol/render/canvas/textreplay.js +++ b/src/ol/render/canvas/textreplay.js @@ -290,6 +290,11 @@ ol.render.canvas.TextReplay.prototype.drawText = function(geometry, feature) { } end = this.appendFlatCoordinates(flatCoordinates, 0, end, stride, false, false); this.beginGeometry(geometry, feature); + if (textState.backgroundFill || textState.backgroundStroke) { + this.setFillStrokeStyle(textState.backgroundFill, textState.backgroundStroke); + this.updateFillStyle(this.state, this.applyFill, geometry); + this.updateStrokeStyle(this.state, this.applyStroke); + } this.drawTextImage_(label, begin, end); this.endGeometry(geometry, feature); } @@ -385,12 +390,18 @@ ol.render.canvas.TextReplay.prototype.drawTextImage_ = function(label, begin, en this.instructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1, true, label.width + 1, true, label.width, + textState.padding == ol.render.canvas.defaultPadding ? + ol.render.canvas.defaultPadding : textState.padding.map(function(p) { + return p * pixelRatio; + }), + !!textState.backgroundFill, !!textState.backgroundStroke ]); this.hitDetectionInstructions.push([ol.render.canvas.Instruction.DRAW_IMAGE, begin, end, label, (anchorX - this.textOffsetX_) * pixelRatio, (anchorY - this.textOffsetY_) * pixelRatio, this.declutterGroup_, label.height, 1, 0, 0, this.textRotateWithView_, this.textRotation_, - 1 / pixelRatio, true, label.width + 1 / pixelRatio, true, label.width, textState.padding, + !!textState.backgroundFill, !!textState.backgroundStroke ]); }; @@ -500,6 +511,9 @@ ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle, declutt textState.placement = textStyle.getPlacement(); textState.textAlign = textStyle.getTextAlign(); textState.textBaseline = textStyle.getTextBaseline() || ol.render.canvas.defaultTextBaseline; + textState.backgroundFill = textStyle.getBackgroundFill(); + textState.backgroundStroke = textStyle.getBackgroundStroke(); + textState.padding = textStyle.getPadding() || ol.render.canvas.defaultPadding; textState.scale = textScale === undefined ? 1 : textScale; var textOffsetX = textStyle.getOffsetX(); diff --git a/src/ol/style/text.js b/src/ol/style/text.js index ddb484f42b..c17d173a79 100644 --- a/src/ol/style/text.js +++ b/src/ol/style/text.js @@ -101,6 +101,24 @@ ol.style.Text = function(opt_options) { * @type {number} */ this.offsetY_ = options.offsetY !== undefined ? options.offsetY : 0; + + /** + * @private + * @type {ol.style.Fill} + */ + this.backgroundFill_ = options.backgroundFill ? options.backgroundFill : null; + + /** + * @private + * @type {ol.style.Stroke} + */ + this.backgroundStroke_ = options.backgroundStroke ? options.backgroundStroke : null; + + /** + * @private + * @type {Array.} + */ + this.padding_ = options.padding === undefined ? null : options.padding; }; @@ -279,6 +297,36 @@ ol.style.Text.prototype.getTextBaseline = function() { }; +/** + * Get the background fill style for the text. + * @return {ol.style.Fill} Fill style. + * @api + */ +ol.style.Text.prototype.getBackgroundFill = function() { + return this.backgroundFill_; +}; + + +/** + * Get the background stroke style for the text. + * @return {ol.style.Stroke} Stroke style. + * @api + */ +ol.style.Text.prototype.getBackgroundStroke = function() { + return this.backgroundStroke_; +}; + + +/** + * Get the padding for the text. + * @return {Array.} Padding. + * @api + */ +ol.style.Text.prototype.getPadding = function() { + return this.padding_; +}; + + /** * Set the `exceedLength` property. * @@ -420,3 +468,36 @@ ol.style.Text.prototype.setTextAlign = function(textAlign) { ol.style.Text.prototype.setTextBaseline = function(textBaseline) { this.textBaseline_ = textBaseline; }; + + +/** + * Set the background fill. + * + * @param {ol.style.Fill} fill Fill style. + * @api + */ +ol.style.Text.prototype.setBackgroundFill = function(fill) { + this.backgroundFill_ = fill; +}; + + +/** + * Set the background stroke. + * + * @param {ol.style.Stroke} stroke Stroke style. + * @api + */ +ol.style.Text.prototype.setBackgroundStroke = function(stroke) { + this.backgroundStroke_ = stroke; +}; + + +/** + * Set the padding (`[top, right, bottom, left]`). + * + * @param {!Array.} padding Padding. + * @api + */ +ol.style.Text.prototype.setPadding = function(padding) { + this.padding_ = padding; +};