@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,1539 @@
1
+ /*
2
+ * Copyright (c) Flowmap.gl contributors
3
+ * Copyright (c) 2018-2020 Teralytics
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import {ascending, descending, extent, min, rollup} from 'd3-array';
8
+ import {ScaleLinear, scaleSqrt} from 'd3-scale';
9
+ import KDBush from 'kdbush';
10
+ import {
11
+ ParametricSelector,
12
+ createSelector,
13
+ createSelectorCreator,
14
+ defaultMemoize,
15
+ } from 'reselect';
16
+ import {alea} from 'seedrandom';
17
+ import FlowmapAggregateAccessors from './FlowmapAggregateAccessors';
18
+ import {FlowmapState} from './FlowmapState';
19
+ import {
20
+ ClusterIndex,
21
+ LocationWeightGetter,
22
+ buildIndex,
23
+ findAppropriateZoomLevel,
24
+ makeLocationWeightGetter,
25
+ } from './cluster/ClusterIndex';
26
+ import {clusterLocations} from './cluster/cluster';
27
+ import getColors, {
28
+ ColorsRGBA,
29
+ DiffColorsRGBA,
30
+ getColorsRGBA,
31
+ getDiffColorsRGBA,
32
+ getFlowColorScale,
33
+ isDiffColors,
34
+ isDiffColorsRGBA,
35
+ } from './colors';
36
+ import {
37
+ addClusterNames,
38
+ getFlowThicknessScale,
39
+ getViewportBoundingBox,
40
+ } from './selector-functions';
41
+ import {
42
+ TimeGranularityKey,
43
+ getTimeGranularityByKey,
44
+ getTimeGranularityByOrder,
45
+ getTimeGranularityForDate,
46
+ } from './time';
47
+ import {
48
+ AggregateFlow,
49
+ Cluster,
50
+ ClusterLevels,
51
+ ClusterNode,
52
+ CountByTime,
53
+ FlowAccessors,
54
+ FlowCirclesLayerAttributes,
55
+ FlowLinesLayerAttributes,
56
+ FlowmapData,
57
+ FlowmapDataAccessors,
58
+ LayersData,
59
+ LocationFilterMode,
60
+ LocationTotals,
61
+ isLocationClusterNode,
62
+ } from './types';
63
+
64
+ const MAX_CLUSTER_ZOOM_LEVEL = 20;
65
+ type KDBushTree = any;
66
+
67
+ export type Selector<L, F, T> = ParametricSelector<
68
+ FlowmapState,
69
+ FlowmapData<L, F>,
70
+ T
71
+ >;
72
+
73
+ export default class FlowmapSelectors<
74
+ L extends Record<string, any>,
75
+ F extends Record<string, any>,
76
+ > {
77
+ accessors: FlowmapAggregateAccessors<L, F>;
78
+
79
+ constructor(accessors: FlowmapDataAccessors<L, F>) {
80
+ this.accessors = new FlowmapAggregateAccessors(accessors);
81
+ this.setAccessors(accessors);
82
+ }
83
+
84
+ setAccessors(accessors: FlowmapDataAccessors<L, F>) {
85
+ this.accessors = new FlowmapAggregateAccessors(accessors);
86
+ }
87
+
88
+ getAggregateAccessors(): FlowmapAggregateAccessors<L, F> {
89
+ return this.accessors;
90
+ }
91
+
92
+ getFlowsFromProps = (state: FlowmapState, props: FlowmapData<L, F>) =>
93
+ props.flows;
94
+ getLocationsFromProps = (state: FlowmapState, props: FlowmapData<L, F>) =>
95
+ props.locations;
96
+ getClusterLevelsFromProps = (
97
+ state: FlowmapState,
98
+ props: FlowmapData<L, F>,
99
+ ) => {
100
+ return props.clusterLevels;
101
+ };
102
+ getMaxTopFlowsDisplayNum = (state: FlowmapState, props: FlowmapData<L, F>) =>
103
+ state.settings.maxTopFlowsDisplayNum;
104
+ getFlowEndpointsInViewportMode = (
105
+ state: FlowmapState,
106
+ props: FlowmapData<L, F>,
107
+ ) => state.settings.flowEndpointsInViewportMode;
108
+ getSelectedLocations = (state: FlowmapState, props: FlowmapData<L, F>) =>
109
+ state.filter?.selectedLocations;
110
+ getLocationFilterMode = (state: FlowmapState, props: FlowmapData<L, F>) =>
111
+ state.filter?.locationFilterMode;
112
+ getClusteringEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
113
+ state.settings.clusteringEnabled;
114
+ getLocationTotalsEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
115
+ state.settings.locationTotalsEnabled;
116
+ getLocationLabelsEnabled = (state: FlowmapState, props: FlowmapData<L, F>) =>
117
+ state.settings.locationLabelsEnabled;
118
+ getZoom = (state: FlowmapState, props: FlowmapData<L, F>) =>
119
+ state.viewport.zoom;
120
+ getViewport = (state: FlowmapState, props: FlowmapData<L, F>) =>
121
+ state.viewport;
122
+ getSelectedTimeRange = (state: FlowmapState, props: FlowmapData<L, F>) =>
123
+ state.filter?.selectedTimeRange;
124
+
125
+ getColorScheme: Selector<L, F, string | string[] | undefined> = (
126
+ state: FlowmapState,
127
+ props: FlowmapData<L, F>,
128
+ ) => state.settings.colorScheme;
129
+
130
+ getDarkMode: Selector<L, F, boolean> = (
131
+ state: FlowmapState,
132
+ props: FlowmapData<L, F>,
133
+ ) => state.settings.darkMode;
134
+
135
+ getFadeEnabled: Selector<L, F, boolean> = (
136
+ state: FlowmapState,
137
+ props: FlowmapData<L, F>,
138
+ ) => state.settings.fadeEnabled;
139
+
140
+ getFadeOpacityEnabled: Selector<L, F, boolean> = (
141
+ state: FlowmapState,
142
+ props: FlowmapData<L, F>,
143
+ ) => state.settings.fadeOpacityEnabled;
144
+
145
+ getFadeAmount: Selector<L, F, number> = (
146
+ state: FlowmapState,
147
+ props: FlowmapData<L, F>,
148
+ ) => state.settings.fadeAmount;
149
+
150
+ getAnimate: Selector<L, F, boolean> = (
151
+ state: FlowmapState,
152
+ props: FlowmapData<L, F>,
153
+ ) => state.settings.animationEnabled;
154
+
155
+ getInvalidLocationIds: Selector<L, F, (string | number)[] | undefined> =
156
+ createSelector(this.getLocationsFromProps, (locations) => {
157
+ if (!locations) return undefined;
158
+ const invalid = [];
159
+ for (const location of locations) {
160
+ const id = this.accessors.getLocationId(location);
161
+ const lon = this.accessors.getLocationLon(location);
162
+ const lat = this.accessors.getLocationLat(location);
163
+ if (!(-90 <= lat && lat <= 90) || !(-180 <= lon && lon <= 180)) {
164
+ invalid.push(id);
165
+ }
166
+ }
167
+ return invalid.length > 0 ? invalid : undefined;
168
+ });
169
+
170
+ getLocations: Selector<L, F, Iterable<L> | undefined> = createSelector(
171
+ this.getLocationsFromProps,
172
+ this.getInvalidLocationIds,
173
+ (locations, invalidIds) => {
174
+ if (!locations) return undefined;
175
+ if (!invalidIds || invalidIds.length === 0) return locations;
176
+ const invalid = new Set(invalidIds);
177
+ const filtered: L[] = [];
178
+ for (const location of locations) {
179
+ const id = this.accessors.getLocationId(location);
180
+ if (!invalid.has(id)) {
181
+ filtered.push(location);
182
+ }
183
+ }
184
+ return filtered;
185
+ },
186
+ );
187
+
188
+ getLocationIds: Selector<L, F, Set<string | number> | undefined> =
189
+ createSelector(this.getLocations, (locations) => {
190
+ if (!locations) return undefined;
191
+ const ids = new Set<string | number>();
192
+ for (const id of locations) {
193
+ ids.add(this.accessors.getLocationId(id));
194
+ }
195
+ return ids;
196
+ });
197
+
198
+ getSelectedLocationsSet: Selector<L, F, Set<string | number> | undefined> =
199
+ createSelector(this.getSelectedLocations, (ids) =>
200
+ ids && ids.length > 0 ? new Set(ids) : undefined,
201
+ );
202
+
203
+ getSortedFlowsForKnownLocations: Selector<L, F, F[] | undefined> =
204
+ createSelector(
205
+ this.getFlowsFromProps,
206
+ this.getLocationIds,
207
+ (flows, ids) => {
208
+ if (!ids || !flows) return undefined;
209
+ const filtered = [];
210
+ for (const flow of flows) {
211
+ const srcId = this.accessors.getFlowOriginId(flow);
212
+ const dstId = this.accessors.getFlowDestId(flow);
213
+ if (ids.has(srcId) && ids.has(dstId)) {
214
+ filtered.push(flow);
215
+ }
216
+ }
217
+ return filtered.sort((a: F, b: F) =>
218
+ descending(
219
+ Math.abs(this.accessors.getFlowMagnitude(a)),
220
+ Math.abs(this.accessors.getFlowMagnitude(b)),
221
+ ),
222
+ );
223
+ },
224
+ );
225
+
226
+ getActualTimeExtent: Selector<L, F, [Date, Date] | undefined> =
227
+ createSelector(this.getSortedFlowsForKnownLocations, (flows) => {
228
+ if (!flows) return undefined;
229
+ let start = null;
230
+ let end = null;
231
+ for (const flow of flows) {
232
+ const time = this.accessors.getFlowTime(flow);
233
+ if (time) {
234
+ if (start == null || start > time) start = time;
235
+ if (end == null || end < time) end = time;
236
+ }
237
+ }
238
+ if (!start || !end) return undefined;
239
+ return [start, end];
240
+ });
241
+
242
+ getTimeGranularityKey: Selector<L, F, TimeGranularityKey | undefined> =
243
+ createSelector(
244
+ this.getSortedFlowsForKnownLocations,
245
+ this.getActualTimeExtent,
246
+ (flows, timeExtent) => {
247
+ if (!flows || !timeExtent) return undefined;
248
+
249
+ const minOrder = min(flows, (d) => {
250
+ const t = this.accessors.getFlowTime(d);
251
+ return t ? getTimeGranularityForDate(t).order : null;
252
+ });
253
+ if (minOrder == null) return undefined;
254
+ const timeGranularity = getTimeGranularityByOrder(minOrder);
255
+ return timeGranularity ? timeGranularity.key : undefined;
256
+ },
257
+ );
258
+
259
+ getTimeExtent: Selector<L, F, [Date, Date] | undefined> = createSelector(
260
+ this.getActualTimeExtent,
261
+ this.getTimeGranularityKey,
262
+ (timeExtent, timeGranularityKey) => {
263
+ const timeGranularity = timeGranularityKey
264
+ ? getTimeGranularityByKey(timeGranularityKey)
265
+ : undefined;
266
+ if (!timeExtent || !timeGranularity?.interval) return undefined;
267
+ const {interval} = timeGranularity;
268
+ return [timeExtent[0], interval.offset(interval.floor(timeExtent[1]), 1)];
269
+ },
270
+ );
271
+
272
+ getSortedFlowsForKnownLocationsFilteredByTime: Selector<
273
+ L,
274
+ F,
275
+ F[] | undefined
276
+ > = createSelector(
277
+ this.getSortedFlowsForKnownLocations,
278
+ this.getTimeExtent,
279
+ this.getSelectedTimeRange,
280
+ (flows, timeExtent, timeRange) => {
281
+ if (!flows) return undefined;
282
+ if (
283
+ !timeExtent ||
284
+ !timeRange ||
285
+ (timeExtent[0] === timeRange[0] && timeExtent[1] === timeRange[1])
286
+ ) {
287
+ return flows;
288
+ }
289
+ return flows.filter((flow) => {
290
+ const time = this.accessors.getFlowTime(flow);
291
+ return time && timeRange[0] <= time && time < timeRange[1];
292
+ });
293
+ },
294
+ );
295
+
296
+ getLocationsHavingFlows: Selector<L, F, Iterable<L> | undefined> =
297
+ createSelector(
298
+ this.getSortedFlowsForKnownLocations,
299
+ this.getLocations,
300
+ (flows, locations) => {
301
+ if (!locations || !flows) return locations;
302
+ const withFlows = new Set();
303
+ for (const flow of flows) {
304
+ withFlows.add(this.accessors.getFlowOriginId(flow));
305
+ withFlows.add(this.accessors.getFlowDestId(flow));
306
+ }
307
+ const filtered = [];
308
+ for (const location of locations) {
309
+ if (withFlows.has(this.accessors.getLocationId(location))) {
310
+ filtered.push(location);
311
+ }
312
+ }
313
+ return filtered;
314
+ },
315
+ );
316
+
317
+ getLocationsById: Selector<L, F, Map<string | number, L> | undefined> =
318
+ createSelector(this.getLocationsHavingFlows, (locations) => {
319
+ if (!locations) return undefined;
320
+ const locationsById = new Map<string | number, L>();
321
+ for (const location of locations) {
322
+ locationsById.set(this.accessors.getLocationId(location), location);
323
+ }
324
+ return locationsById;
325
+ });
326
+
327
+ getLocationWeightGetter: Selector<L, F, LocationWeightGetter | undefined> =
328
+ createSelector(this.getSortedFlowsForKnownLocations, (flows) => {
329
+ if (!flows) return undefined;
330
+ const getLocationWeight = makeLocationWeightGetter(
331
+ flows,
332
+ this.accessors.getFlowmapDataAccessors(),
333
+ );
334
+ return getLocationWeight;
335
+ });
336
+
337
+ getClusterLevels: Selector<L, F, ClusterLevels | undefined> = createSelector(
338
+ this.getClusterLevelsFromProps,
339
+ this.getLocationsHavingFlows,
340
+ this.getLocationWeightGetter,
341
+ (clusterLevelsFromProps, locations, getLocationWeight) => {
342
+ if (clusterLevelsFromProps) return clusterLevelsFromProps;
343
+ if (!locations || !getLocationWeight) return undefined;
344
+ const clusterLevels = clusterLocations(
345
+ locations,
346
+ this.accessors.getFlowmapDataAccessors(),
347
+ getLocationWeight,
348
+ {
349
+ maxZoom: MAX_CLUSTER_ZOOM_LEVEL,
350
+ },
351
+ );
352
+ return clusterLevels;
353
+ },
354
+ );
355
+
356
+ getClusterIndex: Selector<L, F, ClusterIndex<F> | undefined> = createSelector(
357
+ this.getLocationsById,
358
+ this.getLocationWeightGetter,
359
+ this.getClusterLevels,
360
+ (locationsById, getLocationWeight, clusterLevels) => {
361
+ if (!locationsById || !getLocationWeight || !clusterLevels)
362
+ return undefined;
363
+
364
+ const clusterIndex = buildIndex<F>(clusterLevels);
365
+ // Adding meaningful names
366
+ addClusterNames(
367
+ clusterIndex,
368
+ clusterLevels,
369
+ locationsById,
370
+ this.accessors.getFlowmapDataAccessors(),
371
+ getLocationWeight,
372
+ );
373
+ return clusterIndex;
374
+ },
375
+ );
376
+
377
+ getAvailableClusterZoomLevels = createSelector(
378
+ this.getClusterIndex,
379
+ this.getSelectedLocations,
380
+ (clusterIndex, selectedLocations): number[] | undefined => {
381
+ if (!clusterIndex) {
382
+ return undefined;
383
+ }
384
+
385
+ let maxZoom = Number.POSITIVE_INFINITY;
386
+ let minZoom = Number.NEGATIVE_INFINITY;
387
+
388
+ const adjust = (zoneId: string | number) => {
389
+ const cluster = clusterIndex.getClusterById(zoneId);
390
+ if (cluster) {
391
+ minZoom = Math.max(minZoom, cluster.zoom);
392
+ maxZoom = Math.min(maxZoom, cluster.zoom);
393
+ } else {
394
+ const zoom = clusterIndex.getMinZoomForLocation(zoneId);
395
+ minZoom = Math.max(minZoom, zoom);
396
+ }
397
+ };
398
+
399
+ if (selectedLocations) {
400
+ for (const id of selectedLocations) {
401
+ adjust(id);
402
+ }
403
+ }
404
+
405
+ return clusterIndex.availableZoomLevels.filter(
406
+ (level) => minZoom <= level && level <= maxZoom,
407
+ );
408
+ },
409
+ );
410
+
411
+ _getClusterZoom: Selector<L, F, number | undefined> = createSelector(
412
+ this.getClusterIndex,
413
+ this.getZoom,
414
+ this.getAvailableClusterZoomLevels,
415
+ (clusterIndex, mapZoom, availableClusterZoomLevels) => {
416
+ if (!clusterIndex) return undefined;
417
+ if (!availableClusterZoomLevels || mapZoom == null) {
418
+ return undefined;
419
+ }
420
+
421
+ const clusterZoom = findAppropriateZoomLevel(
422
+ availableClusterZoomLevels,
423
+ mapZoom,
424
+ );
425
+ return clusterZoom;
426
+ },
427
+ );
428
+
429
+ getClusterZoom = (state: FlowmapState, props: FlowmapData<L, F>) => {
430
+ const {settings} = state;
431
+ if (!settings.clusteringEnabled) return undefined;
432
+ if (settings.clusteringAuto || settings.clusteringLevel == null) {
433
+ return this._getClusterZoom(state, props);
434
+ }
435
+ return settings.clusteringLevel;
436
+ };
437
+
438
+ getLocationsForSearchBox: Selector<L, F, (L | Cluster)[] | undefined> =
439
+ createSelector(
440
+ this.getClusteringEnabled,
441
+ this.getLocationsHavingFlows,
442
+ this.getSelectedLocations,
443
+ this.getClusterZoom,
444
+ this.getClusterIndex,
445
+ (
446
+ clusteringEnabled,
447
+ locations,
448
+ selectedLocations,
449
+ clusterZoom,
450
+ clusterIndex,
451
+ ) => {
452
+ if (!locations) return undefined;
453
+ let result: (L | Cluster)[] = Array.from(locations);
454
+ // if (clusteringEnabled) {
455
+ // if (clusterIndex) {
456
+ // const zoomItems = clusterIndex.getClusterNodesFor(clusterZoom);
457
+ // if (zoomItems) {
458
+ // result = result.concat(zoomItems.filter(isCluster));
459
+ // }
460
+ // }
461
+ // }
462
+
463
+ if (clusterIndex && selectedLocations) {
464
+ const toAppend = [];
465
+ for (const id of selectedLocations) {
466
+ const cluster = clusterIndex.getClusterById(id);
467
+ if (
468
+ cluster &&
469
+ !result.find(
470
+ (d) =>
471
+ (isLocationClusterNode(d)
472
+ ? d.id
473
+ : this.accessors.getLocationId(d)) === id,
474
+ )
475
+ ) {
476
+ toAppend.push(cluster);
477
+ }
478
+ }
479
+ if (toAppend.length > 0) {
480
+ result = result.concat(toAppend);
481
+ }
482
+ }
483
+ return result;
484
+ },
485
+ );
486
+
487
+ getDiffMode: Selector<L, F, boolean> = createSelector(
488
+ this.getFlowsFromProps,
489
+ (flows) => {
490
+ if (flows) {
491
+ for (const f of flows) {
492
+ if (this.accessors.getFlowMagnitude(f) < 0) {
493
+ return true;
494
+ }
495
+ }
496
+ }
497
+ return false;
498
+ },
499
+ );
500
+
501
+ _getFlowmapColors = createSelector(
502
+ this.getDiffMode,
503
+ this.getColorScheme,
504
+ this.getDarkMode,
505
+ this.getFadeEnabled,
506
+ this.getFadeOpacityEnabled,
507
+ this.getFadeAmount,
508
+ this.getAnimate,
509
+ getColors,
510
+ );
511
+
512
+ getFlowmapColorsRGBA = createSelector(
513
+ this._getFlowmapColors,
514
+ (flowmapColors) => {
515
+ return isDiffColors(flowmapColors)
516
+ ? getDiffColorsRGBA(flowmapColors)
517
+ : getColorsRGBA(flowmapColors);
518
+ },
519
+ );
520
+
521
+ getUnknownLocations: Selector<L, F, Set<string | number> | undefined> =
522
+ createSelector(
523
+ this.getLocationIds,
524
+ this.getFlowsFromProps,
525
+ this.getSortedFlowsForKnownLocations,
526
+ (ids, flows, flowsForKnownLocations) => {
527
+ if (!ids || !flows) return undefined;
528
+ if (
529
+ flowsForKnownLocations
530
+ // && flows.length === flowsForKnownLocations.length
531
+ )
532
+ return undefined;
533
+ const missing = new Set<string | number>();
534
+ for (const flow of flows) {
535
+ if (!ids.has(this.accessors.getFlowOriginId(flow)))
536
+ missing.add(this.accessors.getFlowOriginId(flow));
537
+ if (!ids.has(this.accessors.getFlowDestId(flow)))
538
+ missing.add(this.accessors.getFlowDestId(flow));
539
+ }
540
+ return missing;
541
+ },
542
+ );
543
+
544
+ getSortedAggregatedFilteredFlows: Selector<
545
+ L,
546
+ F,
547
+ (F | AggregateFlow)[] | undefined
548
+ > = createSelector(
549
+ this.getClusterIndex,
550
+ this.getClusteringEnabled,
551
+ this.getSortedFlowsForKnownLocationsFilteredByTime,
552
+ this.getClusterZoom,
553
+ this.getTimeExtent,
554
+ (clusterTree, isClusteringEnabled, flows, clusterZoom, timeExtent) => {
555
+ if (!flows) return undefined;
556
+ let aggregated: (F | AggregateFlow)[];
557
+ if (isClusteringEnabled && clusterTree && clusterZoom != null) {
558
+ aggregated = clusterTree.aggregateFlows(
559
+ // TODO: aggregate across time
560
+ // timeExtent != null
561
+ // ? aggregateFlows(flows) // clusterTree.aggregateFlows won't aggregate unclustered across time
562
+ // : flows,
563
+ flows,
564
+ clusterZoom,
565
+ this.accessors.getFlowmapDataAccessors(),
566
+ );
567
+ } else {
568
+ aggregated = aggregateFlows(
569
+ flows,
570
+ this.accessors.getFlowmapDataAccessors(),
571
+ );
572
+ }
573
+ aggregated.sort((a, b) =>
574
+ descending(
575
+ Math.abs(this.accessors.getFlowMagnitude(a)),
576
+ Math.abs(this.accessors.getFlowMagnitude(b)),
577
+ ),
578
+ );
579
+ return aggregated;
580
+ },
581
+ );
582
+
583
+ getExpandedSelectedLocationsSet: Selector<
584
+ L,
585
+ F,
586
+ Set<string | number> | undefined
587
+ > = createSelector(
588
+ this.getClusteringEnabled,
589
+ this.getSelectedLocationsSet,
590
+ this.getClusterIndex,
591
+ (clusteringEnabled, selectedLocations, clusterIndex) => {
592
+ if (!selectedLocations || !clusterIndex) {
593
+ return selectedLocations;
594
+ }
595
+
596
+ const result = new Set<string | number>();
597
+ for (const locationId of selectedLocations) {
598
+ const cluster = clusterIndex.getClusterById(locationId);
599
+ if (cluster) {
600
+ const expanded = clusterIndex.expandCluster(cluster);
601
+ for (const id of expanded) {
602
+ result.add(id);
603
+ }
604
+ } else {
605
+ result.add(locationId);
606
+ }
607
+ }
608
+ return result;
609
+ },
610
+ );
611
+
612
+ getTotalCountsByTime: Selector<L, F, CountByTime[] | undefined> =
613
+ createSelector(
614
+ this.getSortedFlowsForKnownLocations,
615
+ this.getTimeGranularityKey,
616
+ this.getTimeExtent,
617
+ this.getExpandedSelectedLocationsSet,
618
+ this.getLocationFilterMode,
619
+ (
620
+ flows,
621
+ timeGranularityKey,
622
+ timeExtent,
623
+ selectedLocationSet,
624
+ locationFilterMode,
625
+ ) => {
626
+ const timeGranularity = timeGranularityKey
627
+ ? getTimeGranularityByKey(timeGranularityKey)
628
+ : undefined;
629
+ if (!flows || !timeGranularity || !timeExtent) return undefined;
630
+ const byTime = flows.reduce((m, flow) => {
631
+ if (
632
+ this.isFlowInSelection(
633
+ flow,
634
+ selectedLocationSet,
635
+ locationFilterMode,
636
+ )
637
+ ) {
638
+ const key = timeGranularity
639
+ .interval(this.accessors.getFlowTime(flow))
640
+ .getTime();
641
+ m.set(
642
+ key,
643
+ (m.get(key) ?? 0) + this.accessors.getFlowMagnitude(flow),
644
+ );
645
+ }
646
+ return m;
647
+ }, new Map<number, number>());
648
+
649
+ return Array.from(byTime.entries()).map(([millis, count]) => ({
650
+ time: new Date(millis),
651
+ count,
652
+ }));
653
+ },
654
+ );
655
+
656
+ getMaxLocationCircleSize: Selector<L, F, number> = createSelector(
657
+ this.getLocationTotalsEnabled,
658
+ (locationTotalsEnabled) => (locationTotalsEnabled ? 17 : 1),
659
+ );
660
+
661
+ getViewportBoundingBox: Selector<L, F, [number, number, number, number]> =
662
+ createSelector(
663
+ this.getViewport,
664
+ this.getMaxLocationCircleSize,
665
+ getViewportBoundingBox,
666
+ );
667
+
668
+ getLocationsForZoom: Selector<L, F, Iterable<L> | ClusterNode[] | undefined> =
669
+ createSelector(
670
+ this.getClusteringEnabled,
671
+ this.getLocationsHavingFlows,
672
+ this.getClusterIndex,
673
+ this.getClusterZoom,
674
+ (clusteringEnabled, locationsHavingFlows, clusterIndex, clusterZoom) => {
675
+ if (clusteringEnabled && clusterIndex) {
676
+ return clusterIndex.getClusterNodesFor(clusterZoom);
677
+ } else {
678
+ return locationsHavingFlows;
679
+ }
680
+ },
681
+ );
682
+
683
+ getLocationTotals: Selector<
684
+ L,
685
+ F,
686
+ Map<string | number, LocationTotals> | undefined
687
+ > = createSelector(
688
+ this.getLocationsForZoom,
689
+ this.getSortedAggregatedFilteredFlows,
690
+ this.getSelectedLocationsSet,
691
+ this.getLocationFilterMode,
692
+ (locations, flows, selectedLocationsSet, locationFilterMode) => {
693
+ if (!flows) return undefined;
694
+ const totals = new Map<string | number, LocationTotals>();
695
+ const add = (
696
+ id: string | number,
697
+ d: Partial<LocationTotals>,
698
+ ): LocationTotals => {
699
+ const rv = totals.get(id) ?? {
700
+ incomingCount: 0,
701
+ outgoingCount: 0,
702
+ internalCount: 0,
703
+ };
704
+ if (d.incomingCount != null) rv.incomingCount += d.incomingCount;
705
+ if (d.outgoingCount != null) rv.outgoingCount += d.outgoingCount;
706
+ if (d.internalCount != null) rv.internalCount += d.internalCount;
707
+ return rv;
708
+ };
709
+ for (const f of flows) {
710
+ if (
711
+ this.isFlowInSelection(f, selectedLocationsSet, locationFilterMode)
712
+ ) {
713
+ const originId = this.accessors.getFlowOriginId(f);
714
+ const destId = this.accessors.getFlowDestId(f);
715
+ const count = this.accessors.getFlowMagnitude(f);
716
+ if (originId === destId) {
717
+ totals.set(originId, add(originId, {internalCount: count}));
718
+ } else {
719
+ totals.set(originId, add(originId, {outgoingCount: count}));
720
+ totals.set(destId, add(destId, {incomingCount: count}));
721
+ }
722
+ }
723
+ }
724
+ return totals;
725
+ },
726
+ );
727
+
728
+ getLocationsTree: Selector<L, F, KDBushTree> = createSelector(
729
+ this.getLocationsForZoom,
730
+ (locations) => {
731
+ if (!locations) {
732
+ return undefined;
733
+ }
734
+ const nodes = Array.isArray(locations)
735
+ ? locations
736
+ : Array.from(locations);
737
+ const bush = new KDBush(nodes.length, 64, Float32Array);
738
+ for (let i = 0; i < nodes.length; i++) {
739
+ const node = nodes[i];
740
+ bush.add(
741
+ lngX(this.accessors.getLocationLon(node)),
742
+ latY(this.accessors.getLocationLat(node)),
743
+ );
744
+ }
745
+ bush.finish();
746
+ bush.points = nodes;
747
+ return bush;
748
+ },
749
+ );
750
+
751
+ _getLocationIdsInViewport: Selector<L, F, Set<string> | undefined> =
752
+ createSelector(
753
+ this.getLocationsTree,
754
+ this.getViewportBoundingBox,
755
+ (tree: KDBushTree, bbox: [number, number, number, number]) => {
756
+ const ids = this._getLocationsInBboxIndices(tree, bbox);
757
+ if (ids) {
758
+ return new Set(
759
+ ids.map((idx: number) =>
760
+ this.accessors.getLocationId(tree.points[idx]),
761
+ ) as Array<string>,
762
+ );
763
+ }
764
+ return undefined;
765
+ },
766
+ );
767
+
768
+ getLocationIdsInViewport: Selector<L, F, Set<string | number> | undefined> =
769
+ createSelectorCreator(
770
+ defaultMemoize,
771
+ // @ts-ignore
772
+ (
773
+ s1: Set<string> | undefined,
774
+ s2: Set<string> | undefined,
775
+ index: number,
776
+ ) => {
777
+ if (s1 === s2) return true;
778
+ if (s1 == null || s2 == null) return false;
779
+ if (s1.size !== s2.size) return false;
780
+ for (const item of s1) if (!s2.has(item)) return false;
781
+ return true;
782
+ },
783
+ )(
784
+ this._getLocationIdsInViewport,
785
+ (locationIds: Set<string> | undefined) => {
786
+ if (!locationIds) return undefined;
787
+ return locationIds;
788
+ },
789
+ );
790
+
791
+ getTotalUnfilteredCount: Selector<L, F, number | undefined> = createSelector(
792
+ this.getSortedFlowsForKnownLocations,
793
+ (flows) => {
794
+ if (!flows) return undefined;
795
+ return flows.reduce(
796
+ (m, flow) => m + this.accessors.getFlowMagnitude(flow),
797
+ 0,
798
+ );
799
+ },
800
+ );
801
+
802
+ getTotalFilteredCount: Selector<L, F, number | undefined> = createSelector(
803
+ this.getSortedAggregatedFilteredFlows,
804
+ this.getSelectedLocationsSet,
805
+ this.getLocationFilterMode,
806
+ (flows, selectedLocationSet, locationFilterMode) => {
807
+ if (!flows) return undefined;
808
+ const count = flows.reduce((m, flow) => {
809
+ if (
810
+ this.isFlowInSelection(flow, selectedLocationSet, locationFilterMode)
811
+ ) {
812
+ return m + this.accessors.getFlowMagnitude(flow);
813
+ }
814
+ return m;
815
+ }, 0);
816
+ return count;
817
+ },
818
+ );
819
+
820
+ _getLocationTotalsExtent: Selector<L, F, [number, number] | undefined> =
821
+ createSelector(this.getLocationTotals, (locationTotals) =>
822
+ calcLocationTotalsExtent(locationTotals, undefined),
823
+ );
824
+
825
+ _getLocationTotalsForViewportExtent: Selector<
826
+ L,
827
+ F,
828
+ [number, number] | undefined
829
+ > = createSelector(
830
+ this.getLocationTotals,
831
+ this.getLocationIdsInViewport,
832
+ (locationTotals, locationsInViewport) =>
833
+ calcLocationTotalsExtent(locationTotals, locationsInViewport),
834
+ );
835
+
836
+ getLocationTotalsExtent = (
837
+ state: FlowmapState,
838
+ props: FlowmapData<L, F>,
839
+ ): [number, number] | undefined => {
840
+ if (state.settings.adaptiveScalesEnabled) {
841
+ return this._getLocationTotalsForViewportExtent(state, props);
842
+ } else {
843
+ return this._getLocationTotalsExtent(state, props);
844
+ }
845
+ };
846
+
847
+ getFlowsForFlowmapLayer: Selector<L, F, (F | AggregateFlow)[] | undefined> =
848
+ createSelector(
849
+ this.getSortedAggregatedFilteredFlows,
850
+ this.getLocationIdsInViewport,
851
+ this.getSelectedLocationsSet,
852
+ this.getLocationFilterMode,
853
+ this.getMaxTopFlowsDisplayNum,
854
+ this.getFlowEndpointsInViewportMode,
855
+ (
856
+ flows,
857
+ locationIdsInViewport,
858
+ selectedLocationsSet,
859
+ locationFilterMode,
860
+ maxTopFlowsDisplayNum,
861
+ flowEndpointsInViewportMode,
862
+ ) => {
863
+ if (!flows || !locationIdsInViewport) return undefined;
864
+ const picked: (F | AggregateFlow)[] = [];
865
+ let pickedCount = 0;
866
+ for (const flow of flows) {
867
+ const origin = this.accessors.getFlowOriginId(flow);
868
+ const dest = this.accessors.getFlowDestId(flow);
869
+ const originInView = locationIdsInViewport.has(origin);
870
+ const destInView = locationIdsInViewport.has(dest);
871
+ const isInViewport =
872
+ flowEndpointsInViewportMode === 'both'
873
+ ? originInView && destInView
874
+ : originInView || destInView;
875
+ if (isInViewport) {
876
+ if (
877
+ this.isFlowInSelection(
878
+ flow,
879
+ selectedLocationsSet,
880
+ locationFilterMode,
881
+ )
882
+ ) {
883
+ if (origin !== dest) {
884
+ // exclude self-loops
885
+ picked.push(flow);
886
+ pickedCount++;
887
+ }
888
+ }
889
+ }
890
+ // Only keep top
891
+ if (pickedCount > maxTopFlowsDisplayNum) break;
892
+ }
893
+ // assuming they are sorted in descending order,
894
+ // we need ascending for rendering
895
+ return picked.reverse();
896
+ },
897
+ );
898
+
899
+ _getFlowMagnitudeExtent: Selector<L, F, [number, number] | undefined> =
900
+ createSelector(
901
+ this.getSortedAggregatedFilteredFlows,
902
+ this.getSelectedLocationsSet,
903
+ this.getLocationFilterMode,
904
+ (flows, selectedLocationsSet, locationFilterMode) => {
905
+ if (!flows) return undefined;
906
+ let rv: [number, number] | undefined = undefined;
907
+ for (const f of flows) {
908
+ if (
909
+ this.accessors.getFlowOriginId(f) !==
910
+ this.accessors.getFlowDestId(f) &&
911
+ this.isFlowInSelection(f, selectedLocationsSet, locationFilterMode)
912
+ ) {
913
+ const count = this.accessors.getFlowMagnitude(f);
914
+ if (rv == null) {
915
+ rv = [count, count];
916
+ } else {
917
+ if (count < rv[0]) rv[0] = count;
918
+ if (count > rv[1]) rv[1] = count;
919
+ }
920
+ }
921
+ }
922
+ return rv;
923
+ },
924
+ );
925
+
926
+ _getAdaptiveFlowMagnitudeExtent: Selector<
927
+ L,
928
+ F,
929
+ [number, number] | undefined
930
+ > = createSelector(this.getFlowsForFlowmapLayer, (flows) => {
931
+ if (!flows) return undefined;
932
+ const rv = extent(flows, this.accessors.getFlowMagnitude);
933
+ return rv[0] !== undefined && rv[1] !== undefined ? rv : undefined;
934
+ });
935
+
936
+ getFlowMagnitudeExtent = (
937
+ state: FlowmapState,
938
+ props: FlowmapData<L, F>,
939
+ ): [number, number] | undefined => {
940
+ if (state.settings.adaptiveScalesEnabled) {
941
+ return this._getAdaptiveFlowMagnitudeExtent(state, props);
942
+ } else {
943
+ return this._getFlowMagnitudeExtent(state, props);
944
+ }
945
+ };
946
+
947
+ getLocationMaxAbsTotalGetter = createSelector(
948
+ this.getLocationTotals,
949
+ (locationTotals) => {
950
+ return (locationId: string) => {
951
+ const total = locationTotals?.get(locationId);
952
+ if (!total) return undefined;
953
+ return Math.max(
954
+ Math.abs(total.incomingCount + total.internalCount),
955
+ Math.abs(total.outgoingCount + total.internalCount),
956
+ );
957
+ };
958
+ },
959
+ );
960
+
961
+ getFlowThicknessScale = createSelector(
962
+ this.getFlowMagnitudeExtent,
963
+ getFlowThicknessScale,
964
+ );
965
+
966
+ getCircleSizeScale = createSelector(
967
+ this.getMaxLocationCircleSize,
968
+ this.getLocationTotalsEnabled,
969
+ this.getLocationTotalsExtent,
970
+ (maxLocationCircleSize, locationTotalsEnabled, locationTotalsExtent) => {
971
+ if (!locationTotalsEnabled) {
972
+ return () => maxLocationCircleSize;
973
+ }
974
+ if (!locationTotalsExtent) return undefined;
975
+ return scaleSqrt()
976
+ .range([0, maxLocationCircleSize])
977
+ .domain([
978
+ 0,
979
+ // should support diff mode too
980
+ Math.max.apply(
981
+ null,
982
+ locationTotalsExtent.map((x: number | undefined) =>
983
+ Math.abs(x || 0),
984
+ ),
985
+ ),
986
+ ]);
987
+ },
988
+ );
989
+
990
+ getInCircleSizeGetter = createSelector(
991
+ this.getCircleSizeScale,
992
+ this.getLocationTotals,
993
+ (circleSizeScale, locationTotals) => {
994
+ return (locationId: string | number) => {
995
+ const total = locationTotals?.get(locationId);
996
+ if (total && circleSizeScale) {
997
+ return (
998
+ circleSizeScale(
999
+ Math.abs(total.incomingCount + total.internalCount),
1000
+ ) || 0
1001
+ );
1002
+ }
1003
+ return 0;
1004
+ };
1005
+ },
1006
+ );
1007
+
1008
+ getOutCircleSizeGetter = createSelector(
1009
+ this.getCircleSizeScale,
1010
+ this.getLocationTotals,
1011
+ (circleSizeScale, locationTotals) => {
1012
+ return (locationId: string | number) => {
1013
+ const total = locationTotals?.get(locationId);
1014
+ if (total && circleSizeScale) {
1015
+ return (
1016
+ circleSizeScale(
1017
+ Math.abs(total.outgoingCount + total.internalCount),
1018
+ ) || 0
1019
+ );
1020
+ }
1021
+ return 0;
1022
+ };
1023
+ },
1024
+ );
1025
+
1026
+ getSortedLocationsForZoom: Selector<L, F, L[] | ClusterNode[] | undefined> =
1027
+ createSelector(
1028
+ this.getLocationsForZoom,
1029
+ this.getInCircleSizeGetter,
1030
+ this.getOutCircleSizeGetter,
1031
+ (locations, getInCircleSize, getOutCircleSize) => {
1032
+ if (!locations) return undefined;
1033
+ const nextLocations = [...locations] as L[] | ClusterNode[];
1034
+ return nextLocations.sort((a, b) => {
1035
+ const idA = this.accessors.getLocationId(a);
1036
+ const idB = this.accessors.getLocationId(b);
1037
+ return ascending(
1038
+ Math.max(getInCircleSize(idA), getOutCircleSize(idA)),
1039
+ Math.max(getInCircleSize(idB), getOutCircleSize(idB)),
1040
+ );
1041
+ });
1042
+ },
1043
+ );
1044
+
1045
+ getLocationsForFlowmapLayer: Selector<
1046
+ L,
1047
+ F,
1048
+ Array<L | ClusterNode> | undefined
1049
+ > = createSelector(
1050
+ this.getSortedLocationsForZoom,
1051
+ // this.getLocationIdsInViewport,
1052
+ (
1053
+ locations,
1054
+ // locationIdsInViewport
1055
+ ) => {
1056
+ // if (!locations) return undefined;
1057
+ // if (!locationIdsInViewport) return locations;
1058
+ // if (locationIdsInViewport.size === locations.length) return locations;
1059
+ // const filtered = [];
1060
+ // for (const loc of locations) {
1061
+ // if (locationIdsInViewport.has(loc.id)) {
1062
+ // filtered.push(loc);
1063
+ // }
1064
+ // }
1065
+ // return filtered;
1066
+ // @ts-ignore
1067
+ // return locations.filter(
1068
+ // (loc: L | ClusterNode) => locationIdsInViewport!.has(loc.id)
1069
+ // );
1070
+ // TODO: return location in viewport + "connected" ones
1071
+ return locations;
1072
+ },
1073
+ );
1074
+
1075
+ getLocationsForFlowmapLayerById: Selector<
1076
+ L,
1077
+ F,
1078
+ Map<string, L | ClusterNode> | undefined
1079
+ > = createSelector(this.getLocationsForFlowmapLayer, (locations) => {
1080
+ if (!locations) return undefined;
1081
+ return locations.reduce(
1082
+ (m, d) => (m.set(this.accessors.getLocationId(d), d), m),
1083
+ new Map(),
1084
+ );
1085
+ });
1086
+
1087
+ getLocationOrClusterByIdGetter = createSelector(
1088
+ this.getClusterIndex,
1089
+ this.getLocationsById,
1090
+ (clusterIndex, locationsById) => {
1091
+ return (id: string | number) =>
1092
+ clusterIndex?.getClusterById(id) ?? locationsById?.get(id);
1093
+ },
1094
+ );
1095
+
1096
+ getLayersData: Selector<L, F, LayersData> = createSelector(
1097
+ this.getLocationsForFlowmapLayer,
1098
+ this.getFlowsForFlowmapLayer,
1099
+ this.getFlowmapColorsRGBA,
1100
+ this.getLocationsForFlowmapLayerById,
1101
+ this.getLocationIdsInViewport,
1102
+ this.getInCircleSizeGetter,
1103
+ this.getOutCircleSizeGetter,
1104
+ this.getFlowThicknessScale,
1105
+ this.getAnimate,
1106
+ this.getLocationLabelsEnabled,
1107
+ (
1108
+ locations,
1109
+ flows,
1110
+ flowmapColors,
1111
+ locationsById,
1112
+ locationIdsInViewport,
1113
+ getInCircleSize,
1114
+ getOutCircleSize,
1115
+ flowThicknessScale,
1116
+ animationEnabled,
1117
+ locationLabelsEnabled,
1118
+ ) => {
1119
+ return this._prepareLayersData(
1120
+ locations,
1121
+ flows,
1122
+ flowmapColors,
1123
+ locationsById,
1124
+ locationIdsInViewport,
1125
+ getInCircleSize,
1126
+ getOutCircleSize,
1127
+ flowThicknessScale,
1128
+ animationEnabled,
1129
+ locationLabelsEnabled,
1130
+ );
1131
+ },
1132
+ );
1133
+
1134
+ prepareLayersData(state: FlowmapState, props: FlowmapData<L, F>): LayersData {
1135
+ const locations = this.getLocationsForFlowmapLayer(state, props) || [];
1136
+ const flows = this.getFlowsForFlowmapLayer(state, props) || [];
1137
+ const flowmapColors = this.getFlowmapColorsRGBA(state, props);
1138
+ const locationsById = this.getLocationsForFlowmapLayerById(state, props);
1139
+ const locationIdsInViewport = this.getLocationIdsInViewport(state, props);
1140
+ const getInCircleSize = this.getInCircleSizeGetter(state, props);
1141
+ const getOutCircleSize = this.getOutCircleSizeGetter(state, props);
1142
+ const flowThicknessScale = this.getFlowThicknessScale(state, props);
1143
+ const locationLabelsEnabled = this.getLocationLabelsEnabled(state, props);
1144
+ return this._prepareLayersData(
1145
+ locations,
1146
+ flows,
1147
+ flowmapColors,
1148
+ locationsById,
1149
+ locationIdsInViewport,
1150
+ getInCircleSize,
1151
+ getOutCircleSize,
1152
+ flowThicknessScale,
1153
+ state.settings.animationEnabled,
1154
+ locationLabelsEnabled,
1155
+ );
1156
+ }
1157
+
1158
+ _prepareLayersData(
1159
+ locations: (L | ClusterNode)[] | undefined,
1160
+ flows: (F | AggregateFlow)[] | undefined,
1161
+ flowmapColors: DiffColorsRGBA | ColorsRGBA,
1162
+ locationsById: Map<string | number, L | ClusterNode> | undefined,
1163
+ locationIdsInViewport: Set<string | number> | undefined,
1164
+ getInCircleSize: (locationId: string | number) => number,
1165
+ getOutCircleSize: (locationId: string | number) => number,
1166
+ flowThicknessScale: ScaleLinear<number, number, never> | undefined,
1167
+ animationEnabled: boolean,
1168
+ locationLabelsEnabled: boolean,
1169
+ ): LayersData {
1170
+ if (!locations) locations = [];
1171
+ if (!flows) flows = [];
1172
+ const {
1173
+ getFlowOriginId,
1174
+ getFlowDestId,
1175
+ getFlowMagnitude,
1176
+ getLocationId,
1177
+ getLocationLon,
1178
+ getLocationLat,
1179
+ getLocationName,
1180
+ } = this.accessors;
1181
+
1182
+ const flowMagnitudeExtent = extent(flows, (f) => getFlowMagnitude(f)) as [
1183
+ number,
1184
+ number,
1185
+ ];
1186
+ const flowColorScale = getFlowColorScale(
1187
+ flowmapColors,
1188
+ flowMagnitudeExtent,
1189
+ false,
1190
+ );
1191
+
1192
+ // Using a generator here helps to avoid creating intermediary arrays
1193
+ const circlePositions = Float32Array.from(
1194
+ (function* () {
1195
+ for (const location of locations) {
1196
+ yield getLocationLon(location);
1197
+ yield getLocationLat(location);
1198
+ }
1199
+ })(),
1200
+ );
1201
+
1202
+ // TODO: diff mode
1203
+ const circleColor = isDiffColorsRGBA(flowmapColors)
1204
+ ? flowmapColors.positive.locationCircles.inner
1205
+ : flowmapColors.locationCircles.inner;
1206
+
1207
+ const circleColors = Uint8Array.from(
1208
+ (function* () {
1209
+ for (const location of locations) {
1210
+ yield* circleColor;
1211
+ }
1212
+ })(),
1213
+ );
1214
+
1215
+ const inCircleRadii = Float32Array.from(
1216
+ (function* () {
1217
+ for (const location of locations) {
1218
+ const id = getLocationId(location);
1219
+ yield locationIdsInViewport?.has(id) ? getInCircleSize(id) : 1.0;
1220
+ }
1221
+ })(),
1222
+ );
1223
+ const outCircleRadii = Float32Array.from(
1224
+ (function* () {
1225
+ for (const location of locations) {
1226
+ const id = getLocationId(location);
1227
+ yield locationIdsInViewport?.has(id) ? getOutCircleSize(id) : 1.0;
1228
+ }
1229
+ })(),
1230
+ );
1231
+
1232
+ const sourcePositions = Float32Array.from(
1233
+ (function* () {
1234
+ for (const flow of flows) {
1235
+ const loc = locationsById?.get(getFlowOriginId(flow));
1236
+ yield loc ? getLocationLon(loc) : 0;
1237
+ yield loc ? getLocationLat(loc) : 0;
1238
+ }
1239
+ })(),
1240
+ );
1241
+ const targetPositions = Float32Array.from(
1242
+ (function* () {
1243
+ for (const flow of flows) {
1244
+ const loc = locationsById?.get(getFlowDestId(flow));
1245
+ yield loc ? getLocationLon(loc) : 0;
1246
+ yield loc ? getLocationLat(loc) : 0;
1247
+ }
1248
+ })(),
1249
+ );
1250
+ const thicknesses = Float32Array.from(
1251
+ (function* () {
1252
+ for (const flow of flows) {
1253
+ yield flowThicknessScale
1254
+ ? flowThicknessScale(getFlowMagnitude(flow)) || 0
1255
+ : 0;
1256
+ }
1257
+ })(),
1258
+ );
1259
+ const endpointOffsets = Float32Array.from(
1260
+ (function* () {
1261
+ for (const flow of flows) {
1262
+ const originId = getFlowOriginId(flow);
1263
+ const destId = getFlowDestId(flow);
1264
+ yield Math.max(getInCircleSize(originId), getOutCircleSize(originId));
1265
+ yield Math.max(getInCircleSize(destId), getOutCircleSize(destId));
1266
+ }
1267
+ })(),
1268
+ );
1269
+ const flowLineColors = Uint8Array.from(
1270
+ (function* () {
1271
+ for (const flow of flows) {
1272
+ yield* flowColorScale(getFlowMagnitude(flow));
1273
+ }
1274
+ })(),
1275
+ );
1276
+
1277
+ const staggeringValues = animationEnabled
1278
+ ? Float32Array.from(
1279
+ (function* () {
1280
+ for (const f of flows) {
1281
+ // @ts-ignore
1282
+ yield new alea(`${getFlowOriginId(f)}-${getFlowDestId(f)}`)();
1283
+ }
1284
+ })(),
1285
+ )
1286
+ : undefined;
1287
+
1288
+ return {
1289
+ circleAttributes: {
1290
+ length: locations.length,
1291
+ attributes: {
1292
+ getPosition: {value: circlePositions, size: 2},
1293
+ getColor: {value: circleColors, size: 4},
1294
+ getInRadius: {value: inCircleRadii, size: 1},
1295
+ getOutRadius: {value: outCircleRadii, size: 1},
1296
+ },
1297
+ },
1298
+ lineAttributes: {
1299
+ length: flows.length,
1300
+ attributes: {
1301
+ getSourcePosition: {value: sourcePositions, size: 2},
1302
+ getTargetPosition: {value: targetPositions, size: 2},
1303
+ getThickness: {value: thicknesses, size: 1},
1304
+ getColor: {value: flowLineColors, size: 4},
1305
+ getEndpointOffsets: {value: endpointOffsets, size: 2},
1306
+ ...(staggeringValues
1307
+ ? {getStaggering: {value: staggeringValues, size: 1}}
1308
+ : {}),
1309
+ },
1310
+ },
1311
+ ...(locationLabelsEnabled
1312
+ ? {locationLabels: locations.map(getLocationName)}
1313
+ : undefined),
1314
+ };
1315
+ }
1316
+
1317
+ getLocationsInBbox(
1318
+ tree: KDBushTree,
1319
+ bbox: [number, number, number, number],
1320
+ ): Array<L> | undefined {
1321
+ if (!tree) return undefined;
1322
+ return this._getLocationsInBboxIndices(tree, bbox).map(
1323
+ (idx: number) => tree.points[idx],
1324
+ ) as Array<L>;
1325
+ }
1326
+
1327
+ _getLocationsInBboxIndices(
1328
+ tree: KDBushTree,
1329
+ bbox: [number, number, number, number],
1330
+ ) {
1331
+ if (!tree) return undefined;
1332
+ const [lon1, lat1, lon2, lat2] = bbox;
1333
+ const [x1, y1, x2, y2] = [lngX(lon1), latY(lat1), lngX(lon2), latY(lat2)];
1334
+ return tree.range(
1335
+ Math.min(x1, x2),
1336
+ Math.min(y1, y2),
1337
+ Math.max(x1, x2),
1338
+ Math.max(y1, y2),
1339
+ );
1340
+ }
1341
+
1342
+ isFlowInSelection(
1343
+ flow: F | AggregateFlow,
1344
+ selectedLocationsSet: Set<string | number> | undefined,
1345
+ locationFilterMode?: LocationFilterMode,
1346
+ ) {
1347
+ const origin = this.accessors.getFlowOriginId(flow);
1348
+ const dest = this.accessors.getFlowDestId(flow);
1349
+ if (selectedLocationsSet) {
1350
+ switch (locationFilterMode) {
1351
+ case LocationFilterMode.ALL:
1352
+ return (
1353
+ selectedLocationsSet.has(origin) || selectedLocationsSet.has(dest)
1354
+ );
1355
+ case LocationFilterMode.BETWEEN:
1356
+ return (
1357
+ selectedLocationsSet.has(origin) && selectedLocationsSet.has(dest)
1358
+ );
1359
+ case LocationFilterMode.INCOMING:
1360
+ return selectedLocationsSet.has(dest);
1361
+ case LocationFilterMode.OUTGOING:
1362
+ return selectedLocationsSet.has(origin);
1363
+ }
1364
+ }
1365
+ return true;
1366
+ }
1367
+
1368
+ // calcLocationTotals(
1369
+ // locations: (L | ClusterNode)[],
1370
+ // flows: F[],
1371
+ // ): LocationsTotals {
1372
+ // return flows.reduce(
1373
+ // (acc: LocationsTotals, curr) => {
1374
+ // const originId = this.accessors.getFlowOriginId(curr);
1375
+ // const destId = this.accessors.getFlowDestId(curr);
1376
+ // const magnitude = this.accessors.getFlowMagnitude(curr);
1377
+ // if (originId === destId) {
1378
+ // acc.internal[originId] = (acc.internal[originId] || 0) + magnitude;
1379
+ // } else {
1380
+ // acc.outgoing[originId] = (acc.outgoing[originId] || 0) + magnitude;
1381
+ // acc.incoming[destId] = (acc.incoming[destId] || 0) + magnitude;
1382
+ // }
1383
+ // return acc;
1384
+ // },
1385
+ // {incoming: {}, outgoing: {}, internal: {}},
1386
+ // );
1387
+ // }
1388
+ }
1389
+
1390
+ function calcLocationTotalsExtent(
1391
+ locationTotals: Map<string | number, LocationTotals> | undefined,
1392
+ locationIdsInViewport: Set<string | number> | undefined,
1393
+ ) {
1394
+ if (!locationTotals) return undefined;
1395
+ let rv: [number, number] | undefined = undefined;
1396
+ for (const [
1397
+ id,
1398
+ {incomingCount, outgoingCount, internalCount},
1399
+ ] of locationTotals.entries()) {
1400
+ if (locationIdsInViewport == null || locationIdsInViewport.has(id)) {
1401
+ const lo = Math.min(
1402
+ incomingCount + internalCount,
1403
+ outgoingCount + internalCount,
1404
+ internalCount,
1405
+ );
1406
+ const hi = Math.max(
1407
+ incomingCount + internalCount,
1408
+ outgoingCount + internalCount,
1409
+ internalCount,
1410
+ );
1411
+ if (!rv) {
1412
+ rv = [lo, hi];
1413
+ } else {
1414
+ if (lo < rv[0]) rv[0] = lo;
1415
+ if (hi > rv[1]) rv[1] = hi;
1416
+ }
1417
+ }
1418
+ }
1419
+ return rv;
1420
+ }
1421
+
1422
+ // longitude/latitude to spherical mercator in [0..1] range
1423
+ function lngX(lng: number) {
1424
+ return lng / 360 + 0.5;
1425
+ }
1426
+
1427
+ function latY(lat: number) {
1428
+ const sin = Math.sin((lat * Math.PI) / 180);
1429
+ const y = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI;
1430
+ return y < 0 ? 0 : y > 1 ? 1 : y;
1431
+ }
1432
+
1433
+ function aggregateFlows<F>(
1434
+ flows: F[],
1435
+ flowAccessors: FlowAccessors<F>,
1436
+ ): AggregateFlow[] {
1437
+ // Sum up flows with same origin, dest
1438
+ const byOriginDest = rollup(
1439
+ flows,
1440
+ (ff: F[]) => {
1441
+ const origin = flowAccessors.getFlowOriginId(ff[0]);
1442
+ const dest = flowAccessors.getFlowDestId(ff[0]);
1443
+ // const color = ff[0].color;
1444
+ const rv: AggregateFlow = {
1445
+ aggregate: true,
1446
+ origin,
1447
+ dest,
1448
+ count: ff.reduce((m, f) => {
1449
+ const count = flowAccessors.getFlowMagnitude(f);
1450
+ if (count) {
1451
+ if (!isNaN(count) && isFinite(count)) return m + count;
1452
+ }
1453
+ return m;
1454
+ }, 0),
1455
+ // time: undefined,
1456
+ };
1457
+ // if (color) rv.color = color;
1458
+ return rv;
1459
+ },
1460
+ flowAccessors.getFlowOriginId,
1461
+ flowAccessors.getFlowDestId,
1462
+ );
1463
+
1464
+ const rv: AggregateFlow[] = [];
1465
+ for (const values of byOriginDest.values()) {
1466
+ for (const value of values.values()) {
1467
+ rv.push(value);
1468
+ }
1469
+ }
1470
+ return rv;
1471
+ }
1472
+
1473
+ /**
1474
+ * This is used to augment hover picking info so that we can displace location tooltip
1475
+ * @param circleAttributes
1476
+ * @param index
1477
+ */
1478
+ export function getOuterCircleRadiusByIndex(
1479
+ circleAttributes: FlowCirclesLayerAttributes,
1480
+ index: number,
1481
+ ): number {
1482
+ const {getInRadius, getOutRadius} = circleAttributes.attributes;
1483
+ return Math.max(getInRadius.value[index], getOutRadius.value[index]);
1484
+ }
1485
+
1486
+ export function getLocationCoordsByIndex(
1487
+ circleAttributes: FlowCirclesLayerAttributes,
1488
+ index: number,
1489
+ ): [number, number] {
1490
+ const {getPosition} = circleAttributes.attributes;
1491
+ return [getPosition.value[index * 2], getPosition.value[index * 2 + 1]];
1492
+ }
1493
+
1494
+ export function getFlowLineAttributesByIndex(
1495
+ lineAttributes: FlowLinesLayerAttributes,
1496
+ index: number,
1497
+ ): FlowLinesLayerAttributes {
1498
+ const {
1499
+ getColor,
1500
+ getEndpointOffsets,
1501
+ getSourcePosition,
1502
+ getTargetPosition,
1503
+ getThickness,
1504
+ getStaggering,
1505
+ } = lineAttributes.attributes;
1506
+ return {
1507
+ length: 1,
1508
+ attributes: {
1509
+ getColor: {
1510
+ value: getColor.value.subarray(index * 4, (index + 1) * 4),
1511
+ size: 4,
1512
+ },
1513
+ getEndpointOffsets: {
1514
+ value: getEndpointOffsets.value.subarray(index * 2, (index + 1) * 2),
1515
+ size: 2,
1516
+ },
1517
+ getSourcePosition: {
1518
+ value: getSourcePosition.value.subarray(index * 2, (index + 1) * 2),
1519
+ size: 2,
1520
+ },
1521
+ getTargetPosition: {
1522
+ value: getTargetPosition.value.subarray(index * 2, (index + 1) * 2),
1523
+ size: 2,
1524
+ },
1525
+ getThickness: {
1526
+ value: getThickness.value.subarray(index, index + 1),
1527
+ size: 1,
1528
+ },
1529
+ ...(getStaggering
1530
+ ? {
1531
+ getStaggering: {
1532
+ value: getStaggering.value.subarray(index, index + 1),
1533
+ size: 1,
1534
+ },
1535
+ }
1536
+ : undefined),
1537
+ },
1538
+ };
1539
+ }