@fscharter/flowmap-data 8.0.2-fsc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/FlowmapAggregateAccessors.d.ts +16 -0
  2. package/dist/FlowmapAggregateAccessors.d.ts.map +1 -0
  3. package/dist/FlowmapAggregateAccessors.js +53 -0
  4. package/dist/FlowmapSelectors.d.ts +143 -0
  5. package/dist/FlowmapSelectors.d.ts.map +1 -0
  6. package/dist/FlowmapSelectors.js +881 -0
  7. package/dist/FlowmapState.d.ts +31 -0
  8. package/dist/FlowmapState.d.ts.map +1 -0
  9. package/dist/FlowmapState.js +7 -0
  10. package/dist/cluster/ClusterIndex.d.ts +42 -0
  11. package/dist/cluster/ClusterIndex.d.ts.map +1 -0
  12. package/dist/cluster/ClusterIndex.js +166 -0
  13. package/dist/cluster/cluster.d.ts +51 -0
  14. package/dist/cluster/cluster.d.ts.map +1 -0
  15. package/dist/cluster/cluster.js +267 -0
  16. package/dist/colors.d.ts +103 -0
  17. package/dist/colors.d.ts.map +1 -0
  18. package/dist/colors.js +487 -0
  19. package/dist/getViewStateForLocations.d.ts +23 -0
  20. package/dist/getViewStateForLocations.d.ts.map +1 -0
  21. package/dist/getViewStateForLocations.js +54 -0
  22. package/dist/index.d.ts +14 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +18 -0
  25. package/dist/provider/FlowmapDataProvider.d.ts +21 -0
  26. package/dist/provider/FlowmapDataProvider.d.ts.map +1 -0
  27. package/dist/provider/FlowmapDataProvider.js +22 -0
  28. package/dist/provider/LocalFlowmapDataProvider.d.ts +31 -0
  29. package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -0
  30. package/dist/provider/LocalFlowmapDataProvider.js +115 -0
  31. package/dist/selector-functions.d.ts +10 -0
  32. package/dist/selector-functions.d.ts.map +1 -0
  33. package/dist/selector-functions.js +65 -0
  34. package/dist/time.d.ts +24 -0
  35. package/dist/time.d.ts.map +1 -0
  36. package/dist/time.js +131 -0
  37. package/dist/types.d.ts +120 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +28 -0
  40. package/dist/util.d.ts +5 -0
  41. package/dist/util.d.ts.map +1 -0
  42. package/dist/util.js +16 -0
  43. package/package.json +48 -0
  44. package/src/FlowmapAggregateAccessors.ts +76 -0
  45. package/src/FlowmapSelectors.ts +1539 -0
  46. package/src/FlowmapState.ts +40 -0
  47. package/src/cluster/ClusterIndex.ts +261 -0
  48. package/src/cluster/cluster.ts +394 -0
  49. package/src/colors.ts +771 -0
  50. package/src/getViewStateForLocations.ts +86 -0
  51. package/src/index.ts +19 -0
  52. package/src/provider/FlowmapDataProvider.ts +81 -0
  53. package/src/provider/LocalFlowmapDataProvider.ts +185 -0
  54. package/src/selector-functions.ts +93 -0
  55. package/src/time.ts +166 -0
  56. package/src/types.ts +172 -0
  57. package/src/util.ts +17 -0
  58. package/tsconfig.json +11 -0
  59. package/typings.d.ts +1 -0
