diff --git a/examples/vector-formats.html b/examples/vector-formats.html index 3388b7ea7c..7049a43e53 100644 --- a/examples/vector-formats.html +++ b/examples/vector-formats.html @@ -77,8 +77,8 @@ formats = { wkt: new OpenLayers.Format.WKT(), geojson: new OpenLayers.Format.GeoJSON(), - gml: new OpenLayers.Format.GML() //, - //kml: new OpenLayers.Format.KML() + gml: new OpenLayers.Format.GML(), + kml: new OpenLayers.Format.KML() }; map.setCenter(new OpenLayers.LonLat(0, 0), 1); @@ -146,7 +146,7 @@ diff --git a/lib/OpenLayers/Format/KML.js b/lib/OpenLayers/Format/KML.js index a2aaa7b3ec..48b35fb1bb 100644 --- a/lib/OpenLayers/Format/KML.js +++ b/lib/OpenLayers/Format/KML.js @@ -5,34 +5,63 @@ /** * @requires OpenLayers/Format.js * @requires OpenLayers/Feature/Vector.js - * @requires OpenLayers/Ajax.js - * - * Class: OpenLayers.Format.KML - * Read only KML. Largely Proof of Concept: does not support advanced Features, - * including Polygons. Create a new instance with the - * constructor. * + * Class: OpenLayers.Format.KML + * Read/Wite KML. Create a new instance with the + * constructor. + * * Inherits from: - * - + * - */ -OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format, { +OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, { /** * APIProperty: kmlns - * KML Namespace to use. Defaults to 2.0 namespace. + * {String} KML Namespace to use. Defaults to 2.0 namespace. */ kmlns: "http://earth.google.com/kml/2.0", + /** + * APIProperty: placemarksDesc + * {String} Name of the placemarks. Default is "No description available." + */ + placemarksDesc: "No description available", + + /** + * APIProperty: foldersName + * {String} Name of the folders. Default is "OpenLayers export." + */ + foldersName: "OpenLayers export", + + /** + * APIProperty: foldersDesc + * {String} Description of the folders. Default is "Exported on [date]." + */ + foldersDesc: "Exported on " + new Date(), + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from KML. Default is true. + */ + extractAttributes: true, + /** * Constructor: OpenLayers.Format.KML - * Create a new parser for KML + * Create a new parser for KML. * * Parameters: * options - {Object} An optional object whose properties will be set on - * this instance. + * this instance. */ initialize: function(options) { - OpenLayers.Format.prototype.initialize.apply(this, [options]); + // compile regular expressions once instead of every time they are used + this.regExes = { + trimSpace: (/^\s*|\s*$/g), + removeSpace: (/\s*/g), + splitSpace: (/\s+/), + trimComma: (/\s*,\s*/g) + }; + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); }, /** @@ -40,156 +69,569 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format, { * Read data from a string, and return a list of features. * * Parameters: - * data - {string} or {XMLNode>} data to read/parse. + * data - {String} or {DOMElement} data to read/parse. + * + * Returns: + * {Array()} List of features. */ - read: function(data) { - if (typeof data == "string") { - data = OpenLayers.parseXMLString(data); - } - var featureNodes = OpenLayers.Ajax.getElementsByTagNameNS(data, this.kmlns, "", "Placemark"); - - var features = []; - - // Process all the featureMembers - for (var i = 0; i < featureNodes.length; i++) { + read: function(data) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + var featureNodes = this.getElementsByTagNameNS(data, + this.kmlns, + "Placemark"); + var numFeatures = featureNodes.length; + var features = new Array(numFeatures); + for(var i=0; i} - */ - parseFeature: function(xmlNode) { - var geom; - var p; // [points,bounds] - - var feature = new OpenLayers.Feature.Vector(); - - // match Point - if (OpenLayers.Ajax.getElementsByTagNameNS(xmlNode, - this.kmlns, "", "Point").length != 0) { - var point = OpenLayers.Ajax.getElementsByTagNameNS(xmlNode, - this.kmlns, "", "Point")[0]; - - p = this.parseCoords(point); - if (p.points) { - geom = p.points[0]; - // TBD Bounds only set for one of multiple geometries - geom.extendBounds(p.bounds); - } - - // match LineString - } else if (OpenLayers.Ajax.getElementsByTagNameNS(xmlNode, - this.kmlns, "", "LineString").length != 0) { - var linestring = OpenLayers.Ajax.getElementsByTagNameNS(xmlNode, - this.kmlns, "", "LineString")[0]; - p = this.parseCoords(linestring); - if (p.points) { - geom = new OpenLayers.Geometry.LineString(p.points); - // TBD Bounds only set for one of multiple geometries - geom.extendBounds(p.bounds); + /** + * Method: parseFeature + * This function is the core of the KML parsing code in OpenLayers. + * It creates the geometries that are then attached to the returned + * feature, and calls parseAttributes() to get attribute data out. + * + * Parameters: + * node - {} + * + * Returns: + * {} A vector feature. + */ + parseFeature: function(node) { + // only accept one geometry per feature - look for highest "order" + var order = ["MultiGeometry", "Polygon", "LineString", "Point"]; + var type, nodeList, geometry, parser; + for(var i=0; i 0) { + // only deal with first geometry of this type + var parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + geometry = parser.apply(this, [nodeList[0]]); + } else { + OpenLayers.Console.error("Unsupported geometry type: " + + type); + } + // stop looking for different geometry types + break; } } - - feature.geometry = geom; - feature.attributes = this.parseAttributes(xmlNode); - + + // construct feature (optionally with attributes) + var attributes; + if(this.extractAttributes) { + attributes = this.parseAttributes(node); + } + var feature = new OpenLayers.Feature.Vector(geometry, attributes); + + var fid = node.getAttribute("id"); + if(fid != null) { + feature.fid = fid; + } + return feature; }, /** - * Method: parseAttributes - * recursive function parse the attributes of a KML node. - * Searches for any child nodes which aren't geometries, - * and gets their value. - * - * Parameters: - * xmlNode - {} + * Property: parseGeometry + * Properties of this object are the functions that parse geometries based + * on their type. */ - parseAttributes: function(xmlNode) { - var nodes = xmlNode.childNodes; - var attributes = {}; - for(var i = 0; i < nodes.length; i++) { - var name = nodes[i].nodeName; - var value = OpenLayers.Util.getXmlNodeValue(nodes[i]); - // Ignore Geometry attributes - // match ".//gml:pos|.//gml:posList|.//gml:coordinates" - if((name.search(":pos")!=-1) - ||(name.search(":posList")!=-1) - ||(name.search(":coordinates")!=-1)){ - continue; + parseGeometry: { + + /** + * Method: parseGeometry.point + * Given a KML node representing a point geometry, create an OpenLayers + * point geometry. + * + * Parameters: + * node - {DOMElement} A KML Point node. + * + * Returns: + * {} A point geometry. + */ + point: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.kmlns, + "coordinates"); + var coords = []; + if(nodeList.length > 0) { + var coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.removeSpace, ""); + coords = coordString.split(","); } - - // Check for a leaf node - if((nodes[i].childNodes.length == 1 && nodes[i].childNodes[0].nodeName == "#text") - || (nodes[i].childNodes.length == 0 && nodes[i].nodeName!="#text")) { - attributes[name] = value; - } - OpenLayers.Util.extend(attributes, this.parseAttributes(nodes[i])) - } - return attributes; - }, - - /** - * Method: parseCoords - * Extract Geographic coordinates from an XML node. - * - * Parameters: - * xmlNode - {} - * - * Returns: - * An array of points. - */ - parseCoords: function(xmlNode) { - var p = []; - p.points = []; - // TBD: Need to handle an array of coordNodes not just coordNodes[0] - - var coordNodes = OpenLayers.Ajax.getElementsByTagNameNS(xmlNode, this.kmlns, "", "coordinates")[0]; - var coordString = OpenLayers.Util.getXmlNodeValue(coordNodes); - - var firstCoord = coordString.split(" "); - - while (firstCoord[0] == "") - firstCoord.shift(); - - var dim = firstCoord[0].split(",").length; - // Extract an array of Numbers from CoordString - var nums = (coordString) ? coordString.split(/[, \n\t]+/) : []; - - - // Remove elements caused by leading and trailing white space - while (nums[0] == "") - nums.shift(); - - while (nums[nums.length-1] == "") - nums.pop(); - - for(i = 0; i < nums.length; i = i + dim) { - x = parseFloat(nums[i]); - y = parseFloat(nums[i+1]); - p.points.push(new OpenLayers.Geometry.Point(x, y)); - - if (!p.bounds) { - p.bounds = new OpenLayers.Bounds(x, y, x, y); + var point = null; + if(coords.length > 1) { + // preserve third dimension + if(coords.length == 2) { + coords[2] = null; + } + point = new OpenLayers.Geometry.Point(coords[0], coords[1], + coords[2]); } else { - p.bounds.extend(x, y); + throw "Bad coordinate string: " + coordString; + } + return point; + }, + + /** + * Method: parseGeometry.linestring + * Given a KML node representing a linestring geometry, create an + * OpenLayers linestring geometry. + * + * Parameters: + * node - {DOMElement} A KML LineString node. + * + * Returns: + * {} A linestring geometry. + */ + linestring: function(node, ring) { + var nodeList = this.getElementsByTagNameNS(node, this.kmlns, + "coordinates"); + var line = null; + if(nodeList.length > 0) { + var coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.trimSpace, + ""); + coordString = coordString.replace(this.regExes.trimComma, + ","); + var pointList = coordString.split(this.regExes.splitSpace); + var numPoints = pointList.length; + var points = new Array(numPoints); + var coords, numCoords; + for(var i=0; i 1) { + if(coords.length == 2) { + coords[2] = null; + } + points[i] = new OpenLayers.Geometry.Point(coords[0], + coords[1], + coords[2]); + } else { + throw "Bad LineString point coordinates: " + + pointList[i]; + } + } + if(numPoints) { + if(ring) { + line = new OpenLayers.Geometry.LinearRing(points); + } else { + line = new OpenLayers.Geometry.LineString(points); + } + } else { + throw "Bad LineString coordinates: " + coordString; + } + } + + return line; + }, + + /** + * Method: parseGeometry.polygon + * Given a KML node representing a polygon geometry, create an + * OpenLayers polygon geometry. + * + * Parameters: + * node - {DOMElement} A KML Polygon node. + * + * Returns: + * {} A polygon geometry. + */ + polygon: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.kmlns, + "LinearRing"); + var numRings = nodeList.length; + var components = new Array(numRings); + if(numRings > 0) { + // this assumes exterior ring first, inner rings after + var ring; + for(var i=0; i} A geometry collection. + */ + multigeometry: function(node) { + var child, parser; + var parts = []; + var children = node.childNodes; + for(var i=0; i} + * + * Returns: + * {Object} An attributes object. + */ + parseAttributes: function(node) { + var attributes = {}; + // assume attribute nodes are type 1 children with a type 3 child + var child, grandchildren, grandchild; + var children = node.childNodes; + for(var i=0; i features. + * + * Returns: + * {String} A KML string. + */ + write: function(features) { + if(!(features instanceof Array)) { + features = [features]; + } + var kml = this.createElementNS(this.kmlns, "kml"); + var folder = this.createFolderXML(); + for(var i=0; i} + * + * Returns: + * {DOMElement} + */ + createPlacemarkXML: function(feature) { + // Placemark name + var placemarkName = this.createElementNS(this.kmlns, "name"); + var name = (feature.attributes.name) ? + feature.attributes.name : feature.id; + placemarkName.appendChild(this.createTextNode(name)); + + // Placemark description + var placemarkDesc = this.createElementNS(this.kmlns, "description"); + var desc = (feature.attributes.description) ? + feature.attributes.description : this.placemarksDesc; + placemarkDesc.appendChild(this.createTextNode(desc)); + + // Placemark + var placemarkNode = this.createElementNS(this.kmlns, "Placemark"); + if(feature.fid != null) { + placemarkNode.setAttribute("id", feature.fid); + } + placemarkNode.appendChild(placemarkName); + placemarkNode.appendChild(placemarkDesc); + + // Geometry node (Point, LineString, etc. nodes) + var geometryNode = this.buildGeometryNode(feature.geometry); + placemarkNode.appendChild(geometryNode); + + // TBD - deal with remaining (non name/description) attributes. + return placemarkNode; + }, + + /** + * Method: buildGeometryNode + * Builds and returns a KML geometry node with the given geometry. + * + * Parameters: + * geometry - {} + * + * Returns: + * {DOMElement} + */ + buildGeometryNode: function(geometry) { + var className = geometry.CLASS_NAME; + var type = className.substring(className.lastIndexOf(".") + 1); + var builder = this.buildGeometry[type.toLowerCase()]; + var node = null; + if(builder) { + node = builder.apply(this, [geometry]); + } + return node; + }, + + /** + * Property: buildGeometry + * Object containing methods to do the actual geometry node building + * based on geometry type. + */ + buildGeometry: { + // TBD: Anybody care about namespace aliases here (these nodes have + // no prefixes)? + + /** + * Method: buildGeometry.point + * Given an OpenLayers point geometry, create a KML point. + * + * Parameters: + * geometry - {} A point geometry. + * + * Returns: + * {DOMElement} A KML point node. + */ + point: function(geometry) { + var kml = this.createElementNS(this.kmlns, "Point"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.multipoint + * Given an OpenLayers multipoint geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {} A multipoint geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipoint: function(geometry) { + return this.buildGeometry.collection(geometry); + }, + + /** + * Method: buildGeometry.linestring + * Given an OpenLayers linestring geometry, create a KML linestring. + * + * Parameters: + * geometry - {} A linestring geometry. + * + * Returns: + * {DOMElement} A KML linestring node. + */ + linestring: function(geometry) { + var kml = this.createElementNS(this.kmlns, "LineString"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.multilinestring + * Given an OpenLayers multilinestring geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {} A multilinestring geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multilinestring: function(geometry) { + return this.buildGeometry.collection(geometry); + }, + + /** + * Method: buildGeometry.linearring + * Given an OpenLayers linearring geometry, create a KML linearring. + * + * Parameters: + * geometry - {} A linearring geometry. + * + * Returns: + * {DOMElement} A KML linearring node. + */ + linearring: function(geometry) { + var kml = this.createElementNS(this.kmlns, "LinearRing"); + kml.appendChild(this.buildCoordinatesNode(geometry)); + return kml; + }, + + /** + * Method: buildGeometry.polygon + * Given an OpenLayers polygon geometry, create a KML polygon. + * + * Parameters: + * geometry - {} A polygon geometry. + * + * Returns: + * {DOMElement} A KML polygon node. + */ + polygon: function(geometry) { + var kml = this.createElementNS(this.kmlns, "Polygon"); + var rings = geometry.components; + var ringMember, ringGeom, type; + for(var i=0; i} A multipolygon geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipolygon: function(geometry) { + return this.buildGeometry.collection(geometry); + }, + + /** + * Method: buildGeometry.collection + * Given an OpenLayers geometry collection, create a KML MultiGeometry. + * + * Parameters: + * geometry - {} A geometry collection. + * + * Returns: + * {DOMElement} A KML MultiGeometry node. + */ + collection: function(geometry) { + var kml = this.createElementNS(this.kmlns, "MultiGeometry"); + var child; + for(var i=0; i... + * + * Parameters: + * geometry - {} + * + * Return: + * {DOMElement} + */ + buildCoordinatesNode: function(geometry) { + var coordinatesNode = this.createElementNS(this.kmlns, "coordinates"); + + var path; + var points = geometry.components; + if(points) { + // LineString or LinearRing + var point; + var numPoints = points.length; + var parts = new Array(numPoints); + for(var i=0; i