Skip to content

Commit

Permalink
Merge ca2740e into 9961b98
Browse files Browse the repository at this point in the history
  • Loading branch information
heiyexing committed Jun 13, 2023
2 parents 9961b98 + ca2740e commit 772d7d7
Show file tree
Hide file tree
Showing 35 changed files with 1,733 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -109,3 +109,4 @@ dist
# Packages lock
package-lock.json
yarn.lock
.idea
3 changes: 3 additions & 0 deletions jest.config.js
Expand Up @@ -12,6 +12,9 @@ module.exports = {
testMatch: ['**/__tests__/**/*.+(spec|test).[jt]s'],
moduleNameMapper: {
'^lodash-es$': 'lodash',
'^d3-scale$': 'd3-scale/dist/d3-scale.min.js',
'^d3-color$': 'd3-color/dist/d3-color.min.js',
'^d3-array$': 'd3-array/dist/d3-array.min.js',
},
testTimeout: 5000 * 4,
coverageThreshold: {
Expand Down
4 changes: 2 additions & 2 deletions package.json
Expand Up @@ -67,8 +67,8 @@
"typedoc": "^0.22.5",
"typescript": "^4.9.3"
},
"dependencies": {},
"resolutions": {
"monaco-editor": "0.21.3"
}
},
"repository": "https://github.com/antvis/L7Plot.git"
}
@@ -0,0 +1,93 @@
import { FlowLayer } from '../../../../src/composite-layers/flow-layer';
import { DataProvider } from '../../../../src/composite-layers/flow-layer/data';
import { FlowDataProviderState, FlowLayerOptions, FlowSource } from '../../../../src/composite-layers/flow-layer/types';

const flowSource: FlowSource = {
data: [
{ weight: '5501', f_lon: '121.5838545', f_lat: '31.14749588', t_lon: '121.6664482', t_lat: '31.14343923' },
{ weight: '5290', f_lon: '121.6664482', f_lat: '31.14343923', t_lon: '121.5838545', t_lat: '31.14749588' },
{ weight: '5209', f_lon: '121.3630337', f_lat: '30.72855814', t_lon: '121.4291376', t_lat: '31.15148107' },
{ weight: '4985', f_lon: '121.4291376', f_lat: '31.15148107', t_lon: '121.3630337', t_lat: '30.72855814' },
{ weight: '3972', f_lon: '121.8100774', f_lat: '31.15285522', t_lon: '121.8154177', t_lat: '31.13785948' },
{ weight: '3926', f_lon: '121.4901223', f_lat: '31.22812638', t_lon: '121.4858528', t_lat: '31.23867577' },
{ weight: '3775', f_lon: '121.8100774', f_lat: '31.15285522', t_lon: '121.8164774', t_lat: '31.13345342' },
{ weight: '3743', f_lon: '121.8089762', f_lat: '31.15023081', t_lon: '121.8154177', t_lat: '31.13785948' },
{ weight: '3655', f_lon: '121.8089762', f_lat: '31.15023081', t_lon: '121.8164774', t_lat: '31.13345342' },
{ weight: '3487', f_lon: '121.8089762', f_lat: '31.15023081', t_lon: '121.8186382', t_lat: '31.13167215' },
{ weight: '3402', f_lon: '121.5110947', f_lat: '31.10513713', t_lon: '121.5238634', t_lat: '31.0663601' },
{ weight: '3380', f_lon: '121.7146803', f_lat: '31.3894142', t_lon: '121.6684618', t_lat: '31.30740288' },
{ weight: '3309', f_lon: '121.8100774', f_lat: '31.15285522', t_lon: '121.8186382', t_lat: '31.13167215' },
{ weight: '3254', f_lon: '121.5110947', f_lat: '31.10513713', t_lon: '121.520615', t_lat: '31.06551356' },
{ weight: '2961', f_lon: '121.8089762', f_lat: '31.15023081', t_lon: '121.8143166', t_lat: '31.1352345' },
{ weight: '2799', f_lon: '121.8100774', f_lat: '31.15285522', t_lon: '121.8143166', t_lat: '31.1352345' },
{ weight: '2707', f_lon: '121.4858528', f_lat: '31.23867577', t_lon: '121.4750121', t_lat: '31.23351389' },
{ weight: '2622', f_lon: '121.6163564', f_lat: '31.13496709', t_lon: '121.6282794', t_lat: '31.12874112' },
{ weight: '2583', f_lon: '121.6026647', f_lat: '31.21315281', t_lon: '121.6341416', t_lat: '31.20506045' },
],
parser: {
type: 'json',
x: 'f_lon',
y: 'f_lat',
x1: 't_lon',
y1: 't_lat',
weight: 'weight',
},
};

