feat: point text add overlap

This commit is contained in:
thinkinggis 2019-12-30 15:35:47 +08:00
parent d33d599f50
commit a18561e548
19 changed files with 202 additions and 115 deletions

View File

@ -47,7 +47,7 @@ const defaultLayerConfig: Partial<ILayerConfig> = {
],
shape3d: ['cylinder', 'triangleColumn', 'hexagonColumn', 'squareColumn'],
minZoom: 0,
maxZoom: 20,
maxZoom: 24,
visible: true,
autoFit: false,
zIndex: 0,

View File

@ -55,6 +55,7 @@ export interface ILayerModel {
getDefaultStyle(): unknown;
getAnimateUniforms(): IModelUniform;
buildModels(): IModel[];
needUpdate(): boolean;
}
export interface IModelUniform {
[key: string]: IUniform;

View File

@ -161,7 +161,9 @@ export default class Scene extends EventEmitter implements ISceneService {
this.$container as HTMLDivElement,
this.handleWindowResized,
);
// window.addEventListener('resize', this.handleWindowResized, false);
window
.matchMedia('screen and (-webkit-min-device-pixel-ratio: 1.5)')
.addListener(this.handleWindowResized);
} else {
this.logger.error('容器 id 不存在');
}
@ -227,7 +229,9 @@ export default class Scene extends EventEmitter implements ISceneService {
this.rendererService.destroy();
this.map.destroy();
unbind(this.$container as HTMLDivElement, this.handleWindowResized);
// window.removeEventListener('resize', this.handleWindowResized, false);
window
.matchMedia('screen and (min-resolution: 2dppx)')
.removeListener(this.handleWindowResized);
}
private handleWindowResized = () => {

View File

@ -759,7 +759,16 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
}
protected renderModels() {
throw new Error('Method not implemented.');
if (this.layerModelNeedUpdate) {
this.models = this.layerModel.buildModels();
this.layerModelNeedUpdate = false;
}
this.models.forEach((model) => {
model.draw({
uniforms: this.layerModel.getUninforms(),
});
});
return this;
}
protected getModelType(): unknown {

View File

@ -77,6 +77,9 @@ export default class BaseModel<ChildLayerStyleOptions = {}>
return {};
}
public needUpdate(): boolean {
return false;
}
public buildModels(): IModel[] {
throw new Error('Method not implemented.');
}

View File

