/*eslint no-constant-condition: ["error", { "checkLoops": false }]*/
import ol_source_Vector from 'ol/source/Vector.js'
import ol_control_Control from 'ol/control/Control.js'
import ol_ext_element from '../util/element.js'
/** Timeline control
*
* @constructor
* @extends {ol.control.Control}
* @fires select
* @fires scroll
* @fires collapse
* @param {Object=} options Control options.
* @param {String} options.className class of the control
* @param {Array
} options.features Features to show in the timeline
* @param {ol.SourceImageOptions.vector} options.source class of the control
* @param {Number} options.interval time interval length in ms or a text with a format d, h, mn, s (31 days = '31d'), default none
* @param {String} options.maxWidth width of the time line in px, default 2000px
* @param {String} options.minDate minimum date
* @param {String} options.maxDate maximum date
* @param {Number} options.minZoom Minimum zoom for the line, default .2
* @param {Number} options.maxZoom Maximum zoom for the line, default 4
* @param {boolean} options.zoomButton Are zoom buttons avaliable, default false
* @param {function} options.getHTML a function that takes a feature and returns the html to display
* @param {function} options.getFeatureDate a function that takes a feature and returns its date, default the date propertie
* @param {function} options.endFeatureDate a function that takes a feature and returns its end date, default no end date
* @param {String} options.graduation day|month to show month or day graduation, default show only years
* @param {String} options.scrollTimeout Time in milliseconds to get a scroll event, default 15ms
*/
var ol_control_Timeline = class olcontrolTimeline extends ol_control_Control {
constructor(options) {
var element = ol_ext_element.create('DIV', {
className: (options.className || '') + ' ol-timeline'
+ (options.target ? '' : ' ol-unselectable ol-control')
+ (options.zoomButton ? ' ol-hasbutton' : '')
});
// Initialize
super({
element: element,
target: options.target
});
// Scroll div
this._scrollDiv = ol_ext_element.create('DIV', {
className: 'ol-scroll',
parent: this.element
});
// Add a button bar
this._buttons = ol_ext_element.create('DIV', {
className: 'ol-buttons',
parent: this.element
});
// Zoom buttons
if (options.zoomButton) {
// Zoom in
this.addButton({
className: 'ol-zoom-in',
handleClick: function () {
var zoom = this.get('zoom');
if (zoom >= 1) {
zoom++;
} else {
zoom = Math.min(1, zoom + 0.1);
}
zoom = Math.round(zoom * 100) / 100;
this.refresh(zoom);
}.bind(this)
});
// Zoom out
this.addButton({
className: 'ol-zoom-out',
handleClick: function () {
var zoom = this.get('zoom');
if (zoom > 1) {
zoom--;
} else {
zoom -= 0.1;
}
zoom = Math.round(zoom * 100) / 100;
this.refresh(zoom);
}.bind(this)
});
}
// Draw center date
this._intervalDiv = ol_ext_element.create('DIV', {
className: 'ol-center-date',
parent: this.element
});
// Remove selection
this.element.addEventListener('mouseover', function () {
if (this._select)
this._select.elt.classList.remove('ol-select');
}.bind(this));
// Trigger scroll event
var scrollListener = null;
this._scrollDiv.addEventListener('scroll', function () {
this._setScrollLeft();
if (scrollListener) {
clearTimeout(scrollListener);
scrollListener = null;
}
scrollListener = setTimeout(function () {
this.dispatchEvent({
type: 'scroll',
date: this.getDate(),
dateStart: this.getDate('start'),
dateEnd: this.getDate('end')
});
}.bind(this), options.scrollTimeout || 15);
}.bind(this));
// Magic to give "live" scroll events on touch devices
// this._scrollDiv.addEventListener('gesturechange', function() {});
// Scroll timeline
ol_ext_element.scrollDiv(this._scrollDiv, {
onmove: function (b) {
// Prevent selection on moving
this._moving = b;
}.bind(this)
});
this._tline = [];
// Parameters
this._scrollLeft = 0;
this.set('maxWidth', options.maxWidth || 2000);
this.set('minDate', options.minDate || Infinity);
this.set('maxDate', options.maxDate || -Infinity);
this.set('graduation', options.graduation);
this.set('minZoom', options.minZoom || .2);
this.set('maxZoom', options.maxZoom || 4);
this.setInterval(options.interval);
if (options.getHTML)
this._getHTML = options.getHTML;
if (options.getFeatureDate)
this._getFeatureDate = options.getFeatureDate;
if (options.endFeatureDate)
this._endFeatureDate = options.endFeatureDate;
// Feature source
this.setFeatures(options.features || options.source, options.zoom);
}
/**
* Set the map instance the control is associated with
* and add interaction attached to it to this map.
* @param {_ol_Map_} map The map instance.
*/
setMap(map) {
super.setMap(map);
this.refresh(this.get('zoom') || 1, true);
}
/** Add a button on the timeline
* @param {*} button
* @param {string} button.className
* @param {title} button.className
* @param {Element|string} button.html Content of the element
* @param {function} button.click a function called when the button is clicked
*/
addButton(button) {
this.element.classList.add('ol-hasbutton');
ol_ext_element.create('BUTTON', {
className: button.className || undefined,
title: button.title,
html: button.html,
click: button.handleClick,
parent: this._buttons
});
}
/** Set an interval
* @param {number|string} length the interval length in ms or a farmatted text ie. end with y, 1d, h, mn, s (31 days = '31d'), default none
*/
setInterval(length) {
if (typeof (length) === 'string') {
if (/s$/.test(length)) {
length = parseFloat(length) * 1000;
} else if (/mn$/.test(length)) {
length = parseFloat(length) * 1000 * 60;
} else if (/h$/.test(length)) {
length = parseFloat(length) * 1000 * 3600;
} else if (/d$/.test(length)) {
length = parseFloat(length) * 1000 * 3600 * 24;
} else if (/y$/.test(length)) {
length = parseFloat(length) * 1000 * 3600 * 24 * 365;
} else {
length = 0;
}
}
this.set('interval', length || 0);
if (length)
this.element.classList.add('ol-interval');
else
this.element.classList.remove('ol-interval');
this.refresh(this.get('zoom'));
}
/** Default html to show in the line
* @param {ol.Feature} feature
* @return {DOMElement|string}
* @private
*/
_getHTML(feature) {
return feature.get('name') || '';
}
/** Default function to get the date of a feature, returns the date attribute
* @param {ol.Feature} feature
* @return {Data|string}
* @private
*/
_getFeatureDate(feature) {
return (feature && feature.get) ? feature.get('date') : null;
}
/** Default function to get the end date of a feature, return undefined
* @param {ol.Feature} feature
* @return {Data|string}
* @private
*/
_endFeatureDate( /* feature */) {
return undefined;
}
/** Is the line collapsed
* @return {boolean}
*/
isCollapsed() {
return this.element.classList.contains('ol-collapsed');
}
/** Collapse the line
* @param {boolean} b
*/
collapse(b) {
if (b)
this.element.classList.add('ol-collapsed');
else
this.element.classList.remove('ol-collapsed');
this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() });
}
/** Collapse the line
*/
toggle() {
this.element.classList.toggle('ol-collapsed');
this.dispatchEvent({ type: 'collapse', collapsed: this.isCollapsed() });
}
/** Set the features to display in the timeline
* @param {Array|ol.source.Vector} features An array of features or a vector source
* @param {number} zoom zoom to draw the line default 1
*/
setFeatures(features, zoom) {
this._features = this._source = null;
if (features instanceof ol_source_Vector)
this._source = features;
else if (features instanceof Array)
this._features = features;
else
this._features = [];
this.refresh(zoom);
}
/**
* Get features
* @return {Array}
*/
getFeatures() {
return this._features || this._source.getFeatures();
}
/**
* Refresh the timeline with new data
* @param {Number} zoom Zoom factor from 0.25 to 10, default 1
*/
refresh(zoom, first) {
if (!this.getMap())
return;
if (!zoom)
zoom = this.get('zoom');
zoom = Math.min(this.get('maxZoom'), Math.max(this.get('minZoom'), zoom || 1));
this.set('zoom', zoom);
this._scrollDiv.innerHTML = '';
var features = this.getFeatures();
var d, d2;
// Get features sorted by date
var tline = this._tline = [];
features.forEach(function (f) {
if (d = this._getFeatureDate(f)) {
if (!(d instanceof Date)) {
d = new Date(d);
}
if (this._endFeatureDate) {
d2 = this._endFeatureDate(f);
if (!(d2 instanceof Date)) {
d2 = new Date(d2);
}
}
if (!isNaN(d)) {
tline.push({
date: d,
end: isNaN(d2) ? null : d2,
feature: f
});
}
}
}.bind(this));
tline.sort(function (a, b) {
return (a.date < b.date ? -1 : (a.date === b.date ? 0 : 1));
});
// Draw
var div = ol_ext_element.create('DIV', {
parent: this._scrollDiv
});
// Calculate width
var min = this._minDate = Math.min(this.get('minDate'), tline.length ? tline[0].date : Infinity);
var max = this._maxDate = Math.max(this.get('maxDate'), tline.length ? tline[tline.length - 1].date : -Infinity);
if (!isFinite(min))
this._minDate = min = new Date();
if (!isFinite(max))
this._maxDate = max = new Date();
var delta = (max - min);
var maxWidth = this.get('maxWidth');
var scale = this._scale = (delta > maxWidth ? maxWidth / delta : 1) * zoom;
// Leave 10px on right
min = this._minDate = this._minDate - 10 / scale;
delta = (max - min) * scale;
ol_ext_element.setStyle(div, {
width: delta,
maxWidth: 'unset'
});
// Draw time's bar
this._drawTime(div, min, max, scale);
// Set interval
if (this.get('interval')) {
ol_ext_element.setStyle(this._intervalDiv, { width: this.get('interval') * scale });
} else {
ol_ext_element.setStyle(this._intervalDiv, { width: '' });
}
// Draw features
var line = [];
var lineHeight = ol_ext_element.getStyle(this._scrollDiv, 'lineHeight');
// Wrapper
var fdiv = ol_ext_element.create('DIV', {
className: 'ol-features',
parent: div
});
// Add features on the line
tline.forEach(function (f) {
var d = f.date;
var t = f.elt = ol_ext_element.create('DIV', {
className: 'ol-feature',
style: {
left: Math.round((d - min) * scale),
},
html: this._getHTML(f.feature),
parent: fdiv
});
// Prevent image dragging
var img = t.querySelectorAll('img');
for (var i = 0; i < img.length; i++) {
img[i].ondragstart = function () { return false; };
}
// Calculate image width
if (f.end) {
ol_ext_element.setStyle(t, {
minWidth: (f.end - d) * scale,
width: (f.end - d) * scale,
maxWidth: 'unset'
});
}
var left = ol_ext_element.getStyle(t, 'left');
// Select on click
t.addEventListener('click', function () {
if (!this._moving) {
this.dispatchEvent({ type: 'select', feature: f.feature });
}
}.bind(this));
// Find first free Y position
var pos, l;
for (pos = 0; l = line[pos]; pos++) {
if (left > l) {
break;
}
}
line[pos] = left + ol_ext_element.getStyle(t, 'width');
ol_ext_element.setStyle(t, { top: pos * lineHeight });
}.bind(this));
this._nbline = line.length;
if (first)
this.setDate(this._minDate, { anim: false, position: 'start' });
// Dispatch scroll event
this.dispatchEvent({
type: 'scroll',
date: this.getDate(),
dateStart: this.getDate('start'),
dateEnd: this.getDate('end')
});
}
/** Get offset given a date
* @param {Date} date
* @return {number}
* @private
*/
_getOffsetFromDate(date) {
return (date - this._minDate) * this._scale;
}
/** Get date given an offset
* @param {Date} date
* @return {number}
* @private
*/
_getDateFromOffset(offset) {
return offset / this._scale + this._minDate;
}
/** Set the current position
* @param {number} scrollLeft current position (undefined when scrolling)
* @returns {number}
* @private
*/
_setScrollLeft(scrollLeft) {
this._scrollLeft = scrollLeft;
if (scrollLeft !== undefined) {
this._scrollDiv.scrollLeft = scrollLeft;
}
}
/** Get the current position
* @returns {number}
* @private
*/
_getScrollLeft() {
// Unset when scrolling
if (this._scrollLeft === undefined) {
return this._scrollDiv.scrollLeft;
} else {
// St by user
return this._scrollLeft;
}
}
/**
* Draw dates on line
* @private
*/
_drawTime(div, min, max, scale) {
// Times div
var tdiv = ol_ext_element.create('DIV', {
className: 'ol-times',
parent: div
});
var d, dt, month, dmonth;
var dx = ol_ext_element.getStyle(tdiv, 'left');
var heigth = ol_ext_element.getStyle(tdiv, 'height');
// Year
var year = (new Date(this._minDate)).getFullYear();
dt = ((new Date(0)).setFullYear(String(year)) - new Date(0).setFullYear(String(year - 1))) * scale;
var dyear = Math.round(2 * heigth / dt) + 1;
while (true) {
d = new Date(0).setFullYear(year);
if (d > this._maxDate)
break;
ol_ext_element.create('DIV', {
className: 'ol-time ol-year',
style: {
left: this._getOffsetFromDate(d) - dx
},
html: year,
parent: tdiv
});
year += dyear;
}
// Month
if (/day|month/.test(this.get('graduation'))) {
dt = ((new Date(0, 0, 1)).setFullYear(String(year)) - new Date(0, 0, 1).setFullYear(String(year - 1))) * scale;
dmonth = Math.max(1, Math.round(12 / Math.round(dt / heigth / 2)));
if (dmonth < 12) {
year = (new Date(this._minDate)).getFullYear();
month = dmonth + 1;
while (true) {
d = new Date(0, 0, 1);
d.setFullYear(year);
d.setMonth(month - 1);
if (d > this._maxDate)
break;
ol_ext_element.create('DIV', {
className: 'ol-time ol-month',
style: {
left: this._getOffsetFromDate(d) - dx
},
html: d.toLocaleDateString(undefined, { month: 'short' }),
parent: tdiv
});
month += dmonth;
if (month > 12) {
year++;
month = dmonth + 1;
}
}
}
}
// Day
if (this.get('graduation') === 'day') {
dt = (new Date(0, 1, 1) - new Date(0, 0, 1)) * scale;
var dday = Math.max(1, Math.round(31 / Math.round(dt / heigth / 2)));
if (dday < 31) {
year = (new Date(this._minDate)).getFullYear();
month = 0;
var day = dday;
while (true) {
d = new Date(0, 0, 1);
d.setFullYear(year);
d.setMonth(month);
d.setDate(day);
if (isNaN(d)) {
month++;
if (month > 12) {
month = 1;
year++;
}
day = dday;
} else {
if (d > this._maxDate)
break;
if (day > 1) {
var offdate = this._getOffsetFromDate(d);
if (this._getOffsetFromDate(new Date(year, month + 1, 1)) - offdate > heigth) {
ol_ext_element.create('DIV', {
className: 'ol-time ol-day',
style: {
left: offdate - dx
},
html: day,
parent: tdiv
});
}
}
year = d.getFullYear();
month = d.getMonth();
day = d.getDate() + dday;
if (day > new Date(year, month + 1, 0).getDate()) {
month++;
day = dday;
}
}
}
}
}
}
/** Center timeline on a date
* @param {Date|String|ol.feature} feature a date or a feature with a date
* @param {Object} options
* @param {boolean} options.anim animate scroll
* @param {string} options.position start, end or middle, default middle
*/
setDate(feature, options) {
var date;
options = options || {};
// It's a date
if (feature instanceof Date) {
date = feature;
} else {
// Get date from Feature
if (this.getFeatures().indexOf(feature) >= 0) {
date = this._getFeatureDate(feature);
}
if (date && !(date instanceof Date)) {
date = new Date(date);
}
if (!date || isNaN(date)) {
date = new Date(String(feature));
}
}
if (!isNaN(date)) {
if (options.anim === false)
this._scrollDiv.classList.add('ol-move');
var scrollLeft = this._getOffsetFromDate(date);
if (options.position === 'start') {
scrollLeft += ol_ext_element.outerWidth(this._scrollDiv) / 2 - ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
} else if (options.position === 'end') {
scrollLeft -= ol_ext_element.outerWidth(this._scrollDiv) / 2 - ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
}
this._setScrollLeft(scrollLeft);
if (options.anim === false)
this._scrollDiv.classList.remove('ol-move');
if (feature) {
for (var i = 0, f; f = this._tline[i]; i++) {
if (f.feature === feature) {
f.elt.classList.add('ol-select');
this._select = f;
} else {
f.elt.classList.remove('ol-select');
}
}
}
}
}
/** Get round date (sticked to mn, hour day or month)
* @param {Date} d
* @param {string} stick sticking option to stick date to: 'mn', 'hour', 'day', 'month', default no stick
* @return {Date}
*/
roundDate(d, stick) {
switch (stick) {
case 'mn': {
return new Date(this._roundTo(d, 60 * 1000));
}
case 'hour': {
return new Date(this._roundTo(d, 60 * 60 * 1000));
}
case 'day': {
return new Date(this._roundTo(d, 24 * 60 * 60 * 1000));
}
case 'month': {
d = new Date(this._roundTo(d, 24 * 60 * 60 * 1000));
if (d.getDate() > 15) {
d = new Date(d.setMonth(d.getMonth() + 1));
}
d = d.setDate(1);
return new Date(d);
}
default: return new Date(d);
}
}
/** Get the date of the center
* @param {string} position position to get 'start', 'end' or 'middle', default middle
* @param {string} stick sticking option to stick date to: 'mn', 'hour', 'day', 'month', default no stick
* @return {Date}
*/
getDate(position, stick) {
var pos;
if (!stick)
stick = position;
switch (position) {
case 'start': {
if (this.get('interval')) {
pos = -ol_ext_element.getStyle(this._intervalDiv, 'width') / 2 + ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
} else {
pos = -ol_ext_element.outerWidth(this._scrollDiv) / 2 + ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
}
break;
}
case 'end': {
if (this.get('interval')) {
pos = ol_ext_element.getStyle(this._intervalDiv, 'width') / 2 - ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
} else {
pos = ol_ext_element.outerWidth(this._scrollDiv) / 2 - ol_ext_element.getStyle(this._scrollDiv, 'marginLeft') / 2;
}
break;
}
default: {
pos = 0;
break;
}
}
var d = this._getDateFromOffset(this._getScrollLeft() + pos);
d = this.roundDate(d, stick);
return new Date(d);
}
/** Round number to
* @param {number} d
* @param {number} r
* @return {number}
* @private
*/
_roundTo(d, r) {
return Math.round(d / r) * r;
}
/** Get the start date of the control
* @return {Date}
*/
getStartDate() {
return new Date(this.get('minDate'));
}
/** Get the end date of the control
* @return {Date}
*/
getEndDate() {
return new Date(this.get('maxDate'));
}
}
export default ol_control_Timeline