diff --git a/src/ol/format/gpxformat.js b/src/ol/format/gpxformat.js index ded218bcaa..6e67111182 100644 --- a/src/ol/format/gpxformat.js +++ b/src/ol/format/gpxformat.js @@ -1,4 +1,5 @@ goog.provide('ol.format.GPX'); +goog.provide('ol.format.GPX.V1_1'); goog.require('goog.array'); goog.require('goog.asserts'); @@ -420,3 +421,363 @@ ol.format.GPX.prototype.readProjectionFromDocument = function(doc) { ol.format.GPX.prototype.readProjectionFromNode = function(node) { return ol.proj.get('EPSG:4326'); }; + + +/** + * @param {Node} node Node. + * @param {string} value Value for the link's `href` attribute. + * @param {Array.<*>} objectStack Node stack. + * @private + */ +ol.format.GPX.writeLink_ = function(node, value, objectStack) { + node.setAttribute('href', value); + var context = objectStack[objectStack.length - 1]; + goog.asserts.assert(goog.isObject(context)); + var properties = goog.object.get(context, 'properties'); + var link = [ + goog.object.get(properties, 'linkText'), + goog.object.get(properties, 'linkType') + ]; + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ ({node: node}), + ol.format.GPX.LINK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY, + link, objectStack, ol.format.GPX.LINK_SEQUENCE_); +}; + + +/** + * @param {Node} node Node. + * @param {ol.Coordinate} coordinate Coordinate. + * @param {Array.<*>} objectStack Object stack. + * @private + */ +ol.format.GPX.writeWptType_ = function(node, coordinate, objectStack) { + var context = objectStack[objectStack.length - 1]; + goog.asserts.assert(goog.isObject(context)); + var parentNode = context.node; + goog.asserts.assert(ol.xml.isNode(parentNode)); + var namespaceURI = parentNode.namespaceURI; + var properties = goog.object.get(context, 'properties'); + //FIXME Projection handling + ol.xml.setAttributeNS(node, null, 'lat', coordinate[1]); + ol.xml.setAttributeNS(node, null, 'lon', coordinate[0]); + var geometryLayout = goog.object.get(context, 'geometryLayout'); + switch (geometryLayout) { + case ol.geom.GeometryLayout.XYZM: + if (coordinate[3] !== 0) { + goog.object.set(properties, 'time', coordinate[3]); + } + case ol.geom.GeometryLayout.XYZ: + if (coordinate[2] !== 0) { + goog.object.set(properties, 'ele', coordinate[2]); + } + break; + case ol.geom.GeometryLayout.XYM: + if (coordinate[2] !== 0) { + goog.object.set(properties, 'time', coordinate[2]); + } + } + var orderedKeys = ol.format.GPX.WPT_TYPE_SEQUENCE_[namespaceURI]; + var values = ol.xml.makeSequence(properties, orderedKeys); + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ + ({node: node, 'properties': properties}), + ol.format.GPX.WPT_TYPE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY, + values, objectStack, orderedKeys); +}; + + +/** + * @param {Node} node Node. + * @param {ol.Feature} feature Feature. + * @param {Array.<*>} objectStack Object stack. + * @private + */ +ol.format.GPX.writeRte_ = function(node, feature, objectStack) { + var properties = feature.getProperties(); + var context = {node: node, 'properties': properties}; + var geometry = feature.getGeometry(); + if (goog.isDef(geometry)) { + goog.asserts.assertInstanceof(geometry, ol.geom.LineString); + goog.object.set(context, 'geometryLayout', geometry.getLayout()); + goog.object.set(properties, 'rtept', geometry.getCoordinates()); + } + var parentNode = objectStack[objectStack.length - 1].node; + var orderedKeys = ol.format.GPX.RTE_SEQUENCE_[parentNode.namespaceURI]; + var values = ol.xml.makeSequence(properties, orderedKeys); + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context), + ol.format.GPX.RTE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY, + values, objectStack, orderedKeys); +}; + + +/** + * @param {Node} node Node. + * @param {ol.Feature} feature Feature. + * @param {Array.<*>} objectStack Object stack. + * @private + */ +ol.format.GPX.writeTrk_ = function(node, feature, objectStack) { + var properties = feature.getProperties(); + var context = {node: node, 'properties': properties}; + var geometry = feature.getGeometry(); + if (goog.isDef(geometry)) { + goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString); + goog.object.set(properties, 'trkseg', geometry.getLineStrings()); + } + var parentNode = objectStack[objectStack.length - 1].node; + var orderedKeys = ol.format.GPX.TRK_SEQUENCE_[parentNode.namespaceURI]; + var values = ol.xml.makeSequence(properties, orderedKeys); + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context), + ol.format.GPX.TRK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY, + values, objectStack, orderedKeys); +}; + + +/** + * @param {Node} node Node. + * @param {ol.geom.LineString} lineString LineString. + * @param {Array.<*>} objectStack Object stack. + * @private + */ +ol.format.GPX.writeTrkSeg_ = function(node, lineString, objectStack) { + var context = {node: node, 'geometryLayout': lineString.getLayout(), + 'properties': {}}; + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context), + ol.format.GPX.TRKSEG_SERIALIZERS_, ol.format.GPX.TRKSEG_NODE_FACTORY_, + lineString.getCoordinates(), objectStack); +}; + + +/** + * @param {Node} node Node. + * @param {ol.Feature} feature Feature. + * @param {Array.<*>} objectStack Object stack. + * @private + */ +ol.format.GPX.writeWpt_ = function(node, feature, objectStack) { + var context = objectStack[objectStack.length - 1]; + goog.asserts.assert(goog.isObject(context)); + goog.object.set(context, 'properties', feature.getProperties()); + var geometry = feature.getGeometry(); + if (goog.isDef(geometry)) { + goog.asserts.assertInstanceof(geometry, ol.geom.Point); + goog.object.set(context, 'geometryLayout', geometry.getLayout()); + ol.format.GPX.writeWptType_(node, geometry.getCoordinates(), objectStack); + } +}; + + +/** + * @const + * @type {Array.} + * @private + */ +ol.format.GPX.LINK_SEQUENCE_ = ['text', 'type']; + + +/** + * @type {Object.>} + * @private + */ +ol.format.GPX.LINK_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'text': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode) + }); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.RTE_SEQUENCE_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, [ + 'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'rtept' + ]); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.RTE_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_), + 'number': ol.xml.makeChildAppender( + ol.format.XSD.writeNonNegativeIntegerTextNode), + 'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'rtept': ol.xml.makeArraySerializer(ol.xml.makeChildAppender( + ol.format.GPX.writeWptType_)) + }); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.TRK_SEQUENCE_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, [ + 'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'trkseg' + ]); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.TRK_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_), + 'number': ol.xml.makeChildAppender( + ol.format.XSD.writeNonNegativeIntegerTextNode), + 'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'trkseg': ol.xml.makeArraySerializer(ol.xml.makeChildAppender( + ol.format.GPX.writeTrkSeg_)) + }); + + +/** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ +ol.format.GPX.TRKSEG_NODE_FACTORY_ = ol.xml.makeSimpleNodeFactory('trkpt'); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.TRKSEG_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'trkpt': ol.xml.makeChildAppender(ol.format.GPX.writeWptType_) + }); + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.WPT_TYPE_SEQUENCE_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, [ + 'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src', + 'link', 'sym', 'type', 'fix', 'sat', 'hdop', 'vdop', 'pdop', + 'ageofdgpsdata', 'dgpsid' + ]); + + +/** + * @type {Object.>} + * @private + */ +ol.format.GPX.WPT_TYPE_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'ele': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode), + 'time': ol.xml.makeChildAppender(ol.format.XSD.writeDateTimeTextNode), + 'magvar': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode), + 'geoidheight': ol.xml.makeChildAppender( + ol.format.XSD.writeDecimalTextNode), + 'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_), + 'sym': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'fix': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode), + 'sat': ol.xml.makeChildAppender( + ol.format.XSD.writeNonNegativeIntegerTextNode), + 'hdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode), + 'vdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode), + 'pdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode), + 'ageofdgpsdata': ol.xml.makeChildAppender( + ol.format.XSD.writeDecimalTextNode), + 'dgpsid': ol.xml.makeChildAppender( + ol.format.XSD.writeNonNegativeIntegerTextNode) + }); + + +/** + * @const + * @type {Object.} + * @private + */ +ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_ = { + 'Point': 'wpt', + 'LineString': 'rte', + 'MultiLineString': 'trk' +}; + + +/** + * @const + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node|undefined} Node. + * @private + */ +ol.format.GPX.GPX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) { + goog.asserts.assertInstanceof(value, ol.Feature); + var geometry = value.getGeometry(); + if (goog.isDef(geometry)) { + var parentNode = objectStack[objectStack.length - 1].node; + goog.asserts.assert(ol.xml.isNode(parentNode)); + return ol.xml.createElementNS(parentNode.namespaceURI, + ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_[geometry.getType()]); + } +}; + + +/** + * @const + * @type {Object.>} + * @private + */ +ol.format.GPX.GPX_SERIALIZERS_ = ol.xml.makeStructureNS( + ol.format.GPX.NAMESPACE_URIS_, { + 'rte': ol.xml.makeChildAppender(ol.format.GPX.writeRte_), + 'trk': ol.xml.makeChildAppender(ol.format.GPX.writeTrk_), + 'wpt': ol.xml.makeChildAppender(ol.format.GPX.writeWpt_) + }); + + + +/** + * @constructor + * @extends {ol.format.GPX} + * @todo stability experimental + */ +ol.format.GPX.V1_1 = function() { + goog.base(this); +}; +goog.inherits(ol.format.GPX.V1_1, ol.format.GPX); + + +/** + * @inheritDoc + */ +ol.format.GPX.V1_1.prototype.writeFeaturesNode = function(features) { + //FIXME Serialize metadata + var gpx = ol.xml.createElementNS('http://www.topografix.com/GPX/1/1', 'gpx'); + ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ + ({node: gpx}), ol.format.GPX.GPX_SERIALIZERS_, + ol.format.GPX.GPX_NODE_FACTORY_, features, []); + return gpx; +}; diff --git a/src/ol/format/xsdformat.js b/src/ol/format/xsdformat.js index 058cedd1f9..f60fa4cc4f 100644 --- a/src/ol/format/xsdformat.js +++ b/src/ol/format/xsdformat.js @@ -1,5 +1,6 @@ goog.provide('ol.format.XSD'); +goog.require('goog.asserts'); goog.require('goog.string'); goog.require('ol.xml'); @@ -123,3 +124,51 @@ ol.format.XSD.readString = function(node) { var s = ol.xml.getAllTextContent(node, false); return goog.string.trim(s); }; + + +/** + * @param {Node} node Node to append a TextNode with the dateTime to. + * @param {number} dateTime DateTime in seconds. + */ +ol.format.XSD.writeDateTimeTextNode = function(node, dateTime) { + var date = new Date(dateTime * 1000); + var string = date.getUTCFullYear() + '-' + + goog.string.padNumber(date.getUTCMonth() + 1, 2) + '-' + + goog.string.padNumber(date.getUTCDate(), 2) + 'T' + + goog.string.padNumber(date.getUTCHours(), 2) + ':' + + goog.string.padNumber(date.getUTCMinutes(), 2) + ':' + + goog.string.padNumber(date.getUTCSeconds(), 2) + 'Z'; + node.appendChild(ol.xml.DOCUMENT.createTextNode(string)); +}; + + +/** + * @param {Node} node Node to append a TextNode with the decimal to. + * @param {number} decimal Decimal. + */ +ol.format.XSD.writeDecimalTextNode = function(node, decimal) { + var string = decimal.toPrecision(); + node.appendChild(ol.xml.DOCUMENT.createTextNode(string)); +}; + + +/** + * @param {Node} node Node to append a TextNode with the decimal to. + * @param {number} nonNegativeInteger Non negative integer. + */ +ol.format.XSD.writeNonNegativeIntegerTextNode = + function(node, nonNegativeInteger) { + goog.asserts.assert(nonNegativeInteger >= 0); + goog.asserts.assert(nonNegativeInteger == (nonNegativeInteger | 0)); + var string = nonNegativeInteger.toString(); + node.appendChild(ol.xml.DOCUMENT.createTextNode(string)); +}; + + +/** + * @param {Node} node Node to append a TextNode with the string to. + * @param {string} string String. + */ +ol.format.XSD.writeStringTextNode = function(node, string) { + node.appendChild(ol.xml.DOCUMENT.createTextNode(string)); +}; diff --git a/src/ol/xml.js b/src/ol/xml.js index c21e526ea6..a0d9dde9c5 100644 --- a/src/ol/xml.js +++ b/src/ol/xml.js @@ -1,18 +1,81 @@ +// FIXME Remove ol.xml.makeParsersNS, and use ol.xml.makeStructureNS instead. + goog.provide('ol.xml'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom.NodeType'); +goog.require('goog.dom.xml'); goog.require('goog.object'); goog.require('goog.userAgent'); +/** + * When using {@link ol.xml.makeChildAppender} or + * {@link ol.xml.makeSimpleNodeFactory}, the top `objectStack` item needs to + * have this structure. + * @typedef {{node:Node}} + */ +ol.xml.NodeStackItem; + + /** * @typedef {function(Node, Array.<*>)} */ ol.xml.Parser; +/** + * @typedef {function(Node, *, Array.<*>)} + */ +ol.xml.Serializer; + + +/** + * This document should be used when creating nodes for XML serializations. This + * document is also used by {@link ol.xml.createElementNS} and + * {@link ol.xml.setAttributeNS} + * @const + * @type {Document} + */ +ol.xml.DOCUMENT = goog.dom.xml.createDocument(); + + +/** + * @param {string} namespaceURI Namespace URI. + * @param {string} qualifiedName Qualified name. + * @return {Node} Node. + * @private + */ +ol.xml.createElementNS_ = function(namespaceURI, qualifiedName) { + return ol.xml.DOCUMENT.createElementNS(namespaceURI, qualifiedName); +}; + + +/** + * @param {string} namespaceURI Namespace URI. + * @param {string} qualifiedName Qualified name. + * @return {Node} Node. + * @private + */ +ol.xml.createElementNSActiveX_ = function(namespaceURI, qualifiedName) { + if (goog.isNull(namespaceURI)) { + namespaceURI = ''; + } + return ol.xml.DOCUMENT.createNode(1, qualifiedName, namespaceURI); +}; + + +/** + * @param {string} namespaceURI Namespace URI. + * @param {string} qualifiedName Qualified name. + * @return {Node} Node. + */ +ol.xml.createElementNS = + (document.implementation && document.implementation.createDocument) ? + ol.xml.createElementNS_ : ol.xml.createElementNSActiveX_; + + /** * @param {Node} node Node. * @param {boolean} normalizeWhitespace Normalize whitespace. @@ -138,6 +201,47 @@ ol.xml.isNodeIE_ = function(value) { ol.xml.isNode = goog.userAgent.IE ? ol.xml.isNodeIE_ : ol.xml.isNode_; +/** + * @param {Node} node Node. + * @param {?string} namespaceURI Namespace URI. + * @param {string} name Attribute name. + * @param {string|number} value Value. + * @private + */ +ol.xml.setAttributeNS_ = function(node, namespaceURI, name, value) { + node.setAttributeNS(namespaceURI, name, value); +}; + + +/** + * @param {Node} node Node. + * @param {?string} namespaceURI Namespace URI. + * @param {string} name Attribute name. + * @param {string|number} value Value. + * @private + */ +ol.xml.setAttributeNSActiveX_ = function(node, namespaceURI, name, value) { + if (!goog.isNull(namespaceURI)) { + var attribute = node.ownerDocument.createNode(2, name, namespaceURI); + attribute.nodeValue = value; + node.setAttributeNode(attribute); + } else { + node.setAttribute(name, value); + } +}; + + +/** + * @param {Node} node Node. + * @param {?string} namespaceURI Namespace URI. + * @param {string} name Attribute name. + * @param {string|number} value Value. + */ +ol.xml.setAttributeNS = + (document.implementation && document.implementation.createDocument) ? + ol.xml.setAttributeNS_ : ol.xml.setAttributeNSActiveX_; + + /** * @param {string} xml XML. * @return {Document} Document. @@ -254,13 +358,155 @@ ol.xml.makeObjectPropertySetter = * @return {Object.>} Parsers NS. */ ol.xml.makeParsersNS = function(namespaceURIs, parsers, opt_parsersNS) { - /** @type {Object.>} */ - var parsersNS = goog.isDef(opt_parsersNS) ? opt_parsersNS : {}; + return /** @type {Object.>} */ ( + ol.xml.makeStructureNS(namespaceURIs, parsers, opt_parsersNS)); +}; + + +/** + * Creates a serializer that appends nodes written by its `nodeWriter` to its + * designated parent. The parent is the `node` of the + * {@link ol.xml.NodeStackItem} at the top of the `objectStack`. + * @param {function(this: T, Node, V, Array.<*>)} + * nodeWriter Node writer. + * @param {T=} opt_this The object to use as `this` in `nodeWriter`. + * @return {ol.xml.Serializer} Serializer. + * @template T, V + */ +ol.xml.makeChildAppender = function(nodeWriter, opt_this) { + return function(node, value, objectStack) { + nodeWriter.call(opt_this, node, value, objectStack); + var parent = objectStack[objectStack.length - 1]; + goog.asserts.assert(goog.isObject(parent)); + var parentNode = parent.node; + goog.asserts.assert(ol.xml.isNode(parentNode) || + ol.xml.isDocument(parentNode)); + parentNode.appendChild(node); + }; +}; + + +/** + * Creates a serializer that calls the provided `nodeWriter` from + * {@link ol.xml.serialize}. This can be used by the parent writer to have the + * 'nodeWriter' called with an array of values when the `nodeWriter` was + * designed to serialize a single item. An example would be a LineString + * geometry writer, which could be reused for writing MultiLineString + * geometries. + * @param {function(this: T, Node, V, Array.<*>)} + * nodeWriter Node writer. + * @param {T=} opt_this The object to use as `this` in `nodeWriter`. + * @return {ol.xml.Serializer} Serializer. + * @template T, V + */ +ol.xml.makeArraySerializer = function(nodeWriter, opt_this) { + var serializersNS, nodeFactory; + return function(node, value, objectStack) { + if (!goog.isDef(serializersNS)) { + serializersNS = {}; + var serializers = {}; + goog.object.set(serializers, node.localName, nodeWriter); + goog.object.set(serializersNS, node.namespaceURI, serializers); + nodeFactory = ol.xml.makeSimpleNodeFactory(node.localName); + } + ol.xml.serialize(serializersNS, nodeFactory, value, objectStack); + }; +}; + + +/** + * Creates a node factory which can use the `opt_keys` passed to + * {@link ol.xml.serialize} or {@link ol.xml.pushSerializeAndPop} as node names, + * or a fixed node name. The namespace of the created nodes can either be fixed, + * or the parent namespace will be used. + * @param {string=} opt_nodeName Fixed node name which will be used for all + * created nodes. If not provided, the 3rd argument to the resulting node + * factory needs to be provided and will be the nodeName. + * @param {string=} opt_namespaceURI Fixed namespace URI which will be used for + * all created nodes. If not provided, the namespace of the parent node will + * be used. + * @return {function(*, Array.<*>, string=): (Node|undefined)} Node factory. + */ +ol.xml.makeSimpleNodeFactory = function(opt_nodeName, opt_namespaceURI) { + var fixedNodeName = opt_nodeName; + return ( + /** + * @param {*} value Value. + * @param {Array.<*>} objectStack Object stack. + * @param {string=} opt_nodeName Node name. + * @return {Node} Node. + */ + function(value, objectStack, opt_nodeName) { + var context = objectStack[objectStack.length - 1]; + var node = context.node; + goog.asserts.assert(ol.xml.isNode(node) || ol.xml.isDocument(node)); + var nodeName = fixedNodeName; + if (!goog.isDef(nodeName)) { + nodeName = opt_nodeName; + } + var namespaceURI = opt_namespaceURI; + if (!goog.isDef(opt_namespaceURI)) { + namespaceURI = node.namespaceURI; + } + goog.asserts.assert(goog.isDef(nodeName)); + return ol.xml.createElementNS(namespaceURI, nodeName); + } + ); +}; + + +/** + * A node factory that creates a node using the parent's `namespaceURI` and the + * `nodeName` passed by {@link ol.xml.serialize} or + * {@link ol.xml.pushSerializeAndPop} to the node factory. + * @const + * @type {function(*, Array.<*>, string=): (Node|undefined)} + */ +ol.xml.OBJECT_PROPERTY_NODE_FACTORY = ol.xml.makeSimpleNodeFactory(); + + +/** + * Creates an array of `values` to be used with {@link ol.xml.serialize} or + * {@link ol.xml.pushSerializeAndPop}, where `orderedKeys` has to be provided as + * `opt_key` argument. + * @param {Object.} object Key-value pairs for the sequence. Keys can + * be a subset of the `orderedKeys`. + * @param {Array.} orderedKeys Keys in the order of the sequence. + * @return {Array.} Values in the order of the sequence. The resulting array + * has the same length as the `orderedKeys` array. Values that are not + * present in `object` will be `undefined` in the resulting array. + * @template V + */ +ol.xml.makeSequence = function(object, orderedKeys) { + var length = orderedKeys.length; + var sequence = new Array(length); + for (var i = 0; i < length; ++i) { + sequence[i] = object[orderedKeys[i]]; + } + return sequence; +}; + + +/** + * Creates a namespaced structure, using the same values for each namespace. + * This can be used as a starting point for versioned parsers, when only a few + * values are version specific. + * @param {Array.} namespaceURIs Namespace URIs. + * @param {T} structure Structure. + * @param {Object.=} opt_structureNS Namespaced structure to add to. + * @return {Object.} Namespaced structure. + * @template T + */ +ol.xml.makeStructureNS = function(namespaceURIs, structure, opt_structureNS) { + /** + * @type {Object.} + */ + var structureNS = goog.isDef(opt_structureNS) ? opt_structureNS : {}; var i, ii; for (i = 0, ii = namespaceURIs.length; i < ii; ++i) { - parsersNS[namespaceURIs[i]] = parsers; + structureNS[namespaceURIs[i]] = structure; } - return parsersNS; + return structureNS; }; @@ -301,3 +547,75 @@ ol.xml.pushParseAndPop = function( ol.xml.parse(parsersNS, node, objectStack, opt_this); return objectStack.pop(); }; + + +/** + * Walks through an array of `values` and calls a serializer for each value. + * @param {Object.>} serializersNS + * Namespaced serializers. + * @param {function(this: T, *, Array.<*>, (string|undefined)): (Node|undefined)} nodeFactory + * Node factory. The `nodeFactory` creates the node whose namespace and name + * will be used to choose a node writer from `serializersNS`. This + * separation allows us to decide what kind of node to create, depending on + * the value we want to serialize. An example for this would be different + * geometry writers based on the geometry type. + * @param {Array.<*>} values Values to serialize. An example would be an array + * of {@link ol.Feature} instances. + * @param {Array.<*>} objectStack Node stack. + * @param {Array.=} opt_keys Keys of the `values`. Will be passed to the + * `nodeFactory`. This is used for serializing object literals where the + * node name relates to the property key. The array length of `opt_keys` has + * to match the length of `values`. For serializing a sequence, `opt_keys` + * determines the order of the sequence. + * @param {T=} opt_this The object to use as `this` for the node factory and + * serializers. + * @template T + */ +ol.xml.serialize = function( + serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) { + var length = (goog.isDef(opt_keys) ? opt_keys : values).length; + var value, node; + for (var i = 0; i < length; ++i) { + value = values[i]; + if (goog.isDef(value)) { + node = nodeFactory.call(opt_this, value, objectStack, + goog.isDef(opt_keys) ? opt_keys[i] : undefined); + if (goog.isDef(node)) { + serializersNS[node.namespaceURI][node.localName] + .call(opt_this, node, value, objectStack); + } + } + } +}; + + +/** + * @param {O} object Object. + * @param {Object.>} serializersNS + * Namespaced serializers. + * @param {function(this: T, *, Array.<*>, (string|undefined)): (Node|undefined)} nodeFactory + * Node factory. The `nodeFactory` creates the node whose namespace and name + * will be used to choose a node writer from `serializersNS`. This + * separation allows us to decide what kind of node to create, depending on + * the value we want to serialize. An example for this would be different + * geometry writers based on the geometry type. + * @param {Array.<*>} values Values to serialize. An example would be an array + * of {@link ol.Feature} instances. + * @param {Array.<*>} objectStack Node stack. + * @param {Array.=} opt_keys Keys of the `values`. Will be passed to the + * `nodeFactory`. This is used for serializing object literals where the + * node name relates to the property key. The array length of `opt_keys` has + * to match the length of `values`. For serializing a sequence, `opt_keys` + * determines the order of the sequence. + * @param {T=} opt_this The object to use as `this` for the node factory and + * serializers. + * @return {O|undefined} Object. + * @template O, T + */ +ol.xml.pushSerializeAndPop = function(object, + serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) { + objectStack.push(object); + ol.xml.serialize( + serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this); + return objectStack.pop(); +}; diff --git a/test/spec/ol/format/gpxformat.test.js b/test/spec/ol/format/gpxformat.test.js index b146289a61..5428bae0ba 100644 --- a/test/spec/ol/format/gpxformat.test.js +++ b/test/spec/ol/format/gpxformat.test.js @@ -5,7 +5,7 @@ describe('ol.format.GPX', function() { var format; beforeEach(function() { - format = new ol.format.GPX(); + format = new ol.format.GPX.V1_1(); }); describe('readFeatures', function() { @@ -27,7 +27,7 @@ describe('ol.format.GPX', function() { expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); }); - it('can read various rte attributes', function() { + it('can read and write various rte attributes', function() { var text = '' + ' ' + @@ -56,9 +56,11 @@ describe('ol.format.GPX', function() { expect(f.get('linkType')).to.be('Link type'); expect(f.get('number')).to.be(1); expect(f.get('type')).to.be('Type'); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a rte with multiple rtepts', function() { + it('can read and write a rte with multiple rtepts', function() { var text = '' + ' ' + @@ -74,6 +76,8 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.LineString); expect(g.getCoordinates()).to.eql([[2, 1, 0, 0], [4, 3, 0, 0]]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); }); @@ -95,7 +99,7 @@ describe('ol.format.GPX', function() { expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); }); - it('can read various trk attributes', function() { + it('can read and write various trk attributes', function() { var text = '' + ' ' + @@ -124,9 +128,11 @@ describe('ol.format.GPX', function() { expect(f.get('linkType')).to.be('Link type'); expect(f.get('number')).to.be(1); expect(f.get('type')).to.be('Type'); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a trk with an empty trkseg', function() { + it('can read and write a trk with an empty trkseg', function() { var text = '' + ' ' + @@ -141,9 +147,11 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.MultiLineString); expect(g.getCoordinates()).to.eql([[]]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a trk with a trkseg with multiple trkpts', function() { + it('can read/write a trk with a trkseg with multiple trkpts', function() { var text = '' + ' ' + @@ -169,9 +177,11 @@ describe('ol.format.GPX', function() { [[2, 1, 3, 1263115752], [6, 5, 7, 1263115812]] ]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a trk with multiple trksegs', function() { + it('can read and write a trk with multiple trksegs', function() { var text = '' + ' ' + @@ -208,13 +218,15 @@ describe('ol.format.GPX', function() { [[9, 8, 10, 1263115872], [12, 11, 13, 1263115932]] ]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); }); describe('wpt', function() { - it('can read a wpt', function() { + it('can read and write a wpt', function() { var text = '' + ' ' + @@ -227,9 +239,11 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.Point); expect(g.getCoordinates()).to.eql([2, 1, 0, 0]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a wpt with ele', function() { + it('can read and write a wpt with ele', function() { var text = '' + ' ' + @@ -244,9 +258,11 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.Point); expect(g.getCoordinates()).to.eql([2, 1, 3, 0]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a wpt with time', function() { + it('can read and write a wpt with time', function() { var text = '' + ' ' + @@ -261,9 +277,11 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.Point); expect(g.getCoordinates()).to.eql([2, 1, 0, 1263115752]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read a wpt with ele and time', function() { + it('can read and write a wpt with ele and time', function() { var text = '' + ' ' + @@ -279,9 +297,11 @@ describe('ol.format.GPX', function() { expect(g).to.be.an(ol.geom.Point); expect(g.getCoordinates()).to.eql([2, 1, 3, 1263115752]); expect(g.getLayout()).to.be(ol.geom.GeometryLayout.XYZM); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); - it('can read various wpt attributes', function() { + it('can read and write various wpt attributes', function() { var text = '' + ' ' + @@ -327,12 +347,18 @@ describe('ol.format.GPX', function() { expect(f.get('pdop')).to.be(8); expect(f.get('ageofdgpsdata')).to.be(9); expect(f.get('dgpsid')).to.be(10); + var serialized = format.writeFeatures(fs); + expect(serialized).to.xmleql(ol.xml.load(text)); }); }); describe('XML namespace support', function() { + beforeEach(function() { + format = new ol.format.GPX(); + }); + it('can read features with a version 1.0 namespace', function() { var text = '' + @@ -375,6 +401,8 @@ describe('ol.format.GPX', function() { goog.require('ol.Feature'); goog.require('ol.format.GPX'); +goog.require('ol.format.GPX.V1_1'); goog.require('ol.geom.LineString'); goog.require('ol.geom.MultiLineString'); goog.require('ol.geom.Point'); +goog.require('ol.xml');