Allow styles to configure a custom renderer

Two new examples show how custom renderers can be used to render text along
paths, and to declutter labels using 3rd party libraries.
This commit is contained in:
Andreas Hocevar
2017-07-15 00:06:35 +02:00
parent 2258c00fca
commit c6b942f185
21 changed files with 762 additions and 72 deletions

View File

@@ -41,6 +41,12 @@ ol.geom.SimpleGeometry = function() {
*/
this.flatCoordinates = null;
/**
* @private
* @type {Array.<number>|Array.<Array.<number>>|Array.<Array.<Array.<number>>>}
*/
this.renderCoordinates_ = null;
};
ol.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry);
@@ -141,6 +147,18 @@ ol.geom.SimpleGeometry.prototype.getLayout = function() {
};
/**
* @return {Array.<number>|Array.<Array.<number>>|Array.<Array.<Array.<number>>>}
* Render coordinates.
*/
ol.geom.SimpleGeometry.prototype.getRenderCoordinates = function() {
if (!this.renderCoordinates_) {
this.renderCoordinates_ = [];
}
return this.renderCoordinates_;
};
/**
* @inheritDoc
*/

View File

@@ -8,13 +8,14 @@ ol.render.canvas.Instruction = {
BEGIN_PATH: 1,
CIRCLE: 2,
CLOSE_PATH: 3,
DRAW_IMAGE: 4,
DRAW_TEXT: 5,
END_GEOMETRY: 6,
FILL: 7,
MOVE_TO_LINE_TO: 8,
SET_FILL_STYLE: 9,
SET_STROKE_STYLE: 10,
SET_TEXT_STYLE: 11,
STROKE: 12
CUSTOM: 4,
DRAW_IMAGE: 5,
DRAW_TEXT: 6,
END_GEOMETRY: 7,
FILL: 8,
MOVE_TO_LINE_TO: 9,
SET_FILL_STYLE: 10,
SET_STROKE_STYLE: 11,
SET_TEXT_STYLE: 12,
STROKE: 13
};

View File

