--- a/labs/openlayers/lib/OpenLayers/Format/KML.js +++ b/labs/openlayers/lib/OpenLayers/Format/KML.js @@ -1,1 +1,1430 @@ - +/* Copyright (c) 2006-2010 by OpenLayers Contributors (see authors.txt for + * full list of contributors). Published under the Clear BSD license. + * See http://svn.openlayers.org/trunk/openlayers/license.txt for the + * full text of the license. */ + +/** + * @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/Console.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 + */ + extractAttributes: true, + + /** + * 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 + * Looks for <Style> nodes in the data and parses them + * Also parses <StyleMap> nodes, but only uses the 'normal' key + * + * 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, 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 + * Looks for <Style> nodes in the data and parses them + * Also 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]; + } + + if (styleUrl && key == "highlight") { + // TODO: implement the "select" part + } + + } + } + + }, + + + /** + * 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) { + feature.attributes[name] = obj.attributes[this.trackAttributes[j]][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 { + OpenLayers.Console.error(OpenLayers.i18n( + "unsupportedGeometryType", {'geomType':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);