Parser

The first thing we need is a parser that can take the file and turn it into usable ol.Feature instances.

src/plugin/georss/georssparser.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
 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
goog.declareModuleId('plugin.georss.GeoRSSParser');

import {PROJECTION} from 'opensphere/src/os/map/map.js';

const Feature = goog.require('ol.Feature');
const LineString = goog.require('ol.geom.LineString');
const Point = goog.require('ol.geom.Point');
const Polygon = goog.require('ol.geom.Polygon');
const {isDocument, parse} = goog.require('ol.xml');

const {default: IParser} = goog.requireType('os.parse.IParser');


/**
 * Parser for GeoRSS feeds
 * @implements {IParser<Feature>}
 * @template T
 * @constructor
 */
export default class GeoRSSParser {
  /**
   * Constructor.
   */
  constructor() {
    /**
     * @type {?Document}
     * @protected
     */
    this.document = null;

    /**
     * @type {?NodeList}
     * @protected
     */
    this.entries = null;

    /**
     * @type {number}
     * @protected
     */
    this.nextIndex = 0;
  }

  /**
   * @inheritDoc
   */
  setSource(source) {
    if (isDocument(source)) {
      this.document = /** @type {Document} */ (source);
    } else if (typeof source === 'string') {
      this.document = parse(source);
    }

    if (this.document) {
      this.entries = this.document.querySelectorAll('entry');
    }
  }

  /**
   * @inheritDoc
   */
  cleanup() {
    this.document = null;
    this.entries = null;
    this.nextIndex = 0;
  }

  /**
   * @inheritDoc
   */
  hasNext() {
    return this.entries != null && this.entries.length > this.nextIndex;
  }

  /**
   * @inheritDoc
   */
  parseNext() {
    var nextEntry = this.entries[this.nextIndex++];
    var children = nextEntry.childNodes;
    var properties = {};

    for (var i = 0, n = children.length; i < n; i++) {
      var el = /** @type {Element} */ (children[i]);

      if (el.localName === 'link') {
        properties[el.localName] = el.getAttribute('href');
      } else if (el.namespaceURI === 'http://www.georss.org/georss') {
        var geom = parseGeometry(el);
        if (geom) {
          properties['geometry'] = geom;
        }
      } else {
        properties[el.localName] = el.textContent;
      }
    }

    return new Feature(properties);
  }
}

/**
 * @param {Element} el The element to parse
 * @return {ol.geom.Geometry|undefined} the geometry
 */
export const parseGeometry = function(el) {
  switch (el.localName) {
    case 'point':
      return parsePoint(el);
    case 'line':
      return parseLine(el);
    case 'polygon':
      return parsePolygon(el);
    default:
      break;
  }
};

/**
 * @param {Element} el The element to parse
 * @return {Point|undefined} The point geometry
 */
const parsePoint = function(el) {
  var coords = parseCoords(el);

  if (!coords || coords.length === 0) {
    // no coords found!
    return;
  }

  return new Point(coords[0]);
};

/**
 * @param {Element} el The element to parse
 * @return {LineString|undefined} The line geometry
 * @private
 */
const parseLine = function(el) {
  var coords = parseCoords(el);

  if (!coords) {
    // no coords found!
    return;
  }

  if (coords.length < 2) {
    // need at least 2 coords for line!
    return;
  }

  return new LineString(coords);
};

/**
 * @param {Element} el The element to parse
 * @return {Polygon|undefined} The polygon geometry
 */
const parsePolygon = function(el) {
  var coords = parseCoords(el);

  if (!coords) {
    // no coords found!
    return;
  }

  if (coords.length < 3) {
    // need at least 3 coords for polygon!
    return;
  }

  return new Polygon([coords]);
};

/**
 * @param {Element} el The element to parse
 * @return {Array<ol.Coordinate>|undefined} The array of coordinates
 */
const parseCoords = function(el) {
  var parts = el.textContent.trim().split(/\s+/);

  if (parts.length % 2 !== 0) {
    // odd amount of numbers, cannot produce pairs!
    return;
  }

  var coords = [];
  for (var i = 1, n = parts.length; i < n; i += 2) {
    var lat = parseFloat(parts[i - 1]);
    var lon = parseFloat(parts[i]);

    if (isNaN(lat) || isNaN(lon)) {
      // could not parse all lat/lons of coordinates!
      return;
    }

    var coord = [lon, lat];

    // convert to the application projection
    coords.push(ol.proj.fromLonLat(coord, PROJECTION));
  }

  return coords;
};

Whew. That was a lot for one step. It is not exhaustive, and a full implementation would want to support RSS in addition to Atom as well as the <georss:elev> tag. However, it still would not be complete without some tests.

test/plugin/georss/georssparser.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
 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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
goog.require('ol.xml');
goog.require('plugin.georss.GeoRSSParser');