const dataProviderState: FlowDataProviderState = {
mapStatus: {
zoom: 10.68,
bounds: [121.489159, 31.053299, 121.779643, 31.279859],
},
clusterType: 'HCA',
clusterZoomStep: 1,
clusterNodeSize: 64,
clusterRadius: 40,
clusterExtent: 512,
maxTopFlowNum: 5000,
maxZoom: 20,
minZoom: 0,
};

const layerOptions: FlowLayerOptions = {
source: flowSource,
circleColor: {
field: 'weight',
value: ['#f00', '#0f0'],
},
circleRadius: {
field: 'weight',
value: [1, 20],
},
circleOpacity: 0.5,
circleStrokeWidth: 2,
circleStrokeColor: '#f00',
lineWidth: 3,
lineColor: '#00f',
lineOpacity: 0.7,
};

describe('flow layer', () => {
const layer = new FlowLayer(layerOptions);

const dataProvider = new DataProvider();

it('layer', () => {
expect(layer.circleLayer.type).toBe('pointLayer');
expect(layer.lineLayer.type).toBe('lineLayer');
});

it('data', () => {
expect(dataProvider.getClusterLevels(flowSource, dataProviderState).length).toBe(10);
expect(dataProvider.getFilterLocations(flowSource, dataProviderState).length).toBe(7);
expect(dataProvider.getFilterFlows(flowSource, dataProviderState).length).toBe(4);
});

it('style', () => {
expect(layer.circleLayer.options['style'].opacity).toBe(0.5);
expect(layer.circleLayer.options['style'].strokeWidth).toBe(2);
expect(layer.circleLayer.options['style'].stroke).toBe('#f00');

expect(layer.lineLayer.options['style'].opacity).toBe(0.7);
});
});
16 changes: 14 additions & 2 deletions packages/composite-layers/package.json
Expand Up @@ -39,10 +39,22 @@
},
"dependencies": {
"@antv/event-emitter": "^0.1.2",
"@antv/util": "^2.0.14"
"@antv/util": "^2.0.14",
"d3-array": "^3.2.4",
"d3-color": "^3.1.0",
"d3-scale": "^3.0.0",
"kdbush": "^3.0.0",
"lodash-es": "^4.17.21",
"reselect": "^4.1.8"
},
"devDependencies": {
"@antv/l7": "^2.11.5"
"@antv/l7": "^2.11.5",
"@babel/core": "^7.22.5",
"@types/d3-array": "^3.0.5",
"@types/d3-color": "^3.1.0",
"@types/d3-scale": "^3.0.0",
"@types/kdbush": "^3.0.2",
"@types/lodash-es": "^4.17.7"
},
"peerDependencies": {
"@antv/l7": "^2.11.5"
Expand Down
@@ -0,0 +1,38 @@
import { FlowLayerOptions } from './types';

export const DEFAULT_OPTIONS: FlowLayerOptions = {
source: {
data: [],
parser: {
type: 'json',
x: 'f_lon',
y: 'f_lat',
x1: 't_lon',
y1: 't_lat',
weight: 'weight',
},
},
clusterType: 'HCA',
clusterZoomStep: 1,
clusterNodeSize: 64,
clusterRadius: 40,
clusterExtent: 512,
maxTopFlowNum: 5000,
circleColor: '#fff',
circleRadius: {
field: 'weight',
value: [1, 16],
},
circleStrokeColor: '#000',
circleStrokeWidth: 1,
lineColor: {
field: 'weight',
value: ['#2a5674', '#d1eeea'],
},
lineWidth: {
field: 'weight',
value: [1, 16],
},
fadeOpacityEnabled: true,
fadeOpacityAmount: 0,
};
@@ -0,0 +1,108 @@
import { ClusterLevel, ClusterLocation } from '../types';
import { findAppropriateZoom } from '../utils';
import { latY, lngX } from './transform';

/**
* 建立聚合点的检索索引对象
* @param clusterLevels
* @returns
*/
export function buildIndex(clusterLevels: ClusterLevel[]) {
const zoomList = clusterLevels.map((i) => i.zoom).reverse();
const clusterIdMap = new Map<string, ClusterLocation>();
const clusterZoomMap = new Map<string, ClusterLocation>();

clusterLevels.forEach(({ locations }) => {
locations.forEach((location) => {
clusterIdMap.set(location.id, location);
});
});

/**
* 计算距离 targetZoom 最近的有效 zoom
* @param targetZoom
* @returns
*/
function getAppropriateLevel(targetZoom: number) {
const matchZoom = findAppropriateZoom(zoomList, targetZoom);
return clusterLevels.find((clusterLevel) => clusterLevel.zoom === matchZoom)!;
}

/**
* 获取当前地图范围内的点
* @param targetZoom
* @param bounds
*/
function getMapLocations(targetZoom: number, bounds: [number, number, number, number]): ClusterLocation[] {
const [lng1, lat1, lng2, lat2] = bounds;
const targetLevel = getAppropriateLevel(targetZoom);
const { locations, locationTree } = targetLevel;
const minX = Math.min(lngX(lng1), lngX(lng2));
const maxX = Math.max(lngX(lng1), lngX(lng2));
const minY = Math.min(latY(lat1), latY(lat2));
const maxY = Math.max(latY(lat1), latY(lat2));
const indexes = locationTree.range(minX, minY, maxX, maxY);
return indexes.map((index) => locations[index]!);
}

/**
* 获取目标聚合点所包含的所有原始客流点 id
* @param clusterId
* @returns
*/
function getLocationIdsFromCluster(clusterId: string): string[] {
const set = new Set<string>();
const clusterIds = [clusterId];
while (clusterIds.length) {
const id = clusterIds.pop();
if (id) {
const cluster = clusterIdMap.get(id);
if (cluster) {
if (cluster.childIds?.length) {
clusterIds.push(...cluster.childIds);
} else {
set.add(cluster.id);
}
}
}
}

return Array.from(set.values());
}

/**
* 为原始聚合点找到其目标层级下的聚合点
* @param locationId
* @param zoom
* @returns
*/
function findClusterForZoom(locationId: string, zoom: number) {
const key = `${locationId}-${zoom}`;
let targetCluster = clusterZoomMap.get(`${locationId}-${zoom}`);
if (targetCluster) {
return targetCluster;
}
let cluster = clusterIdMap.get(locationId);
let parentCluster = (cluster?.parentId && clusterIdMap.get(cluster.parentId)) || undefined;
while (cluster && parentCluster) {
if (cluster.zoom >= zoom && zoom >= parentCluster.zoom) {
break;
}
cluster = parentCluster;
parentCluster = (cluster?.parentId && clusterIdMap.get(cluster.parentId)) || undefined;
}
targetCluster = cluster && cluster.zoom <= zoom ? cluster : parentCluster;
if (targetCluster) {
clusterZoomMap.set(key, targetCluster);
}
return targetCluster;
}

return {
zoomList,
getAppropriateLevel,
getMapLocations,
getLocationIdsFromCluster,
findClusterForZoom,
};
}
@@ -0,0 +1,45 @@
import { ClusterFlow, OriginFlow } from '../../types';
import { buildIndex } from '../build-index';

/**
* 计算客流聚合线
* @param originFlows
* @param clusterIndex
* @param zoom
* @returns
*/
export function clusterFlows(
originFlows: OriginFlow[],
clusterIndex: ReturnType<typeof buildIndex>,
zoom: number
): ClusterFlow[] {
const clusterFlowMap = new Map<string, ClusterFlow>();
// 最底层的 location Id 数组(去重后)
const { zoom: targetZoom } = clusterIndex.getAppropriateLevel(zoom);
originFlows.forEach(({ fromId, toId, weight }) => {
const fromCluster = clusterIndex.findClusterForZoom(fromId, targetZoom);
const toCluster = clusterIndex.findClusterForZoom(toId, targetZoom);

if (!fromCluster || !toCluster || fromCluster.id === toCluster.id) {
return;
}
const clusterFlowId = `${fromCluster.id}-${toCluster.id}`;
let clusterFlow = clusterFlowMap.get(clusterFlowId);
if (!clusterFlow) {
clusterFlow = {
id: clusterFlowId,
fromId: fromCluster.id,
fromLng: fromCluster.lng,
fromLat: fromCluster.lat,
toId: toCluster.id,
toLng: toCluster.lng,
toLat: toCluster.lat,
weight: 0,
};
clusterFlowMap.set(clusterFlowId, clusterFlow);
}
clusterFlow.weight += weight;
});

return Array.from(clusterFlowMap.values()).sort((a, b) => a.weight - b.weight);
}

0 comments on commit 772d7d7

Please sign in to comment.