@@ -37,7 +37,7 @@ ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, o
* currentLineJoin: (string|undefined),
* currentLineWidth: (number|undefined),
* currentMiterLimit: (number|undefined),
* lastStroke: number,
* lastStroke: (number|undefined),
* strokeStyle: (ol.ColorLike|undefined),
* lineCap: (string|undefined),
* lineDash: Array.<number>,
@@ -54,7 +54,7 @@ ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution, o
currentLineJoin: undefined,
currentLineWidth: undefined,
currentMiterLimit: undefined,
lastStroke: 0,
lastStroke: undefined,
strokeStyle: undefined,
lineCap: undefined,
lineDash: null,
@@ -122,10 +122,11 @@ ol.render.canvas.LineStringReplay.prototype.setStrokeStyle_ = function() {
state.currentLineJoin != lineJoin ||
state.currentLineWidth != lineWidth ||
state.currentMiterLimit != miterLimit) {
if (state.lastStroke != this.coordinates.length) {
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, lineCap, lineJoin, miterLimit, lineDash, lineDashOffset, true, 1
@@ -208,7 +209,7 @@ ol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multi
*/
ol.render.canvas.LineStringReplay.prototype.finish = function() {
var state = this.state_;
if (state.lastStroke != this.coordinates.length) {
if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
this.instructions.push([ol.render.canvas.Instruction.STROKE]);
}
this.reverseHitDetectionInstructions();

View File

@@ -4,6 +4,8 @@ goog.require('ol');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.extent.Relationship');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.obj');
@@ -174,6 +176,75 @@ ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinat
};
/**
* @param {Array.<number>} flatCoordinates Flat coordinates.
* @param {number} offset Offset.
* @param {Array.<number>} ends Ends.
* @param {number} stride Stride.
* @param {Array.<number>} replayEnds Replay ends.
* @return {number} Offset.
*/
ol.render.canvas.Replay.prototype.drawCustomCoordinates_ = function(flatCoordinates, offset, ends, stride, replayEnds) {
for (var i = 0, ii = ends.length; i < ii; ++i) {
var end = ends[i];
var replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false);
replayEnds.push(replayEnd);
offset = end;
}
return offset;
};
/**
* @inheritDoc.
*/
ol.render.canvas.Replay.prototype.drawCustom = function(geometry, feature, renderer) {
this.beginGeometry(geometry, feature);
var type = geometry.getType();
var stride = geometry.getStride();
var replayBegin = this.coordinates.length;
var flatCoordinates, replayEnd, replayEnds, replayEndss;
var offset;
if (type == ol.geom.GeometryType.MULTI_POLYGON) {
geometry = /** @type {ol.geom.MultiPolygon} */ (geometry);
flatCoordinates = geometry.getOrientedFlatCoordinates();
replayEndss = [];
var endss = geometry.getEndss();
offset = 0;
for (var i = 0, ii = endss.length; i < ii; ++i) {
var myEnds = [];
offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds);
replayEndss.push(myEnds);
}
this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
replayBegin, replayEndss, geometry, renderer, ol.geom.flat.inflate.coordinatesss]);
} else if (type == ol.geom.GeometryType.POLYGON || type == ol.geom.GeometryType.MULTI_LINE_STRING) {
replayEnds = [];
flatCoordinates = (type == ol.geom.GeometryType.POLYGON) ?
/** @type {ol.geom.Polygon} */ (geometry).getOrientedFlatCoordinates() :
geometry.getFlatCoordinates();
offset = this.drawCustomCoordinates_(flatCoordinates, 0,
/** @type {ol.geom.Polygon|ol.geom.MultiLineString} */ (geometry).getEnds(),
stride, replayEnds);
this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
replayBegin, replayEnds, geometry, renderer, ol.geom.flat.inflate.coordinatess]);
} else if (type == ol.geom.GeometryType.LINE_STRING || type == ol.geom.GeometryType.MULTI_POINT) {
flatCoordinates = geometry.getFlatCoordinates();
replayEnd = this.appendFlatCoordinates(
flatCoordinates, 0, flatCoordinates.length, stride, false, false);
this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
replayBegin, replayEnd, geometry, renderer, ol.geom.flat.inflate.coordinates]);
} else if (type == ol.geom.GeometryType.POINT) {
flatCoordinates = geometry.getFlatCoordinates();
this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
replayEnd = this.coordinates.length;
this.instructions.push([ol.render.canvas.Instruction.CUSTOM,
replayBegin, replayEnd, geometry, renderer]);
}
this.endGeometry(geometry, feature);
};
/**
* @protected
* @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
@@ -249,6 +320,17 @@ ol.render.canvas.Replay.prototype.replay_ = function(
var prevX, prevY, roundX, roundY;
var pendingFill = 0;
var pendingStroke = 0;
/**
* @type {olx.render.State}
*/
var replayState = {
context: context,
pixelRatio: pixelRatio,
resolution: this.resolution,
rotation: viewRotation
};
// When the batch size gets too big, performance decreases. 200 is a good
// balance between batch size and number of fill/stroke instructions.
var batchSize =
@@ -303,6 +385,21 @@ ol.render.canvas.Replay.prototype.replay_ = function(
context.closePath();
++i;
break;
case ol.render.canvas.Instruction.CUSTOM:
d = /** @type {number} */ (instruction[1]);
dd = instruction[2];
var geometry = /** @type {ol.geom.SimpleGeometry} */ (instruction[3]);
var renderer = instruction[4];
var coords;
if (instruction.length == 6) {
var fn = instruction[5];
coords = fn(pixelCoordinates, d, dd, 2, geometry.getRenderCoordinates());
} else {
coords = pixelCoordinates.slice(d, dd);
}
renderer(coords, geometry, feature, replayState);
++i;
break;
case ol.render.canvas.Instruction.DRAW_IMAGE:
d = /** @type {number} */ (instruction[1]);
dd = /** @type {number} */ (instruction[2]);

View File

@@ -7,6 +7,7 @@ goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.obj');
goog.require('ol.render.ReplayGroup');
goog.require('ol.render.canvas.Replay');
goog.require('ol.render.canvas.ImageReplay');
goog.require('ol.render.canvas.LineStringReplay');
goog.require('ol.render.canvas.PolygonReplay');
@@ -161,6 +162,24 @@ ol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) {
return arr;
};
/**
* @param {Array.<ol.render.ReplayType>} replays Replays.
* @return {boolean} Has replays of the provided types.
*/
ol.render.canvas.ReplayGroup.prototype.hasReplays = function(replays) {
for (var zIndex in this.replaysByZIndex_) {
var candidates = this.replaysByZIndex_[zIndex];
for (var i = 0, ii = replays.length; i < ii; ++i) {
if (replays[i] in candidates) {
return true;
}
}
}
return false;
};
/**
* FIXME empty description for jsdoc
*/
@@ -387,6 +406,7 @@ ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
*/
ol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
'Circle': ol.render.canvas.PolygonReplay,
'Default': ol.render.canvas.Replay,
'Image': ol.render.canvas.ImageReplay,
'LineString': ol.render.canvas.LineStringReplay,
'Polygon': ol.render.canvas.PolygonReplay,

