From 7720d9e0799fa4b3bec9b596e9bcc1702f90cc6d Mon Sep 17 00:00:00 2001 From: David Manthey Date: Fri, 27 Sep 2019 14:06:06 -0400 Subject: [PATCH] Add a track feature. The track feature is a set of lines each of which is composed of points with position and strictly monotonically increasing time. There are utility functions for setting a start and end time. Two points are interpolated along the track line: one each for the start and end times. A marker or text value can be rendered at the end time. Internally, the track is rendered as three line features; one for all the points before the start time, one for the start to the end time, and one for the end time onward. Additional a marker and/or text feature is used to render the track heads. --- CHANGELOG.md | 1 + examples/flights/main.js | 7 +- examples/flights/worker.js | 6 +- src/canvas/index.js | 3 +- src/canvas/trackFeature.js | 34 ++ src/index.js | 1 + src/lineFeature.js | 3 +- src/svg/index.js | 1 + src/svg/trackFeature.js | 34 ++ src/trackFeature.js | 882 +++++++++++++++++++++++++++++++++ src/webgl/index.js | 1 + src/webgl/trackFeature.js | 33 ++ tests/cases/trackFeature.js | 417 ++++++++++++++++ tutorials/tracks/index.pug | 167 +++++++ tutorials/tracks/thumb.jpg | Bin 0 -> 71689 bytes tutorials/tracks/tutorial.json | 8 + 16 files changed, 1590 insertions(+), 8 deletions(-) create mode 100644 src/canvas/trackFeature.js create mode 100644 src/svg/trackFeature.js create mode 100644 src/trackFeature.js create mode 100644 src/webgl/trackFeature.js create mode 100644 tests/cases/trackFeature.js create mode 100644 tutorials/tracks/index.pug create mode 100755 tutorials/tracks/thumb.jpg create mode 100644 tutorials/tracks/tutorial.json diff --git a/CHANGELOG.md b/CHANGELOG.md index f9f45075c5..4d6adc9ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added a marker feature (#1035) - The mapInteractor cancelOnMove option can now take a movement threshold (#1058) - GCS can now be specified in pointSearch, boxSearch, and polygonSearch (#1051) +- Added a track feature (#1040) ## Version 0.19.8 diff --git a/examples/flights/main.js b/examples/flights/main.js index 547bbe0434..d6d8dd5472 100644 --- a/examples/flights/main.js +++ b/examples/flights/main.js @@ -73,7 +73,6 @@ function draw(drawData) { } feature.data(data); } - // feature.draw(); map.scheduleAnimationFrame(feature.draw); } @@ -115,12 +114,12 @@ feature = layer.createFeature('line', {selectionAPI: canSelect}) .style({ strokeColor: function (d, i, l) { if (!hasScale) { - // d3 scales are slow + // d3 scales are slow; don't do: // return d3Scale(0); return scale[0]; } var val = (l.v[i] - ranges.v.min) / ranges.v.range; - // d3 scales are slow + // d3 scales are slow, don't do: // return d3Scale(val); return val < 0 ? scale[0] : val > 1 ? scale[d3ScaleParts] : scale[Math.round(d3ScaleParts * val)]; }, @@ -151,7 +150,7 @@ feature = layer.createFeature('line', {selectionAPI: canSelect}) d.hover = false; }); evt.data.hover = true; - console.log(evt.data.id); + console.log(evt.data.id, evt.data.callsign); this.modified(); feature.draw(); }); diff --git a/examples/flights/worker.js b/examples/flights/worker.js index cbf637e084..29f5b33806 100644 --- a/examples/flights/worker.js +++ b/examples/flights/worker.js @@ -104,7 +104,7 @@ onmessage = function (evt) { }); } // Process each aircraft state. - var id, state = {}, last; + var id, callsign, state = {}, last; data.states.forEach(function (record, idx) { // Only process the data if passes some basic checks. if (!record.length) { @@ -122,6 +122,7 @@ onmessage = function (evt) { return; } id = record[rr.icao24]; + callsign = record[rr.callsign].trim(); state.time = data.time; state.lon = record[rr.longitude]; state.lat = record[rr.latitude]; @@ -149,7 +150,7 @@ onmessage = function (evt) { } // Store data in arrays to reduce memory use. if (!flights[id]) { - flights[id] = {time: [], lon: [], lat: [], z: [], v: [], dir: [], id: id}; + flights[id] = {time: [], lon: [], lat: [], z: [], v: [], dir: [], id: id, callsign: callsign}; } ['time', 'lon', 'lat', 'z', 'v', 'dir'].forEach(function (key) { flights[id][key].push(state[key]); @@ -169,6 +170,7 @@ onmessage = function (evt) { z: [flights[id].z[0] - 1e-6, flights[id].z[0]], v: [flights[id].v[0] - 1e-6, flights[id].v[0]], dir: [flights[id].dir[0], flights[id].dir[0]], + callsign: flights[id].callsign, id: flights[id].id }); } diff --git a/src/canvas/index.js b/src/canvas/index.js index 747b8692db..88ec4afe34 100644 --- a/src/canvas/index.js +++ b/src/canvas/index.js @@ -9,5 +9,6 @@ module.exports = { pixelmapFeature: require('./pixelmapFeature'), quadFeature: require('./quadFeature'), textFeature: require('./textFeature'), - tileLayer: require('./tileLayer') + tileLayer: require('./tileLayer'), + trackFeature: require('./trackFeature') }; diff --git a/src/canvas/trackFeature.js b/src/canvas/trackFeature.js new file mode 100644 index 0000000000..11d67c3546 --- /dev/null +++ b/src/canvas/trackFeature.js @@ -0,0 +1,34 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var trackFeature = require('../trackFeature'); + +/** + * Create a new instance of class trackFeature. + * + * @class + * @alias geo.canvas.trackFeature + * @extends geo.trackFeature + * @param {geo.trackFeature.spec} arg + * @returns {geo.canvas.trackFeature} + */ +var canvas_trackFeature = function (arg) { + 'use strict'; + if (!(this instanceof canvas_trackFeature)) { + return new canvas_trackFeature(arg); + } + + arg = arg || {}; + trackFeature.call(this, arg); + + var object = require('./object'); + object.call(this); + + this._init(arg); + return this; +}; + +inherit(canvas_trackFeature, trackFeature); + +// Now register it +registerFeature('canvas', 'track', canvas_trackFeature); +module.exports = canvas_trackFeature; diff --git a/src/index.js b/src/index.js index 61a235b00e..b8338432e0 100644 --- a/src/index.js +++ b/src/index.js @@ -75,6 +75,7 @@ module.exports = $.extend({ tileCache: require('./tileCache'), tileLayer: require('./tileLayer'), timestamp: require('./timestamp'), + trackFeature: require('./trackFeature'), transform: require('./transform'), typedef: require('./typedef'), vectorFeature: require('./vectorFeature'), diff --git a/src/lineFeature.js b/src/lineFeature.js index 556f18fdff..82de7aeeed 100644 --- a/src/lineFeature.js +++ b/src/lineFeature.js @@ -316,7 +316,8 @@ var lineFeature = function (arg) { i, j, record, u, v, r; for (i = 0; i < m_pointSearchInfo.length; i += 1) { record = m_pointSearchInfo[i]; - if (record.max.x < min.x - record.max.r * scale || + if (!record.max || + record.max.x < min.x - record.max.r * scale || record.min.x > max.x + record.max.r * scale || record.max.y < min.y - record.max.r * scale || record.min.y > max.y + record.max.r * scale) { diff --git a/src/svg/index.js b/src/svg/index.js index 5e4cd7c553..d08564f291 100644 --- a/src/svg/index.js +++ b/src/svg/index.js @@ -15,6 +15,7 @@ module.exports = { quadFeature: require('./quadFeature'), renderer: require('./svgRenderer'), tileLayer: require('./tileLayer'), + trackFeature: require('./trackFeature'), uniqueID: require('./uniqueID'), vectorFeature: require('./vectorFeature') }; diff --git a/src/svg/trackFeature.js b/src/svg/trackFeature.js new file mode 100644 index 0000000000..3a77b21311 --- /dev/null +++ b/src/svg/trackFeature.js @@ -0,0 +1,34 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var trackFeature = require('../trackFeature'); + +/** + * Create a new instance of class trackFeature. + * + * @class + * @alias geo.svg.trackFeature + * @extends geo.trackFeature + * @param {geo.trackFeature.spec} arg + * @returns {geo.svg.trackFeature} + */ +var svg_trackFeature = function (arg) { + 'use strict'; + if (!(this instanceof svg_trackFeature)) { + return new svg_trackFeature(arg); + } + + arg = arg || {}; + trackFeature.call(this, arg); + + var object = require('./object'); + object.call(this); + + this._init(arg); + return this; +}; + +inherit(svg_trackFeature, trackFeature); + +// Now register it +registerFeature('svg', 'track', svg_trackFeature); +module.exports = svg_trackFeature; diff --git a/src/trackFeature.js b/src/trackFeature.js new file mode 100644 index 0000000000..bb9dc9b949 --- /dev/null +++ b/src/trackFeature.js @@ -0,0 +1,882 @@ +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 + * @property {geo.geoPosition|function} [position] Position of the data. + * Default is (data). + * @property {float|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 {float|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 {float} [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 {float} [duration=null] Duration between start and end times. + * Ignored if both start and end times are specified. + * @property {float|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 $ = require('jquery'); + 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 {float} 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)); + pos1 = trans.forward(m_tracks.positionFunc(tracks[i][d.posidx1], d.posidx1, tracks[i], i)); + 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 + }); + } + 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)); + pos1 = trans.forward(d.angidx1 === d.posidx ? pos : m_tracks.positionFunc(tracks[i][d.angidx1], d.angidx1, tracks[i], i)); + } + 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[key + 'Style']()) + .style(m_this.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) { + let 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'); + let 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) { + let 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'); + let 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); + let 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; + }; + + /** + * Set or get 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] = $.extend(true, 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 {float} [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 {float|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 existsing + * 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 {float|null} [val] If specified, the new start time. + * @returns {float|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 {float|null} [val] If specified, the new end time. + * @returns {float|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 {float|null} [val] If specified, the new duration. + * @returns {float|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 + * segement 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) { + let 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) { + let 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 = $.extend( + true, + {}, + { + track: (d) => d, + position: (d) => d, + time: (d, i) => (d.t !== undefined ? d.t : i) + }, + arg.style === undefined ? {} : arg.style + ); + var markerStyle = $.extend( + true, + {}, + { + rotateWithMap: true, + rotation: (d, i) => -m_tracks.endPosition[i].angle || 0 + }, + arg.markerStyle === undefined ? {} : arg.markerStyle + ); + var textStyle = $.extend( + true, + {}, + { + 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 = $.extend( + true, + {}, + { + strokeOpacity: 0.25 + }, + arg.pastStyle === undefined ? {} : arg.pastStyle + ); + var currentStyle = $.extend( + true, + {}, + { + // defaults go here + }, + arg.currentStyle === undefined ? {} : arg.currentStyle + ); + var futureStyle = $.extend( + true, + {}, + { + 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; diff --git a/src/webgl/index.js b/src/webgl/index.js index 2c149e0f37..0d38e5b8b8 100644 --- a/src/webgl/index.js +++ b/src/webgl/index.js @@ -12,5 +12,6 @@ module.exports = { polygonFeature: require('./polygonFeature'), quadFeature: require('./quadFeature'), tileLayer: require('./tileLayer'), + trackFeature: require('./trackFeature'), webglRenderer: require('./webglRenderer') }; diff --git a/src/webgl/trackFeature.js b/src/webgl/trackFeature.js new file mode 100644 index 0000000000..0a3ce9b557 --- /dev/null +++ b/src/webgl/trackFeature.js @@ -0,0 +1,33 @@ +var inherit = require('../inherit'); +var registerFeature = require('../registry').registerFeature; +var trackFeature = require('../trackFeature'); + +/** + * Create a new instance of trackFeature. + * + * @class + * @alias geo.webgl.trackFeature + * @extends geo.trackFeature + * @param {geo.trackFeature.spec} arg + * @returns {geo.webgl.trackFeature} + */ +var webgl_trackFeature = function (arg) { + 'use strict'; + if (!(this instanceof webgl_trackFeature)) { + return new webgl_trackFeature(arg); + } + arg = arg || {}; + trackFeature.call(this, arg); + + var object = require('./object'); + object.call(this); + + this._init(arg); + return this; +}; + +inherit(webgl_trackFeature, trackFeature); + +// Now register it +registerFeature('webgl', 'track', webgl_trackFeature); +module.exports = webgl_trackFeature; diff --git a/tests/cases/trackFeature.js b/tests/cases/trackFeature.js new file mode 100644 index 0000000000..5161b37f51 --- /dev/null +++ b/tests/cases/trackFeature.js @@ -0,0 +1,417 @@ +// Test geo.trackFeature, geo.svg.trackFeature, and geo.webgl.trackFeature + +// var $ = require('jquery'); +var geo = require('../test-utils').geo; +var createMap = require('../test-utils').createMap; +var destroyMap = require('../test-utils').destroyMap; +var mockWebglRenderer = geo.util.mockWebglRenderer; +var restoreWebglRenderer = geo.util.restoreWebglRenderer; +// var vgl = require('vgl'); +// var waitForIt = require('../test-utils').waitForIt; + +describe('geo.trackFeature', function () { + 'use strict'; + + var testTracks = [ + { + id: 'AAL1221', + x: [-75.0789, -75.0384, -74.9988, -74.9605, -74.9235, -74.8836, -74.8436, -74.802, -74.7533], + y: [40.1137, 40.1467, 40.1791, 40.2103, 40.2403, 40.2703, 40.2991, 40.3284, 40.3669], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'UPS2424', + x: [-75.1656, -75.1997, -75.2477], + y: [39.968, 39.993, 40.0253], + t: [1570129759, 1570129790, 1570129830] + }, { + id: 'N316JS', + x: [-74.9619, -74.9875, -75.0146, -75.0392, -75.0623, -75.0936, -75.1188, -75.1447, -75.1764], + y: [40.1677, 40.1695, 40.1716, 40.1736, 40.1755, 40.1783, 40.1804, 40.1826, 40.1852], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'RPA4550', + x: [-75.1074, -75.075, -75.0871, -75.1157, -75.1437, -75.1729, -75.2044, -75.2371, -75.2812], + y: [39.8434, 39.8172, 39.7799, 39.7508, 39.72, 39.685, 39.6484, 39.6097, 39.558], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'N913CR', + x: [-74.8837, -74.8696, -74.8693, -74.8703, -74.8769, -74.8826, -74.8994, -74.9209, -74.9501], + y: [39.8171, 39.857, 39.8732, 39.9092, 39.9312, 39.9497, 39.9557, 39.9511, 39.9422], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'JBU623', + x: [-75.1583, -75.2288, -75.3011, -75.3711, -75.4414, -75.5162, -75.5847, -75.6578, -75.7545], + y: [40.149, 40.1432, 40.1371, 40.1311, 40.1251, 40.1186, 40.1127, 40.1063, 40.0977], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'SWA865', + x: [-74.7815, -74.7381, -74.6956, -74.652, -74.611, -74.5693, -74.5282, -74.4869, -74.4348], + y: [40.0032, 40.0335, 40.0631, 40.0936, 40.1225, 40.1513, 40.1795, 40.2087, 40.25], + t: [1570129579, 1570129609, 1570129640, 1570129670, 1570129699, 1570129730, 1570129759, 1570129790, 1570129830] + }, { + id: 'AAL341', + x: [], + y: [], + t: [] + }, { + id: 'UPS2424', + x: [-75.2145, -75.1835], + y: [39.876, 39.8802], + t: [1570129609, 1570129640] + }, { + id: 'GAJ834', + x: [-75.2399], + y: [39.895], + t: [1570129579] + } + ]; + + describe('create', function () { + it('direct', function () { + var map, layer, track; + map = createMap(); + layer = map.createLayer('feature', {features: ['track']}); + track = geo.trackFeature({layer: layer}); + expect(track instanceof geo.trackFeature).toBe(true); + destroyMap(); + }); + it('svg', function () { + var map, layer, track; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'svg'}); + track = layer.createFeature('track'); + expect(track instanceof geo.svg.trackFeature).toBe(true); + destroyMap(); + }); + it('canvas', function () { + var map, layer, track; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + track = layer.createFeature('track'); + expect(track instanceof geo.canvas.trackFeature).toBe(true); + destroyMap(); + }); + it('webgl', function () { + mockWebglRenderer(); + var map, layer, track; + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + track = layer.createFeature('track'); + expect(track instanceof geo.webgl.trackFeature).toBe(true); + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('Public utility methods', function () { + var map, layer, track; + it('setup', function () { + // the marker feature needs webgl + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {features: ['track']}); + }); + it('calculateTimePosition', function () { + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }); + var ctp = track.calculateTimePosition(1570129640); + expect(ctp.length).toBe(10); + expect(ctp[0]).toEqual({x: -74.9988, y: 40.1791, z: 0, posidx: 2, angidx0: 1, angidx1: 3}); + ctp = track.calculateTimePosition(1570129640, undefined, true); + expect(ctp[0].angle).toBeCloseTo(0.6847); + ctp = track.calculateTimePosition(1570129635); + expect(ctp[0].x).toBeCloseTo(-75.0052); + ctp = track.calculateTimePosition(1570129570); + expect(ctp[0].x).toBe(-75.0789); + ctp = track.calculateTimePosition(1570129840); + expect(ctp[0].x).toBe(-74.7533); + }); + it('modified', function () { + layer = map.createLayer('feature', {features: ['track']}); + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + track._build(); + sinon.stub(layer.features()[1], 'modified', function () {}); + track.modified(); + expect(layer.features()[1].modified.calledOnce).toBe(true); + layer.features()[1].modified.restore(); + }); + it('draw', function () { + sinon.stub(layer.features()[1], 'draw', function () {}); + track.draw(); + expect(layer.features()[1].draw.calledOnce).toBe(true); + layer.features()[1].draw.restore(); + }); + it('cleanup', function () { + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('Check class accessors', function () { + var map, layer, track, track2; + it('setup', function () { + // the marker feature needs webgl + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {features: ['track']}); + track = layer.createFeature('track', { + track: function (d) { return d.t; } + }) + .data(testTracks) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + track2 = layer.createFeature('track'); + }); + it('styles', function () { + expect(track.style().opacity).toBe(1); + expect(track.style('opacity')).toBe(1); + expect(track.style.get('opacity')()).toBe(1); + expect(track.style('opacity', 0.5)).toBe(track); + expect(track.style().opacity).toBe(0.5); + expect(track.style({opacity: 1})).toBe(track); + expect(track.style().opacity).toBe(1); + expect(track.pastStyle().strokeOpacity).toBe(0.25); + expect(track.currentStyle().strokeOpacity).toBe(undefined); + expect(track.futureStyle().strokeOpacity).toBe(0.25); + expect(track.markerStyle().rotateWithMap).toBe(true); + expect(track.textStyle().rotateWithMap).toBe(true); + expect(track.currentStyle('strokeOpacity', 0.5)).toBe(track); + expect(track.currentStyle().strokeOpacity).toBe(0.5); + expect(track.style('strokeOpacity', 1, 'currentStyle')).toBe(track); + expect(track.currentStyle().strokeOpacity).toBe(1); + expect(track.style('strokeOpacity', undefined, 'currentStyle')).toBe(1); + }); + it('track', function () { + let oldfunc = track.track(); + expect(track.track()({t: 'x'})).toBe('x'); + expect(track.track(function (d) { return d; })).toBe(track); + expect(track.track()('y')).toBe('y'); + expect(track.track(oldfunc)).toBe(track); + expect(track.track()({t: 'x'})).toBe('x'); + expect(track2.track()('z')).toBe('z'); + }); + it('position', function () { + let oldfunc = track.position(); + expect(track.position()(0, 0, 0, 0).x).toBe(-75.0789); + expect(track.position(function (d) { return d; })).toBe(track); + expect(track.position()('y')).toBe('y'); + expect(track.position(oldfunc)).toBe(track); + expect(track.position()(0, 0, 0, 0).x).toBe(-75.0789); + expect(track2.position()('z')).toBe('z'); + }); + it('time', function () { + let oldfunc = track.time(); + expect(track.time()('x')).toBe('x'); + expect(track.time(function (d) { return d + 'a'; })).toBe(track); + expect(track.time()('y')).toBe('ya'); + expect(track.time(oldfunc)).toBe(track); + expect(track.time()('x')).toBe('x'); + expect(track2.time()('z', 'c')).toBe('c'); + expect(track2.time()({t: 'b'})).toBe('b'); + }); + it('timeRange', function () { + track._build(); + expect(track.timeRange()).toEqual({ + startTime: null, + endTime: null, + duration: null, + start: 1570129579, + end: 1570129830, + minimum: 1570129579, + maximum: 1570129830 + }); + // start only + expect(track.timeRange({startTime: 1570129600})).toBe(track); + expect(track.timeRange().start).toBe(1570129600); + expect(track.timeRange().end).toBe(1570129830); + // start and end + expect(track.timeRange({endTime: 1570129800})).toBe(track); + expect(track.timeRange().start).toBe(1570129600); + expect(track.timeRange().end).toBe(1570129800); + // start, end, and duration + expect(track.timeRange({duration: 120})).toBe(track); + expect(track.timeRange().start).toBe(1570129600); + expect(track.timeRange().end).toBe(1570129800); + // end and duration + expect(track.timeRange({startTime: null})).toBe(track); + expect(track.timeRange().start).toBe(1570129680); + expect(track.timeRange().end).toBe(1570129800); + // start and duration + expect(track.timeRange({endTime: null, startTime: 1570129600})).toBe(track); + expect(track.timeRange().start).toBe(1570129600); + expect(track.timeRange().end).toBe(1570129720); + // duration only + expect(track.timeRange({startTime: null})).toBe(track); + expect(track.timeRange().start).toBe(1570129710); + expect(track.timeRange().end).toBe(1570129830); + // end only + expect(track.timeRange({endTime: 1570129800, duration: null})).toBe(track); + expect(track.timeRange().start).toBe(1570129579); + expect(track.timeRange().end).toBe(1570129800); + // none + expect(track.timeRange({endTime: null})).toBe(track); + expect(track.timeRange().start).toBe(1570129579); + expect(track.timeRange().end).toBe(1570129830); + }); + it('startTime', function () { + expect(track.startTime()).toBe(null); + expect(track.startTime(1570129600)).toBe(track); + expect(track.startTime()).toBe(1570129600); + expect(track.startTime(null)).toBe(track); + expect(track.startTime()).toBe(null); + }); + it('endTime', function () { + expect(track.endTime()).toBe(null); + expect(track.endTime(1570129800)).toBe(track); + expect(track.endTime()).toBe(1570129800); + expect(track.endTime(null)).toBe(track); + expect(track.endTime()).toBe(null); + }); + it('duration', function () { + expect(track.duration()).toBe(null); + expect(track.duration(120)).toBe(track); + expect(track.duration()).toBe(120); + expect(track.duration(null)).toBe(track); + expect(track.duration()).toBe(null); + }); + it('pointSearch', function () { + var pt; + track.pastStyle({strokeOpacity: 0}); + map.zoom(6); + pt = track.pointSearch({x: -74.9875, y: 40.1695}); + expect(pt.found.length).toBe(2); + expect(pt.index[1]).toEqual(2); + expect(pt.extra[2]).toEqual(0); + pt = track.pointSearch({x: -75.0011, y: 40.17055}); + expect(pt.found.length).toBe(2); + expect(pt.index[1]).toEqual(2); + expect(pt.extra[2]).toEqual(0); + track.startTime(1570129750); + pt = track.pointSearch({x: -74.9875, y: 40.1695}); + expect(pt.found.length).toBe(0); + track.startTime(null); + track.endTime(1570129580); + pt = track.pointSearch({x: -74.9875, y: 40.1695}); + expect(pt.found.length).toBe(2); + expect(pt.index[0]).toEqual(2); + expect(pt.extra[0].where).toEqual('marker'); + track.endTime(null); + }); + it('polygonSearch', function () { + var pt; + pt = track.polygonSearch([{x: -75.03, y: 40.16}, {x: -75.03, y: 40.20}, {x: -75.05, y: 40.18}]); + expect(pt.found.length).toBe(0); + pt = track.polygonSearch([{x: -75.03, y: 40.16}, {x: -75.03, y: 40.20}, {x: -75.05, y: 40.18}], {partial: true}); + expect(pt.found.length).toBe(2); + expect(pt.index[1]).toEqual(2); + }); + it('cleanup', function () { + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('Private utility methods', function () { + var map, layer, track; + /* Only private utility methods that weere not completely covered by + * public methods and accessors are included here. */ + it('setup', function () { + // the marker feature needs webgl + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {features: ['track']}); + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + }); + it('_updateTimeAndPosition', function () { + // don't fail if _build was never called. + expect(track._updateTimeAndPosition()).toBe(undefined); + }); + it('cleanup', function () { + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('geo.svg.trackFeature', function () { + it('basic usage', function () { + var map, layer, track; + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {renderer: 'svg'}); + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + track.draw(); + expect(layer.children().length).toBe(6); + expect(layer.children()[4].features()[0] instanceof geo.webgl.markerFeature).toBe(true); + expect(layer.children()[5].features()[0] instanceof geo.canvas.textFeature).toBe(true); + layer.deleteFeature(track); + expect(layer.children().length).toBe(0); + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('geo.canvas.trackFeature', function () { + it('basic usage', function () { + var map, layer, track; + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {renderer: 'canvas'}); + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + track.draw(); + expect(layer.children().length).toBe(6); + expect(layer.children()[4].features()[0] instanceof geo.webgl.markerFeature).toBe(true); + expect(layer.children()[5] instanceof geo.canvas.textFeature).toBe(true); + layer.deleteFeature(track); + expect(layer.children().length).toBe(0); + destroyMap(); + restoreWebglRenderer(); + }); + }); + + describe('geo.webgl.trackFeature', function () { + it('basic usage', function () { + var map, layer, track; + mockWebglRenderer(); + map = createMap(); + layer = map.createLayer('feature', {renderer: 'webgl'}); + track = layer.createFeature('track') + .data(testTracks) + .track(function (d) { return d.t; }) + .position(function (d, i, t, j) { return {x: testTracks[j].x[i], y: testTracks[j].y[i]}; }) + .time(function (d, i, t, j) { return d; }) + .style('text', function (d, i) { return i % 2 ? testTracks[i].id : undefined; }); + track.draw(); + expect(layer.children().length).toBe(6); + expect(layer.children()[4] instanceof geo.webgl.markerFeature).toBe(true); + expect(layer.children()[5].features()[0] instanceof geo.canvas.textFeature).toBe(true); + layer.deleteFeature(track); + expect(layer.children().length).toBe(0); + destroyMap(); + restoreWebglRenderer(); + }); + }); +}); diff --git a/tutorials/tracks/index.pug b/tutorials/tracks/index.pug new file mode 100644 index 0000000000..3b47065966 --- /dev/null +++ b/tutorials/tracks/index.pug @@ -0,0 +1,167 @@ +extends ../common/index.pug + +block mainTutorial + :markdown-it + # Tutorial - Tracks + Track features are like line features with a time value at each vertex. + + We'll use a few custom controls and styles in this tutorials. These are + set up first. + + +codeblock('html', 1). + + + + + + +
+ + + + + +codeblock('css', 2). + html,body,#map{ + width: 100%; + height: 100%; + padding: 0; + margin: 0; + overflow: hidden; + } + #controls { + position: absolute; + top: 10px; + left: 10px; + user-select: none; + display: flex; + align-items: center; + } + #controls span { + padding: 5px; + } + #controls.hidden { + display: none; + } + + :markdown-it + + Create a map and a feature layer. + + +codeblock('javascript', 3). + var map = geo.map({ + node: "#map", + center: {x: -73.7781, y: 40.6413}, + zoom: 10 + }); + // create a layer that supports drawing line features + var layer = map.createLayer('feature', {features: ['line']}); + + :markdown-it + Specify some data. + + A track feature is an array of data elements, each of which has a list of + vertices. + + If a time is not specified on a vertex, the index of the vertex is used. + + +codeblock('javascript', 4). + var trackData = [ + [{id:'alpha', t:0.0,x:-73.7679,y:40.6445},{t:6.0,x:-73.6959,y:40.6744},{t:12.0,x:-73.6244,y:40.6367},{t:18.0,x:-73.5793,y:40.567},{t:22.0,x:-73.5464,y:40.5183}], + [{id:'beta', t:0.0,x:-73.7676,y:40.645},{t:4.0,x:-73.7403,y:40.6678},{t:9.0,x:-73.678,y:40.6712},{t:13.0,x:-73.6119,y:40.6741},{t:17.0,x:-73.5412,y:40.676}], + [{id:'gamma', t:0.0,x:-73.7651,y:40.6488},{t:6.0,x:-73.7059,y:40.6517},{t:12.0,x:-73.6242,y:40.6181},{t:18.0,x:-73.5493,y:40.5618},{t:24.0,x:-73.4698,y:40.5002}], + [{id:'delta', t:0.0,x:-73.7615,y:40.6527},{t:7.0,x:-73.6772,y:40.6735},{t:13.1,x:-73.5788,y:40.678},{t:19.1,x:-73.4699,y:40.6828},{t:25.1,x:-73.4303,y:40.7493},{t:27.1,x:-73.445,y:40.7747}], + [{id:'epsilon',t:0.0,x:-73.7856,y:40.6222},{t:3.0,x:-73.753,y:40.6585},{t:6.0,x:-73.7097,y:40.6633},{t:8.0,x:-73.6924,y:40.6641},{t:12.0,x:-73.6165,y:40.6667},{t:13.0,x:-73.6074,y:40.6673}], + [{id:'zeta', t:0.0,x:-73.7591,y:40.6563},{t:6.0,x:-73.6847,y:40.6672},{t:12.0,x:-73.597,y:40.6897},{t:18.0,x:-73.5229,y:40.7398},{t:24.0,x:-73.4897,y:40.8127},{t:25.0,x:-73.4926,y:40.8215}], + [{id:'eta', t:0.0,x:-73.7555,y:40.6593},{t:8.0,x:-73.6495,y:40.6708},{t:16.1,x:-73.5306,y:40.7073},{t:23.1,x:-73.5107,y:40.7884},{t:31.1,x:-73.5582,y:40.8988},{t:33.1,x:-73.5762,y:40.9285}], + [{id:'theta', t:0.0,x:-73.8287,y:40.6524},{t:8.1,x:-73.9031,y:40.6026},{t:15.1,x:-73.9611,y:40.5312},{t:23.1,x:-74.0295,y:40.4473},{t:30.1,x:-74.1117,y:40.3775},{t:31.1,x:-74.1319,y:40.3686}], + [{id:'iota', t:0.0,x:-73.8586,y:40.7728},{t:3.0,x:-73.8424,y:40.7564},{t:6.0,x:-73.8285,y:40.738},{t:9.0,x:-73.7938,y:40.7451},{t:12.0,x:-73.7738,y:40.7722},{t:13.0,x:-73.7667,y:40.783}], + [{id:'kappa', t:0.0,x:-73.8709,y:40.7786},{t:5.0,x:-73.8384,y:40.7523},{t:10.0,x:-73.7904,y:40.7354},{t:14.0,x:-73.7561,y:40.7696},{t:18.0,x:-73.7631,y:40.8134},{t:22.0,x:-73.8214,y:40.8257}] + ]; + + :markdown-it + Create the track feature. By default, the entirety of each track is shown. + + +codeblock('javascript', 5, undefined, true). + var track = layer.createFeature('track') + // set the data to our example data + .data(trackData) + // set some style to our lines + .style({ + strokeWidth: 4, + strokeColor: 'black' + }) + .markerStyle({ + symbol: geo.markerFeature.symbols.arrow, + symbolValue: [1, 1, 0, true], + radius: 15 + }); + // draw the feature + track.draw(); + +codeblock_test('map has a feature layer with ten tracks', [ + 'map.layers().length === 1', + 'map.layers()[0] instanceof geo.featureLayer', + 'map.layers()[0].features()[0] instanceof geo.trackFeature', + 'map.layers()[0].features()[0].data().length === 10' + ]) + + :markdown-it + Highlight a particular time range. + + The track feature has a start time, end time, and duration. Parts of the + track before the start time are styled with the `pastStyle`, those after + the end time are styled with the `futureStyle`, and those between the + start and end with the `presentStyle`. The head of the track is at the + end time and can either have a marker or text styled with `markerStyle` + and `textStyle`. + + +codeblock('javascript', 6). + track.startTime(5).endTime(20).draw(); + + :markdown-it + Hide future parts of the track. + + If the end time is after the end of the track, hide the marker, too. + + +codeblock('javascript', 7). + track.futureStyle('strokeOpacity', 0).draw(); + track.markerStyle({ + radius: (d) => track.timeRange().end <= d[d.length - 1].t ? 15 : 0 + }).draw(); + + :markdown-it + Use text instead of a marker. + + +codeblock('javascript', 8). + track.style('text', (d) => d[0].id).draw(); + // we can style the text + track.textStyle('color', 'blue').draw(); + + :markdown-it + We can add controls to dynamically adjust the track times and to show + other information. + + If you adjust the start time to a point after the end time, the specified + start time is ignored. Instead, the minimum value for the time range is + used. + + +codeblock('javascript', 9). + var startctl = document.getElementById('start'), + endctl = document.getElementById('end'); + startctl.setAttribute('max', track.timeRange().maximum); + startctl.setAttribute('min', track.timeRange().minimum); + startctl.setAttribute('value', track.timeRange().startTime); + endctl.setAttribute('min', track.timeRange().minimum); + endctl.setAttribute('max', track.timeRange().maximum); + endctl.setAttribute('value', track.timeRange().endTime); + + startctl.addEventListener('input', (event) => { + track.startTime(event.target.value).draw(); + }); + endctl.addEventListener('input', (event) => { + track.endTime(event.target.value).draw(); + }); + // show the controls + document.getElementById('controls').classList.remove('hidden'); diff --git a/tutorials/tracks/thumb.jpg b/tutorials/tracks/thumb.jpg new file mode 100755 index 0000000000000000000000000000000000000000..21828561cfeb20fed68cbf91eaf48e423acf33cd GIT binary patch literal 71689 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 literal 0 HcmV?d00001 diff --git a/tutorials/tracks/tutorial.json b/tutorials/tracks/tutorial.json new file mode 100644 index 0000000000..31389f8920 --- /dev/null +++ b/tutorials/tracks/tutorial.json @@ -0,0 +1,8 @@ +{ + "title": "Tracks", + "hideNavbar": true, + "level": 0, + "about": { + "text": "Track features are much like line featrues with a time value per vertex. A start and end time can be specified." + } +}