describe('plugin.georss.GeoRSSParser', function() {
  const {parse} = goog.module.get('ol.xml');
  const {default: GeoRSSParser} = goog.module.get('plugin.georss.GeoRSSParser');

  it('should parse points correctly', function() {
    var el = parse('<point>  40    -105  ' +
      '</point>').firstElementChild;

    var geom = GeoRSSParser.parseGeometry(el);

    expect(geom instanceof ol.geom.Point).toBe(true);
    expect(geom.getCoordinates()[0]).toBe(-105);
    expect(geom.getCoordinates()[1]).toBe(40);
  });

  it('should choose the first point if there is more than one', function() {
    var el = parse('<point>40 -105 50 -95</point>').firstElementChild;

    var geom = GeoRSSParser.parseGeometry(el);

    expect(geom instanceof ol.geom.Point).toBe(true);
    expect(geom.getCoordinates()[0]).toBe(-105);
    expect(geom.getCoordinates()[1]).toBe(40);
  });

  it('should return undefined when pairs for point are incomplete', function() {
    var el = parse('<point>40 -105 50</point>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should return undefined when points do not contain adequate coordinate pairs', function() {
    var el = parse('<point></point>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should not parse nonsense', function() {
    var el = parse('<point>10 yang</point>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);

    el = parse('<point>ying 10</point>').firstElementChild;
    geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should parse lines correctly', function() {
    var el = parse('<line>40 100 50 110</line>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom instanceof ol.geom.LineString).toBe(true);
    expect(geom.getCoordinates()[0][0]).toBe(100);
    expect(geom.getCoordinates()[0][1]).toBe(40);
    expect(geom.getCoordinates()[1][0]).toBe(110);
    expect(geom.getCoordinates()[1][1]).toBe(50);
  });

  it('should return undefined when pairs for lines are incomplete', function() {
    var el = parse('<line>40 100 50 110 60</line>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should return undefined when lines do not contain adequate coordinate pairs', function() {
    var el = parse('<line>40 100</line>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should parse polygons correctly', function() {
    var el = parse('<polygon>40 100 50 110 60 100</polygon>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom instanceof ol.geom.Polygon).toBe(true);
    expect(geom.getCoordinates()[0][0][0]).toBe(100);
    expect(geom.getCoordinates()[0][0][1]).toBe(40);
    expect(geom.getCoordinates()[0][1][0]).toBe(110);
    expect(geom.getCoordinates()[0][1][1]).toBe(50);
    expect(geom.getCoordinates()[0][2][0]).toBe(100);
    expect(geom.getCoordinates()[0][2][1]).toBe(60);
  });

  it('should return undefined when pairs for polygons are incomplete', function() {
    var el = parse('<polygon>40 100 50 110 60 100 70</polygon>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should return undefined when polygons do not contain adequate coordinate pairs', function() {
    var el = parse('<polygon>40 100 50 110</polygon>').firstElementChild;
    var geom = GeoRSSParser.parseGeometry(el);
    expect(geom).toBe(undefined);
  });

  it('should return undefined for incorrect tag names', function() {
    var el = parse('<something>is wrong here</something>').firstElementChild;
    expect(GeoRSSParser.parseGeometry(el)).toBe(undefined);
  });

  it('should parse GeoRSS feeds', function() {
    var p = new GeoRSSParser();

    var feed = '<?xml version="1.0" encoding="utf-8"?>' +
      '<feed xmlns="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss">' +
         '<title>Earthquakes</title>' +
         '<subtitle>International earthquake observation labs</subtitle>' +
         '<link href="http://example.org/"/>' +
         '<updated>2005-12-13T18:30:02Z</updated>' +
         '<author>' +
            '<name>Dr. Thaddeus Remor</name>' +
            '<email>tremor@quakelab.edu</email>' +
         '</author>' +
         '<id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>' +
         '<entry>' +
            '<title>M 3.2, Mona Passage</title>' +
            '<link href="http://example.org/2005/09/09/atom01"/>' +
            '<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>' +
            '<updated>2005-08-17T07:02:32Z</updated>' +
            '<summary>We just had a big one.</summary>' +
            '<georss:point>45.256 -71.92</georss:point>' +
            // we want to make sure this is properly ignored for now, we'll leave it as an
            // exercise for the user to implement it
            '<georss:elev>-19372</georss:elev>' +
          '</entry>' +
      '</feed>';

    p.setSource(feed);
    expect(p.hasNext()).toBe(true);

    var source = parse(feed);
    p.setSource(source);
    expect(p.hasNext()).toBe(true);

    var feature = p.parseNext();

    expect(p.hasNext()).toBe(false);
    expect(feature instanceof ol.Feature).toBe(true);
    expect(feature.get('title')).toBe('M 3.2, Mona Passage');
    expect(feature.get('link')).toBe('http://example.org/2005/09/09/atom01');
    expect(feature.get('id')).toBe('urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
    expect(feature.get('updated')).toBe('2005-08-17T07:02:32Z');
    expect(feature.get('summary')).toBe('We just had a big one.');
    expect(feature.getGeometry() instanceof ol.geom.Point).toBe(true);

    p.cleanup();
    expect(p.hasNext()).toBe(false);
    expect(p.document).toBe(null);
    expect(p.entries).toBe(null);
    expect(p.nextIndex).toBe(0);
  });

  it('should not use other potential sources', function() {
    var p = new GeoRSSParser();
    p.setSource({something: true});
    expect(p.document).toBe(null);
    expect(p.hasNext()).toBe(false);
  });
});

There. Now we can fully test our parser with yarn test.