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