@flowmap.gl/data 8.0.0-alpha.0

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 (56) hide show
  1. package/LICENSE +199 -0
  2. package/dist/FlowMapAggregateAccessors.d.ts +15 -0
  3. package/dist/FlowMapAggregateAccessors.d.ts.map +1 -0
  4. package/dist/FlowMapAggregateAccessors.js +43 -0
  5. package/dist/FlowMapSelectors.d.ts +156 -0
  6. package/dist/FlowMapSelectors.d.ts.map +1 -0
  7. package/dist/FlowMapSelectors.js +831 -0
  8. package/dist/FlowMapState.d.ts +24 -0
  9. package/dist/FlowMapState.d.ts.map +1 -0
  10. package/dist/FlowMapState.js +2 -0
  11. package/dist/cluster/ClusterIndex.d.ts +42 -0
  12. package/dist/cluster/ClusterIndex.d.ts.map +1 -0
  13. package/dist/cluster/ClusterIndex.js +178 -0
  14. package/dist/cluster/cluster.d.ts +31 -0
  15. package/dist/cluster/cluster.d.ts.map +1 -0
  16. package/dist/cluster/cluster.js +206 -0
  17. package/dist/colors.d.ts +103 -0
  18. package/dist/colors.d.ts.map +1 -0
  19. package/dist/colors.js +441 -0
  20. package/dist/getViewStateForLocations.d.ts +16 -0
  21. package/dist/getViewStateForLocations.d.ts.map +1 -0
  22. package/dist/getViewStateForLocations.js +30 -0
  23. package/dist/index.d.ts +11 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +10 -0
  26. package/dist/provider/FlowMapDataProvider.d.ts +16 -0
  27. package/dist/provider/FlowMapDataProvider.d.ts.map +1 -0
  28. package/dist/provider/FlowMapDataProvider.js +17 -0
  29. package/dist/provider/LocalFlowMapDataProvider.d.ts +20 -0
  30. package/dist/provider/LocalFlowMapDataProvider.d.ts.map +1 -0
  31. package/dist/provider/LocalFlowMapDataProvider.js +87 -0
  32. package/dist/time.d.ts +24 -0
  33. package/dist/time.d.ts.map +1 -0
  34. package/dist/time.js +126 -0
  35. package/dist/types.d.ts +116 -0
  36. package/dist/types.d.ts.map +1 -0
  37. package/dist/types.js +23 -0
  38. package/dist/util.d.ts +2 -0
  39. package/dist/util.d.ts.map +1 -0
  40. package/dist/util.js +4 -0
  41. package/package.json +48 -0
  42. package/src/FlowMapAggregateAccessors.ts +60 -0
  43. package/src/FlowMapSelectors.ts +1407 -0
  44. package/src/FlowMapState.ts +26 -0
  45. package/src/cluster/ClusterIndex.ts +266 -0
  46. package/src/cluster/cluster.ts +299 -0
  47. package/src/colors.ts +723 -0
  48. package/src/getViewStateForLocations.ts +64 -0
  49. package/src/index.ts +10 -0
  50. package/src/provider/FlowMapDataProvider.ts +63 -0
  51. package/src/provider/LocalFlowMapDataProvider.ts +108 -0
  52. package/src/time.ts +160 -0
  53. package/src/types.ts +162 -0
  54. package/src/util.ts +3 -0
  55. package/tsconfig.json +11 -0
  56. package/typings.d.ts +1 -0