@ -21,8 +21,12 @@ export default class HeatMapLayer extends BaseLayer<IHeatMapLayerStyleOptions> {
protected renderModels() {
const shape = this.getModelType();
if (shape === 'heatmap') {
this.layerModel.render();
return;
this.layerModel.render(); // 独立的渲染流程
return this;
}
if (this.layerModelNeedUpdate) {
this.models = this.layerModel.buildModels();
this.layerModelNeedUpdate = false;
}
this.models.forEach((model) =>
model.draw({

View File

@ -20,8 +20,8 @@ import MultiPassRendererPlugin from './plugins/MultiPassRendererPlugin';
import PixelPickingPlugin from './plugins/PixelPickingPlugin';
import RegisterStyleAttributePlugin from './plugins/RegisterStyleAttributePlugin';
import ShaderUniformPlugin from './plugins/ShaderUniformPlugin';
import UpdateModelPlugin from './plugins/UpdateModelPlugin';
import UpdateStyleAttributePlugin from './plugins/UpdateStyleAttributePlugin';
/**
*
* @see /dev-docs/ConfigSchemaValidation.md
@ -74,6 +74,15 @@ container
.bind<ILayerPlugin>(TYPES.ILayerPlugin)
.to(UpdateStyleAttributePlugin)
.inRequestScope();
/**
* Model更新
*/
container
.bind<ILayerPlugin>(TYPES.ILayerPlugin)
.to(UpdateModelPlugin)
.inRequestScope();
/**
* Multi Pass 线
*/

View File

@ -26,14 +26,6 @@ export default class LineLayer extends BaseLayer<ILineLayerStyleOptions> {
};
return defaultConfig[type];
}
protected renderModels() {
this.models.forEach((model) =>
model.draw({
uniforms: this.layerModel.getUninforms(),
}),
);
return this;
}
protected buildModels() {
const shape = this.getModelType();

View File

@ -2,6 +2,9 @@ import { ILayer, ILayerPlugin, IMapService, TYPES } from '@antv/l7-core';
import Source from '@antv/l7-source';
import { encodePickingColor, rgb2arr } from '@antv/l7-utils';
import { injectable } from 'inversify';
/**
*
*/
@injectable()
export default class LayerStylePlugin implements ILayerPlugin {
public apply(layer: ILayer) {

View File

@ -0,0 +1,17 @@
import { ILayer, ILayerPlugin, IMapService, TYPES } from '@antv/l7-core';
import { injectable } from 'inversify';
/**
* Model
*/
@injectable()
export default class UpdateModelPlugin implements ILayerPlugin {
public apply(layer: ILayer) {
layer.hooks.beforeRender.tap('UpdateModelPlugin', () => {
// 处理文本更新
layer.layerModel.needUpdate();
// if (layer.layerModel.needUpdate()) {
// layer.layerModelNeedUpdate = true;
// }
});
}
}

View File

@ -28,23 +28,12 @@ export default class PointLayer extends BaseLayer<IPointLayerStyleOptions> {
fill: {},
extrude: {},
image: {},
text: {},
text: {
blend: 'normal',
},
};
return defaultConfig[type];
}
protected renderModels() {
if (this.layerModelNeedUpdate) {
this.models = this.layerModel.buildModels();
this.layerModelNeedUpdate = false;
}
this.models.forEach((model) => {
model.draw({
uniforms: this.layerModel.getUninforms(),
});
});
return this;
}
protected buildModels() {
const modelType = this.getModelType();
this.layerModel = new PointModels[modelType](this);
@ -79,9 +68,4 @@ export default class PointLayer extends BaseLayer<IPointLayerStyleOptions> {
return 'text';
}
}
private updateData() {
// const bounds = this.mapService.getBounds();
// console.log(bounds);
}
}

View File

@ -21,15 +21,7 @@ export default class ExtrudeModel extends BaseModel {
vertexShader: pointExtrudeVert,
fragmentShader: pointExtrudeFrag,
triangulation: PointExtrudeTriangulation,
blend: {
enable: true,
func: {
srcRGB: gl.SRC_ALPHA,
srcAlpha: 1,
dstRGB: gl.ONE_MINUS_SRC_ALPHA,
dstAlpha: 1,
},
},
blend: this.getBlend(),
}),
];
}

View File

@ -42,15 +42,7 @@ export default class ImageModel extends BaseModel {
triangulation: PointImageTriangulation,
primitive: gl.POINTS,
depth: { enable: false },
blend: {
enable: true,
func: {
srcRGB: gl.SRC_ALPHA,
srcAlpha: 1,
dstRGB: gl.ONE_MINUS_SRC_ALPHA,
dstAlpha: 1,
},
},
blend: this.getBlend(),
}),
];
}

View File

