-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtimeline.js
140 lines (132 loc) · 4.59 KB
/
timeline.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/* global L */
import IntervalTree from 'diesal/src/ds/IntervalTree';
L.Timeline = L.GeoJSON.extend({
times: null,
ranges: null,
/**
* @constructor
* @param {Object} geojson The GeoJSON data for this layer
* @param {Object} options Hash of options
* @param {Function} [options.getInterval] A function which returns an object
* with `start` and `end` properties, called for each feature in the GeoJSON
* data.
* @param {Boolean} [options.drawOnSetTime=true] Make the layer draw as soon
* as `setTime` is called. If this is set to false, you will need to call
* `updateDisplayedLayers()` manually.
*/
initialize(geojson, options = {}) {
this.times = [];
this.ranges = new IntervalTree();
const defaultOptions = {
drawOnSetTime: true,
};
L.GeoJSON.prototype.initialize.call(this, null, options);
L.Util.setOptions(this, defaultOptions);
L.Util.setOptions(this, options);
if (this.options.getInterval) {
this._getInterval = (...args) => this.options.getInterval(...args);
}
if (geojson) {
this._process(geojson);
}
},
_getInterval(feature) {
const hasStart = 'start' in feature.properties;
const hasEnd = 'end' in feature.properties;
if (hasStart && hasEnd) {
return {
start: new Date(feature.properties.start).getTime(),
end: new Date(feature.properties.end).getTime(),
};
}
return false;
},
/**
* Finds the first and last times in the dataset, adds all times into an
* array, and puts everything into an IntervalTree for quick lookup.
*
* @param {Object} data GeoJSON to process
*/
_process(data) {
// In case we don't have a manually set start or end time, we need to find
// the extremes in the data. We can do that while we're inserting everything
// into the interval tree.
let start = Infinity;
let end = -Infinity;
data.features.forEach((feature) => {
const interval = this._getInterval(feature);
if (!interval) { return; }
this.ranges.insert(interval.start, interval.end, feature);
this.times.push(interval.start);
this.times.push(interval.end);
start = Math.min(start, interval.start);
end = Math.max(end, interval.end);
});
this.start = this.options.start || start;
this.end = this.options.end || end;
this.time = this.start;
if (this.times.length === 0) {
return;
}
// default sort is lexicographic, even for number types. so need to
// specify sorting function.
this.times.sort((a, b) => a - b);
// de-duplicate the times
this.times = this.times.reduce((newList, x, i) => {
if (i === 0) {
return newList;
}
const lastTime = newList[newList.length - 1];
if (lastTime !== x) {
newList.push(x);
}
return newList;
}, [this.times[0]]);
},
/**
* Sets the time for this layer.
*
* @param {Number|String} time The time to set. Usually a number, but if your
* data is really time-based then you can pass a string (e.g. '2015-01-01')
* and it will be processed into a number automatically.
*/
setTime(time) {
this.time = typeof time === 'number' ? time : new Date(time).getTime();
if (this.options.drawOnSetTime) {
this.updateDisplayedLayers();
}
this.fire('change');
},
/**
* Update the layer to show only the features that are relevant at the current
* time. Usually shouldn't need to be called manually, unless you set
* `drawOnSetTime` to `false`.
*/
updateDisplayedLayers() {
// This loop is intended to help optimize things a bit. First, we find all
// the features that should be displayed at the current time.
const features = this.ranges.lookup(this.time);
// Then we try to match each currently displayed layer up to a feature. If
// we find a match, then we remove it from the feature list. If we don't
// find a match, then the displayed layer is no longer valid at this time.
// We should remove it.
for (let i = 0; i < this.getLayers().length; i++) {
let found = false;
const layer = this.getLayers()[i];
for (let j = 0; j < features.length; j++) {
if (layer.feature === features[j]) {
found = true;
features.splice(j, 1);
break;
}
}
if (!found) {
const toRemove = this.getLayers()[i--];
this.removeLayer(toRemove);
}
}
// Finally, with any features left, they must be new data! We can add them.
features.forEach(feature => this.addData(feature));
},
});
L.timeline = (geojson, options) => new L.Timeline(geojson, options);