@flowmap.gl/data 8.0.0-alpha.2 → 8.0.0-alpha.22
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/dist/FlowmapAggregateAccessors.d.ts +16 -0
- package/dist/FlowmapAggregateAccessors.d.ts.map +1 -0
- package/dist/FlowmapAggregateAccessors.js +48 -0
- package/dist/FlowmapSelectors.d.ts +213 -0
- package/dist/FlowmapSelectors.d.ts.map +1 -0
- package/dist/FlowmapSelectors.js +861 -0
- package/dist/{FlowMapState.d.ts → FlowmapState.d.ts} +12 -8
- package/dist/FlowmapState.d.ts.map +1 -0
- package/dist/FlowmapState.js +2 -0
- package/dist/cluster/ClusterIndex.d.ts +3 -3
- package/dist/cluster/ClusterIndex.d.ts.map +1 -1
- package/dist/cluster/ClusterIndex.js +1 -1
- package/dist/cluster/cluster.d.ts +6 -5
- package/dist/cluster/cluster.d.ts.map +1 -1
- package/dist/cluster/cluster.js +76 -20
- package/dist/colors.d.ts +7 -7
- package/dist/colors.d.ts.map +1 -1
- package/dist/colors.js +55 -20
- package/dist/getViewStateForLocations.d.ts +18 -11
- package/dist/getViewStateForLocations.d.ts.map +1 -1
- package/dist/getViewStateForLocations.js +37 -23
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -6
- package/dist/provider/FlowmapDataProvider.d.ts +21 -0
- package/dist/provider/FlowmapDataProvider.d.ts.map +1 -0
- package/dist/provider/FlowmapDataProvider.js +17 -0
- package/dist/provider/LocalFlowmapDataProvider.d.ts +25 -0
- package/dist/provider/LocalFlowmapDataProvider.d.ts.map +1 -0
- package/dist/provider/LocalFlowmapDataProvider.js +111 -0
- package/dist/selector-functions.d.ts +10 -0
- package/dist/selector-functions.d.ts.map +1 -0
- package/dist/selector-functions.js +56 -0
- package/dist/types.d.ts +20 -16
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -4
- package/dist/util.d.ts +0 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +1 -4
- package/package.json +10 -12
- package/src/FlowmapAggregateAccessors.ts +67 -0
- package/src/{FlowMapSelectors.ts → FlowmapSelectors.ts} +453 -398
- package/src/{FlowMapState.ts → FlowmapState.ts} +11 -7
- package/src/cluster/ClusterIndex.ts +19 -12
- package/src/cluster/cluster.ts +96 -35
- package/src/colors.ts +70 -28
- package/src/getViewStateForLocations.ts +56 -40
- package/src/index.ts +9 -6
- package/src/provider/FlowmapDataProvider.ts +75 -0
- package/src/provider/LocalFlowmapDataProvider.ts +143 -0
- package/src/selector-functions.ts +87 -0
- package/src/types.ts +23 -19
- package/src/util.ts +0 -4
- package/dist/FlowMapAggregateAccessors.d.ts +0 -15
- package/dist/FlowMapAggregateAccessors.d.ts.map +0 -1
- package/dist/FlowMapAggregateAccessors.js +0 -43
- package/dist/FlowMapSelectors.d.ts +0 -182
- package/dist/FlowMapSelectors.d.ts.map +0 -1
- package/dist/FlowMapSelectors.js +0 -834
- package/dist/FlowMapState.d.ts.map +0 -1
- package/dist/FlowMapState.js +0 -2
- package/dist/provider/FlowMapDataProvider.d.ts +0 -16
- package/dist/provider/FlowMapDataProvider.d.ts.map +0 -1
- package/dist/provider/FlowMapDataProvider.js +0 -17
- package/dist/provider/LocalFlowMapDataProvider.d.ts +0 -20
- package/dist/provider/LocalFlowMapDataProvider.d.ts.map +0 -1
- package/dist/provider/LocalFlowMapDataProvider.js +0 -87
- package/src/FlowMapAggregateAccessors.ts +0 -60
- package/src/provider/FlowMapDataProvider.ts +0 -63
- package/src/provider/LocalFlowMapDataProvider.ts +0 -105
|
@@ -1,62 +1,78 @@
|
|
|
1
|
-
import {BoundingBox, viewport} from '@mapbox/geo-viewport';
|
|
2
1
|
import {geoBounds} from 'd3-geo';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import {fitBounds} from '@math.gl/web-mercator';
|
|
3
|
+
import type {
|
|
4
|
+
FeatureCollection,
|
|
5
|
+
GeometryCollection,
|
|
6
|
+
GeometryObject,
|
|
7
|
+
} from 'geojson';
|
|
8
|
+
import type {ViewState} from './types';
|
|
5
9
|
|
|
6
10
|
export type LocationProperties = any;
|
|
7
11
|
|
|
12
|
+
export type GetViewStateOptions = {
|
|
13
|
+
pad?: number; // size ratio
|
|
14
|
+
padding?: {top: number; bottom: number; left: number; right: number};
|
|
15
|
+
tileSize?: number;
|
|
16
|
+
// minZoom?: number; // not supported by fitBounds
|
|
17
|
+
maxZoom?: number;
|
|
18
|
+
};
|
|
19
|
+
|
|
8
20
|
export function getViewStateForFeatures(
|
|
9
21
|
featureCollection:
|
|
10
22
|
| FeatureCollection<GeometryObject, LocationProperties>
|
|
11
23
|
| GeometryCollection,
|
|
12
24
|
size: [number, number],
|
|
13
|
-
opts?:
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
x2 + pad * (x2 - x1),
|
|
26
|
-
y2 + pad * (y2 - y1),
|
|
27
|
-
];
|
|
28
|
-
const {
|
|
29
|
-
center: [longitude, latitude],
|
|
30
|
-
zoom,
|
|
31
|
-
} = viewport(bounds, size, undefined, undefined, tileSize, true);
|
|
32
|
-
|
|
25
|
+
opts?: GetViewStateOptions,
|
|
26
|
+
): ViewState & {width: number; height: number} {
|
|
27
|
+
const {pad = 0.05, maxZoom = 100} = opts || {};
|
|
28
|
+
const bounds = geoBounds(featureCollection as any);
|
|
29
|
+
const [[x1, y1], [x2, y2]] = bounds;
|
|
30
|
+
const paddedBounds: [[number, number], [number, number]] = pad
|
|
31
|
+
? [
|
|
32
|
+
[x1 - pad * (x2 - x1), y1 - pad * (y2 - y1)],
|
|
33
|
+
[x2 + pad * (x2 - x1), y2 + pad * (y2 - y1)],
|
|
34
|
+
]
|
|
35
|
+
: bounds;
|
|
36
|
+
const [width, height] = size;
|
|
33
37
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
...fitBounds({
|
|
39
|
+
width,
|
|
40
|
+
height,
|
|
41
|
+
bounds: paddedBounds,
|
|
42
|
+
padding: opts?.padding,
|
|
43
|
+
// minZoom,
|
|
44
|
+
maxZoom,
|
|
45
|
+
}),
|
|
46
|
+
width,
|
|
47
|
+
height,
|
|
37
48
|
bearing: 0,
|
|
38
49
|
pitch: 0,
|
|
39
50
|
};
|
|
40
51
|
}
|
|
41
52
|
|
|
42
|
-
export function getViewStateForLocations(
|
|
43
|
-
locations:
|
|
44
|
-
|
|
53
|
+
export function getViewStateForLocations<L>(
|
|
54
|
+
locations: Iterable<L>,
|
|
55
|
+
getLocationCoords: (location: L) => [number, number],
|
|
45
56
|
size: [number, number],
|
|
46
|
-
opts?:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
57
|
+
opts?: GetViewStateOptions,
|
|
58
|
+
): ViewState & {width: number; height: number} {
|
|
59
|
+
const asGeometry = (location: L) => ({
|
|
60
|
+
type: 'Point',
|
|
61
|
+
coordinates: getLocationCoords(location),
|
|
62
|
+
});
|
|
63
|
+
let geometries;
|
|
64
|
+
if (Array.isArray(locations)) {
|
|
65
|
+
geometries = locations.map(asGeometry);
|
|
66
|
+
} else {
|
|
67
|
+
geometries = [];
|
|
68
|
+
for (const location of locations) {
|
|
69
|
+
geometries.push(asGeometry(location));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
53
72
|
return getViewStateForFeatures(
|
|
54
73
|
{
|
|
55
74
|
type: 'GeometryCollection',
|
|
56
|
-
geometries
|
|
57
|
-
type: 'Point',
|
|
58
|
-
coordinates: getLocationCentroid(location),
|
|
59
|
-
})),
|
|
75
|
+
geometries,
|
|
60
76
|
} as any,
|
|
61
77
|
size,
|
|
62
78
|
opts,
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
export * from './types';
|
|
2
2
|
export * from './colors';
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
3
|
+
export * from './FlowmapState';
|
|
4
|
+
export * from './FlowmapSelectors';
|
|
5
|
+
export * from './selector-functions';
|
|
5
6
|
export * from './time';
|
|
6
7
|
export * from './getViewStateForLocations';
|
|
7
|
-
export * from './provider/
|
|
8
|
-
export
|
|
9
|
-
export
|
|
10
|
-
export {default as
|
|
8
|
+
export * from './provider/FlowmapDataProvider';
|
|
9
|
+
export * from './cluster/cluster';
|
|
10
|
+
export * from './cluster/ClusterIndex';
|
|
11
|
+
export {default as FlowmapAggregateAccessors} from './FlowmapAggregateAccessors';
|
|
12
|
+
export type {default as FlowmapDataProvider} from './provider/FlowmapDataProvider';
|
|
13
|
+
export {default as LocalFlowmapDataProvider} from './provider/LocalFlowmapDataProvider';
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {AggregateFlow, Cluster, LocationAccessors, LocationTotals} from '..';
|
|
2
|
+
import {FlowmapState} from '../FlowmapState';
|
|
3
|
+
import {
|
|
4
|
+
ClusterNode,
|
|
5
|
+
FlowmapData,
|
|
6
|
+
FlowmapDataAccessors,
|
|
7
|
+
LayersData,
|
|
8
|
+
ViewportProps,
|
|
9
|
+
} from '../types';
|
|
10
|
+
|
|
11
|
+
export default interface FlowmapDataProvider<L, F> {
|
|
12
|
+
setAccessors(accessors: FlowmapDataAccessors<L, F>): void;
|
|
13
|
+
|
|
14
|
+
setFlowmapState(flowmapState: FlowmapState): Promise<void>;
|
|
15
|
+
|
|
16
|
+
// clearData(): void;
|
|
17
|
+
|
|
18
|
+
getViewportForLocations(
|
|
19
|
+
dims: [number, number],
|
|
20
|
+
): Promise<ViewportProps | undefined>;
|
|
21
|
+
|
|
22
|
+
// getFlowTotals(): Promise<FlowTotals>;
|
|
23
|
+
|
|
24
|
+
getFlowByIndex(index: number): Promise<F | AggregateFlow | undefined>;
|
|
25
|
+
|
|
26
|
+
getLocationById(id: string | number): Promise<L | Cluster | undefined>;
|
|
27
|
+
|
|
28
|
+
getLocationByIndex(idx: number): Promise<L | ClusterNode | undefined>;
|
|
29
|
+
|
|
30
|
+
getTotalsForLocation(
|
|
31
|
+
id: string | number,
|
|
32
|
+
): Promise<LocationTotals | undefined>;
|
|
33
|
+
|
|
34
|
+
// getLocationsInBbox(
|
|
35
|
+
// bbox: [number, number, number, number],
|
|
36
|
+
// ): Promise<Array<FlowLocation | ClusterNode> | undefined>;
|
|
37
|
+
|
|
38
|
+
// getLocationsForSearchBox(): Promise<(FlowLocation | ClusterNode)[] | undefined>;
|
|
39
|
+
|
|
40
|
+
getLayersData(): Promise<LayersData | undefined>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* This is to give the data provider control over when/how often layersData
|
|
44
|
+
* is updated which leads to the flowmap being redrawn.
|
|
45
|
+
*/
|
|
46
|
+
updateLayersData(
|
|
47
|
+
setLayersData: (layersData: LayersData | undefined) => void,
|
|
48
|
+
changeFlags: Record<string, boolean>,
|
|
49
|
+
): Promise<void>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isFlowmapData<L, F>(
|
|
53
|
+
data: Record<string, any>,
|
|
54
|
+
): data is FlowmapData<L, F> {
|
|
55
|
+
return (
|
|
56
|
+
data && data.locations && data.flows
|
|
57
|
+
// TODO: test that they are iterable
|
|
58
|
+
// Array.isArray(data.locations) &&
|
|
59
|
+
// Array.isArray(data.flows)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isFlowmapDataProvider<L, F>(
|
|
64
|
+
dataProvider: Record<string, any>,
|
|
65
|
+
): dataProvider is FlowmapDataProvider<L, F> {
|
|
66
|
+
return (
|
|
67
|
+
dataProvider &&
|
|
68
|
+
typeof dataProvider.setFlowmapState === 'function' &&
|
|
69
|
+
typeof dataProvider.getViewportForLocations === 'function' &&
|
|
70
|
+
typeof dataProvider.getFlowByIndex === 'function' &&
|
|
71
|
+
typeof dataProvider.getLocationById === 'function' &&
|
|
72
|
+
typeof dataProvider.getLocationByIndex === 'function' &&
|
|
73
|
+
typeof dataProvider.getLayersData === 'function'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type FlowmapDataProvider from './FlowmapDataProvider';
|
|
2
|
+
import type {
|
|
3
|
+
Cluster,
|
|
4
|
+
ClusterNode,
|
|
5
|
+
FlowmapData,
|
|
6
|
+
FlowmapDataAccessors,
|
|
7
|
+
LayersData,
|
|
8
|
+
LocationTotals,
|
|
9
|
+
ViewportProps,
|
|
10
|
+
AggregateFlow,
|
|
11
|
+
} from '../types';
|
|
12
|
+
import {FlowmapState} from '../FlowmapState';
|
|
13
|
+
import FlowmapSelectors from '../FlowmapSelectors';
|
|
14
|
+
import {
|
|
15
|
+
GetViewStateOptions,
|
|
16
|
+
getViewStateForLocations,
|
|
17
|
+
} from '../getViewStateForLocations';
|
|
18
|
+
|
|
19
|
+
export default class LocalFlowmapDataProvider<L, F>
|
|
20
|
+
implements FlowmapDataProvider<L, F>
|
|
21
|
+
{
|
|
22
|
+
private selectors: FlowmapSelectors<L, F>;
|
|
23
|
+
private flowmapData: FlowmapData<L, F> | undefined;
|
|
24
|
+
private flowmapState: FlowmapState | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(accessors: FlowmapDataAccessors<L, F>) {
|
|
27
|
+
// scope selectors to the concrete instance of FlowmapDataProvider
|
|
28
|
+
this.selectors = new FlowmapSelectors<L, F>(accessors);
|
|
29
|
+
this.flowmapData = undefined;
|
|
30
|
+
this.flowmapState = undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
setAccessors(accessors: FlowmapDataAccessors<L, F>) {
|
|
34
|
+
this.selectors.setAccessors(accessors);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
setFlowmapData(flowmapData: FlowmapData<L, F>): void {
|
|
38
|
+
this.flowmapData = flowmapData;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getSelectors(): FlowmapSelectors<L, F> {
|
|
42
|
+
return this.selectors;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
getFlowmapData(): FlowmapData<L, F> | undefined {
|
|
46
|
+
return this.flowmapData;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async setFlowmapState(flowmapState: FlowmapState): Promise<void> {
|
|
50
|
+
this.flowmapState = flowmapState;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
getFlowmapState(): FlowmapState | undefined {
|
|
54
|
+
return this.flowmapState;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async getFlowByIndex(idx: number): Promise<F | AggregateFlow | undefined> {
|
|
58
|
+
if (!this.flowmapState || !this.flowmapData) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
const flows = this.selectors.getFlowsForFlowmapLayer(
|
|
62
|
+
this.flowmapState,
|
|
63
|
+
this.flowmapData,
|
|
64
|
+
);
|
|
65
|
+
return flows?.[idx];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// TODO: this is unreliable, should replace by unqiue ID
|
|
69
|
+
async getLocationByIndex(idx: number): Promise<L | ClusterNode | undefined> {
|
|
70
|
+
if (!this.flowmapState || !this.flowmapData) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const locations = this.selectors.getLocationsForFlowmapLayer(
|
|
74
|
+
this.flowmapState,
|
|
75
|
+
this.flowmapData,
|
|
76
|
+
);
|
|
77
|
+
return locations?.[idx];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getLayersData(): Promise<LayersData | undefined> {
|
|
81
|
+
if (!this.flowmapState || !this.flowmapData) {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
return this.selectors.getLayersData(this.flowmapState, this.flowmapData);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async getLocationById(id: string | number): Promise<L | Cluster | undefined> {
|
|
88
|
+
if (!this.flowmapState || !this.flowmapData) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const clusterIndex = this.selectors.getClusterIndex(
|
|
92
|
+
this.flowmapState,
|
|
93
|
+
this.flowmapData,
|
|
94
|
+
);
|
|
95
|
+
if (clusterIndex) {
|
|
96
|
+
const cluster = clusterIndex.getClusterById(id);
|
|
97
|
+
if (cluster) {
|
|
98
|
+
return cluster;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const locationsById = this.selectors.getLocationsById(
|
|
102
|
+
this.flowmapState,
|
|
103
|
+
this.flowmapData,
|
|
104
|
+
);
|
|
105
|
+
return locationsById?.get(id);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async getTotalsForLocation(
|
|
109
|
+
id: string | number,
|
|
110
|
+
): Promise<LocationTotals | undefined> {
|
|
111
|
+
if (!this.flowmapState || !this.flowmapData) {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
return this.selectors
|
|
115
|
+
.getLocationTotals(this.flowmapState, this.flowmapData)
|
|
116
|
+
?.get(id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async getViewportForLocations(
|
|
120
|
+
dims: [number, number],
|
|
121
|
+
opts?: GetViewStateOptions,
|
|
122
|
+
): Promise<ViewportProps | undefined> {
|
|
123
|
+
if (!this.flowmapData?.locations) {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
return getViewStateForLocations(
|
|
128
|
+
this.flowmapData.locations,
|
|
129
|
+
(loc) => [
|
|
130
|
+
this.selectors.accessors.getLocationLon(loc),
|
|
131
|
+
this.selectors.accessors.getLocationLat(loc),
|
|
132
|
+
],
|
|
133
|
+
dims,
|
|
134
|
+
opts,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async updateLayersData(
|
|
139
|
+
setLayersData: (layersData: LayersData | undefined) => void,
|
|
140
|
+
) {
|
|
141
|
+
setLayersData(await this.getLayersData());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import {WebMercatorViewport} from '@math.gl/web-mercator';
|
|
2
|
+
import {
|
|
3
|
+
ClusterLevel,
|
|
4
|
+
isCluster,
|
|
5
|
+
LocationAccessors,
|
|
6
|
+
ViewportProps,
|
|
7
|
+
} from './types';
|
|
8
|
+
import {scaleLinear} from 'd3-scale';
|
|
9
|
+
import {ClusterIndex, LocationWeightGetter} from './cluster/ClusterIndex';
|
|
10
|
+
import {descending} from 'd3-array';
|
|
11
|
+
|
|
12
|
+
// TODO: use re-reselect
|
|
13
|
+
|
|
14
|
+
export const getViewportBoundingBox = (
|
|
15
|
+
viewport: ViewportProps,
|
|
16
|
+
maxLocationCircleSize = 0,
|
|
17
|
+
): [number, number, number, number] => {
|
|
18
|
+
const pad = maxLocationCircleSize;
|
|
19
|
+
const bounds = new WebMercatorViewport({
|
|
20
|
+
...viewport,
|
|
21
|
+
width: viewport.width + pad * 2,
|
|
22
|
+
height: viewport.height + pad * 2,
|
|
23
|
+
}).getBounds();
|
|
24
|
+
return [bounds[0][0], bounds[0][1], bounds[1][0], bounds[1][1]];
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const getFlowThicknessScale = (
|
|
28
|
+
magnitudeExtent: [number, number] | undefined,
|
|
29
|
+
) => {
|
|
30
|
+
if (!magnitudeExtent) return undefined;
|
|
31
|
+
return scaleLinear()
|
|
32
|
+
.range([0.025, 0.5])
|
|
33
|
+
.domain([
|
|
34
|
+
0,
|
|
35
|
+
// should support diff mode too
|
|
36
|
+
Math.max.apply(
|
|
37
|
+
null,
|
|
38
|
+
magnitudeExtent.map((x: number | undefined) => Math.abs(x || 0)),
|
|
39
|
+
),
|
|
40
|
+
]);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adding meaningful cluster names.
|
|
45
|
+
* NOTE: this will mutate the nodes in clusterIndex
|
|
46
|
+
*/
|
|
47
|
+
export function addClusterNames<L, F>(
|
|
48
|
+
clusterIndex: ClusterIndex<F>,
|
|
49
|
+
clusterLevels: ClusterLevel[],
|
|
50
|
+
locationsById: Map<string | number, L>,
|
|
51
|
+
locationAccessors: LocationAccessors<L>,
|
|
52
|
+
getLocationWeight: LocationWeightGetter,
|
|
53
|
+
): void {
|
|
54
|
+
const {getLocationId, getLocationName, getLocationClusterName} =
|
|
55
|
+
locationAccessors;
|
|
56
|
+
const getName = (id: string | number) => {
|
|
57
|
+
const loc = locationsById.get(id);
|
|
58
|
+
if (loc) {
|
|
59
|
+
return getLocationName ? getLocationName(loc) : getLocationId(loc) || id;
|
|
60
|
+
}
|
|
61
|
+
return `"${id}"`;
|
|
62
|
+
};
|
|
63
|
+
for (const level of clusterLevels) {
|
|
64
|
+
for (const node of level.nodes) {
|
|
65
|
+
// Here mutating the nodes (adding names)
|
|
66
|
+
if (isCluster(node)) {
|
|
67
|
+
const leaves = clusterIndex.expandCluster(node);
|
|
68
|
+
|
|
69
|
+
leaves.sort((a, b) =>
|
|
70
|
+
descending(getLocationWeight(a), getLocationWeight(b)),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (getLocationClusterName) {
|
|
74
|
+
node.name = getLocationClusterName(leaves);
|
|
75
|
+
} else {
|
|
76
|
+
const topId = leaves[0];
|
|
77
|
+
const otherId = leaves.length === 2 ? leaves[1] : undefined;
|
|
78
|
+
node.name = `"${getName(topId)}" and ${
|
|
79
|
+
otherId ? `"${getName(otherId)}"` : `${leaves.length - 1} others`
|
|
80
|
+
}`;
|
|
81
|
+
}
|
|
82
|
+
} else {
|
|
83
|
+
(node as any).name = getName(node.id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
export type
|
|
2
|
-
locations: L
|
|
3
|
-
flows: F
|
|
1
|
+
export type FlowmapData<L, F> = {
|
|
2
|
+
locations: Iterable<L> | undefined;
|
|
3
|
+
flows: Iterable<F> | undefined;
|
|
4
|
+
clusterLevels?: ClusterLevels;
|
|
4
5
|
};
|
|
5
6
|
|
|
6
7
|
export interface ViewState {
|
|
@@ -16,24 +17,25 @@ export type FlowAccessor<F, T> = (flow: F) => T; // objectInfo?: AccessorObjectI
|
|
|
16
17
|
export type LocationAccessor<L, T> = (location: L) => T;
|
|
17
18
|
|
|
18
19
|
export interface FlowAccessors<F> {
|
|
19
|
-
getFlowOriginId: FlowAccessor<F, string>;
|
|
20
|
-
getFlowDestId: FlowAccessor<F, string>;
|
|
20
|
+
getFlowOriginId: FlowAccessor<F, string | number>;
|
|
21
|
+
getFlowDestId: FlowAccessor<F, string | number>;
|
|
21
22
|
getFlowMagnitude: FlowAccessor<F, number>;
|
|
22
23
|
getFlowTime?: FlowAccessor<F, Date>; // TODO: use number instead of Date
|
|
23
24
|
// getFlowColor?: FlowAccessor<string | undefined>;
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface LocationAccessors<L> {
|
|
27
|
-
getLocationId: LocationAccessor<L, string>;
|
|
28
|
+
getLocationId: LocationAccessor<L, string | number>;
|
|
28
29
|
getLocationName?: LocationAccessor<L, string>;
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
getLocationLat: LocationAccessor<L, number>;
|
|
31
|
+
getLocationLon: LocationAccessor<L, number>;
|
|
32
|
+
getLocationClusterName?: (locationIds: (string | number)[]) => string;
|
|
31
33
|
// getLocationTotalIn?: LocationAccessor<number>;
|
|
32
34
|
// getLocationTotalOut?: LocationAccessor<number>;
|
|
33
35
|
// getLocationTotalInternal?: LocationAccessor<number>;
|
|
34
36
|
}
|
|
35
37
|
|
|
36
|
-
export type
|
|
38
|
+
export type FlowmapDataAccessors<L, F> = LocationAccessors<L> &
|
|
37
39
|
FlowAccessors<F>;
|
|
38
40
|
|
|
39
41
|
export interface LocationTotals {
|
|
@@ -58,9 +60,9 @@ export interface ViewportProps {
|
|
|
58
60
|
height: number;
|
|
59
61
|
latitude: number;
|
|
60
62
|
longitude: number;
|
|
61
|
-
zoom
|
|
62
|
-
bearing
|
|
63
|
-
pitch
|
|
63
|
+
zoom?: number;
|
|
64
|
+
bearing?: number;
|
|
65
|
+
pitch?: number;
|
|
64
66
|
altitude?: number;
|
|
65
67
|
maxZoom?: number;
|
|
66
68
|
minZoom?: number;
|
|
@@ -73,9 +75,10 @@ export interface ViewportProps {
|
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
export interface ClusterNode {
|
|
76
|
-
id: string;
|
|
78
|
+
id: string | number;
|
|
77
79
|
zoom: number;
|
|
78
|
-
|
|
80
|
+
lat: number;
|
|
81
|
+
lon: number;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
export interface ClusterLevel {
|
|
@@ -102,8 +105,8 @@ export function isLocationClusterNode<L>(l: L | ClusterNode): l is ClusterNode {
|
|
|
102
105
|
}
|
|
103
106
|
|
|
104
107
|
export interface AggregateFlow {
|
|
105
|
-
origin: string;
|
|
106
|
-
dest: string;
|
|
108
|
+
origin: string | number;
|
|
109
|
+
dest: string | number;
|
|
107
110
|
count: number;
|
|
108
111
|
aggregate: true;
|
|
109
112
|
}
|
|
@@ -113,9 +116,9 @@ export function isAggregateFlow(
|
|
|
113
116
|
): flow is AggregateFlow {
|
|
114
117
|
return (
|
|
115
118
|
flow &&
|
|
116
|
-
flow.origin !== undefined &&
|
|
117
|
-
flow.dest !== undefined &&
|
|
118
|
-
flow.count !== undefined &&
|
|
119
|
+
// flow.origin !== undefined &&
|
|
120
|
+
// flow.dest !== undefined &&
|
|
121
|
+
// flow.count !== undefined &&
|
|
119
122
|
(flow.aggregate ? true : false)
|
|
120
123
|
);
|
|
121
124
|
}
|
|
@@ -157,6 +160,7 @@ export interface FlowLinesLayerAttributes {
|
|
|
157
160
|
export interface LayersData {
|
|
158
161
|
circleAttributes: FlowCirclesLayerAttributes;
|
|
159
162
|
lineAttributes: FlowLinesLayerAttributes;
|
|
163
|
+
locationLabels?: string[];
|
|
160
164
|
}
|
|
161
165
|
|
|
162
166
|
export type LayersDataAttrValues<T> = {value: T; size: number};
|
package/src/util.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import {createSelectorCreator, defaultMemoize} from 'reselect';
|
|
2
2
|
|
|
3
|
-
export function flatMap<S, T>(xs: S[], f: (item: S) => T | T[]): T[] {
|
|
4
|
-
return xs.reduce((acc: T[], x: S) => acc.concat(f(x)), []);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
3
|
export const createDebugSelector = createSelectorCreator(defaultMemoize, {
|
|
8
4
|
equalityCheck: (previousVal: any, currentVal: any) => {
|
|
9
5
|
const rv = currentVal === previousVal;
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { AggregateFlow, ClusterNode, FlowMapDataAccessors } from './types';
|
|
2
|
-
export default class FlowMapAggregateAccessors<L, F> {
|
|
3
|
-
private accessors;
|
|
4
|
-
constructor(accessors: FlowMapDataAccessors<L, F>);
|
|
5
|
-
setAccessors(accessors: FlowMapDataAccessors<L, F>): void;
|
|
6
|
-
getFlowMapDataAccessors(): FlowMapDataAccessors<L, F>;
|
|
7
|
-
getLocationId: (location: L | ClusterNode) => string;
|
|
8
|
-
getLocationName: (location: L | ClusterNode) => string;
|
|
9
|
-
getLocationCentroid: (location: L | ClusterNode) => [number, number];
|
|
10
|
-
getFlowOriginId: (f: F | AggregateFlow) => string;
|
|
11
|
-
getFlowDestId: (f: F | AggregateFlow) => string;
|
|
12
|
-
getFlowMagnitude: (f: F | AggregateFlow) => number;
|
|
13
|
-
getFlowTime: (f: F) => Date | undefined;
|
|
14
|
-
}
|
|
15
|
-
//# sourceMappingURL=FlowMapAggregateAccessors.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"FlowMapAggregateAccessors.d.ts","sourceRoot":"","sources":["../src/FlowMapAggregateAccessors.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,aAAa,EACb,WAAW,EACX,oBAAoB,EAIrB,MAAM,SAAS,CAAC;AAEjB,MAAM,CAAC,OAAO,OAAO,yBAAyB,CAAC,CAAC,EAAE,CAAC;IACjD,OAAO,CAAC,SAAS,CAA6B;gBAClC,SAAS,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC;IAIjD,YAAY,CAAC,SAAS,EAAE,oBAAoB,CAAC,CAAC,EAAE,CAAC,CAAC;IAIlD,uBAAuB;IAIvB,aAAa,aAAc,CAAC,GAAG,WAAW,KAAG,MAAM,CAGN;IAE7C,eAAe,aAAc,CAAC,GAAG,WAAW,KAAG,MAAM,CAGJ;IAMjD,mBAAmB,aAAc,CAAC,GAAG,WAAW,KAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAGhB;IAEnD,eAAe,MAAO,CAAC,GAAG,aAAa,YAErC;IAEF,aAAa,MAAO,CAAC,GAAG,aAAa,YAEnC;IAEF,gBAAgB,MAAO,CAAC,GAAG,aAAa,YAEtC;IAGF,WAAW,MAAO,CAAC,sBAGjB;CACH"}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { isAggregateFlow, isCluster, isLocationClusterNode, } from './types';
|
|
2
|
-
export default class FlowMapAggregateAccessors {
|
|
3
|
-
constructor(accessors) {
|
|
4
|
-
this.getLocationId = (location) => isLocationClusterNode(location)
|
|
5
|
-
? location.id
|
|
6
|
-
: this.accessors.getLocationId(location);
|
|
7
|
-
this.getLocationName = (location) => {
|
|
8
|
-
var _a;
|
|
9
|
-
return (_a = (isLocationClusterNode(location) && isCluster(location)
|
|
10
|
-
? location.name
|
|
11
|
-
: undefined)) !== null && _a !== void 0 ? _a : this.getLocationId(location);
|
|
12
|
-
};
|
|
13
|
-
// ? location.name // TODO getLocationName for locations and clusters
|
|
14
|
-
// : this.accessors.getLocationName
|
|
15
|
-
// ? this.accessors.getLocationName(location)
|
|
16
|
-
// : this.getLocationId(location);
|
|
17
|
-
this.getLocationCentroid = (location) => isLocationClusterNode(location)
|
|
18
|
-
? location.centroid
|
|
19
|
-
: this.accessors.getLocationCentroid(location);
|
|
20
|
-
this.getFlowOriginId = (f) => {
|
|
21
|
-
return isAggregateFlow(f) ? f.origin : this.accessors.getFlowOriginId(f);
|
|
22
|
-
};
|
|
23
|
-
this.getFlowDestId = (f) => {
|
|
24
|
-
return isAggregateFlow(f) ? f.dest : this.accessors.getFlowDestId(f);
|
|
25
|
-
};
|
|
26
|
-
this.getFlowMagnitude = (f) => {
|
|
27
|
-
return isAggregateFlow(f) ? f.count : this.accessors.getFlowMagnitude(f);
|
|
28
|
-
};
|
|
29
|
-
// Note: Aggregate flows have no time
|
|
30
|
-
this.getFlowTime = (f) => {
|
|
31
|
-
const { getFlowTime } = this.accessors;
|
|
32
|
-
return getFlowTime ? getFlowTime(f) : undefined;
|
|
33
|
-
};
|
|
34
|
-
this.accessors = accessors;
|
|
35
|
-
}
|
|
36
|
-
setAccessors(accessors) {
|
|
37
|
-
this.accessors = accessors;
|
|
38
|
-
}
|
|
39
|
-
getFlowMapDataAccessors() {
|
|
40
|
-
return this.accessors;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiRmxvd01hcEFnZ3JlZ2F0ZUFjY2Vzc29ycy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9GbG93TWFwQWdncmVnYXRlQWNjZXNzb3JzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFJTCxlQUFlLEVBQ2YsU0FBUyxFQUNULHFCQUFxQixHQUN0QixNQUFNLFNBQVMsQ0FBQztBQUVqQixNQUFNLENBQUMsT0FBTyxPQUFPLHlCQUF5QjtJQUU1QyxZQUFZLFNBQXFDO1FBWWpELGtCQUFhLEdBQUcsQ0FBQyxRQUF5QixFQUFVLEVBQUUsQ0FDcEQscUJBQXFCLENBQUMsUUFBUSxDQUFDO1lBQzdCLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRTtZQUNiLENBQUMsQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQztRQUU3QyxvQkFBZSxHQUFHLENBQUMsUUFBeUIsRUFBVSxFQUFFOztZQUN0RCxPQUFBLE1BQUEsQ0FBQyxxQkFBcUIsQ0FBQyxRQUFRLENBQUMsSUFBSSxTQUFTLENBQUMsUUFBUSxDQUFDO2dCQUNyRCxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUk7Z0JBQ2YsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxtQ0FBSSxJQUFJLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFBO1NBQUEsQ0FBQztRQUNqRCxxRUFBcUU7UUFDckUsbUNBQW1DO1FBQ25DLDZDQUE2QztRQUM3QyxrQ0FBa0M7UUFFbEMsd0JBQW1CLEdBQUcsQ0FBQyxRQUF5QixFQUFvQixFQUFFLENBQ3BFLHFCQUFxQixDQUFDLFFBQVEsQ0FBQztZQUM3QixDQUFDLENBQUMsUUFBUSxDQUFDLFFBQVE7WUFDbkIsQ0FBQyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsbUJBQW1CLENBQUMsUUFBUSxDQUFDLENBQUM7UUFFbkQsb0JBQWUsR0FBRyxDQUFDLENBQW9CLEVBQUUsRUFBRTtZQUN6QyxPQUFPLGVBQWUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxlQUFlLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDM0UsQ0FBQyxDQUFDO1FBRUYsa0JBQWEsR0FBRyxDQUFDLENBQW9CLEVBQUUsRUFBRTtZQUN2QyxPQUFPLGVBQWUsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLFNBQVMsQ0FBQyxhQUFhLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDdkUsQ0FBQyxDQUFDO1FBRUYscUJBQWdCLEdBQUcsQ0FBQyxDQUFvQixFQUFFLEVBQUU7WUFDMUMsT0FBTyxlQUFlLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsZ0JBQWdCLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDM0UsQ0FBQyxDQUFDO1FBRUYscUNBQXFDO1FBQ3JDLGdCQUFXLEdBQUcsQ0FBQyxDQUFJLEVBQUUsRUFBRTtZQUNyQixNQUFNLEVBQUMsV0FBVyxFQUFDLEdBQUcsSUFBSSxDQUFDLFNBQVMsQ0FBQztZQUNyQyxPQUFPLFdBQVcsQ0FBQyxDQUFDLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxTQUFTLENBQUM7UUFDbEQsQ0FBQyxDQUFDO1FBOUNBLElBQUksQ0FBQyxTQUFTLEdBQUcsU0FBUyxDQUFDO0lBQzdCLENBQUM7SUFFRCxZQUFZLENBQUMsU0FBcUM7UUFDaEQsSUFBSSxDQUFDLFNBQVMsR0FBRyxTQUFTLENBQUM7SUFDN0IsQ0FBQztJQUVELHVCQUF1QjtRQUNyQixPQUFPLElBQUksQ0FBQyxTQUFTLENBQUM7SUFDeEIsQ0FBQztDQXNDRiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7XG4gIEFnZ3JlZ2F0ZUZsb3csXG4gIENsdXN0ZXJOb2RlLFxuICBGbG93TWFwRGF0YUFjY2Vzc29ycyxcbiAgaXNBZ2dyZWdhdGVGbG93LFxuICBpc0NsdXN0ZXIsXG4gIGlzTG9jYXRpb25DbHVzdGVyTm9kZSxcbn0gZnJvbSAnLi90eXBlcyc7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIEZsb3dNYXBBZ2dyZWdhdGVBY2Nlc3NvcnM8TCwgRj4ge1xuICBwcml2YXRlIGFjY2Vzc29yczogRmxvd01hcERhdGFBY2Nlc3NvcnM8TCwgRj47XG4gIGNvbnN0cnVjdG9yKGFjY2Vzc29yczogRmxvd01hcERhdGFBY2Nlc3NvcnM8TCwgRj4pIHtcbiAgICB0aGlzLmFjY2Vzc29ycyA9IGFjY2Vzc29ycztcbiAgfVxuXG4gIHNldEFjY2Vzc29ycyhhY2Nlc3NvcnM6IEZsb3dNYXBEYXRhQWNjZXNzb3JzPEwsIEY+KSB7XG4gICAgdGhpcy5hY2Nlc3NvcnMgPSBhY2Nlc3NvcnM7XG4gIH1cblxuICBnZXRGbG93TWFwRGF0YUFjY2Vzc29ycygpIHtcbiAgICByZXR1cm4gdGhpcy5hY2Nlc3NvcnM7XG4gIH1cblxuICBnZXRMb2NhdGlvbklkID0gKGxvY2F0aW9uOiBMIHwgQ2x1c3Rlck5vZGUpOiBzdHJpbmcgPT5cbiAgICBpc0xvY2F0aW9uQ2x1c3Rlck5vZGUobG9jYXRpb24pXG4gICAgICA/IGxvY2F0aW9uLmlkXG4gICAgICA6IHRoaXMuYWNjZXNzb3JzLmdldExvY2F0aW9uSWQobG9jYXRpb24pO1xuXG4gIGdldExvY2F0aW9uTmFtZSA9IChsb2NhdGlvbjogTCB8IENsdXN0ZXJOb2RlKTogc3RyaW5nID0+XG4gICAgKGlzTG9jYXRpb25DbHVzdGVyTm9kZShsb2NhdGlvbikgJiYgaXNDbHVzdGVyKGxvY2F0aW9uKVxuICAgICAgPyBsb2NhdGlvbi5uYW1lXG4gICAgICA6IHVuZGVmaW5lZCkgPz8gdGhpcy5nZXRMb2NhdGlvbklkKGxvY2F0aW9uKTtcbiAgLy8gPyBsb2NhdGlvbi5uYW1lIC8vIFRPRE8gZ2V0TG9jYXRpb25OYW1lIGZvciBsb2NhdGlvbnMgYW5kIGNsdXN0ZXJzXG4gIC8vIDogdGhpcy5hY2Nlc3NvcnMuZ2V0TG9jYXRpb25OYW1lXG4gIC8vID8gdGhpcy5hY2Nlc3NvcnMuZ2V0TG9jYXRpb25OYW1lKGxvY2F0aW9uKVxuICAvLyA6IHRoaXMuZ2V0TG9jYXRpb25JZChsb2NhdGlvbik7XG5cbiAgZ2V0TG9jYXRpb25DZW50cm9pZCA9IChsb2NhdGlvbjogTCB8IENsdXN0ZXJOb2RlKTogW251bWJlciwgbnVtYmVyXSA9PlxuICAgIGlzTG9jYXRpb25DbHVzdGVyTm9kZShsb2NhdGlvbilcbiAgICAgID8gbG9jYXRpb24uY2VudHJvaWRcbiAgICAgIDogdGhpcy5hY2Nlc3NvcnMuZ2V0TG9jYXRpb25DZW50cm9pZChsb2NhdGlvbik7XG5cbiAgZ2V0Rmxvd09yaWdpbklkID0gKGY6IEYgfCBBZ2dyZWdhdGVGbG93KSA9PiB7XG4gICAgcmV0dXJuIGlzQWdncmVnYXRlRmxvdyhmKSA/IGYub3JpZ2luIDogdGhpcy5hY2Nlc3NvcnMuZ2V0Rmxvd09yaWdpbklkKGYpO1xuICB9O1xuXG4gIGdldEZsb3dEZXN0SWQgPSAoZjogRiB8IEFnZ3JlZ2F0ZUZsb3cpID0+IHtcbiAgICByZXR1cm4gaXNBZ2dyZWdhdGVGbG93KGYpID8gZi5kZXN0IDogdGhpcy5hY2Nlc3NvcnMuZ2V0Rmxvd0Rlc3RJZChmKTtcbiAgfTtcblxuICBnZXRGbG93TWFnbml0dWRlID0gKGY6IEYgfCBBZ2dyZWdhdGVGbG93KSA9PiB7XG4gICAgcmV0dXJuIGlzQWdncmVnYXRlRmxvdyhmKSA/IGYuY291bnQgOiB0aGlzLmFjY2Vzc29ycy5nZXRGbG93TWFnbml0dWRlKGYpO1xuICB9O1xuXG4gIC8vIE5vdGU6IEFnZ3JlZ2F0ZSBmbG93cyBoYXZlIG5vIHRpbWVcbiAgZ2V0Rmxvd1RpbWUgPSAoZjogRikgPT4ge1xuICAgIGNvbnN0IHtnZXRGbG93VGltZX0gPSB0aGlzLmFjY2Vzc29ycztcbiAgICByZXR1cm4gZ2V0Rmxvd1RpbWUgPyBnZXRGbG93VGltZShmKSA6IHVuZGVmaW5lZDtcbiAgfTtcbn1cbiJdfQ==
|