@ -3,6 +3,7 @@ import {
BlendType,
gl,
IEncodeFeature,
ILayer,
ILayerConfig,
IModel,
IModelUniform,
@ -10,7 +11,7 @@ import {
} from '@antv/l7-core';
import { rgb2arr } from '@antv/l7-utils';
import BaseModel from '../../core/BaseModel';
import { PointFillTriangulation } from '../../core/triangulation';
import CollisionIndex from '../../utils/collision-index';
import {
getGlyphQuads,
IGlyphQuad,
@ -81,14 +82,18 @@ export function TextTriangulation(feature: IEncodeFeature) {
export default class TextModel extends BaseModel {
private texture: ITexture2D;
private glyphInfo: IEncodeFeature[];
private currentZoom: number = -1;
private extent: [[number, number], [number, number]];
public getUninforms(): IModelUniform {
const {
fontWeight = 'normal',
fontWeight = 800,
fontFamily,
stroke,
strokeWidth,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const { canvas, fontAtlas, mapping } = this.fontService;
const { canvas } = this.fontService;
return {
u_opacity: 1.0,
u_sdf_map: this.texture,
@ -100,10 +105,9 @@ export default class TextModel extends BaseModel {
}
public buildModels(): IModel[] {
this.initTextFont();
this.generateGlyphLayout();
this.registerBuiltinAttributes();
this.initGlyph();
this.updateTexture();
this.filterGlyphs();
return [
this.layer.buildLayerModel({
moduleName: 'pointText',
@ -115,9 +119,36 @@ export default class TextModel extends BaseModel {
}),
];
}
public needUpdate() {
const {
textAllowOverlap = false,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const zoom = this.mapService.getZoom();
const extent = this.mapService.getBounds();
const flag =
extent[0][0] < this.extent[0][0] ||
extent[0][1] < this.extent[0][1] ||
extent[1][0] > this.extent[1][0] ||
extent[1][1] < this.extent[1][1];
if (!textAllowOverlap && (Math.abs(this.currentZoom - zoom) > 1 || flag)) {
this.filterGlyphs();
this.layer.models = [
this.layer.buildLayerModel({
moduleName: 'pointText',
vertexShader: textVert,
fragmentShader: textFrag,
triangulation: TextTriangulation,
depth: { enable: false },
blend: this.getBlend(),
}),
];
return true;
}
return false;
}
protected registerBuiltinAttributes() {
const viewProjection = this.cameraService.getViewProjectionMatrix();
this.styleAttributeService.registerStyleAttribute({
name: 'textOffsets',
type: AttributeType.Attribute,
@ -190,6 +221,18 @@ export default class TextModel extends BaseModel {
},
});
}
private textExtent(): [[number, number], [number, number]] {
const bounds = this.mapService.getBounds();
const step =
Math.min(bounds[1][0] - bounds[0][0], bounds[1][1] - bounds[1][0]) / 2;
return [
[bounds[0][0] - step, bounds[0][1] - step],
[bounds[1][0] + step, bounds[1][1] + step],
];
}
/**
*
*/
private initTextFont() {
const {
fontWeight = 'normal',
@ -213,20 +256,19 @@ export default class TextModel extends BaseModel {
fontFamily,
});
}
/**
*
*/
private generateGlyphLayout() {
const { canvas, fontAtlas, mapping } = this.fontService;
const { 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;
this.glyphInfo = data.map((feature: IEncodeFeature) => {
const { shape = '' } = feature;
const shaping = shapeText(
shape.toString(),
mapping,
@ -239,19 +281,60 @@ export default class TextModel extends BaseModel {
const glyphQuads = getGlyphQuads(shaping, textOffset, false);
feature.shaping = shaping;
feature.glyphQuads = glyphQuads;
return feature;
});
}
private drawGlyph() {
/**
*
*/
private filterGlyphs() {
const {
spacing = 2,
textAnchor = 'center',
textOffset = [0, 0],
padding = [4, 4],
textAllowOverlap,
textAllowOverlap = false,
} = this.layer.getLayerConfig() as IPointTextLayerStyleOptions;
const viewProjection = this.cameraService.getViewProjectionMatrix();
if (textAllowOverlap) {
return;
}
this.currentZoom = this.mapService.getZoom();
this.extent = this.textExtent();
const { width, height } = this.rendererService.getViewportSize();
const collisionIndex = new CollisionIndex(width, height);
const filterData = this.glyphInfo.filter((feature: IEncodeFeature) => {
const { shaping, id = 0 } = feature;
const coordinates = feature.coordinates as [number, number];
const size = feature.size as number;
const fontScale: number = size / 24;
const pixels = this.mapService.lngLatToContainer(coordinates);
const { box } = collisionIndex.placeCollisionBox({
x1: shaping.left * fontScale - padding[0],
x2: shaping.right * fontScale + padding[0],
y1: shaping.top * fontScale - padding[1],
y2: shaping.bottom * fontScale + padding[1],
anchorPointX: pixels.x,
anchorPointY: pixels.y,
});
if (box && box.length) {
// TODOfeatureIndex
collisionIndex.insertCollisionBox(box, id);
return true;
} else {
return false;
}
});
this.layer.setEncodedData(filterData);
}
/**
*
*/
private initGlyph() {
// 1.生成文字纹理
this.initTextFont();
// 2.生成文字布局
this.generateGlyphLayout();
}
/**
*
*/
private updateTexture() {
const { createTexture2D } = this.rendererService;
const { canvas } = this.fontService;

View File

@ -27,7 +27,7 @@ void main() {
vec4 projected_position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0));
gl_Position = vec4(projected_position.xy / projected_position.w
+ a_textOffsets * fontScale / u_ViewportSize * 2., 0.0, 1.0);
+ a_textOffsets * fontScale / u_ViewportSize * 2. * u_DevicePixelRatio, 0.0, 1.0);
v_gamma_scale = gl_Position.w;

View File

@ -20,16 +20,6 @@ export default class PolygonLayer extends BaseLayer<IPolygonLayerStyleOptions> {
},
};
}
protected renderModels() {
this.models.forEach((model) =>
model.draw({
uniforms: this.layerModel.getUninforms(),
}),
);
return this;
}
protected buildModels() {
const shape = this.getModelType();
this.layerModel = new PolygonModels[shape](this);

View File

@ -9,10 +9,6 @@ export interface ICollisionBox {
// @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
@ -21,6 +17,7 @@ export default class CollisionIndex {
private width: number;
private height: number;
private grid: GridIndex;
private viewportPadding: number = 100;
private screenRightBoundary: number;
private screenBottomBoundary: number;
private gridRightBoundary: number;
@ -28,30 +25,35 @@ export default class CollisionIndex {
constructor(width: number, height: number) {
this.width = width;
this.height = height;
this.viewportPadding = Math.max(width, height);
// 创建网格索引
this.grid = new GridIndex(
width + 2 * viewportPadding,
height + 2 * viewportPadding,
width + this.viewportPadding,
height + this.viewportPadding,
25,
);
this.screenRightBoundary = width + viewportPadding;
this.screenBottomBoundary = height + viewportPadding;
this.gridRightBoundary = width + 2 * viewportPadding;
this.gridBottomBoundary = height + 2 * viewportPadding;
this.screenRightBoundary = width + this.viewportPadding;
this.screenBottomBoundary = height + this.viewportPadding;
this.gridRightBoundary = width + 2 * this.viewportPadding;
this.gridBottomBoundary = height + 2 * this.viewportPadding;
}
public placeCollisionBox(collisionBox: ICollisionBox, mvpMatrix: mat4) {
const projectedPoint = this.project(
mvpMatrix,
collisionBox.anchorPointX,
collisionBox.anchorPointY,
);
public placeCollisionBox(collisionBox: ICollisionBox) {
// 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;
const tlX =
collisionBox.x1 + collisionBox.anchorPointX + this.viewportPadding;
const tlY =
collisionBox.y1 + collisionBox.anchorPointY + this.viewportPadding;
const brX =
collisionBox.x2 + collisionBox.anchorPointX + this.viewportPadding;
const brY =
collisionBox.y2 + collisionBox.anchorPointY + this.viewportPadding;
if (
!this.isInsideGrid(tlX, tlY, brX, brY) ||
@ -79,14 +81,16 @@ export default class CollisionIndex {
* @param {number} y P20 Y
* @return {Point} projectedPoint
*/
public project(mvpMatrix: mat4, x: number, y: number) {
public project(mvpMatrix: number[], x: number, y: number) {
const point = vec4.fromValues(x, y, 0, 1);
const out = vec4.create();
vec4.transformMat4(out, point, mvpMatrix);
// @ts-ignore
const mat = mat4.fromValues(...mvpMatrix);
vec4.transformMat4(out, point, mat);
// 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,
x: ((out[0] / out[3] + 1) / 2) * this.width + this.viewportPadding,
y: ((-out[1] / out[3] + 1) / 2) * this.height + this.viewportPadding,
};
}

View File

@ -8,7 +8,7 @@ type CallBack = (...args: any[]) => any;
* @see https://zhuanlan.zhihu.com/p/74373214
*/
class GridIndex {
private boxCells: number[][];
private boxCells: number[][] = [];
private xCellCount: number;
private yCellCount: number;
private boxKeys: string[];

View File

@ -44,7 +44,7 @@ export default class TextLayerDemo extends React.Component {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
map: new Mapbox({
center: [120.19382669582967, 30.258134],
pitch: 0,
style: 'dark',
@ -61,16 +61,16 @@ export default class TextLayerDemo extends React.Component {
},
})
.shape('m', 'text')
.size(24)
.size(12)
.color('#fff')
.style({
fontWeight: 800,
fontWeight: 200,
textAnchor: 'center', // 文本相对锚点的位置 center|left|right|top|bottom|top-left
textOffset: [0, 0], // 文本相对锚点的偏移量 [水平, 垂直]
spacing: 2, // 字符间距
padding: [4, 4], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
strokeColor: 'white', // 描边颜色
strokeWidth: 4, // 描边宽度
padding: [1, 1], // 文本包围盒 padding [水平,垂直],影响碰撞检测结果,避免相邻文本靠的太近
stroke: 'red', // 描边颜色
strokeWidth: 2, // 描边宽度
strokeOpacity: 1.0,
});
scene.addLayer(pointLayer);