Tracks¶
Problem¶
You have a data source that updates over time, such as a near-real time track feed.
Example of this could include:
- Automatic Dependent Surveillance - Broadcast (from aircraft)
- Automatic Identification System (from shipping)
- Vehicle tracking systems
- Radar
- Military tactical data links
Solution¶
Use the OpenSphere tracks plugin.
Your code (e.g. plugin) ensures that the tracks plugin is available:
1 2 | import {addToTrack} from 'opensphere/src/os/track/track.js';
import {createAndAdd} from 'opensphere/src/plugin/track/track.js';
|
Your code needs to wait for the Places plugin to be available (fully loaded) before attempting to add the track:
1 2 3 4 5 6 | const placesManager = PlacesManager.getInstance();
if (placesManager.isLoaded()) {
this.onPlacesLoaded();
} else {
placesManager.listenOnce(EventType.LOADED, this.onPlacesLoaded, false, this);
}
|
You can then create a new track, which might be in response to an initial connection, or in response to a server update (e.g. over a WebSocket)
1 2 3 4 5 6 7 | onPlacesLoaded() {
const track = createAndAdd(/** @type {!CreateOptions} */({
features: this.getFeatures_(),
name: 'Cookbook track',
color: '#00ff00'
}));
}
|
The track can then be updated with additional features in response to changes:
1 2 3 4 | addToTrack({
features: this.getFeatures_(),
track: track
});
|
Discussion¶
The approach shown above will produce the track under the Saved Places layer (on the Places tab), as shown below:
You could create a separate track using createTrack()
in place of createAndAdd()
, and then add it to your own layer definition. See the KML
parser implementation for an example of this.
Instead of using features, you can just pass coordinates instead, as shown below:
1 2 3 4 5 | const track = createAndAdd(/** @type {!CreateOptions} */({
coordinates: this.getCoordinates_(),
name: 'Cookbook track',
color: '#00ff00'
}));
|
Tip
In case you missed it, the CreateOptions object has a different key - coordinates
in place of features
. You pass exactly one of coordinates
or features
.
Similarly, you can pass coordinates to the update method as well:
1 2 3 4 | addToTrack({
coordinates: this.getCoordinates_(),
track: track
});
|
Tip
With both features and coordinates, you have to make sure your geometry is transformed into the map projection.
While your code could poll for updates, a streaming “server push” may be more appropriate in some scenarios. OpenSphere has two options for streams:
os.net.LongPoll
goog.net.WebSocket
If Web Sockets are supported, prefer goog.net.WebSocket
.
os.net.LongPoll
attempts to mimic the goog.net.WebSocket
interface as much as possible.
There may be other calls needed to setup/teardown streams depending on the remote server’s API.
If neither the long poll or websocket options are supported (e.g. direct socket only), then it is not possible to connect from a web application such as OpenSphere. In this case, you will likely need a server proxy to adapt (e.g. “websockify”) your streaming source.
Full code¶
To avoid the need for a server that provides updates, the example code makes periodic updates to the track position using setInterval()
and the modifyPosition_()
function. Those are artifacts of the example, and you wouldn’t have that kind of function in your code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | goog.declareModuleId('plugin.cookbook_tracks.TracksPlugin');
import EventType from 'opensphere/src/os/config/eventtype.js';
import RecordField from 'opensphere/src/os/data/recordfield.js';
import {PROJECTION} from 'opensphere/src/os/map/map.js';
import AbstractPlugin from 'opensphere/src/os/plugin/abstractplugin.js';
import PluginManager from 'opensphere/src/os/plugin/pluginmanager.js';
import {EPSG4326} from 'opensphere/src/os/proj/proj.js';
import TimeInstant from 'opensphere/src/os/time/timeinstant.js';
import {addToTrack} from 'opensphere/src/os/track/track.js';
import PlacesManager from 'opensphere/src/plugin/places/placesmanager.js';
import {createAndAdd} from 'opensphere/src/plugin/track/track.js';
const Feature = goog.require('ol.Feature');
const Point = goog.require('ol.geom.Point');
const olProj = goog.require('ol.proj');
const {CreateOptions} = goog.requireType('os.track');
let transformToMap;
/**
* Provides a plugin cookbook example for track creation and update.
*/
export default class TracksPlugin extends AbstractPlugin {
/**
* Constructor.
*/
constructor() {
super();
this.id = ID;
this.errorMessage = null;
/**
* @type {number}
*/
this.lat = -35.0;
/**
* @type {number}
*/
this.lon = 135.0;
/**
* @type {number}
*/
this.latDelta = 0.1;
/**
* @type {number}
*/
this.lonDelta = 0.1;
}
/**
* @inheritDoc
*/
init() {
transformToMap = olProj.getTransform(EPSG4326, PROJECTION);
const placesManager = PlacesManager.getInstance();
if (placesManager.isLoaded()) {
this.onPlacesLoaded();
} else {
placesManager.listenOnce(EventType.LOADED, this.onPlacesLoaded, false, this);
}
}
/**
* @private
*/
onPlacesLoaded() {
const track = createAndAdd(/** @type {!CreateOptions} */({
features: this.getFeatures_(),
name: 'Cookbook track',
color: '#00ff00'
}));
setInterval(() => {
this.updateTrack(/** @type {!Feature} */ (track));
}, 2000);
}
/**
* @private
* @return {!Array<!Feature>} features array for current location
*/
getFeatures_() {
const coordinate = transformToMap([this.lon, this.lat]);
const point = new Point(coordinate);
const feature = new Feature(point);
feature.set(RecordField.TIME, new TimeInstant(Date.now()));
const features = [feature];
return features;
}
/**
* Update the position and post the new track location.
* @param {!Feature} track the track to update
*/
updateTrack(track) {
this.modifyPosition_();
addToTrack({
features: this.getFeatures_(),
track: track
});
}
/**
* @private
*/
modifyPosition_() {
this.lat += this.latDelta;
this.lon += this.lonDelta;
if (this.lat > 50.0) {
this.latDelta = -0.05;
}
if (this.lat < -50.0) {
this.latDelta = 0.05;
}
if (this.lon >= 160.0) {
this.lonDelta = -0.05;
}
if (this.lon < 0.0) {
this.lonDelta = 0.05;
}
}
}
/**
* @type {string}
*/
const ID = 'cookbook_tracks';
// add the plugin to the application
PluginManager.getInstance().addPlugin(new TracksPlugin());
|
If you’d prefer to use the coordinates approach, a complete example is shown below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 | goog.declareModuleId('plugin.cookbook_tracks.TracksPlugin');
import EventType from 'opensphere/src/os/config/eventtype.js';
import {PROJECTION} from 'opensphere/src/os/map/map.js';
import AbstractPlugin from 'opensphere/src/os/plugin/abstractplugin.js';
import PluginManager from 'opensphere/src/os/plugin/pluginmanager.js';
import {EPSG4326} from 'opensphere/src/os/proj/proj.js';
import {addToTrack} from 'opensphere/src/os/track/track.js';
import PlacesManager from 'opensphere/src/plugin/places/placesmanager.js';
import {createAndAdd} from 'opensphere/src/plugin/track/track.js';
const olProj = goog.require('ol.proj');
const Feature = goog.requireType('ol.Feature');
const {CreateOptions} = goog.requireType('os.track');
let transformToMap;
/**
* Provides a plugin cookbook example for track creation and update.
*/
export default class TracksPlugin extends AbstractPlugin {
/**
* Constructor.
*/
constructor() {
super();
this.id = ID;
this.errorMessage = null;
/**
* @type {number}
*/
this.lat = -35.0;
/**
* @type {number}
*/
this.lon = 135.0;
/**
* @type {number}
*/
this.latDelta = 0.1;
/**
* @type {number}
*/
this.lonDelta = 0.1;
}
/**
* @inheritDoc
*/
init() {
transformToMap = olProj.getTransform(EPSG4326, PROJECTION);
const placesManager = PlacesManager.getInstance();
if (placesManager.isLoaded()) {
this.onPlacesLoaded();
} else {
placesManager.listenOnce(EventType.LOADED, this.onPlacesLoaded, false, this);
}
}
/**
* @private
*/
onPlacesLoaded() {
const track = createAndAdd(/** @type {!CreateOptions} */({
coordinates: this.getCoordinates_(),
name: 'Cookbook track',
color: '#00ff00'
}));
setInterval(() => {
this.updateTrack(/** @type {!Feature} */ (track));
}, 2000);
}
/**
* @private
* @return {!Array<!Array<number>>} coordinates array for current location
*/
getCoordinates_() {
const coordinate = transformToMap([this.lon, this.lat]);
coordinate.push(0);
coordinate.push(Date.now());
const coordinates = [coordinate];
return coordinates;
}
/**
* Update the position and post the new track location.
* @param {!Feature} track the track to update
*/
updateTrack(track) {
this.modifyPosition_();
addToTrack({
coordinates: this.getCoordinates_(),
track: track
});
}
/**
* @private
*/
modifyPosition_() {
this.lat += this.latDelta;
this.lon += this.lonDelta;
if (this.lat > 50.0) {
this.latDelta = -0.05;
}
if (this.lat < -50.0) {
this.latDelta = 0.05;
}
if (this.lon >= 160.0) {
this.lonDelta = -0.05;
}
if (this.lon < 0.0) {
this.lonDelta = 0.05;
}
}
}
/**
* @type {string}
*/
const ID = 'cookbook_tracks';
// add the plugin to the application
PluginManager.getInstance().addPlugin(new TracksPlugin());
|