var inherit = require('./inherit');
var feature = require('./feature');
var registry = require('./registry');
var util = require('./util');
/**
* Track feature specification.
*
* @typedef {geo.feature.spec} geo.trackFeature.spec
* @extends geo.feature.spec
* @property {geo.geoPosition|function} [position] Position of the data.
* Default is (data).
* @property {number|function} [time] Time of the data. Default is `(data).t`.
* @property {object|function} [track] Tracks from the data. Default is
* (data). Typically, the data is an array of tracks, each of which is an
* array of points, each of which has a position and time. The position and
* time functions are called for each point as `position(trackPoint,
* pointIndex, trackEntry, trackEntryIndex)`.
* @property {number|null} [startTime=null] Start time. Used for styling. If
* `null`, this is the duration before the end time if `duration` is not
* `null` and the minimum time in any track if `duration` is `null`.
* @property {number} [endTime=null] End time. Used for styling and position
* of the track head. If `null` and either of `startTime` or `duration` are
* `null`, this is the maximum time in any track.
* @property {number} [duration=null] Duration between start and end times.
* Ignored if both start and end times are specified.
* @property {number|function} [text] Text to use for the head of the track.
* If specified, the track head is rendered as text. If `undefined` a marker
* is used instead. If `null` or an empty string (`''`), neither a marker
* nor text is used.
* @property {geo.trackFeature.styleSpec} [style] Style object with default
* style options.
* @property {geo.lineFeature.styleSpec} [pastStyle] Style object with
* style options for the track before the start time.
* @property {geo.lineFeature.styleSpec} [currentStyle] Style object with
* style options for the track between the start and end time.
* @property {geo.lineFeature.styleSpec} [futureStyle] Style object with
* style options for the track after the end time.
* @property {geo.markerFeature.styleSpec} [markerStyle] Style object with
* style options for the track head marker.
* @property {geo.textFeature.styleSpec} [textStyle] Style object with style
* options for the track head text.
*/
/**
* Style specification for a track feature. Extends
* {@link geo.lineFeasture.styleSpec}.
*
* @typedef {geo.feature.styleSpec} geo.trackFeature.styleSpec
* @extends geo.feature.styleSpec
* @extends geo.lineFeature.styleSpec
*/
/**
* Create a new instance of class trackFeature.
*
* @class
* @alias geo.trackFeature
* @extends geo.feature
* @param {geo.trackFeature.spec} arg
* @returns {geo.trackFeature}
*/
var trackFeature = function (arg) {
'use strict';
if (!(this instanceof trackFeature)) {
return new trackFeature(arg);
}
var transform = require('./transform');
arg = arg || {};
feature.call(this, arg);
/**
* @private
*/
var m_this = this,
m_styles = {},
m_tracks = {
// user specified
startTime: arg.startTime !== undefined ? arg.startTime : null,
endTime: arg.endTime !== undefined ? arg.endTime : null,
duration: arg.duration !== undefined ? arg.duration : null,
// internal
start: 0,
end: 0
},
m_lineFeatures,
m_markerLayer,
m_markerFeature,
m_textLayer,
m_textFeature,
s_draw = this.draw,
s_exit = this._exit,
s_init = this._init,
s_modified = this.modified,
s_style = this.style,
s_update = this._update;
this.featureType = 'track';
/**
* Return a function for position of a dependent line feature.
*
* @param {string} key One of `past`, `current` or `future`.
* @returns {function} The position function.
*/
this._linePosition = function (key) {
return function (d, i, l, j) {
var time = m_tracks.timeFunc(d, i, l, j);
if ((key === 'past' && time >= m_tracks.start) ||
(key === 'current' && time < m_tracks.start)) {
return m_tracks.startPosition[j];
}
if ((key === 'current' && time > m_tracks.end) ||
(key === 'future' && time < m_tracks.end)) {
return m_tracks.endPosition[j];
}
return m_tracks.positionFunc(d, i, l, j);
};
};
/**
* Return the position for the head of the track.
*
* @param {object} d The data object.
* @param {number} i The data idex.
* @returns {geo.geoPosition} The position.
*/
this._headPosition = function (d, i) {
return m_tracks.endPosition[i];
};
/**
* Return the text associated with a track.
*
* @param {object} d The data object.
* @param {number} i The data idex.
* @returns {string|undefined} The text.
*/
this._headText = function (d, i) {
return m_tracks.text[i];
};
/**
* Based on the user-specified start time, end time, and duration, and the
* maximum and minimum track times, compute the functional track start and
* end times.
*/
this._updateTimeRange = function () {
if (m_tracks.endTime !== null || (m_tracks.endTime === null && (m_tracks.startTime === null || m_tracks.duration == null))) {
m_tracks.end = m_tracks.endTime !== null ? m_tracks.endTime : m_tracks.timeExtents.end;
if (m_tracks.startTime !== null && (m_tracks.endTime === null || m_tracks.startTime <= m_tracks.endTime)) {
m_tracks.start = m_tracks.startTime;
} else if (m_tracks.duration !== null) {
m_tracks.start = m_tracks.end - m_tracks.duration;
} else {
m_tracks.start = m_tracks.timeExtents.start;
}
} else {
m_tracks.start = m_tracks.startTime;
m_tracks.end = m_tracks.start + m_tracks.duration;
}
};
/**
* Calculate an interpolated position given a time. If the time is outside
* the range of a track, the first or last point is returned.
*
* @param {number} time The time to compute a position array for.
* @param {string|geo.transform|null} [gcs] `undefined` to use the feature
* gcs, `null` to use the map gcs, or any other transform. This transform
* is used for the interpolation; the results are still in feature gcs.
* @param {boolean} [calcAngle] If truthy, also calculate the angle.
* @returns {geo.geoPosition[]} An array of positions, one per track. If the
* angle is computed, these position objects are supplemented with an
* `angle` key in radians.
*/
this.calculateTimePosition = function (time, gcs, calcAngle) {
if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp()) {
m_this._build();
}
gcs = (gcs === null ? m_this.layer().map().gcs() : (gcs === undefined ? m_this.gcs() : gcs));
var trans = transform({source: m_this.gcs(), target: gcs});
var data = m_this.data();
var tracks = [];
var positions = data.map((d, i) => {
var track = m_tracks.trackFunc(d, i);
tracks.push(track);
if (!track.length) {
return {x: 0, y: 0, z: 0, posidx: -1};
}
var lowidx = 0, lowt, highidx = track.length - 1, hight, testidx, testt;
if (track.length === 1) {
return {posidx: lowidx, angidx0: lowidx, angidx1: lowidx};
}
lowt = m_tracks.timeFunc(track[lowidx], lowidx, d, i);
if (lowt >= time) {
return {posidx: lowidx, angidx0: lowidx, angidx1: lowidx + 1};
}
hight = m_tracks.timeFunc(track[highidx], highidx, d, i);
if (hight <= time) {
return {posidx: highidx, angidx0: highidx - 1, angidx1: highidx};
}
while (highidx - lowidx > 1) {
testidx = Math.floor((highidx + lowidx) / 2);
testt = m_tracks.timeFunc(track[testidx], testidx, d, i);
if (testt === time) {
return {posidx: testidx, angidx0: testidx - 1, angidx1: testidx + 1};
}
if (testt < time) {
lowt = testt;
lowidx = testidx;
} else {
hight = testt;
highidx = testidx;
}
}
var fh = (time - lowt) / (hight - lowt), fl = 1 - fh;
return {posidx0: lowidx, posidx1: highidx, factor0: fl, factor1: fh, angidx0: lowidx, angidx1: highidx};
});
positions.forEach((d, i) => {
if (d.posidx < 0) {
return;
}
var pos, pos0, pos1;
if (d.posidx1 === undefined) {
pos = m_tracks.positionFunc(tracks[i][d.posidx], d.posidx, tracks[i], i);
} else {
pos0 = trans.forward(m_tracks.positionFunc(tracks[i][d.posidx0], d.posidx0, tracks[i], i), true);
pos1 = trans.forward(m_tracks.positionFunc(tracks[i][d.posidx1], d.posidx1, tracks[i], i), true);
pos = trans.inverse({
x: pos0.x * d.factor0 + pos1.x * d.factor1,
y: pos0.y * d.factor0 + pos1.y * d.factor1,
z: (pos0.z || 0) * d.factor0 + (pos1.z || 0) * d.factor1
}, true);
}
d.x = pos.x;
d.y = pos.y;
d.z = pos.z || 0;
if (calcAngle) {
if (d.posidx1 === undefined) {
pos0 = trans.forward(d.angidx0 === d.posidx ? pos : m_tracks.positionFunc(tracks[i][d.angidx0], d.angidx0, tracks[i], i), true);
pos1 = trans.forward(d.angidx1 === d.posidx ? pos : m_tracks.positionFunc(tracks[i][d.angidx1], d.angidx1, tracks[i], i), true);
}
d.angle = Math.atan2(pos1.y - pos0.y, pos1.x - pos0.x);
}
});
return positions;
};
/**
* Build. Generate the tracks. Create sub-features if necessary and
* update it.
*
* @returns {this}
*/
this._build = function () {
m_this.buildTime().modified();
if (!m_lineFeatures) {
/* This determines the z-order of the time segments */
m_lineFeatures = {
past: m_this.layer().createFeature('line'),
future: m_this.layer().createFeature('line'),
current: m_this.layer().createFeature('line')
};
m_this.dependentFeatures([m_lineFeatures.past, m_lineFeatures.current, m_lineFeatures.future]);
}
var data = m_this.data();
m_tracks.data = data;
m_tracks.timeFunc = m_this.style.get('time');
m_tracks.positionFunc = m_this.style.get('position');
m_tracks.trackFunc = m_this.style.get('track');
m_tracks.textFunc = m_this.style.get('text');
['past', 'current', 'future'].forEach(key => {
m_lineFeatures[key]
.style(m_this.style())
.style(m_this[key + 'Style']())
.line(m_this.style('track'))
.gcs(m_this.gcs())
.data(data)
.position(m_this._linePosition(key));
});
var timeExtents = {};
data.forEach((d, i) => {
var track = m_tracks.trackFunc(d, i);
var time;
if (track.length) {
time = m_tracks.timeFunc(track[0], 0, d, i);
if (timeExtents.start === undefined || time < timeExtents.start) {
timeExtents.start = time;
}
if (track.length > 1) {
time = m_tracks.timeFunc(track[track.length - 1], track.length - 1, d, i);
}
if (timeExtents.end === undefined || time > timeExtents.end) {
timeExtents.end = time;
}
}
});
m_tracks.timeExtents = timeExtents;
m_this._updateTimeRange();
m_tracks.startPosition = m_this.calculateTimePosition(m_tracks.start, null);
m_tracks.endPosition = m_this.calculateTimePosition(m_tracks.end, null, true);
var hasMarker, hasText;
m_tracks.text = data.map((d, i) => {
var val = m_tracks.textFunc(d, i);
hasMarker |= (val === undefined || val === null);
if (val === undefined || val === null || val === '') {
return '';
}
hasText = true;
if (m_tracks.startPosition[i].posidx < 0) {
val = '';
}
return val;
});
if (hasMarker && !m_markerFeature) {
if (!(registry.registries.features[m_this.layer().rendererName()] || {}).marker) {
const renderer = registry.rendererForFeatures(['marker']);
m_markerLayer = registry.createLayer('feature', m_this.layer().map(), {renderer: renderer});
m_this.layer().addChild(m_markerLayer);
m_this.layer().node().append(m_markerLayer.node());
}
m_markerFeature = (m_markerLayer || m_this.layer()).createFeature('marker');
const df = m_this.dependentFeatures();
df.push(m_markerFeature);
m_this.dependentFeatures(df);
}
if (hasText && !m_textFeature) {
if (!(registry.registries.features[m_this.layer().rendererName()] || {}).text) {
const renderer = registry.rendererForFeatures(['text']);
m_textLayer = registry.createLayer('feature', m_this.layer().map(), {renderer: renderer});
m_this.layer().addChild(m_textLayer);
m_this.layer().node().append(m_textLayer.node());
}
m_textFeature = (m_textLayer || m_this.layer()).createFeature('text');
const df = m_this.dependentFeatures();
df.push(m_textFeature);
m_this.dependentFeatures(df);
}
if (m_markerFeature) {
m_markerFeature.headData = m_tracks.endPosition;
m_markerFeature
.style(m_this.markerStyle())
.gcs(m_this.gcs())
.data(data)
.position(m_this._headPosition);
const radiusFunc = m_markerFeature.style.get('radius');
m_markerFeature.style('radius', (d, i) => {
if (m_tracks.text[i] || m_tracks.startPosition[i].posidx < 0) {
return 0;
}
return radiusFunc(d, i);
});
}
if (m_textFeature) {
m_textFeature.headData = m_tracks.endPosition;
m_textFeature
.style(m_this.textStyle())
.gcs(m_this.gcs())
.data(data)
.text(m_this._headText)
.position(m_this._headPosition);
}
return m_this;
};
/**
* Update the time and position and mark features as modified.
*/
this._updateTimeAndPosition = function () {
if (!m_lineFeatures) {
return;
}
m_this._updateTimeRange();
m_tracks.startPosition = m_this.calculateTimePosition(m_tracks.start, null);
m_tracks.endPosition = m_this.calculateTimePosition(m_tracks.end, null, true);
m_lineFeatures.past.modified();
m_lineFeatures.current.modified();
m_lineFeatures.future.modified();
if (m_markerFeature) {
m_markerFeature.modified();
}
if (m_textFeature) {
m_textFeature.modified();
}
m_this.updateTime().modified();
m_this.modified();
};
/**
* Update. Rebuild if necessary.
*
* @returns {this}
*/
this._update = function () {
s_update.call(m_this);
if (m_this.dataTime().timestamp() >= m_this.buildTime().timestamp() ||
m_this.updateTime().timestamp() <= m_this.timestamp()) {
m_this._build();
}
m_this.updateTime().modified();
return m_this;
};
/**
* Redraw the object.
*
* @returns {object} The results of the superclass draw function.
*/
this.draw = function () {
var result = s_draw();
if (m_lineFeatures) {
m_lineFeatures.past.draw();
m_lineFeatures.current.draw();
m_lineFeatures.future.draw();
}
if (m_markerFeature) {
m_markerFeature.draw();
}
if (m_textFeature) {
m_textFeature.draw();
}
return result;
};
/**
* Update the timestamp to the next global timestamp value. Mark
* sub-features as modified, too.
*
* @returns {object} The results of the superclass modified function.
*/
this.modified = function () {
var result = s_modified();
if (m_lineFeatures) {
m_lineFeatures.past.modified();
m_lineFeatures.current.modified();
m_lineFeatures.future.modified();
}
if (m_markerFeature) {
m_markerFeature.modified();
}
if (m_textFeature) {
m_textFeature.modified();
}
return result;
};
/**
* Get or set style.
*
* @param {string|object} [arg1] If `undefined`, return the current style
* object. If a string and `arg2` is undefined, return the style
* associated with the specified key. If a string and `arg2` is defined,
* set the named style to the specified value. Otherwise, extend the
* current style with the values in the specified object.
* @param {*} [arg2] If `arg1` is a string, the new value for that style.
* @param {string} [styleType='style'] The name of the style type, such as
* `markerStyle`, `textStyle`, `pastStyle`, `currentStyle`, or
* `futureStyle`.
* @returns {object|this} Either the entire style object, the value of a
* specific style, or the current class instance.
*/
this.style = function (arg1, arg2, styleType) {
styleType = styleType || 'style';
if (styleType === 'style') {
return s_style(arg1, arg2);
}
if (arg1 === undefined) {
return m_styles[styleType];
}
if (typeof arg1 === 'string' && arg2 === undefined) {
return (m_styles[styleType] || {})[arg1];
}
if (m_styles[styleType] === undefined) {
m_styles[styleType] = {};
}
if (arg2 === undefined) {
m_styles[styleType] = util.deepMerge(m_styles[styleType], arg1);
} else {
m_styles[styleType][arg1] = arg2;
}
m_this.modified();
return m_this;
};
this.style.get = s_style.get;
/**
* Calls {@link geo.annotation#style} with `styleType='markerStyle'`.
* @function markerStyle
* @memberof geo.trackFeature
* @instance
*/
/**
* Calls {@link geo.annotation#style} with `styleType='textStyle'`.
* @function textStyle
* @memberof geo.trackFeature
* @instance
*/
/**
* Calls {@link geo.annotation#style} with `styleType='pastStyle'`.
* @function pastStyle
* @memberof geo.trackFeature
* @instance
*/
/**
* Calls {@link geo.annotation#style} with `styleType='currentStyle'`.
* @function currentStyle
* @memberof geo.trackFeature
* @instance
*/
/**
* Calls {@link geo.annotation#style} with `styleType='futureStyle'`.
* @function futureStyle
* @memberof geo.trackFeature
* @instance
*/
['markerStyle', 'textStyle', 'pastStyle', 'currentStyle', 'futureStyle'
].forEach(function (styleType) {
m_this[styleType] = function (arg1, arg2) {
return m_this.style(arg1, arg2, styleType);
};
});
/**
* Get/set track accessor.
*
* @param {object|function} [val] If not specified, return the current track
* accessor. If specified, use this for the track accessor and return
* `this`. If a function is given, the function is passed `(dataElement,
* dataIndex)` and returns an array of vertex elements.
* @returns {object|function|this} The current track accessor or this feature.
*/
this.track = function (val) {
if (val === undefined) {
return m_this.style('track');
} else {
m_this.style('track', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set position accessor.
*
* @param {geo.geoPosition|function} [val] If not specified, return the
* current position accessor. If specified, use this for the position
* accessor and return `this`. If a function is given, this is called
* with `(vertexElement, vertexIndex, dataElement, dataIndex)`.
* @returns {geo.geoPosition|function|this} The current position or this
* feature.
*/
this.position = function (val) {
if (val === undefined) {
return m_this.style('position');
} else {
m_this.style('position', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Get/Set time accessor.
*
* @param {number} [val] If not specified, return the current time accessor.
* If specified, use this for the time accessor and return `this`. If a
* function is given, this is called with `(vertexElement, vertexIndex,
* dataElement, dataIndex)`.
* @returns {number|function|this} The current time or this feature.
*/
this.time = function (val) {
if (val === undefined) {
return m_this.style('time');
} else {
m_this.style('time', val);
m_this.dataTime().modified();
m_this.modified();
}
return m_this;
};
/**
* Set or query the time range for the tracks. Tracks are rendered
* differently before the start time and after the end time. The track's
* marker or text is rendered at the position corresponding to the end time.
*
* @param {object} [val] An object with any of `startTime`, `endTime`, and
* `duration`. A value of `undefined` won't change that field. A value
* of `null` uses the default. If `val` is `undefined`, the existing
* settings are returned.
* @returns {object|this} Either the instance or the current settings. If
* the current settings, `start` and `end` are included with the
* calculated start and end times, and `minimum` and `maximum` are values
* computed from the data.
*/
this.timeRange = function (val) {
if (val === undefined) {
return {
startTime: m_tracks.startTime,
endTime: m_tracks.endTime,
duration: m_tracks.duration,
start: m_tracks.start,
end: m_tracks.end,
minimum: (m_tracks.timeExtents || {}).start,
maximum: (m_tracks.timeExtents || {}).end
};
}
let update = false;
if (val.startTime !== undefined && val.startTime !== m_tracks.startTime) {
m_tracks.startTime = val.startTime === null ? val.startTime : +val.startTime;
update = true;
}
if (val.endTime !== undefined && val.endTime !== m_tracks.endTime) {
m_tracks.endTime = val.endTime === null ? val.endTime : +val.endTime;
update = true;
}
if (val.duration !== undefined && val.duration !== m_tracks.duration) {
m_tracks.duration = val.duration === null ? val.duration : +val.duration;
update = true;
}
if (update) {
m_this._updateTimeAndPosition();
}
return m_this;
};
/**
* Get or set the start time.
*
* @param {number|null} [val] If specified, the new start time.
* @returns {number|null|this} If set, the instance. Otherwise, the current
* start time value.
*/
this.startTime = function (val) {
if (val === undefined) {
return m_tracks.startTime;
}
if (val !== m_tracks.startTime) {
m_tracks.startTime = val === null ? val : +val;
m_this._updateTimeAndPosition();
}
return m_this;
};
/**
* Get or set the end time.
*
* @param {number|null} [val] If specified, the new end time.
* @returns {number|null|this} If set, the instance. Otherwise, the current
* end time value.
*/
this.endTime = function (val) {
if (val === undefined) {
return m_tracks.endTime;
}
if (val !== m_tracks.endTime) {
m_tracks.endTime = val === null ? val : +val;
m_this._updateTimeAndPosition();
}
return m_this;
};
/**
* Get or set the duration.
*
* @param {number|null} [val] If specified, the new duration.
* @returns {number|null|this} If set, the instance. Otherwise, the current
* duration.
*/
this.duration = function (val) {
if (val === undefined) {
return m_tracks.duration;
}
if (val !== m_tracks.duration) {
m_tracks.duration = val === null ? val : +val;
m_this._updateTimeAndPosition();
}
return m_this;
};
/**
* Merge search results from multiple features.
*
* @param {object} result The result from the base feature.
* @param {object[]} additional A list of additional feature search results.
* Each entry has `key`, the name of the feature, and `value`, the value
* of the search results.
* @returns {object} The combined search results.
*/
this._mergeSearchResults = function (result, additional) {
result.extra = result.extra || {};
additional.forEach(add => add.value.index.forEach((index, i) => {
if (result.index.indexOf(index) < 0) {
result.index.push(index);
result.found.push(add.value.found[i]);
result.where = result.where || {};
if (add.value.extra && add.value.extra[index]) {
result.extra[index] = add.value.extra[index];
}
if (!util.isObject(result.extra[index])) {
result.extra[index] = {value: result.extra[index]};
}
result.extra[index].where = add.key;
}
}));
return result;
};
/**
* Returns an array of datum indices that contain the given point.
*
* @param {geo.geoPosition} p point to search for in map interface gcs.
* @returns {object} An object with `index`: a list of track indices, `found`:
* a list of tracks that contain the specified coordinate, `extra`: an
* object with keys that are track indices and values that are the first
* segment index for which the track was matched, and `where`: an object
* with keys that are track indices and values that are `past`, `future`,
* or `marker` if the point was found in that part of the track, or unset
* if the point was found in the current part of the track.
*/
this.pointSearch = function (p) {
const result = m_lineFeatures.current.pointSearch(p),
past = m_lineFeatures.past.pointSearch(p),
future = m_lineFeatures.future.pointSearch(p),
marker = m_markerFeature ? m_markerFeature.pointSearch(p) : {index: []};
return this._mergeSearchResults(result, [
{key: 'marker', value: marker},
{key: 'past', value: past},
{key: 'future', value: future}]);
};
/**
* Returns tracks that are contained in the given polygon.
*
* @param {geo.polygonObject} poly A polygon as an array of coordinates or an
* object with `outer` and optionally `inner` parameters. All coordinates
* are in map interface gcs.
* @param {object} [opts] Additional search options.
* @param {boolean} [opts.partial=false] If truthy, include tracks that are
* partially in the polygon, otherwise only include tracks that are fully
* within the region.
* @returns {object} An object with `index`: a list of track indices,
* `found`: a list of tracks within the polygon, `extra`: an object with
* index keys containing an object with a `segment` key with a value
* indicating one of the track segments that is inside the polygon and
* `partial` key and a boolean value to indicate if the track is on the
* polygon's border, and `where`: an object with keys that are track
* indices and values that are `past`, `future`, or `marker` if the point
* was found in that part of the track, or unset if the point was found in
* the current part of the track.
*/
this.polygonSearch = function (poly, opts) {
const result = m_lineFeatures.current.polygonSearch(poly, opts),
past = m_lineFeatures.past.polygonSearch(poly, opts),
future = m_lineFeatures.future.polygonSearch(poly, opts),
marker = m_markerFeature ? m_markerFeature.polygonSearch(poly, opts) : {index: []};
return this._mergeSearchResults(result, [
{key: 'marker', value: marker},
{key: 'past', value: past},
{key: 'future', value: future}]);
};
/**
* Destroy.
*/
this._exit = function () {
if (m_lineFeatures && m_this.layer()) {
m_this.layer().deleteFeature(m_lineFeatures.past);
m_this.layer().deleteFeature(m_lineFeatures.current);
m_this.layer().deleteFeature(m_lineFeatures.future);
}
m_lineFeatures = null;
if (m_markerLayer || m_this.layer()) {
if (m_markerFeature) {
(m_markerLayer || m_this.layer()).deleteFeature(m_markerFeature);
}
}
if (m_markerLayer && m_this.layer()) {
m_this.layer().removeChild(m_markerLayer);
}
m_markerLayer = null;
if (m_textLayer || m_this.layer()) {
if (m_textFeature) {
(m_textLayer || m_this.layer()).deleteFeature(m_textFeature);
}
}
if (m_textLayer && m_this.layer()) {
m_this.layer().removeChild(m_textLayer);
}
m_textLayer = null;
m_this.dependentFeatures([]);
s_exit();
};
/**
* Initialize.
*
* @param {geo.trackFeature.spec} arg The track feature specification.
*/
this._init = function (arg) {
arg = arg || {};
s_init.call(m_this, arg);
var style = util.deepMerge(
{},
{
track: util.identityFunction,
position: util.identityFunction,
time: (d, i) => (d.t !== undefined ? d.t : i),
uniformLine: 'drop',
closed: false
},
arg.style === undefined ? {} : arg.style
);
var markerStyle = util.deepMerge(
{},
{
rotateWithMap: true,
rotation: (d, i) => -m_tracks.endPosition[i].angle || 0
},
arg.markerStyle === undefined ? {} : arg.markerStyle
);
var textStyle = util.deepMerge(
{},
{
rotateWithMap: true,
rotation: (d, i) => m_tracks.endPosition[i].angle !== undefined ? -m_tracks.endPosition[i].angle + Math.PI / 2 : 0
},
arg.textStyle === undefined ? {} : arg.textStyle
);
var pastStyle = util.deepMerge(
{},
{
strokeOpacity: 0.25
},
arg.pastStyle === undefined ? {} : arg.pastStyle
);
var currentStyle = util.deepMerge(
{},
{
// defaults go here
},
arg.currentStyle === undefined ? {} : arg.currentStyle
);
var futureStyle = util.deepMerge(
{},
{
strokeOpacity: 0.25
},
arg.futureStyle === undefined ? {} : arg.futureStyle
);
['track', 'position', 'time'].forEach((key) => {
if (arg[key] !== undefined) {
style[key] = arg[key];
}
});
m_this.style(style);
m_this.markerStyle(markerStyle);
m_this.textStyle(textStyle);
m_this.pastStyle(pastStyle);
m_this.currentStyle(currentStyle);
m_this.futureStyle(futureStyle);
};
return this;
};
inherit(trackFeature, feature);
module.exports = trackFeature;