Import UI

For external plugins, you will need to create at least one goog.define for your project. This will allow Angular to find your templates properly.

src/plugin/georss/georss.js
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
goog.declareModuleId('plugin.georss');

/**
 * Plugin identifier.
 * @type {string}
 */
export const ID = 'georss';

/**
 * @define {string} The path to this project.
 */
export const ROOT = goog.define('plugin.georss.ROOT', '../opensphere-plugin-georss/');

Note that the path should be the relative path from opensphere to your project.

Now we will create an Angular directive that will let the user change the title and color of the the layer.

src/plugin/georss/georssimport.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
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
goog.declareModuleId('plugin.georss.GeoRSSImportUI');

import AbstractFileImportCtrl from 'opensphere/src/os/ui/file/ui/abstractfileimport.js';
import Module from 'opensphere/src/os/ui/module.js';

import {ROOT} from './georss.js';
import {createFromConfig} from './georssdescriptor.js';
import GeoRSSProvider from './georssprovider.js';

const {default: GeoRSSDescriptor} = goog.requireType('plugin.georss.GeoRSSDescriptor');


/**
 * The GeoRSS import directive
 * @return {angular.Directive}
 */
/* istanbul ignore next */
export const directive = function() {
  return {
    restrict: 'E',
    replace: true,
    scope: true,
    // The plugin.georss.ROOT define used here helps to fix the paths in the debug instance
    // vs. the compiled instance. This example assumes that you are creating an external
    // plugin. You do not necessarily need a ROOT define per plugin, but rather per project
    // so that the OpenSphere build can find the files properly.
    //
    // For an internal plugin, just require os and use os.ROOT.
    templateUrl: ROOT + 'views/plugin/georss/georssimport.html',
    controller: Controller,
    controllerAs: 'georssImport'
  };
};

/**
 * Add the directive to the module
 */
Module.directive('georssimport', [directive]);

/**
 * Controller for the GeoRSS import dialog
 */
export class Controller extends AbstractFileImportCtrl {
  /**
   * Controller for the GeoRSS import dialog
   * @param {!angular.Scope} $scope
   * @param {!angular.JQLite} $element
   * @ngInject
   */
  constructor($scope, $element) {
    super($scope, $element);
    this.formName = 'georssForm';
  }

  /**
   * @inheritDoc
   */
  createDescriptor() {
    var descriptor = null;
    if (this.config['descriptor']) { // existing descriptor, update it
      descriptor = /** @type {!GeoRSSDescriptor} */ (this.config['descriptor']);
      descriptor.updateFromConfig(this.config);
    } else { // this is a new import
      descriptor = createFromConfig(this.config);
    }

    return descriptor;
  }

  /**
   * @inheritDoc
   */
  getProvider() {
    return GeoRSSProvider.getInstance();
  }
}

The parent class, once again, does most of the heavy lifting. We do, however, need to provide the template that we referenced for Angular.

views/plugin/georss/georssimport.html
 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
<div class="d-flex flex-column flex-fill">
  <div class="modal-body">
    <form name="georssForm">

      <div class="d-flex flex-row form-group">
        <label class="col-3 col-form-label text-right">Layer Title</label>
        <div class="col">
          <input class="form-control" type="text" name="title" ng-model="config.title" ng-required="true" ng-maxlength="50" />
          <validation-message target="georssForm.title"></validation-message>
        </div>
      </div>

      <div class="d-flex flex-row form-group">
        <label class="col-3 col-form-label text-right">Description</label>
        <div class="col">
          <input class="form-control" type="text" name="desc" ng-model="config.description" ng-maxlength="2000" placeholder="Add a custom description"
          />
          <validation-message target="georssForm.desc"></validation-message>
        </div>
      </div>

      <div class="d-flex flex-row form-group">
        <label class="col-3 col-form-label text-right">Tags</label>
        <div class="col">
          <input class="form-control" type="text" name="tags" ng-model="config.tags" ng-maxlength="2000" placeholder="Tags organize layers: e.g. states, population"
          />
          <validation-message target="georssForm.tags"></validation-message>
        </div>
      </div>

      <div class="d-flex flex-row form-group">
        <label class="col-3 col-form-label text-right">Color</label>
        <div class="col">
          <colorpicker name="color" class="no-text" color="config.color">
        </div>
      </div>

    </form>
  </div>

  <div class="modal-footer">
    <button class="btn btn-primary" ng-click="georssImport.accept()" ng-disabled="georssForm.$invalid" title="Import the file">
      <i class="fa fa-check"></i>
      OK
    </button>
    <button class="btn btn-secondary" ng-click="georssImport.cancel()" title="Cancel file import">
      <i class="fa fa-ban"></i>
      Cancel
    </button>
  </div>

