mirror of https://gitee.com/antv-l7/antv-l7
feat(source): wip cluster
This commit is contained in:
parent
a243dc867d
commit
9ad51fe641
|
@ -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);
|
||||
});
|
||||
|
|
@ -6,8 +6,11 @@
|
|||
"demos": [
|
||||
{
|
||||
"filename": "line.js",
|
||||
"title": "json数据"",
|
||||
"screenshot": ""
|
||||
"title": "json数据"
|
||||
},
|
||||
{
|
||||
"filename": "data_update.js",
|
||||
"title": "数据更新"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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 插件实例
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 !!(
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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')
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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:脏检查,决定是否需要渲染
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
|
@ -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;
|
||||
});
|
||||
}
|
|
@ -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 />)
|
||||
|
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue