feat(layer): add point line polygon image layer

This commit is contained in:
thinkinggis 2019-10-12 21:26:54 +08:00
parent 83d6dcc4a4
commit c0289113cf
45 changed files with 1452 additions and 169 deletions

View File

@ -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';

View File

@ -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)

View File

@ -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;
}

View File

@ -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;
});
}
}

View File

@ -45,4 +45,4 @@ export default class GlobalConfigService implements IGlobalConfigService {
public reset() {
this.config = defaultGlobalConfig;
}
}
}

View File

@ -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;

View File

@ -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'),

View File

@ -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)));
}

View File

@ -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",

View File

@ -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) {

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 };

View File

@ -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);
}
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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);
});
}
}

View File

@ -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,
}),
}),
);
}
}

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
};

View File

@ -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,
};
}

View File

@ -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);
}
}

View File

@ -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); // 顶部坐标
}

View File

@ -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,

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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));
}

View File

@ -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;
}

View File

@ -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,
};
}

View File

@ -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",

View File

@ -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;
} = {};

View File

@ -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,

View File

@ -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 = {

View File

@ -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(); // 数据解析
}
}

View File

@ -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,
};
});
}

View File

@ -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];
});
}

View File

@ -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 };

View File

@ -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 />);

View File

@ -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,
}}
/>
);
}
}

View File

@ -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);
/*** 运行时修改样式属性 ***/

View File

@ -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"