diff --git a/examples/data/gpx/yahoo.xml b/examples/data/gpx/yahoo.xml
new file mode 100644
index 0000000000..ccaa9bc5cc
--- /dev/null
+++ b/examples/data/gpx/yahoo.xml
@@ -0,0 +1,1073 @@
+
+
+
+
+
+ 44.586548
+
+ 5066
+
+ Crossing
+
+
+
+ 57.607200
+
+ 5067
+
+ Dot
+
+
+
+ 44.826904
+
+ 5096
+
+ Dot
+
+
+
+ 50.594727
+
+ 5142
+
+ Dot
+
+
+
+ 127.711200
+
+ 5156
+
+ Dot
+
+
+
+ 96.926400
+
+ 5224
+
+ Dot
+
+
+
+ 82.600800
+
+ 5229
+
+ Dot
+
+
+
+ 82.905600
+
+ 5237
+
+ Dot
+
+
+
+ 66.696655
+
+ 5254
+
+ Dot
+
+
+
+ 74.627442
+
+ 5258
+
+ Dot
+
+
+
+ 65.254761
+
+ 5264
+
+ Dot
+
+
+
+ 77.419200
+
+ 526708
+
+ Dot
+
+
+
+ 74.676000
+
+ 526750
+
+ Dot
+
+
+
+ 78.713135
+
+ 527614
+
+ Dot
+
+
+
+ 78.713135
+
+ 527631
+
+ Dot
+
+
+
+ 68.275200
+
+ 5278
+
+ Dot
+
+
+
+ 64.008000
+
+ 5289
+
+ Dot
+
+
+
+ 52.997925
+
+ 5374FIRE
+
+ Dot
+
+
+
+ 56.388000
+
+ 5376
+
+ Dot
+
+
+
+ 56.388000
+
+ 6006
+
+ Dot
+
+
+
+ 46.028564
+
+ 6006BLUE
+
+ Dot
+
+
+
+ 37.616943
+
+ 6014MEADOW
+
+ Dot
+
+
+
+ 56.388000
+
+ 6029
+
+ Dot
+
+
+
+ 50.292000
+
+ 6053
+
+ Dot
+
+
+
+ 25.603200
+
+ 6066
+
+ Dot
+
+
+
+ 34.442400
+
+ 6067
+
+ Dot
+
+
+
+ 30.480000
+
+ 6071
+
+ Dot
+
+
+
+ 15.240000
+
+ 6073
+
+ Dot
+
+
+
+ 37.795200
+
+ 6084
+
+ Dot
+
+
+
+ 64.008000
+
+ 6130
+
+ Dot
+
+
+
+ 64.008000
+
+ 6131
+
+ Dot
+
+
+
+ 62.788800
+
+ 6153
+
+ Dot
+
+
+
+ 55.473600
+
+ 6171
+
+ Dot
+
+
+
+ 62.484000
+
+ 6176
+
+ Dot
+
+
+
+ 62.179200
+
+ 6177
+
+ Dot
+
+
+
+ 69.799200
+
+ 6272
+
+ Dot
+
+
+
+ 73.152000
+
+ 6272
+
+ Dot
+
+
+
+ 70.104000
+
+ 6278
+
+ Dot
+
+
+
+ 57.564209
+
+ 6280
+
+ Dot
+
+
+
+ 66.696655
+
+ 6283
+
+ Dot
+
+
+
+ 72.945191
+
+ 6289
+
+ Dot
+
+
+
+ 72.847200
+
+ 6297
+
+ Dot
+
+
+
+ 53.644800
+
+ 6328
+
+ Dot
+
+
+
+ 43.891200
+
+ 6354
+
+ Dot
+
+
+
+ 48.768000
+
+ 635722
+
+ Dot
+
+
+
+ 49.072800
+
+ 635783
+
+ Dot
+
+
+
+ 62.484000
+
+ 6373
+
+ Dot
+
+
+
+ 3.962400
+
+ 6634
+
+ Dot
+
+
+
+ 13.411200
+
+ 6979
+
+ Dot
+
+
+
+ 34.012085
+
+ 6997
+
+ Dot
+
+
+
+ 87.782400
+
+ BEAR HILL
+ BEAR HILL TOWER
+
+ Tall Tower
+
+
+
+ 23.469600
+
+ BELLEVUE
+ BELLEVUE
+
+ Parking Area
+
+
+
+ 43.384766
+
+ 6016
+
+ Trailhead
+
+
+
+ 89.916000
+
+ 5236BRIDGE
+
+ Bridge
+
+
+
+ 55.473600
+
+ 5376BRIDGE
+
+ Bridge
+
+
+
+ 52.730400
+
+ 6181CROSS
+
+ Crossing
+
+
+
+ 45.110400
+
+ 6042CROSS
+
+ Crossing
+
+
+
+ DARKHOLLPO
+
+ Fishing Area
+
+
+ 56.083200
+
+ 6121DEAD
+
+ Danger Area
+
+
+
+ 117.043200
+
+ 5179DEAD
+
+ Danger Area
+
+
+
+ 69.494400
+
+ 5299DEAD
+
+ Danger Area
+
+
+
+ 56.997600
+
+ 5376DEAD
+
+ Danger Area
+
+
+
+ 46.939200
+
+ 6353DEAD
+
+ Danger Area
+
+
+
+ 61.264800
+
+ 6155DEAD
+
+ Danger Area
+
+
+
+ 110.947200
+
+ GATE14
+
+ Truck Stop
+
+
+
+ 77.724000
+
+ GATE16
+
+ Truck Stop
+
+
+
+ 65.836800
+
+ GATE17
+
+ Truck Stop
+
+
+
+ 57.302400
+
+ GATE19
+
+ Truck Stop
+
+
+
+ 49.377600
+
+ GATE21
+
+ Truck Stop
+
+
+
+ 81.076800
+
+ GATE24
+
+ Truck Stop
+
+
+
+ 21.515015
+
+ GATE5
+
+ Truck Stop
+
+
+
+ 26.561890
+
+ GATE6
+
+ Trailhead
+
+
+
+ 32.004000
+
+ 6077LOGS
+
+ Amusement Park
+
+
+
+ 119.809082
+
+ 5148NANEPA
+
+ Trailhead
+
+
+
+ 73.761600
+
+ 5267OBSTAC
+
+ Amusement Park
+
+
+
+ 45.307495
+
+ PANTHRCAVE
+
+ Tunnel
+
+
+
+ 77.992066
+
+ 5252PURPLE
+
+ Summit
+
+
+
+ 67.970400
+
+ 5287WATER
+
+ Swimming Area
+
+
+
+ 81.076800
+
+ 5239ROAD
+
+ Truck Stop
+
+
+
+ 67.360800
+
+ 5278ROAD
+
+ Truck Stop
+
+
+
+ 53.949600
+
+ 5058ROAD
+ ROAD CROSSING
+
+ Dot
+
+
+
+ 69.799200
+
+ SHEEPFOLD
+
+ Parking Area
+
+
+
+ 64.008000
+
+ SOAPBOX
+
+ Cemetery
+
+
+
+ 64.533692
+
+ 5376STREAM
+
+ Bridge
+
+
+
+ 61.649902
+
+ 5144SUMMIT
+
+ Summit
+
+
+
+ 67.360800
+
+ 5150TANK
+ WATER TANK
+
+ Museum
+
+
+
+ BELLEVUE
+
+ 1
+
+ 23.469600
+
+ BELLEVUE
+ BELLEVUE
+
+ Parking Area
+
+
+
+ 26.561890
+
+ GATE6
+
+ Trailhead
+
+
+
+ 45.307495
+
+ PANTHRCAVE
+
+ Tunnel
+
+
+
+ 37.616943
+
+ 6014MEADOW
+
+ Dot
+
+
+
+ 56.388000
+
+ 6006
+
+ Dot
+
+
+
+ 46.028564
+
+ 6006BLUE
+
+ Dot
+
+
+
+ 44.826904
+
+ 5096
+
+ Dot
+
+
+
+ 44.586548
+
+ 5066
+
+ Crossing
+
+
+
+ 57.607200
+
+ 5067
+
+ Dot
+
+
+
+ 53.949600
+
+ 5058ROAD
+ ROAD CROSSING
+
+ Dot
+
+
+
+ 67.360800
+
+ 5150TANK
+ WATER TANK
+
+ Museum
+
+
+
+ 50.594727
+
+ 5142
+
+ Dot
+
+
+
+ 61.649902
+
+ 5144SUMMIT
+
+ Summit
+
+
+
+ 127.711200
+
+ 5156
+
+ Dot
+
+
+
+ 119.809082
+
+ 5148NANEPA
+
+ Trailhead
+
+
+
+ 74.627442
+
+ 5258
+
+ Dot
+
+
+
+ 77.992066
+
+ 5252PURPLE
+
+ Summit
+
+
+
+ 78.713135
+
+ 527631
+
+ Dot
+
+
+
+ 78.713135
+
+ 527614
+
+ Dot
+
+
+
+ 73.761600
+
+ 5267OBSTAC
+
+ Amusement Park
+
+
+
+ 68.275200
+
+ 5278
+
+ Dot
+
+
+
+ 64.008000
+
+ 5289
+
+ Dot
+
+
+
+ 52.997925
+
+ 5374FIRE
+
+ Dot
+
+
+
+ 56.388000
+
+ 5376
+
+ Dot
+
+
+
+ 64.533692
+
+ 5376STREAM
+
+ Bridge
+
+
+
+ 53.644800
+
+ 6328
+
+ Dot
+
+
+
+ 48.768000
+
+ 635722
+
+ Dot
+
+
+
+ 49.072800
+
+ 635783
+
+ Dot
+
+
+
+ 62.484000
+
+ 6373
+
+ Dot
+
+
+
+ 87.782400
+
+ BEAR HILL
+ BEAR HILL TOWER
+
+ Tall Tower
+
+
+
+ 72.945191
+
+ 6289
+
+ Dot
+
+
+
+ 72.847200
+
+ 6297
+
+ Dot
+
+
+
+ 66.696655
+
+ 6283
+
+ Dot
+
+
+
+ 57.564209
+
+ 6280
+
+ Dot
+
+
+
+ 62.179200
+
+ 6177
+
+ Dot
+
+
+
+ 62.484000
+
+ 6176
+
+ Dot
+
+
+
+ 62.788800
+
+ 6153
+
+ Dot
+
+
+
+ 55.473600
+
+ 6171
+
+ Dot
+
+
+
+ 64.008000
+
+ 6131
+
+ Dot
+
+
+
+ 64.008000
+
+ 6130
+
+ Dot
+
+
+
+ 56.388000
+
+ 6029
+
+ Dot
+
+
+
+ 56.388000
+
+ 6006
+
+ Dot
+
+
+
+ 37.616943
+
+ 6014MEADOW
+
+ Dot
+
+
+
+ 45.307495
+
+ PANTHRCAVE
+
+ Tunnel
+
+
+
+ 26.561890
+
+ GATE6
+
+ Trailhead
+
+
+
+ 23.469600
+
+ BELLEVUE
+ BELLEVUE
+
+ Parking Area
+
+
+
+
+
diff --git a/examples/gpx.html b/examples/gpx.html
new file mode 100644
index 0000000000..1d557850f0
--- /dev/null
+++ b/examples/gpx.html
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+ GPX example
+
+
+
+
+
+
+
+
+
+
+
+
+
GPX example
+
Example of using the GPX parser.
+
+
GPX
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/gpx.js b/examples/gpx.js
new file mode 100644
index 0000000000..771fa6becf
--- /dev/null
+++ b/examples/gpx.js
@@ -0,0 +1,49 @@
+goog.require('ol.Collection');
+goog.require('ol.Map');
+goog.require('ol.RendererHint');
+goog.require('ol.View2D');
+goog.require('ol.layer.TileLayer');
+goog.require('ol.layer.Vector');
+goog.require('ol.parser.GPX');
+goog.require('ol.projection');
+goog.require('ol.source.OSM');
+goog.require('ol.source.Vector');
+
+var raster = new ol.layer.TileLayer({
+ source: new ol.source.OSM()
+});
+
+var vector = new ol.layer.Vector({
+ source: new ol.source.Vector({
+ projection: ol.projection.get('EPSG:4326')
+ })
+});
+
+var map = new ol.Map({
+ layers: new ol.Collection([raster, vector]),
+ renderer: ol.RendererHint.CANVAS,
+ target: 'map',
+ view: new ol.View2D({
+ center: [-7916461.9312699, 5226343.9091441],
+ zoom: 11
+ })
+});
+
+var gpx = new ol.parser.GPX();
+
+var url = 'data/gpx/yahoo.xml';
+var xhr = new XMLHttpRequest();
+xhr.open('GET', url, true);
+
+
+/**
+ * onload handler for the XHR request.
+ */
+xhr.onload = function() {
+ if (xhr.status == 200) {
+ // this is silly to have to tell the layer the destination projection
+ var projection = map.getView().getProjection();
+ vector.parseFeatures(xhr.responseText, gpx, projection);
+ }
+};
+xhr.send();
diff --git a/src/objectliterals.jsdoc b/src/objectliterals.jsdoc
index 3c8d01753e..02269b07e0 100644
--- a/src/objectliterals.jsdoc
+++ b/src/objectliterals.jsdoc
@@ -340,6 +340,31 @@
* geometries. Default is `false´. This only applies to GML version 3.
*/
+/**
+ * @typedef {Object} ol.parser.GPXOptions
+ * @property {string|undefined} creator The creator attribute to be added to
+ * the written GPX files. Defaults to `OpenLayers´.
+ * @property {string|undefined} defaultDesc Default description for the
+ * waypoints/tracks in the case where the feature has no 'description'
+ * attribute. Default is 'No description available'.
+ * @property {boolean|undefined} extractAttributes Should we extract attributes
+ * from the GPX? Default is `true´.
+ * @property {boolean|undefined} extractWaypoints Extract waypoints from GPX.
+ * Default is `true´.
+ * @property {boolean|undefined} extractTracks Extract tracks from GPX.
+ * Default is `true´.
+ * @property {boolean|undefined} extractRoutes Extract routes from GPX.
+ * Default is `true´.
+ */
+
+/**
+ * @typedef {Object} ol.parser.GPXWriteOptions
+ * @property {Array.|ol.Feature} features The features to write
+ * out.
+ * @property {Object|undefined} metadata Metadata key/value pair with keys:
+ * name, desc and author.
+ */
+
/**
* @typedef {Object} ol.source.BingMapsOptions
* @property {string|undefined} culture Culture.
diff --git a/src/ol/parser/gpx.exports b/src/ol/parser/gpx.exports
new file mode 100644
index 0000000000..57d874bb80
--- /dev/null
+++ b/src/ol/parser/gpx.exports
@@ -0,0 +1,3 @@
+@exportSymbol ol.parser.GPX
+@exportProperty ol.parser.GPX.prototype.read
+@exportProperty ol.parser.GPX.prototype.write
diff --git a/src/ol/parser/gpx.js b/src/ol/parser/gpx.js
new file mode 100644
index 0000000000..de2b26cbbd
--- /dev/null
+++ b/src/ol/parser/gpx.js
@@ -0,0 +1,308 @@
+goog.provide('ol.parser.GPX');
+
+goog.require('goog.dom.xml');
+goog.require('ol.Feature');
+goog.require('ol.geom.GeometryType');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.Polygon');
+goog.require('ol.parser.DomFeatureParser');
+goog.require('ol.parser.ObjectFeatureParser');
+goog.require('ol.parser.ReadFeaturesOptions');
+goog.require('ol.parser.StringFeatureParser');
+goog.require('ol.parser.XML');
+
+
+
+/**
+ * @constructor
+ * @implements {ol.parser.DomFeatureParser}
+ * @implements {ol.parser.StringFeatureParser}
+ * @implements {ol.parser.ObjectFeatureParser}
+ * @param {ol.parser.GPXOptions=} opt_options Optional configuration object.
+ * @extends {ol.parser.XML}
+ */
+ol.parser.GPX = function(opt_options) {
+ var options = /** @type {ol.parser.GPXOptions} */
+ (goog.isDef(opt_options) ? opt_options : {});
+ this.extractAttributes = goog.isDef(options.extractAttributes) ?
+ options.extractAttributes : true;
+ this.extractWaypoints = goog.isDef(options.extractWaypoints) ?
+ options.extractWaypoints : true;
+ this.extractTracks = goog.isDef(options.extractTracks) ?
+ options.extractTracks : true;
+ this.extractRoutes = goog.isDef(options.extractRoutes) ?
+ options.extractRoutes : true;
+ this.creator = goog.isDef(options.creator) ?
+ options.creator : 'OpenLayers';
+ this.defaultDesc = goog.isDef(options.defaultDesc) ?
+ options.defaultDesc : 'No description available';
+ this.defaultNamespaceURI = 'http://www.topografix.com/GPX/1/1';
+ this.schemaLocation = 'http://www.topografix.com/GPX/1/1 ' +
+ 'http://www.topografix.com/GPX/1/1/gpx.xsd';
+ this.readers = {
+ 'http://www.topografix.com/GPX/1/1': {
+ 'gpx': function(node, obj) {
+ if (!goog.isDef(obj.features)) {
+ obj.features = [];
+ }
+ this.readChildNodes(node, obj);
+ },
+ 'wpt': function(node, obj) {
+ if (this.extractWaypoints) {
+ var properties = {};
+ var coordinates = [parseFloat(node.getAttribute('lon')),
+ parseFloat(node.getAttribute('lat'))];
+ this.readChildNodes(node, properties);
+ var feature = new ol.Feature(properties);
+ var sharedVertices;
+ if (this.readFeaturesOptions_) {
+ var callback = this.readFeaturesOptions_.callback;
+ if (callback) {
+ sharedVertices = callback(feature, ol.geom.GeometryType.POINT);
+ }
+ }
+ var geometry = new ol.geom.Point(coordinates, sharedVertices);
+ feature.setGeometry(geometry);
+ obj.features.push(feature);
+ }
+ },
+ 'rte': function(node, obj) {
+ if (this.extractRoutes || obj.force) {
+ var type = ol.geom.GeometryType.LINESTRING;
+ var container = {
+ properties: {},
+ geometry: {
+ type: type,
+ coordinates: []
+ }
+ };
+ this.readChildNodes(node, container);
+ var feature = new ol.Feature(container.properties);
+ var sharedVertices;
+ if (this.readFeaturesOptions_) {
+ var callback = this.readFeaturesOptions_.callback;
+ if (callback) {
+ sharedVertices = callback(feature, type);
+ }
+ }
+ var geometry = new ol.geom.LineString(container.geometry.coordinates,
+ sharedVertices);
+ feature.setGeometry(geometry);
+ obj.features.push(feature);
+ }
+ },
+ 'rtept': function(node, container) {
+ var coordinate = [parseFloat(node.getAttribute('lon')),
+ parseFloat(node.getAttribute('lat'))];
+ container.geometry.coordinates.push(coordinate);
+ },
+ 'trk': function(node, obj) {
+ if (this.extractTracks) {
+ var readers = this.readers[this.defaultNamespaceURI];
+ obj.force = true;
+ readers['rte'].apply(this, arguments);
+ }
+ },
+ 'trkseg': function(node, container) {
+ this.readChildNodes(node, container);
+ },
+ 'trkpt': function(node, container) {
+ var readers = this.readers[this.defaultNamespaceURI];
+ readers['rtept'].apply(this, arguments);
+ },
+ '*': function(node, obj) {
+ if (this.extractAttributes === true) {
+ var len = node.childNodes.length;
+ if ((len === 1 || len === 2) && (node.firstChild.nodeType === 3 ||
+ node.firstChild.nodeType === 4)) {
+ var readers = this.readers[this.defaultNamespaceURI];
+ readers['_attribute'].apply(this, arguments);
+ }
+ }
+ },
+ '_attribute': function(node, obj) {
+ var local = node.localName || node.nodeName.split(':').pop();
+ var value = this.getChildValue(node);
+ if (obj.properties) {
+ obj.properties[local] = value.replace(this.regExes.trimSpace, '');
+ }
+ }
+ }
+ };
+ // create an alias for GXP 1.0
+ this.readers['http://www.topografix.com/GPX/1/0'] =
+ this.readers[this.defaultNamespaceURI];
+ this.writers = {
+ 'http://www.topografix.com/GPX/1/1': {
+ '_feature': function(feature) {
+ var geom = feature.getGeometry();
+ if (geom instanceof ol.geom.Point) {
+ return this.writeNode('wpt', feature);
+ } else if ((geom instanceof ol.geom.LineString) ||
+ (geom instanceof ol.geom.MultiLineString) ||
+ (geom instanceof ol.geom.Polygon)) {
+ return this.writeNode('trk', feature);
+ }
+ },
+ 'wpt': function(feature) {
+ var node = this.createElementNS('wpt');
+ var geom = feature.getGeometry();
+ var coordinates = geom.getCoordinates();
+ node.setAttribute('lon', coordinates[0]);
+ node.setAttribute('lat', coordinates[1]);
+ var attributes = feature.getAttributes();
+ var name = attributes['name'] || goog.getUid(feature).toString();
+ this.writeNode('name', name, undefined, node);
+ var desc = attributes['description'] || this.defaultDesc;
+ this.writeNode('desc', desc, undefined, node);
+ return node;
+ },
+ 'trk': function(feature) {
+ var attributes = feature.getAttributes();
+ var node = this.createElementNS('trk');
+ var name = attributes['name'] || goog.getUid(feature).toString();
+ this.writeNode('name', name, undefined, node);
+ var desc = attributes['description'] || this.defaultDesc;
+ this.writeNode('desc', desc, undefined, node);
+ var geom = feature.getGeometry();
+ var i, ii;
+ if (geom instanceof ol.geom.LineString) {
+ this.writeNode('trkseg', feature.getGeometry(), undefined, node);
+ } else if (geom instanceof ol.geom.MultiLineString) {
+ for (i = 0, ii = geom.components.length; i < ii; ++i) {
+ this.writeNode('trkseg', geom.components[i], undefined, node);
+ }
+ } else if (geom instanceof ol.geom.Polygon) {
+ for (i = 0, ii = geom.rings.length; i < ii; ++i) {
+ this.writeNode('trkseg', geom.rings[i], undefined, node);
+ }
+ }
+ return node;
+ },
+ 'trkseg': function(geometry) {
+ var node = this.createElementNS('trkseg');
+ var coordinates = geometry.getCoordinates();
+ for (var i = 0, ii = coordinates.length; i < ii; ++i) {
+ this.writeNode('trkpt', coordinates[i], undefined, node);
+ }
+ return node;
+ },
+ 'trkpt': function(coord) {
+ var node = this.createElementNS('trkpt');
+ node.setAttribute('lon', coord[0]);
+ node.setAttribute('lat', coord[1]);
+ return node;
+ },
+ 'metadata': function(metadata) {
+ var node = this.createElementNS('metadata');
+ if (goog.isDef(metadata['name'])) {
+ this.writeNode('name', metadata['name'], undefined, node);
+ }
+ if (goog.isDef(metadata['desc'])) {
+ this.writeNode('desc', metadata['desc'], undefined, node);
+ }
+ if (goog.isDef(metadata['author'])) {
+ this.writeNode('author', metadata['author'], undefined, node);
+ }
+ return node;
+ },
+ 'name': function(name) {
+ var node = this.createElementNS('name');
+ node.appendChild(this.createTextNode(name));
+ return node;
+ },
+ 'desc': function(desc) {
+ var node = this.createElementNS('desc');
+ node.appendChild(this.createTextNode(desc));
+ return node;
+ },
+ 'author': function(author) {
+ var node = this.createElementNS('author');
+ node.appendChild(this.createTextNode(author));
+ return node;
+ }
+ }
+ };
+ goog.base(this);
+};
+goog.inherits(ol.parser.GPX, ol.parser.XML);
+
+
+/**
+ * @param {string|Document|Element|Object} data Data to read.
+ * @return {Object} An object representing the document.
+ */
+ol.parser.GPX.prototype.read = function(data) {
+ if (goog.isString(data)) {
+ data = goog.dom.xml.loadXml(data);
+ }
+ if (data && data.nodeType == 9) {
+ data = data.documentElement;
+ }
+ var obj = {};
+ this.readNode(data, obj);
+ return obj;
+};
+
+
+/**
+ * Parse a GPX document provided as a string.
+ * @param {string} str GPX document.
+ * @param {ol.parser.ReadFeaturesOptions=} opt_options Reader options.
+ * @return {Array.} Array of features.
+ */
+ol.parser.GPX.prototype.readFeaturesFromString =
+ function(str, opt_options) {
+ this.readFeaturesOptions_ = opt_options;
+ return this.read(str).features;
+};
+
+
+/**
+ * Parse a GPX document provided as a DOM structure.
+ * @param {Element|Document} node Document or element node.
+ * @param {ol.parser.ReadFeaturesOptions=} opt_options Feature reading options.
+ * @return {Array.} Array of features.
+ */
+ol.parser.GPX.prototype.readFeaturesFromNode =
+ function(node, opt_options) {
+ this.readFeaturesOptions_ = opt_options;
+ return this.read(node).features;
+};
+
+
+/**
+ * @param {Object} obj Object representing features.
+ * @param {ol.parser.ReadFeaturesOptions=} opt_options Feature reading options.
+ * @return {Array.} Array of features.
+ */
+ol.parser.GPX.prototype.readFeaturesFromObject =
+ function(obj, opt_options) {
+ this.readFeaturesOptions_ = opt_options;
+ return this.read(obj).features;
+};
+
+
+/**
+ * @param {ol.parser.GPXWriteOptions} obj Object structure to write out
+ * as GPX.
+ * @return {string} An string representing the GPX document.
+ */
+ol.parser.GPX.prototype.write = function(obj) {
+ var features = goog.isArray(obj.features) ? obj.features : [obj.features];
+ var root = this.createElementNS('gpx');
+ root.setAttribute('version', '1.1');
+ root.setAttribute('creator', this.creator);
+ this.setAttributeNS(
+ root, 'http://www.w3.org/2001/XMLSchema-instance',
+ 'xsi:schemaLocation', this.schemaLocation);
+ if (goog.isDef(obj.metadata)) {
+ this.writeNode('metadata', obj.metadata, undefined, root);
+ }
+ for (var i = 0, ii = features.length; i < ii; i++) {
+ this.writeNode('_feature', features[i], undefined, root);
+ }
+ return this.serialize(root);
+};
diff --git a/test/spec/ol/parser/gpx.test.js b/test/spec/ol/parser/gpx.test.js
new file mode 100644
index 0000000000..91bda4ed78
--- /dev/null
+++ b/test/spec/ol/parser/gpx.test.js
@@ -0,0 +1,129 @@
+goog.provide('ol.test.parser.gpx');
+
+describe('ol.parser.gpx', function() {
+
+ var parser = new ol.parser.GPX();
+
+ describe('Test GPX parser', function() {
+ it('Read works correctly', function() {
+ var url = 'spec/ol/parser/gpx/data.xml';
+ afterLoadXml(url, function(xml) {
+ var obj = parser.read(xml);
+ expect(obj.features.length).to.eql(3);
+ // waypoint feature
+ var feature = obj.features[0];
+ var geom = feature.getGeometry();
+ expect(geom.getType()).to.eql(ol.geom.GeometryType.POINT);
+ expect(geom.getCoordinates()).to.eql([-0.1853562259, 51.3697845627]);
+ // route feature
+ feature = obj.features[1];
+ geom = feature.getGeometry();
+ var attributes = feature.getAttributes();
+ expect(geom.getType()).to.eql(ol.geom.GeometryType.LINESTRING);
+ expect(geom.getCoordinates()).to.eql([[-0.1829991904, 51.3761803674],
+ [-0.1758887005, 51.3697894659], [-0.1833202965, 51.3639790884],
+ [-0.1751119509, 51.3567607069]]);
+ expect(attributes['name']).to.eql('Route8');
+ expect(attributes['type']).to.eql('Route');
+ // track feature
+ feature = obj.features[2];
+ geom = feature.getGeometry();
+ attributes = feature.getAttributes();
+ expect(geom.getType()).to.eql(ol.geom.GeometryType.LINESTRING);
+ expect(geom.getCoordinates()).to.eql([[-0.1721292044, 51.3768216433],
+ [-0.1649230916, 51.370833767], [-0.1736741378, 51.3644368725],
+ [-0.166259525, 51.3576354272]]);
+ expect(attributes['name']).to.eql('Track');
+ expect(attributes['type']).to.eql('Track');
+ });
+ });
+ it('Write works correctly for points', function() {
+ var feature1 = new ol.Feature({name: 'foo', description: 'bar'});
+ feature1.setGeometry(new ol.geom.Point([-111.04, 45.68]));
+ var feature2 = new ol.Feature({name: 'foo', description: 'bar'});
+ feature2.setGeometry(new ol.geom.Point([-112.04, 45.68]));
+ var output = parser.write({features: [feature1, feature2]});
+ var expected = 'foo' +
+ 'bar' +
+ 'foobar';
+ expect(goog.dom.xml.loadXml(expected)).to.xmleql(
+ goog.dom.xml.loadXml(output));
+ });
+ it('Write works correctly for lines', function() {
+ var feature1 = new ol.Feature({name: 'foo', description: 'bar'});
+ feature1.setGeometry(new ol.geom.LineString([[-111.04, 45.68],
+ [-112.04, 45.68]]));
+ var feature2 = new ol.Feature({name: 'dude', description: 'truite'});
+ feature2.setGeometry(new ol.geom.LineString([[1, 2], [3, 4]]));
+ var output = parser.write({features: [feature1, feature2]});
+ var expected = 'foobar' +
+ 'dudetruite' +
+ '' +
+ '';
+ expect(goog.dom.xml.loadXml(expected)).to.xmleql(
+ goog.dom.xml.loadXml(output));
+ });
+ it('Write works correctly for multilines', function() {
+ var multi = new ol.geom.MultiLineString([[[-111.04, 45.68],
+ [-112.04, 45.68]], [[1, 2], [3, 4]]]);
+ var feature = new ol.Feature({name: 'foo', description: 'bar'});
+ feature.setGeometry(multi);
+ var output = parser.write({features: [feature]});
+ var expected = 'foobar' +
+ '' +
+ '';
+ expect(goog.dom.xml.loadXml(expected)).to.xmleql(
+ goog.dom.xml.loadXml(output));
+ });
+ it('Write works correctly for polygon', function() {
+ var polygon = new ol.geom.Polygon([[[-111.04, 45.68],
+ [-112.04, 45.68], [-111.04, 45.68]]]);
+ var feature = new ol.Feature({name: 'foo', description: 'bar'});
+ feature.setGeometry(polygon);
+ var output = parser.write({features: [feature]});
+ var expected = '' +
+ 'foobar';
+ expect(goog.dom.xml.loadXml(expected)).to.xmleql(
+ goog.dom.xml.loadXml(output));
+ });
+ it('Write works correctly for metadata', function() {
+ var output = parser.write({features: [], metadata: {'name': 'foo',
+ 'desc': 'bar'}});
+ var expected = '' +
+ 'foobar';
+ expect(goog.dom.xml.loadXml(expected)).to.xmleql(
+ goog.dom.xml.loadXml(output));
+ });
+ });
+});
+
+goog.require('ol.geom.GeometryType');
+goog.require('goog.dom.xml');
+goog.require('ol.Feature');
+goog.require('ol.geom.Point');
+goog.require('ol.geom.LineString');
+goog.require('ol.geom.MultiLineString');
+goog.require('ol.geom.Polygon');
+goog.require('ol.parser.GPX');
diff --git a/test/spec/ol/parser/gpx/data.xml b/test/spec/ol/parser/gpx/data.xml
new file mode 100644
index 0000000000..3f1d0b8afb
--- /dev/null
+++ b/test/spec/ol/parser/gpx/data.xml
@@ -0,0 +1 @@
+Mark