feat(source): wip cluster

This commit is contained in:
thinkinggis 2019-12-04 19:00:43 +08:00
parent a243dc867d
commit 9ad51fe641
20 changed files with 6485 additions and 10 deletions

View File

@ -0,0 +1,46 @@
import { Scene, PointLayer } from '@antv/l7';
import { GaodeMap } from '@antv/l7-maps';
const scene = new Scene({
id: 'map',
map: new GaodeMap({
style: 'light',
pitch: 0,
center: [ 120.19382669582967, 30.258134 ],
zoom: 5
})
});
const radius = 0.1;
function pointOnCircle(angle) {
return {
type: 'FeatureCollection',
features: [{
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [ 120.19382669582967 + Math.cos(angle) * radius, 30.258134 + Math.sin(angle) * radius ]
}
}]
};
}
const layer = new PointLayer()
.source(pointOnCircle(0))
.shape('circle')
.size(15) // default 1
.color('#2F54EB')
.style({
stroke: 'rgb(255,255,255)',
strokeWidth: 2,
opacity: 1
});
scene.addLayer(layer);
function animateMarker(timestamp) {
layer.setData(pointOnCircle(timestamp / 1000));
requestAnimationFrame(animateMarker);
}
layer.on('inited', () => {
animateMarker(0);
});

View File

@ -6,8 +6,11 @@
"demos": [
{
"filename": "line.js",
"title": "json数据"",
"screenshot": ""
"title": "json数据"
},
{
"filename": "data_update.js",
"title": "数据更新"
}
]
}

View File

@ -123,8 +123,8 @@
"site:clean": "gatsby clean",
"site:deploy": "yarn run site:build && gh-pages -d public",
"site:publish": "gh-pages -d public",
"lint:fix": "prettier --write docs/api/**/*.md packages/**/*.{spec,story}.ts{,x} stories/**/**/*.tsx",
"lint:examples": "eslint examples/**/**/*.js --fix",
"lint-fix": "prettier --write docs/api/**/*.md",
"lin-examples": "eslint examples/**/**/*.js --fix",
"prebuild": "run-p tsc lint",
"build": "yarn clean && lerna run build",
"postbuild": "yarn build:declarations",

View File

