mirror of https://gitee.com/antv-l7/antv-l7
feat(layer): add point line polygon image layer
This commit is contained in:
parent
2570b8c242
commit
54f28be495
|
@ -45,6 +45,7 @@ export * from './services/camera/ICameraService';
|
|||
export * from './services/config/IConfigService';
|
||||
export * from './services/scene/ISceneService';
|
||||
export * from './services/shader/IShaderModuleService';
|
||||
export * from './services/asset/IIconService';
|
||||
|
||||
/** 全部渲染服务接口 */
|
||||
export * from './services/renderer/IAttribute';
|
||||
|
|
|
@ -6,6 +6,7 @@ import getDecorators from 'inversify-inject-decorators';
|
|||
import { TYPES } from './types';
|
||||
|
||||
/** Service interfaces */
|
||||
import { IIconService} from './services/asset/IIconService';
|
||||
import { ICameraService } from './services/camera/ICameraService';
|
||||
import { IGlobalConfigService } from './services/config/IConfigService';
|
||||
import { ICoordinateSystemService } from './services/coordinate/ICoordinateSystemService';
|
||||
|
@ -14,6 +15,7 @@ import { ILogService } from './services/log/ILogService';
|
|||
import { IShaderModuleService } from './services/shader/IShaderModuleService';
|
||||
|
||||
/** Service implements */
|
||||
import IconService from './services/asset/IconService';
|
||||
import CameraService from './services/camera/CameraService';
|
||||
import GlobalConfigService from './services/config/ConfigService';
|
||||
import CoordinateSystemService from './services/coordinate/CoordinateSystemService';
|
||||
|
@ -44,6 +46,10 @@ container
|
|||
.bind<ICoordinateSystemService>(TYPES.ICoordinateSystemService)
|
||||
.to(CoordinateSystemService)
|
||||
.inSingletonScope();
|
||||
container
|
||||
.bind<IIconService>(TYPES.IIconService)
|
||||
.to(IconService)
|
||||
.inSingletonScope();
|
||||
container
|
||||
.bind<IShaderModuleService>(TYPES.IShaderModuleService)
|
||||
.to(ShaderModuleService)
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
import { ITexture2D } from '../renderer/ITexture2D';
|
||||
export type IImage = HTMLImageElement | File | string;
|
||||
export interface IIconValue {
|
||||
x: number;
|
||||
y: number;
|
||||
image: HTMLImageElement;
|
||||
}
|
||||
export interface IIcon {
|
||||
id: string;
|
||||
image: HTMLImageElement;
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
export interface IICONMap {
|
||||
[key: string]: IIconValue;
|
||||
}
|
||||
export interface IIconService {
|
||||
addImage(id: string, image: IImage): void;
|
||||
getTexture(): ITexture2D;
|
||||
getIconMap(): IICONMap;
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import { inject, injectable } from 'inversify';
|
||||
import { buildIconMaping } from '../../utils/font_util';
|
||||
import { ITexture2D } from '../renderer/ITexture2D';
|
||||
import {
|
||||
IIcon,
|
||||
IICONMap,
|
||||
IIconService,
|
||||
IIconValue,
|
||||
IImage,
|
||||
} from './IIconService';
|
||||
const BUFFER = 3;
|
||||
const MAX_CANVAS_WIDTH = 1024;
|
||||
const imageSize = 64;
|
||||
@injectable()
|
||||
export default class IconService implements IIconService {
|
||||
private canvas: HTMLCanvasElement;
|
||||
private iconData: IIcon[];
|
||||
private iconMap: IICONMap;
|
||||
private canvasHeigth: number;
|
||||
private textrure: ITexture2D;
|
||||
private ctx: CanvasRenderingContext2D;
|
||||
|
||||
constructor() {
|
||||
this.iconData = [];
|
||||
this.iconMap = {};
|
||||
this.canvas = document.createElement('canvas');
|
||||
// this.texture =
|
||||
this.ctx = this.canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
}
|
||||
|
||||
public async addImage(id: string, image: IImage) {
|
||||
const imagedata = (await this.loadImage(image)) as HTMLImageElement;
|
||||
this.iconData.push({
|
||||
id,
|
||||
image: imagedata,
|
||||
width: imageSize,
|
||||
height: imageSize,
|
||||
});
|
||||
const { mapping, canvasHeight } = buildIconMaping(
|
||||
this.iconData,
|
||||
BUFFER,
|
||||
MAX_CANVAS_WIDTH,
|
||||
);
|
||||
this.iconMap = mapping;
|
||||
this.canvasHeigth = canvasHeight;
|
||||
this.updateIconAtlas();
|
||||
}
|
||||
|
||||
public getTexture(): ITexture2D {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
public getIconMap() {
|
||||
return this.iconMap;
|
||||
}
|
||||
|
||||
private updateIconAtlas() {
|
||||
this.canvas.width = MAX_CANVAS_WIDTH;
|
||||
this.canvas.height = this.canvasHeigth;
|
||||
Object.keys(this.iconMap).forEach((item: string) => {
|
||||
const { x, y, image } = this.iconMap[item];
|
||||
this.ctx.drawImage(image, x, y, imageSize, imageSize);
|
||||
});
|
||||
// this.texture.magFilter = THREE.LinearFilter;
|
||||
// this.texture.minFilter = THREE.LinearFilter;
|
||||
// this.texture.needsUpdate = true;
|
||||
}
|
||||
|
||||
private loadImage(url: IImage) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (url instanceof HTMLImageElement) {
|
||||
resolve(url);
|
||||
return;
|
||||
}
|
||||
const image = new Image();
|
||||
image.onload = () => {
|
||||
resolve(image);
|
||||
};
|
||||
image.onerror = () => {
|
||||
reject(new Error('Could not load image at ' + url));
|
||||
};
|
||||
image.src = url instanceof File ? URL.createObjectURL(url) : url;
|
||||
});
|
||||
}
|
||||
}
|
|
@ -45,4 +45,4 @@ export default class GlobalConfigService implements IGlobalConfigService {
|
|||
public reset() {
|
||||
this.config = defaultGlobalConfig;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,6 +31,7 @@ export interface IStyleScale {
|
|||
scale: any;
|
||||
field: string;
|
||||
type: StyleScaleType;
|
||||
option: IScaleOption;
|
||||
}
|
||||
|
||||
export interface ILayerGlobalConfig {
|
||||
|
@ -44,12 +45,13 @@ export interface ILayerGlobalConfig {
|
|||
type CallBack = (...args: any[]) => any;
|
||||
export type StyleAttributeField = string | string[];
|
||||
export type StyleAttributeOption = string | number | boolean | any[] | CallBack;
|
||||
export type StyleAttrField = string | string[] | number | number[];
|
||||
export interface ILayerStyleAttribute {
|
||||
type: string;
|
||||
names: string[];
|
||||
field: StyleAttributeField;
|
||||
values?: any[];
|
||||
scales?: any[];
|
||||
scales?: IStyleScale[];
|
||||
setScales: (scales: IStyleScale[]) => void;
|
||||
callback?: (...args: any[]) => [];
|
||||
mapping?(...params: unknown[]): unknown[];
|
||||
|
@ -77,9 +79,9 @@ export interface ILayer {
|
|||
};
|
||||
multiPassRenderer: IMultiPassRenderer;
|
||||
init(): ILayer;
|
||||
size(field: string, value?: StyleAttributeOption): ILayer;
|
||||
color(field: string, value?: StyleAttributeOption): ILayer;
|
||||
shape(field: string, value?: StyleAttributeOption): ILayer;
|
||||
size(field: StyleAttrField, value?: StyleAttributeOption): ILayer;
|
||||
color(field: StyleAttrField, value?: StyleAttributeOption): ILayer;
|
||||
shape(field: StyleAttrField, value?: StyleAttributeOption): ILayer;
|
||||
// pattern(field: string, value: StyleAttributeOption): ILayer;
|
||||
// filter(field: string, value: StyleAttributeOption): ILayer;
|
||||
// active(option: ActiveOption): ILayer;
|
||||
|
|
|
@ -9,6 +9,7 @@ const TYPES = {
|
|||
IMapService: Symbol.for('IMapService'),
|
||||
IRendererService: Symbol.for('IRendererService'),
|
||||
IShaderModuleService: Symbol.for('IShaderModuleService'),
|
||||
IIconService: Symbol.for('IIconService'),
|
||||
|
||||
/** multi-pass */
|
||||
ClearPass: Symbol.for('ClearPass'),
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import {
|
||||
IIcon,
|
||||
IICONMap,
|
||||
IIconService,
|
||||
IIconValue,
|
||||
IImage,
|
||||
} from '../services/asset/IIconService';
|
||||
export function buildIconMaping(
|
||||
icons: IIcon[],
|
||||
buffer: number,
|
||||
maxCanvasWidth: number,
|
||||
) {
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
let rowHeight = 0;
|
||||
let columns = [];
|
||||
const mapping: IICONMap = {};
|
||||
for (const icon of icons) {
|
||||
if (!mapping[icon.id]) {
|
||||
const { height, width } = icon;
|
||||
|
||||
// fill one row
|
||||
if (xOffset + width + buffer > maxCanvasWidth) {
|
||||
buildRowMapping(mapping, columns, yOffset);
|
||||
|
||||
xOffset = 0;
|
||||
yOffset = rowHeight + yOffset + buffer;
|
||||
rowHeight = 0;
|
||||
columns = [];
|
||||
}
|
||||
|
||||
columns.push({
|
||||
icon,
|
||||
xOffset,
|
||||
});
|
||||
|
||||
xOffset = xOffset + width + buffer;
|
||||
rowHeight = Math.max(rowHeight, height);
|
||||
}
|
||||
}
|
||||
|
||||
if (columns.length > 0) {
|
||||
buildRowMapping(mapping, columns, yOffset);
|
||||
}
|
||||
|
||||
const canvasHeight = nextPowOfTwo(rowHeight + yOffset + buffer);
|
||||
|
||||
return {
|
||||
mapping,
|
||||
canvasHeight,
|
||||
};
|
||||
}
|
||||
function buildRowMapping(
|
||||
mapping: IICONMap,
|
||||
columns: Array<{
|
||||
icon: IIcon;
|
||||
xOffset: number;
|
||||
}>,
|
||||
yOffset: number,
|
||||
) {
|
||||
for (const column of columns) {
|
||||
const { icon, xOffset } = column;
|
||||
mapping[icon.id] = { ...icon, x: xOffset, y: yOffset, image: icon.image };
|
||||
}
|
||||
}
|
||||
export function nextPowOfTwo(num: number) {
|
||||
return Math.pow(2, Math.ceil(Math.log2(num)));
|
||||
}
|
|
@ -22,7 +22,9 @@
|
|||
"@l7/core": "^0.0.1",
|
||||
"@l7/source": "^0.0.1",
|
||||
"@turf/meta": "^6.0.2",
|
||||
"@types/d3-color": "^1.2.2",
|
||||
"d3-array": "^2.3.1",
|
||||
"d3-color": "^1.4.0",
|
||||
"d3-scale": "^3.1.0",
|
||||
"earcut": "^2.2.1",
|
||||
"eventemitter3": "^3.1.0",
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import { ILayerStyleOptions } from '@l7/core';
|
||||
import { lngLatToMeters } from '@l7/utils';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
interface IBufferCfg {
|
||||
data: unknown[];
|
||||
imagePos?: unknown;
|
||||
uv?: boolean;
|
||||
style?: ILayerStyleOptions;
|
||||
}
|
||||
type Position = number[];
|
||||
export type Position = number[];
|
||||
type Color = [number, number, number, number];
|
||||
import { lngLatToMeters } from '@l7/utils';
|
||||
import { vec3 } from 'gl-matrix';
|
||||
export interface IBufferInfo {
|
||||
vertices?: any;
|
||||
indexArray?: any;
|
||||
indexOffset: any;
|
||||
verticesOffset: any;
|
||||
verticesOffset: number;
|
||||
faceNum?: any;
|
||||
dimensions: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface IEncodeFeature {
|
||||
color?: Color;
|
||||
|
@ -21,8 +23,8 @@ export interface IEncodeFeature {
|
|||
shape?: string | number;
|
||||
pattern?: string;
|
||||
id?: number;
|
||||
coordinates: Position[][];
|
||||
bufferInfo: IBufferInfo;
|
||||
coordinates: unknown;
|
||||
bufferInfo: unknown;
|
||||
}
|
||||
export default class Buffer {
|
||||
public attributes: {
|
||||
|
@ -34,20 +36,23 @@ export default class Buffer {
|
|||
|
||||
protected data: unknown[];
|
||||
protected imagePos: unknown;
|
||||
protected uv: boolean;
|
||||
protected style: any;
|
||||
|
||||
constructor({ data, imagePos, uv }: IBufferCfg) {
|
||||
constructor({ data, imagePos, style }: IBufferCfg) {
|
||||
this.data = data;
|
||||
this.imagePos = imagePos;
|
||||
this.uv = !!uv;
|
||||
this.style = style;
|
||||
this.init();
|
||||
}
|
||||
public computeVertexNormals() {
|
||||
public computeVertexNormals(
|
||||
field: string = 'positions',
|
||||
flag: boolean = true,
|
||||
) {
|
||||
const normals = (this.attributes.normals = new Float32Array(
|
||||
this.verticesCount * 3,
|
||||
));
|
||||
const indexArray = this.indexArray;
|
||||
const { positions } = this.attributes;
|
||||
const positions = this.attributes[field];
|
||||
let vA;
|
||||
let vB;
|
||||
let vC;
|
||||
|
@ -58,11 +63,17 @@ export default class Buffer {
|
|||
vA = indexArray[i + 0] * 3;
|
||||
vB = indexArray[i + 1] * 3;
|
||||
vC = indexArray[i + 2] * 3;
|
||||
const [ax, ay] = lngLatToMeters([positions[vA], positions[vA + 1]]);
|
||||
const [ax, ay] = flag
|
||||
? lngLatToMeters([positions[vA], positions[vA + 1]])
|
||||
: [positions[vA], positions[vA + 1]];
|
||||
const pA = vec3.fromValues(ax, ay, positions[vA + 2]);
|
||||
const [bx, by] = lngLatToMeters([positions[vB], positions[vB + 1]]);
|
||||
const [bx, by] = flag
|
||||
? lngLatToMeters([positions[vB], positions[vB + 1]])
|
||||
: [positions[vB], positions[vB + 1]];
|
||||
const pB = vec3.fromValues(bx, by, positions[vB + 2]);
|
||||
const [cx, cy] = lngLatToMeters([positions[vC], positions[vC + 1]]);
|
||||
const [cx, cy] = flag
|
||||
? lngLatToMeters([positions[vC], positions[vC + 1]])
|
||||
: [positions[vC], positions[vC + 1]];
|
||||
const pC = vec3.fromValues(cx, cy, positions[vC + 2]);
|
||||
vec3.sub(cb, pC, pB);
|
||||
vec3.sub(ab, pA, pB);
|
||||
|
@ -113,7 +124,8 @@ export default class Buffer {
|
|||
}
|
||||
protected encodeArray(feature: IEncodeFeature, num: number) {
|
||||
const { color, id, pattern, size } = feature;
|
||||
const { verticesOffset } = feature.bufferInfo;
|
||||
const bufferInfo = feature.bufferInfo as IBufferInfo;
|
||||
const { verticesOffset } = bufferInfo;
|
||||
const imagePos = this.imagePos;
|
||||
const start1 = verticesOffset;
|
||||
for (let i = 0; i < num; i++) {
|
||||
|
@ -130,7 +142,7 @@ export default class Buffer {
|
|||
let size2: number[] = [];
|
||||
if (Array.isArray(size) && size.length === 2) {
|
||||
// TODO 多维size支持
|
||||
size2 = [size[0]];
|
||||
size2 = [size[0], size[0], size[1]];
|
||||
}
|
||||
if (!Array.isArray(size)) {
|
||||
size2 = [size];
|
||||
|
@ -145,90 +157,24 @@ export default class Buffer {
|
|||
}
|
||||
}
|
||||
}
|
||||
protected calculateWall(feature: IEncodeFeature) {
|
||||
const size = feature.size || 0;
|
||||
const {
|
||||
vertices,
|
||||
indexOffset,
|
||||
verticesOffset,
|
||||
faceNum,
|
||||
dimensions,
|
||||
} = feature.bufferInfo;
|
||||
this.encodeArray(feature, faceNum * 4);
|
||||
for (let i = 0; i < faceNum; i++) {
|
||||
const prePoint = vertices.slice(i * dimensions, (i + 1) * dimensions);
|
||||
const nextPoint = vertices.slice(
|
||||
(i + 1) * dimensions,
|
||||
(i + 2) * dimensions,
|
||||
);
|
||||
this.calculateExtrudeFace(
|
||||
prePoint,
|
||||
nextPoint,
|
||||
verticesOffset + i * 4,
|
||||
indexOffset + i * 6,
|
||||
size as number,
|
||||
);
|
||||
feature.bufferInfo.verticesOffset += 4;
|
||||
feature.bufferInfo.indexOffset += 6;
|
||||
}
|
||||
}
|
||||
|
||||
protected calculateExtrudeFace(
|
||||
prePoint: number[],
|
||||
nextPoint: number[],
|
||||
positionOffset: number,
|
||||
indexOffset: number | undefined,
|
||||
size: number,
|
||||
) {
|
||||
this.attributes.positions.set(
|
||||
[
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
size,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
size,
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
0,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
0,
|
||||
],
|
||||
positionOffset * 3,
|
||||
);
|
||||
const indexArray = [1, 2, 0, 3, 2, 1].map((v) => {
|
||||
return v + positionOffset;
|
||||
});
|
||||
if (this.uv) {
|
||||
this.attributes.uv.set(
|
||||
[0.1, 0, 0, 0, 0.1, size / 2000, 0, size / 2000],
|
||||
positionOffset * 2,
|
||||
);
|
||||
}
|
||||
this.indexArray.set(indexArray, indexOffset);
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 将每个多边形三角化,存储顶点坐标和索引坐标
|
||||
this.calculateFeatures();
|
||||
// 拼接成一个 attribute
|
||||
this.initAttributes();
|
||||
this.buildFeatures();
|
||||
}
|
||||
|
||||
private initAttributes() {
|
||||
protected initAttributes() {
|
||||
this.attributes.positions = new Float32Array(this.verticesCount * 3);
|
||||
this.attributes.colors = new Float32Array(this.verticesCount * 4);
|
||||
this.attributes.pickingIds = new Float32Array(this.verticesCount);
|
||||
this.attributes.sizes = new Float32Array(this.verticesCount);
|
||||
this.attributes.pickingIds = new Float32Array(this.verticesCount);
|
||||
if (this.uv) {
|
||||
this.attributes.uv = new Float32Array(this.verticesCount * 2);
|
||||
}
|
||||
this.indexArray = new Uint32Array(this.indexCount);
|
||||
}
|
||||
|
||||
private init() {
|
||||
// 1. 计算 attribute 长度
|
||||
this.calculateFeatures();
|
||||
// 2. 初始化 attribute
|
||||
this.initAttributes();
|
||||
// 3. 拼接attribute
|
||||
this.buildFeatures();
|
||||
}
|
||||
|
||||
private normalizeNormals() {
|
||||
const { normals } = this.attributes;
|
||||
for (let i = 0, li = normals.length; i < li; i += 3) {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {
|
||||
IGlobalConfigService,
|
||||
IIconService,
|
||||
ILayer,
|
||||
ILayerInitializationOptions,
|
||||
ILayerPlugin,
|
||||
|
@ -50,7 +51,9 @@ export default class BaseLayer implements ILayer {
|
|||
data: any;
|
||||
options?: ISourceCFG;
|
||||
};
|
||||
public styleOption: ILayerStyleOptions;
|
||||
public styleOption: ILayerStyleOptions = {
|
||||
opacity: 1.0,
|
||||
};
|
||||
// 样式属性
|
||||
public styleAttributes: {
|
||||
[key: string]: Required<ILayerStyleAttribute>;
|
||||
|
@ -67,6 +70,9 @@ export default class BaseLayer implements ILayer {
|
|||
@lazyInject(TYPES.IRendererService)
|
||||
private readonly rendererService: IRendererService;
|
||||
|
||||
@lazyInject(TYPES.IIconService)
|
||||
private readonly iconService: IIconService;
|
||||
|
||||
constructor(initializationOptions: Partial<ILayerInitializationOptions>) {
|
||||
this.initializationOptions = initializationOptions;
|
||||
}
|
||||
|
|
|
@ -36,6 +36,7 @@ export default class ScaleController {
|
|||
field,
|
||||
scale: undefined,
|
||||
type: StyleScaleType.VARIABLE,
|
||||
option: scaleOption,
|
||||
};
|
||||
if (!data || !data.length) {
|
||||
// 数据为空
|
||||
|
@ -69,6 +70,7 @@ export default class ScaleController {
|
|||
Object.assign(cfg, scaleOption);
|
||||
scaleOption = cfg; // 更新scale配置
|
||||
scale.scale = this.generateScale(type, cfg);
|
||||
scale.option = scaleOption;
|
||||
}
|
||||
return scale;
|
||||
}
|
||||
|
@ -100,10 +102,10 @@ export default class ScaleController {
|
|||
|
||||
private generateScale(type: ScaleTypes, scaleOption: IScaleOption) {
|
||||
// @ts-ignore
|
||||
const scale = scaleMap[type]();
|
||||
let scale = scaleMap[type]();
|
||||
if (scaleOption.hasOwnProperty('domain')) {
|
||||
// 处理同一字段映射不同视觉通道的问题
|
||||
scale.copy().domain(scaleOption.domain);
|
||||
scale = scale.copy().domain(scaleOption.domain);
|
||||
}
|
||||
// TODO 其他属性支持
|
||||
return scale;
|
||||
|
|
|
@ -22,13 +22,6 @@ export default class StyleAttribute implements ILayerStyleAttribute {
|
|||
this.scales = scales;
|
||||
this.values = values;
|
||||
this.names = this.parseFields(field) || [];
|
||||
// 设置 range TODO 2维映射
|
||||
// this.scales.forEach((scale) => {
|
||||
// scale.scale.range(values);
|
||||
// if (scale.type === StyleScaleType.VARIABLE) {
|
||||
// this.type = StyleScaleType.VARIABLE;
|
||||
// }
|
||||
// });
|
||||
if (callback) {
|
||||
this.type = StyleScaleType.VARIABLE;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import BaseLayer from './core/BaseLayer';
|
||||
import PointLayer from './point';
|
||||
import Point from './point/point';
|
||||
import PolygonLayer from './polygon';
|
||||
|
||||
export { BaseLayer, PointLayer, PolygonLayer };
|
||||
export { BaseLayer, PointLayer, PolygonLayer, Point };
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
import BufferBase, { IEncodeFeature, Position } from '../../core/BaseBuffer';
|
||||
interface IBufferInfo {
|
||||
normals: number[];
|
||||
arrayIndex: number[];
|
||||
positions: number[];
|
||||
attrDistance: number[];
|
||||
miters: number[];
|
||||
verticesOffset: number;
|
||||
indexOffset: number;
|
||||
}
|
||||
export default class FillBuffer extends BufferBase {
|
||||
private hasPattern: boolean;
|
||||
protected buildFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
this.calculateLine(feature);
|
||||
delete feature.bufferInfo;
|
||||
});
|
||||
this.hasPattern = layerData.some((feature: IEncodeFeature) => {
|
||||
return feature.pattern;
|
||||
});
|
||||
}
|
||||
protected initAttributes() {
|
||||
super.initAttributes();
|
||||
this.attributes.dashArray = new Float32Array(this.verticesCount);
|
||||
this.attributes.attrDistance = new Float32Array(this.verticesCount);
|
||||
this.attributes.totalDistances = new Float32Array(this.verticesCount);
|
||||
this.attributes.patterns = new Float32Array(this.verticesCount * 2);
|
||||
this.attributes.miters = new Float32Array(this.verticesCount);
|
||||
this.attributes.normals = new Float32Array(this.verticesCount * 3);
|
||||
}
|
||||
protected calculateFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
// 计算长
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
let { coordinates } = feature;
|
||||
if (Array.isArray(coordinates[0][0])) {
|
||||
coordinates = coordinates[0];
|
||||
}
|
||||
const { normals, attrIndex, attrPos, attrDistance, miters } = getNormals(
|
||||
coordinates,
|
||||
false,
|
||||
this.verticesCount,
|
||||
);
|
||||
const bufferInfo: IBufferInfo = {
|
||||
normals,
|
||||
arrayIndex: attrIndex,
|
||||
positions: attrPos,
|
||||
attrDistance,
|
||||
miters,
|
||||
verticesOffset: this.verticesCount,
|
||||
indexOffset: this.indexCount,
|
||||
};
|
||||
this.verticesCount += attrPos.length / 3;
|
||||
this.indexCount += attrIndex.length;
|
||||
feature.bufferInfo = bufferInfo;
|
||||
});
|
||||
}
|
||||
private calculateLine(feature: IEncodeFeature) {
|
||||
const bufferInfo = feature.bufferInfo as IBufferInfo;
|
||||
const {
|
||||
normals,
|
||||
arrayIndex,
|
||||
positions,
|
||||
attrDistance,
|
||||
miters,
|
||||
verticesOffset,
|
||||
indexOffset,
|
||||
} = bufferInfo;
|
||||
const { dashArray = 200 } = this.style;
|
||||
|
||||
this.encodeArray(feature, positions.length / 3);
|
||||
const totalLength = attrDistance[attrDistance.length - 1];
|
||||
// 增加长度
|
||||
const totalDistances = Array(positions.length / 3).fill(totalLength);
|
||||
// 虚线比例
|
||||
const ratio = dashArray / totalLength;
|
||||
const dashArrays = Array(positions.length / 3).fill(ratio);
|
||||
this.attributes.positions.set(positions, verticesOffset * 3);
|
||||
this.indexArray.set(arrayIndex, indexOffset);
|
||||
this.attributes.miters.set(miters, verticesOffset);
|
||||
this.attributes.normals.set(normals, verticesOffset * 3);
|
||||
this.attributes.attrDistance.set(attrDistance, verticesOffset);
|
||||
this.attributes.totalDistances.set(totalDistances, verticesOffset);
|
||||
this.attributes.dashArray.set(dashArrays, verticesOffset);
|
||||
}
|
||||
}
|
|
@ -40,7 +40,7 @@ export default class DataEncodePlugin implements ILayerPlugin {
|
|||
const attribute = layer.styleAttributes[attributeName];
|
||||
const scales: any[] = [];
|
||||
attribute.names.forEach((field: string) => {
|
||||
scales.push(this.getOrCreateScale(attribute, dataArray));
|
||||
scales.push(this.getOrCreateScale(attribute, field, dataArray));
|
||||
});
|
||||
attribute.setScales(scales);
|
||||
});
|
||||
|
@ -54,9 +54,9 @@ export default class DataEncodePlugin implements ILayerPlugin {
|
|||
|
||||
private getOrCreateScale(
|
||||
attribute: ILayerStyleAttribute,
|
||||
field: string,
|
||||
data: any[],
|
||||
): IStyleScale {
|
||||
const { field } = attribute;
|
||||
let scale = this.scaleCache[field as string];
|
||||
if (!scale) {
|
||||
scale = this.scaleController.createScale(field as string, data);
|
||||
|
@ -84,10 +84,10 @@ export default class DataEncodePlugin implements ILayerPlugin {
|
|||
// TODO: 数据过滤
|
||||
Object.keys(attributes).forEach((attributeName: string) => {
|
||||
const attribute = attributes[attributeName];
|
||||
const { type } = attribute;
|
||||
if (type === StyleScaleType.CONSTANT) {
|
||||
return;
|
||||
}
|
||||
// const { type } = attribute; // TODO: 支持常量 或变量
|
||||
// if (type === StyleScaleType.CONSTANT) {
|
||||
// return;
|
||||
// }
|
||||
let values = this.getAttrValue(attribute, record);
|
||||
if (attributeName === 'color') {
|
||||
values = values.map((c: unknown) => {
|
||||
|
@ -109,7 +109,7 @@ export default class DataEncodePlugin implements ILayerPlugin {
|
|||
const params: unknown[] = [];
|
||||
|
||||
scales.forEach((scale) => {
|
||||
const { field, type, value } = scale;
|
||||
const { field, type } = scale;
|
||||
if (type === StyleScaleType.CONSTANT) {
|
||||
params.push(scale.field);
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
import BaseBuffer, {
|
||||
IBufferInfo,
|
||||
IEncodeFeature,
|
||||
Position,
|
||||
} from '../../core/BaseBuffer';
|
||||
import extrudePolygon, { IExtrudeGeomety } from '../shape/extrude';
|
||||
import { geometryShape, ShapeType } from '../shape/Path';
|
||||
interface IGeometryCache {
|
||||
[key: string]: IExtrudeGeomety;
|
||||
}
|
||||
export default class ExtrudeBuffer extends BaseBuffer {
|
||||
private indexOffset: number = 0;
|
||||
private verticesOffset: number = 0;
|
||||
private geometryCache: IGeometryCache;
|
||||
public buildFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
this.calculateFill(feature);
|
||||
});
|
||||
}
|
||||
|
||||
protected calculateFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
this.geometryCache = {};
|
||||
this.verticesOffset = 0;
|
||||
this.indexOffset = 0;
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
const { shape } = feature;
|
||||
const { positions, index } = this.getGeometry(shape as ShapeType);
|
||||
this.verticesCount += positions.length / 3;
|
||||
this.indexCount += index.length;
|
||||
});
|
||||
}
|
||||
protected initAttributes() {
|
||||
super.initAttributes();
|
||||
this.attributes.miters = new Float32Array(this.verticesCount * 3);
|
||||
this.attributes.normals = new Float32Array(this.verticesCount * 3);
|
||||
this.attributes.sizes = new Float32Array(this.verticesCount * 3);
|
||||
}
|
||||
private calculateFill(feature: IEncodeFeature) {
|
||||
const { coordinates, shape } = feature;
|
||||
const instanceGeometry = this.getGeometry(shape as ShapeType);
|
||||
const numPoint = instanceGeometry.positions.length / 3;
|
||||
feature.bufferInfo = {
|
||||
verticesOffset: this.verticesOffset,
|
||||
indexOffset: this.indexOffset,
|
||||
dimensions: 3,
|
||||
};
|
||||
this.encodeArray(feature, numPoint);
|
||||
this.attributes.miters.set(
|
||||
instanceGeometry.positions,
|
||||
this.verticesOffset * 3,
|
||||
);
|
||||
const indexArray = instanceGeometry.index.map((v) => {
|
||||
return v + this.verticesOffset;
|
||||
});
|
||||
this.indexArray.set(indexArray, this.indexOffset);
|
||||
const position: number[] = [];
|
||||
for (let i = 0; i < numPoint; i++) {
|
||||
const coor = coordinates as Position;
|
||||
position.push(coor[0], coor[1], coor[2] || 0);
|
||||
}
|
||||
this.attributes.positions.set(position, this.verticesOffset * 3);
|
||||
this.verticesOffset += numPoint;
|
||||
this.indexOffset += indexArray.length;
|
||||
}
|
||||
|
||||
private getGeometry(shape: ShapeType): IExtrudeGeomety {
|
||||
if (this.geometryCache && this.geometryCache[shape]) {
|
||||
return this.geometryCache[shape];
|
||||
}
|
||||
const path = geometryShape[shape]
|
||||
? geometryShape[shape]()
|
||||
: geometryShape.cylinder();
|
||||
const geometry = extrudePolygon([path]);
|
||||
this.geometryCache[shape] = geometry;
|
||||
return geometry;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import BaseBuffer, { IEncodeFeature, Position } from '../../core/BaseBuffer';
|
||||
export default class ImageBuffer extends BaseBuffer {
|
||||
protected calculateFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
this.verticesCount = layerData.length;
|
||||
this.indexCount = layerData.length;
|
||||
}
|
||||
protected buildFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
layerData.forEach((item: IEncodeFeature, index: number) => {
|
||||
const { color = [0, 0, 0, 0], size, id, shape, coordinates } = item;
|
||||
const { x, y } = this.imagePos[shape];
|
||||
const coor = coordinates as Position;
|
||||
this.attributes.vertices.set([coor[0], coor[1], coor[2] || 0], index * 3);
|
||||
this.attributes.colors.set(color, index * 4);
|
||||
this.attributes.pickingIds.set([id], index);
|
||||
this.attributes.sizes.set([size as number], index); //
|
||||
this.attributes.uv.set([x, y], index * 2);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
import {
|
||||
gl,
|
||||
IRendererService,
|
||||
IShaderModuleService,
|
||||
lazyInject,
|
||||
TYPES,
|
||||
} from '@l7/core';
|
||||
import BaseLayer from '../core/BaseLayer';
|
||||
import ExtrudeBuffer from './buffers/ExtrudeBuffer';
|
||||
import extrude_frag from './shaders/extrude_frag.glsl';
|
||||
import extrude_vert from './shaders/extrude_vert.glsl';
|
||||
|
||||
export default class PointLayer extends BaseLayer {
|
||||
public name: string = 'PointLayer';
|
||||
|
||||
@lazyInject(TYPES.IShaderModuleService)
|
||||
private readonly shaderModule: IShaderModuleService;
|
||||
|
||||
@lazyInject(TYPES.IRendererService)
|
||||
private readonly renderer: IRendererService;
|
||||
|
||||
protected renderModels() {
|
||||
this.models.forEach((model) =>
|
||||
model.draw({
|
||||
uniforms: {
|
||||
u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
|
||||
},
|
||||
}),
|
||||
);
|
||||
return this;
|
||||
}
|
||||
|
||||
protected buildModels(): void {
|
||||
this.shaderModule.registerModule('point', {
|
||||
vs: extrude_vert,
|
||||
fs: extrude_frag,
|
||||
});
|
||||
|
||||
this.models = [];
|
||||
const { vs, fs, uniforms } = this.shaderModule.getModule('point');
|
||||
const buffer = new ExtrudeBuffer({
|
||||
data: this.getEncodedData(),
|
||||
});
|
||||
buffer.computeVertexNormals('miters', false);
|
||||
console.log(buffer); // TODO: normal
|
||||
const {
|
||||
createAttribute,
|
||||
createBuffer,
|
||||
createElements,
|
||||
createModel,
|
||||
} = this.renderer;
|
||||
|
||||
this.models.push(
|
||||
createModel({
|
||||
attributes: {
|
||||
a_Position: createAttribute({
|
||||
buffer: createBuffer({
|
||||
data: buffer.attributes.positions,
|
||||
type: gl.FLOAT,
|
||||
}),
|
||||
size: 3,
|
||||
}),
|
||||
a_normal: createAttribute({
|
||||
buffer: createBuffer({
|
||||
data: buffer.attributes.normals,
|
||||
type: gl.FLOAT,
|
||||
}),
|
||||
size: 3,
|
||||
}),
|
||||
a_color: createAttribute({
|
||||
buffer: createBuffer({
|
||||
data: buffer.attributes.colors,
|
||||
type: gl.FLOAT,
|
||||
}),
|
||||
size: 4,
|
||||
}),
|
||||
a_size: createAttribute({
|
||||
buffer: createBuffer({
|
||||
data: buffer.attributes.sizes,
|
||||
type: gl.FLOAT,
|
||||
}),
|
||||
size: 3,
|
||||
}),
|
||||
a_shape: createAttribute({
|
||||
buffer: createBuffer({
|
||||
data: buffer.attributes.miters,
|
||||
type: gl.FLOAT,
|
||||
}),
|
||||
size: 3,
|
||||
}),
|
||||
},
|
||||
uniforms: {
|
||||
...uniforms,
|
||||
u_opacity: this.styleOption.opacity as number,
|
||||
},
|
||||
fs,
|
||||
vs,
|
||||
count: buffer.indexArray.length,
|
||||
elements: createElements({
|
||||
data: buffer.indexArray,
|
||||
type: gl.UNSIGNED_INT,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
varying vec4 v_color;
|
||||
uniform float u_opacity: 1.0;
|
||||
void main() {
|
||||
gl_FragColor = v_color;
|
||||
gl_FragColor.a *= u_opacity;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
precision highp float;
|
||||
attribute vec3 a_Position;
|
||||
attribute vec4 a_color;
|
||||
attribute vec3 a_size;
|
||||
attribute vec3 a_shape;
|
||||
attribute vec3 a_normal;
|
||||
|
||||
uniform mat4 u_ModelMatrix;
|
||||
varying vec4 v_color;
|
||||
|
||||
#pragma include "projection"
|
||||
void main() {
|
||||
vec3 size = a_size * a_shape;
|
||||
v_color = vec4(a_normal,1.0);
|
||||
vec2 offset = project_pixel(size.xy);
|
||||
vec4 project_pos = project_position(vec4(a_Position.xy, 0, 1.0));
|
||||
gl_Position = project_common_position_to_clipspace(vec4(project_pos.xy + offset, size.z, 1.0));
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
uniform sampler2D u_texture;
|
||||
varying vec4 v_color;
|
||||
void main(){
|
||||
vec2 pos=v_uv+gl_PointCoord / 512.*64.;
|
||||
pos.y=1.-pos.y;
|
||||
vec4 textureColor=texture2D(u_texture,pos);
|
||||
if(v_color == vec4(0.)){
|
||||
gl_FragColor= textureColor;
|
||||
}else {
|
||||
gl_FragColor= step(0.01, textureColor.x) * v_color;
|
||||
}
|
||||
return;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
precision highp float;
|
||||
attribute vec3 a_Position;
|
||||
attribute vec4 a_color;
|
||||
attribute float a_size;
|
||||
attribute float a_shape;
|
||||
varying vec4 v_color;
|
||||
varying vec2 v_uv;
|
||||
#pragma include "projection"
|
||||
void main() {
|
||||
v_color = a_color;
|
||||
vec4 project_pos = project_position(vec4(a_Position, 1.0));
|
||||
gl_Position = project_common_position_to_clipspace(vec4(project_pos, 1.0));
|
||||
gl_PointSize = a_size;
|
||||
v_uv = uv;
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
type IPosition = [number, number, number];
|
||||
export type IPath = IPosition[];
|
||||
export enum ShapeType {
|
||||
CIRCLE = 'cylinder',
|
||||
SQUARE = 'squareColumn',
|
||||
TRIANGLE = 'triangleColumn',
|
||||
HEXAGON = 'hexagonColumn',
|
||||
PENTAGON = 'pentagonColumn',
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成规则多边形顶点个数
|
||||
* @param pointCount 顶点个数 3 => 三角形
|
||||
* @param start 顶点起始角度 调整图形的方向
|
||||
*/
|
||||
export function polygonPath(pointCount: number, start: number = 0): IPath {
|
||||
const step = (Math.PI * 2) / pointCount;
|
||||
const line = [];
|
||||
for (let i = 0; i < pointCount; i++) {
|
||||
line.push(step * i + (start * Math.PI) / 12);
|
||||
}
|
||||
const path: IPath = line.map((t) => {
|
||||
const x = Math.sin(t + Math.PI / 4);
|
||||
const y = Math.cos(t + Math.PI / 4);
|
||||
return [x, y, 0];
|
||||
});
|
||||
path.push(path[0]);
|
||||
return path;
|
||||
}
|
||||
|
||||
export function circle(): IPath {
|
||||
return polygonPath(30);
|
||||
}
|
||||
export function square(): IPath {
|
||||
return polygonPath(4);
|
||||
}
|
||||
export function triangle(): IPath {
|
||||
return polygonPath(3);
|
||||
}
|
||||
export function hexagon(): IPath {
|
||||
return polygonPath(6);
|
||||
}
|
||||
export function pentagon(): IPath {
|
||||
return polygonPath(5);
|
||||
}
|
||||
|
||||
export const geometryShape = {
|
||||
[ShapeType.CIRCLE]: circle,
|
||||
[ShapeType.HEXAGON]: hexagon,
|
||||
[ShapeType.TRIANGLE]: triangle,
|
||||
[ShapeType.SQUARE]: square,
|
||||
[ShapeType.PENTAGON]: pentagon,
|
||||
};
|
|
@ -0,0 +1,62 @@
|
|||
import earcut from 'earcut';
|
||||
import { IPath } from './Path';
|
||||
export interface IExtrudeGeomety {
|
||||
positions: number[];
|
||||
index: number[];
|
||||
}
|
||||
/**
|
||||
* 拉伸多边形顶点,返回拉伸后的顶点信息
|
||||
* @param paths 路径数据组
|
||||
* @param extrude 是否拉伸
|
||||
*/
|
||||
export default function extrudePolygon(path: IPath[]): IExtrudeGeomety {
|
||||
const p1 = path[0][0];
|
||||
const p2 = path[0][path[0].length - 1];
|
||||
if (p1[0] === p2[0] && p1[1] === p2[1]) {
|
||||
path[0] = path[0].slice(0, path[0].length - 1);
|
||||
}
|
||||
const n = path[0].length;
|
||||
const flattengeo = earcut.flatten(path);
|
||||
const positions = [];
|
||||
const indexArray = [];
|
||||
const normals = [];
|
||||
// 设置顶部z值
|
||||
for (let j = 0; j < flattengeo.vertices.length / 3; j++) {
|
||||
flattengeo.vertices[j * 3 + 2] = 1;
|
||||
normals.push(0, 0, 1);
|
||||
}
|
||||
positions.push(...flattengeo.vertices);
|
||||
const triangles = earcut(
|
||||
flattengeo.vertices,
|
||||
flattengeo.holes,
|
||||
flattengeo.dimensions,
|
||||
);
|
||||
indexArray.push(...triangles);
|
||||
for (let i = 0; i < n; i++) {
|
||||
const prePoint = flattengeo.vertices.slice(i * 3, i * 3 + 3);
|
||||
let nextPoint = flattengeo.vertices.slice(i * 3 + 3, i * 3 + 6);
|
||||
if (nextPoint.length === 0) {
|
||||
nextPoint = flattengeo.vertices.slice(0, 3);
|
||||
}
|
||||
const indexOffset = positions.length / 3;
|
||||
positions.push(
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
1,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
1,
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
0,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
0,
|
||||
);
|
||||
indexArray.push(...[1, 2, 0, 3, 2, 1].map((v) => v + indexOffset));
|
||||
}
|
||||
return {
|
||||
positions,
|
||||
index: indexArray,
|
||||
};
|
||||
}
|
|
@ -1,5 +1,9 @@
|
|||
import earcut from 'earcut';
|
||||
import BufferBase, { IBufferInfo, IEncodeFeature } from '../../core/BaseBuffer';
|
||||
import BufferBase, {
|
||||
IBufferInfo,
|
||||
IEncodeFeature,
|
||||
Position,
|
||||
} from '../../core/BaseBuffer';
|
||||
export default class ExtrudeBuffer extends BufferBase {
|
||||
public buildFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
|
@ -14,7 +18,7 @@ export default class ExtrudeBuffer extends BufferBase {
|
|||
const layerData = this.data as IEncodeFeature[];
|
||||
// 计算长
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
const { coordinates } = feature;
|
||||
const coordinates = feature.coordinates as Position[][];
|
||||
const flattengeo = earcut.flatten(coordinates);
|
||||
const n = this.checkIsClosed(coordinates)
|
||||
? coordinates[0].length - 1
|
||||
|
@ -36,15 +40,45 @@ export default class ExtrudeBuffer extends BufferBase {
|
|||
feature.bufferInfo = bufferInfo;
|
||||
});
|
||||
}
|
||||
protected calculateWall(feature: IEncodeFeature) {
|
||||
const size = feature.size || 0;
|
||||
const bufferInfo = feature.bufferInfo as IBufferInfo;
|
||||
const {
|
||||
vertices,
|
||||
indexOffset,
|
||||
verticesOffset,
|
||||
faceNum,
|
||||
dimensions,
|
||||
} = bufferInfo;
|
||||
this.encodeArray(feature, faceNum * 4);
|
||||
for (let i = 0; i < faceNum; i++) {
|
||||
const prePoint = vertices.slice(i * dimensions, (i + 1) * dimensions);
|
||||
const nextPoint = vertices.slice(
|
||||
(i + 1) * dimensions,
|
||||
(i + 2) * dimensions,
|
||||
);
|
||||
this.calculateExtrudeFace(
|
||||
prePoint,
|
||||
nextPoint,
|
||||
verticesOffset + i * 4,
|
||||
indexOffset + i * 6,
|
||||
size as number,
|
||||
);
|
||||
bufferInfo.verticesOffset += 4;
|
||||
bufferInfo.indexOffset += 6;
|
||||
feature.bufferInfo = bufferInfo;
|
||||
}
|
||||
}
|
||||
private calculateTop(feature: IEncodeFeature) {
|
||||
const size = feature.size || 1;
|
||||
const bufferInfo = feature.bufferInfo as IBufferInfo;
|
||||
const {
|
||||
indexArray,
|
||||
vertices,
|
||||
indexOffset,
|
||||
verticesOffset,
|
||||
dimensions,
|
||||
} = feature.bufferInfo;
|
||||
} = bufferInfo;
|
||||
const pointCount = vertices.length / dimensions;
|
||||
this.encodeArray(feature, vertices.length / dimensions);
|
||||
// 添加顶点
|
||||
|
@ -54,14 +88,50 @@ export default class ExtrudeBuffer extends BufferBase {
|
|||
(verticesOffset + i) * 3,
|
||||
);
|
||||
// 顶部文理坐标计算
|
||||
if (this.uv) {
|
||||
// TODO 用过BBox计算纹理坐标
|
||||
this.attributes.uv.set([-1, -1], (verticesOffset + i) * 2);
|
||||
}
|
||||
// if (this.uv) {
|
||||
// // TODO 用过BBox计算纹理坐标
|
||||
// this.attributes.uv.set([-1, -1], (verticesOffset + i) * 2);
|
||||
// }
|
||||
}
|
||||
feature.bufferInfo.verticesOffset += pointCount;
|
||||
bufferInfo.verticesOffset += pointCount;
|
||||
// 添加顶点索引
|
||||
this.indexArray.set(indexArray, indexOffset); // 顶部坐标
|
||||
feature.bufferInfo.indexOffset += indexArray.length;
|
||||
bufferInfo.indexOffset += indexArray.length;
|
||||
feature.bufferInfo = bufferInfo;
|
||||
}
|
||||
private calculateExtrudeFace(
|
||||
prePoint: number[],
|
||||
nextPoint: number[],
|
||||
positionOffset: number,
|
||||
indexOffset: number | undefined,
|
||||
size: number,
|
||||
) {
|
||||
this.attributes.positions.set(
|
||||
[
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
size,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
size,
|
||||
prePoint[0],
|
||||
prePoint[1],
|
||||
0,
|
||||
nextPoint[0],
|
||||
nextPoint[1],
|
||||
0,
|
||||
],
|
||||
positionOffset * 3,
|
||||
);
|
||||
const indexArray = [1, 2, 0, 3, 2, 1].map((v) => {
|
||||
return v + positionOffset;
|
||||
});
|
||||
// if (this.uv) {
|
||||
// this.attributes.uv.set(
|
||||
// [0.1, 0, 0, 0, 0.1, size / 2000, 0, size / 2000],
|
||||
// positionOffset * 2,
|
||||
// );
|
||||
// }
|
||||
this.indexArray.set(indexArray, indexOffset);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import earcut from 'earcut';
|
||||
import BufferBase, { IBufferInfo, IEncodeFeature } from '../../core/BaseBuffer';
|
||||
import BufferBase, {
|
||||
IBufferInfo,
|
||||
IEncodeFeature,
|
||||
Position,
|
||||
} from '../../core/BaseBuffer';
|
||||
export default class FillBuffer extends BufferBase {
|
||||
protected buildFeatures() {
|
||||
const layerData = this.data as IEncodeFeature[];
|
||||
|
@ -14,7 +18,7 @@ export default class FillBuffer extends BufferBase {
|
|||
// 计算长
|
||||
layerData.forEach((feature: IEncodeFeature) => {
|
||||
const { coordinates } = feature;
|
||||
const flattengeo = earcut.flatten(coordinates);
|
||||
const flattengeo = earcut.flatten(coordinates as Position[][]);
|
||||
const { vertices, dimensions, holes } = flattengeo;
|
||||
const indexArray = earcut(vertices, holes, dimensions).map(
|
||||
(v) => this.verticesCount + v,
|
||||
|
@ -33,13 +37,14 @@ export default class FillBuffer extends BufferBase {
|
|||
}
|
||||
|
||||
private calculateFill(feature: IEncodeFeature) {
|
||||
const bufferInfo = feature.bufferInfo as IBufferInfo;
|
||||
const {
|
||||
indexArray,
|
||||
vertices,
|
||||
indexOffset,
|
||||
verticesOffset,
|
||||
dimensions = 3,
|
||||
} = feature.bufferInfo;
|
||||
} = bufferInfo;
|
||||
const pointCount = vertices.length / dimensions;
|
||||
this.encodeArray(feature, pointCount);
|
||||
// 添加顶点
|
||||
|
@ -48,15 +53,16 @@ export default class FillBuffer extends BufferBase {
|
|||
[vertices[i * dimensions], vertices[i * dimensions + 1], 0],
|
||||
(verticesOffset + i) * 3,
|
||||
);
|
||||
if (this.uv) {
|
||||
// TODO 用过BBox计算纹理坐标
|
||||
this.attributes.uv.set(
|
||||
[0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0],
|
||||
(verticesOffset + i) * 3,
|
||||
);
|
||||
}
|
||||
// if (this.uv) {
|
||||
// // TODO 用过BBox计算纹理坐标
|
||||
// this.attributes.uv.set(
|
||||
// [0, 0, 1, 1, 1, 0, 0, 1, 1, 1, 0, 0],
|
||||
// (verticesOffset + i) * 3,
|
||||
// );
|
||||
// }
|
||||
}
|
||||
feature.bufferInfo.verticesOffset += pointCount;
|
||||
bufferInfo.verticesOffset += pointCount;
|
||||
feature.bufferInfo = bufferInfo;
|
||||
// 添加顶点索引
|
||||
this.indexArray.set(indexArray, indexOffset); // 顶部坐标
|
||||
}
|
||||
|
|
|
@ -39,15 +39,13 @@ export default class PolygonLayer extends BaseLayer {
|
|||
|
||||
this.models = [];
|
||||
const { vs, fs, uniforms } = this.shaderModule.getModule('polygon');
|
||||
const buffer = new ExtrudeBuffer({
|
||||
// const buffer = new ExtrudeBuffer({
|
||||
// data: this.getEncodedData(),
|
||||
// });
|
||||
// buffer.computeVertexNormals();
|
||||
const buffer = new FillBuffer({
|
||||
data: this.getEncodedData(),
|
||||
});
|
||||
buffer.computeVertexNormals();
|
||||
const buffer2 = new FillBuffer({
|
||||
data: this.getEncodedData(),
|
||||
});
|
||||
console.log(buffer);
|
||||
console.log(buffer2);
|
||||
const {
|
||||
createAttribute,
|
||||
createBuffer,
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
import BaseBuffer, { IEncodeFeature, Position } from '../../core/BaseBuffer';
|
||||
interface IImageFeature extends IEncodeFeature {
|
||||
images: any[];
|
||||
}
|
||||
export default class ImageBuffer extends BaseBuffer {
|
||||
protected calculateFeatures() {
|
||||
const layerData = this.data as IImageFeature[];
|
||||
this.verticesCount = 4;
|
||||
this.indexCount = 6;
|
||||
}
|
||||
protected buildFeatures() {
|
||||
const layerData = this.data as IImageFeature[];
|
||||
const coordinates = layerData[0].coordinates as Position[];
|
||||
const images = layerData[0].images;
|
||||
const positions: number[] = [
|
||||
...coordinates[0],
|
||||
0,
|
||||
coordinates[1][0],
|
||||
coordinates[0][1],
|
||||
0,
|
||||
...coordinates[1],
|
||||
0,
|
||||
...coordinates[0],
|
||||
0,
|
||||
...coordinates[1],
|
||||
0,
|
||||
coordinates[0][0],
|
||||
coordinates[1][1],
|
||||
0,
|
||||
];
|
||||
this.attributes.positions.set(positions, 0);
|
||||
this.attributes.uv.set([0, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 0], 0);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
precision mediump float;
|
||||
uniform sampler2D u_texture;
|
||||
uniform float u_opacity;
|
||||
varying vec2 v_texCoord;
|
||||
void main() {
|
||||
vec4 color = texture2D(u_texture,vec2(v_texCoord.x,1.0-v_texCoord.y));
|
||||
gl_FragColor = color * u_opacity;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
precision highp float;
|
||||
varying vec2 v_texCoord;
|
||||
uniform mat4 u_ModelMatrix;
|
||||
attribute vec3 a_Position;
|
||||
void main() {
|
||||
v_texCoord = uv;
|
||||
vec4 project_pos = project_position(vec4(a_Position, 1.0));
|
||||
gl_Position = project_common_position_to_clipspace(vec4(project_pos.xyz, 1.0));
|
||||
}
|
|
@ -1,11 +1,12 @@
|
|||
import * as d3 from 'd3-color';
|
||||
export function rgb2arr(str: string) {
|
||||
const arr = [];
|
||||
if (str.length === 4) {
|
||||
str = `#${str[1]}${str[1]}${str[2]}${str[2]}${str[3]}${str[3]}`;
|
||||
const color = d3.color(str) as d3.RGBColor;
|
||||
const arr = [0, 0, 0, 0];
|
||||
if (color != null) {
|
||||
arr[0] = color.r / 255;
|
||||
arr[1] = color.g / 255;
|
||||
arr[2] = color.b / 255;
|
||||
arr[3] = color.opacity;
|
||||
}
|
||||
arr.push(parseInt(str.substr(1, 2), 16) / 255);
|
||||
arr.push(parseInt(str.substr(3, 2), 16) / 255);
|
||||
arr.push(parseInt(str.substr(5, 2), 16) / 255);
|
||||
arr.push(1.0);
|
||||
return arr;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,199 @@
|
|||
import { vec2 } from 'gl-matrix';
|
||||
|
||||
export function computeMiter(
|
||||
tangent: vec2,
|
||||
miter: vec2,
|
||||
lineA: vec2,
|
||||
lineB: vec2,
|
||||
halfThick: number,
|
||||
) {
|
||||
vec2.add(tangent, lineA, lineB);
|
||||
vec2.normalize(tangent, tangent);
|
||||
miter = vec2.fromValues(-tangent[1], tangent[0]);
|
||||
const tmp = vec2.fromValues(-lineA[1], lineA[0]);
|
||||
return halfThick / vec2.dot(miter, tmp);
|
||||
}
|
||||
export function computeNormal(out: vec2, dir: vec2) {
|
||||
return vec2.set(out, -dir[1], dir[0]);
|
||||
}
|
||||
export function direction(out: vec2, a: vec2, b: vec2) {
|
||||
vec2.sub(out, a, b);
|
||||
vec2.normalize(out, out);
|
||||
return out;
|
||||
}
|
||||
function extrusions(
|
||||
positions: number[],
|
||||
out: vec2,
|
||||
miters: vec2,
|
||||
point: vec2,
|
||||
normal: vec2,
|
||||
scale,
|
||||
) {
|
||||
addNext(out, miters, normal, -scale);
|
||||
addNext(out, miters, normal, scale);
|
||||
positions.push(...point);
|
||||
positions.push(...point);
|
||||
}
|
||||
|
||||
function addNext(out, miters, normal, length) {
|
||||
out.push(normal[0], normal[1], 0);
|
||||
miters.push(length);
|
||||
}
|
||||
|
||||
function lineSegmentDistance(end, start) {
|
||||
const dx = start[0] - end[0];
|
||||
const dy = start[1] - end[1];
|
||||
const dz = start[2] - end[2];
|
||||
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
||||
}
|
||||
|
||||
function isPointEqual(a, b) {
|
||||
return a[0] === b[0] && a[1] === b[1];
|
||||
}
|
||||
|
||||
export default function(points, closed, indexOffset) {
|
||||
const lineA = vec2.fromValues(0, 0);
|
||||
const lineB = vec2.fromValues(0, 0);
|
||||
const tangent = vec2.fromValues(0, 0);
|
||||
const miter = vec2.fromValues(0, 0);
|
||||
let _started = false;
|
||||
let _normal = null;
|
||||
const tmp = vec2.create();
|
||||
let count = indexOffset || 0;
|
||||
const miterLimit = 3;
|
||||
|
||||
const out = [];
|
||||
const attrPos = [];
|
||||
const attrIndex = [];
|
||||
const miters = [];
|
||||
const attrDistance = [0, 0];
|
||||
if (closed) {
|
||||
points = points.slice();
|
||||
points.push(points[0]);
|
||||
}
|
||||
|
||||
const total = points.length;
|
||||
|
||||
for (let i = 1; i < total; i++) {
|
||||
const index = count;
|
||||
const last = points[i - 1];
|
||||
const cur = points[i];
|
||||
let next = i < points.length - 1 ? points[i + 1] : null;
|
||||
// 如果当前点和前一点相同,跳过
|
||||
if (isPointEqual(last, cur)) {
|
||||
continue;
|
||||
}
|
||||
if (next) {
|
||||
let nextIndex = i + 1;
|
||||
// 找到不相同的下一点
|
||||
while (next && isPointEqual(cur, next)) {
|
||||
next = nextIndex < points.length - 1 ? points[++nextIndex] : null;
|
||||
}
|
||||
}
|
||||
const lineDistance = lineSegmentDistance(cur, last);
|
||||
const d = lineDistance + attrDistance[attrDistance.length - 1];
|
||||
|
||||
direction(lineA, cur, last);
|
||||
|
||||
if (!_normal) {
|
||||
_normal = [0, 0];
|
||||
computeNormal(_normal, lineA);
|
||||
}
|
||||
|
||||
if (!_started) {
|
||||
_started = true;
|
||||
extrusions(attrPos, out, miters, last, _normal, 1);
|
||||
}
|
||||
|
||||
attrIndex.push(index + 0, index + 2, index + 1);
|
||||
|
||||
// no miter, simple segment
|
||||
if (!next) {
|
||||
// reset normal
|
||||
computeNormal(_normal, lineA);
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrDistance.push(d, d);
|
||||
attrIndex.push(index + 1, index + 2, index + 3);
|
||||
count += 2;
|
||||
} else {
|
||||
// get unit dir of next line
|
||||
direction(lineB, next, cur);
|
||||
|
||||
// stores tangent & miter
|
||||
let miterLen = computeMiter(tangent, miter, lineA, lineB, 1);
|
||||
|
||||
// get orientation
|
||||
const flip = vec2.dot(tangent, _normal) < 0 ? -1 : 1;
|
||||
const bevel = Math.abs(miterLen) > miterLimit;
|
||||
|
||||
// 处理前后两条线段重合的情况,这种情况不需要使用任何接头(miter/bevel)。
|
||||
// 理论上这种情况下 miterLen = Infinity,本应通过 isFinite(miterLen) 判断,
|
||||
// 但是 AMap 投影变换后丢失精度,只能通过一个阈值(1000)判断。
|
||||
if (Math.abs(miterLen) > 1000) {
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrIndex.push(index + 1, index + 2, index + 3);
|
||||
attrIndex.push(index + 2, index + 4, index + 3);
|
||||
computeNormal(tmp, lineB);
|
||||
vec2.copy(_normal, tmp); // store normal for next round
|
||||
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrDistance.push(d, d, d, d);
|
||||
|
||||
// the miter is now the normal for our next join
|
||||
count += 4;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (bevel) {
|
||||
miterLen = miterLimit;
|
||||
|
||||
// next two points in our first segment
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
|
||||
attrIndex.push(index + 1, index + 2, index + 3);
|
||||
|
||||
// now add the bevel triangle
|
||||
attrIndex.push(
|
||||
...(flip === 1
|
||||
? [index + 2, index + 4, index + 5]
|
||||
: [index + 4, index + 5, index + 3]),
|
||||
);
|
||||
|
||||
computeNormal(tmp, lineB);
|
||||
vec2.copy(_normal, tmp); // store normal for next round
|
||||
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrDistance.push(d, d, d, d);
|
||||
|
||||
// the miter is now the normal for our next join
|
||||
count += 4;
|
||||
} else {
|
||||
// next two points in our first segment
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrIndex.push(index + 1, index + 2, index + 3);
|
||||
|
||||
// now add the miter triangles
|
||||
addNext(out, miters, miter, miterLen * -flip);
|
||||
attrPos.push(...cur);
|
||||
attrIndex.push(index + 2, index + 4, index + 3);
|
||||
attrIndex.push(index + 4, index + 5, index + 6);
|
||||
computeNormal(tmp, lineB);
|
||||
vec2.copy(_normal, tmp); // store normal for next round
|
||||
|
||||
extrusions(attrPos, out, miters, cur, _normal, 1);
|
||||
attrDistance.push(d, d, d, d, d);
|
||||
|
||||
// the miter is now the normal for our next join
|
||||
count += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
normals: out,
|
||||
attrIndex,
|
||||
attrPos,
|
||||
attrDistance,
|
||||
miters,
|
||||
};
|
||||
}
|
|
@ -22,6 +22,7 @@
|
|||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@l7/utils": "0.0.1",
|
||||
"@l7/core": "0.0.1",
|
||||
"@mapbox/geojson-rewind": "^0.4.0",
|
||||
"@turf/helpers": "^6.1.4",
|
||||
"@turf/invariant": "^6.1.2",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { IParserCfg, ITransform } from '@l7/core';
|
||||
import { IParserData } from './interface';
|
||||
type ParserFunction = (data: any, cfg?: any) => IParserData;
|
||||
type transformFunction = (data: IParserData, cfg?: object) => IParserData;
|
||||
type transformFunction = (data: IParserData, cfg?: any) => IParserData;
|
||||
const TRANSFORMS: {
|
||||
[type: string]: transformFunction;
|
||||
} = {};
|
||||
|
|
|
@ -4,11 +4,15 @@ import geojson from './parser/geojson';
|
|||
import image from './parser/image';
|
||||
import json from './parser/json';
|
||||
import Source from './source';
|
||||
import { cluster } from './transform/cluster';
|
||||
import { aggregatorToGrid } from './transform/grid';
|
||||
export default Source;
|
||||
registerParser('geojson', geojson);
|
||||
registerParser('image', image);
|
||||
registerParser('csv', csv);
|
||||
registerParser('json', json);
|
||||
registerTransform('cluster', cluster);
|
||||
registerTransform('grid', aggregatorToGrid);
|
||||
export {
|
||||
getTransform,
|
||||
registerTransform,
|
||||
|
|
|
@ -3,7 +3,10 @@ import { IParserData } from '../interface';
|
|||
interface IImageCfg {
|
||||
extent: [number, number, number, number];
|
||||
}
|
||||
export default function image(data: string | [], cfg: IImageCfg): IParserData {
|
||||
export default function image(
|
||||
data: string | string[],
|
||||
cfg: IImageCfg,
|
||||
): IParserData {
|
||||
const { extent } = cfg;
|
||||
|
||||
const resultData: IParserData = {
|
||||
|
|
|
@ -1,41 +1,65 @@
|
|||
import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core';
|
||||
import { extent } from '@l7/utils';
|
||||
import { BBox, FeatureCollection, Geometries, Properties } from '@turf/helpers';
|
||||
import { EventEmitter } from 'eventemitter3';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { getParser } from './';
|
||||
import { IDictionary, IParserData, ISourceCFG } from './interface';
|
||||
import { SyncHook } from 'tapable';
|
||||
import { getParser, getTransform } from './';
|
||||
export default class Source extends EventEmitter {
|
||||
public data: IParserData;
|
||||
|
||||
// 数据范围
|
||||
public extent: BBox;
|
||||
private attrs: IDictionary<any> = {};
|
||||
// 生命周期钩子
|
||||
public hooks = {
|
||||
init: new SyncHook(['source']),
|
||||
layout: new SyncHook(['source']),
|
||||
update: new SyncHook(['source']),
|
||||
};
|
||||
public parser: IParserCfg = { type: 'geojson' };
|
||||
public transforms: ITransform[] = [];
|
||||
|
||||
// 原始数据
|
||||
private originData: any;
|
||||
constructor(data: any, cfg?: ISourceCFG) {
|
||||
super();
|
||||
this.set('data', data);
|
||||
Object.assign(this.attrs, cfg);
|
||||
this.originData = cloneDeep(this.get('data'));
|
||||
this.data = cloneDeep(data);
|
||||
this.originData = data;
|
||||
if (cfg) {
|
||||
if (cfg.parser) {
|
||||
this.parser = cfg.parser;
|
||||
}
|
||||
if (cfg.transforms) {
|
||||
this.transforms = cfg.transforms;
|
||||
}
|
||||
}
|
||||
this.hooks.init.tap('parser', () => {
|
||||
this.excuteParser();
|
||||
});
|
||||
this.init();
|
||||
}
|
||||
|
||||
public get(name: string): any {
|
||||
return this.attrs[name];
|
||||
}
|
||||
public set(name: string, value: any) {
|
||||
this.attrs[name] = value;
|
||||
}
|
||||
private excuteParser(): void {
|
||||
const parser = this.get('parser') || {};
|
||||
const parser = this.parser;
|
||||
const type: string = parser.type || 'geojson';
|
||||
const sourceParser = getParser(type);
|
||||
this.data = sourceParser(this.originData, parser);
|
||||
// 计算范围
|
||||
this.extent = extent(this.data.dataArray);
|
||||
}
|
||||
/**
|
||||
* 数据统计
|
||||
*/
|
||||
private executeTrans() {
|
||||
const trans = this.transforms;
|
||||
trans.forEach((tran: ITransform) => {
|
||||
const { type } = tran;
|
||||
const data = getTransform(type)(this.data, tran);
|
||||
Object.assign(this.data, data);
|
||||
});
|
||||
}
|
||||
private init() {
|
||||
this.excuteParser(); // 数据解析
|
||||
this.hooks.init.call(this);
|
||||
// this.excuteParser(); // 数据解析
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core';
|
||||
import Supercluster from 'supercluster';
|
||||
export function cluster(data: IParserData, option: ITransform): IParserData {
|
||||
const { radius = 80, maxZoom = 18, minZoom = 0, field, zoom = 2 } = option;
|
||||
if (data.pointIndex) {
|
||||
const clusterData = data.pointIndex.getClusters(data.extent, zoom);
|
||||
data.dataArray = formatData(clusterData);
|
||||
return data;
|
||||
}
|
||||
const pointIndex = new Supercluster({
|
||||
radius,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
map: (props) => ({ sum: props[field] }), // 根据指定字段求和
|
||||
reduce: (accumulated, props) => {
|
||||
accumulated.sum += props.sum;
|
||||
},
|
||||
});
|
||||
const geojson: {
|
||||
type: string;
|
||||
features: any[];
|
||||
} = {
|
||||
type: 'FeatureCollection',
|
||||
features: [],
|
||||
};
|
||||
geojson.features = data.dataArray.map((item) => {
|
||||
return {
|
||||
type: 'Feature',
|
||||
properties: {
|
||||
[field]: item[field],
|
||||
},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: item.coordinates,
|
||||
},
|
||||
};
|
||||
});
|
||||
pointIndex.load(geojson.features);
|
||||
const clusterPoint = pointIndex.getClusters(data.extent, zoom);
|
||||
const resultData = clusterPoint.map((point, index) => {
|
||||
return {
|
||||
coordinates: point.geometry.coordinates,
|
||||
_id: index + 1,
|
||||
...point.properties,
|
||||
};
|
||||
});
|
||||
data.dataArray = resultData;
|
||||
data.pointIndex = pointIndex;
|
||||
return data;
|
||||
}
|
||||
export function formatData(clusterPoint: any[]) {
|
||||
return clusterPoint.map((point, index) => {
|
||||
return {
|
||||
coordinates: point.geometry.coordinates,
|
||||
_id: index + 1,
|
||||
...point.properties,
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/**
|
||||
* 生成四边形热力图
|
||||
*/
|
||||
import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@l7/core';
|
||||
import { max, mean, min, sum } from './statistics';
|
||||
const statMap: { [key: string]: any } = {
|
||||
min,
|
||||
max,
|
||||
mean,
|
||||
sum,
|
||||
};
|
||||
interface IGridHash {
|
||||
[key: string]: any;
|
||||
}
|
||||
interface IGridOffset {
|
||||
yOffset: number;
|
||||
xOffset: number;
|
||||
}
|
||||
const R_EARTH = 6378000;
|
||||
|
||||
export function aggregatorToGrid(data: IParserData, option: ITransform) {
|
||||
const dataArray = data.dataArray;
|
||||
const { size = 10 } = option;
|
||||
const { gridHash, gridOffset } = _pointsGridHash(dataArray, size);
|
||||
const layerData = _getGridLayerDataFromGridHash(gridHash, gridOffset, option);
|
||||
return {
|
||||
yOffset: ((gridOffset.xOffset / 360) * (256 << 20)) / 2,
|
||||
xOffset: ((gridOffset.xOffset / 360) * (256 << 20)) / 2,
|
||||
radius: ((gridOffset.xOffset / 360) * (256 << 20)) / 2,
|
||||
dataArray: layerData,
|
||||
};
|
||||
}
|
||||
|
||||
function _pointsGridHash(dataArray: any[], size: number) {
|
||||
let latMin = Infinity;
|
||||
let latMax = -Infinity;
|
||||
let pLat;
|
||||
for (const point of dataArray) {
|
||||
pLat = point.coordinates[1];
|
||||
if (Number.isFinite(pLat)) {
|
||||
latMin = pLat < latMin ? pLat : latMin;
|
||||
latMax = pLat > latMax ? pLat : latMax;
|
||||
}
|
||||
}
|
||||
// const centerLat = (latMin + latMax) / 2;
|
||||
const centerLat = 34.54083;
|
||||
const gridOffset = _calculateGridLatLonOffset(size, centerLat);
|
||||
if (gridOffset.xOffset <= 0 || gridOffset.yOffset <= 0) {
|
||||
return { gridHash: {}, gridOffset };
|
||||
}
|
||||
const gridHash: IGridHash = {};
|
||||
for (const point of dataArray) {
|
||||
const lat = point.coordinates[1];
|
||||
const lng = point.coordinates[0];
|
||||
|
||||
if (Number.isFinite(lat) && Number.isFinite(lng)) {
|
||||
const latIdx = Math.floor((lat + 90) / gridOffset.yOffset);
|
||||
const lonIdx = Math.floor((lng + 180) / gridOffset.xOffset);
|
||||
const key = `${latIdx}-${lonIdx}`;
|
||||
|
||||
gridHash[key] = gridHash[key] || { count: 0, points: [] };
|
||||
gridHash[key].count += 1;
|
||||
gridHash[key].points.push(point);
|
||||
}
|
||||
}
|
||||
|
||||
return { gridHash, gridOffset };
|
||||
}
|
||||
// 计算网格偏移量
|
||||
function _calculateGridLatLonOffset(cellSize: number, latitude: number) {
|
||||
const yOffset = _calculateLatOffset(cellSize);
|
||||
const xOffset = _calculateLonOffset(latitude, cellSize);
|
||||
return { yOffset, xOffset };
|
||||
}
|
||||
|
||||
function _calculateLatOffset(dy: number) {
|
||||
return (dy / R_EARTH) * (180 / Math.PI);
|
||||
}
|
||||
|
||||
function _calculateLonOffset(lat: number, dx: number) {
|
||||
return ((dx / R_EARTH) * (180 / Math.PI)) / Math.cos((lat * Math.PI) / 180);
|
||||
}
|
||||
function _getGridLayerDataFromGridHash(
|
||||
gridHash: IGridHash,
|
||||
gridOffset: IGridOffset,
|
||||
option: ITransform,
|
||||
) {
|
||||
return Object.keys(gridHash).reduce((accu, key, i) => {
|
||||
const idxs = key.split('-');
|
||||
const latIdx = parseInt(idxs[0], 10);
|
||||
const lonIdx = parseInt(idxs[1], 10);
|
||||
const item: {
|
||||
[key: string]: any;
|
||||
} = {};
|
||||
if (option.field && option.method) {
|
||||
const columns = getColumn(gridHash[key].points, option.field);
|
||||
item[option.method] = statMap[option.method](columns);
|
||||
}
|
||||
Object.assign(item, {
|
||||
_id: i + 1,
|
||||
coordinates: [
|
||||
-180 + gridOffset.xOffset * lonIdx,
|
||||
-90 + gridOffset.yOffset * latIdx,
|
||||
],
|
||||
count: gridHash[key].count,
|
||||
});
|
||||
// @ts-ignore
|
||||
accu.push(item);
|
||||
return accu;
|
||||
}, []);
|
||||
}
|
||||
function getColumn(data: any[], columnName: string) {
|
||||
return data.map((item) => {
|
||||
return item[columnName];
|
||||
});
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
function max(x: number[]) {
|
||||
if (x.length === 0) {
|
||||
throw new Error('max requires at least one data point');
|
||||
}
|
||||
|
||||
let value = x[0];
|
||||
for (let i = 1; i < x.length; i++) {
|
||||
// On the first iteration of this loop, max is
|
||||
// undefined and is thus made the maximum element in the array
|
||||
if (x[i] > value) {
|
||||
value = x[i];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function min(x: number[]) {
|
||||
if (x.length === 0) {
|
||||
throw new Error('min requires at least one data point');
|
||||
}
|
||||
|
||||
let value = x[0];
|
||||
for (let i = 1; i < x.length; i++) {
|
||||
// On the first iteration of this loop, min is
|
||||
// undefined and is thus made the minimum element in the array
|
||||
if (x[i] < value) {
|
||||
value = x[i];
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function sum(x: number[]) {
|
||||
// If the array is empty, we needn't bother computing its sum
|
||||
if (x.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Initializing the sum as the first number in the array
|
||||
let sumNum = x[0];
|
||||
|
||||
// Keeping track of the floating-point error correction
|
||||
let correction = 0;
|
||||
|
||||
let transition;
|
||||
|
||||
for (let i = 1; i < x.length; i++) {
|
||||
transition = sumNum + x[i];
|
||||
|
||||
// Here we need to update the correction in a different fashion
|
||||
// if the new absolute value is greater than the absolute sum
|
||||
if (Math.abs(sumNum) >= Math.abs(x[i])) {
|
||||
correction += sumNum - transition + x[i];
|
||||
} else {
|
||||
correction += x[i] - transition + sumNum;
|
||||
}
|
||||
|
||||
sumNum = transition;
|
||||
}
|
||||
|
||||
// Returning the corrected sum
|
||||
return sumNum + correction;
|
||||
}
|
||||
function mean(x: number[]) {
|
||||
if (x.length === 0) {
|
||||
throw new Error('mean requires at least one data point');
|
||||
}
|
||||
return sum(x) / x.length;
|
||||
}
|
||||
|
||||
export { sum, max, min, mean };
|
|
@ -3,6 +3,7 @@ import * as React from 'react';
|
|||
import AMap from './components/AMap';
|
||||
import Mapbox from './components/Mapbox';
|
||||
import Polygon from './components/Polygon';
|
||||
import Point3D from './components/Point3D';
|
||||
// @ts-ignore
|
||||
import notes from './Map.md';
|
||||
|
||||
|
@ -13,4 +14,5 @@ storiesOf('地图底图测试', module)
|
|||
.add('Mapbox', () => <Mapbox />, {
|
||||
notes: { markdown: notes },
|
||||
})
|
||||
.add('Polygon', () => <Polygon />);
|
||||
.add('Polygon', () => <Polygon />)
|
||||
.add('Point3D', () => <Point3D />);
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { Point } from '@l7/layers';
|
||||
import { Scene } from '@l7/scene';
|
||||
import * as React from 'react';
|
||||
import data from './data.json';
|
||||
|
||||
export default class Point3D extends React.Component {
|
||||
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 Point({});
|
||||
const p1 = {
|
||||
type: 'FeatureCollection',
|
||||
features: [
|
||||
{
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [83.671875, 44.84029065139799],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
pointLayer
|
||||
.source(data)
|
||||
.color('blue')
|
||||
.shape('scalerank', [ 'triangleColumn', 'squareColumn', 'hexagonColumn' ,'cylinder' ])
|
||||
.size([25, 10]);
|
||||
scene.addLayer(pointLayer);
|
||||
// function run() {
|
||||
// scene.render();
|
||||
// requestAnimationFrame(run);
|
||||
// }
|
||||
// requestAnimationFrame(run);
|
||||
scene.render();
|
||||
this.scene = scene;
|
||||
console.log(pointLayer);
|
||||
|
||||
// @ts-ignore
|
||||
window.layer = pointLayer;
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div
|
||||
id="map"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -75,7 +75,11 @@ export default class Mapbox extends React.Component {
|
|||
opacity: 0.8,
|
||||
});
|
||||
scene.addLayer(layer);
|
||||
scene.render();
|
||||
function run() {
|
||||
scene.render();
|
||||
requestAnimationFrame(run);
|
||||
}
|
||||
requestAnimationFrame(run);
|
||||
this.scene = scene;
|
||||
console.log(layer);
|
||||
/*** 运行时修改样式属性 ***/
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -2754,6 +2754,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.0.0.tgz#a0d63a296a2d8435a9ec59393dcac746c6174a96"
|
||||
integrity sha512-rGqfPVowNDTszSFvwoZIXvrPG7s/qKzm9piCRIH6xwTTRu7pPZ3ootULFnPkTt74B6i5lN0FpLQL24qGOw1uZA==
|
||||
|
||||
"@types/d3-color@^1.2.2":
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-1.2.2.tgz#80cf7cfff7401587b8f89307ba36fe4a576bc7cf"
|
||||
integrity sha512-6pBxzJ8ZP3dYEQ4YjQ+NVbQaOflfgXq/JbDiS99oLobM2o72uAST4q6yPxHv6FOTCRC/n35ktuo8pvw/S4M7sw==
|
||||
|
||||
"@types/d3-dsv@^1.0.36":
|
||||
version "1.0.36"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-dsv/-/d3-dsv-1.0.36.tgz#e91129d7c02b1b814838d001e921e8b9a67153d0"
|
||||
|
@ -5557,6 +5562,11 @@ d3-color@1:
|
|||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.3.0.tgz#675818359074215b020dc1d41d518136dcb18fa9"
|
||||
integrity sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg==
|
||||
|
||||
d3-color@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.4.0.tgz#89c45a995ed773b13314f06460df26d60ba0ecaf"
|
||||
integrity sha512-TzNPeJy2+iEepfiL92LAAB7fvnp/dV2YwANPVHdDWmYMm23qIJBYww3qT8I8C1wXrmrg4UWs7BKc2tKIgyjzHg==
|
||||
|
||||
d3-dsv@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-1.1.1.tgz#aaa830ecb76c4b5015572c647cc6441e3c7bb701"
|
||||
|
|
Loading…
Reference in New Issue