View File

@@ -43,6 +43,12 @@ ol.render.Feature = function(type, flatCoordinates, ends, properties, id) {
*/
this.flatCoordinates_ = flatCoordinates;
/**
* @private
* @type {Array.<number>|Array.<Array.<number>>|Array.<Array.<Array.<number>>>}
*/
this.renderCoordinates_ = null;
/**
* @private
* @type {Array.<number>|Array.<Array.<number>>}
@@ -111,6 +117,18 @@ ol.render.Feature.prototype.getOrientedFlatCoordinates = function() {
};
/**
* @return {Array.<number>|Array.<Array.<number>>|Array.<Array.<Array.<number>>>}
* Render coordinates.
*/
ol.render.Feature.prototype.getRenderCoordinates = function() {
if (!this.renderCoordinates_) {
this.renderCoordinates_ = [];
}
return this.renderCoordinates_;
};
/**
* @return {Array.<number>} Flat coordinates.
*/

View File

@@ -12,5 +12,6 @@ ol.render.replay.ORDER = [
ol.render.ReplayType.CIRCLE,
ol.render.ReplayType.LINE_STRING,
ol.render.ReplayType.IMAGE,
ol.render.ReplayType.TEXT
ol.render.ReplayType.TEXT,
ol.render.ReplayType.DEFAULT
];

View File

@@ -6,6 +6,7 @@ goog.provide('ol.render.ReplayType');
*/
ol.render.ReplayType = {
CIRCLE: 'Circle',
DEFAULT: 'Default',
IMAGE: 'Image',
LINE_STRING: 'LineString',
POLYGON: 'Polygon',

View File

@@ -13,6 +13,16 @@ ol.render.VectorContext = function() {
};
/**
* Render a geometry with a custom renderer.
*
* @param {ol.geom.SimpleGeometry} geometry Geometry.
* @param {ol.Feature|ol.render.Feature} feature Feature.
* @param {Function} renderer Renderer.
*/
ol.render.VectorContext.prototype.drawCustom = function(geometry, feature, renderer) {};
/**
* Render a geometry.
*

View File

@@ -61,7 +61,8 @@ ol.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.TileLayer);
* @type {!Object.<string, Array.<ol.render.ReplayType>>}
*/
ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS = {
'image': ol.render.replay.ORDER,
'image': [ol.render.ReplayType.POLYGON, ol.render.ReplayType.CIRCLE,
ol.render.ReplayType.LINE_STRING, ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT],
'hybrid': [ol.render.ReplayType.POLYGON, ol.render.ReplayType.LINE_STRING]
};
@@ -71,7 +72,8 @@ ol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS = {
* @type {!Object.<string, Array.<ol.render.ReplayType>>}
*/
ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS = {
'hybrid': [ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT],
'image': [ol.render.ReplayType.DEFAULT],
'hybrid': [ol.render.ReplayType.IMAGE, ol.render.ReplayType.TEXT, ol.render.ReplayType.DEFAULT],
'vector': ol.render.replay.ORDER
};
@@ -332,61 +334,62 @@ ol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, fra
var source = layer.getSource();
var renderMode = layer.getRenderMode();
var replays = ol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode];
if (replays) {
var pixelRatio = frameState.pixelRatio;
var rotation = frameState.viewState.rotation;
var size = frameState.size;
var offsetX = Math.round(pixelRatio * size[0] / 2);
var offsetY = Math.round(pixelRatio * size[1] / 2);
var tiles = this.renderedTiles;
var tilePixelRatio = layer.getSource().getTilePixelRatio();
var sourceTileGrid = source.getTileGrid();
var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
var clips = [];
var zs = [];
for (var i = tiles.length - 1; i >= 0; --i) {
var tile = /** @type {ol.VectorImageTile} */ (tiles[i]);
if (tile.getState() == ol.TileState.ABORT) {
var pixelRatio = frameState.pixelRatio;
var rotation = frameState.viewState.rotation;
var size = frameState.size;
var offsetX = Math.round(pixelRatio * size[0] / 2);
var offsetY = Math.round(pixelRatio * size[1] / 2);
var tiles = this.renderedTiles;
var tilePixelRatio = layer.getSource().getTilePixelRatio();
var sourceTileGrid = source.getTileGrid();
var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
var clips = [];
var zs = [];
for (var i = tiles.length - 1; i >= 0; --i) {
var tile = /** @type {ol.VectorImageTile} */ (tiles[i]);
if (tile.getState() == ol.TileState.ABORT) {
continue;
}
var tileCoord = tile.tileCoord;
var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
var sourceTile = tile.getTile(tile.tileKeys[t]);
var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
if (renderMode != ol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) {
continue;
}
var tileCoord = tile.tileCoord;
var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
var sourceTile = tile.getTile(tile.tileKeys[t]);
var currentZ = sourceTile.tileCoord[0];
var sourceResolution = sourceTileGrid.getResolution(currentZ);
var transform = this.getReplayTransform_(sourceTile, frameState);
ol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResolution, 0);
var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
var currentClip = replayGroup.getClipCoords(transform);
context.save();
context.globalAlpha = layerState.opacity;
ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
// Create a clip mask for regions in this low resolution tile that are
// already filled by a higher resolution tile
for (var j = 0, jj = clips.length; j < jj; ++j) {
var clip = clips[j];
if (currentZ < zs[j]) {
context.beginPath();
// counter-clockwise (outer ring) for current tile
context.moveTo(currentClip[0], currentClip[1]);
context.lineTo(currentClip[2], currentClip[3]);
context.lineTo(currentClip[4], currentClip[5]);
context.lineTo(currentClip[6], currentClip[7]);
// clockwise (inner ring) for higher resolution tile
context.moveTo(clip[6], clip[7]);
context.lineTo(clip[4], clip[5]);
context.lineTo(clip[2], clip[3]);
context.lineTo(clip[0], clip[1]);
context.clip();
}
var currentZ = sourceTile.tileCoord[0];
var sourceResolution = sourceTileGrid.getResolution(currentZ);
var transform = this.getReplayTransform_(sourceTile, frameState);
ol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResolution, 0);
var currentClip = replayGroup.getClipCoords(transform);
context.save();
context.globalAlpha = layerState.opacity;
ol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
// Create a clip mask for regions in this low resolution tile that are
// already filled by a higher resolution tile
for (var j = 0, jj = clips.length; j < jj; ++j) {
var clip = clips[j];
if (currentZ < zs[j]) {
context.beginPath();
// counter-clockwise (outer ring) for current tile
context.moveTo(currentClip[0], currentClip[1]);
context.lineTo(currentClip[2], currentClip[3]);
context.lineTo(currentClip[4], currentClip[5]);
context.lineTo(currentClip[6], currentClip[7]);
// clockwise (inner ring) for higher resolution tile
context.moveTo(clip[6], clip[7]);
context.lineTo(clip[4], clip[5]);
context.lineTo(clip[2], clip[3]);
context.lineTo(clip[0], clip[1]);
context.clip();
}
replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays);
context.restore();
clips.push(currentClip);
zs.push(currentZ);
}
replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays);
context.restore();
clips.push(currentClip);
zs.push(currentZ);
}
}
ol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments);
@@ -466,7 +469,7 @@ ol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function(
ol.transform.translate(transform, -tileExtent[0], -tileExtent[3]);
}
var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString());
replayGroup.replay(context, pixelRatio, transform, 0, {}, replays);
replayGroup.replay(context, pixelRatio, transform, 0, {}, replays, true);
}
}
};

