@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.
- package/LICENSE +199 -0
- package/dist/FlowMapAggregateAccessors.d.ts +15 -0
- package/dist/FlowMapAggregateAccessors.d.ts.map +1 -0
- package/dist/FlowMapAggregateAccessors.js +43 -0
- package/dist/FlowMapSelectors.d.ts +156 -0
- package/dist/FlowMapSelectors.d.ts.map +1 -0
- package/dist/FlowMapSelectors.js +831 -0
- package/dist/FlowMapState.d.ts +24 -0
- package/dist/FlowMapState.d.ts.map +1 -0
- package/dist/FlowMapState.js +2 -0
- package/dist/cluster/ClusterIndex.d.ts +42 -0
- package/dist/cluster/ClusterIndex.d.ts.map +1 -0
- package/dist/cluster/ClusterIndex.js +178 -0
- package/dist/cluster/cluster.d.ts +31 -0
- package/dist/cluster/cluster.d.ts.map +1 -0
- package/dist/cluster/cluster.js +206 -0
- package/dist/colors.d.ts +103 -0
- package/dist/colors.d.ts.map +1 -0
- package/dist/colors.js +441 -0
- package/dist/getViewStateForLocations.d.ts +16 -0
- package/dist/getViewStateForLocations.d.ts.map +1 -0
- package/dist/getViewStateForLocations.js +30 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/provider/FlowMapDataProvider.d.ts +16 -0
- package/dist/provider/FlowMapDataProvider.d.ts.map +1 -0
- package/dist/provider/FlowMapDataProvider.js +17 -0
- package/dist/provider/LocalFlowMapDataProvider.d.ts +20 -0
- package/dist/provider/LocalFlowMapDataProvider.d.ts.map +1 -0
- package/dist/provider/LocalFlowMapDataProvider.js +87 -0
- package/dist/time.d.ts +24 -0
- package/dist/time.d.ts.map +1 -0
- package/dist/time.js +126 -0
- package/dist/types.d.ts +116 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +23 -0
- package/dist/util.d.ts +2 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +4 -0
- package/package.json +48 -0
- package/src/FlowMapAggregateAccessors.ts +60 -0
- package/src/FlowMapSelectors.ts +1407 -0
- package/src/FlowMapState.ts +26 -0
- package/src/cluster/ClusterIndex.ts +266 -0
- package/src/cluster/cluster.ts +299 -0
- package/src/colors.ts +723 -0
- package/src/getViewStateForLocations.ts +64 -0
- package/src/index.ts +10 -0
- package/src/provider/FlowMapDataProvider.ts +63 -0
- package/src/provider/LocalFlowMapDataProvider.ts +108 -0
- package/src/time.ts +160 -0
- package/src/types.ts +162 -0
- package/src/util.ts +3 -0
- package/tsconfig.json +11 -0
- 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
|
+
}
|