From c7eedf50120a5dfa1f74dddc45b07fb93b0a14c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarno=20Le=20Cont=C3=A9?= Date: Tue, 17 May 2016 00:09:06 +0200 Subject: [PATCH 1/5] output gpx routes if option gpx.rte=true --- index.js | 174 +++++++++++++++++---------------- test/test.js | 266 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 360 insertions(+), 80 deletions(-) diff --git a/index.js b/index.js index eb7859a..35f4cd8 100644 --- a/index.js +++ b/index.js @@ -14,7 +14,10 @@ function togpx( geojson, options ) { featureTitle: get_feature_title, featureDescription: get_feature_description, featureLink: undefined, - featureCoordTimes: get_feature_coord_times, + featureCoordTimes: get_feature_coord_times, // provided function should return an array of UTC ISO 8601 timestamp strings + "gpx.wpt": true, // include waypoints in output + "gpx.trk": true, // include tracks in output + "gpx.rte": false, // include routes in output }, options || {}); function get_feature_title(props) { @@ -56,6 +59,13 @@ function togpx( geojson, options ) { if (options.featureLink) o.link = { "@href": options.featureLink(f.properties) } } + function defaults(obj1, obj2) { + for (var attr in obj2) + if (!obj1.hasOwnProperty(attr)) + obj1[attr] = obj2[attr]; + return obj1; + } + // make gpx object var gpx = {"gpx": { "@xmlns":"http://www.topografix.com/GPX/1/1", @@ -64,107 +74,99 @@ function togpx( geojson, options ) { "@version":"1.1", "wpt": [], "trk": [], + "rte": [], }}; if (options.creator) gpx.gpx["@creator"] = options.creator; if (options.metadata) gpx.gpx["metadata"] = options.metadata; - var features; - if (geojson.type === "FeatureCollection") - features = geojson.features; - else if (geojson.type === "Feature") - features = [geojson]; - else - features = [{type:"Feature", properties: {}, geometry: geojson}]; - features.forEach(function mapFeature(f) { - if (!f.hasOwnProperty('properties')) + function mapFeature(f, options) { + if (!f.hasOwnProperty('properties')) { f.properties = {}; + } switch (f.geometry.type) { // POIs case "Point": case "MultiPoint": - var coords = f.geometry.coordinates; - if (f.geometry.type == "Point") coords = [coords]; - coords.forEach(function (coordinates) { - o = { - "@lat": coordinates[1], - "@lon": coordinates[0], - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - if (coordinates[2] !== undefined) { - o.ele = coordinates[2]; - } - add_feature_link(o,f); - gpx.gpx.wpt.push(o); - }); - break; - // LineStrings - case "LineString": - case "MultiLineString": - var coords = f.geometry.coordinates; - var times = options.featureCoordTimes(f.properties); - if (f.geometry.type == "LineString") coords = [coords]; - o = { - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - add_feature_link(o,f); - o.trkseg = []; - coords.forEach(function(coordinates) { - var seg = {trkpt: []}; - coordinates.forEach(function(c, i) { - var o = { - "@lat": c[1], - "@lon":c[0] + if (options["gpx.wpt"]) { // include waypoints + var coords = f.geometry.coordinates; + if (f.geometry.type == "Point") coords = [coords]; + coords.forEach(function (coordinates) { + o = { + "@lat": coordinates[1], + "@lon": coordinates[0], + "name": options.featureTitle(f.properties), + "desc": options.featureDescription(f.properties) }; - if (c[2] !== undefined) { - o.ele = c[2]; - } - if (times && times[i]) { - o.time = times[i]; + if (coordinates[2] !== undefined) { + o.ele = coordinates[2]; } - seg.trkpt.push(o); + add_feature_link(o,f); + gpx.gpx.wpt.push(o); }); - o.trkseg.push(seg); - }); - gpx.gpx.trk.push(o); + } break; - // Polygons / Multipolygons + // LineStrings (tracks / routes) + case "LineString": + case "MultiLineString": case "Polygon": case "MultiPolygon": - o = { + var times = options.featureCoordTimes(f.properties); + var o = { "name": options.featureTitle(f.properties), "desc": options.featureDescription(f.properties) }; add_feature_link(o,f); - o.trkseg = []; - var coords = f.geometry.coordinates; - var times = options.featureCoordTimes(f.properties); - if (f.geometry.type == "Polygon") coords = [coords]; - coords.forEach(function(poly) { - poly.forEach(function(ring) { - var seg = {trkpt: []}; - var i = 0; - ring.forEach(function(c) { - var o = { - "@lat": c[1], - "@lon":c[0] + // Geometry represented uniformly as MultiLineString + var coordsLists = (function(geometry) { + var coords = geometry.coordinates; + switch (geometry.type) { + case "LineString": + return [coords]; + case "MultiPolygon": + return [].concat.apply([], coords); + default: + return coords + } + })(f.geometry); + // Point within a track or route + function point(c,i) { + var o = { + "@lat": c[1], + "@lon":c[0] + }; + if (c[2] !== undefined) { + o.ele = c[2]; + } + if (times && times[i] !== undefined) { + o.time = times[i]; + } + return o; + } + // Create gpx route + if (options["gpx.rte"]) { // include route + if (coordsLists.length === 1) { // single route + o.rtept = coordsLists[0].map(point); + gpx.gpx.rte.push(o); + } else { // multiple routes, handled as individual LineString features + coordsLists.forEach(function (coords) { + var pseudo_feature = { + "properties": f.properties, + "geometry": {type: "LineString", coordinates: coords} }; - if (c[2] !== undefined) { - o.ele = c[2]; - } - if (times && times[i]) { - o.time = times[i]; - } - i++; - seg.trkpt.push(o); + var recurse_options = defaults({"gpx.trk": false}, options); + mapFeature(pseudo_feature, recurse_options); }); - o.trkseg.push(seg); - }); - }); - gpx.gpx.trk.push(o); + } + } + // Create gpx track + if(options["gpx.trk"]) { // include track + o.trkseg = coordsLists.map(function(coords) { + return {"trkpt": coords.map(point)} + }) + gpx.gpx.trk.push(o); + } break; case "GeometryCollection": f.geometry.geometries.forEach(function (geometry) { @@ -172,13 +174,25 @@ function togpx( geojson, options ) { "properties": f.properties, "geometry": geometry }; - mapFeature(pseudo_feature); + mapFeature(pseudo_feature, options); }); break; default: console.log("warning: unsupported geometry type: "+f.geometry.type); } + }; + + var features; + if (geojson.type === "FeatureCollection") + features = geojson.features; + else if (geojson.type === "Feature") + features = [geojson]; + else + features = [{type:"Feature", properties: {}, geometry: geojson}]; + features.forEach(function(f) { + mapFeature(f, options); }); + gpx_str = JXON.stringify(gpx); return gpx_str; }; diff --git a/test/test.js b/test/test.js index cee1067..96973f3 100644 --- a/test/test.js +++ b/test/test.js @@ -340,6 +340,237 @@ describe("geometries", function () { }); }); +describe("routes", function () { + + + it('LineString (route)', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "LineString", + coordinates: [[1.0,2.0],[3.0,4.0]] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(1); + var rte = result.getElementsByTagName("rte")[0]; + var rtepts = rte.getElementsByTagName("rtept"); + expect(rtepts).to.have.length(2); + expect(rtepts[0].getAttribute("lat")).to.eql(2.0); + expect(rtepts[0].getAttribute("lon")).to.eql(1.0); + expect(rtepts[1].getAttribute("lat")).to.eql(4.0); + expect(rtepts[1].getAttribute("lon")).to.eql(3.0); + }); + + it('MultiLineString (route)', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "MultiLineString", + coordinates: [[[1.0,2.0],[3.0,4.0]],[[1.0,1.0],[2.0,2.0]]] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(2); + var rtes = result.getElementsByTagName("rte"); + var rtepts = rtes[0].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(2); + expect(rtepts[0].getAttribute("lat")).to.eql(2.0); + expect(rtepts[0].getAttribute("lon")).to.eql(1.0); + expect(rtepts[1].getAttribute("lat")).to.eql(4.0); + expect(rtepts[1].getAttribute("lon")).to.eql(3.0); + rtepts = rtes[1].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(2); + expect(rtepts[0].getAttribute("lat")).to.eql(1.0); + expect(rtepts[0].getAttribute("lon")).to.eql(1.0); + expect(rtepts[1].getAttribute("lat")).to.eql(2.0); + expect(rtepts[1].getAttribute("lon")).to.eql(2.0); + }); + + it('Polygon (no holes) (route)', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ] + ] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(1); + var rtes = result.getElementsByTagName("rte"); + expect(rtes).to.have.length(1); + var rtepts = rtes[0].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(0.0); + expect(rtepts[0].getAttribute("lon")).to.eql(100.0); + // skip remaining points, should be ok + }); + + it('Polygon (with hole)', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "Polygon", + coordinates: [ + [ [100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0] ], + [ [100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2] ] + ] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(2); + var rtes = result.getElementsByTagName("rte"); + expect(rtes).to.have.length(2); + var rtepts = rtes[0].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(0.0); + expect(rtepts[0].getAttribute("lon")).to.eql(100.0); + // skip remaining points, should be ok + rtepts = rtes[1].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(0.2); + expect(rtepts[0].getAttribute("lon")).to.eql(100.2); + // skip remaining points, should be ok + }); + + it('MultiPolygon', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "MultiPolygon", + coordinates: [ + [[[102.0, 2.0], [103.0, 2.0], [103.0, 3.0], [102.0, 3.0], [102.0, 2.0]]], + [[[100.0, 0.0], [101.0, 0.0], [101.0, 1.0], [100.0, 1.0], [100.0, 0.0]], + [[100.2, 0.2], [100.8, 0.2], [100.8, 0.8], [100.2, 0.8], [100.2, 0.2]]] + ] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(3); + var rtes = result.getElementsByTagName("rte"); + expect(rtes).to.have.length(3); + var rtepts = rtes[0].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(2.0); + expect(rtepts[0].getAttribute("lon")).to.eql(102.0); + // skip remaining points, should be ok + rtepts = rtes[1].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(0.0); + expect(rtepts[0].getAttribute("lon")).to.eql(100.0); + // skip remaining points, should be ok + rtepts = rtes[2].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(5); + expect(rtepts[0].getAttribute("lat")).to.eql(0.2); + expect(rtepts[0].getAttribute("lon")).to.eql(100.2); + // skip remaining points, should be ok + }); + + it('GeometryCollection', function() { + var geojson, options, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "GeometryCollection", + geometries: [ + { "type": "Point", + "coordinates": [100.0, 0.0] + }, + { "type": "LineString", + "coordinates": [ [101.0, 0.0], [102.0, 1.0] ] + } + ] + } + }] + }; + options = { + "gpx.rte": true, + "gpx.trk": false, + } + result = togpx(geojson, options); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(1); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(1); + var wpt = result.getElementsByTagName("wpt")[0]; + expect(wpt.getAttribute("lat")).to.eql(0.0); + expect(wpt.getAttribute("lon")).to.eql(100.0); + var rtes = result.getElementsByTagName("rte"); + expect(rtes).to.have.length(1); + var rtepts = rtes[0].getElementsByTagName("rtept"); + expect(rtepts).to.have.length(2); + expect(rtepts[0].getAttribute("lat")).to.eql(0.0); + expect(rtepts[0].getAttribute("lon")).to.eql(101.0); + expect(rtepts[1].getAttribute("lat")).to.eql(1.0); + expect(rtepts[1].getAttribute("lon")).to.eql(102.0); + }); +}) + describe("properties", function () { it('Name', function() { @@ -719,4 +950,39 @@ describe("options", function () { expect(wpt.getElementsByTagName("link")[0].getAttribute("href")).to.equal("http://example.com"); }); + it('gpx.{wpt|trk|rte}', function() { + var geojson, result; + geojson = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: {}, + geometry: { + type: "GeometryCollection", + geometries: [ + { "type": "MultiPoint", + "coordinates": [ [31.0, 0.0], [32.0, 0.0] ] + }, + { "type": "MultiLineString", + "coordinates": [ [[101.0, 0.0], [102.0, 1.0]], [[103.0, 0.0], [104.0, 1.0]] ] + } + ] + } + }] + }; + + result = togpx(geojson, { + "gpx.wpt": true, + "gpx.rte": true, + "gpx.trk": true, + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(2); + expect(result.getElementsByTagName("trk")).to.have.length(1); + expect(result.getElementsByTagName("rte")).to.have.length(2); + var trk = result.getElementsByTagName("trk")[0]; + var trksegs = trk.getElementsByTagName("trkseg"); + expect(trksegs).to.have.length(2); + }); + }); From 13c65a82e3fbe8694f189d34fb7a339fa07700ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jarno=20Le=20Conte=CC=81?= Date: Wed, 18 May 2016 23:50:33 +0200 Subject: [PATCH 2/5] Allow customisation by transform functions. --- index.js | 190 ++++++++++++++++++++++++++++++--------------------- test/test.js | 178 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 278 insertions(+), 90 deletions(-) diff --git a/index.js b/index.js index 35f4cd8..0d5204d 100644 --- a/index.js +++ b/index.js @@ -15,6 +15,7 @@ function togpx( geojson, options ) { featureDescription: get_feature_description, featureLink: undefined, featureCoordTimes: get_feature_coord_times, // provided function should return an array of UTC ISO 8601 timestamp strings + transform: {}, "gpx.wpt": true, // include waypoints in output "gpx.trk": true, // include tracks in output "gpx.rte": false, // include routes in output @@ -55,33 +56,93 @@ function togpx( geojson, options ) { function get_feature_coord_times(props) { return props.times || props.coordTimes || null; } - function add_feature_link(o, f) { + + // CREATE GPX DOCUMENT + + // general data shared between: , and + function mkEntity(feature) { + var entity = {}; + entity["name"] = options.featureTitle(feature.properties); + entity["desc"] = options.featureDescription(feature.properties); if (options.featureLink) - o.link = { "@href": options.featureLink(f.properties) } + entity["link"] = { "@href": options.featureLink(feature.properties) }; + return entity; } - function defaults(obj1, obj2) { - for (var attr in obj2) - if (!obj1.hasOwnProperty(attr)) - obj1[attr] = obj2[attr]; - return obj1; + // general data shared between all types of points: , , + function mkPoint(feature, coord, index) { + var pnt = {}; + pnt["@lat"] = coord[1]; + pnt["@lon"] = coord[0]; + if (coord[2] !== undefined) { + pnt["ele"] = coord[2]; + } + var times = options.featureCoordTimes(feature.properties); + if (times && times[index] !== undefined) { + pnt["time"] = times[index]; + } + return pnt; + } + // way point + function mkWpt(feature, coord, index) { + var wpt = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + return options.transform.wpt && options.transform.wpt(wpt, feature, coord, index) || wpt; + } + // route point + function mkRtept(feature, coord, index) { + var rtept = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + return options.transform.rtept && options.transform.rtept(rtept, feature, coord, index) || rtept; + } + // track point + function mkTrkpt(feature, coord, index) { + var trkpt = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + return options.transform.trkpt && options.transform.trkpt(trkpt, feature, coord, index) || trkpt; + } + // route + function mkRte(feature, coords) { + var rte = { + rtept: coords.map(function(coord, index) { + return mkRtept(feature, coord, index); + }) + }; + return options.transform.rte && options.transform.rte(rte, feature, coords) || rte; + } + // track + function mkTrk(feature, coordsList) { + var trk = { + "trkseg": coordsList.map(function(coords) { + return { + "trkpt": coords.map(function(coord, index) { + return mkTrkpt(feature, coord, index); + }) + }; + }) + }; + return options.transform.trk && options.transform.trk(trk, feature, coordsList) || trk; + } + // gpx root element + function mkGpx(features) { + var gpx = { + "@xmlns":"http://www.topografix.com/GPX/1/1", + "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", + "@xsi:schemaLocation":"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd", + "@version":"1.1", + "wpt": [], + "trk": [], + "rte": [] + } + if (options.creator) + gpx["@creator"] = options.creator; + if (options.metadata) + gpx["metadata"] = options.metadata; + features.forEach(function(f) { + mapFeature(f, options, gpx); + }); + return options.transform.gpx && options.transform.gpx(gpx, features) || gpx; } - // make gpx object - var gpx = {"gpx": { - "@xmlns":"http://www.topografix.com/GPX/1/1", - "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation":"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd", - "@version":"1.1", - "wpt": [], - "trk": [], - "rte": [], - }}; - if (options.creator) - gpx.gpx["@creator"] = options.creator; - if (options.metadata) - gpx.gpx["metadata"] = options.metadata; - - function mapFeature(f, options) { + // extract the entities , , and from a feature and + // push them to the gpx object (last argument) + function mapFeature(f, options, gpx) { if (!f.hasOwnProperty('properties')) { f.properties = {}; } @@ -92,18 +153,9 @@ function togpx( geojson, options ) { if (options["gpx.wpt"]) { // include waypoints var coords = f.geometry.coordinates; if (f.geometry.type == "Point") coords = [coords]; - coords.forEach(function (coordinates) { - o = { - "@lat": coordinates[1], - "@lon": coordinates[0], - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - if (coordinates[2] !== undefined) { - o.ele = coordinates[2]; - } - add_feature_link(o,f); - gpx.gpx.wpt.push(o); + coords.forEach(function(coord, index) { + var wpt = mkWpt(f, coord, index); + gpx.wpt.push(wpt); }); } break; @@ -112,60 +164,33 @@ function togpx( geojson, options ) { case "MultiLineString": case "Polygon": case "MultiPolygon": - var times = options.featureCoordTimes(f.properties); - var o = { - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - add_feature_link(o,f); // Geometry represented uniformly as MultiLineString - var coordsLists = (function(geometry) { - var coords = geometry.coordinates; - switch (geometry.type) { - case "LineString": - return [coords]; - case "MultiPolygon": - return [].concat.apply([], coords); - default: - return coords - } - })(f.geometry); - // Point within a track or route - function point(c,i) { - var o = { - "@lat": c[1], - "@lon":c[0] - }; - if (c[2] !== undefined) { - o.ele = c[2]; - } - if (times && times[i] !== undefined) { - o.time = times[i]; - } - return o; + var coordsLists; + switch (f.geometry.type) { + case "LineString": coordsLists = [f.geometry.coordinates]; break; + case "MultiPolygon": coordsLists = [].concat.apply([], f.geometry.coordinates); break; + default: coordsLists = f.geometry.coordinates; break; } // Create gpx route if (options["gpx.rte"]) { // include route if (coordsLists.length === 1) { // single route - o.rtept = coordsLists[0].map(point); - gpx.gpx.rte.push(o); - } else { // multiple routes, handled as individual LineString features + var rte = mkRte(f, coordsLists[0]); + gpx.rte.push(rte); + } else { // multiple routes are handled individually using recursive call coordsLists.forEach(function (coords) { var pseudo_feature = { "properties": f.properties, "geometry": {type: "LineString", coordinates: coords} }; var recurse_options = defaults({"gpx.trk": false}, options); - mapFeature(pseudo_feature, recurse_options); + mapFeature(pseudo_feature, recurse_options, gpx); }); } } // Create gpx track if(options["gpx.trk"]) { // include track - o.trkseg = coordsLists.map(function(coords) { - return {"trkpt": coords.map(point)} - }) - gpx.gpx.trk.push(o); + var trk = mkTrk(f, coordsLists); + gpx.trk.push(trk); } break; case "GeometryCollection": @@ -174,7 +199,7 @@ function togpx( geojson, options ) { "properties": f.properties, "geometry": geometry }; - mapFeature(pseudo_feature, options); + mapFeature(pseudo_feature, options, gpx); }); break; default: @@ -182,6 +207,7 @@ function togpx( geojson, options ) { } }; + // get features var features; if (geojson.type === "FeatureCollection") features = geojson.features; @@ -189,12 +215,18 @@ function togpx( geojson, options ) { features = [geojson]; else features = [{type:"Feature", properties: {}, geometry: geojson}]; - features.forEach(function(f) { - mapFeature(f, options); - }); - gpx_str = JXON.stringify(gpx); - return gpx_str; + // create gpx document + return JXON.stringify({ + gpx: mkGpx(features) + }); }; +function defaults(obj1, obj2) { + for (var attr in obj2) + if (!obj1.hasOwnProperty(attr)) + obj1[attr] = obj2[attr]; + return obj1; +} + module.exports = togpx; diff --git a/test/test.js b/test/test.js index 96973f3..9918d8c 100644 --- a/test/test.js +++ b/test/test.js @@ -343,7 +343,7 @@ describe("geometries", function () { describe("routes", function () { - it('LineString (route)', function() { + it('LineString', function() { var geojson, options, result; geojson = { type: "FeatureCollection", @@ -374,7 +374,7 @@ describe("routes", function () { expect(rtepts[1].getAttribute("lon")).to.eql(3.0); }); - it('MultiLineString (route)', function() { + it('MultiLineString', function() { var geojson, options, result; geojson = { type: "FeatureCollection", @@ -411,7 +411,7 @@ describe("routes", function () { expect(rtepts[1].getAttribute("lon")).to.eql(2.0); }); - it('Polygon (no holes) (route)', function() { + it('Polygon (no holes)', function() { var geojson, options, result; geojson = { type: "FeatureCollection", @@ -970,19 +970,175 @@ describe("options", function () { } }] }; - - result = togpx(geojson, { - "gpx.wpt": true, - "gpx.rte": true, - "gpx.trk": true, - }); + result = togpx(geojson, {"gpx.wpt": false, "gpx.rte": false, "gpx.trk": false}); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(0); + result = togpx(geojson, {"gpx.wpt": true, "gpx.rte": false, "gpx.trk": false}); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(2); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(0); + result = togpx(geojson, {"gpx.wpt": false, "gpx.rte": true, "gpx.trk": false}); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(2); + result = togpx(geojson, {"gpx.wpt": false, "gpx.rte": false, "gpx.trk": true}); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(1); + expect(result.getElementsByTagName("rte")).to.have.length(0); + result = togpx(geojson, {"gpx.wpt": true, "gpx.rte": true, "gpx.trk": true}); result = (new DOMParser()).parseFromString(result, 'text/xml'); expect(result.getElementsByTagName("wpt")).to.have.length(2); expect(result.getElementsByTagName("trk")).to.have.length(1); expect(result.getElementsByTagName("rte")).to.have.length(2); + }); + +}); + + +describe("tranformers", function () { + + function pointTransformer(pnt, feature, coord, index) { + pnt.name = feature.properties.names[index]; + pnt.extensions = [{ + lat: coord[1], + lon: coord[0] + }]; + } + + var geojsonPoint = { + type: "FeatureCollection", + features: [{ + type: "Feature", + properties: { + names: ["pnt1", "pnt2"] + }, + geometry: { + type: "GeometryCollection", + geometries: [ + { "type": "MultiPoint", + "coordinates": [ [31.0, 0.0], [32.0, 0.0] ] + }, + { "type": "MultiLineString", + "coordinates": [ [[101.0, 0.0], [102.0, 1.0]], [[103.0, 0.0], [104.0, 1.0]] ] + } + ] + } + }] + }; + + it('wpt', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + wpt: pointTransformer + } + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + var wpts = result.getElementsByTagName("wpt"); + var ext = wpts[0].getElementsByTagName("extensions")[0]; + expect(wpts[0].getElementsByTagName("name")[0].textContent).to.eql('pnt1'); + expect(wpts[1].getElementsByTagName("name")[0].textContent).to.eql('pnt2'); + expect(ext.getElementsByTagName("lat")[0].textContent).to.eql(0.0); + expect(ext.getElementsByTagName("lon")[0].textContent).to.eql(31.0); + }); + + it('rtept', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + rtept: pointTransformer + }, + "gpx.rte": true, + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + var rte = result.getElementsByTagName("rte")[0]; + var rtepts = rte.getElementsByTagName("rtept"); + var ext = rtepts[0].getElementsByTagName("extensions")[0]; + expect(rtepts[0].getElementsByTagName("name")[0].textContent).to.eql('pnt1'); + expect(rtepts[1].getElementsByTagName("name")[0].textContent).to.eql('pnt2'); + expect(ext.getElementsByTagName("lat")[0].textContent).to.eql(0.0); + expect(ext.getElementsByTagName("lon")[0].textContent).to.eql(101.0); + }); + + it('trkpt', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + trkpt: pointTransformer + } + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); var trk = result.getElementsByTagName("trk")[0]; - var trksegs = trk.getElementsByTagName("trkseg"); - expect(trksegs).to.have.length(2); + var trkpts = trk.getElementsByTagName("trkpt"); + var ext = trkpts[0].getElementsByTagName("extensions")[0]; + expect(trkpts[0].getElementsByTagName("name")[0].textContent).to.eql('pnt1'); + expect(trkpts[1].getElementsByTagName("name")[0].textContent).to.eql('pnt2'); + expect(ext.getElementsByTagName("lat")[0].textContent).to.eql(0.0); + expect(ext.getElementsByTagName("lon")[0].textContent).to.eql(101.0); + }); + + it('rte', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + rte: function (rte, feature, coords) { + rte.extensions = [{ + coordsNumber: coords.length + }]; + } + }, + "gpx.rte": true, + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + var rte = result.getElementsByTagName("rte")[0]; + var ext = rte.getElementsByTagName("extensions")[0]; + expect(ext.getElementsByTagName("coordsNumber")[0].textContent).to.eql(2); + }); + + it('trk', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + trk: function (trk, feature, coordsList) { + trk.extensions = [{ + segments: coordsList.length, + coordsNumber: coordsList.reduce(function(memo,coords){ return memo + coords.length; }, 0), + }]; + } + }, + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + var trk = result.getElementsByTagName("trk")[0]; + var ext = trk.getElementsByTagName("extensions")[0]; + expect(ext.getElementsByTagName("segments")[0].textContent).to.eql(2); + expect(ext.getElementsByTagName("coordsNumber")[0].textContent).to.eql(4); + }); + + it('gpx', function() { + var result; + result = togpx(geojsonPoint, { + transform: { + gpx: function(gpx, features) { + gpx.extensions = [{ + features: features.length + }]; + gpx.rte = []; // remove routes + gpx.trk = []; // remove tracks + gpx.wpt = []; // remove waypoints + } + } + }); + result = (new DOMParser()).parseFromString(result, 'text/xml'); + var ext = result.getElementsByTagName("extensions")[0]; + expect(ext.getElementsByTagName("features")[0].textContent).to.eql(1); + expect(result.getElementsByTagName("wpt")).to.have.length(0); + expect(result.getElementsByTagName("trk")).to.have.length(0); + expect(result.getElementsByTagName("rte")).to.have.length(0); }); }); From 95f4bf7472c4c7d417f29cbb0791916e1f48efcf Mon Sep 17 00:00:00 2001 From: Norbert Renner Date: Mon, 8 Mar 2021 17:05:37 +0100 Subject: [PATCH 3/5] Implement #11 review comments - Remove superfluous `;` (and another) - Reverse `default` logic to match Object.assign and options init - Rename `default` to `assign` - Replace default options overrride on init --- index.js | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/index.js b/index.js index 7290793..121b482 100644 --- a/index.js +++ b/index.js @@ -2,13 +2,7 @@ var JXON = require("jxon"); JXON.config({attrPrefix: '@'}); function togpx( geojson, options ) { - options = (function (defaults, options) { - for (var k in defaults) { - if (options.hasOwnProperty(k)) - defaults[k] = options[k]; - } - return defaults; - })({ + options = assign({ creator: "togpx", metadata: undefined, featureTitle: get_feature_title, @@ -95,17 +89,17 @@ function togpx( geojson, options ) { } // way point function mkWpt(feature, coord, index) { - var wpt = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + var wpt = assign( mkEntity(feature), mkPoint(feature, coord, index) ); return options.transform.wpt && options.transform.wpt(wpt, feature, coord, index) || wpt; } // route point function mkRtept(feature, coord, index) { - var rtept = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + var rtept = assign( mkEntity(feature), mkPoint(feature, coord, index) ); return options.transform.rtept && options.transform.rtept(rtept, feature, coord, index) || rtept; } // track point function mkTrkpt(feature, coord, index) { - var trkpt = defaults( mkEntity(feature), mkPoint(feature, coord, index) ); + var trkpt = assign( mkEntity(feature), mkPoint(feature, coord, index) ); return options.transform.trkpt && options.transform.trkpt(trkpt, feature, coord, index) || trkpt; } // route @@ -148,6 +142,7 @@ function togpx( geojson, options ) { gpx["metadata"] = options.metadata; else delete options.metadata; + features.forEach(function(f) { mapFeature(f, options, gpx); }); @@ -196,7 +191,8 @@ function togpx( geojson, options ) { "properties": f.properties, "geometry": {type: "LineString", coordinates: coords} }; - var recurse_options = defaults({"gpx.trk": false}, options); + var recurse_options = assign({}, options); + recurse_options = assign(recurse_options, {"gpx.trk": false}); mapFeature(pseudo_feature, recurse_options, gpx); }); } @@ -219,7 +215,7 @@ function togpx( geojson, options ) { default: console.log("warning: unsupported geometry type: "+f.geometry.type); } - }; + } // get features var features; @@ -234,11 +230,11 @@ function togpx( geojson, options ) { return JXON.stringify({ gpx: mkGpx(features) }); -}; +} -function defaults(obj1, obj2) { +function assign(obj1, obj2) { for (var attr in obj2) - if (!obj1.hasOwnProperty(attr)) + if (obj2.hasOwnProperty(attr)) obj1[attr] = obj2[attr]; return obj1; } From 46c5e8449e61cc23a6207b5686de7a52c2dbaab4 Mon Sep 17 00:00:00 2001 From: Norbert Renner Date: Mon, 8 Mar 2021 17:09:57 +0100 Subject: [PATCH 4/5] Don't set feature name/etc. on every trkpt/rtept --- index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index 121b482..9d1cea2 100644 --- a/index.js +++ b/index.js @@ -94,12 +94,12 @@ function togpx( geojson, options ) { } // route point function mkRtept(feature, coord, index) { - var rtept = assign( mkEntity(feature), mkPoint(feature, coord, index) ); + var rtept = mkPoint(feature, coord, index); return options.transform.rtept && options.transform.rtept(rtept, feature, coord, index) || rtept; } // track point function mkTrkpt(feature, coord, index) { - var trkpt = assign( mkEntity(feature), mkPoint(feature, coord, index) ); + var trkpt = mkPoint(feature, coord, index); return options.transform.trkpt && options.transform.trkpt(trkpt, feature, coord, index) || trkpt; } // route From 4199a901f1d68b8a31fb890408de97ddaac4f2d1 Mon Sep 17 00:00:00 2001 From: Norbert Renner Date: Mon, 8 Mar 2021 17:16:29 +0100 Subject: [PATCH 5/5] Order rte before trk, according to GPX schema see http://www.topografix.com/GPX/1/1/#element_gpx --- index.js | 4 +- togpx.js | 381 ++++++++++++++++++++++++++++++++----------------------- 2 files changed, 227 insertions(+), 158 deletions(-) diff --git a/index.js b/index.js index 9d1cea2..47076bf 100644 --- a/index.js +++ b/index.js @@ -133,8 +133,8 @@ function togpx( geojson, options ) { "@version":"1.1", "metadata": null, "wpt": [], - "trk": [], - "rte": [] + "rte": [], + "trk": [] } if (options.creator) gpx["@creator"] = options.creator; diff --git a/togpx.js b/togpx.js index c1e8bbd..8b39049 100644 --- a/togpx.js +++ b/togpx.js @@ -1,21 +1,19 @@ -(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.togpx = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o look for the specified property @@ -64,120 +62,147 @@ function togpx( geojson, options ) { if (!feature.properties) return null; return feature.properties.times || feature.properties.coordTimes || null; } - function add_feature_link(o, f) { + + // CREATE GPX DOCUMENT + + // general data shared between: , and + function mkEntity(feature) { + var entity = {}; + entity["name"] = options.featureTitle(feature.properties); + entity["desc"] = options.featureDescription(feature.properties); if (options.featureLink) - o.link = { "@href": options.featureLink(f.properties) } + entity["link"] = { "@href": options.featureLink(feature.properties) }; + return entity; + } + // general data shared between all types of points: , , + function mkPoint(feature, coord, index) { + var pnt = {}; + pnt["@lat"] = coord[1]; + pnt["@lon"] = coord[0]; + if (coord[2] !== undefined) { + pnt["ele"] = coord[2]; + } + var times = options.featureCoordTimes(feature); + if (times && times[index] !== undefined) { + pnt["time"] = times[index]; + } + return pnt; + } + // way point + function mkWpt(feature, coord, index) { + var wpt = assign( mkEntity(feature), mkPoint(feature, coord, index) ); + return options.transform.wpt && options.transform.wpt(wpt, feature, coord, index) || wpt; + } + // route point + function mkRtept(feature, coord, index) { + var rtept = mkPoint(feature, coord, index); + return options.transform.rtept && options.transform.rtept(rtept, feature, coord, index) || rtept; + } + // track point + function mkTrkpt(feature, coord, index) { + var trkpt = mkPoint(feature, coord, index); + return options.transform.trkpt && options.transform.trkpt(trkpt, feature, coord, index) || trkpt; + } + // route + function mkRte(feature, coords) { + var rte = { + rtept: coords.map(function(coord, index) { + return mkRtept(feature, coord, index); + }) + }; + return options.transform.rte && options.transform.rte(rte, feature, coords) || rte; + } + // track + function mkTrk(feature, coordsList) { + var trk = { + "trkseg": coordsList.map(function(coords) { + return { + "trkpt": coords.map(function(coord, index) { + return mkTrkpt(feature, coord, index); + }) + }; + }) + }; + return options.transform.trk && options.transform.trk(trk, feature, coordsList) || trk; + } + // gpx root element + function mkGpx(features) { + var gpx = { + "@xmlns":"http://www.topografix.com/GPX/1/1", + "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", + "@xsi:schemaLocation":"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd", + "@version":"1.1", + "metadata": null, + "wpt": [], + "rte": [], + "trk": [] + } + if (options.creator) + gpx["@creator"] = options.creator; + if (options.metadata) + gpx["metadata"] = options.metadata; + else + delete options.metadata; + + features.forEach(function(f) { + mapFeature(f, options, gpx); + }); + return options.transform.gpx && options.transform.gpx(gpx, features) || gpx; } - // make gpx object - var gpx = {"gpx": { - "@xmlns":"http://www.topografix.com/GPX/1/1", - "@xmlns:xsi":"http://www.w3.org/2001/XMLSchema-instance", - "@xsi:schemaLocation":"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd", - "@version":"1.1", - "metadata": null, - "wpt": [], - "trk": [], - }}; - if (options.creator) - gpx.gpx["@creator"] = options.creator; - if (options.metadata) - gpx.gpx["metadata"] = options.metadata; - else - delete options.metadata; - var features; - if (geojson.type === "FeatureCollection") - features = geojson.features; - else if (geojson.type === "Feature") - features = [geojson]; - else - features = [{type:"Feature", properties: {}, geometry: geojson}]; - features.forEach(function mapFeature(f) { + // extract the entities , , and from a feature and + // push them to the gpx object (last argument) + function mapFeature(f, options, gpx) { + if (!f.hasOwnProperty('properties')) { + f.properties = {}; + } switch (f.geometry.type) { // POIs case "Point": case "MultiPoint": - var coords = f.geometry.coordinates; - if (f.geometry.type == "Point") coords = [coords]; - coords.forEach(function (coordinates) { - o = { - "@lat": coordinates[1], - "@lon": coordinates[0], - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - if (coordinates[2] !== undefined) { - o.ele = coordinates[2]; - } - add_feature_link(o,f); - gpx.gpx.wpt.push(o); - }); + if (options["gpx.wpt"]) { // include waypoints + var coords = f.geometry.coordinates; + if (f.geometry.type == "Point") coords = [coords]; + coords.forEach(function(coord, index) { + var wpt = mkWpt(f, coord, index); + gpx.wpt.push(wpt); + }); + } break; - // LineStrings + // LineStrings (tracks / routes) case "LineString": case "MultiLineString": - var coords = f.geometry.coordinates; - var times = options.featureCoordTimes(f); - if (f.geometry.type == "LineString") coords = [coords]; - o = { - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - add_feature_link(o,f); - o.trkseg = []; - coords.forEach(function(coordinates) { - var seg = {trkpt: []}; - coordinates.forEach(function(c, i) { - var o = { - "@lat": c[1], - "@lon":c[0] - }; - if (c[2] !== undefined) { - o.ele = c[2]; - } - if (times && times[i]) { - o.time = times[i]; - } - seg.trkpt.push(o); - }); - o.trkseg.push(seg); - }); - gpx.gpx.trk.push(o); - break; - // Polygons / Multipolygons case "Polygon": case "MultiPolygon": - o = { - "name": options.featureTitle(f.properties), - "desc": options.featureDescription(f.properties) - }; - add_feature_link(o,f); - o.trkseg = []; - var coords = f.geometry.coordinates; - var times = options.featureCoordTimes(f); - if (f.geometry.type == "Polygon") coords = [coords]; - coords.forEach(function(poly) { - poly.forEach(function(ring) { - var seg = {trkpt: []}; - var i = 0; - ring.forEach(function(c) { - var o = { - "@lat": c[1], - "@lon":c[0] + // Geometry represented uniformly as MultiLineString + var coordsLists; + switch (f.geometry.type) { + case "LineString": coordsLists = [f.geometry.coordinates]; break; + case "MultiPolygon": coordsLists = [].concat.apply([], f.geometry.coordinates); break; + default: coordsLists = f.geometry.coordinates; break; + } + // Create gpx route + if (options["gpx.rte"]) { // include route + if (coordsLists.length === 1) { // single route + var rte = mkRte(f, coordsLists[0]); + gpx.rte.push(rte); + } else { // multiple routes are handled individually using recursive call + coordsLists.forEach(function (coords) { + var pseudo_feature = { + "properties": f.properties, + "geometry": {type: "LineString", coordinates: coords} }; - if (c[2] !== undefined) { - o.ele = c[2]; - } - if (times && times[i]) { - o.time = times[i]; - } - i++; - seg.trkpt.push(o); + var recurse_options = assign({}, options); + recurse_options = assign(recurse_options, {"gpx.trk": false}); + mapFeature(pseudo_feature, recurse_options, gpx); }); - o.trkseg.push(seg); - }); - }); - gpx.gpx.trk.push(o); + } + } + // Create gpx track + if(options["gpx.trk"]) { // include track + var trk = mkTrk(f, coordsLists); + gpx.trk.push(trk); + } break; case "GeometryCollection": f.geometry.geometries.forEach(function (geometry) { @@ -185,16 +210,35 @@ function togpx( geojson, options ) { "properties": f.properties, "geometry": geometry }; - mapFeature(pseudo_feature); + mapFeature(pseudo_feature, options, gpx); }); break; default: console.log("warning: unsupported geometry type: "+f.geometry.type); } + } + + // get features + var features; + if (geojson.type === "FeatureCollection") + features = geojson.features; + else if (geojson.type === "Feature") + features = [geojson]; + else + features = [{type:"Feature", properties: {}, geometry: geojson}]; + + // create gpx document + return JXON.stringify({ + gpx: mkGpx(features) }); - gpx_str = JXON.stringify(gpx); - return gpx_str; -}; +} + +function assign(obj1, obj2) { + for (var attr in obj2) + if (obj2.hasOwnProperty(attr)) + obj1[attr] = obj2[attr]; + return obj1; +} module.exports = togpx; @@ -352,27 +396,6 @@ module.exports = togpx; vResult = nVerb === 0 ? objectify(vBuiltVal) : {}; } - for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) { - - sProp = aCache[nElId].nodeName; - if (opts.lowerCaseTags) { - sProp = sProp.toLowerCase(); - } - - vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr); - if (vResult.hasOwnProperty(sProp)) { - if (vResult[sProp].constructor !== Array) { - vResult[sProp] = [vResult[sProp]]; - } - - vResult[sProp].push(vContent); - } else { - vResult[sProp] = vContent; - - nLength++; - } - } - if (bAttributes) { var nAttrLen = oParentNode.attributes.length, sAPrefix = bNesteAttr ? '' : opts.attrPrefix, @@ -402,6 +425,27 @@ module.exports = togpx; } + for (var nElId = nLevelStart; nElId < nLevelEnd; nElId++) { + + sProp = aCache[nElId].nodeName; + if (opts.lowerCaseTags) { + sProp = sProp.toLowerCase(); + } + + vContent = createObjTree(aCache[nElId], nVerb, bFreeze, bNesteAttr); + if (vResult.hasOwnProperty(sProp)) { + if (vResult[sProp].constructor !== Array) { + vResult[sProp] = [vResult[sProp]]; + } + + vResult[sProp].push(vContent); + } else { + vResult[sProp] = vContent; + + nLength++; + } + } + if (nVerb === 3 || (nVerb === 2 || nVerb === 1 && nLength > 0) && sCollectedTxt) { vResult[opts.valueKey] = vBuiltVal; } else if (!bHighVerb && nLength === 0 && sCollectedTxt) { @@ -415,10 +459,47 @@ module.exports = togpx; return vResult; } + + function getElementNS(sName, vValue, oParentEl) { + var xmlns = opts.attrPrefix + 'xmlns', + isObject = vValue && vValue instanceof Object, + elementNS, + prefix; + + if (sName.indexOf(':') !== -1) { + prefix = sName.split(':')[0]; + + if (isObject) { + elementNS = vValue[xmlns + ':' + prefix]; + if (elementNS) return elementNS; + } + + elementNS = oParentEl.lookupNamespaceURI(prefix); + if (elementNS) return elementNS; + } + if (isObject) { + elementNS = vValue[xmlns]; + } + + return elementNS || oParentEl.lookupNamespaceURI(null); + } + + function createElement(sName, vValue, oParentEl, oXMLDoc) { + var elementNS = getElementNS(sName, vValue, oParentEl), + element; + + if (elementNS) { + element = oXMLDoc.createElementNS(elementNS, sName); + } else { + element = oXMLDoc.createElement(sName); + } + + return element; + } + function loadObjTree(oXMLDoc, oParentEl, oParentObj) { var vValue, - oChild, - elementNS; + oChild; if (oParentObj.constructor === String || oParentObj.constructor === Number || oParentObj.constructor === Boolean) { oParentEl.appendChild(oXMLDoc.createTextNode(oParentObj.toString())); /* verbosity level is 0 or 1 */ @@ -453,39 +534,27 @@ module.exports = togpx; for (var sAttrib in vValue) { oParentEl.setAttribute(sAttrib, vValue[sAttrib]); } - } else if (sName === opts.attrPrefix + 'xmlns') { - if (isNodeJs) { - oParentEl.setAttribute(sName.slice(1), vValue); - } - // do nothing: special handling of xml namespaces is done via createElementNS() + } else if (sName.indexOf(opts.attrPrefix + 'xmlns') === 0) { + // explicitly set xmlns and xmlns:* attributes, so they can be set anywhere in the tag hierarchy + oParentEl.setAttributeNS('http://www.w3.org/2000/xmlns/', sName.slice(1), vValue); } else if (sName.charAt(0) === opts.attrPrefix) { oParentEl.setAttribute(sName.slice(1), vValue); } else if (vValue.constructor === Array) { for (var nItem in vValue) { if (!vValue.hasOwnProperty(nItem)) continue; - elementNS = (vValue[nItem] && vValue[nItem][opts.attrPrefix + 'xmlns']) || oParentEl.namespaceURI; - if (elementNS) { - oChild = oXMLDoc.createElementNS(elementNS, sName); - } else { - oChild = oXMLDoc.createElement(sName); - } + oChild = createElement(sName, vValue[nItem], oParentEl, oXMLDoc); + oParentEl.appendChild(oChild); loadObjTree(oXMLDoc, oChild, vValue[nItem] || {}); - oParentEl.appendChild(oChild); } } else { - elementNS = (vValue || {})[opts.attrPrefix + 'xmlns'] || oParentEl.namespaceURI; - if (elementNS) { - oChild = oXMLDoc.createElementNS(elementNS, sName); - } else { - oChild = oXMLDoc.createElement(sName); - } + oChild = createElement(sName, vValue, oParentEl, oXMLDoc); + oParentEl.appendChild(oChild); if (vValue instanceof Object) { loadObjTree(oXMLDoc, oChild, vValue); } else if (vValue !== null && (vValue !== true || !opts.trueIsEmpty)) { oChild.appendChild(oXMLDoc.createTextNode(vValue.toString())); } - oParentEl.appendChild(oChild); } } } @@ -544,4 +613,4 @@ module.exports = togpx; },{"xmldom":3}],3:[function(require,module,exports){ },{}]},{},[1])(1) -}); \ No newline at end of file +});