diff --git a/examples/regularshape.html b/examples/regularshape.html index 1e3430a20a..990a9ec148 100644 --- a/examples/regularshape.html +++ b/examples/regularshape.html @@ -5,7 +5,9 @@ shortdesc: Example of some Regular Shape styles. docs: > This example shows how several regular shapes or symbols (representing `x`, `cross`, `star`, - `triangle` and `square`) can be created. + `triangle`, `square` and `stacked`) can be created. + + Style `stacked` represents possility to stack multiple shapes with offset tags: "vector, symbol, regularshape, style, square, cross, star, triangle, x" ---
diff --git a/examples/regularshape.js b/examples/regularshape.js index 6d26f9d3e3..0c194892d3 100644 --- a/examples/regularshape.js +++ b/examples/regularshape.js @@ -59,18 +59,39 @@ const styles = { radius2: 0, angle: Math.PI / 4 }) - }) + }), + 'stacked': [ + new Style({ + image: new RegularShape({ + fill: fill, + stroke: stroke, + points: 4, + radius: 5, + angle: Math.PI / 4, + displacement: [0, 10] + }) + }), + new Style({ + image: new RegularShape({ + fill: fill, + stroke: stroke, + points: 4, + radius: 10, + angle: Math.PI / 4 + }) + }) + ] }; -const styleKeys = ['x', 'cross', 'star', 'triangle', 'square']; +const styleKeys = ['x', 'cross', 'star', 'triangle', 'square', 'stacked']; const count = 250; const features = new Array(count); const e = 4500000; for (let i = 0; i < count; ++i) { const coordinates = [2 * e * Math.random() - e, 2 * e * Math.random() - e]; features[i] = new Feature(new Point(coordinates)); - features[i].setStyle(styles[styleKeys[Math.floor(Math.random() * 5)]]); + features[i].setStyle(styles[styleKeys[Math.floor(Math.random() * 6)]]); } const source = new VectorSource({ diff --git a/rendering/cases/regularshape-style/main.js b/rendering/cases/regularshape-style/main.js index 9d98bcd573..eec94e8c25 100644 --- a/rendering/cases/regularshape-style/main.js +++ b/rendering/cases/regularshape-style/main.js @@ -13,16 +13,17 @@ const vectorSource = new VectorSource(); function createFeatures(stroke, fill, offSet = [0, 0]) { let feature; feature = new Feature({ - geometry: new Point([-15 + offSet[0], 15 + offSet[1]]) + geometry: new Point([offSet[0], offSet[1]]) }); - // square + // square with offset feature.setStyle(new Style({ image: new RegularShape({ fill: fill, stroke: stroke, points: 4, radius: 10, - angle: Math.PI / 4 + angle: Math.PI / 4, + displacement: [-15, 15] }) })); vectorSource.addFeature(feature); diff --git a/src/ol/style/Circle.js b/src/ol/style/Circle.js index 59161e0456..64dc96cd3a 100644 --- a/src/ol/style/Circle.js +++ b/src/ol/style/Circle.js @@ -10,6 +10,7 @@ import RegularShape from './RegularShape.js'; * @property {import("./Fill.js").default} [fill] Fill style. * @property {number} radius Circle radius. * @property {import("./Stroke.js").default} [stroke] Stroke style. + * @property {Array} [displacement=[0,0]] displacement */ @@ -30,7 +31,8 @@ class CircleStyle extends RegularShape { points: Infinity, fill: options.fill, radius: options.radius, - stroke: options.stroke + stroke: options.stroke, + displacement: options.displacement !== undefined ? options.displacement : [0, 0] }); } @@ -45,7 +47,8 @@ class CircleStyle extends RegularShape { const style = new CircleStyle({ fill: this.getFill() ? this.getFill().clone() : undefined, stroke: this.getStroke() ? this.getStroke().clone() : undefined, - radius: this.getRadius() + radius: this.getRadius(), + displacement: this.getDisplacement().slice() }); style.setOpacity(this.getOpacity()); style.setScale(this.getScale()); diff --git a/src/ol/style/Icon.js b/src/ol/style/Icon.js index b918487247..9882e4e274 100644 --- a/src/ol/style/Icon.js +++ b/src/ol/style/Icon.js @@ -33,6 +33,7 @@ import ImageStyle from './Image.js'; * to provide the size of the image, with the `imgSize` option. * @property {Array} [offset=[0, 0]] Offset, which, together with the size and the offset origin, define the * sub-rectangle to use from the original icon image. + * @property {Array} [displacement=[0,0]] Displacement the icon * @property {import("./IconOrigin.js").default} [offsetOrigin='top-left'] Origin of the offset: `bottom-left`, `bottom-right`, * `top-left` or `top-right`. * @property {number} [opacity=1] Opacity of the icon. @@ -84,6 +85,7 @@ class Icon extends ImageStyle { opacity: opacity, rotation: rotation, scale: scale, + displacement: options.displacement !== undefined ? options.displacement : [0, 0], rotateWithView: rotateWithView }); @@ -177,7 +179,6 @@ class Icon extends ImageStyle { * @type {Array} */ this.offset_ = options.offset !== undefined ? options.offset : [0, 0]; - /** * @private * @type {import("./IconOrigin.js").default} @@ -336,6 +337,7 @@ class Icon extends ImageStyle { return this.origin_; } let offset = this.offset_; + const displacement = this.getDisplacement(); if (this.offsetOrigin_ != IconOrigin.TOP_LEFT) { const size = this.getSize(); @@ -353,6 +355,8 @@ class Icon extends ImageStyle { offset[1] = iconImageSize[1] - size[1] - offset[1]; } } + offset[0] += displacement[0]; + offset[1] += displacement[1]; this.origin_ = offset; return this.origin_; } diff --git a/src/ol/style/Image.js b/src/ol/style/Image.js index 6d8fd421b0..3e01ddc84f 100644 --- a/src/ol/style/Image.js +++ b/src/ol/style/Image.js @@ -10,6 +10,7 @@ import {abstract} from '../util.js'; * @property {boolean} rotateWithView * @property {number} rotation * @property {number} scale + * @property {Array} displacement */ @@ -51,6 +52,12 @@ class ImageStyle { */ this.scale_ = options.scale; + /** + * @private + * @type {Array} + */ + this.displacement_ = options.displacement; + } /** @@ -63,7 +70,8 @@ class ImageStyle { opacity: this.getOpacity(), scale: this.getScale(), rotation: this.getRotation(), - rotateWithView: this.getRotateWithView() + rotateWithView: this.getRotateWithView(), + displacement: this.getDisplacement().slice() }); } @@ -103,6 +111,15 @@ class ImageStyle { return this.scale_; } + /** + * Get the displacement of the shape + * @return {Array} Shape's center displacement + * @api + */ + getDisplacement() { + return this.displacement_; + } + /** * Get the anchor point in pixels. The anchor determines the center point for the * symbolizer. diff --git a/src/ol/style/RegularShape.js b/src/ol/style/RegularShape.js index b313c07ab7..30f017ef91 100644 --- a/src/ol/style/RegularShape.js +++ b/src/ol/style/RegularShape.js @@ -20,6 +20,7 @@ import ImageStyle from './Image.js'; * @property {number} [radius1] Outer radius of a star. * @property {number} [radius2] Inner radius of a star. * @property {number} [angle=0] Shape's angle in radians. A value of 0 will have one of the shape's point facing up. + * @property {Array} [displacement=[0,0]] Displacement of the shape * @property {import("./Stroke.js").default} [stroke] Stroke style. * @property {number} [rotation=0] Rotation in radians (positive rotation clockwise). * @property {boolean} [rotateWithView=false] Whether to rotate the shape with the view. @@ -61,7 +62,8 @@ class RegularShape extends ImageStyle { opacity: 1, rotateWithView: rotateWithView, rotation: options.rotation !== undefined ? options.rotation : 0, - scale: 1 + scale: 1, + displacement: options.displacement !== undefined ? options.displacement : [0, 0] }); /** @@ -160,7 +162,8 @@ class RegularShape extends ImageStyle { angle: this.getAngle(), stroke: this.getStroke() ? this.getStroke().clone() : undefined, rotation: this.getRotation(), - rotateWithView: this.getRotateWithView() + rotateWithView: this.getRotateWithView(), + displacement: this.getDisplacement().slice() }); style.setOpacity(this.getOpacity()); style.setScale(this.getScale()); @@ -353,12 +356,13 @@ class RegularShape extends ImageStyle { // canvas.width and height are rounded to the closest integer size = this.canvas_.width; const imageSize = size; + const displacement = this.getDisplacement(); this.draw_(renderOptions, context, 0, 0); this.createHitDetectionCanvas_(renderOptions); - this.anchor_ = [size / 2, size / 2]; + this.anchor_ = [size / 2 - displacement[0], size / 2 + displacement[1]]; this.size_ = [size, size]; this.imageSize_ = [imageSize, imageSize]; } diff --git a/test/spec/ol/style/icon.test.js b/test/spec/ol/style/icon.test.js index 6c790e32a1..b8e9cc35d2 100644 --- a/test/spec/ol/style/icon.test.js +++ b/test/spec/ol/style/icon.test.js @@ -97,22 +97,26 @@ describe('ol.style.Icon', function() { color: [1, 2, 3, 0.4], src: src, offset: [1, 2], - size: [10, 12] + size: [10, 12], + displacement: [5, 6] }); const clone = original.clone(); expect(original.getAnchor()).not.to.be(clone.getAnchor()); expect(original.offset_).not.to.be(clone.offset_); expect(original.getColor()).not.to.be(clone.getColor()); expect(original.getSize()).not.to.be(clone.getSize()); + expect(original.getDisplacement()).not.to.be(clone.getDisplacement()); clone.anchor_[0] = 0; clone.offset_[0] = 0; clone.color_[0] = 0; clone.size_[0] = 5; + clone.displacement_[0] = 10; expect(original.anchor_).not.to.eql(clone.anchor_); expect(original.offset_).not.to.eql(clone.offset_); expect(original.color_).not.to.eql(clone.color_); expect(original.size_).not.to.eql(clone.size_); + expect(original.displacement_).not.to.eql(clone.displacement_); }); }); @@ -229,6 +233,18 @@ describe('ol.style.Icon', function() { iconStyle.iconImage_.size_ = imageSize; expect(iconStyle.getOrigin()).to.eql([92, 20]); }); + + it('uses a top right offset origin + displacement', function() { + const iconStyle = new Icon({ + src: 'test.png', + size: size, + offset: offset, + offsetOrigin: 'top-right', + displacement: [20, 10] + }); + iconStyle.iconImage_.size_ = imageSize; + expect(iconStyle.getOrigin()).to.eql([92 + 20, 20 + 10]); + }); }); describe('#getImageSize', function() { diff --git a/test/spec/ol/style/regularshape.test.js b/test/spec/ol/style/regularshape.test.js index aaff4b2d7d..3b2c81cb1f 100644 --- a/test/spec/ol/style/regularshape.test.js +++ b/test/spec/ol/style/regularshape.test.js @@ -82,6 +82,24 @@ describe('ol.style.RegularShape', function() { expect(style.getHitDetectionImageSize()).to.eql([21, 21]); }); + it('sets default displacement [0, 0]', function() { + const style = new RegularShape({ + radius: 5 + }); + expect(style.getDisplacement()).to.an('array'); + expect(style.getDisplacement()[0]).to.eql(0); + expect(style.getDisplacement()[1]).to.eql(0); + }); + + it('can use offset', function() { + const style = new RegularShape({ + radius: 5, + displacement: [10, 20] + }); + expect(style.getDisplacement()).to.an('array'); + expect(style.getDisplacement()[0]).to.eql(10); + expect(style.getDisplacement()[1]).to.eql(20); + }); }); describe('#clone', function() { @@ -108,7 +126,8 @@ describe('ol.style.RegularShape', function() { color: '#319FD3' }), rotation: 2, - rotateWithView: true + rotateWithView: true, + displacement: [10, 20] }); original.setOpacity(0.5); original.setScale(1.5); @@ -123,6 +142,8 @@ describe('ol.style.RegularShape', function() { expect(original.getRotateWithView()).to.eql(clone.getRotateWithView()); expect(original.getScale()).to.eql(clone.getScale()); expect(original.getStroke().getColor()).to.eql(clone.getStroke().getColor()); + expect(original.getDisplacement()[0]).to.eql(clone.getDisplacement()[0]); + expect(original.getDisplacement()[1]).to.eql(clone.getDisplacement()[1]); }); it('the clone does not reference the same objects as the original', function() { @@ -132,11 +153,13 @@ describe('ol.style.RegularShape', function() { }), stroke: new Stroke({ color: '#319FD3' - }) + }), + displacement: [0, 5] }); const clone = original.clone(); expect(original.getFill()).to.not.be(clone.getFill()); expect(original.getStroke()).to.not.be(clone.getStroke()); + expect(original.getDisplacement()).to.not.be(clone.getDisplacement()); clone.getFill().setColor('#012345'); clone.getStroke().setColor('#012345');