View File

@@ -2,6 +2,7 @@ goog.provide('ol.renderer.vector');
goog.require('ol');
goog.require('ol.ImageState');
goog.require('ol.geom.GeometryType');
goog.require('ol.render.ReplayType');
@@ -111,9 +112,34 @@ ol.renderer.vector.renderFeature_ = function(
return;
}
var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
var geometryRenderer =
ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
var renderer = style.getRenderer();
if (renderer) {
ol.renderer.vector.renderGeometry_(replayGroup, simplifiedGeometry, style, feature);
} else {
var geometryRenderer =
ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
}
};
/**
* @param {ol.render.ReplayGroup} replayGroup Replay group.
* @param {ol.geom.Geometry} geometry Geometry.
* @param {ol.style.Style} style Style.
* @param {ol.Feature|ol.render.Feature} feature Feature.
* @private
*/
ol.renderer.vector.renderGeometry_ = function(replayGroup, geometry, style, feature) {
if (geometry.getType() == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
var geometries = /** @type {ol.geom.GeometryCollection} */ (geometry).getGeometries();
for (var i = 0, ii = geometries.length; i < ii; ++i) {
ol.renderer.vector.renderGeometry_(replayGroup, geometries[i], style, feature);
}
return;
}
var replay = replayGroup.getReplay(style.getZIndex(), ol.render.ReplayType.DEFAULT);
replay.drawCustom(/** @type {ol.geom.SimpleGeometry} */ (geometry), feature, style.getRenderer());
};

