diff --git a/examples/icon.html b/examples/icon.html new file mode 100644 index 0000000000..27bead7b5e --- /dev/null +++ b/examples/icon.html @@ -0,0 +1,68 @@ + + + + + + + + + + + Vector Icon Example + + + + + + +
+ +
+
+
+ +
+ +
+ +
+

Icon example

+

Example using an icon to symbolize a point.

+
+

See the icon.js source to see how this is done.

+
+
vector, style, icon, marker, popup
+
+ +
+ +
+ + + + + + + + diff --git a/examples/icon.js b/examples/icon.js new file mode 100644 index 0000000000..55e6a32f98 --- /dev/null +++ b/examples/icon.js @@ -0,0 +1,92 @@ +goog.require('ol.Map'); +goog.require('ol.Overlay'); +goog.require('ol.RendererHint'); +goog.require('ol.View2D'); +goog.require('ol.layer.TileLayer'); +goog.require('ol.layer.Vector'); +goog.require('ol.parser.GeoJSON'); +goog.require('ol.source.TileJSON'); +goog.require('ol.source.Vector'); +goog.require('ol.style.Icon'); +goog.require('ol.style.Style'); + + +var raster = new ol.layer.TileLayer({ + source: new ol.source.TileJSON({ + url: 'http://api.tiles.mapbox.com/v3/mapbox.geography-class.jsonp' + }) +}); + +var data = { + type: 'FeatureCollection', + features: [{ + type: 'Feature', + properties: { + name: 'Null Island', + population: 4000, + rainfall: 500 + }, + geometry: { + type: 'Point', + coordinates: [0, 0] + } + }] +}; + +var style = new ol.style.Style({ + symbolizers: [ + new ol.style.Icon({ + url: 'icon.png', + yOffset: -32 + }) + ] +}); + +var vector = new ol.layer.Vector({ + source: new ol.source.Vector({ + parser: new ol.parser.GeoJSON(), + data: data + }), + style: style +}); + +var map = new ol.Map({ + layers: [raster, vector], + renderer: ol.RendererHint.CANVAS, + target: 'map', + view: new ol.View2D({ + center: [0, 0], + zoom: 3 + }) +}); + +var element = document.getElementById('popup'); + +var popup = new ol.Overlay({ + map: map, + element: element +}); + + +map.on('click', function(evt) { + map.getFeatures({ + pixel: evt.getPixel(), + layers: [vector], + success: function(layerFeatures) { + var feature = layerFeatures[0][0]; + if (feature) { + var geometry = feature.getGeometry(); + var coord = geometry.getCoordinates(); + popup.setPosition(coord); + $(element).popover({ + 'placement': 'top', + 'html': true, + 'content': feature.get('name') + }); + $(element).popover('show'); + } else { + $(element).popover('hide'); + } + } + }); +}); diff --git a/examples/icon.png b/examples/icon.png new file mode 100644 index 0000000000..8f511d436c Binary files /dev/null and b/examples/icon.png differ diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc index cc90365820..973fd47471 100644 --- a/src/objectliterals.jsdoc +++ b/src/objectliterals.jsdoc @@ -608,7 +608,11 @@ * @property {number|ol.expr.Expression|undefined} opacity Icon opacity * (0-1). * @property {number|ol.expr.Expression|undefined} rotation Rotation in - * degrees (0-360). + * radians (positive rotation clockwise). + * @property {number|ol.expr.Expression|undefined} xOffset Pixel offset from the + * point to the center of the icon (positive values shift image left). + * @property {number|ol.expr.Expression|undefined} yOffset Pixel offset from the + * point to the center of the icon (positive values shift image down). */ /** diff --git a/src/ol/geom.exports b/src/ol/geom.exports index 4e936f0941..389bae1d6e 100644 --- a/src/ol/geom.exports +++ b/src/ol/geom.exports @@ -8,6 +8,9 @@ @exportProperty ol.geom.GeometryType.MULTIPOLYGON @exportProperty ol.geom.GeometryType.GEOMETRYCOLLECTION +@exportSymbol ol.geom.Geometry +@exportProperty ol.geom.Geometry.prototype.getCoordinates + @exportSymbol ol.geom.Point @exportSymbol ol.geom.LineString @exportSymbol ol.geom.Polygon diff --git a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js index 51bb84eb39..8d704eb2c0 100644 --- a/src/ol/renderer/canvas/canvasvectorlayerrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorlayerrenderer.js @@ -249,6 +249,7 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = var cachedTile = this.tileCache_.get(key); var symbolSizes = cachedTile[1]; var maxSymbolSize = cachedTile[2]; + var symbolOffsets = cachedTile[3]; var halfMaxWidth = maxSymbolSize[0] / 2; var halfMaxHeight = maxSymbolSize[1] / 2; var locationMin = [location[0] - halfMaxWidth, location[1] - halfMaxHeight]; @@ -264,8 +265,8 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = return; } - var candidate, geom, type, symbolBounds, symbolSize, halfWidth, halfHeight, - coordinates, j; + var candidate, geom, type, symbolBounds, symbolSize, symbolOffset, + halfWidth, halfHeight, uid, coordinates, j; for (var id in candidates) { candidate = candidates[id]; geom = candidate.getGeometry(); @@ -274,12 +275,17 @@ ol.renderer.canvas.VectorLayer.prototype.getFeaturesForPixel = type === ol.geom.GeometryType.MULTIPOINT) { // For points, check if the pixel coordinate is inside the candidate's // symbol - symbolSize = symbolSizes[goog.getUid(candidate)]; + uid = goog.getUid(candidate); + symbolSize = symbolSizes[uid]; + symbolOffset = symbolOffsets[uid]; halfWidth = symbolSize[0] / 2; halfHeight = symbolSize[1] / 2; - symbolBounds = ol.extent.boundingExtent( - [[location[0] - halfWidth, location[1] - halfHeight], - [location[0] + halfWidth, location[1] + halfHeight]]); + symbolBounds = ol.extent.boundingExtent([ + [location[0] - halfWidth - symbolOffset[0], + location[1] - halfHeight + symbolOffset[1]], + [location[0] + halfWidth - symbolOffset[0], + location[1] + halfHeight + symbolOffset[1]] + ]); coordinates = geom.getCoordinates(); if (!goog.isArray(coordinates[0])) { coordinates = [coordinates]; @@ -519,7 +525,8 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = } var symbolSizes = sketchCanvasRenderer.getSymbolSizes(), - maxSymbolSize = sketchCanvasRenderer.getMaxSymbolSize(); + maxSymbolSize = sketchCanvasRenderer.getMaxSymbolSize(), + symbolOffsets = sketchCanvasRenderer.getSymbolOffsets(); for (key in tilesToRender) { tileCoord = tilesToRender[key]; if (this.tileCache_.containsKey(key)) { @@ -531,7 +538,8 @@ ol.renderer.canvas.VectorLayer.prototype.renderFrame = (tileRange.minX - tileCoord.x) * tileSize[0], (tileCoord.y - tileRange.maxY) * tileSize[1]); // TODO: Create an ol.VectorTile subclass of ol.Tile - this.tileCache_.set(key, [tile, symbolSizes, maxSymbolSize]); + this.tileCache_.set(key, + [tile, symbolSizes, maxSymbolSize, symbolOffsets]); } finalContext.drawImage(tile, tileSize[0] * (tileCoord.x - tileRange.minX), diff --git a/src/ol/renderer/canvas/canvasvectorrenderer.js b/src/ol/renderer/canvas/canvasvectorrenderer.js index 813fce0d84..354a91803e 100644 --- a/src/ol/renderer/canvas/canvasvectorrenderer.js +++ b/src/ol/renderer/canvas/canvasvectorrenderer.js @@ -73,6 +73,12 @@ ol.renderer.canvas.VectorRenderer = */ this.symbolSizes_ = {}; + /** + * @type {Object.>} + * @private + */ + this.symbolOffsets_ = {}; + /** * @type {Array.} * @private @@ -90,6 +96,14 @@ ol.renderer.canvas.VectorRenderer.prototype.getSymbolSizes = function() { }; +/** + * @return {Object.>} Symbolizer offsets. + */ +ol.renderer.canvas.VectorRenderer.prototype.getSymbolOffsets = function() { + return this.symbolOffsets_; +}; + + /** * @return {Array.} Maximum symbolizer size. */ @@ -209,6 +223,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = content, alpha, i, ii, feature, id, size, geometry, components, j, jj, point, vec; + var xOffset = 0; + var yOffset = 0; if (symbolizer instanceof ol.style.ShapeLiteral) { content = ol.renderer.canvas.VectorRenderer.renderShape(symbolizer); alpha = 1; @@ -216,6 +232,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = content = ol.renderer.canvas.VectorRenderer.renderIcon( symbolizer, this.iconLoadedCallback_); alpha = symbolizer.opacity; + xOffset = symbolizer.xOffset; + yOffset = symbolizer.yOffset; } else { throw new Error('Unsupported symbolizer: ' + symbolizer); } @@ -228,6 +246,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = var midHeight = content.height / 2; var contentWidth = content.width * this.inverseScale_; var contentHeight = content.height * this.inverseScale_; + var contentXOffset = xOffset * this.inverseScale_; + var contentYOffset = yOffset * this.inverseScale_; context.save(); context.setTransform(1, 0, 0, 1, -midWidth, -midHeight); context.globalAlpha = alpha; @@ -238,9 +258,13 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = this.symbolSizes_[id] = goog.isDef(size) ? [Math.max(size[0], contentWidth), Math.max(size[1], contentHeight)] : [contentWidth, contentHeight]; + this.symbolOffsets_[id] = + [xOffset * this.inverseScale_, yOffset * this.inverseScale_]; this.maxSymbolSize_ = - [Math.max(this.maxSymbolSize_[0], this.symbolSizes_[id][0]), - Math.max(this.maxSymbolSize_[1], this.symbolSizes_[id][1])]; + [Math.max(this.maxSymbolSize_[0], + this.symbolSizes_[id][0] + 2 * Math.abs(contentXOffset)), + Math.max(this.maxSymbolSize_[1], + this.symbolSizes_[id][1] + 2 * Math.abs(contentYOffset))]; geometry = feature.getGeometry(); if (geometry instanceof ol.geom.Point) { components = [geometry]; @@ -253,7 +277,8 @@ ol.renderer.canvas.VectorRenderer.prototype.renderPointFeatures_ = point = components[j]; vec = [point.get(0), point.get(1), 0]; goog.vec.Mat4.multVec3(this.transform_, vec, vec); - context.drawImage(content, vec[0], vec[1], content.width, content.height); + context.drawImage(content, vec[0] + xOffset, vec[1] + yOffset, + content.width, content.height); } } context.restore(); diff --git a/src/ol/style/iconliteral.js b/src/ol/style/iconliteral.js index 833f9fa86e..63a55e6e8f 100644 --- a/src/ol/style/iconliteral.js +++ b/src/ol/style/iconliteral.js @@ -4,11 +4,13 @@ goog.require('ol.style.PointLiteral'); /** - * @typedef {{url: (string), + * @typedef {{url: string, * width: (number|undefined), * height: (number|undefined), - * opacity: (number), - * rotation: (number)}} + * opacity: number, + * rotation: number, + * xOffset: number, + * yOffset: number}} */ ol.style.IconLiteralOptions; @@ -36,6 +38,12 @@ ol.style.IconLiteral = function(options) { /** @type {number} */ this.rotation = options.rotation; + /** @type {number} */ + this.xOffset = options.xOffset; + + /** @type {number} */ + this.yOffset = options.yOffset; + }; goog.inherits(ol.style.IconLiteral, ol.style.PointLiteral); @@ -48,5 +56,7 @@ ol.style.IconLiteral.prototype.equals = function(iconLiteral) { this.width == iconLiteral.width && this.height == iconLiteral.height && this.opacity == iconLiteral.opacity && - this.rotation == iconLiteral.rotation; + this.rotation == iconLiteral.rotation && + this.xOffset == iconLiteral.xOffset && + this.yOffset == iconLiteral.yOffset; }; diff --git a/src/ol/style/iconsymbolizer.js b/src/ol/style/iconsymbolizer.js index 781c6eb73d..2443ea6d3b 100644 --- a/src/ol/style/iconsymbolizer.js +++ b/src/ol/style/iconsymbolizer.js @@ -63,6 +63,24 @@ ol.style.Icon = function(options) { (options.rotation instanceof ol.expr.Expression) ? options.rotation : new ol.expr.Literal(options.rotation); + /** + * @type {ol.expr.Expression} + * @private + */ + this.xOffset_ = !goog.isDef(options.xOffset) ? + new ol.expr.Literal(ol.style.IconDefaults.xOffset) : + (options.xOffset instanceof ol.expr.Expression) ? + options.xOffset : new ol.expr.Literal(options.xOffset); + + /** + * @type {ol.expr.Expression} + * @private + */ + this.yOffset_ = !goog.isDef(options.yOffset) ? + new ol.expr.Literal(ol.style.IconDefaults.yOffset) : + (options.yOffset instanceof ol.expr.Expression) ? + options.yOffset : new ol.expr.Literal(options.yOffset); + }; @@ -106,12 +124,20 @@ ol.style.Icon.prototype.createLiteral = function(featureOrType) { var rotation = Number(ol.expr.evaluateFeature(this.rotation_, feature)); goog.asserts.assert(!isNaN(rotation), 'rotation must be a number'); + var xOffset = Number(ol.expr.evaluateFeature(this.xOffset_, feature)); + goog.asserts.assert(!isNaN(xOffset), 'xOffset must be a number'); + + var yOffset = Number(ol.expr.evaluateFeature(this.yOffset_, feature)); + goog.asserts.assert(!isNaN(yOffset), 'yOffset must be a number'); + literal = new ol.style.IconLiteral({ url: url, width: width, height: height, opacity: opacity, - rotation: rotation + rotation: rotation, + xOffset: xOffset, + yOffset: yOffset }); } @@ -164,6 +190,24 @@ ol.style.Icon.prototype.getWidth = function() { }; +/** + * Get the xOffset. + * @return {ol.expr.Expression} Icon xOffset. + */ +ol.style.Icon.prototype.getXOffset = function() { + return this.xOffset_; +}; + + +/** + * Get the yOffset. + * @return {ol.expr.Expression} Icon yOffset. + */ +ol.style.Icon.prototype.getYOffset = function() { + return this.yOffset_; +}; + + /** * Set the height. * @param {ol.expr.Expression} height Icon height. @@ -215,10 +259,34 @@ ol.style.Icon.prototype.setWidth = function(width) { /** - * @typedef {{opacity: (number), - * rotation: (number)}} + * Set the xOffset. + * @param {ol.expr.Expression} xOffset Icon xOffset. + */ +ol.style.Icon.prototype.setXOffset = function(xOffset) { + goog.asserts.assertInstanceof(xOffset, ol.expr.Expression); + this.xOffset_ = xOffset; +}; + + +/** + * Set the yOffset. + * @param {ol.expr.Expression} yOffset Icon yOffset. + */ +ol.style.Icon.prototype.setYOffset = function(yOffset) { + goog.asserts.assertInstanceof(yOffset, ol.expr.Expression); + this.yOffset_ = yOffset; +}; + + +/** + * @typedef {{opacity: number, + * rotation: number, + * xOffset: number, + * yOffset: number}} */ ol.style.IconDefaults = { opacity: 1, - rotation: 0 + rotation: 0, + xOffset: 0, + yOffset: 0 }; diff --git a/test/spec/ol/style/iconsymbolizer.test.js b/test/spec/ol/style/iconsymbolizer.test.js index 10cc573af4..a7cee1486a 100644 --- a/test/spec/ol/style/iconsymbolizer.test.js +++ b/test/spec/ol/style/iconsymbolizer.test.js @@ -10,7 +10,9 @@ describe('ol.style.Icon', function() { width: 20, opacity: 1, rotation: 0.1, - url: 'http://example.com/1.png' + url: 'http://example.com/1.png', + xOffset: 10, + yOffset: 15 }); expect(symbolizer).to.be.a(ol.style.Icon); }); @@ -21,7 +23,9 @@ describe('ol.style.Icon', function() { width: ol.expr.parse('20'), opacity: ol.expr.parse('1'), rotation: ol.expr.parse('0.1'), - url: ol.expr.parse('"http://example.com/1.png"') + url: ol.expr.parse('"http://example.com/1.png"'), + xOffset: ol.expr.parse('xOffset'), + yOffset: ol.expr.parse('yOffset') }); expect(symbolizer).to.be.a(ol.style.Icon); }); @@ -36,7 +40,9 @@ describe('ol.style.Icon', function() { width: ol.expr.parse('widthAttr'), opacity: ol.expr.parse('opacityAttr'), rotation: ol.expr.parse('rotationAttr'), - url: ol.expr.parse('urlAttr') + url: ol.expr.parse('urlAttr'), + xOffset: ol.expr.parse('xOffset'), + yOffset: ol.expr.parse('yOffset') }); var feature = new ol.Feature({ @@ -45,6 +51,8 @@ describe('ol.style.Icon', function() { opacityAttr: 0.5, rotationAttr: 123, urlAttr: 'http://example.com/1.png', + xOffset: 20, + yOffset: 30, geometry: new ol.geom.Point([1, 2]) }); @@ -54,6 +62,8 @@ describe('ol.style.Icon', function() { expect(literal.width).to.be(.42); expect(literal.opacity).to.be(0.5); expect(literal.rotation).to.be(123); + expect(literal.xOffset).to.be(20); + expect(literal.yOffset).to.be(30); expect(literal.url).to.be('http://example.com/1.png'); }); @@ -63,6 +73,8 @@ describe('ol.style.Icon', function() { width: ol.expr.parse('20'), opacity: ol.expr.parse('1'), rotation: ol.expr.parse('0.1'), + xOffset: ol.expr.parse('10'), + yOffset: ol.expr.parse('20'), url: ol.expr.parse('"http://example.com/1.png"') }); @@ -72,6 +84,8 @@ describe('ol.style.Icon', function() { expect(literal.width).to.be(20); expect(literal.opacity).to.be(1); expect(literal.rotation).to.be(0.1); + expect(literal.xOffset).to.be(10); + expect(literal.yOffset).to.be(20); expect(literal.url).to.be('http://example.com/1.png'); }); @@ -151,6 +165,64 @@ describe('ol.style.Icon', function() { expect(literal.height).to.be(42); }); + it('applies default xOffset if none', function() { + var symbolizer = new ol.style.Icon({ + height: 10, + width: 20, + url: 'http://example.com/1.png' + }); + + var literal = symbolizer.createLiteral(ol.geom.GeometryType.POINT); + expect(literal).to.be.a(ol.style.IconLiteral); + expect(literal.xOffset).to.be(0); + }); + + it('casts xOffset to number', function() { + var symbolizer = new ol.style.Icon({ + xOffset: ol.expr.parse('xOffset'), + width: 10, + url: 'http://example.com/1.png' + }); + + var feature = new ol.Feature({ + xOffset: '42', + geometry: new ol.geom.Point([1, 2]) + }); + + var literal = symbolizer.createLiteral(feature); + expect(literal).to.be.a(ol.style.IconLiteral); + expect(literal.xOffset).to.be(42); + }); + + it('applies default yOffset if none', function() { + var symbolizer = new ol.style.Icon({ + height: 10, + width: 20, + url: 'http://example.com/1.png' + }); + + var literal = symbolizer.createLiteral(ol.geom.GeometryType.POINT); + expect(literal).to.be.a(ol.style.IconLiteral); + expect(literal.yOffset).to.be(0); + }); + + it('casts yOffset to number', function() { + var symbolizer = new ol.style.Icon({ + yOffset: ol.expr.parse('yOffset'), + width: 10, + url: 'http://example.com/1.png' + }); + + var feature = new ol.Feature({ + yOffset: '42', + geometry: new ol.geom.Point([1, 2]) + }); + + var literal = symbolizer.createLiteral(feature); + expect(literal).to.be.a(ol.style.IconLiteral); + expect(literal.yOffset).to.be(42); + }); + }); describe('#getHeight()', function() {