diff --git a/src/ol/style/RegularShape.js b/src/ol/style/RegularShape.js index b4d120c8b1..2d289e5930 100644 --- a/src/ol/style/RegularShape.js +++ b/src/ol/style/RegularShape.js @@ -339,6 +339,96 @@ class RegularShape extends ImageStyle { */ unlistenImageChange(listener) {} + /** + * Calculate additional canvas size needed for the miter. + * @param {string} lineJoin Line join + * @param {number} strokeWidth Stroke width + * @param {number} miterLimit Miter limit + * @return {number} Additional canvas size needed + * @private + */ + calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit) { + if ( + strokeWidth === 0 || + this.points_ === Infinity || + (lineJoin !== 'bevel' && lineJoin !== 'miter') + ) { + return strokeWidth; + } + // m | ^ + // i | |\ . + // t >| #\ + // e | |\ \ . + // r \s\ + // | \t\ . . + // \r\ . . + // | \o\ . . . . . + // e \k\ . . . . + // | \e\ . . . . . + // d \ \ . . . . + // | _ _a_ _\# . . . + // r1 / ` . . + // | . . + // b / . . + // | . . + // / r2 . . + // | . . + // / . . + // |α . . + // / . . + // ° center + let r1 = this.radius_; + let r2 = this.radius2_ === undefined ? r1 : this.radius2_; + if (r1 < r2) { + const tmp = r1; + r1 = r2; + r2 = tmp; + } + const points = + this.radius2_ === undefined ? this.points_ : this.points_ * 2; + const alpha = (2 * Math.PI) / points; + const a = r2 * Math.sin(alpha); + const b = Math.sqrt(r2 * r2 - a * a); + const d = r1 - b; + const e = Math.sqrt(a * a + d * d); + const miterRatio = e / a; + if (lineJoin === 'miter' && miterRatio <= miterLimit) { + return miterRatio * strokeWidth; + } + // Calculate the distnce from center to the stroke corner where + // it was cut short because of the miter limit. + // l + // ----+---- <= distance from center to here is maxr + // /####|k ##\ + // /#####^#####\ + // /#### /+\# s #\ + // /### h/+++\# t #\ + // /### t/+++++\# r #\ + // /### a/+++++++\# o #\ + // /### p/++ fill +\# k #\ + ///#### /+++++^+++++\# e #\ + //#####/+++++/+\+++++\#####\ + const k = strokeWidth / 2 / miterRatio; + const l = (strokeWidth / 2) * (d / e); + const maxr = Math.sqrt((r1 + k) * (r1 + k) + l * l); + const bevelAdd = maxr - r1; + if (this.radius2_ === undefined || lineJoin === 'bevel') { + return bevelAdd * 2; + } + // If outer miter is over the miter limit the inner miter may reach through the + // center and be longer than the bevel, same calculation as above but swap r1 / r2. + const aa = r1 * Math.sin(alpha); + const bb = Math.sqrt(r1 * r1 - aa * aa); + const dd = r2 - bb; + const ee = Math.sqrt(aa * aa + dd * dd); + const innerMiterRatio = ee / aa; + if (innerMiterRatio <= miterLimit) { + const innerLength = (innerMiterRatio * strokeWidth) / 2 - r2 - r1; + return 2 * Math.max(bevelAdd, innerLength); + } + return bevelAdd * 2; + } + /** * @return {RenderOptions} The render options * @protected @@ -378,8 +468,9 @@ class RegularShape extends ImageStyle { } } + const add = this.calculateLineJoinSize_(lineJoin, strokeWidth, miterLimit); const maxRadius = Math.max(this.radius_, this.radius2_ || 0); - const size = 2 * (maxRadius + strokeWidth) + 1; + const size = Math.ceil(2 * maxRadius + add); return { strokeStyle: strokeStyle, diff --git a/test/browser/spec/ol/style/circle.test.js b/test/browser/spec/ol/style/circle.test.js index 201e94adb1..a654f6a3a0 100644 --- a/test/browser/spec/ol/style/circle.test.js +++ b/test/browser/spec/ol/style/circle.test.js @@ -7,14 +7,14 @@ describe('ol.style.Circle', function () { it('creates a canvas (no fill-style)', function () { const style = new CircleStyle({radius: 10}); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // no hit-detection image is created, because no fill style is set expect(style.getImage(1)).to.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); }); it('creates a canvas (transparent fill-style)', function () { @@ -25,14 +25,14 @@ describe('ol.style.Circle', function () { }), }); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // hit-detection image is created, because transparent fill style is set expect(style.getImage(1)).to.not.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); }); it('creates a canvas (non-transparent fill-style)', function () { @@ -43,14 +43,14 @@ describe('ol.style.Circle', function () { }), }); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // no hit-detection image is created, because non-transparent fill style is set expect(style.getImage(1)).to.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); }); }); diff --git a/test/browser/spec/ol/style/regularshape.test.js b/test/browser/spec/ol/style/regularshape.test.js index ce95d29a5f..6793ba31cc 100644 --- a/test/browser/spec/ol/style/regularshape.test.js +++ b/test/browser/spec/ol/style/regularshape.test.js @@ -33,14 +33,14 @@ describe('ol.style.RegularShape', function () { it('creates a canvas (no fill-style)', function () { const style = new RegularShape({radius: 10}); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // no hit-detection image is created, because no fill style is set expect(style.getImage(1)).to.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); }); it('creates a canvas (transparent fill-style)', function () { @@ -51,18 +51,18 @@ describe('ol.style.RegularShape', function () { }), }); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getImage(1).width).to.be(21); - expect(style.getImage(2).width).to.be(42); + expect(style.getImage(1).width).to.be(20); + expect(style.getImage(2).width).to.be(40); expect(style.getPixelRatio(2)).to.be(2); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // hit-detection image is created, because transparent fill style is set expect(style.getImage(1)).to.not.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); - expect(style.getHitDetectionImage().width).to.be(21); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); + expect(style.getHitDetectionImage().width).to.be(20); }); it('creates a canvas (non-transparent fill-style)', function () { @@ -73,14 +73,14 @@ describe('ol.style.RegularShape', function () { }), }); expect(style.getImage(1)).to.be.an(HTMLCanvasElement); - expect(style.getSize()).to.eql([21, 21]); - expect(style.getImageSize()).to.eql([21, 21]); + expect(style.getSize()).to.eql([20, 20]); + expect(style.getImageSize()).to.eql([20, 20]); expect(style.getOrigin()).to.eql([0, 0]); - expect(style.getAnchor()).to.eql([10.5, 10.5]); + expect(style.getAnchor()).to.eql([10, 10]); // no hit-detection image is created, because non-transparent fill style is set expect(style.getImage(1)).to.be(style.getHitDetectionImage()); expect(style.getHitDetectionImage()).to.be.an(HTMLCanvasElement); - expect(style.getHitDetectionImageSize()).to.eql([21, 21]); + expect(style.getHitDetectionImageSize()).to.eql([20, 20]); }); it('sets default displacement [0, 0]', function () { @@ -228,4 +228,67 @@ describe('ol.style.RegularShape', function () { expect(canvas.closePath.callCount).to.be(1); }); }); + + describe('#calculateLineJoinSize_', function () { + function create({ + radius = 10, + radius2, + points = 4, + strokeWidth = 10, + lineJoin = 'miter', + miterLimit = 10, + }) { + return new RegularShape({ + radius, + radius2, + points, + stroke: new Stroke({ + color: 'red', + width: strokeWidth, + lineJoin, + miterLimit, + }), + }); + } + describe('polygon', function () { + it('sets size to diameter', function () { + const style = create({strokeWidth: 0}); + expect(style.getSize()).to.eql([20, 20]); + }); + it('sets size to diameter rounded up', function () { + const style = create({radius: 9.9, strokeWidth: 0}); + expect(style.getSize()).to.eql([20, 20]); + }); + it('sets size to diameter plus miter', function () { + const style = create({}); + expect(style.getSize()).to.eql([35, 35]); + }); + it('sets size to diameter plus miter with miter limit', function () { + const style = create({miterLimit: 0}); + expect(style.getSize()).to.eql([28, 28]); + }); + it('sets size to diameter plus bevel', function () { + const style = create({lineJoin: 'bevel'}); + expect(style.getSize()).to.eql([28, 28]); + }); + it('sets size to diameter plus stroke width with round line join', function () { + const style = create({lineJoin: 'round'}); + expect(style.getSize()).to.eql([30, 30]); + }); + }); + describe('star', function () { + it('sets size to diameter plus miter r1 > r2', function () { + const style = create({radius2: 1, miterLimit: 100}); + expect(style.getSize()).to.eql([152, 152]); + }); + it('sets size to diameter plus miter r1 < r2', function () { + const style = create({radius2: 2, points: 7, miterLimit: 100}); + expect(style.getSize()).to.eql([116, 116]); + }); + it('sets size with spokes through center and outer bevel', function () { + const style = create({radius2: 80, points: 9, strokeWidth: 90}); + expect(style.getSize()).to.eql([213, 213]); + }); + }); + }); });