View File

@@ -50,6 +50,12 @@ ol.style.Style = function(opt_options) {
*/
this.image_ = options.image !== undefined ? options.image : null;
/**
* @private
* @type {ol.StyleRenderFunction|null}
*/
this.renderer_ = options.renderer !== undefined ? options.renderer : null;
/**
* @private
* @type {ol.style.Stroke}
@@ -92,6 +98,28 @@ ol.style.Style.prototype.clone = function() {
};
/**
* Get the custom renderer function that was configured with
* {@link #setRenderer} or the `renderer` constructor option.
* @return {ol.StyleRenderFunction|null} Custom renderer function.
* @api
*/
ol.style.Style.prototype.getRenderer = function() {
return this.renderer_;
};
/**
* Sets a custom renderer function for this style. When set, `fill`, `stroke`
* and `image` optins of the style will be ignored.
* @param {ol.StyleRenderFunction|null} renderer Custom renderer function.
* @api
*/
ol.style.Style.prototype.setRenderer = function(renderer) {
this.renderer_ = renderer;
};
/**
* Get the geometry to be rendered.
* @return {string|ol.geom.Geometry|ol.StyleGeometryFunction}

View File

@@ -626,6 +626,19 @@ ol.StyleFunction;
ol.StyleGeometryFunction;
/**
* Custom renderer function. Takes 4 arguments:
*
* 1. The pixel coordinates of the geometry in GeoJSON notation.
* 2. The original {@link ol.geom.SimpleGeometry}.
* 3. The underlying {@link ol.Feature} or {@link ol.render.Feature}.
* 4. The {@link olx.render.State} of the layer renderer.
*
* @typedef {function(Array,ol.geom.SimpleGeometry,(ol.Feature|ol.render.Feature),olx.render.State)}
*/
ol.StyleRenderFunction;
/**
* @typedef {{opacity: number,
* rotateWithView: boolean,