@@ -0,0 +1,40 @@
1
+ /*
2
+ * Copyright (c) Flowmap.gl contributors
3
+ * Copyright (c) 2018-2020 Teralytics
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {LocationFilterMode, ViewportProps} from './types';
8
+
9
+ export type FlowEndpointsInViewportMode = 'any' | 'both';
10
+
11
+ export interface FilterState {
12
+ selectedLocations?: (string | number)[];
13
+ locationFilterMode?: LocationFilterMode;
14
+ selectedTimeRange?: [Date, Date];
15
+ }
16
+
17
+ export interface SettingsState {
18
+ animationEnabled: boolean;
19
+ fadeEnabled: boolean;
20
+ fadeOpacityEnabled: boolean;
21
+ locationsEnabled: boolean;
22
+ locationTotalsEnabled: boolean;
23
+ locationLabelsEnabled: boolean;
24
+ adaptiveScalesEnabled: boolean;
25
+ clusteringEnabled: boolean;
26
+ clusteringAuto: boolean;
27
+ clusteringLevel?: number;
28
+ darkMode: boolean;
29
+ fadeAmount: number;
30
+ colorScheme: string | string[] | undefined;
31
+ highlightColor: string;
32
+ maxTopFlowsDisplayNum: number;
33
+ flowEndpointsInViewportMode: FlowEndpointsInViewportMode;
34
+ }
35
+
36
+ export interface FlowmapState {
37
+ filter?: FilterState;
38
+ settings: SettingsState;
39
+ viewport: ViewportProps;
40
+ }
@@ -0,0 +1,261 @@
1
+ /*
2
+ * Copyright (c) Flowmap.gl contributors
3
+ * Copyright (c) 2018-2020 Teralytics
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {
8
+ AggregateFlow,
9
+ Cluster,
10
+ ClusterLevels,
11
+ ClusterNode,
12
+ FlowAccessors,
13
+ FlowCountsMapReduce,
14
+ isCluster,
15
+ } from './../types';
16
+ import {ascending, bisectLeft, extent} from 'd3-array';
17
+
18
+ export type LocationWeightGetter = (id: string | number) => number;
19
+
20
+ /**
21
+ * A data structure representing the cluster levels for efficient flow aggregation.
22
+ */
23
+ export interface ClusterIndex<F> {
24
+ availableZoomLevels: number[];
25
+ getClusterById: (clusterId: string | number) => Cluster | undefined;
26
+ /**
27
+ * List the nodes on the given zoom level.
28
+ */
29
+ getClusterNodesFor: (zoom: number | undefined) => ClusterNode[] | undefined;
30
+ /**
31
+ * Get the min zoom level on which the location is not clustered.
32
+ */
33
+ getMinZoomForLocation: (locationId: string | number) => number;
34
+ /**
35
+ * List the IDs of all locations in the cluster (leaves of the subtree starting in the cluster).
36
+ */
37
+ expandCluster: (cluster: Cluster, targetZoom?: number) => string[];
38
+ /**
39
+ * Find the cluster the given location is residing in on the specified zoom level.
40
+ */
41
+ findClusterFor: (
42
+ locationId: string | number,
43
+ zoom: number,
44
+ ) => string | number | undefined;
45
+ /**
46
+ * Aggregate flows for the specified zoom level.
47
+ */
48
+ aggregateFlows: (
49
+ flows: F[],
50
+ zoom: number,
51
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
52
+ options?: {
53
+ flowCountsMapReduce?: FlowCountsMapReduce<F>;
54
+ },
55
+ ) => (F | AggregateFlow)[];
56
+ }
57
+
58
+ /**
59
+ * Build ClusterIndex from the given cluster hierarchy
60
+ */
61
+ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
62
+ const nodesByZoom = new Map<number, ClusterNode[]>();
63
+ const clustersById = new Map<string | number, Cluster>();
64
+ const minZoomByLocationId = new Map<string | number, number>();
65
+ for (const {zoom, nodes} of clusterLevels) {
66
+ nodesByZoom.set(zoom, nodes);
67
+ for (const node of nodes) {
68
+ if (isCluster(node)) {
69
+ clustersById.set(node.id, node);
70
+ } else {
71
+ const {id} = node;
72
+ const mz = minZoomByLocationId.get(id);
73
+ if (mz == null || mz > zoom) {
74
+ minZoomByLocationId.set(id, zoom);
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ const [minZoom, maxZoom] = extent(clusterLevels, (cl) => cl.zoom);
81
+ if (minZoom == null || maxZoom == null) {
82
+ throw new Error('Could not determine minZoom or maxZoom');
83
+ }
84
+
85
+ const leavesToClustersByZoom = new Map<
86
+ number,
87
+ Map<string | number, Cluster>
88
+ >();
89
+
90
+ for (const cluster of clustersById.values()) {
91
+ const {zoom} = cluster;
92
+ let leavesToClusters = leavesToClustersByZoom.get(zoom);
93
+ if (!leavesToClusters) {
94
+ leavesToClusters = new Map<string, Cluster>();
95
+ leavesToClustersByZoom.set(zoom, leavesToClusters);
96
+ }
97
+ visitClusterLeaves(cluster, (leafId) => {
98
+ leavesToClusters?.set(leafId, cluster);
99
+ });
100
+ }
101
+
102
+ function visitClusterLeaves(cluster: Cluster, visit: (id: string) => void) {
103
+ for (const childId of cluster.children) {
104
+ const child = clustersById.get(childId);
105
+ if (child) {
106
+ visitClusterLeaves(child, visit);
107
+ } else {
108
+ visit(childId);
109
+ }
110
+ }
111
+ }
112
+
113
+ const expandCluster = (cluster: Cluster, targetZoom: number = maxZoom) => {
114
+ const ids: string[] = [];
115
+ const visit = (c: Cluster, expandedIds: (string | number)[]) => {
116
+ if (targetZoom > c.zoom) {
117
+ for (const childId of c.children) {
118
+ const child = clustersById.get(childId);
119
+ if (child) {
120
+ visit(child, expandedIds);
121
+ } else {
122
+ expandedIds.push(childId);
123
+ }
124
+ }
125
+ } else {
126
+ expandedIds.push(c.id);
127
+ }
128
+ };
129
+ visit(cluster, ids);
130
+ return ids;
131
+ };
132
+
133
+ function findClusterFor(locationId: string | number, zoom: number) {
134
+ const leavesToClusters = leavesToClustersByZoom.get(zoom);
135
+ if (!leavesToClusters) {
136
+ return undefined;
137
+ }
138
+ const cluster = leavesToClusters.get(locationId);
139
+ return cluster ? cluster.id : undefined;
140
+ }
141
+
142
+ const availableZoomLevels = clusterLevels
143
+ .map((cl) => +cl.zoom)
144
+ .sort((a, b) => ascending(a, b));
145
+
146
+ return {
147
+ availableZoomLevels,
148
+
149
+ getClusterNodesFor: (zoom) => {
150
+ if (zoom === undefined) {
151
+ return undefined;
152
+ }
153
+ return nodesByZoom.get(zoom);
154
+ },
155
+
156
+ getClusterById: (clusterId) => clustersById.get(clusterId),
157
+
158
+ getMinZoomForLocation: (locationId) =>
159
+ minZoomByLocationId.get(locationId) || minZoom,
160
+
161
+ expandCluster,
162
+
163
+ findClusterFor,
164
+
165
+ aggregateFlows: (
166
+ flows,
167
+ zoom,
168
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude},
169
+ options = {},
170
+ ) => {
171
+ if (zoom > maxZoom) {
172
+ return flows;
173
+ }
174
+ const result: (F | AggregateFlow)[] = [];
175
+ const aggFlowsByKey = new Map<string, AggregateFlow>();
176
+ const makeKey = (origin: string | number, dest: string | number) =>
177
+ `${origin}:${dest}`;
178
+ const {
179
+ flowCountsMapReduce = {
180
+ map: getFlowMagnitude,
181
+ reduce: (acc: any, count: number) => (acc || 0) + count,
182
+ },
183
+ } = options;
184
+ for (const flow of flows) {
185
+ const origin = getFlowOriginId(flow);
186
+ const dest = getFlowDestId(flow);
187
+ const originCluster = findClusterFor(origin, zoom) || origin;
188
+ const destCluster = findClusterFor(dest, zoom) || dest;
189
+ const key = makeKey(originCluster, destCluster);
190
+ if (originCluster === origin && destCluster === dest) {
191
+ result.push(flow);
192
+ } else {
193
+ let aggregateFlow = aggFlowsByKey.get(key);
194
+ if (!aggregateFlow) {
195
+ aggregateFlow = {
196
+ origin: originCluster,
197
+ dest: destCluster,
198
+ count: flowCountsMapReduce.map(flow),
199
+ aggregate: true,
200
+ };
201
+ result.push(aggregateFlow);
202
+ aggFlowsByKey.set(key, aggregateFlow);
203
+ } else {
204
+ aggregateFlow.count = flowCountsMapReduce.reduce(
205
+ aggregateFlow.count,
206
+ flowCountsMapReduce.map(flow),
207
+ );
208
+ }
209
+ }
210
+ }
211
+ return result;
212
+ },
213
+ };
214
+ }
215
+
216
+ export function makeLocationWeightGetter<F>(
217
+ flows: F[],
218
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
219
+ ): LocationWeightGetter {
220
+ const locationTotals = {
221
+ incoming: new Map<string | number, number>(),
222
+ outgoing: new Map<string | number, number>(),
223
+ };
224
+ for (const flow of flows) {
225
+ const origin = getFlowOriginId(flow);
226
+ const dest = getFlowDestId(flow);
227
+ const count = getFlowMagnitude(flow);
228
+ locationTotals.incoming.set(
229
+ dest,
230
+ (locationTotals.incoming.get(dest) || 0) + count,
231
+ );
232
+ locationTotals.outgoing.set(
233
+ origin,
234
+ (locationTotals.outgoing.get(origin) || 0) + count,
235
+ );
236
+ }
237
+ return (id: string | number) =>
238
+ Math.max(
239
+ Math.abs(locationTotals.incoming.get(id) || 0),
240
+ Math.abs(locationTotals.outgoing.get(id) || 0),
241
+ );
242
+ }
243
+
244
+ /**
245
+ * @param availableZoomLevels Must be sorted in ascending order
246
+ * @param targetZoom
247
+ */
248
+ export function findAppropriateZoomLevel(
249
+ availableZoomLevels: number[],
250
+ targetZoom: number,
251
+ ) {
252
+ if (!availableZoomLevels.length) {
253
+ throw new Error('No available zoom levels');
254
+ }
255
+ return availableZoomLevels[
256
+ Math.min(
257
+ bisectLeft(availableZoomLevels, Math.floor(targetZoom)),
258
+ availableZoomLevels.length - 1,
259
+ )
260
+ ];
261
+ }
@@ -0,0 +1,394 @@
1
+ /*
2
+ * Copyright (c) Flowmap.gl contributors
3
+ * Copyright (c) 2018-2020 Teralytics
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {min, rollup} from 'd3-array';
8
+ import KDBush from 'kdbush';
9
+ import {LocationWeightGetter} from './ClusterIndex';
10
+ import {Cluster, ClusterLevel, ClusterNode, LocationAccessors} from '../types';
11
+
12
+ /**
13
+ * The code in this file is a based on https://github.com/mapbox/supercluster
14
+ *
15
+ * ISC License
16
+ *
17
+ * Copyright (c) 2016, Mapbox
18
+ *
19
+ * Permission to use, copy, modify, and/or distribute this software for any purpose
20
+ * with or without fee is hereby granted, provided that the above copyright notice
21
+ * and this permission notice appear in all copies.
22
+ *
23
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
24
+ * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
25
+ * FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
26
+ * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
27
+ * OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
28
+ * TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
29
+ * THIS SOFTWARE.
30
+ */
31
+
32
+ export interface Options {
33
+ minZoom: number; // min zoom to generate clusters on
34
+ maxZoom: number; // max zoom level to cluster the points on
35
+ radius: number; // cluster radius in pixels
36
+ extent: number; // tile extent (radius is calculated relative to it)
37
+ nodeSize: number; // size of the KD-tree leaf node, affects performance
38
+ makeClusterName: (id: number, numPoints: number) => string | undefined;
39
+ makeClusterId: (id: number) => string;
40
+ }
41
+
42
+ const defaultOptions: Options = {
43
+ minZoom: 0,
44
+ maxZoom: 16,
45
+ radius: 40,
46
+ extent: 512,
47
+ nodeSize: 64,
48
+ makeClusterName: (id: number, numPoints: number) => undefined,
49
+ makeClusterId: (id: number) => `{[${id}]}`,
50
+ };
51
+
52
+ interface BasePoint {
53
+ x: number; // projected point coordinates
54
+ y: number;
55
+ weight: number;
56
+ zoom: number; // the last zoom the point was processed at
57
+ parentId: number; // parent cluster id
58
+ }
59
+
60
+ interface LeafPoint<L> extends BasePoint {
61
+ index: number; // index of the source feature in the original input array,
62
+ location: L;
63
+ }
64
+
65
+ interface ClusterPoint extends BasePoint {
66
+ id: number;
67
+ numPoints: number;
68
+ }
69
+
70
+ type Point<L> = LeafPoint<L> | ClusterPoint;
71
+
72
+ export function isLeafPoint<L>(p: Point<L>): p is LeafPoint<L> {
73
+ const {index} = p as LeafPoint<L>;
74
+ return index != null;
75
+ }
76
+
77
+ export function isClusterPoint<L>(p: Point<L>): p is ClusterPoint {
78
+ const {id} = p as ClusterPoint;
79
+ return id != null;
80
+ }
81
+
82
+ type ZoomLevelKDBush = any;
83
+
84
+ export function clusterLocations<L>(
85
+ locations: Iterable<L>,
86
+ locationAccessors: LocationAccessors<L>,
87
+ getLocationWeight: LocationWeightGetter,
88
+ options?: Partial<Options>,
89
+ ): ClusterLevel[] {
90
+ const {getLocationLon, getLocationLat, getLocationId} = locationAccessors;
91
+ const opts = {
92
+ ...defaultOptions,
93
+ ...options,
94
+ };
95
+ const {minZoom, maxZoom, nodeSize, makeClusterName, makeClusterId} = opts;
96
+
97
+ const trees = new Array<ZoomLevelKDBush>(maxZoom + 1);
98
+
99
+ // generate a cluster object for each point and index input points into a KD-tree
100
+ let clusters = new Array<Point<L>>();
101
+ let locationsCount = 0;
102
+ for (const location of locations) {
103
+ const x = getLocationLon(location);
104
+ const y = getLocationLat(location);
105
+ clusters.push({
106
+ x: lngX(x), // projected point coordinates
107
+ y: latY(y),
108
+ weight: getLocationWeight(getLocationId(location)),
109
+ zoom: Infinity, // the last zoom the point was processed at
110
+ index: locationsCount, // index of the source feature in the original input array,
111
+ parentId: -1, // parent cluster id
112
+ location,
113
+ });
114
+ locationsCount++;
115
+ }
116
+
117
+ const makeBush = (points: Point<L>[]) => {
118
+ const bush = new KDBush(points.length, nodeSize, Float32Array);
119
+ for (let i = 0; i < points.length; i++) {
120
+ bush.add(points[i].x, points[i].y);
121
+ }
122
+ bush.finish();
123
+ bush.points = points;
124
+ return bush;
125
+ };
126
+
127
+ // cluster points on max zoom, then cluster the results on previous zoom, etc.;
128
+ // results in a cluster hierarchy across zoom levels
129
+ trees[maxZoom + 1] = makeBush(clusters);
130
+ let prevZoom = maxZoom + 1;
131
+
132
+ for (let z = maxZoom; z >= minZoom; z--) {
133
+ // create a new set of clusters for the zoom and index them with a KD-tree
134
+ const _clusters = cluster(clusters, z, trees[prevZoom], opts);
135
+ if (_clusters.length === clusters.length) {
136
+ // same number of clusters => move the higher level clusters up
137
+ // no need to keep the same data on multiple levels
138
+ trees[z] = trees[prevZoom];
139
+ trees[prevZoom] = undefined;
140
+ prevZoom = z;
141
+ clusters = _clusters;
142
+ } else {
143
+ prevZoom = z;
144
+ clusters = _clusters;
145
+ trees[z] = makeBush(clusters);
146
+ }
147
+ }
148
+
149
+ if (trees.length === 0) {
150
+ return [];
151
+ }
152
+
153
+ const numbersOfClusters: number[] = trees.map((d) => d?.points.length);
154
+ const minClusters = min(numbersOfClusters.filter((d) => d > 0));
155
+
156
+ let maxAvailZoom =
157
+ findIndexOfMax(numbersOfClusters) ?? numbersOfClusters.length - 1;
158
+
159
+ const numUniqueLocations = countUniqueLocations(locations, locationAccessors);
160
+ if (numUniqueLocations < locationsCount) {
161
+ // Duplicate locations would be clustered together at any zoom level which can lead to having too many zooms.
162
+ // To avoid that, we need to find the max zoom level that has less or equal clusters than unique locations
163
+ // and drop all zoom levels beyond that (except the unclustered level).
164
+ const maxClustersZoom = findLastIndex(
165
+ numbersOfClusters,
166
+ (d) => d <= numUniqueLocations,
167
+ );
168
+ if (maxClustersZoom >= 0) {
169
+ // Now, move the unclustered points to the next zoom level to avoid having a gap
170
+ if (maxClustersZoom < maxAvailZoom) {
171
+ trees[maxClustersZoom + 1] = trees[maxAvailZoom];
172
+ trees.splice(maxClustersZoom + 2); // Remove all zoom levels beyond maxClustersZoom
173
+ }
174
+ maxAvailZoom = maxClustersZoom + 1;
175
+ }
176
+ }
177
+
178
+ const minAvailZoom = Math.min(
179
+ maxAvailZoom,
180
+ minClusters ? numbersOfClusters.lastIndexOf(minClusters) : maxAvailZoom,
181
+ );
182
+
183
+ const clusterLevels = new Array<ClusterLevel>();
184
+ prevZoom = NaN;
185
+ for (let zoom = maxAvailZoom; zoom >= minAvailZoom; zoom--) {
186
+ let childrenByParent: Map<number, (string | number)[]> | undefined;
187
+ const tree = trees[zoom];
188
+ if (!tree) continue;
189
+ if (trees[prevZoom] && zoom < maxAvailZoom) {
190
+ childrenByParent = rollup(
191
+ trees[prevZoom].points,
192
+ (points: any[]) =>
193
+ points.map((p: any) =>
194
+ p.id ? makeClusterId(p.id) : getLocationId(p.location),
195
+ ),
196
+ (point: any) => point.parentId,
197
+ );
198
+ }
199
+
200
+ const nodes: ClusterNode[] = [];
201
+ for (const point of tree.points) {
202
+ const {x, y, numPoints, location} = point;
203
+ if (isLeafPoint(point)) {
204
+ nodes.push({
205
+ id: getLocationId(location),
206
+ zoom,
207
+ lat: getLocationLat(location),
208
+ lon: getLocationLon(location),
209
+ });
210
+ } else if (isClusterPoint(point)) {
211
+ const {id} = point;
212
+ const children = childrenByParent && childrenByParent.get(id);
213
+ if (!children) {
214
+ // Might happen if there are multiple locations with same coordinates
215
+ console.warn(`Omitting cluster with no children, point:`, point);
216
+ continue;
217
+ }
218
+ const cluster = {
219
+ id: makeClusterId(id),
220
+ name: makeClusterName(id, numPoints),
221
+ zoom,
222
+ lat: yLat(y),
223
+ lon: xLng(x),
224
+ children: children ?? [],
225
+ } as Cluster;
226
+ nodes.push(cluster);
227
+ }
228
+ }
229
+ clusterLevels.push({
230
+ zoom,
231
+ nodes,
232
+ });
233
+ prevZoom = zoom;
234
+ }
235
+ return clusterLevels;
236
+ }
237
+
238
+ function createCluster(
239
+ x: number,
240
+ y: number,
241
+ id: number,
242
+ numPoints: number,
243
+ weight: number,
244
+ ): ClusterPoint {
245
+ return {
246
+ x, // weighted cluster center
247
+ y,
248
+ zoom: Infinity, // the last zoom the cluster was processed at
249
+ id, // encodes index of the first child of the cluster and its zoom level
250
+ parentId: -1, // parent cluster id
251
+ numPoints,
252
+ weight,
253
+ };
254
+ }
255
+
256
+ function cluster<L>(
257
+ points: Point<L>[],
258
+ zoom: number,
259
+ tree: ZoomLevelKDBush,
260
+ options: Options,
261
+ ) {
262
+ const clusters: Point<L>[] = [];
263
+ const {radius, extent} = options;
264
+ const r = radius / (extent * Math.pow(2, zoom));
265
+
266
+ // loop through each point
267
+ for (let i = 0; i < points.length; i++) {
268
+ const p = points[i];
269
+ // if we've already visited the point at this zoom level, skip it
270
+ if (p.zoom <= zoom) {
271
+ continue;
272
+ }
273
+ p.zoom = zoom;
274
+
275
+ // find all nearby points
276
+ const neighborIds = tree.within(p.x, p.y, r);
277
+
278
+ let weight = p.weight || 1;
279
+ let numPoints = isClusterPoint(p) ? p.numPoints : 1;
280
+ let wx = p.x * weight;
281
+ let wy = p.y * weight;
282
+
283
+ // encode both zoom and point index on which the cluster originated
284
+ const id = (i << 5) + (zoom + 1);
285
+
286
+ for (const neighborId of neighborIds) {
287
+ const b = tree.points[neighborId];
288
+ // filter out neighbors that are already processed
289
+ if (b.zoom <= zoom) {
290
+ continue;
291
+ }
292
+ b.zoom = zoom; // save the zoom (so it doesn't get processed twice)
293
+
294
+ const weight2 = b.weight || 1;
295
+ const numPoints2 = b.numPoints || 1;
296
+ wx += b.x * weight2; // accumulate coordinates for calculating weighted center
297
+ wy += b.y * weight2;
298
+
299
+ weight += weight2;
300
+ numPoints += numPoints2;
301
+ b.parentId = id;
302
+ }
303
+
304
+ if (numPoints === 1) {
305
+ clusters.push(p);
306
+ } else {
307
+ p.parentId = id;
308
+ clusters.push(
309
+ createCluster(wx / weight, wy / weight, id, numPoints, weight),
310
+ );
311
+ }
312
+ }
313
+
314
+ return clusters;
315
+ }
316
+
317
+ // spherical mercator to longitude/latitude
318
+ function xLng(x: number) {
319
+ return (x - 0.5) * 360;
320
+ }
321
+
322
+ function yLat(y: number) {
323
+ const y2 = ((180 - y * 360) * Math.PI) / 180;
324
+ return (360 * Math.atan(Math.exp(y2))) / Math.PI - 90;
325
+ }
326
+
327
+ // longitude/latitude to spherical mercator in [0..1] range
328
+ function lngX(lng: number) {
329
+ return lng / 360 + 0.5;
330
+ }
331
+
332
+ function latY(lat: number) {
333
+ const sin = Math.sin((lat * Math.PI) / 180);
334
+ const y = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI;
335
+ return y < 0 ? 0 : y > 1 ? 1 : y;
336
+ }
337
+
338
+ function getX<L>(p: Point<L>) {
339
+ return p.x;
340
+ }
341
+
342
+ function getY<L>(p: Point<L>) {
343
+ return p.y;
344
+ }
345
+
346
+ function countUniqueLocations<L>(
347
+ locations: Iterable<L>,
348
+ locationAccessors: LocationAccessors<L>,
349
+ ) {
350
+ const {getLocationLon, getLocationLat} = locationAccessors;
351
+ const countByLatLon = new Map<string, number>();
352
+ let uniqueCnt = 0;
353
+ for (const loc of locations) {
354
+ const lon = getLocationLon(loc);
355
+ const lat = getLocationLat(loc);
356
+ const key = `${lon},${lat}`;
357
+ const prev = countByLatLon.get(key);
358
+ if (!prev) {
359
+ uniqueCnt++;
360
+ }
361
+ countByLatLon.set(key, prev ? prev + 1 : 1);
362
+ }
363
+ return uniqueCnt;
364
+ }
365
+
366
+ function findIndexOfMax(arr: (number | undefined)[]): number | undefined {
367
+ let max = -Infinity;
368
+ let maxIndex: number | undefined = undefined;
369
+
370
+ for (let i = 0; i < arr.length; i++) {
371
+ const value = arr[i];
372
+
373
+ if (typeof value === 'number') {
374
+ if (value > max) {
375
+ max = value;
376
+ maxIndex = i;
377
+ }
378
+ }
379
+ }
380
+
381
+ return maxIndex;
382
+ }
383
+
384
+ function findLastIndex<T>(
385
+ arr: T[],
386
+ predicate: (value: T, index: number, array: T[]) => boolean,
387
+ ): number {
388
+ for (let i = arr.length - 1; i >= 0; i--) {
389
+ if (predicate(arr[i], i, arr)) {
390
+ return i;
391
+ }
392
+ }
393
+ return -1;
394
+ }