diff options
Diffstat (limited to 'misc/openlayers/lib/OpenLayers/Format/KML.js')
-rw-r--r-- | misc/openlayers/lib/OpenLayers/Format/KML.js | 1517 |
1 files changed, 1517 insertions, 0 deletions
diff --git a/misc/openlayers/lib/OpenLayers/Format/KML.js b/misc/openlayers/lib/OpenLayers/Format/KML.js new file mode 100644 index 0000000..e10bce7 --- /dev/null +++ b/misc/openlayers/lib/OpenLayers/Format/KML.js @@ -0,0 +1,1517 @@ +/* Copyright (c) 2006-2013 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the 2-clause BSD license. + * See license.txt in the OpenLayers distribution or repository for the + * full text of the license. */ + +/** + * @requires OpenLayers/BaseTypes/Date.js + * @requires OpenLayers/Format/XML.js + * @requires OpenLayers/Feature/Vector.js + * @requires OpenLayers/Geometry/Point.js + * @requires OpenLayers/Geometry/LineString.js + * @requires OpenLayers/Geometry/Polygon.js + * @requires OpenLayers/Geometry/Collection.js + * @requires OpenLayers/Request/XMLHttpRequest.js + * @requires OpenLayers/Projection.js + */ + +/** + * Class: OpenLayers.Format.KML + * Read/Write KML. Create a new instance with the <OpenLayers.Format.KML> + * constructor. + * + * Inherits from: + * - <OpenLayers.Format.XML> + */ +OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, { + + /** + * Property: namespaces + * {Object} Mapping of namespace aliases to namespace URIs. + */ + namespaces: { + kml: "http://www.opengis.net/kml/2.2", + gx: "http://www.google.com/kml/ext/2.2" + }, + + /** + * APIProperty: kmlns + * {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". + * If set to null, no name element will be created. + */ + foldersName: "OpenLayers export", + + /** + * APIProperty: foldersDesc + * {String} Description of the folders. Default is "Exported on [date]." + * If set to null, no description element will be created. + */ + foldersDesc: "Exported on " + new Date(), + + /** + * APIProperty: extractAttributes + * {Boolean} Extract attributes from KML. Default is true. + * Extracting styleUrls requires this to be set to true + * Note that currently only Data and SimpleData + * elements are handled. + */ + extractAttributes: true, + + /** + * APIProperty: kvpAttributes + * {Boolean} Only used if extractAttributes is true. + * If set to true, attributes will be simple + * key-value pairs, compatible with other formats, + * Any displayName elements will be ignored. + * If set to false, attributes will be objects, + * retaining any displayName elements, but not + * compatible with other formats. Any CDATA in + * displayName will be read in as a string value. + * Default is false. + */ + kvpAttributes: false, + + /** + * Property: extractStyles + * {Boolean} Extract styles from KML. Default is false. + * Extracting styleUrls also requires extractAttributes to be + * set to true + */ + extractStyles: false, + + /** + * APIProperty: extractTracks + * {Boolean} Extract gx:Track elements from Placemark elements. Default + * is false. If true, features will be generated for all points in + * all gx:Track elements. Features will have a when (Date) attribute + * based on when elements in the track. If tracks include angle + * elements, features will have heading, tilt, and roll attributes. + * If track point coordinates have three values, features will have + * an altitude attribute with the third coordinate value. + */ + extractTracks: false, + + /** + * APIProperty: trackAttributes + * {Array} If <extractTracks> is true, points within gx:Track elements will + * be parsed as features with when, heading, tilt, and roll attributes. + * Any additional attribute names can be provided in <trackAttributes>. + */ + trackAttributes: null, + + /** + * Property: internalns + * {String} KML Namespace to use -- defaults to the namespace of the + * Placemark node being parsed, but falls back to kmlns. + */ + internalns: null, + + /** + * Property: features + * {Array} Array of features + * + */ + features: null, + + /** + * Property: styles + * {Object} Storage of style objects + * + */ + styles: null, + + /** + * Property: styleBaseUrl + * {String} + */ + styleBaseUrl: "", + + /** + * Property: fetched + * {Object} Storage of KML URLs that have been fetched before + * in order to prevent reloading them. + */ + fetched: null, + + /** + * APIProperty: maxDepth + * {Integer} Maximum depth for recursive loading external KML URLs + * Defaults to 0: do no external fetching + */ + maxDepth: 0, + + /** + * Constructor: OpenLayers.Format.KML + * Create a new parser for KML. + * + * Parameters: + * options - {Object} An optional object whose properties will be set on + * this instance. + */ + initialize: function(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), + kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/), + kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/), + straightBracket: (/\$\[(.*?)\]/g) + }; + // KML coordinates are always in longlat WGS84 + this.externalProjection = new OpenLayers.Projection("EPSG:4326"); + + OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); + }, + + /** + * APIMethod: read + * Read data from a string, and return a list of features. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} List of features. + */ + read: function(data) { + this.features = []; + this.styles = {}; + this.fetched = {}; + + // Set default options + var options = { + depth: 0, + styleBaseUrl: this.styleBaseUrl + }; + + return this.parseData(data, options); + }, + + /** + * Method: parseData + * Read data from a string, and return a list of features. + * + * Parameters: + * data - {String} or {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + * Returns: + * {Array(<OpenLayers.Feature.Vector>)} List of features. + */ + parseData: function(data, options) { + if(typeof data == "string") { + data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); + } + + // Loop throught the following node types in this order and + // process the nodes found + var types = ["Link", "NetworkLink", "Style", "StyleMap", "Placemark"]; + for(var i=0, len=types.length; i<len; ++i) { + var type = types[i]; + + var nodes = this.getElementsByTagNameNS(data, "*", type); + + // skip to next type if no nodes are found + if(nodes.length == 0) { + continue; + } + + switch (type.toLowerCase()) { + + // Fetch external links + case "link": + case "networklink": + this.parseLinks(nodes, options); + break; + + // parse style information + case "style": + if (this.extractStyles) { + this.parseStyles(nodes, options); + } + break; + case "stylemap": + if (this.extractStyles) { + this.parseStyleMaps(nodes, options); + } + break; + + // parse features + case "placemark": + this.parseFeatures(nodes, options); + break; + } + } + + return this.features; + }, + + /** + * Method: parseLinks + * Finds URLs of linked KML documents and fetches them + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseLinks: function(nodes, options) { + + // Fetch external links <NetworkLink> and <Link> + // Don't do anything if we have reached our maximum depth for recursion + if (options.depth >= this.maxDepth) { + return false; + } + + // increase depth + var newOptions = OpenLayers.Util.extend({}, options); + newOptions.depth++; + + for(var i=0, len=nodes.length; i<len; i++) { + var href = this.parseProperty(nodes[i], "*", "href"); + if(href && !this.fetched[href]) { + this.fetched[href] = true; // prevent reloading the same urls + var data = this.fetchLink(href); + if (data) { + this.parseData(data, newOptions); + } + } + } + + }, + + /** + * Method: fetchLink + * Fetches a URL and returns the result + * + * Parameters: + * href - {String} url to be fetched + * + */ + fetchLink: function(href) { + var request = OpenLayers.Request.GET({url: href, async: false}); + if (request) { + return request.responseText; + } + }, + + /** + * Method: parseStyles + * Parses <Style> nodes + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseStyles: function(nodes, options) { + for(var i=0, len=nodes.length; i<len; i++) { + var style = this.parseStyle(nodes[i]); + if(style) { + var styleName = (options.styleBaseUrl || "") + "#" + style.id; + + this.styles[styleName] = style; + } + } + }, + + /** + * Method: parseKmlColor + * Parses a kml color (in 'aabbggrr' format) and returns the corresponding + * color and opacity or null if the color is invalid. + * + * Parameters: + * kmlColor - {String} a kml formated color + * + * Returns: + * {Object} + */ + parseKmlColor: function(kmlColor) { + var color = null; + if (kmlColor) { + var matches = kmlColor.match(this.regExes.kmlColor); + if (matches) { + color = { + color: '#' + matches[4] + matches[3] + matches[2], + opacity: parseInt(matches[1], 16) / 255 + }; + } + } + return color; + }, + + /** + * Method: parseStyle + * Parses the children of a <Style> node and builds the style hash + * accordingly + * + * Parameters: + * node - {DOMElement} <Style> node + * + */ + parseStyle: function(node) { + var style = {}; + + var types = ["LineStyle", "PolyStyle", "IconStyle", "BalloonStyle", + "LabelStyle"]; + var type, styleTypeNode, nodeList, geometry, parser; + for(var i=0, len=types.length; i<len; ++i) { + type = types[i]; + styleTypeNode = this.getElementsByTagNameNS(node, "*", type)[0]; + if(!styleTypeNode) { + continue; + } + + // only deal with first geometry of this type + switch (type.toLowerCase()) { + case "linestyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["strokeColor"] = color.color; + style["strokeOpacity"] = color.opacity; + } + + var width = this.parseProperty(styleTypeNode, "*", "width"); + if (width) { + style["strokeWidth"] = width; + } + break; + + case "polystyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["fillOpacity"] = color.opacity; + style["fillColor"] = color.color; + } + // Check if fill is disabled + var fill = this.parseProperty(styleTypeNode, "*", "fill"); + if (fill == "0") { + style["fillColor"] = "none"; + } + // Check if outline is disabled + var outline = this.parseProperty(styleTypeNode, "*", "outline"); + if (outline == "0") { + style["strokeWidth"] = "0"; + } + + break; + + case "iconstyle": + // set scale + var scale = parseFloat(this.parseProperty(styleTypeNode, + "*", "scale") || 1); + + // set default width and height of icon + var width = 32 * scale; + var height = 32 * scale; + + var iconNode = this.getElementsByTagNameNS(styleTypeNode, + "*", + "Icon")[0]; + if (iconNode) { + var href = this.parseProperty(iconNode, "*", "href"); + if (href) { + + var w = this.parseProperty(iconNode, "*", "w"); + var h = this.parseProperty(iconNode, "*", "h"); + + // Settings for Google specific icons that are 64x64 + // We set the width and height to 64 and halve the + // scale to prevent icons from being too big + var google = "http://maps.google.com/mapfiles/kml"; + if (OpenLayers.String.startsWith( + href, google) && !w && !h) { + w = 64; + h = 64; + scale = scale / 2; + } + + // if only dimension is defined, make sure the + // other one has the same value + w = w || h; + h = h || w; + + if (w) { + width = parseInt(w) * scale; + } + + if (h) { + height = parseInt(h) * scale; + } + + // support for internal icons + // (/root://icons/palette-x.png) + // x and y tell the position on the palette: + // - in pixels + // - starting from the left bottom + // We translate that to a position in the list + // and request the appropriate icon from the + // google maps website + var matches = href.match(this.regExes.kmlIconPalette); + if (matches) { + var palette = matches[1]; + var file_extension = matches[2]; + + var x = this.parseProperty(iconNode, "*", "x"); + var y = this.parseProperty(iconNode, "*", "y"); + + var posX = x ? x/32 : 0; + var posY = y ? (7 - y/32) : 7; + + var pos = posY * 8 + posX; + href = "http://maps.google.com/mapfiles/kml/pal" + + palette + "/icon" + pos + file_extension; + } + + style["graphicOpacity"] = 1; // fully opaque + style["externalGraphic"] = href; + } + + } + + + // hotSpots define the offset for an Icon + var hotSpotNode = this.getElementsByTagNameNS(styleTypeNode, + "*", + "hotSpot")[0]; + if (hotSpotNode) { + var x = parseFloat(hotSpotNode.getAttribute("x")); + var y = parseFloat(hotSpotNode.getAttribute("y")); + + var xUnits = hotSpotNode.getAttribute("xunits"); + if (xUnits == "pixels") { + style["graphicXOffset"] = -x * scale; + } + else if (xUnits == "insetPixels") { + style["graphicXOffset"] = -width + (x * scale); + } + else if (xUnits == "fraction") { + style["graphicXOffset"] = -width * x; + } + + var yUnits = hotSpotNode.getAttribute("yunits"); + if (yUnits == "pixels") { + style["graphicYOffset"] = -height + (y * scale) + 1; + } + else if (yUnits == "insetPixels") { + style["graphicYOffset"] = -(y * scale) + 1; + } + else if (yUnits == "fraction") { + style["graphicYOffset"] = -height * (1 - y) + 1; + } + } + + style["graphicWidth"] = width; + style["graphicHeight"] = height; + break; + + case "balloonstyle": + var balloonStyle = OpenLayers.Util.getXmlNodeValue( + styleTypeNode); + if (balloonStyle) { + style["balloonStyle"] = balloonStyle.replace( + this.regExes.straightBracket, "${$1}"); + } + break; + case "labelstyle": + var kmlColor = this.parseProperty(styleTypeNode, "*", "color"); + var color = this.parseKmlColor(kmlColor); + if (color) { + style["fontColor"] = color.color; + style["fontOpacity"] = color.opacity; + } + break; + + default: + } + } + + // Some polygons have no line color, so we use the fillColor for that + if (!style["strokeColor"] && style["fillColor"]) { + style["strokeColor"] = style["fillColor"]; + } + + var id = node.getAttribute("id"); + if (id && style) { + style.id = id; + } + + return style; + }, + + /** + * Method: parseStyleMaps + * Parses <StyleMap> nodes, but only uses the 'normal' key + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseStyleMaps: function(nodes, options) { + // Only the default or "normal" part of the StyleMap is processed now + // To do the select or "highlight" bit, we'd need to change lots more + + for(var i=0, len=nodes.length; i<len; i++) { + var node = nodes[i]; + var pairs = this.getElementsByTagNameNS(node, "*", + "Pair"); + + var id = node.getAttribute("id"); + for (var j=0, jlen=pairs.length; j<jlen; j++) { + var pair = pairs[j]; + // Use the shortcut in the SLD format to quickly retrieve the + // value of a node. Maybe it's good to have a method in + // Format.XML to do this + var key = this.parseProperty(pair, "*", "key"); + var styleUrl = this.parseProperty(pair, "*", "styleUrl"); + + if (styleUrl && key == "normal") { + this.styles[(options.styleBaseUrl || "") + "#" + id] = + this.styles[(options.styleBaseUrl || "") + styleUrl]; + } + + // TODO: implement the "select" part + //if (styleUrl && key == "highlight") { + //} + + } + } + + }, + + + /** + * Method: parseFeatures + * Loop through all Placemark nodes and parse them. + * Will create a list of features + * + * Parameters: + * nodes - {Array} of {DOMElement} data to read/parse. + * options - {Object} Hash of options + * + */ + parseFeatures: function(nodes, options) { + var features = []; + for(var i=0, len=nodes.length; i<len; i++) { + var featureNode = nodes[i]; + var feature = this.parseFeature.apply(this,[featureNode]) ; + if(feature) { + + // Create reference to styleUrl + if (this.extractStyles && feature.attributes && + feature.attributes.styleUrl) { + feature.style = this.getStyle(feature.attributes.styleUrl, options); + } + + if (this.extractStyles) { + // Make sure that <Style> nodes within a placemark are + // processed as well + var inlineStyleNode = this.getElementsByTagNameNS(featureNode, + "*", + "Style")[0]; + if (inlineStyleNode) { + var inlineStyle= this.parseStyle(inlineStyleNode); + if (inlineStyle) { + feature.style = OpenLayers.Util.extend( + feature.style, inlineStyle + ); + } + } + } + + // check if gx:Track elements should be parsed + if (this.extractTracks) { + var tracks = this.getElementsByTagNameNS( + featureNode, this.namespaces.gx, "Track" + ); + if (tracks && tracks.length > 0) { + var track = tracks[0]; + var container = { + features: [], + feature: feature + }; + this.readNode(track, container); + if (container.features.length > 0) { + features.push.apply(features, container.features); + } + } + } else { + // add feature to list of features + features.push(feature); + } + } else { + throw "Bad Placemark: " + i; + } + } + + // add new features to existing feature list + this.features = this.features.concat(features); + }, + + /** + * Property: readers + * Contains public functions, grouped by namespace prefix, that will + * be applied when a namespaced node is found matching the function + * name. The function will be applied in the scope of this parser + * with two arguments: the node being read and a context object passed + * from the parent. + */ + readers: { + "kml": { + "when": function(node, container) { + container.whens.push(OpenLayers.Date.parse( + this.getChildValue(node) + )); + }, + "_trackPointAttribute": function(node, container) { + var name = node.nodeName.split(":").pop(); + container.attributes[name].push(this.getChildValue(node)); + } + }, + "gx": { + "Track": function(node, container) { + var obj = { + whens: [], + points: [], + angles: [] + }; + if (this.trackAttributes) { + var name; + obj.attributes = {}; + for (var i=0, ii=this.trackAttributes.length; i<ii; ++i) { + name = this.trackAttributes[i]; + obj.attributes[name] = []; + if (!(name in this.readers.kml)) { + this.readers.kml[name] = this.readers.kml._trackPointAttribute; + } + } + } + this.readChildNodes(node, obj); + if (obj.whens.length !== obj.points.length) { + throw new Error("gx:Track with unequal number of when (" + + obj.whens.length + ") and gx:coord (" + + obj.points.length + ") elements."); + } + var hasAngles = obj.angles.length > 0; + if (hasAngles && obj.whens.length !== obj.angles.length) { + throw new Error("gx:Track with unequal number of when (" + + obj.whens.length + ") and gx:angles (" + + obj.angles.length + ") elements."); + } + var feature, point, angles; + for (var i=0, ii=obj.whens.length; i<ii; ++i) { + feature = container.feature.clone(); + feature.fid = container.feature.fid || container.feature.id; + point = obj.points[i]; + feature.geometry = point; + if ("z" in point) { + feature.attributes.altitude = point.z; + } + if (this.internalProjection && this.externalProjection) { + feature.geometry.transform( + this.externalProjection, this.internalProjection + ); + } + if (this.trackAttributes) { + for (var j=0, jj=this.trackAttributes.length; j<jj; ++j) { + var name = this.trackAttributes[j]; + feature.attributes[name] = obj.attributes[name][i]; + } + } + feature.attributes.when = obj.whens[i]; + feature.attributes.trackId = container.feature.id; + if (hasAngles) { + angles = obj.angles[i]; + feature.attributes.heading = parseFloat(angles[0]); + feature.attributes.tilt = parseFloat(angles[1]); + feature.attributes.roll = parseFloat(angles[2]); + } + container.features.push(feature); + } + }, + "coord": function(node, container) { + var str = this.getChildValue(node); + var coords = str.replace(this.regExes.trimSpace, "").split(/\s+/); + var point = new OpenLayers.Geometry.Point(coords[0], coords[1]); + if (coords.length > 2) { + point.z = parseFloat(coords[2]); + } + container.points.push(point); + }, + "angles": function(node, container) { + var str = this.getChildValue(node); + var parts = str.replace(this.regExes.trimSpace, "").split(/\s+/); + container.angles.push(parts); + } + } + }, + + /** + * 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 - {DOMElement} + * + * Returns: + * {<OpenLayers.Feature.Vector>} 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, len=order.length; i<len; ++i) { + type = order[i]; + this.internalns = node.namespaceURI ? + node.namespaceURI : this.kmlns; + nodeList = this.getElementsByTagNameNS(node, + this.internalns, type); + if(nodeList.length > 0) { + // only deal with first geometry of this type + var parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + geometry = parser.apply(this, [nodeList[0]]); + if (this.internalProjection && this.externalProjection) { + geometry.transform(this.externalProjection, + this.internalProjection); + } + } else { + throw new TypeError("Unsupported geometry type: " + type); + } + // stop looking for different geometry types + break; + } + } + + // 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") || node.getAttribute("name"); + if(fid != null) { + feature.fid = fid; + } + + return feature; + }, + + /** + * Method: getStyle + * Retrieves a style from a style hash using styleUrl as the key + * If the styleUrl doesn't exist yet, we try to fetch it + * Internet + * + * Parameters: + * styleUrl - {String} URL of style + * options - {Object} Hash of options + * + * Returns: + * {Object} - (reference to) Style hash + */ + getStyle: function(styleUrl, options) { + + var styleBaseUrl = OpenLayers.Util.removeTail(styleUrl); + + var newOptions = OpenLayers.Util.extend({}, options); + newOptions.depth++; + newOptions.styleBaseUrl = styleBaseUrl; + + // Fetch remote Style URLs (if not fetched before) + if (!this.styles[styleUrl] + && !OpenLayers.String.startsWith(styleUrl, "#") + && newOptions.depth <= this.maxDepth + && !this.fetched[styleBaseUrl] ) { + + var data = this.fetchLink(styleBaseUrl); + if (data) { + this.parseData(data, newOptions); + } + + } + + // return requested style + var style = OpenLayers.Util.extend({}, this.styles[styleUrl]); + return style; + }, + + /** + * Property: parseGeometry + * Properties of this object are the functions that parse geometries based + * on their type. + */ + 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: + * {<OpenLayers.Geometry.Point>} A point geometry. + */ + point: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "coordinates"); + var coords = []; + if(nodeList.length > 0) { + var coordString = nodeList[0].firstChild.nodeValue; + coordString = coordString.replace(this.regExes.removeSpace, ""); + coords = coordString.split(","); + } + + 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 { + 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: + * {<OpenLayers.Geometry.LineString>} A linestring geometry. + */ + linestring: function(node, ring) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "coordinates"); + var line = null; + if(nodeList.length > 0) { + var coordString = this.getChildValue(nodeList[0]); + + 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<numPoints; ++i) { + coords = pointList[i].split(","); + numCoords = coords.length; + if(numCoords > 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: + * {<OpenLayers.Geometry.Polygon>} A polygon geometry. + */ + polygon: function(node) { + var nodeList = this.getElementsByTagNameNS(node, this.internalns, + "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, len=nodeList.length; i<len; ++i) { + ring = this.parseGeometry.linestring.apply(this, + [nodeList[i], true]); + if(ring) { + components[i] = ring; + } else { + throw "Bad LinearRing geometry: " + i; + } + } + } + return new OpenLayers.Geometry.Polygon(components); + }, + + /** + * Method: parseGeometry.multigeometry + * Given a KML node representing a multigeometry, create an + * OpenLayers geometry collection. + * + * Parameters: + * node - {DOMElement} A KML MultiGeometry node. + * + * Returns: + * {<OpenLayers.Geometry.Collection>} A geometry collection. + */ + multigeometry: function(node) { + var child, parser; + var parts = []; + var children = node.childNodes; + for(var i=0, len=children.length; i<len; ++i ) { + child = children[i]; + if(child.nodeType == 1) { + var type = (child.prefix) ? + child.nodeName.split(":")[1] : + child.nodeName; + var parser = this.parseGeometry[type.toLowerCase()]; + if(parser) { + parts.push(parser.apply(this, [child])); + } + } + } + return new OpenLayers.Geometry.Collection(parts); + } + + }, + + /** + * Method: parseAttributes + * + * Parameters: + * node - {DOMElement} + * + * Returns: + * {Object} An attributes object. + */ + parseAttributes: function(node) { + var attributes = {}; + + // Extended Data is parsed first. + var edNodes = node.getElementsByTagName("ExtendedData"); + if (edNodes.length) { + attributes = this.parseExtendedData(edNodes[0]); + } + + // assume attribute nodes are type 1 children with a type 3 or 4 child + var child, grandchildren, grandchild; + var children = node.childNodes; + + for(var i=0, len=children.length; i<len; ++i) { + child = children[i]; + if(child.nodeType == 1) { + grandchildren = child.childNodes; + if(grandchildren.length >= 1 && grandchildren.length <= 3) { + var grandchild; + switch (grandchildren.length) { + case 1: + grandchild = grandchildren[0]; + break; + case 2: + var c1 = grandchildren[0]; + var c2 = grandchildren[1]; + grandchild = (c1.nodeType == 3 || c1.nodeType == 4) ? + c1 : c2; + break; + case 3: + default: + grandchild = grandchildren[1]; + break; + } + if(grandchild.nodeType == 3 || grandchild.nodeType == 4) { + var name = (child.prefix) ? + child.nodeName.split(":")[1] : + child.nodeName; + var value = OpenLayers.Util.getXmlNodeValue(grandchild); + if (value) { + value = value.replace(this.regExes.trimSpace, ""); + attributes[name] = value; + } + } + } + } + } + return attributes; + }, + + /** + * Method: parseExtendedData + * Parse ExtendedData from KML. Limited support for schemas/datatypes. + * See http://code.google.com/apis/kml/documentation/kmlreference.html#extendeddata + * for more information on extendeddata. + */ + parseExtendedData: function(node) { + var attributes = {}; + var i, len, data, key; + var dataNodes = node.getElementsByTagName("Data"); + for (i = 0, len = dataNodes.length; i < len; i++) { + data = dataNodes[i]; + key = data.getAttribute("name"); + var ed = {}; + var valueNode = data.getElementsByTagName("value"); + if (valueNode.length) { + ed['value'] = this.getChildValue(valueNode[0]); + } + if (this.kvpAttributes) { + attributes[key] = ed['value']; + } else { + var nameNode = data.getElementsByTagName("displayName"); + if (nameNode.length) { + ed['displayName'] = this.getChildValue(nameNode[0]); + } + attributes[key] = ed; + } + } + var simpleDataNodes = node.getElementsByTagName("SimpleData"); + for (i = 0, len = simpleDataNodes.length; i < len; i++) { + var ed = {}; + data = simpleDataNodes[i]; + key = data.getAttribute("name"); + ed['value'] = this.getChildValue(data); + if (this.kvpAttributes) { + attributes[key] = ed['value']; + } else { + ed['displayName'] = key; + attributes[key] = ed; + } + } + + return attributes; + }, + + /** + * Method: parseProperty + * Convenience method to find a node and return its value + * + * Parameters: + * xmlNode - {<DOMElement>} + * namespace - {String} namespace of the node to find + * tagName - {String} name of the property to parse + * + * Returns: + * {String} The value for the requested property (defaults to null) + */ + parseProperty: function(xmlNode, namespace, tagName) { + var value; + var nodeList = this.getElementsByTagNameNS(xmlNode, namespace, tagName); + try { + value = OpenLayers.Util.getXmlNodeValue(nodeList[0]); + } catch(e) { + value = null; + } + + return value; + }, + + /** + * APIMethod: write + * Accept Feature Collection, and return a string. + * + * Parameters: + * features - {Array(<OpenLayers.Feature.Vector>)} An array of features. + * + * Returns: + * {String} A KML string. + */ + write: function(features) { + if(!(OpenLayers.Util.isArray(features))) { + features = [features]; + } + var kml = this.createElementNS(this.kmlns, "kml"); + var folder = this.createFolderXML(); + for(var i=0, len=features.length; i<len; ++i) { + folder.appendChild(this.createPlacemarkXML(features[i])); + } + kml.appendChild(folder); + return OpenLayers.Format.XML.prototype.write.apply(this, [kml]); + }, + + /** + * Method: createFolderXML + * Creates and returns a KML folder node + * + * Returns: + * {DOMElement} + */ + createFolderXML: function() { + // Folder + var folder = this.createElementNS(this.kmlns, "Folder"); + + // Folder name + if (this.foldersName) { + var folderName = this.createElementNS(this.kmlns, "name"); + var folderNameText = this.createTextNode(this.foldersName); + folderName.appendChild(folderNameText); + folder.appendChild(folderName); + } + + // Folder description + if (this.foldersDesc) { + var folderDesc = this.createElementNS(this.kmlns, "description"); + var folderDescText = this.createTextNode(this.foldersDesc); + folderDesc.appendChild(folderDescText); + folder.appendChild(folderDesc); + } + + return folder; + }, + + /** + * Method: createPlacemarkXML + * Creates and returns a KML placemark node representing the given feature. + * + * Parameters: + * feature - {<OpenLayers.Feature.Vector>} + * + * Returns: + * {DOMElement} + */ + createPlacemarkXML: function(feature) { + // Placemark name + var placemarkName = this.createElementNS(this.kmlns, "name"); + var label = (feature.style && feature.style.label) ? feature.style.label : feature.id; + var name = feature.attributes.name || label; + placemarkName.appendChild(this.createTextNode(name)); + + // Placemark description + var placemarkDesc = this.createElementNS(this.kmlns, "description"); + var desc = 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); + + // output attributes as extendedData + if (feature.attributes) { + var edNode = this.buildExtendedData(feature.attributes); + if (edNode) { + placemarkNode.appendChild(edNode); + } + } + + return placemarkNode; + }, + + /** + * Method: buildGeometryNode + * Builds and returns a KML geometry node with the given geometry. + * + * Parameters: + * geometry - {<OpenLayers.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 - {<OpenLayers.Geometry.Point>} 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 - {<OpenLayers.Geometry.Point>} A multipoint geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipoint: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.linestring + * Given an OpenLayers linestring geometry, create a KML linestring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LineString>} 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 - {<OpenLayers.Geometry.Point>} A multilinestring geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multilinestring: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.linearring + * Given an OpenLayers linearring geometry, create a KML linearring. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.LinearRing>} 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 - {<OpenLayers.Geometry.Polygon>} 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, len=rings.length; i<len; ++i) { + type = (i==0) ? "outerBoundaryIs" : "innerBoundaryIs"; + ringMember = this.createElementNS(this.kmlns, type); + ringGeom = this.buildGeometry.linearring.apply(this, + [rings[i]]); + ringMember.appendChild(ringGeom); + kml.appendChild(ringMember); + } + return kml; + }, + + /** + * Method: buildGeometry.multipolygon + * Given an OpenLayers multipolygon geometry, create a KML + * GeometryCollection. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Point>} A multipolygon geometry. + * + * Returns: + * {DOMElement} A KML GeometryCollection node. + */ + multipolygon: function(geometry) { + return this.buildGeometry.collection.apply(this, [geometry]); + }, + + /** + * Method: buildGeometry.collection + * Given an OpenLayers geometry collection, create a KML MultiGeometry. + * + * Parameters: + * geometry - {<OpenLayers.Geometry.Collection>} 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, len=geometry.components.length; i<len; ++i) { + child = this.buildGeometryNode.apply(this, + [geometry.components[i]]); + if(child) { + kml.appendChild(child); + } + } + return kml; + } + }, + + /** + * Method: buildCoordinatesNode + * Builds and returns the KML coordinates node with the given geometry + * <coordinates>...</coordinates> + * + * Parameters: + * geometry - {<OpenLayers.Geometry>} + * + * Returns: + * {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<numPoints; ++i) { + point = points[i]; + parts[i] = this.buildCoordinates(point); + } + path = parts.join(" "); + } else { + // Point + path = this.buildCoordinates(geometry); + } + + var txtNode = this.createTextNode(path); + coordinatesNode.appendChild(txtNode); + + return coordinatesNode; + }, + + /** + * Method: buildCoordinates + * + * Parameters: + * point - {<OpenLayers.Geometry.Point>} + * + * Returns + * {String} a coordinate pair + */ + buildCoordinates: function(point) { + if (this.internalProjection && this.externalProjection) { + point = point.clone(); + point.transform(this.internalProjection, + this.externalProjection); + } + return point.x + "," + point.y; + }, + + /** + * Method: buildExtendedData + * + * Parameters: + * attributes - {Object} + * + * Returns + * {DOMElement} A KML ExtendedData node or {null} if no attributes. + */ + buildExtendedData: function(attributes) { + var extendedData = this.createElementNS(this.kmlns, "ExtendedData"); + for (var attributeName in attributes) { + // empty, name, description, styleUrl attributes ignored + if (attributes[attributeName] && attributeName != "name" && attributeName != "description" && attributeName != "styleUrl") { + var data = this.createElementNS(this.kmlns, "Data"); + data.setAttribute("name", attributeName); + var value = this.createElementNS(this.kmlns, "value"); + if (typeof attributes[attributeName] == "object") { + // cater for object attributes with 'value' properties + // other object properties will output an empty node + if (attributes[attributeName].value) { + value.appendChild(this.createTextNode(attributes[attributeName].value)); + } + if (attributes[attributeName].displayName) { + var displayName = this.createElementNS(this.kmlns, "displayName"); + // displayName always written as CDATA + displayName.appendChild(this.getXMLDoc().createCDATASection(attributes[attributeName].displayName)); + data.appendChild(displayName); + } + } else { + value.appendChild(this.createTextNode(attributes[attributeName])); + } + data.appendChild(value); + extendedData.appendChild(data); + } + } + if (this.isSimpleContent(extendedData)) { + return null; + } else { + return extendedData; + } + }, + + CLASS_NAME: "OpenLayers.Format.KML" +}); |