Time Support

You may have noticed that there is a little clock icon on the layer. The parent class to our descriptor, os.data.FileDescriptor, sets the animate flag to true in our layer options:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
  getLayerOptions() {
    var options = {};
    options['id'] = this.getId();

    options['animate'] = true; // TODO: add checkbox to toggle this in import UI
    options['color'] = this.getColor();
    options['icon'] = this.getIcon();
    options['shapeName'] = this.getShapeName();
    options['load'] = true;
    options['originalUrl'] = this.getOriginalUrl();
    options['parserConfig'] = this.parserConfig;
    options['provider'] = this.getProvider();
    options['tags'] = this.getTags();
    options['title'] = this.getTitle();
    options['url'] = this.getUrl();
    options['mappings'] = this.getMappings();
    options['detectColumnTypes'] = true;

    return options;
  }

That turns on time indexing on the layer. However, nothing is taking the time values from the <updated> tags in the GeoRSS entries and turning them into os.time.TimeInstant values in the proper place on the feature. Hence every feature is treated as “timeless” even though the source has time indexing enabled. This is where mappings come in.

Import mappings all implement os.im.mapping.IMapping and are used to either change fields (e.g. unit conversion) or derive fields (e.g. LAT/LON columns in CSVs to ol.geom.Point, adding altitude, etc.). However, the most common mappings of all are time mappings. Import UIs for types such as GeoJSON and CSV generally walk the user through creating these mappings (CSV, being the worst file format of all time for geospatial data, has to basically create every type of mapping possible).

In our case, we have several options.

Reinvent the wheel

We could simply parse the time in our parser since the format is common and only shows up in one place (the <updated> tag). KML is similar in this regard as the format and tags for time are defined in the spec. But that really just duplicates code that exists within the mappings.

Define a mapping explicitly

We could define a os.im.mapping.time.DateTimeMapping instance ourselves. This could be placed in several different places but the best place is probably the layer config. That would look something like this:

src/plugin/georss/georsslayerconfig.js
 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
goog.declareModuleId('plugin.georss.GeoRSSLayerConfig');

import DateTimeMapping from 'opensphere/src/os/im/mapping/time/datetimemapping.js';
import TimeType from 'opensphere/src/os/im/mapping/timetype.js';
import AbstractDataSourceLayerConfig from 'opensphere/src/os/layer/config/abstractdatasourcelayerconfig.js';

import GeoRSSParser from './georssparser.js';

/**
 * GeoRSS layer config.
 */
export default class GeoRSSLayerConfig extends AbstractDataSourceLayerConfig {
  /**
   * Constructor.
   */
  constructor() {
    super();
  }

  /**
   * @inheritDoc
   */
  getParser(options) {
    return new GeoRSSParser();
  }

  /**
   * @inheritDoc
   */
  getImporter(options) {
    const importer = super.getImporter(options);

    // add time mapping to importer
    const timeMapping = new DateTimeMapping(TimeType.INSTANT);
    timeMapping.field = 'updated';
    // there's no need to call timeMapping.setFormat() since the default is what we want

    importer.setExecMappings([timeMapping]);
    return importer;
  }
}

Use the auto-detected mappings

Because many import UIs configure mappings, it helps the user if those default to the best value possible. Therefore, OpenSphere includes auto-detection logic with each mapping to make a best guess on what the mapping should be based on the fields and values available in a sample of records from the source. We could simply make use of that like so:

src/plugin/georss/georsslayerconfig.js
 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
goog.declareModuleId('plugin.georss.GeoRSSLayerConfig');

import DateTimeMapping from 'opensphere/src/os/im/mapping/time/datetimemapping.js';
import AbstractDataSourceLayerConfig from 'opensphere/src/os/layer/config/abstractdatasourcelayerconfig.js';

import GeoRSSParser from './georssparser.js';

/**
 * GeoRSS layer config.
 */
export default class GeoRSSLayerConfig extends AbstractDataSourceLayerConfig {
  /**
   * Constructor.
   */
  constructor() {
    super();
  }

  /**
   * @inheritDoc
   */
  getParser(options) {
    return new GeoRSSParser();
  }

  /**
   * @inheritDoc
   */
  getImporter(options) {
    const importer = super.getImporter(options);

    // Auto detect the time from the fields.
    // This should only be called for mappings not explicitly set by the user in
    // the import UI. See other file types for examples.
    importer.selectAutoMappings([DateTimeMapping.ID]);

    return importer;
  }
}

After picking any of those options, fire up the debug instance and load your URL. Open the timeline by clicking the yellow clock icon in the Date Control at the top or choosing Windows > Timeline. If you were using a recent feed, you should see your data for today immediately and you can hit the Play button to animate it. If your data is older, you can zoom/pan to it and draw (or explicitly set) a new Load or Animation range before playing.

To complete testing of the importer, we can extend the tests as shown below:

test/plugin/georss/georsslayerconfig.test.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
goog.require('os.im.FeatureImporter');
goog.require('plugin.georss.GeoRSSLayerConfig');
goog.require('plugin.georss.GeoRSSParser');

describe('plugin.georss.GeoRSSLayerConfig', function() {
  const {default: FeatureImporter} = goog.module.get('os.im.FeatureImporter');
  const {default: GeoRSSLayerConfig} = goog.module.get('plugin.georss.GeoRSSLayerConfig');
  const {default: GeoRSSParser} = goog.module.get('plugin.georss.GeoRSSParser');

  it('should return a GeoRSS parser', function() {
    var config = new GeoRSSLayerConfig();
    expect(config.getParser() instanceof GeoRSSParser).toBe(true);
  });

  it('should return a GeoRSS importer', function() {
    var config = new GeoRSSLayerConfig();
    expect(config.getImporter('http://www.example.com/testname.rss', {}) instanceof FeatureImporter).toBe(true);
  });
});

That concludes the File Type Plugin Guide! If you have any further questions or requests for new guides, please feel free to use our GitHub issues. Also, remember to check out the core plugins as examples for other things you can do!.