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:

Tracks Cookbook example - imports
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:

Tracks Cookbook example - Places plugin initialisation
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)

Tracks Cookbook example - Create Track
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:

Tracks Cookbook example - Update Track
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:

../../_images/TracksLayer.png

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:

Tracks Cookbook Coordinates example - Create Track
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:

Tracks Cookbook Coordinates example - Update Track
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.

Tracks Cookbook Features variation example - Full 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:

Tracks Cookbook Coordinates variation example - Full 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
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());