Add KML Styling support. Thanks rdewit for this great contribution. r=crschmidt,me (closes #1259)

git-svn-id: http://svn.openlayers.org/trunk/openlayers@6179 dc9f47b5-9b13-0410-9fdd-eb0c1a62fdaf
This commit is contained in:
ahocevar
2008-02-09 20:42:32 +00:00
parent 06571e2cf3
commit c70fb21103
3 changed files with 556 additions and 21 deletions

View File

@@ -48,9 +48,18 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
/**
* 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,
/**
* Property: internalns
* {String} KML Namespace to use -- defaults to the namespace of the
@@ -58,6 +67,40 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
*/
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.
@@ -72,7 +115,10 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
trimSpace: (/^\s*|\s*$/g),
removeSpace: (/\s*/g),
splitSpace: (/\s+/),
trimComma: (/\s*,\s*/g)
trimComma: (/\s*,\s*/g),
kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/),
kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/),
straightBracket: (/\$\[(.*?)\]/g)
};
OpenLayers.Format.XML.prototype.initialize.apply(this, [options]);
},
@@ -82,29 +128,431 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
* Read data from a string, and return a list of features.
*
* Parameters:
* data - {String} or {DOMElement} data to read/parse.
* 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: this.maxDepth,
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]);
}
var featureNodes = this.getElementsByTagNameNS(data,
'*',
"Placemark");
var numFeatures = featureNodes.length;
var features = new Array(numFeatures);
for(var i=0; i<numFeatures; i++) {
var feature = this.parseFeature(featureNodes[i]);
// 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; i<types.length; ++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; i < nodes.length; 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) {
if (OpenLayers.ProxyHost
&& OpenLayers.String.startsWith(href, "http")) {
href = OpenLayers.ProxyHost + escape(href);
}
var request = new OpenLayers.Ajax.Request(href,
{method: 'get', asynchronous: false });
if (request && request.transport) {
return request.transport.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; i < nodes.length; i++) {
var style = this.parseStyle(nodes[i]);
if(style) {
styleName = (options.styleBaseUrl || "") + "#" + style.id;
this.styles[styleName] = style;
}
}
},
/**
* 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"];
var type, nodeList, geometry, parser;
for(var i=0; i<types.length; ++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 color = this.parseProperty(styleTypeNode, "*", "color");
if (color) {
var matches = (color.toString()).match(
this.regExes.kmlColor);
// transparency
var alpha = matches[1];
style["strokeOpacity"] = parseInt(alpha, 16) / 255;
// rgb colors (google uses bgr)
var b = matches[2];
var g = matches[3];
var r = matches[4];
style["strokeColor"] = "#" + r + g + b;
}
var width = this.parseProperty(styleTypeNode, "*", "width");
if (width) {
style["strokeWidth"] = width;
}
case "polystyle":
var color = this.parseProperty(styleTypeNode, "*", "color");
if (color) {
var matches = (color.toString()).match(
this.regExes.kmlColor);
// transparency
var alpha = matches[1];
style["fillOpacity"] = parseInt(alpha, 16) / 255;
// rgb colors (google uses bgr)
var b = matches[2];
var g = matches[3];
var r = matches[4];
style["fillColor"] = "#" + r + g + b;
}
break;
case "iconstyle":
var iconNode = this.getElementsByTagNameNS(styleTypeNode,
"*",
"Icon")[0];
// set default width and height of icon
style["graphicWidth"] = 32;
style["graphicHeight"] = 32;
if (iconNode) {
var href = this.parseProperty(iconNode, "*", "href");
if (href) {
// 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;
}
var w = this.parseProperty(iconNode, "*", "w");
if (w) {
style["graphicWidth"] = parseInt(w);
}
var h = this.parseProperty(iconNode, "*", "h");
if (h) {
style["graphicHeight"] = parseInt(h);
}
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 = hotSpotNode.getAttribute("x");
var y = hotSpotNode.getAttribute("y");
var xUnits = hotSpotNode.getAttribute("xunits");
if (xUnits == "pixels") {
style["graphicXOffset"] = parseInt(x);
}
else if (xUnits == "insetPixels") {
style["graphicXOffset"] = style["graphicWidth"]
- parseInt(x);
}
else if (xUnits == "fraction") {
style["graphicXOffset"] = style["graphicWidth"]
* parseFloat(x);
}
var yUnits = hotSpotNode.getAttribute("yunits");
if (yUnits == "pixels") {
style["graphicYOffset"] = parseInt(y);
}
else if (yUnits == "insetPixels") {
style["graphicYOffset"] = style["graphicHeight"]
- parseInt(y);
}
else if (yUnits == "fraction") {
style["graphicYOffset"] = style["graphicHeight"]
* parseFloat(y);
}
}
break;
case "balloonstyle":
var balloonStyle = OpenLayers.Util.getXmlNodeValue(
styleTypeNode);
if (balloonStyle) {
style["balloonStyle"] = balloonStyle.replace(
this.regExes.straightBracket, "${$1}");
}
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; i < nodes.length; i++) {
var node = nodes[i];
var pairs = this.getElementsByTagNameNS(node, "*",
"Pair");
var id = node.getAttribute("id");
for (var j=0; j<pairs.length; 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 = new Array(nodes.length);
for(var i=0; i < nodes.length; 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);
}
// 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(styleNode);
if (inlineStyle) {
feature.style = OpenLayers.Util.extend({},
feature.style);
OpenLayers.Util.extend(feature.style, inlineStyle);
}
}
// add feature to list of features
features[i] = feature;
} else {
throw "Bad Placemark: " + i;
}
}
return features;
// add new features to existing feature list
this.features = this.features.concat(features);
},
/**
@@ -154,7 +602,7 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
}
var feature = new OpenLayers.Feature.Vector(geometry, attributes);
var fid = node.getAttribute("id");
var fid = node.getAttribute("id") || node.getAttribute("name");
if(fid != null) {
feature.fid = fid;
}
@@ -162,6 +610,45 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
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 = this.styles[styleUrl];
return style;
},
/**
* Property: parseGeometry
* Properties of this object are the functions that parse geometries based
@@ -334,29 +821,65 @@ OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
*/
parseAttributes: function(node) {
var attributes = {};
// assume attribute nodes are type 1 children with a type 3 child
// 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; i<children.length; ++i) {
child = children[i];
if(child.nodeType == 1) {
grandchildren = child.childNodes;
if(grandchildren.length == 1) {
grandchild = grandchildren[0];
if(grandchildren.length == 1 || grandchildren.length == 3) {
var grandchild;
switch (grandchildren.length) {
case 1:
grandchild = grandchildren[0];
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 = grandchild.nodeValue.replace(
this.regExes.trimSpace, "");
attributes[name] = value;
var value = OpenLayers.Util.getXmlNodeValue(grandchild)
if (value) {
value = value.replace(this.regExes.trimSpace, "");
attributes[name] = value;
}
}
}
}
}
}
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.