@ -95,6 +95,7 @@ export interface ILayer {
render(): ILayer;
destroy(): void;
source(data: any, option?: ISourceCFG): ILayer;
setData(data: any, option?: ISourceCFG): ILayer;
/**
*
* @param plugin

View File

@ -16,9 +16,19 @@ export interface ITransform {
}
export interface ISourceCFG {
cluster?: boolean;
clusterOptions?: Partial<IClusterOptions>;
parser?: IParserCfg;
transforms?: ITransform[];
}
export interface IClusterOptions {
enable: false;
radius: number;
maxZoom: number;
minZoom: number;
field: string;
method: 'max' | 'sum' | 'min' | 'mean' | 'count' | CallBack;
}
export interface IDictionary<TValue> {
[key: string]: TValue;
}

View File

@ -249,6 +249,7 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
// 获取插件集
this.plugins = this.container.getAll<ILayerPlugin>(TYPES.ILayerPlugin);
console.log(this.plugins)
// 完成插件注册,传入场景和图层容器内的服务
for (const plugin of this.plugins) {
plugin.apply(this, {
@ -338,6 +339,16 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
};
return this;
}
public setData(data: any, options?: ISourceCFG) {
this.sourceOption.data = data;
this.sourceOption.options = options;
console.time('init')
this.hooks.init.call();
console.timeEnd('init')
this.buildModels();
return this;
}
public style(options: object & Partial<ILayerConfig>): ILayer {
const { passes, ...rest } = options;
@ -456,6 +467,12 @@ export default class BaseLayer<ChildLayerStyleOptions = {}> extends EventEmitter
// 解绑图层容器中的服务
// this.container.unbind(TYPES.IStyleAttributeService);
}
public clear() {
this.styleAttributeService.clearAllAttributes();
// 销毁所有 model
this.models.forEach((model) => model.destroy());
}
public isDirty() {
return !!(

View File

@ -27,6 +27,7 @@ export default class DataMappingPlugin implements ILayerPlugin {
}: { styleAttributeService: IStyleAttributeService },
) {
layer.hooks.init.tap('DataMappingPlugin', () => {
console.time('DataMappingPlugin')
const attributes = styleAttributeService.getLayerStyleAttributes() || [];
const { dataArray } = layer.getSource().data;
@ -37,6 +38,7 @@ export default class DataMappingPlugin implements ILayerPlugin {
// mapping with source data
layer.setEncodedData(this.mapping(attributes, dataArray));
console.timeEnd('DataMappingPlugin')
});
// remapping before render

View File

@ -6,8 +6,10 @@ import { injectable } from 'inversify';
export default class DataSourcePlugin implements ILayerPlugin {
public apply(layer: ILayer) {
layer.hooks.init.tap('DataSourcePlugin', () => {
console.time('DataSourcePlugin')
const { data, options } = layer.sourceOption;
layer.setSource(new Source(data, options));
console.timeEnd('DataSourcePlugin')
});
}
}

View File

@ -57,10 +57,12 @@ export default class FeatureScalePlugin implements ILayerPlugin {
}: { styleAttributeService: IStyleAttributeService },
) {
layer.hooks.init.tap('FeatureScalePlugin', () => {
console.time('FeatureScalePlugin')
this.scaleOptions = layer.getScaleOptions();
const attributes = styleAttributeService.getLayerStyleAttributes();
const { dataArray } = layer.getSource().data;
this.caculateScalesForAttributes(attributes || [], dataArray);
console.timeEnd('FeatureScalePlugin')
});
layer.hooks.beforeRender.tap('FeatureScalePlugin', () => {

View File

@ -38,6 +38,7 @@ export default class PixelPickingPlugin implements ILayerPlugin {
) {
// TODO: 由于 Shader 目前无法根据是否开启拾取进行内容修改,因此即使不开启也需要生成 a_PickingColor
layer.hooks.init.tap('PixelPickingPlugin', () => {
console.time('PixelPickingPlugin')
const { enablePicking } = layer.getLayerConfig();
styleAttributeService.registerStyleAttribute({
name: 'pickingColor',
@ -55,6 +56,7 @@ export default class PixelPickingPlugin implements ILayerPlugin {
enablePicking ? encodePickingColor(featureIdx) : [0, 0, 0],
},
});
console.timeEnd('PixelPickingPlugin')
});
// 必须要与 PixelPickingPass 结合使用,因此必须开启 multiPassRenderer
// if (layer.multiPassRenderer) {

View File

@ -25,7 +25,9 @@ export default class RegisterStyleAttributePlugin implements ILayerPlugin {
}: { styleAttributeService: IStyleAttributeService },
) {
layer.hooks.init.tap('RegisterStyleAttributePlugin', () => {
console.time('RegisterStyleAttributePlugin')
this.registerBuiltinAttributes(styleAttributeService);
console.timeEnd('RegisterStyleAttributePlugin')
});
}

View File

@ -31,6 +31,7 @@ export default class ShaderUniformPlugin implements ILayerPlugin {
public apply(layer: ILayer) {
layer.hooks.beforeRender.tap('ShaderUniformPlugin', () => {
// 重新计算坐标系参数
console.time('ShaderUniformPlugin')
this.coordinateSystemService.refresh();
const { width, height } = this.rendererService.getViewportSize();
@ -58,7 +59,7 @@ export default class ShaderUniformPlugin implements ILayerPlugin {
u_ModelMatrix: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
}),
);
console.timeEnd('ShaderUniformPlugin')
// TODO脏检查决定是否需要渲染
});
}

View File

@ -21,7 +21,8 @@ export default class UpdateStyleAttributePlugin implements ILayerPlugin {
styleAttributeService,
}: { styleAttributeService: IStyleAttributeService },
) {
layer.hooks.beforeRender.tap('UpdateStyleAttributePlugin', () => {
layer.hooks.init.tap('UpdateStyleAttributePlugin', () => {
console.time('UpdateStyleAttributePlugin')
const attributes = styleAttributeService.getLayerStyleAttributes() || [];
attributes
.filter((attribute) => attribute.needRegenerateVertices)
@ -38,6 +39,7 @@ export default class UpdateStyleAttributePlugin implements ILayerPlugin {
`regenerate vertex attributes: ${attribute.name} finished`,
);
});
console.timeEnd('UpdateStyleAttributePlugin')
});
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
import Source from '../src/source';
import Point from './data/point';
import Polygon from './data/polygon';
describe('source constructor', () => {
@ -11,4 +12,14 @@ describe('source constructor', () => {
30.60807236997211,
]);
});
it('source.cluster', () => {
const source = new Source(Point, {
cluster: true,
clusterOptions: {
method: 'sum',
field: 'mag',
},
});
source.updateClusterData(2, [10, 0, 130, 75]);
});
});

View File

@ -1,10 +1,26 @@
import { IParserCfg, IParserData, ISourceCFG, ITransform } from '@antv/l7-core';
import {
IClusterOptions,
IParserCfg,
IParserData,
ISourceCFG,
ITransform,
} from '@antv/l7-core';
import { extent } from '@antv/l7-utils';
import { BBox, FeatureCollection, Geometries, Properties } from '@turf/helpers';
import {
BBox,
Feature,
FeatureCollection,
Geometries,
Properties,
} from '@turf/helpers';
import { EventEmitter } from 'eventemitter3';
import { cloneDeep } from 'lodash';
import { cloneDeep, isString, isFunction } from 'lodash';
import Supercluster from 'supercluster';
import { SyncHook } from 'tapable';
import { getParser, getTransform } from './';
import { statMap } from './utils/statistics';
import { getColumn } from './utils/util';
export default class Source extends EventEmitter {
public data: IParserData;
@ -21,9 +37,19 @@ export default class Source extends EventEmitter {
// 原始数据
private originData: any;
private rawData: any;
private clusterOptions: Partial<IClusterOptions> = {
enable: false,
radius: 40,
maxZoom: 20,
method: 'count',
};
private cluster: boolean = false;
private clusterIndex: Supercluster;
constructor(data: any, cfg?: ISourceCFG) {
super();
this.data = cloneDeep(data);
this.rawData = cloneDeep(data);
this.originData = data;
if (cfg) {
if (cfg.parser) {
@ -32,15 +58,63 @@ export default class Source extends EventEmitter {
if (cfg.transforms) {
this.transforms = cfg.transforms;
}
if (cfg.clusterOptions) {
this.cluster = true;
this.clusterOptions = {
...this.clusterOptions,
...cfg.clusterOptions,
};
}
}
this.hooks.init.tap('parser', () => {
this.excuteParser();
});
this.hooks.init.tap('cluster', () => {
this.initCluster();
});
this.hooks.init.tap('transform', () => {
this.executeTrans();
});
this.init();
}
public updateClusterData(
zoom: number,
bbox: [number, number, number, number],
): any {
const { method = 'sum', field } = this.clusterOptions;
const data = this.clusterIndex.getClusters(bbox, zoom);
if (!field && !isFunction(method)) {
return;
}
const clusterdata = data.map((item) => {
const id = item.id as number;
if (id) {
const points = this.clusterIndex.getLeaves(id, Infinity);
const properties = points.map((d) => d.properties);
let statNum;
if (isString(method) && field) {
const column = getColumn(properties, field);
statNum = statMap[method](column);
}
if (isFunction(method)) {
statNum = method(properties);
}
item.properties.stat = statNum;
}
return item;
});
this.data = getParser('geojson')({
type: 'FeatureCollection',
features: clusterdata,
});
this.executeTrans();
}
private excuteParser(): void {
const parser = this.parser;
const type: string = parser.type || 'geojson';
@ -60,6 +134,20 @@ export default class Source extends EventEmitter {
Object.assign(this.data, data);
});
}
private initCluster() {
if (!this.cluster) {
return;
}
const { radius, minZoom = 0, maxZoom } = this.clusterOptions;
this.clusterIndex = new Supercluster({
radius,
minZoom,
maxZoom,
});
this.clusterIndex.load(this.rawData.features);
}
private init() {
this.hooks.init.call(this);
}

View File

@ -0,0 +1,77 @@
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] * 1;
// 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 * 1;
}
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 };
export const statMap: { [key: string]: any } = {
min,
max,
mean,
sum,
};

View File

@ -0,0 +1,9 @@
interface IDataItem {
[key: string]: any;
};
export function getColumn(data: IDataItem[], columnName: string) {
return data.map((item: IDataItem) => {
return item[columnName] * 1;
});
}

View File

@ -3,6 +3,7 @@ import * as React from 'react';
import Arc2DLineDemo from './components/Arc2DLine';
import ArcLineDemo from './components/Arcline';
import Column from './components/column';
import DataUpdate from './components/data_update';
import HeatMapDemo from './components/HeatMap';
import GridHeatMap from './components/HeatmapGrid';
import LineLayer from './components/Line';
@ -16,6 +17,7 @@ import RasterLayerDemo from './components/RasterLayer';
// @ts-ignore
storiesOf('图层', module)
.add('点图层', () => <PointDemo />)
.add('数据更新', () => <DataUpdate />)
.add('3D点', () => <Point3D />)
.add('Column', () => <Column />)
.add('图片标注', () => <PointImage />)

View File

@ -0,0 +1,84 @@
import { PointLayer, Scene } from '@antv/l7';
import { GaodeMap, Mapbox } from '@antv/l7-maps';
import * as React from 'react';
// @ts-ignore
export default class DataUpdate extends React.Component {
// @ts-ignore
private scene: Scene;
public componentWillUnmount() {
this.scene.destroy();
}
public async componentDidMount() {
const scene = new Scene({
id: 'map',
map: new GaodeMap({
style: 'light',
pitch: 0,
center: [120.19382669582967, 30.258134],
zoom: 11,
}),
});
this.scene = scene;
const radius = 0.1;
function pointOnCircle(angle: number) {
return {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
properties: {},
geometry: {
type: 'Point',
coordinates: [
120.19382669582967 + Math.cos(angle) * radius,
30.258134 + Math.sin(angle) * radius,
],
},
},
],
};
}
const layer = new PointLayer({})
.source(pointOnCircle(0))
.shape('circle')
.size(15) // default 1
.color('#2F54EB')
.style({
strokeColor: '#fff',
strokeWidth: 2,
opacity: 1,
});
scene.addLayer(layer);
function animateMarker(timestamp: number) {
console.time('updatedata');
layer.setData(pointOnCircle(timestamp / 1000));
console.timeEnd('updatedata');
scene.render();
// setTimeout(animateMarker, 100);
requestAnimationFrame(animateMarker);
}
layer.on('inited', () => {
animateMarker(0);
console.log('inited');
});
}
public render() {
return (
<div
id="map"
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
);
}
}