@@ -0,0 +1,26 @@
1
+ import {LocationFilterMode, ViewportProps} from './types';
2
+
3
+ export interface FilterState {
4
+ selectedLocations: string[] | undefined;
5
+ selectedTimeRange: [Date, Date] | undefined;
6
+ locationFilterMode: LocationFilterMode;
7
+ }
8
+
9
+ export interface SettingsState {
10
+ animationEnabled: boolean;
11
+ fadeEnabled: boolean;
12
+ locationTotalsEnabled: boolean;
13
+ adaptiveScalesEnabled: boolean;
14
+ clusteringEnabled: boolean;
15
+ clusteringAuto: boolean;
16
+ clusteringLevel?: number;
17
+ darkMode: boolean;
18
+ fadeAmount: number;
19
+ colorScheme: string | undefined;
20
+ }
21
+
22
+ export interface FlowMapState {
23
+ filterState: FilterState;
24
+ settingsState: SettingsState;
25
+ viewport: ViewportProps;
26
+ }
@@ -0,0 +1,266 @@
1
+ /*
2
+ * Copyright 2022 FlowmapBlue
3
+ * Copyright 2018-2020 Teralytics, modified by FlowmapBlue
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ */
18
+
19
+ import {
20
+ AggregateFlow,
21
+ Cluster,
22
+ ClusterLevels,
23
+ ClusterNode,
24
+ FlowAccessors,
25
+ FlowCountsMapReduce,
26
+ isCluster,
27
+ } from './../types';
28
+ import {ascending, bisectLeft, extent} from 'd3-array';
29
+
30
+ export type LocationWeightGetter = (id: string) => number;
31
+
32
+ /**
33
+ * A data structure representing the cluster levels for efficient flow aggregation.
34
+ */
35
+ export interface ClusterIndex<F> {
36
+ availableZoomLevels: number[];
37
+ getClusterById: (clusterId: string) => Cluster | undefined;
38
+ /**
39
+ * List the nodes on the given zoom level.
40
+ */
41
+ getClusterNodesFor: (zoom: number | undefined) => ClusterNode[] | undefined;
42
+ /**
43
+ * Get the min zoom level on which the location is not clustered.
44
+ */
45
+ getMinZoomForLocation: (locationId: string) => number;
46
+ /**
47
+ * List the IDs of all locations in the cluster (leaves of the subtree starting in the cluster).
48
+ */
49
+ expandCluster: (cluster: Cluster, targetZoom?: number) => string[];
50
+ /**
51
+ * Find the cluster the given location is residing in on the specified zoom level.
52
+ */
53
+ findClusterFor: (locationId: string, zoom: number) => string | undefined;
54
+ /**
55
+ * Aggregate flows for the specified zoom level.
56
+ */
57
+ aggregateFlows: (
58
+ flows: F[],
59
+ zoom: number,
60
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
61
+ options?: {
62
+ flowCountsMapReduce?: FlowCountsMapReduce<F>;
63
+ },
64
+ ) => (F | AggregateFlow)[];
65
+ }
66
+
67
+ /**
68
+ * Build ClusterIndex from the given cluster hierarchy
69
+ */
70
+ export function buildIndex<F>(clusterLevels: ClusterLevels): ClusterIndex<F> {
71
+ const nodesByZoom = new Map<number, ClusterNode[]>();
72
+ const clustersById = new Map<string, Cluster>();
73
+ const minZoomByLocationId = new Map<string, number>();
74
+ for (const {zoom, nodes} of clusterLevels) {
75
+ nodesByZoom.set(zoom, nodes);
76
+ for (const node of nodes) {
77
+ if (isCluster(node)) {
78
+ clustersById.set(node.id, node);
79
+ } else {
80
+ const {id} = node;
81
+ const mz = minZoomByLocationId.get(id);
82
+ if (mz == null || mz > zoom) {
83
+ minZoomByLocationId.set(id, zoom);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ const [minZoom, maxZoom] = extent(clusterLevels, (cl) => cl.zoom);
90
+ if (minZoom == null || maxZoom == null) {
91
+ throw new Error('Could not determine minZoom or maxZoom');
92
+ }
93
+
94
+ const leavesToClustersByZoom = new Map<number, Map<string, Cluster>>();
95
+
96
+ for (const cluster of clustersById.values()) {
97
+ const {zoom} = cluster;
98
+ let leavesToClusters = leavesToClustersByZoom.get(zoom);
99
+ if (!leavesToClusters) {
100
+ leavesToClusters = new Map<string, Cluster>();
101
+ leavesToClustersByZoom.set(zoom, leavesToClusters);
102
+ }
103
+ visitClusterLeaves(cluster, (leafId) => {
104
+ leavesToClusters?.set(leafId, cluster);
105
+ });
106
+ }
107
+
108
+ function visitClusterLeaves(cluster: Cluster, visit: (id: string) => void) {
109
+ for (const childId of cluster.children) {
110
+ const child = clustersById.get(childId);
111
+ if (child) {
112
+ visitClusterLeaves(child, visit);
113
+ } else {
114
+ visit(childId);
115
+ }
116
+ }
117
+ }
118
+
119
+ const expandCluster = (cluster: Cluster, targetZoom: number = maxZoom) => {
120
+ const ids: string[] = [];
121
+ const visit = (c: Cluster, expandedIds: string[]) => {
122
+ if (targetZoom > c.zoom) {
123
+ for (const childId of c.children) {
124
+ const child = clustersById.get(childId);
125
+ if (child) {
126
+ visit(child, expandedIds);
127
+ } else {
128
+ expandedIds.push(childId);
129
+ }
130
+ }
131
+ } else {
132
+ expandedIds.push(c.id);
133
+ }
134
+ };
135
+ visit(cluster, ids);
136
+ return ids;
137
+ };
138
+
139
+ function findClusterFor(locationId: string, zoom: number) {
140
+ const leavesToClusters = leavesToClustersByZoom.get(zoom);
141
+ if (!leavesToClusters) {
142
+ return undefined;
143
+ }
144
+ const cluster = leavesToClusters.get(locationId);
145
+ return cluster ? cluster.id : undefined;
146
+ }
147
+
148
+ const availableZoomLevels = clusterLevels
149
+ .map((cl) => +cl.zoom)
150
+ .sort((a, b) => ascending(a, b));
151
+
152
+ return {
153
+ availableZoomLevels,
154
+
155
+ getClusterNodesFor: (zoom) => {
156
+ if (zoom === undefined) {
157
+ return undefined;
158
+ }
159
+ return nodesByZoom.get(zoom);
160
+ },
161
+
162
+ getClusterById: (clusterId) => clustersById.get(clusterId),
163
+
164
+ getMinZoomForLocation: (locationId) =>
165
+ minZoomByLocationId.get(locationId) || minZoom,
166
+
167
+ expandCluster,
168
+
169
+ findClusterFor,
170
+
171
+ aggregateFlows: (
172
+ flows,
173
+ zoom,
174
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude},
175
+ options = {},
176
+ ) => {
177
+ if (zoom > maxZoom) {
178
+ return flows;
179
+ }
180
+ const result: (F | AggregateFlow)[] = [];
181
+ const aggFlowsByKey = new Map<string, AggregateFlow>();
182
+ const makeKey = (origin: string, dest: string) => `${origin}:${dest}`;
183
+ const {
184
+ flowCountsMapReduce = {
185
+ map: getFlowMagnitude,
186
+ reduce: (acc: any, count: number) => (acc || 0) + count,
187
+ },
188
+ } = options;
189
+ for (const flow of flows) {
190
+ const origin = getFlowOriginId(flow);
191
+ const dest = getFlowDestId(flow);
192
+ const originCluster = findClusterFor(origin, zoom) || origin;
193
+ const destCluster = findClusterFor(dest, zoom) || dest;
194
+ const key = makeKey(originCluster, destCluster);
195
+ if (originCluster === origin && destCluster === dest) {
196
+ result.push(flow);
197
+ } else {
198
+ let aggregateFlow = aggFlowsByKey.get(key);
199
+ if (!aggregateFlow) {
200
+ aggregateFlow = {
201
+ origin: originCluster,
202
+ dest: destCluster,
203
+ count: flowCountsMapReduce.map(flow),
204
+ aggregate: true,
205
+ };
206
+ result.push(aggregateFlow);
207
+ aggFlowsByKey.set(key, aggregateFlow);
208
+ } else {
209
+ aggregateFlow.count = flowCountsMapReduce.reduce(
210
+ aggregateFlow.count,
211
+ flowCountsMapReduce.map(flow),
212
+ );
213
+ }
214
+ }
215
+ }
216
+ return result;
217
+ },
218
+ };
219
+ }
220
+
221
+ export function makeLocationWeightGetter<F>(
222
+ flows: F[],
223
+ {getFlowOriginId, getFlowDestId, getFlowMagnitude}: FlowAccessors<F>,
224
+ ): LocationWeightGetter {
225
+ const locationTotals = {
226
+ incoming: new Map<string, number>(),
227
+ outgoing: new Map<string, number>(),
228
+ };
229
+ for (const flow of flows) {
230
+ const origin = getFlowOriginId(flow);
231
+ const dest = getFlowDestId(flow);
232
+ const count = getFlowMagnitude(flow);
233
+ locationTotals.incoming.set(
234
+ dest,
235
+ (locationTotals.incoming.get(dest) || 0) + count,
236
+ );
237
+ locationTotals.outgoing.set(
238
+ origin,
239
+ (locationTotals.outgoing.get(origin) || 0) + count,
240
+ );
241
+ }
242
+ return (id: string) =>
243
+ Math.max(
244
+ Math.abs(locationTotals.incoming.get(id) || 0),
245
+ Math.abs(locationTotals.outgoing.get(id) || 0),
246
+ );
247
+ }
248
+
249
+ /**
250
+ * @param availableZoomLevels Must be sorted in ascending order
251
+ * @param targetZoom
252
+ */
253
+ export function findAppropriateZoomLevel(
254
+ availableZoomLevels: number[],
255
+ targetZoom: number,
256
+ ) {
257
+ if (!availableZoomLevels.length) {
258
+ throw new Error('No available zoom levels');
259
+ }
260
+ return availableZoomLevels[
261
+ Math.min(
262
+ bisectLeft(availableZoomLevels, Math.floor(targetZoom)),
263
+ availableZoomLevels.length - 1,
264
+ )
265
+ ];
266
+ }
@@ -0,0 +1,299 @@
1
+ /*
2
+ * Copyright 2022 FlowmapBlue
3
+ * Copyright 2018-2020 Teralytics, modified by FlowmapBlue
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ *
17
+ */
18
+
19
+ /**
20
+ * The code in this file is a based on https://github.com/mapbox/supercluster
21
+ */
22
+
23
+ // ISC License
24
+ //
25
+ // Copyright (c) 2016, Mapbox
26
+ //
27
+ // Permission to use, copy, modify, and/or distribute this software for any purpose
28
+ // with or without fee is hereby granted, provided that the above copyright notice
29
+ // and this permission notice appear in all copies.
30
+ //
31
+ // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
32
+ // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
33
+ // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
34
+ // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
35
+ // OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
36
+ // TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
37
+ // THIS SOFTWARE.
38
+
39
+ import {rollup} from 'd3-array';
40
+ import KDBush from 'kdbush';
41
+ import {LocationWeightGetter} from './ClusterIndex';
42
+ import {Cluster, ClusterLevel, ClusterNode, LocationAccessors} from '../types';
43
+
44
+ export interface Options {
45
+ minZoom: number; // min zoom to generate clusters on
46
+ maxZoom: number; // max zoom level to cluster the points on
47
+ radius: number; // cluster radius in pixels
48
+ extent: number; // tile extent (radius is calculated relative to it)
49
+ nodeSize: number; // size of the KD-tree leaf node, affects performance
50
+ makeClusterName: (id: number, numPoints: number) => string | undefined;
51
+ makeClusterId: (id: number) => string;
52
+ }
53
+
54
+ const defaultOptions: Options = {
55
+ minZoom: 0,
56
+ maxZoom: 16,
57
+ radius: 40,
58
+ extent: 512,
59
+ nodeSize: 64,
60
+ makeClusterName: (id: number, numPoints: number) => undefined,
61
+ makeClusterId: (id: number) => `{[${id}]}`,
62
+ };
63
+
64
+ interface BasePoint {
65
+ x: number; // projected point coordinates
66
+ y: number;
67
+ weight: number;
68
+ zoom: number; // the last zoom the point was processed at
69
+ parentId: number; // parent cluster id
70
+ }
71
+
72
+ interface LeafPoint extends BasePoint {
73
+ index: number; // index of the source feature in the original input array,
74
+ }
75
+
76
+ interface ClusterPoint extends BasePoint {
77
+ id: number;
78
+ numPoints: number;
79
+ }
80
+
81
+ type Point = LeafPoint | ClusterPoint;
82
+
83
+ export function isLeafPoint(p: Point): p is LeafPoint {
84
+ const {index} = p as LeafPoint;
85
+ return index != null;
86
+ }
87
+
88
+ export function isClusterPoint(p: Point): p is ClusterPoint {
89
+ const {id} = p as ClusterPoint;
90
+ return id != null;
91
+ }
92
+
93
+ type ZoomLevelKDBush = any;
94
+
95
+ export function clusterLocations<L>(
96
+ locations: L[],
97
+ locationAccessors: LocationAccessors<L>,
98
+ getLocationWeight: LocationWeightGetter,
99
+ options?: Partial<Options>,
100
+ ): ClusterLevel[] {
101
+ const {getLocationCentroid, getLocationId} = locationAccessors;
102
+ const opts = {
103
+ ...defaultOptions,
104
+ ...options,
105
+ };
106
+ const {minZoom, maxZoom, nodeSize, makeClusterName, makeClusterId} = opts;
107
+
108
+ const trees = new Array<ZoomLevelKDBush>(maxZoom + 1);
109
+
110
+ // generate a cluster object for each point and index input points into a KD-tree
111
+ let clusters = new Array<Point>();
112
+ for (let i = 0; i < locations.length; i++) {
113
+ const [x, y] = getLocationCentroid(locations[i]);
114
+ clusters.push({
115
+ x: lngX(x), // projected point coordinates
116
+ y: latY(y),
117
+ weight: getLocationWeight(getLocationId(locations[i])),
118
+ zoom: Infinity, // the last zoom the point was processed at
119
+ index: i, // index of the source feature in the original input array,
120
+ parentId: -1, // parent cluster id
121
+ });
122
+ }
123
+ trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
124
+
125
+ // cluster points on max zoom, then cluster the results on previous zoom, etc.;
126
+ // results in a cluster hierarchy across zoom levels
127
+ for (let z = maxZoom; z >= minZoom; z--) {
128
+ // create a new set of clusters for the zoom and index them with a KD-tree
129
+ clusters = cluster(clusters, z, trees[z + 1], opts);
130
+ trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
131
+ }
132
+
133
+ if (trees.length === 0) {
134
+ return [];
135
+ }
136
+ const numbersOfClusters = trees.map((d) => d.points.length);
137
+ const maxAvailZoom = numbersOfClusters.indexOf(
138
+ numbersOfClusters[numbersOfClusters.length - 1],
139
+ );
140
+ const minAvailZoom = Math.min(
141
+ maxAvailZoom,
142
+ numbersOfClusters.lastIndexOf(numbersOfClusters[0]),
143
+ );
144
+
145
+ const clusterLevels = new Array<ClusterLevel>();
146
+ for (let zoom = minAvailZoom; zoom <= maxAvailZoom; zoom++) {
147
+ let childrenByParent: Map<number, string[]> | undefined;
148
+ const tree = trees[zoom];
149
+ if (zoom < maxAvailZoom) {
150
+ childrenByParent = rollup<Point, string[], number>(
151
+ trees[zoom + 1].points,
152
+ (points: any[]) =>
153
+ points.map((p: any) =>
154
+ p.id ? makeClusterId(p.id) : getLocationId(locations[p.index]),
155
+ ),
156
+ (point: any) => point.parentId,
157
+ );
158
+ }
159
+
160
+ const nodes: ClusterNode[] = [];
161
+ for (const point of tree.points) {
162
+ const {x, y, numPoints} = point;
163
+ if (isLeafPoint(point)) {
164
+ const location = locations[point.index];
165
+ nodes.push({
166
+ id: getLocationId(location),
167
+ zoom,
168
+ centroid: getLocationCentroid(location),
169
+ });
170
+ } else if (isClusterPoint(point)) {
171
+ const {id} = point;
172
+ const children = childrenByParent && childrenByParent.get(id);
173
+ if (!children) {
174
+ throw new Error(`Cluster ${id} doesn't have children`);
175
+ }
176
+ nodes.push({
177
+ id: makeClusterId(id),
178
+ name: makeClusterName(id, numPoints),
179
+ zoom,
180
+ centroid: [xLng(x), yLat(y)] as [number, number],
181
+ children,
182
+ } as Cluster);
183
+ }
184
+ }
185
+ clusterLevels.push({
186
+ zoom,
187
+ nodes,
188
+ });
189
+ }
190
+ return clusterLevels;
191
+ }
192
+
193
+ function createCluster(
194
+ x: number,
195
+ y: number,
196
+ id: number,
197
+ numPoints: number,
198
+ weight: number,
199
+ ): ClusterPoint {
200
+ return {
201
+ x, // weighted cluster center
202
+ y,
203
+ zoom: Infinity, // the last zoom the cluster was processed at
204
+ id, // encodes index of the first child of the cluster and its zoom level
205
+ parentId: -1, // parent cluster id
206
+ numPoints,
207
+ weight,
208
+ };
209
+ }
210
+
211
+ function cluster(
212
+ points: Point[],
213
+ zoom: number,
214
+ tree: ZoomLevelKDBush,
215
+ options: Options,
216
+ ) {
217
+ const clusters: Point[] = [];
218
+ const {radius, extent} = options;
219
+ const r = radius / (extent * Math.pow(2, zoom));
220
+
221
+ // loop through each point
222
+ for (let i = 0; i < points.length; i++) {
223
+ const p = points[i];
224
+ // if we've already visited the point at this zoom level, skip it
225
+ if (p.zoom <= zoom) {
226
+ continue;
227
+ }
228
+ p.zoom = zoom;
229
+
230
+ // find all nearby points
231
+ const neighborIds = tree.within(p.x, p.y, r);
232
+
233
+ let weight = p.weight || 1;
234
+ let numPoints = isClusterPoint(p) ? p.numPoints : 1;
235
+ let wx = p.x * weight;
236
+ let wy = p.y * weight;
237
+
238
+ // encode both zoom and point index on which the cluster originated
239
+ const id = (i << 5) + (zoom + 1);
240
+
241
+ for (const neighborId of neighborIds) {
242
+ const b = tree.points[neighborId];
243
+ // filter out neighbors that are already processed
244
+ if (b.zoom <= zoom) {
245
+ continue;
246
+ }
247
+ b.zoom = zoom; // save the zoom (so it doesn't get processed twice)
248
+
249
+ const weight2 = b.weight || 1;
250
+ const numPoints2 = b.numPoints || 1;
251
+ wx += b.x * weight2; // accumulate coordinates for calculating weighted center
252
+ wy += b.y * weight2;
253
+
254
+ weight += weight2;
255
+ numPoints += numPoints2;
256
+ b.parentId = id;
257
+ }
258
+
259
+ if (numPoints === 1) {
260
+ clusters.push(p);
261
+ } else {
262
+ p.parentId = id;
263
+ clusters.push(
264
+ createCluster(wx / weight, wy / weight, id, numPoints, weight),
265
+ );
266
+ }
267
+ }
268
+
269
+ return clusters;
270
+ }
271
+
272
+ // spherical mercator to longitude/latitude
273
+ function xLng(x: number) {
274
+ return (x - 0.5) * 360;
275
+ }
276
+
277
+ function yLat(y: number) {
278
+ const y2 = ((180 - y * 360) * Math.PI) / 180;
279
+ return (360 * Math.atan(Math.exp(y2))) / Math.PI - 90;
280
+ }
281
+
282
+ // longitude/latitude to spherical mercator in [0..1] range
283
+ function lngX(lng: number) {
284
+ return lng / 360 + 0.5;
285
+ }
286
+
287
+ function latY(lat: number) {
288
+ const sin = Math.sin((lat * Math.PI) / 180);
289
+ const y = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI;
290
+ return y < 0 ? 0 : y > 1 ? 1 : y;
291
+ }
292
+
293
+ function getX(p: Point) {
294
+ return p.x;
295
+ }
296
+
297
+ function getY(p: Point) {
298
+ return p.y;
299
+ }