diff --git a/src/core/Canvas.ts b/src/core/Canvas.ts index ee203eb9e6..a8e1bf9da4 100644 --- a/src/core/Canvas.ts +++ b/src/core/Canvas.ts @@ -13,12 +13,76 @@ import { createEl } from './util/dom'; import Browser from './Browser'; import Point from '../geo/Point'; import { getFont, getAlignPoint } from './util/strings'; -import { BBOX_TEMP, resetBBOX, setBBOX } from './util/bbox'; +import { BBOX, BBOX_TEMP, getDefaultBBOX, pointsBBOX, resetBBOX, setBBOX, validateBBOX } from './util/bbox'; import Extent from '../geo/Extent'; import Size from '../geo/Size'; +import CollisionIndex from './CollisionIndex'; +import LRUCache from './util/LRUCache'; + +const charTextureLRUCache = new LRUCache(50000); +function getCharTexture(tempCtx: Ctx, style, char: string) { + const font = tempCtx.font; + const fillStyle = tempCtx.fillStyle; + const strokeStyle = tempCtx.strokeStyle; + const textHaloFill = style.textHaloFill || DEFAULT_STROKE_COLOR; + const textHaloRadius = style.textHaloRadius || 0; + let textHaloOpacity = style.textHaloOpacity; + if (!isNumber(textHaloOpacity)) { + textHaloOpacity = 1; + } + const key = `${font}_${fillStyle}_${textHaloFill}_${textHaloRadius}_${textHaloOpacity}_${char}`; + let texture = charTextureLRUCache.get(key); + if (!texture) { + const drawHalo = textHaloOpacity !== 0 && textHaloRadius !== 0; + const textSize = ((style.textSize || 14) * 1.2 + textHaloRadius) * 2; + const canvas = Canvas.createCanvas(textSize, textSize); + canvas.style.width = `${textSize / 2}px`; + canvas.style.height = `${textSize / 2}px`; + const x = textSize / 4, y = textSize / 4; + const ctx = Canvas.getCanvas2DContext(canvas); + ctx.scale(2, 2); + + ctx.font = font; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + if (drawHalo) { + ctx.strokeStyle = strokeStyle; + ctx.lineWidth = textHaloRadius * 2; + ctx.globalAlpha = 1; + ctx.globalAlpha *= textHaloOpacity; + ctx.strokeText(char, x, y); + ctx.globalAlpha = 1; + } + ctx.fillStyle = fillStyle; + ctx.fillText(char, x, y); + texture = canvas; + charTextureLRUCache.add(key, texture); + } + return texture; + +} export type Ctx = CanvasRenderingContext2D; + +type segmentType = { + p1: Point, + p2: Point, + distance: number +} + +type linechunkType = { + points: Array, + distance: number +} + +type charItemType = { + char: string, + charSize: number, + point: Point, + bbox: BBOX +} + const DEFAULT_STROKE_COLOR = '#000'; const DEFAULT_FILL_COLOR = 'rgba(255,255,255,0)'; const DEFAULT_TEXT_COLOR = '#000'; @@ -31,6 +95,307 @@ const RADIAN = Math.PI / 180; const textOffsetY = 1; const TEXT_BASELINE = 'top'; +const textCharsCollisionIndex = new CollisionIndex(); +const textPathsCollisionIndex = new CollisionIndex(); + +function getDefaultCharacterSet(): Record { + const charSet = {}; + for (let i = 32; i < 128; i++) { + const char = String.fromCharCode(i) + charSet[char] = char; + } + return charSet; +} + +/** + * 默认的字符 + */ +const defaultChars = getDefaultCharacterSet(); + +/** + * 文本是否全部是默认字符 + * @param chars + * @returns + */ +function textIsDefaultChars(chars: string[]) { + for (let i = 0, len = chars.length; i < len; i++) { + const char = chars[i]; + if (!defaultChars[char]) { + return false; + } + } + return true; +} + +function reverseChars(chars: string[]) { + if (chars.toReversed) { + return chars.toReversed(); + } + const newChars = []; + for (let i = 0, len = chars.length - 1; i <= len; i++) { + newChars[i] = chars[len - i]; + } + return newChars; +} + +/** + * 字符的旋转角度 + * @param p1 + * @param p2 + * @param char + * @param direction + * @param isDefaultChars + * @returns + */ +function getCharRotation(p1: Point, p2: Point, char: string, direction: string, isDefaultChars: boolean) { + const x0 = p1.x, y0 = p1.y; + const x1 = p2.x, y1 = p2.y; + const dx = x1 - x0; + const dy = y1 - y0; + let rad = Math.atan2(dy, dx); + let degree = rad / Math.PI * 180; + // console.log(degree); + if (direction === 'left') { + degree += 180; + rad = degree / 180 * Math.PI; + return rad; + } + if (direction === 'down' && !isDefaultChars) { + degree -= 90; + rad = degree / 180 * Math.PI; + return rad; + } + if (direction === 'up' && !isDefaultChars) { + degree += 90; + rad = degree / 180 * Math.PI; + return rad; + } + return rad; +} + +/** + * 测量字符的大小 + * @param char + * @param fontSize + * @returns + */ +function measureCharSize(char: string, fontSize: number) { + let w = fontSize, h = fontSize; + if (defaultChars[char]) { + w = fontSize / 4; + h = fontSize; + } + const d = Math.sqrt(w * w + h * h); + return d; +} + +/** + * 计算文本的长度 + * @param textName + * @param fontSize + * @returns + */ +function measureTextLength(textName: string, fontSize: number) { + let textLen = 0; + for (let i = 0, len = textName.length; i < len; i++) { + const char = textName[i]; + textLen += measureCharSize(char, fontSize); + } + return textLen; +} + +function getPercentPoint(segment: segmentType, dis: number) { + const { distance, p1, p2 } = segment; + const dx = p2.x - p1.x, + dy = p2.y - p1.y, + dh = (p2.z || 0) - (p1.z || 0); + const percent = dis / distance; + const x = p1.x + percent * dx; + const y = p1.y + percent * dy; + const z = (p1.z || 0) + percent * dh; + return new Point(x, y, z); +} + +/** + * path 分割 + * https://github.com/deyihu/lineseg + * @param points + * @param options + * @returns + */ +function lineSeg(points: Array, options: any) { + options = Object.assign({ segDistance: 1, isGeo: true }, options); + const segDistance = Math.max(options.segDistance, 0.00000000000000001); + const segments: Array = []; + let totalLen = 0, idx = 0; + for (let i = 0, len = points.length; i < len - 1; i++) { + const p1 = points[i], p2 = points[i + 1]; + const dis = p2.distanceTo(p1); + segments[idx] = { + p1, + distance: dis, + p2 + } + idx++; + totalLen += dis; + } + if (totalLen <= segDistance) { + return [{ + distance: totalLen, + points + }] + } + const len = segments.length; + const first = segments[0]; + idx = 0; + let currentPoint; + let currentLen = 0; + const lines = []; + let tempLine = [first.p1]; + while (idx < len) { + const { distance, p2 } = segments[idx]; + currentLen += distance; + if (currentLen < segDistance) { + tempLine.push(p2); + idx++; + continue; + } + if (currentLen === segDistance) { + tempLine.push(p2); + currentLen = 0; + lines.push(tempLine); + // next + tempLine = [p2]; + idx++; + continue; + } + if (currentLen > segDistance) { + const offsetLen = segDistance - (currentLen - distance); + currentPoint = getPercentPoint(segments[idx], offsetLen); + tempLine.push(currentPoint); + lines.push(tempLine); + currentLen = 0; + segments[idx].p1 = currentPoint; + segments[idx].distance = distance - offsetLen; + // next + tempLine = [currentPoint]; + } + } + if (tempLine.length) { + lines.push(tempLine); + } + const result: Array = []; + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + result[i] = { + points: line, + distance: pathDistance(line) + } + } + return result; +} + +/** + * 文本路径方向 + * @param path + * @returns + */ +function textPathDirection(path: Array) { + const len = path.length; + const first = path[0].point, last = path[len - 1].point; + const bbox = getDefaultBBOX(); + for (let i = 0, len = path.length; i < len; i++) { + const point = path[i].point; + pointsBBOX(point, bbox); + } + const [minx, miny, maxx, maxy] = bbox; + const dx = maxx - minx, dy = maxy - miny; + let ishorizontal = true; + if (dy > dx) { + ishorizontal = false; + } + if (ishorizontal) { + if (first.x < last.x) { + return 'right'; + } + return 'left'; + } else { + if (first.y < last.y) { + return 'down'; + } + return 'up' + } +} + +/** + * 获取文本沿线路径的点 + * @param chunk + * @param chars + * @param fontSize + * @returns + */ +function getTextPath(chunk: Array, chars: string[], fontSize: number, globalCollisonIndex: CollisionIndex) { + const total = pathDistance(chunk); + const result: Array = []; + let tempLen = 0; + let hasCollision = false; + textCharsCollisionIndex.clear(); + let idx = 1; + for (let i = 0, len = chars.length; i < len; i++) { + const char = chars[i]; + const charSize = measureCharSize(char, fontSize); + const d = charSize + tempLen; + for (let j = idx, len1 = chunk.length; j < len1; j++) { + const p1 = chunk[j - 1]; + const p2 = chunk[j]; + if (p2.distance < d) { + continue; + } + idx = j; + const dDistance = d - p1.distance; + const percent = dDistance / (p2.distance - p1.distance); + const dx = p2.x - p1.x, dy = p2.y - p1.y; + const point = new Point(p1.x + dx * percent, p1.y + dy * percent); + const { x, y } = point; + let bufferSize = charSize / 3; + const bbox = [x - bufferSize, y - bufferSize, x + bufferSize, y + bufferSize]; + //全局碰撞器里文本内部是否已经碰撞 + if (globalCollisonIndex && globalCollisonIndex.collides(bbox)) { + hasCollision = true; + break; + } + //文本内部是否已经碰撞 + if (textCharsCollisionIndex.collides(bbox)) { + hasCollision = true; + break; + } else { + textCharsCollisionIndex.insertBox(bbox); + } + bufferSize = charSize / 2; + result.push({ + char, + charSize, + point, + bbox: [x - bufferSize, y - bufferSize, x + bufferSize, y + bufferSize] + }); + tempLen += charSize; + break; + } + if (hasCollision) { + break; + } + } + if (hasCollision) { + return []; + } + if (total < tempLen) { + return []; + } + return result; +} + + + //推算 cubic 贝塞尔曲线片段的起终点和控制点坐标 //t0: 片段起始比例 0-1 //t1: 片段结束比例 0-1 @@ -108,12 +473,15 @@ function pathDistance(points: Array) { if (points.length < 2) { return 0; } - let distance = 0; + let total = 0; + points[0].distance = 0; for (let i = 1, len = points.length; i < len; i++) { const p1 = points[i - 1], p2 = points[i]; - distance += p1.distanceTo(p2); + const distance = p1.distanceTo(p2); + p2.distance = distance + p1.distance; + total += distance; } - return distance; + return total; } function getColorInMinStep(colorIn: any) { @@ -430,7 +798,7 @@ const Canvas = { }, normalizeColorToRGBA(fill: number[], opacity = 1) { - return `rgba(${fill[0] * 255},${fill[1] * 255},${fill[2] * 255},${(fill.length === 4 ? fill[3] : 1) * opacity})`; + return `rgba(${fill[0] * 255}, ${fill[1] * 255}, ${fill[2] * 255}, ${(fill.length === 4 ? fill[3] : 1) * opacity})`; }, image(ctx: Ctx, img: CanvasImageSource, x: number, y: number, width?: number, height?: number) { @@ -541,6 +909,173 @@ const Canvas = { ctx.fillText(text, pt.x, pt.y + textOffsetY); }, + textAlongLine(ctx: Ctx, text: string, paths: Array>, style, textDesc, globalCollisonIndex: CollisionIndex): BBOX { + if (!text) { + return; + } + + const fontSize = style.textSize || 14; + const textSpacing = style.textSpacing || 0; + + const textLen = measureTextLength(text, fontSize); + if (textSpacing < textLen) { + return; + } + if (!Array.isArray(paths[0])) { + paths = [paths] as unknown as Array>; + } + const textBaseline = ctx.textBaseline; + const textAlign = ctx.textAlign; + const strokeStyle = ctx.strokeStyle; + const globalAlpha = ctx.globalAlpha; + const lineWidth = ctx.lineWidth; + + const textAlongDebug = style.textAlongDebug; + let textHaloFill = style.textHaloFill || DEFAULT_STROKE_COLOR; + const textHaloRadius = style.textHaloRadius || 0; + let textHaloOpacity = style.textHaloOpacity; + if (!isNumber(textHaloOpacity)) { + textHaloOpacity = 1; + } + const drawHalo = textHaloOpacity !== 0 && textHaloRadius !== 0; + + ctx.textBaseline = 'middle'; + ctx.textAlign = 'center'; + if (drawHalo) { + if (Array.isArray(textHaloFill)) { + textHaloFill = Canvas.normalizeColorToRGBA(textHaloFill); + } + ctx.strokeStyle = textHaloFill; + ctx.lineWidth = textHaloRadius * 2; + } + textPathsCollisionIndex.clear(); + const charsBBOX = getDefaultBBOX(); + const charArray = text.split(''); + const isDefaultChars = textIsDefaultChars(charArray); + paths.forEach(path => { + const pathLen = pathDistance(path); + if (pathLen < textLen) { + return; + } + const lines = lineSeg(path, { segDistance: textSpacing }); + if (!lines || !lines.length) { + return; + } + for (let m = 0, len1 = lines.length; m < len1; m++) { + const chunk = lines[m]; + const chunkDistance = chunk.distance; + if (chunkDistance < textLen) { + continue; + } + const points = chunk.points; + //for debug + if (textAlongDebug) { + const fillStyle = ctx.fillStyle; + ctx.fillStyle = 'red'; + const { x, y } = points[0]; + ctx.beginPath(); + ctx.fillRect(x - 2, y - 2, 4, 4); + ctx.fillStyle = fillStyle; + } + let chars = charArray; + let items = getTextPath(points, chars, fontSize, globalCollisonIndex); + if (!items.length) { + continue; + } + + const direction = textPathDirection(items); + if (direction === 'left') { + //反向文本顺序 + chars = reverseChars(chars); + items = getTextPath(points, chars, fontSize, globalCollisonIndex); + } + if (direction === 'up' && !isDefaultChars) { + //反向文本顺序 + chars = reverseChars(chars); + items = getTextPath(points, chars, fontSize, globalCollisonIndex); + } + let hasCollision = false; + for (let i = 0, len = items.length; i < len; i++) { + const { bbox } = items[i]; + //当前pahts绘制时是否已经碰撞 + if (textPathsCollisionIndex.collides(bbox)) { + hasCollision = true; + break; + } + //全局碰撞器里是否已经碰撞了,全局碰撞器可能来自地图或者图层 + if (globalCollisonIndex && globalCollisonIndex.collides(bbox)) { + hasCollision = true; + break; + } + } + //没有通过碰撞检测 + if (hasCollision) { + continue; + } + for (let i = 0, len = items.length; i < len; i++) { + let p1, p2; + if (i === 0) { + p1 = items[i].point; + p2 = items[1].point; + } else if (i === len - 1) { + p1 = items[len - 2].point; + p2 = items[len - 1].point; + } else { + p1 = items[i - 1].point; + p2 = items[i].point; + } + const char = chars[i]; + const { point, bbox } = items[i]; + textPathsCollisionIndex.insertBox(bbox); + globalCollisonIndex && globalCollisonIndex.insertBox(bbox); + const { x, y } = point; + const rad = getCharRotation(p1, p2, char, direction, isDefaultChars); + ctx.save(); + ctx.translate(x, y); + ctx.rotate(rad); + const texture = getCharTexture(ctx, style, char); + const { width, height } = texture; + ctx.drawImage(texture, -width / 4, -height / 4, width / 2, height / 2); + // //描边 why? canvas text performance is poor + // if (drawHalo) { + // const alpha = ctx.globalAlpha; + // ctx.globalAlpha = 1; + // ctx.globalAlpha *= textHaloOpacity; + // ctx.strokeText(char, 0, 0); + // ctx.globalAlpha = alpha; + // } + // ctx.fillText(char, 0, 0); + ctx.restore(); + //合并所有的字符的包围盒 + setBBOX(charsBBOX, bbox); + //for debug + if (textAlongDebug) { + const strokeStyle = ctx.strokeStyle; + const lineWidth = ctx.lineWidth; + ctx.strokeStyle = 'red'; + ctx.lineWidth = 0.5; + const [minx, miny, maxx, maxy] = bbox; + ctx.beginPath(); + ctx.rect(minx, miny, maxx - minx, maxy - miny); + ctx.stroke(); + ctx.lineWidth = lineWidth; + ctx.strokeStyle = strokeStyle; + } + } + } + }); + //restore canvas states + ctx.textBaseline = textBaseline; + ctx.textAlign = textAlign; + ctx.strokeStyle = strokeStyle; + ctx.globalAlpha = globalAlpha; + ctx.lineWidth = lineWidth; + if (!validateBBOX(charsBBOX)) { + return; + } + return charsBBOX; + }, + //@internal _stroke(ctx, strokeOpacity, x?, y?) { if (hitTesting) { @@ -611,7 +1146,7 @@ const Canvas = { let preX, preY, currentX, currentY, nextPoint; const [r, g, b, a] = colorIn.getColor(0); - preColor = `rgba(${r},${g},${b},${a})`; + preColor = `rgba(${r}, ${g}, ${b}, ${a})`; const firstPoint = points[0]; preX = firstPoint.x; @@ -660,7 +1195,7 @@ const Canvas = { //segment的步数小于minStep if (percent <= minStep) { const [r, g, b, a] = colorIn.getColor(step + percent); - color = `rgba(${r},${g},${b},${a})`; + color = `rgba(${r}, ${g}, ${b}, ${a})`; currentX = x; currentY = y; drawSegment(); @@ -671,7 +1206,7 @@ const Canvas = { for (let n = 1; n <= segments; n++) { const tempStep = Math.min((n * minStep), percent); const [r, g, b, a] = colorIn.getColor(step + tempStep); - color = `rgba(${r},${g},${b},${a})`; + color = `rgba(${r}, ${g}, ${b}, ${a})`; if (color === preColor) { continue; } diff --git a/src/geo/Point.ts b/src/geo/Point.ts index e56c7e49dc..de5d17b36c 100644 --- a/src/geo/Point.ts +++ b/src/geo/Point.ts @@ -34,6 +34,7 @@ export type PointLike = Point | PointJson | PointArray; class Point extends Position { arrowPrePoint?: Point; arrowNextPoint?: Point; + distance?: number; /** * 使用差值与另一个点进行比较,判断是否临近 * diff --git a/src/renderer/geometry/Painter.ts b/src/renderer/geometry/Painter.ts index bb9cafd113..61fa3cd2ee 100644 --- a/src/renderer/geometry/Painter.ts +++ b/src/renderer/geometry/Painter.ts @@ -485,12 +485,12 @@ class Painter extends Class { let _2DExtent, glExtent, pitch; if (mapStateCache) { //@internal - _2DExtent = mapStateCache._2DExtent; + _2DExtent = mapStateCache._2DExtent; glExtent = mapStateCache.glExtent; pitch = mapStateCache.pitch; } else { //@internal - _2DExtent = map.get2DExtent(); + _2DExtent = map.get2DExtent(); glExtent = map.get2DExtentAtRes(map.getGLRes()); pitch = map.getPitch(); } @@ -1018,6 +1018,16 @@ class Painter extends Class { //@internal _afterPaint() { } + + getPathTempRenderPoints() { + const symbolizers = this.symbolizers || []; + for (let i = 0, len = symbolizers.length; i < len; i++) { + const symbolizer = this.symbolizers[i]; + if ((symbolizer instanceof Symbolizers.StrokeAndFillSymbolizer) && symbolizer._tempRenderPoints) { + return symbolizer._tempRenderPoints; + } + } + } } function interpolateAlt(points, orig, altitude) { diff --git a/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts b/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts index ad78dc8ab0..e4718cf19e 100644 --- a/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts +++ b/src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts @@ -22,6 +22,9 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer { _lineColorStopsKey?: string; //@internal _lineColorIn?: any; + + _tempRenderPoints: any; + static test(symbol: any, geometry: Geometry): boolean { if (!symbol) { return false; @@ -119,6 +122,7 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer { if (ctx.setLineDash && Array.isArray(style['lineDasharray'])) { ctx.setLineDash([]); } + this._tempRenderPoints = points; } get2DExtent(): PointExtent { diff --git a/src/renderer/geometry/symbolizers/TextMarkerSymbolizer.ts b/src/renderer/geometry/symbolizers/TextMarkerSymbolizer.ts index 12817c838e..89837d9e6c 100644 --- a/src/renderer/geometry/symbolizers/TextMarkerSymbolizer.ts +++ b/src/renderer/geometry/symbolizers/TextMarkerSymbolizer.ts @@ -1,5 +1,5 @@ import { DEFAULT_TEXT_SIZE } from '../../../core/Constants'; -import { isNumber, isArrayHasData, getValueOrDefault, getAlignPoint, } from '../../../core/util'; +import { isNumber, isArrayHasData, getValueOrDefault, getAlignPoint } from '../../../core/util'; import Point from '../../../geo/Point'; import PointExtent from '../../../geo/PointExtent'; import { hasFunctionDefinition } from '../../../core/mapbox'; @@ -10,9 +10,47 @@ import { replaceVariable, describeText } from '../../../core/util/strings'; import { Geometry } from '../../../geometry'; import Painter from '../Painter'; import { ResourceCache } from '../..'; +import { clipLine } from '../../../core/util/path'; const TEMP_EXTENT = new PointExtent(); +function filterPathByMapSize(paths, mapSize) { + const { width, height } = mapSize; + const buffer = 0; + const minx = -buffer, miny = -buffer, maxx = width + buffer, maxy = height + buffer; + TEMP_EXTENT.xmin = minx; + TEMP_EXTENT.ymin = miny; + TEMP_EXTENT.xmax = maxx; + TEMP_EXTENT.ymax = maxy; + if (!Array.isArray(paths[0])) { + paths = [paths]; + } + const result = []; + paths.forEach(path => { + let hasDirty = false; + for (let i = 0, len = path.length; i < len; i++) { + const { x, y } = path[i]; + if (x < minx || x > maxx || y < miny || y > maxy) { + hasDirty = true; + break; + } + } + if (hasDirty) { + const parts = clipLine(path, TEMP_EXTENT, false, false); + parts.forEach(part => { + const line = []; + for (let j = 0, len1 = part.length; j < len1; j++) { + line[j] = part[j].point; + } + result.push(line); + }); + } else { + result.push(path); + } + }); + return result; +} + export default class TextMarkerSymbolizer extends PointSymbolizer { //@internal _dynamic: any; @@ -39,6 +77,12 @@ export default class TextMarkerSymbolizer extends PointSymbolizer { this.strokeAndFill = this._defineStyle(this.translateLineAndFill(this.style)); } + isAlongLine() { + const placement = this.getPlacement(); + const textSpacing = this.style.textSpacing; + return placement === 'line' && isNumber(textSpacing) && textSpacing > 0; + } + symbolize(ctx: CanvasRenderingContext2D, resources: ResourceCache): void { if (!this.isVisible()) { return; @@ -48,10 +92,7 @@ export default class TextMarkerSymbolizer extends PointSymbolizer { this.style['textWrapWidth'] === 0)) { return; } - const cookedPoints = this._getRenderContainerPoints(); - if (!isArrayHasData(cookedPoints)) { - return; - } + const style = this.style, strokeAndFill = this.strokeAndFill; const textContent = replaceVariable(this.style['textName'], this.geometry.getProperties()); @@ -64,30 +105,54 @@ export default class TextMarkerSymbolizer extends PointSymbolizer { Canvas.prepareCanvasFont(ctx, style); const textHaloRadius = style.textHaloRadius || 0; this.rotations = []; - for (let i = 0, len = cookedPoints.length; i < len; i++) { - let p = cookedPoints[i]; - const origin = this._rotate(ctx, p, this._getRotationAt(i)); - let extent: PointExtent; - if (origin) { - //坐标对应的像素点 - const pixel = p.sub(origin); - p = origin; - const rad = this._getRotationAt(i); - const { width, height } = textDesc.size || { width: 0, height: 0 }; - const alignPoint = getAlignPoint(textDesc.size, style['textHorizontalAlignment'], style['textVerticalAlignment']); - extent = getMarkerRotationExtent(TEMP_EXTENT, rad, width, height, p, alignPoint); - extent._add(pixel); - this.rotations.push(rad); + if (this.isAlongLine()) { + const painter = this.getPainter(); + //复用path渲染的结果集 + let paths = painter.getPathTempRenderPoints(); + if (!paths) { + return; } - const bbox = Canvas.text(ctx, textContent, p, style, textDesc); - if (origin) { - this._setBBOX(ctx, extent.xmin, extent.ymin, extent.xmax, extent.ymax); - ctx.restore(); - } else { - this._setBBOX(ctx, bbox); + const map = this.getMap(); + paths = filterPathByMapSize(paths, map.getSize()); + if (paths) { + const layer = this.geometry.getLayer(); + const bbox = Canvas.textAlongLine(ctx, textContent, paths, style, textDesc, layer.options.collision ? layer.getCollisionIndex() : null); + if (bbox) { + this._setBBOX(ctx, bbox); + this._bufferBBOX(ctx, textHaloRadius); + } + } + } else { + const cookedPoints = this._getRenderContainerPoints(); + if (!isArrayHasData(cookedPoints)) { + return; + } + for (let i = 0, len = cookedPoints.length; i < len; i++) { + let p = cookedPoints[i]; + const origin = this._rotate(ctx, p, this._getRotationAt(i)); + let extent: PointExtent; + if (origin) { + //坐标对应的像素点 + const pixel = p.sub(origin); + p = origin; + const rad = this._getRotationAt(i); + const { width, height } = textDesc.size || { width: 0, height: 0 }; + const alignPoint = getAlignPoint(textDesc.size, style['textHorizontalAlignment'], style['textVerticalAlignment']); + extent = getMarkerRotationExtent(TEMP_EXTENT, rad, width, height, p, alignPoint); + extent._add(pixel); + this.rotations.push(rad); + } + const bbox = Canvas.text(ctx, textContent, p, style, textDesc); + if (origin) { + this._setBBOX(ctx, extent.xmin, extent.ymin, extent.xmax, extent.ymax); + ctx.restore(); + } else { + this._setBBOX(ctx, bbox); + } + this._bufferBBOX(ctx, textHaloRadius); } - this._bufferBBOX(ctx, textHaloRadius); } + } getPlacement(): any { @@ -151,6 +216,8 @@ export default class TextMarkerSymbolizer extends PointSymbolizer { textMaxWidth: getValueOrDefault(s['textMaxWidth'], 0), textMaxHeight: getValueOrDefault(s['textMaxHeight'], 0), + textSpacing: getValueOrDefault(s['textSpacing'], 0), + textAlongDebug: getValueOrDefault(s['textAlongDebug'], false), }; if (result['textMaxWidth'] > 0 && (!result['textWrapWidth'] || result['textWrapWidth'] > result['textMaxWidth'])) { diff --git a/src/renderer/layer/vectorlayer/VectorLayerCanvasRenderer.ts b/src/renderer/layer/vectorlayer/VectorLayerCanvasRenderer.ts index 96df3e823c..92d4efcb54 100644 --- a/src/renderer/layer/vectorlayer/VectorLayerCanvasRenderer.ts +++ b/src/renderer/layer/vectorlayer/VectorLayerCanvasRenderer.ts @@ -5,7 +5,6 @@ import OverlayLayerCanvasRenderer from './OverlayLayerCanvasRenderer'; import Extent from '../../../geo/Extent'; import PointExtent from '../../../geo/PointExtent'; import * as vec3 from '../../../core/util/vec3'; -import CollisionIndex from '../../../core/CollisionIndex'; import Canvas from '../../../core/Canvas'; import type { Painter, CollectionPainter } from '../../geometry'; import { Point } from '../../../geo'; @@ -16,7 +15,6 @@ const TEMP_EXTENT = new PointExtent(); const TEMP_VEC3: Vector3 = [] as unknown as Vector3; const TEMP_FIXEDEXTENT = new PointExtent(); const PLACEMENT_CENTER = 'center'; -const tempCollisionIndex = new CollisionIndex(); function clearCanvas(canvas: HTMLCanvasElement): CanvasRenderingContext2D { if (!canvas) { @@ -385,9 +383,8 @@ class VectorLayerRenderer extends OverlayLayerCanvasRenderer { return this; } const collisionScope = this.layer.options['collisionScope']; - const map = this.layer.getMap(); - const collisionIndex = collisionScope === 'map' ? map.getCollisionIndex() : tempCollisionIndex; - if (collisionIndex === tempCollisionIndex) { + const collisionIndex = this.layer.getCollisionIndex(); + if (collisionScope === 'layer') { collisionIndex.clear(); } const geos = this._geosToDraw; diff --git a/src/symbol/index.ts b/src/symbol/index.ts index f3a993ceff..a21f8fbe5a 100644 --- a/src/symbol/index.ts +++ b/src/symbol/index.ts @@ -96,6 +96,7 @@ export type PathMarkerSymbol = { export type TextSymbol = { textName?: string; textPlacement?: 'point' | 'vertex' | 'line' | 'vertex-first' | 'vertex-last'; + textSpacing?: number; textFaceName?: string; textFont?: string; textWeight?: string; diff --git a/test/geometry/LineStringSpec.js b/test/geometry/LineStringSpec.js index 014cc80cf4..002e675a19 100644 --- a/test/geometry/LineStringSpec.js +++ b/test/geometry/LineStringSpec.js @@ -719,7 +719,7 @@ describe('Geometry.LineString', function () { map.setView({ "center": [120.61702517, 31.17030688], "zoom": 14.838606578929996, "pitch": 0.20000000000003001, "bearing": -3 }); - + setTimeout(() => { expect(line._getPainter().symbolizers[0].rotations.length).to.be.above(0); @@ -750,4 +750,48 @@ describe('Geometry.LineString', function () { }); }); + describe('text along path', function () { + it('#573 text along path ', function (done) { + + layer.clear(); + layer.config({ + collision: true, + collisionDelay: 0, + }); + + const symbol = { + lineWidth: 8, + lineColor: 'black', + textName: '苏州湾大道', + // textName: 'Hello World', + // textPlacement?: 'point' | 'vertex' | 'line' | 'vertex-first' | 'vertex-last'; + textPlacement: 'line', + textSpacing: 500, + textFill: 'yellow', + textFaceName: '微软雅黑', + textWeight: 'bold', + textSize: 12, + textOpacity: 1, + // textDy: 10, + textHaloFill: '#000', + textHaloRadius: 1, + textHaloOpacity: 1, + // textAlongDebug: true + } + const line = new maptalks.LineString(getLineCoordinates(), { + symbol: Object.assign({}, symbol) + }).addTo(layer); + + map.setView({ + "center": getLineCoordinates()[1], "zoom": 18.530837475845765, "pitch": 0, "bearing": 4.499999999999204 + }) + setTimeout(() => { + expect(layer).to.be.painted(0, 0); + done(); + }, 1000); + }); + + + }); + });