From 8ed3d4b690b47288bbf1cba749bd74205d032496 Mon Sep 17 00:00:00 2001 From: hu de yi Date: Mon, 11 Aug 2025 16:47:04 +0800 Subject: [PATCH 1/2] canvas layer Implement symbol.lineStrokeColor --- packages/maptalks/src/core/Canvas.ts | 40 +++++++++++++++++++ packages/maptalks/src/geometry/Geometry.ts | 4 +- packages/maptalks/src/geometry/Path.ts | 8 +++- .../symbolizers/StrokeAndFillSymbolizer.ts | 2 + packages/maptalks/src/symbol/index.ts | 2 + packages/maptalks/src/types/typings.ts | 2 + 6 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/maptalks/src/core/Canvas.ts b/packages/maptalks/src/core/Canvas.ts index 62998c30a9..13ab91023d 100644 --- a/packages/maptalks/src/core/Canvas.ts +++ b/packages/maptalks/src/core/Canvas.ts @@ -592,6 +592,20 @@ const Canvas = { } const strokePattern = style['linePatternFile']; let strokeColor = style['lineColor'] || DEFAULT_STROKE_COLOR; + //prepare for line border + let lineStrokeColor = style['lineStrokeColor']; + const lineStrokeWidth = style['lineStrokeWidth']; + ctx.lineStrokeColor = null; + ctx.lineStrokeWidth = null; + if (lineStrokeColor) { + if (Array.isArray(lineStrokeColor)) { + lineStrokeColor = Canvas.normalizeColorToRGBA(lineStrokeColor); + } + ctx.lineStrokeColor = lineStrokeColor; + } + if (lineStrokeWidth && lineStrokeWidth > 0) { + ctx.lineStrokeWidth = lineStrokeWidth; + } if (testing) { ctx.strokeStyle = '#000'; } else if (strokePattern && resources) { @@ -1110,6 +1124,30 @@ const Canvas = { if (strokeOpacity === 0) { return; } + + const drawLineBorder = () => { + if (ctx.lineStrokeColor && ctx.lineStrokeWidth && ctx.lineStrokeWidth > 0) { + const lineStrokeColor = ctx.lineStrokeColor; + const lineStrokeWidth = ctx.lineStrokeWidth; + //cache context state + const alpha = ctx.globalAlpha; + const strokeStyle = ctx.strokeStyle; + const lineWidth = ctx.lineWidth; + + if (isNumber(strokeOpacity) && strokeOpacity < 1) { + ctx.globalAlpha = 1; + ctx.globalAlpha *= strokeOpacity; + } + ctx.lineWidth = lineWidth + lineStrokeWidth * 2; + ctx.strokeStyle = lineStrokeColor; + ctx.stroke(); + //restore context state + ctx.lineWidth = lineWidth; + ctx.strokeStyle = strokeStyle; + ctx.globalAlpha = alpha; + } + } + drawLineBorder(); const offset = ctx.strokeStyle && ctx.strokeStyle['linePatternOffset']; const dx = offset ? offset[0] : 0, dy = offset ? offset[1] : 0; @@ -1989,6 +2027,8 @@ function copyProperties(ctx: CanvasRenderingContext2D, savedCtx) { ctx.shadowOffsetY = savedCtx.shadowOffsetY; ctx.strokeStyle = savedCtx.strokeStyle; ctx.lineColorIn = savedCtx.lineColorIn; + ctx.lineStrokeColor = savedCtx.lineStrokeColor; + ctx.lineStrokeWidth = savedCtx.lineStrokeWidth; } function setLineDash(ctx: CanvasRenderingContext2D, lineDashArray: number[]) { diff --git a/packages/maptalks/src/geometry/Geometry.ts b/packages/maptalks/src/geometry/Geometry.ts index 529b9899fb..e939ed1056 100644 --- a/packages/maptalks/src/geometry/Geometry.ts +++ b/packages/maptalks/src/geometry/Geometry.ts @@ -664,7 +664,9 @@ export class Geometry extends JSONAble(Eventable(Handlerable(Class))) { this._fixedExtent = new PointExtent(); } const symbol = this._sizeSymbol; - const t = (symbol && symbol['lineWidth'] || 1) / 2; + let t = (symbol && symbol['lineWidth'] || 1) / 2; + const border = (symbol && symbol['lineStrokeWidth'] || 0); + t += border; this._fixedExtent.set(-t, -t, t, t); const dx = (symbol && symbol['lineDx']) || 0; this._fixedExtent._add([dx, 0]); diff --git a/packages/maptalks/src/geometry/Path.ts b/packages/maptalks/src/geometry/Path.ts index 68b6e68a76..99d40fe9cc 100644 --- a/packages/maptalks/src/geometry/Path.ts +++ b/packages/maptalks/src/geometry/Path.ts @@ -451,19 +451,25 @@ export class Path extends Geometry { //@internal _hitTestTolerance(): number { const symbol = this._getInternalSymbol(); - let w; + let w, border = 0; if (Array.isArray(symbol)) { w = 0; + for (let i = 0; i < symbol.length; i++) { if (isNumber(symbol[i]['lineWidth'])) { if (symbol[i]['lineWidth'] > w) { w = symbol[i]['lineWidth']; } } + if (isNumber(symbol[i]['lineStrokeWidth'])) { + border = Math.max(border, symbol[i]['lineStrokeWidth']); + } } } else { w = symbol['lineWidth']; + border = symbol['lineStrokeWidth'] || 0; } + w += border * 2; return super._hitTestTolerance() + (isNumber(w) ? w / 2 : 1.5); } diff --git a/packages/maptalks/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts b/packages/maptalks/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts index 0e63f2c503..e96aaa0586 100644 --- a/packages/maptalks/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts +++ b/packages/maptalks/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts @@ -189,6 +189,8 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer { linePatternDx: getValueOrDefault(s['linePatternDx'], 0), linePatternDy: getValueOrDefault(s['linePatternDy'], 0), lineGradientProperty: getValueOrDefault(s['lineGradientProperty'], null), + lineStrokeColor: getValueOrDefault(s['lineStrokeColor'], null), + lineStrokeWidth: getValueOrDefault(s['lineStrokeWidth'], null), }; if (result['lineWidth'] === 0) { result['lineOpacity'] = 0; diff --git a/packages/maptalks/src/symbol/index.ts b/packages/maptalks/src/symbol/index.ts index dafc6a5811..db2853cadd 100644 --- a/packages/maptalks/src/symbol/index.ts +++ b/packages/maptalks/src/symbol/index.ts @@ -129,6 +129,8 @@ export type LineSymbol = { lineDx?: SymbolNumberType; lineDy?: SymbolNumberType; lineGradientProperty?: string; + lineStrokeColor?: SymbolColorType; + lineStrokeWidth?: SymbolNumberType; } export type FillSymbol = { diff --git a/packages/maptalks/src/types/typings.ts b/packages/maptalks/src/types/typings.ts index 5b52f3c438..8818b33d12 100644 --- a/packages/maptalks/src/types/typings.ts +++ b/packages/maptalks/src/types/typings.ts @@ -18,5 +18,7 @@ declare global { isMultiClip: boolean; dpr: number; lineColorIn: any; + lineStrokeColor: any; + lineStrokeWidth:any; } } From 129240193008fc53d9d9e46d35f997401786e9e6 Mon Sep 17 00:00:00 2001 From: hu de yi Date: Mon, 11 Aug 2025 16:47:14 +0800 Subject: [PATCH 2/2] spec --- .../test/geometry/symbol/SymbolSpec.js | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/packages/maptalks/test/geometry/symbol/SymbolSpec.js b/packages/maptalks/test/geometry/symbol/SymbolSpec.js index d9cb8c5f89..ec5da259a5 100644 --- a/packages/maptalks/test/geometry/symbol/SymbolSpec.js +++ b/packages/maptalks/test/geometry/symbol/SymbolSpec.js @@ -496,4 +496,68 @@ describe('SymbolSpec', function () { }, 200); }); + + describe('#2599 symbol with lineStrokeColor and lineStrokeWidth', function () { + it('line with lineStrokeColor', function (done) { + + const symbol = { + lineColor: 'black', + lineWidth: 4, + lineStrokeWidth: 4, + lineStrokeColor: 'red' + }; + layer = new maptalks.VectorLayer('layer').addTo(map); + + const c1 = map.getCenter(); + const c2 = c1.add(0.1, 0); + // eslint-disable-next-line no-unused-vars + var line = new maptalks.LineString([c1, c2], { + symbol + + }).addTo(layer); + + setTimeout(() => { + expect(layer).to.be.painted(0, 0); + expect(layer).to.be.painted(0, -3); + expect(layer).to.be.painted(0, 3); + layer.remove(); + done(); + + }, 200); + + }); + it('polygon line with lineStrokeColor', function (done) { + + const symbol = { + lineColor: 'black', + lineWidth: 4, + lineStrokeWidth: 4, + lineStrokeColor: 'red' + }; + layer = new maptalks.VectorLayer('layer').addTo(map); + + const c1 = map.getCenter(); + const c2 = c1.add(0.1, 0); + const c3 = c1.add(0.1, -0.1); + const c4 = c1.add(0, -0.1); + // eslint-disable-next-line no-unused-vars + var line = new maptalks.Polygon([[c1, c2, c3, c4]], { + symbol + + }).addTo(layer); + + setTimeout(() => { + expect(layer).to.be.painted(0, 0); + expect(layer).to.be.painted(0, -3); + expect(layer).to.be.painted(0, 3); + layer.remove(); + done(); + + }, 200); + + }); + + }); + + });