Skip to content

Commit

Permalink
implement text path (#2445)
Browse files Browse the repository at this point in the history
* implement text path

* fix typing

* textAlongDebug

* updates

* some tweak

* updates

* support collision

* add notes

* some tweak

* some tweak

* use char texture for Improve performance

* updates

* some updates

* some tweak

* fix lint

* spec
  • Loading branch information
deyihu authored Oct 24, 2024
1 parent a879826 commit a08297c
Show file tree
Hide file tree
Showing 8 changed files with 701 additions and 42 deletions.
551 changes: 543 additions & 8 deletions src/core/Canvas.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/geo/Point.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export type PointLike = Point | PointJson | PointArray;
class Point extends Position {
arrowPrePoint?: Point;
arrowNextPoint?: Point;
distance?: number;
/**
* 使用差值与另一个点进行比较,判断是否临近
*
Expand Down
14 changes: 12 additions & 2 deletions src/renderer/geometry/Painter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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) {
Expand Down
4 changes: 4 additions & 0 deletions src/renderer/geometry/symbolizers/StrokeAndFillSymbolizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -119,6 +122,7 @@ export default class StrokeAndFillSymbolizer extends CanvasSymbolizer {
if (ctx.setLineDash && Array.isArray(style['lineDasharray'])) {
ctx.setLineDash([]);
}
this._tempRenderPoints = points;
}

get2DExtent(): PointExtent {
Expand Down
119 changes: 93 additions & 26 deletions src/renderer/geometry/symbolizers/TextMarkerSymbolizer.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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());
Expand All @@ -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 {
Expand Down Expand Up @@ -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'])) {
Expand Down
7 changes: 2 additions & 5 deletions src/renderer/layer/vectorlayer/VectorLayerCanvasRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions src/symbol/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
46 changes: 45 additions & 1 deletion test/geometry/LineStringSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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);
});


});

});

0 comments on commit a08297c

Please sign in to comment.