</div>

Cool. Now let’s undo our launcher changes from the last step and make it look like this:

src/plugin/georss/georssimportui.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
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
goog.declareModuleId('plugin.georss.GeoRSSImportUI');

import './georssimport.js';

import FileParserConfig from 'opensphere/src/os/parse/fileparserconfig.js';
import FileImportUI from 'opensphere/src/os/ui/im/fileimportui.js';
import {create} from 'opensphere/src/os/ui/window.js';


/**
 * GeoRSS import UI.
 */
export default class GeoRSSImportUI extends FileImportUI {
  /**
   * Constructor.
   */
  constructor() {
    super();
  }

  // Let's be honest, testing getters like this is pedantic. Let's ignore it
  // this time.
  /* istanbul ignore next */
  /**
   * @inheritDoc
   */
  getTitle() {
    return 'GeoRSS';
  }

  // TODO: This function doesn't do much yet, after it does, let's test the
  // finished product.
  /* istanbul ignore next */
  /**
   * @inheritDoc
   */
  launchUI(file, opt_config) {
    super.launchUI(file, opt_config);

    const config = new FileParserConfig();

    // if an existing config was provided, merge it in
    if (opt_config) {
      this.mergeConfig(opt_config, config);
    }

    config['file'] = file;
    config['title'] = file.getFileName();

    const scopeOptions = {
      'config': config
    };
    const windowOptions = {
      'label': 'Import GeoRSS',
      'icon': 'fa fa-file-text',
      'x': 'center',
      'y': 'center',
      'width': 350,
      'min-width': 350,
      'max-width': 600,
      'height': 'auto',
      'modal': true,
      'show-close': true
    };
    const template = '<georssimport></georssimport>';
    create(windowOptions, template, undefined, undefined, undefined, scopeOptions);
  }
}

To test that Import UI, we need a few tests:

test/plugin/georss/georssimportui.test.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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
goog.require('os.file');
goog.require('plugin.georss.GeoRSSImportUI');

describe('plugin.georss.GeoRSSImportUI', function() {
  const {createFromContent} = goog.module.get('os.file');
  const {default: GeoRSSImportUI} = goog.module.get('plugin.georss.GeoRSSImportUI');

  const formSelector = 'form[name="georssForm"]';

  it('should have the proper title', function() {
    var importui = new GeoRSSImportUI();
    expect(importui.getTitle()).toBe('GeoRSS');
  });

  it('should launch an import UI with an empty config', function() {
    var importui = new GeoRSSImportUI();
    var file = createFromContent('testname.rss', 'http://www.example.com/testname.rss', undefined, '<?xml version="1.0" encoding="utf-8"?><feed/>');
    spyOn(os.ui.window, 'create');
    importui.launchUI(file, {});
    expect(os.ui.window.create).toHaveBeenCalled();
  });

  it('should launch an import UI with a null config', function() {
    var importui = new GeoRSSImportUI();
    var file = createFromContent('testname.rss', 'http://www.example.com/testname.rss', undefined, '<?xml version="1.0" encoding="utf-8"?><feed/>');
    importui.launchUI(file, undefined);

    waitsFor(() => !!document.querySelector(formSelector), 'import ui to open');

    runs(() => {
      const formEl = document.querySelector(formSelector);
      const scope = $(formEl).scope();
      expect(scope).toBeDefined();
      expect(scope.georssImport).toBeDefined();

      scope.georssImport.cancel();
    });

    waitsFor(() => !document.querySelector(formSelector), 'import ui to close');
  });

  it('should launch an import UI with a config', function() {
    var importui = new GeoRSSImportUI();
    var file = createFromContent('testname.rss', 'http://www.example.com/testname.rss', undefined, '<?xml version="1.0" encoding="utf-8"?><feed/>');
    importui.launchUI(file, {'title': 'other'});

    waitsFor(() => !!document.querySelector(formSelector), 'import ui to open');

    runs(() => {
      const formEl = document.querySelector(formSelector);
      const scope = $(formEl).scope();
      expect(scope).toBeDefined();
      expect(scope.georssImport).toBeDefined();

      scope.georssImport.cancel();
    });

    waitsFor(() => !document.querySelector(formSelector), 'import ui to close');
  });
});

Save, build, test, and pull it up.

  1. Go to Add Data > GeoRSS Files and delete any entries under there by highlighting and clicking the trash can
  2. Import https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.atom or your URL again

This time it should launch the UI that we just made. Change the title, description, tags, and or color and hit “OK” to save it.

Most of our import UIs are not quite this simple. Even GeoJSON requires the user to set up time mappings. Our format has the updated field which contains a time, so let’s get that supported in order for the layer to animate properly.