feat(layer): pointLayer add text model

This commit is contained in:
thinkinggis 2019-12-26 22:36:50 +08:00
parent 4d98324f24
commit 73c2c36c21
15 changed files with 726 additions and 250 deletions

View File

@ -53,6 +53,7 @@ export interface ILayerModel {
render(): void;
getUninforms(): IModelUniform;
getDefaultStyle(): unknown;
getAnimateOption(): unknown;
buildModels(): IModel[];
}
export interface IModelUniform {

View File

@ -119,7 +119,7 @@ export interface IStyleAttributeInitializationOptions {
type: AttributeType;
scale?: {
field: StyleAttributeField;
values: unknown[];
values: unknown[] | string;
names: string[];
type: StyleScaleType;
callback?: (...args: any[]) => [];

View File

@ -70,6 +70,10 @@ export default class BaseModel<ChildLayerStyleOptions = {}>
throw new Error('Method not implemented.');
}
public getAnimateOption(): IModelUniform {
throw new Error('Method not implemented.');
}
public buildModels(): IModel[] {
throw new Error('Method not implemented.');
}

View File

@ -118,7 +118,7 @@ export default class FeatureScalePlugin implements ILayerPlugin {
scales.forEach((scale) => {
// 如果设置了回调, 这不需要设置让range
if (!attributeScale.callback) {
if (attributeScale.values) {
if (attributeScale.values && attributeScale.values !== 'text') {
if (
scale.option?.type === 'linear' &&
attributeScale.values.length > 2
@ -131,6 +131,7 @@ export default class FeatureScalePlugin implements ILayerPlugin {
scale.scale.range(attributeScale.values);
} else if (scale.option?.type === 'cat') {
// 如果没有设置初值且 类型为catrange ==domain;
scale.scale.range(scale.option.domain);
}
}
@ -159,20 +160,13 @@ export default class FeatureScalePlugin implements ILayerPlugin {
dataArray: IParseDataItem[],
) {
const scalekey = [field, attribute.name].join('_');
const values = attribute.scale?.values;
if (this.scaleCache[scalekey]) {
return this.scaleCache[scalekey];
}
const styleScale = this.createScale(field, dataArray);
const styleScale = this.createScale(field, values, dataArray);
this.scaleCache[scalekey] = styleScale;
// if (
// styleScale.type === StyleScaleType.VARIABLE &&
// attribute.scale?.values &&
// attribute.scale?.values.length > 0
// ) { // 只有变量初始化range
// styleScale.scale.range(attribute.scale?.values);
// }
return this.scaleCache[scalekey];
}
@ -191,7 +185,11 @@ export default class FeatureScalePlugin implements ILayerPlugin {
return [field];
}
private createScale(field: string, data?: IParseDataItem[]): IStyleScale {
private createScale(
field: string,
values: unknown[] | string | undefined,
data?: IParseDataItem[],
): IStyleScale {
// 首先查找全局默认配置例如 color
const scaleOption: IScale | undefined = this.scaleOptions[field];
const styleScale: IStyleScale = {
@ -200,6 +198,7 @@ export default class FeatureScalePlugin implements ILayerPlugin {
type: StyleScaleType.VARIABLE,
option: scaleOption,
};
if (!data || !data.length) {
if (scaleOption && scaleOption.type) {
styleScale.scale = this.createDefaultScale(scaleOption);
@ -216,9 +215,12 @@ export default class FeatureScalePlugin implements ILayerPlugin {
styleScale.type = StyleScaleType.CONSTANT;
} else {
// 根据数据类型判断 默认等分位,时间,和枚举类型
const type =
let type =
(scaleOption && scaleOption.type) || this.getDefaultType(firstValue);
if (values === 'text') {
// text 为内置变 如果是文本则为cat
type = ScaleTypes.CAT;
}
const cfg = this.createDefaultScaleConfig(type, field, data);
Object.assign(cfg, scaleOption);
styleScale.scale = this.createDefaultScale(cfg);

View File

@ -0,0 +1,33 @@
import {
CameraUniform,
CoordinateUniform,
ICameraService,
ICoordinateSystemService,
ILayer,
ILayerPlugin,
IModel,
IRendererService,
TYPES,
} from '@antv/l7-core';
import { inject, injectable } from 'inversify';
@injectable()
export default class LayerAnimateStylePlugin implements ILayerPlugin {
@inject(TYPES.ICameraService)
private readonly cameraService: ICameraService;
@inject(TYPES.IRendererService)
private readonly rendererService: IRendererService;
public apply(layer: ILayer) {
layer.hooks.beforeRender.tap('ShaderUniformPlugin', () => {
// 重新计算坐标系参数
layer.models.forEach((model: IModel) => {
model.addUniforms({
// 相机参数,包含 VP 矩阵、缩放等级
});
});
});
}
}

View File

@ -3,6 +3,7 @@ import ExtrudeModel from './extrude';
import FillModel from './fill';
import IMageModel from './image';
import NormalModel from './normal';
import TextModel from './text';
export type PointType = 'fill' | 'image' | 'normal' | 'extrude' | 'text';
@ -11,7 +12,7 @@ const PointModels: { [key in PointType]: any } = {
image: IMageModel,
normal: NormalModel,
extrude: ExtrudeModel,
text: null,
text: TextModel,
};
export default PointModels;

View File

@ -1,15 +1,264 @@
import { IModel, IModelUniform } from '@antv/l7-core';
import {
AttributeType,
BlendType,
gl,
IEncodeFeature,
ILayerConfig,
IModel,
IModelUniform,
ITexture2D,
} from '@antv/l7-core';
import { rgb2arr } from '@antv/l7-utils';
import BaseModel from '../../core/BaseModel';
import { PointFillTriangulation } from '../../core/triangulation';
import {
getGlyphQuads,
IGlyphQuad,
shapeText,
} from '../../utils/symbol-layout';
import textFrag from '../shaders/text_frag.glsl';
import textVert from '../shaders/text_vert.glsl';
interface IPointTextLayerStyleOptions {
opacity: number;
textAnchor: string;
spacing: number;
padding: [number, number];
stroke: string;
strokeWidth: number;
strokeOpacity: number;
fontWeight: string;
fontFamily: string;
textOffset: [number, number];
textAllowOverlap: boolean;
}
export function TextTriangulation(feature: IEncodeFeature) {
const coordinates = feature.coordinates as number[];
const { glyphQuads } = feature;
const vertices: number[] = [];
const indices: number[] = [];
const coord =
coordinates.length === 2
? [coordinates[0], coordinates[1], 0]
: coordinates;
glyphQuads.forEach((quad: IGlyphQuad, index: number) => {
vertices.push(
...coord,
quad.tex.x,
quad.tex.y + quad.tex.height,
quad.tl.x,
quad.tl.y,
...coord,
quad.tex.x + quad.tex.width,
quad.tex.y + quad.tex.height,
quad.tr.x,
quad.tr.y,
...coord,
quad.tex.x + quad.tex.width,
quad.tex.y,
quad.br.x,
quad.br.y,
...coord,
quad.tex.x,
quad.tex.y,
quad.bl.x,
quad.bl.y,
);
indices.push(
0 + index * 4,
1 + index * 4,
2 + index * 4,
2 + index * 4,
3 + index * 4,
0 + index * 4,
);
});
return {
vertices, // [ x, y, z, tex.x,tex.y, offset.x. offset.y]
indices,
size: 7,
};
}
export default class ExtrudeModel extends BaseModel {
export default class TextModel extends BaseModel {
private texture: ITexture2D;
public getUninforms(): IModelUniform {
throw new Error('Method not implemented.');
const {
fontWeight = 'normal',
fontFamily,
stroke,
strokeWidth,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const { canvas, fontAtlas, mapping } = this.fontService;
return {
u_opacity: 1.0,
u_sdf_map: this.texture,
u_stroke: rgb2arr(stroke),
u_halo_blur: 0.5,
u_sdf_map_size: [canvas.width, canvas.height],
u_strokeWidth: strokeWidth,
};
}
public buildModels(): IModel[] {
throw new Error('Method not implemented.');
this.initTextFont();
this.generateGlyphLayout();
this.registerBuiltinAttributes();
this.updateTexture();
return [
this.layer.buildLayerModel({
moduleName: 'pointText',
vertexShader: textVert,
fragmentShader: textFrag,
triangulation: TextTriangulation,
depth: { enable: false },
blend: this.getBlend(),
}),
];
}
protected registerBuiltinAttributes() {
throw new Error('Method not implemented.');
const viewProjection = this.cameraService.getViewProjectionMatrix();
this.styleAttributeService.registerStyleAttribute({
name: 'textOffsets',
type: AttributeType.Attribute,
descriptor: {
name: 'a_textOffsets',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.STATIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 2,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
return [vertex[5], vertex[6]];
},
},
});
// point layer size;
this.styleAttributeService.registerStyleAttribute({
name: 'size',
type: AttributeType.Attribute,
descriptor: {
name: 'a_Size',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.DYNAMIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 1,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
const { size } = feature;
return Array.isArray(size) ? [size[0]] : [size as number];
},
},
});
// point layer size;
this.styleAttributeService.registerStyleAttribute({
name: 'textUv',
type: AttributeType.Attribute,
descriptor: {
name: 'a_tex',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.DYNAMIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 2,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
return [vertex[3], vertex[4]];
},
},
});
}
private initTextFont() {
const {
fontWeight = 'normal',
fontFamily,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const data = this.layer.getEncodedData();
const characterSet: string[] = [];
data.forEach((item: IEncodeFeature) => {
let { shape = '' } = item;
shape = shape.toString();
for (const char of shape) {
// 去重
if (characterSet.indexOf(char) === -1) {
characterSet.push(char);
}
}
});
this.fontService.setFontOptions({
characterSet,
fontWeight,
fontFamily,
});
}
private generateGlyphLayout() {
const { canvas, fontAtlas, mapping } = this.fontService;
const {
spacing = 2,
textAnchor = 'center',
textOffset,
padding = [4, 4],
textAllowOverlap,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const data = this.layer.getEncodedData();
data.forEach((feature: IEncodeFeature) => {
const { coordinates, shape = '' } = feature;
const size = feature.size as number;
const fontScale = size / 24;
const shaping = shapeText(
shape.toString(),
mapping,
24,
textAnchor,
'center',
spacing,
textOffset,
);
const glyphQuads = getGlyphQuads(shaping, textOffset, false);
feature.shaping = shaping;
feature.glyphQuads = glyphQuads;
});
}
private drawGlyph() {
const {
spacing = 2,
textAnchor = 'center',
textOffset = [0, 0],
padding = [4, 4],
textAllowOverlap,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const viewProjection = this.cameraService.getViewProjectionMatrix();
}
private updateTexture() {
const { createTexture2D } = this.rendererService;
const { canvas } = this.fontService;
this.texture = createTexture2D({
data: canvas,
width: canvas.width,
height: canvas.height,
});
}
}

View File

@ -1,3 +1,5 @@
#define SDF_PX 8.0
#define EDGE_GAMMA 0.105
uniform sampler2D u_sdf_map;
uniform float u_gamma_scale : 0.5;
uniform float u_font_size : 24;
@ -5,6 +7,7 @@ uniform float u_opacity : 1.0;
uniform vec4 u_stroke : [0, 0, 0, 1];
uniform float u_strokeWidth : 2.0;
uniform float u_halo_blur : 0.5;
uniform float u_DevicePixelRatio;
varying vec4 v_color;
varying vec2 v_uv;
@ -17,7 +20,7 @@ void main() {
float fontScale = u_font_size / 24.0;
lowp float buff = (6.0 - u_strokeWidth / fontScale) / SDF_PX;
highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale);
highp float gamma = (u_halo_blur * 1.19 / SDF_PX + EDGE_GAMMA) / (fontScale * u_gamma_scale) / 1.0;
highp float gamma_scaled = gamma * v_gamma_scale;

View File

@ -1,14 +1,13 @@
#define SDF_PX 8.0
#define EDGE_GAMMA 0.105
attribute vec3 a_Position;
attribute vec2 a_tex;
attribute vec2 a_offset;
attribute vec4 a_color;
attribute float a_size;
attribute vec2 a_textOffsets;
attribute vec4 a_Color;
attribute float a_Size;
uniform vec2 u_sdf_map_size;
uniform vec2 u_viewport_size;
uniform float u_activeId : 0;
uniform vec4 u_activeColor : [1.0, 0.0, 0.0, 1.0];
uniform mat4 u_ModelMatrix;
varying vec2 v_uv;
varying float v_gamma_scale;
@ -17,18 +16,18 @@ varying vec4 v_color;
#pragma include "projection"
void main() {
v_color = a_color;
v_color = a_Color;
v_uv = a_tex / u_sdf_map_size;
// 文本缩放比例
float fontScale = a_size / 24.;
float fontScale = a_Size / 24.;
vec4 project_pos = project_position(vec4(a_Position, 1.0));
vec4 projected_position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0));
gl_Position = vec4(projected_position.xy / projected_position.w
+ a_offset * fontScale / u_viewport_size * 2., 0.0, 1.0);
+ a_textOffsets * fontScale / u_ViewportSize * 2., 0.0, 1.0);
v_gamma_scale = gl_Position.w;

View File

@ -1,177 +0,0 @@
import { AttributeType, gl, IEncodeFeature } from '@antv/l7-core';
import BaseLayer from '../core/BaseLayer';
import { getGlyphQuads, shapeText } from '../utils/symbol-layout';
import textFrag from './shaders/text_frag.glsl';
import textVert from './shaders/text_vert.glsl';
interface IPointTextLayerStyleOptions {
opacity: number;
textAnchor: string;
textOffset: [number, number];
spacing: number;
padding: [number, number];
stroke: string;
strokeWidth: number;
strokeOpacity: number;
fontWeight: string;
fontFamily: string;
textAllowOverlap: boolean;
}
export function PointTriangulation(feature: IEncodeFeature) {
const coordinates = feature.coordinates as number[];
return {
vertices: [...coordinates, ...coordinates, ...coordinates, ...coordinates],
indices: [0, 1, 2, 2, 3, 0],
size: coordinates.length,
};
}
export default class TextLayer extends BaseLayer<IPointTextLayerStyleOptions> {
public type: string = 'PointLayer';
protected getConfigSchema() {
return {
properties: {
opacity: {
type: 'number',
minimum: 0,
maximum: 1,
},
},
};
}
protected renderModels() {
const { opacity } = this.getLayerConfig();
this.models.forEach((model) =>
model.draw({
uniforms: {
u_opacity: opacity || 1.0,
},
}),
);
return this;
}
protected buildModels() {
this.registerBuiltinAttributes();
this.models = [
this.buildLayerModel({
moduleName: 'pointText',
vertexShader: textVert,
fragmentShader: textFrag,
triangulation: PointTriangulation,
depth: { enable: false },
blend: {
enable: true,
func: {
srcRGB: gl.SRC_ALPHA,
srcAlpha: 1,
dstRGB: gl.ONE_MINUS_SRC_ALPHA,
dstAlpha: 1,
},
},
}),
];
}
private registerBuiltinAttributes() {
this.styleAttributeService.registerStyleAttribute({
name: 'textOffsets',
type: AttributeType.Attribute,
descriptor: {
name: 'a_textOffsets',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.STATIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 2,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
const extrude = [-1, -1, 1, -1, 1, 1, -1, 1];
const extrudeIndex = (attributeIdx % 4) * 2;
return [extrude[extrudeIndex], extrude[extrudeIndex + 1]];
},
},
});
// point layer size;
this.styleAttributeService.registerStyleAttribute({
name: 'size',
type: AttributeType.Attribute,
descriptor: {
name: 'a_Size',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.DYNAMIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 1,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
const { size } = feature;
return Array.isArray(size) ? [size[0]] : [size as number];
},
},
});
// point layer size;
this.styleAttributeService.registerStyleAttribute({
name: 'shape',
type: AttributeType.Attribute,
descriptor: {
name: 'a_Shape',
buffer: {
// give the WebGL driver a hint that this buffer may change
usage: gl.DYNAMIC_DRAW,
data: [],
type: gl.FLOAT,
},
size: 1,
update: (
feature: IEncodeFeature,
featureIdx: number,
vertex: number[],
attributeIdx: number,
) => {
const { shape = 2 } = feature;
const shape2d = this.getLayerConfig().shape2d as string[];
const shapeIndex = shape2d.indexOf(shape as string);
return [shapeIndex];
},
},
});
}
private initTextFont() {
const { fontWeight = 'normal', fontFamily } = this.getLayerConfig();
const data = this.getEncodedData();
const characterSet: string[] = [];
data.forEach((item: IEncodeFeature) => {
let { shape = '' } = item;
shape = shape.toString();
for (const char of shape) {
// 去重
if (characterSet.indexOf(char) === -1) {
characterSet.push(char);
}
}
});
this.fontService.setFontOptions({
characterSet,
fontWeight,
fontFamily,
});
}
}

View File

@ -0,0 +1,109 @@
export interface ICollisionBox {
x1: number;
y1: number;
x2: number;
y2: number;
anchorPointX: number;
anchorPointY: number;
}
// @mapbox/grid-index 并没有类似 hitTest 的单纯获取碰撞检测结果的方法query 将导致计算大量多余的包围盒结果,因此使用改良版
import { mat4, vec4 } from 'gl-matrix';
import GridIndex from './grid-index';
// 为 viewport 加上 buffer避免边缘处的文本无法显示
const viewportPadding = 100;
/**
*
* @see https://zhuanlan.zhihu.com/p/74373214
*/
export default class CollisionIndex {
private width: number;
private height: number;
private grid: GridIndex;
private screenRightBoundary: number;
private screenBottomBoundary: number;
private gridRightBoundary: number;
private gridBottomBoundary: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
// 创建网格索引
this.grid = new GridIndex(
width + 2 * viewportPadding,
height + 2 * viewportPadding,
25,
);
this.screenRightBoundary = width + viewportPadding;
this.screenBottomBoundary = height + viewportPadding;
this.gridRightBoundary = width + 2 * viewportPadding;
this.gridBottomBoundary = height + 2 * viewportPadding;
}
public placeCollisionBox(collisionBox: ICollisionBox, mvpMatrix: mat4) {
const projectedPoint = this.project(
mvpMatrix,
collisionBox.anchorPointX,
collisionBox.anchorPointY,
);
const tlX = collisionBox.x1 + projectedPoint.x;
const tlY = collisionBox.y1 + projectedPoint.y;
const brX = collisionBox.x2 + projectedPoint.x;
const brY = collisionBox.y2 + projectedPoint.y;
if (
!this.isInsideGrid(tlX, tlY, brX, brY) ||
this.grid.hitTest(tlX, tlY, brX, brY)
) {
return {
box: [],
};
}
return {
box: [tlX, tlY, brX, brY],
};
}
public insertCollisionBox(box: number[], featureIndex: number) {
const key = { featureIndex };
this.grid.insert(key, box[0], box[1], box[2], box[3]);
}
/**
* viewport
* @param {THREE.Matrix4} mvpMatrix mvp矩阵
* @param {number} x P20 X
* @param {number} y P20 Y
* @return {Point} projectedPoint
*/
public project(mvpMatrix: mat4, x: number, y: number) {
const point = vec4.fromValues(x, y, 0, 1);
const out = vec4.create();
vec4.transformMat4(out, point, mvpMatrix);
// GL 坐标系[-1, 1] -> viewport 坐标系[width, height]
return {
x: ((out[0] / out[3] + 1) / 2) * this.width + viewportPadding,
y: ((-out[1] / out[3] + 1) / 2) * this.height + viewportPadding,
};
}
/**
* buffer
* @param {number} x1 x1
* @param {number} y1 y1
* @param {number} x2 x2
* @param {number} y2 y2
* @return {Point} isInside
*/
public isInsideGrid(x1: number, y1: number, x2: number, y2: number) {
return (
x2 >= 0 &&
x1 < this.gridRightBoundary &&
y2 >= 0 &&
y1 < this.gridBottomBoundary
);
}
}

View File

@ -0,0 +1,210 @@
interface IQueryArgs {
hitTest: boolean;
seenUids: { box: any; circle: any };
}
type CallBack = (...args: any[]) => any;
/**
* @mapbox/grid-index
* @see https://zhuanlan.zhihu.com/p/74373214
*/
class GridIndex {
private boxCells: number[][];
private xCellCount: number;
private yCellCount: number;
private boxKeys: string[];
private bboxes: number[];
private width: number;
private height: number;
private xScale: number;
private yScale: number;
private boxUid: number;
constructor(width: number, height: number, cellSize: number) {
const boxCells = this.boxCells;
this.xCellCount = Math.ceil(width / cellSize);
this.yCellCount = Math.ceil(height / cellSize);
for (let i = 0; i < this.xCellCount * this.yCellCount; i++) {
boxCells.push([]);
}
this.boxKeys = [];
this.bboxes = [];
this.width = width;
this.height = height;
this.xScale = this.xCellCount / width;
this.yScale = this.yCellCount / height;
this.boxUid = 0;
}
public insert(key: any, x1: number, y1: number, x2: number, y2: number) {
this.forEachCell(x1, y1, x2, y2, this.insertBoxCell, this.boxUid++);
this.boxKeys.push(key);
this.bboxes.push(x1);
this.bboxes.push(y1);
this.bboxes.push(x2);
this.bboxes.push(y2);
}
public query(
x1: number,
y1: number,
x2: number,
y2: number,
predicate?: CallBack,
) {
return this.queryHitTest(x1, y1, x2, y2, false, predicate);
}
public hitTest(
x1: number,
y1: number,
x2: number,
y2: number,
predicate?: CallBack,
) {
return this.queryHitTest(x1, y1, x2, y2, true, predicate);
}
private insertBoxCell(
x1: number,
y1: number,
x2: number,
y2: number,
cellIndex: number,
uid: number,
) {
this.boxCells[cellIndex].push(uid);
}
private queryHitTest(
x1: number,
y1: number,
x2: number,
y2: number,
hitTest: boolean,
predicate?: CallBack,
) {
if (x2 < 0 || x1 > this.width || y2 < 0 || y1 > this.height) {
return hitTest ? false : [];
}
const result = [];
if (x1 <= 0 && y1 <= 0 && this.width <= x2 && this.height <= y2) {
// 这一步是高效的关键,后续精确碰撞检测结果在计算文本可见性时并不需要
if (hitTest) {
return true;
}
for (let boxUid = 0; boxUid < this.boxKeys.length; boxUid++) {
result.push({
key: this.boxKeys[boxUid],
x1: this.bboxes[boxUid * 4],
y1: this.bboxes[boxUid * 4 + 1],
x2: this.bboxes[boxUid * 4 + 2],
y2: this.bboxes[boxUid * 4 + 3],
});
}
return predicate ? result.filter(predicate) : result;
}
const queryArgs = {
hitTest,
seenUids: { box: {}, circle: {} },
};
this.forEachCell(
x1,
y1,
x2,
y2,
this.queryCell,
result,
queryArgs,
predicate,
);
return hitTest ? result.length > 0 : result;
}
private queryCell(
x1: number,
y1: number,
x2: number,
y2: number,
cellIndex: number,
result: any[],
queryArgs?: any,
predicate?: CallBack,
) {
const seenUids = queryArgs.seenUids;
const boxCell = this.boxCells[cellIndex];
if (boxCell !== null) {
const bboxes = this.bboxes;
for (const boxUid of boxCell) {
if (!seenUids.box[boxUid]) {
seenUids.box[boxUid] = true;
const offset = boxUid * 4;
if (
x1 <= bboxes[offset + 2] &&
y1 <= bboxes[offset + 3] &&
x2 >= bboxes[offset + 0] &&
y2 >= bboxes[offset + 1] &&
(!predicate || predicate(this.boxKeys[boxUid]))
) {
if (queryArgs.hitTest) {
result.push(true);
return true;
}
result.push({
key: this.boxKeys[boxUid],
x1: bboxes[offset],
y1: bboxes[offset + 1],
x2: bboxes[offset + 2],
y2: bboxes[offset + 3],
});
}
}
}
}
return false;
}
private forEachCell(
x1: number,
y1: number,
x2: number,
y2: number,
fn: CallBack,
arg1: any[] | number,
arg2?: IQueryArgs,
predicate?: CallBack,
) {
const cx1 = this.convertToXCellCoord(x1);
const cy1 = this.convertToYCellCoord(y1);
const cx2 = this.convertToXCellCoord(x2);
const cy2 = this.convertToYCellCoord(y2);
for (let x = cx1; x <= cx2; x++) {
for (let y = cy1; y <= cy2; y++) {
const cellIndex = this.xCellCount * y + x;
if (fn.call(this, x1, y1, x2, y2, cellIndex, arg1, arg2, predicate)) {
return;
}
}
}
}
private convertToXCellCoord(x: number) {
return Math.max(
0,
Math.min(this.xCellCount - 1, Math.floor(x * this.xScale)),
);
}
private convertToYCellCoord(y: number) {
return Math.max(
0,
Math.min(this.yCellCount - 1, Math.floor(y * this.yScale)),
);
}
}
export default GridIndex;

View File

@ -1,3 +1,22 @@
interface IPoint {
x: number;
y: number;
}
export interface IGlyphQuad {
tr: IPoint;
tl: IPoint;
bl: IPoint;
br: IPoint;
tex: {
x: number;
y: number;
height: number;
width: number;
advance: number;
};
glyphOffset: [number, number];
}
/**
*
* @param {string} anchor
@ -181,7 +200,7 @@ export function shapeText(
textAnchor: string,
textJustify: string,
spacing: number,
translate: [number, number],
translate: [number, number] = [0, 0],
) {
// TODO处理换行
const lines = text.split('\n');
@ -215,11 +234,11 @@ export function shapeText(
export function getGlyphQuads(
shaping: any,
textOffset: [number, number],
textOffset: [number, number] = [0, 0],
alongLine: boolean,
) {
): IGlyphQuad[] {
const { positionedGlyphs } = shaping;
const quads = [];
const quads: IGlyphQuad[] = [];
for (const positionedGlyph of positionedGlyphs) {
const rect = positionedGlyph.metrics;
@ -229,7 +248,7 @@ export function getGlyphQuads(
const halfAdvance = (rect.advance * positionedGlyph.scale) / 2;
const glyphOffset = alongLine
const glyphOffset: [number, number] = alongLine
? [positionedGlyph.x + halfAdvance, positionedGlyph.y]
: [0, 0];

View File

@ -15,6 +15,7 @@ import PointImage from './components/PointImage';
import Polygon3D from './components/Polygon3D';
import ImageLayerDemo from './components/RasterImage';
import RasterLayerDemo from './components/RasterLayer';
import TextLayerDemo from './components/Text';
// @ts-ignore
storiesOf('图层', module)
@ -22,6 +23,7 @@ storiesOf('图层', module)
.add('数据更新', () => <DataUpdate />)
.add('亮度图', () => <LightDemo />)
.add('3D点', () => <Point3D />)
.add('文字', () => <TextLayerDemo />)
.add('Column', () => <Column />)
.add('图片标注', () => <PointImage />)
.add('面3d图层', () => <Polygon3D />)

View File

@ -1,61 +1,82 @@
import { PointLayer, Scene } from '@antv/l7';
import { GaodeMap, Mapbox } from '@antv/l7-maps';
import * as React from 'react';
// @ts-ignore
import data from '../data/data.json';
export default class Point3D extends React.Component {
export default class TextLayerDemo extends React.Component {
// @ts-ignore
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public componentDidMount() {
const scene = new Scene({
center: [120.19382669582967, 30.258134],
id: 'map',
pitch: 0,
type: 'mapbox',
style: 'mapbox://styles/mapbox/streets-v9',
zoom: 1,
});
const pointLayer = new PointLayer({});
const p1 = {
public async componentDidMount() {
const data = {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
properties: {
name: '中华人民共和国',
},
geometry: {
type: 'Point',
coordinates: [83.671875, 44.84029065139799],
coordinates: [103.0078125, 36.03133177633187],
},
},
{
type: 'Feature',
properties: {
name: '中华人民共和国',
},
geometry: {
type: 'Point',
coordinates: [122.6953125, 10.833305983642491],
},
},
],
};
pointLayer
.source(data)
.color('name', [
'#FFF5B8',
'#FFDC7D',
'#FFAB5C',
'#F27049',
'#D42F31',
'#730D1C',
])
.shape('subregion', [
'circle',
'triangle',
'square',
'pentagon',
'hexagon',
'octogon',
'hexagram',
'rhombus',
'vesica',
])
.size('scalerank', [2, 4, 6, 8, 10]);
const response = await fetch(
'https://gw.alipayobjects.com/os/rmsportal/oVTMqfzuuRFKiDwhPSFL.json',
);
const pointsData = await response.json();
const scene = new Scene({
id: 'map',
map: new GaodeMap({
center: [120.19382669582967, 30.258134],
pitch: 0,
style: 'dark',
zoom: 3,
}),
});
// scene.on('loaded', () => {
const pointLayer = new PointLayer({})
.source(pointsData.list, {
parser: {
type: 'json',
x: 'j',
y: 'w',
},
})
.shape('m', 'text')
.size(24)
.color('#fff')
.style({
fontWeight: 800,
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
spacing: 2, // 字符间距
padding: [4, 4], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
strokeColor: 'white', // 描边颜色
strokeWidth: 4, // 描边宽度
strokeOpacity: 1.0,
});
scene.addLayer(pointLayer);
scene.render();
this.scene = scene;
// });
}
public render() {