leaflet-timedimension
Version:
Add time dimension capabilities on a Leaflet map
597 lines (551 loc) • 21.6 kB
JavaScript
/*
* L.TimeDimension.Layer.WMS.TimeSeries: create timeseries for specific locations
*/
L.TimeDimension.Layer.WMS.TimeSeries = L.TimeDimension.Layer.WMS.extend({
initialize: function(layer, options) {
this._markers = options.markers || [];
this._markerColors = options.markerColors || ["#2f7ed8", "#0d233a", "#8bbc21", "#910000", "#1aadce", "#492970", "#f28f43", "#77a1e5", "#c42525", "#a6c96a"];
this._name = options.name || "";
this._defaultRangeSelector = options.defaultRangeSelector || 1;
this._enableNewMarkers = options.enableNewMarkers || false;
this._chartOptions = options.chartOptions || {};
this._currentMarkerColor = 0;
L.TimeDimension.Layer.WMS.prototype.initialize.call(this, layer, options);
if (options.units) {
this._units = options.units;
} else {
this._loadUnits();
}
this._circleLabelMarkers = [];
},
onAdd: function(map) {
if (this._enableNewMarkers && this._enabledNewMarkers === undefined) {
this._enabledNewMarkers = true;
map.doubleClickZoom.disable();
map.on('dblclick', (function(e) {
// e.originalEvent.preventDefault();
this.addPositionMarker({
position: [e.latlng.lat, e.latlng.lng]
});
return false;
}).bind(this));
}
this._setDateRanges();
if (this._dateRange !== undefined) {
this._addMarkers();
}
return L.TimeDimension.Layer.WMS.prototype.onAdd.call(this, map);
},
eachLayer: function(method, context) {
for (var i = 0, l = this._circleLabelMarkers.length; i < l; i++) {
method.call(context, this._circleLabelMarkers[i]);
}
return L.TimeDimension.Layer.WMS.prototype.eachLayer.call(this, method, context);
},
onRemove: function(map){
if (this._chart){
this._chart.destroy();
delete this._chart;
}
return L.TimeDimension.Layer.WMS.prototype.onRemove.call(this, map);
},
// we need to overwrite this function, which is called when the layer has availabletimes loaded,
// in order to initialize dates ranges (current min-max and layer min-max date ranges) and after that
// add the default markers to the map
_updateTimeDimensionAvailableTimes: function() {
L.TimeDimension.Layer.WMS.prototype._updateTimeDimensionAvailableTimes.call(this);
if (this._dateRange === undefined) {
this._setDateRanges();
this._addMarkers();
}
},
_getNextMarkerColor: function() {
return this._markerColors[this._currentMarkerColor++ % this._markerColors.length];
},
_addMarkers: function() {
for (var i = 0, l = this._markers.length; i < l; i++) {
this.addPositionMarker(this._markers[i]);
}
},
addPositionMarker: function(point) {
if (!this._map) {
return;
}
var color = this._getNextMarkerColor();
var circle = L.circleMarker([point.position[0], point.position[1]], {
color: '#FFFFFF',
fillColor: color,
fillOpacity: 0.8,
radius: 5,
weight: 2
}).addTo(this._map);
var afterLoadData = function(color, data) {
var serie = this._showData(color, data, point.name);
var marker = L.timeDimension.layer.circleLabelMarker(circle, {
serieId: serie,
dataLayer: this._currentLayer,
proxy: this._proxy
})
this._circleLabelMarkers.push(marker);
marker.addTo(this._map);
if (this._chart) {
this._chart.hideLoading();
}
};
if (this._chart) {
this._chart.showLoading();
}
this._loadData(circle.getLatLng(), afterLoadData.bind(this, color));
},
_loadData: function(latlng, callback) {
var min = new Date(this._getNearestTime(this._currentDateRange.min.getTime()));
var max = new Date(this._getNearestTime(this._currentDateRange.max.getTime()));
var point = this._map.latLngToContainerPoint(latlng);
var url = this._baseLayer.getURL() + '?SERVICE=WMS&VERSION=1.1.1&REQUEST=GetFeatureInfo&SRS=EPSG:4326';
url = url + '&LAYER=' + this._baseLayer.options.layers;
url = url + '&QUERY_LAYERS=' + this._baseLayer.options.layers;
url = url + '&X=' + point.x + '&Y=' + point.y + '&I=' + point.x + '&J=' + point.y;
var size = this._map.getSize();
url = url + '&BBox=' + this._map.getBounds().toBBoxString();
url = url + '&WIDTH=' + size.x + '&HEIGHT=' + size.y;
url = url + '&INFO_FORMAT=text/xml';
var url_without_time = url;
url = url + '&TIME=' + min.toISOString() + '/' + max.toISOString();
if (this._proxy) url = this._proxy + '?url=' + encodeURIComponent(url);
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", (function(xhr) {
var data = xhr.currentTarget.responseXML;
var result = {
time: [],
values: []
};
// Add min and max values to be able to get more data later
if (this._currentDateRange.min > this._dateRange.min) {
result.time.push(this._dateRange.min);
result.values.push(null);
}
data.querySelectorAll('FeatureInfo').forEach(function(fi) {
var this_time = fi.querySelector('time').textContent;
var this_data = fi.querySelector('value').textContent;
try {
this_data = parseFloat(this_data);
} catch (e) {
this_data = null;
}
result.time.push(this_time);
result.values.push(this_data);
});
if (this._currentDateRange.max < this._dateRange.max) {
result.time.push(this._dateRange.max);
result.values.push(null);
}
result.longitude = data.querySelector('longitude').textContent;
try {
result.longitude = parseFloat(result.longitude).toFixed(4);
} catch (e) {}
result.latitude = data.querySelector('latitude').textContent;
try {
result.latitude = parseFloat(result.latitude).toFixed(4);
} catch (e) {}
result.url = url_without_time;
if (callback !== undefined) {
callback(result);
}
}).bind(this));
oReq.overrideMimeType('application/xml');
oReq.open("GET", url);
oReq.send();
},
_checkLoadNewData: function(min, max) {
min = new Date(min);
max = new Date(max);
var afterLoadData = (function(serie, data) {
if (data !== undefined)
this._updateSerie(serie, data.time, data.values);
this._chart.hideLoading();
}).bind(this);
var i, l, serie;
min = new Date(this._getNearestTime(min.getTime()));
max = new Date(this._getNearestTime(max.getTime()));
if (min < this._currentDateRange.min) {
var old_min = this._currentDateRange.min;
this._currentDateRange.min = min;
this._chart.showLoading();
for (i = 0, l = this._chart.series.length; i < l; i++) {
serie = this._chart.series[i];
if (serie.name != "Navigator")
this._loadMoreData(serie.options.custom.url, min, old_min, afterLoadData.bind(this, serie));
}
}
if (max > this._currentDateRange.max) {
var old_max = this._currentDateRange.max;
this._currentDateRange.max = max;
this._chart.showLoading();
for (i = 0, l = this._chart.series.length; i < l; i++) {
serie = this._chart.series[i];
if (serie.name != "Navigator")
this._loadMoreData(serie.options.custom.url, old_max, max, afterLoadData.bind(this, serie));
}
}
},
_setDateRanges: function() {
if (!this._timeDimension) {
return;
}
var times = this._timeDimension.getAvailableTimes();
if (times.length == 0){
return;
}
this._dateRange = {
min: new Date(times[0]),
max: new Date(times[times.length - 1])
};
var max = this._dateRange.max;
// check if max is a valid date
if (!max.getTime || isNaN(max.getTime())) {
return;
}
var min = new Date(Date.UTC(max.getUTCFullYear(), max.getUTCMonth(), max.getUTCDate()));
if (this._defaultRangeSelector === 0) {
min.setUTCDate(min.getUTCDate() - 3);
} else if (this._defaultRangeSelector === 1) {
min.setUTCDate(min.getUTCDate() - 7);
} else if (this._defaultRangeSelector === 2) {
min.setUTCMonth(min.getUTCMonth() - 1);
} else if (this._defaultRangeSelector === 3) {
min.setUTCMonth(min.getUTCMonth() - 3);
} else if (this._defaultRangeSelector === 4) {
min.setUTCMonth(min.getUTCMonth() - 6);
} else {
min.setUTCFullYear(min.getUTCFullYear() - 1);
}
if (min < this._dateRange.min) {
min = this._dateRange.min;
}
min = new Date(this._getNearestTime(min.getTime()));
this._currentDateRange = {
min: min,
max: max
};
},
_loadUnits: function() {
var url = this._baseLayer.getURL() + '?service=WMS&version=1.1.1&request=GetMetadata&item=layerDetails';
url = url + '&layerName=' + this._baseLayer.options.layers;
if (this._proxy) url = this._proxy + '?url=' + encodeURIComponent(url);
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", (function(xhr) {
var response = xhr.currentTarget.response;
var data = JSON.parse(response);
this._units = data.units;
}).bind(this));
oReq.open("GET", url);
oReq.send();
},
_createChart: function() {
var mapContainerParent = this._map.getContainer().parentNode;
var chart_wrapper = mapContainerParent.querySelector('.chart-wrapper');
if (!chart_wrapper) {
var wrapper = document.createElement("div");
wrapper.setAttribute("class", "chart-wrapper");
mapContainerParent.appendChild(wrapper);
chart_wrapper = mapContainerParent.querySelector('.chart-wrapper');
}
var chart_container = chart_wrapper.querySelector('.chart-' + this._baseLayer.options.layers);
if (!chart_container) {
var container = document.createElement("div");
container.setAttribute("class", "chart chart-" + this._baseLayer.options.layers);
chart_wrapper.appendChild(container);
chart_container = chart_wrapper.querySelector('.chart-' + this._baseLayer.options.layers);
}
var options = {
legend: {
enabled: true
},
chart: {
zoomType: 'x'
},
rangeSelector: {
selected: this._defaultRangeSelector,
buttons: [{
type: 'day',
count: 3,
text: '3d'
}, {
type: 'day',
count: 7,
text: '7d'
}, {
type: 'month',
count: 1,
text: '1m'
}, {
type: 'month',
count: 3,
text: '3m'
}, {
type: 'month',
count: 6,
text: '6m'
}, {
type: 'year',
count: 1,
text: '1y'
}, {
type: 'all',
text: 'All'
}]
},
xAxis: {
events: {
afterSetExtremes: (function(e) {
this._checkLoadNewData(e.min, e.max);
}).bind(this)
},
plotLines: [{
color: 'red',
dashStyle: 'solid',
value: new Date(this._timeDimension.getCurrentTime()),
width: 2,
id: 'pbCurrentTime'
}]
},
title: {
text: this._name
},
series: [],
plotOptions: {
series: {
cursor: 'pointer',
point: {
events: {
click: (function(event) {
var day = new Date(event.point.x);
this._timeDimension.setCurrentTime(day.getTime());
}).bind(this)
}
}
}
}
};
if (this._baseLayer.options.layers.substring(0, 3) == 'QC_') {
options['yAxis'] = {};
options['yAxis']['tickPositions'] = [0, 1, 2, 3, 4, 6, 9];
options['yAxis']['plotBands'] = [{
from: 0,
to: 0.5,
color: '#FFFFFF',
label: {
text: 'No QC performed',
style: {
color: '#606060'
}
}
}, {
from: 0.5,
to: 1.5,
color: 'rgba(0, 255, 0, 0.5)',
label: {
text: 'Good data',
style: {
color: '#606060'
}
}
}, {
from: 1.5,
to: 2.5,
color: 'rgba(0, 255, 0, 0.2)',
label: {
text: 'Probably good data',
style: {
color: '#606060'
}
}
}, {
from: 2.5,
to: 3.5,
color: 'rgba(255, 0, 0, 0.2)',
label: {
text: 'Probably bad data',
style: {
color: '#606060'
}
}
}, {
from: 3.5,
to: 4.5,
color: 'rgba(255, 0, 0, 0.5)',
label: {
text: 'Bad data',
style: {
color: '#606060'
}
}
}, {
from: 5.5,
to: 6.5,
color: 'rgba(177, 11, 255, 0.5)',
label: {
text: 'Spike',
style: {
color: '#606060'
}
}
}, { // High wind
from: 8.5,
to: 9.5,
color: 'rgba(200, 200, 200, 0.2)',
label: {
text: 'Missing value',
style: {
color: '#606060'
}
}
}];
}
if (this._units == 'degree') {
options['yAxis'] = {};
options['yAxis']['tickPositions'] = [0, 90, 180, 270, 360, 361];
options['yAxis']['labels'] = {
formatter: function() {
if (this.value == 0)
return 'N';
if (this.value == 90)
return 'E';
if (this.value == 180)
return 'S';
if (this.value == 270)
return 'W';
if (this.value == 360)
return 'N';
return this.value;
}
};
// options['chart']['type'] = 'heatmap';
};
var combinedHighChartsOptions = {};
for (var attrname in options) { combinedHighChartsOptions[attrname] = options[attrname]; }
for (var attrname in this._chartOptions) { combinedHighChartsOptions[attrname] = this._chartOptions[attrname]; }
this._chart = Highcharts.stockChart(chart_container, combinedHighChartsOptions);
this._timeDimension.on('timeload', (function(data) {
if (!this._chart){
return;
}
this._chart.xAxis[0].removePlotBand("pbCurrentTime");
this._chart.xAxis[0].addPlotLine({
color: 'red',
dashStyle: 'solid',
value: new Date(this._timeDimension.getCurrentTime()),
width: 2,
id: 'pbCurrentTime'
});
}).bind(this));
return this._chart;
},
_showData: function(color, data, positionName) {
var position = data.latitude + ', ' + data.longitude;
if (positionName !== undefined) {
position = positionName;
}
return this._addSerie(data.time, data.values, position, data.url, color);
},
_addSerie: function(time, variableData, position, url, color) {
var serie = this._createSerie(time, variableData, position, url, color);
if (!this._chart){
this._createChart();
}
this._chart.addSeries(serie);
return serie.id;
},
_createSerie: function(time, variableData, position, url, color) {
return {
name: this._name + ' at ' + position,
type: 'line',
id: Math.random().toString(36).substring(7),
color: color,
data: (function() {
var length = time.length;
var data = new Array(length);
var this_time = new Date();
var this_data = null;
for (var i = 0; i < length; i++) {
this_time = (new Date(time[i])).getTime();
this_data = variableData[i];
if (isNaN(this_data))
this_data = null;
data[i] = [this_time, this_data];
}
return data;
})(),
tooltip: {
valueDecimals: 2,
valueSuffix: ' ' + this._units,
xDateFormat: '%A, %b %e, %H:%M',
headerFormat: '<span style="font-size: 12px; font-weight:bold;">{point.key} (Click to visualize the map on this time)</span><br/>'
},
custom: {
variable: this._name,
position: position,
url: url
}
};
},
_updateSerie: function(serie, time, variableData) {
var length = time.length;
var new_data = new Array(length);
var this_time = new Date();
var this_data = null;
for (var i = 0; i < length; i++) {
this_time = (new Date(time[i])).getTime();
this_data = variableData[i];
if (isNaN(this_data))
this_data = null;
new_data[i] = [this_time, this_data];
}
var old_data = serie.options.data;
serie.options.data = old_data.concat(new_data).sort();
serie.setData(serie.options.data);
},
_loadMoreData: function(url, mindate, maxdate, callback) {
var min = new Date(this._getNearestTime(mindate.getTime()));
var max = new Date(this._getNearestTime(maxdate.getTime()));
url = url + '&TIME=' + min.toISOString() + '/' + max.toISOString();
if (this._proxy) url = this._proxy + '?url=' + encodeURIComponent(url);
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", (function(xhr) {
var data = xhr.currentTarget.responseXML;
var result = {
time: [],
values: []
};
data.querySelectorAll('FeatureInfo').forEach(function(fi) {
var this_time = fi.querySelector('time').textContent;
var this_data = fi.querySelector('value').textContent;
try {
this_data = parseFloat(this_data);
} catch (e) {
this_data = null;
}
result.time.push(this_time);
result.values.push(this_data);
});
if (callback !== undefined) {
callback(result);
}
}).bind(this));
oReq.addEventListener("error", (function(xhr) {
if (callback !== undefined) {
var result = {
time: [],
values: []
};
callback(result);
}
}).bind(this));
oReq.overrideMimeType('application/xml');
oReq.open("GET", url);
oReq.send();
},
});
L.timeDimension.layer.wms.timeseries = function(layer, options) {
return new L.TimeDimension.Layer.WMS.TimeSeries(layer, options);
};