]> dev.renevier.net Git - syp.git/blob - openlayers/lib/OpenLayers/Format/KML.js
initial commit
[syp.git] / openlayers / lib / OpenLayers / Format / KML.js
1 /* Copyright (c) 2006-2008 MetaCarta, Inc., published under the Clear BSD
2  * license.  See http://svn.openlayers.org/trunk/openlayers/license.txt for the
3  * full text of the license. */
4
5 /**
6  * @requires OpenLayers/Format/XML.js
7  * @requires OpenLayers/Feature/Vector.js
8  * @requires OpenLayers/Geometry/Point.js
9  * @requires OpenLayers/Geometry/LineString.js
10  * @requires OpenLayers/Geometry/Polygon.js
11  * @requires OpenLayers/Geometry/Collection.js
12  * @requires OpenLayers/Request/XMLHttpRequest.js
13  * @requires OpenLayers/Console.js
14  */
15
16 /**
17  * Class: OpenLayers.Format.KML
18  * Read/Wite KML. Create a new instance with the <OpenLayers.Format.KML>
19  *     constructor. 
20  * 
21  * Inherits from:
22  *  - <OpenLayers.Format.XML>
23  */
24 OpenLayers.Format.KML = OpenLayers.Class(OpenLayers.Format.XML, {
25     
26     /**
27      * APIProperty: kmlns
28      * {String} KML Namespace to use. Defaults to 2.0 namespace.
29      */
30     kmlns: "http://earth.google.com/kml/2.0",
31     
32     /** 
33      * APIProperty: placemarksDesc
34      * {String} Name of the placemarks.  Default is "No description available."
35      */
36     placemarksDesc: "No description available",
37     
38     /** 
39      * APIProperty: foldersName
40      * {String} Name of the folders.  Default is "OpenLayers export."
41      */
42     foldersName: "OpenLayers export",
43     
44     /** 
45      * APIProperty: foldersDesc
46      * {String} Description of the folders. Default is "Exported on [date]."
47      */
48     foldersDesc: "Exported on " + new Date(),
49     
50     /**
51      * APIProperty: extractAttributes
52      * {Boolean} Extract attributes from KML.  Default is true.
53      *           Extracting styleUrls requires this to be set to true
54      */
55     extractAttributes: true,
56     
57     /**
58      * Property: extractStyles
59      * {Boolean} Extract styles from KML.  Default is false.
60      *           Extracting styleUrls also requires extractAttributes to be
61      *           set to true
62      */
63     extractStyles: false,
64     
65     /**
66      * Property: internalns
67      * {String} KML Namespace to use -- defaults to the namespace of the
68      *     Placemark node being parsed, but falls back to kmlns. 
69      */
70     internalns: null,
71
72     /**
73      * Property: features
74      * {Array} Array of features
75      *     
76      */
77     features: null,
78
79     /**
80      * Property: styles
81      * {Object} Storage of style objects
82      *     
83      */
84     styles: null,
85     
86     /**
87      * Property: styleBaseUrl
88      * {String}
89      */
90     styleBaseUrl: "",
91
92     /**
93      * Property: fetched
94      * {Object} Storage of KML URLs that have been fetched before
95      *     in order to prevent reloading them.
96      */
97     fetched: null,
98
99     /**
100      * APIProperty: maxDepth
101      * {Integer} Maximum depth for recursive loading external KML URLs 
102      *           Defaults to 0: do no external fetching
103      */
104     maxDepth: 0,
105
106     /**
107      * Constructor: OpenLayers.Format.KML
108      * Create a new parser for KML.
109      *
110      * Parameters:
111      * options - {Object} An optional object whose properties will be set on
112      *     this instance.
113      */
114     initialize: function(options) {
115         // compile regular expressions once instead of every time they are used
116         this.regExes = {
117             trimSpace: (/^\s*|\s*$/g),
118             removeSpace: (/\s*/g),
119             splitSpace: (/\s+/),
120             trimComma: (/\s*,\s*/g),
121             kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/),
122             kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/),
123             straightBracket: (/\$\[(.*?)\]/g)
124         };
125         OpenLayers.Format.XML.prototype.initialize.apply(this, [options]);
126     },
127
128     /**
129      * APIMethod: read
130      * Read data from a string, and return a list of features. 
131      * 
132      * Parameters: 
133      * data    - {String} or {DOMElement} data to read/parse.
134      *
135      * Returns:
136      * {Array(<OpenLayers.Feature.Vector>)} List of features.
137      */
138     read: function(data) {
139         this.features = [];
140         this.styles   = {};
141         this.fetched  = {};
142
143         // Set default options 
144         var options = {
145             depth: 0,
146             styleBaseUrl: this.styleBaseUrl
147         };
148
149         return this.parseData(data, options);
150     },
151
152     /**
153      * Method: parseData
154      * Read data from a string, and return a list of features. 
155      * 
156      * Parameters: 
157      * data    - {String} or {DOMElement} data to read/parse.
158      * options - {Object} Hash of options
159      *
160      * Returns:
161      * {Array(<OpenLayers.Feature.Vector>)} List of features.
162      */
163     parseData: function(data, options) {
164         if(typeof data == "string") {
165             data = OpenLayers.Format.XML.prototype.read.apply(this, [data]);
166         }
167
168         // Loop throught the following node types in this order and
169         // process the nodes found 
170         var types = ["Link", "NetworkLink", "Style", "StyleMap", "Placemark"];
171         for(var i=0, len=types.length; i<len; ++i) {
172             var type = types[i];
173
174             var nodes = this.getElementsByTagNameNS(data, "*", type);
175
176             // skip to next type if no nodes are found
177             if(nodes.length == 0) { 
178                 continue;
179             }
180
181             switch (type.toLowerCase()) {
182
183                 // Fetch external links 
184                 case "link":
185                 case "networklink":
186                     this.parseLinks(nodes, options);
187                     break;
188
189                 // parse style information
190                 case "style":
191                     if (this.extractStyles) {
192                         this.parseStyles(nodes, options);
193                     }
194                     break;
195                 case "stylemap":
196                     if (this.extractStyles) {
197                         this.parseStyleMaps(nodes, options);
198                     }
199                     break;
200
201                 // parse features
202                 case "placemark":
203                     this.parseFeatures(nodes, options);
204                     break;
205             }
206         }
207         
208         return this.features;
209     },
210
211     /**
212      * Method: parseLinks
213      * Finds URLs of linked KML documents and fetches them
214      * 
215      * Parameters: 
216      * nodes   - {Array} of {DOMElement} data to read/parse.
217      * options - {Object} Hash of options
218      * 
219      */
220     parseLinks: function(nodes, options) {
221         
222         // Fetch external links <NetworkLink> and <Link>
223         // Don't do anything if we have reached our maximum depth for recursion
224         if (options.depth >= this.maxDepth) {
225             return false;
226         }
227
228         // increase depth
229         var newOptions = OpenLayers.Util.extend({}, options);
230         newOptions.depth++;
231
232         for(var i=0, len=nodes.length; i<len; i++) {
233             var href = this.parseProperty(nodes[i], "*", "href");
234             if(href && !this.fetched[href]) {
235                 this.fetched[href] = true; // prevent reloading the same urls
236                 var data = this.fetchLink(href);
237                 if (data) {
238                     this.parseData(data, newOptions);
239                 }
240             } 
241         }
242
243     },
244
245     /**
246      * Method: fetchLink
247      * Fetches a URL and returns the result
248      * 
249      * Parameters: 
250      * href  - {String} url to be fetched
251      * 
252      */
253     fetchLink: function(href) {
254         var request = OpenLayers.Request.GET({url: href, async: false});
255         if (request) {
256             return request.responseText;
257         }
258     },
259
260     /**
261      * Method: parseStyles
262      * Looks for <Style> nodes in the data and parses them
263      * Also parses <StyleMap> nodes, but only uses the 'normal' key
264      * 
265      * Parameters: 
266      * nodes    - {Array} of {DOMElement} data to read/parse.
267      * options  - {Object} Hash of options
268      * 
269      */
270     parseStyles: function(nodes, options) {
271         for(var i=0, len=nodes.length; i<len; i++) {
272             var style = this.parseStyle(nodes[i]);
273             if(style) {
274                 styleName = (options.styleBaseUrl || "") + "#" + style.id;
275                 
276                 this.styles[styleName] = style;
277             }
278         }
279     },
280
281     /**
282      * Method: parseStyle
283      * Parses the children of a <Style> node and builds the style hash
284      * accordingly
285      * 
286      * Parameters: 
287      * node - {DOMElement} <Style> node
288      * 
289      */
290     parseStyle: function(node) {
291         var style = {};
292         
293         var types = ["LineStyle", "PolyStyle", "IconStyle", "BalloonStyle"];
294         var type, nodeList, geometry, parser;
295         for(var i=0, len=types.length; i<len; ++i) {
296             type = types[i];
297             styleTypeNode = this.getElementsByTagNameNS(node, 
298                                                    "*", type)[0];
299             if(!styleTypeNode) { 
300                 continue;
301             }
302
303             // only deal with first geometry of this type
304             switch (type.toLowerCase()) {
305                 case "linestyle":
306                     var color = this.parseProperty(styleTypeNode, "*", "color");
307                     if (color) {
308                         var matches = (color.toString()).match(
309                                                          this.regExes.kmlColor);
310
311                         // transparency
312                         var alpha = matches[1];
313                         style["strokeOpacity"] = parseInt(alpha, 16) / 255;
314
315                         // rgb colors (google uses bgr)
316                         var b = matches[2]; 
317                         var g = matches[3]; 
318                         var r = matches[4]; 
319                         style["strokeColor"] = "#" + r + g + b;
320                     }
321                     
322                     var width = this.parseProperty(styleTypeNode, "*", "width");
323                     if (width) {
324                         style["strokeWidth"] = width;
325                     }
326
327                 case "polystyle":
328                     var color = this.parseProperty(styleTypeNode, "*", "color");
329                     if (color) {
330                         var matches = (color.toString()).match(
331                                                          this.regExes.kmlColor);
332
333                         // transparency
334                         var alpha = matches[1];
335                         style["fillOpacity"] = parseInt(alpha, 16) / 255;
336
337                         // rgb colors (google uses bgr)
338                         var b = matches[2]; 
339                         var g = matches[3]; 
340                         var r = matches[4]; 
341                         style["fillColor"] = "#" + r + g + b;
342                     }
343                      // Check is fill is disabled
344                     var fill = this.parseProperty(styleTypeNode, "*", "fill");
345                     if (fill == "0") {
346                         style["fillColor"] = "none";
347                     }
348                    
349                     break;
350                 case "iconstyle":
351                     // set scale
352                     var scale = parseFloat(this.parseProperty(styleTypeNode, 
353                                                           "*", "scale") || 1);
354   
355                     // set default width and height of icon
356                     var width = 32 * scale;
357                     var height = 32 * scale;
358
359                     var iconNode = this.getElementsByTagNameNS(styleTypeNode, 
360                                                "*", 
361                                                "Icon")[0];
362                     if (iconNode) {
363                         var href = this.parseProperty(iconNode, "*", "href");
364                         if (href) {                                                   
365
366                             var w = this.parseProperty(iconNode, "*", "w");
367                             var h = this.parseProperty(iconNode, "*", "h");
368
369                             // Settings for Google specific icons that are 64x64
370                             // We set the width and height to 64 and halve the
371                             // scale to prevent icons from being too big
372                             var google = "http://maps.google.com/mapfiles/kml";
373                             if (OpenLayers.String.startsWith(
374                                                  href, google) && !w && !h) {
375                                 w = 64;
376                                 h = 64;
377                                 scale = scale / 2;
378                             }
379                                 
380                             // if only dimension is defined, make sure the
381                             // other one has the same value
382                             w = w || h;
383                             h = h || w;
384
385                             if (w) {
386                                 width = parseInt(w) * scale;
387                             }
388
389                             if (h) {
390                                 height = parseInt(h) * scale;
391                             }
392
393                             // support for internal icons 
394                             //    (/root://icons/palette-x.png)
395                             // x and y tell the position on the palette:
396                             // - in pixels
397                             // - starting from the left bottom
398                             // We translate that to a position in the list 
399                             // and request the appropriate icon from the 
400                             // google maps website
401                             var matches = href.match(this.regExes.kmlIconPalette);
402                             if (matches)  {
403                                 var palette = matches[1];
404                                 var file_extension = matches[2];
405
406                                 var x = this.parseProperty(iconNode, "*", "x");
407                                 var y = this.parseProperty(iconNode, "*", "y");
408
409                                 var posX = x ? x/32 : 0;
410                                 var posY = y ? (7 - y/32) : 7;
411
412                                 var pos = posY * 8 + posX;
413                                 href = "http://maps.google.com/mapfiles/kml/pal" 
414                                      + palette + "/icon" + pos + file_extension;
415                             }
416
417                             style["graphicOpacity"] = 1; // fully opaque
418                             style["externalGraphic"] = href;
419                         }
420
421                     }
422
423
424                     // hotSpots define the offset for an Icon
425                     var hotSpotNode = this.getElementsByTagNameNS(styleTypeNode, 
426                                                "*", 
427                                                "hotSpot")[0];
428                     if (hotSpotNode) {
429                         var x = parseFloat(hotSpotNode.getAttribute("x"));
430                         var y = parseFloat(hotSpotNode.getAttribute("y"));
431
432                         var xUnits = hotSpotNode.getAttribute("xunits");
433                         if (xUnits == "pixels") {
434                             style["graphicXOffset"] = -x * scale;
435                         }
436                         else if (xUnits == "insetPixels") {
437                             style["graphicXOffset"] = -width + (x * scale);
438                         }
439                         else if (xUnits == "fraction") {
440                             style["graphicXOffset"] = -width * x;
441                         }
442
443                         var yUnits = hotSpotNode.getAttribute("yunits");
444                         if (yUnits == "pixels") {
445                             style["graphicYOffset"] = -height + (y * scale) + 1;
446                         }
447                         else if (yUnits == "insetPixels") {
448                             style["graphicYOffset"] = -(y * scale) + 1;
449                         }
450                         else if (yUnits == "fraction") {
451                             style["graphicYOffset"] =  -height * (1 - y) + 1;
452                         }
453                     }
454
455                     style["graphicWidth"] = width;
456                     style["graphicHeight"] = height;
457                     break;
458
459                 case "balloonstyle":
460                     var balloonStyle = OpenLayers.Util.getXmlNodeValue(
461                                             styleTypeNode);
462                     if (balloonStyle) {
463                         style["balloonStyle"] = balloonStyle.replace(
464                                        this.regExes.straightBracket, "${$1}");
465                     }
466                     break;
467                 default:
468             }
469         }
470
471         // Some polygons have no line color, so we use the fillColor for that
472         if (!style["strokeColor"] && style["fillColor"]) {
473             style["strokeColor"] = style["fillColor"];
474         }
475
476         var id = node.getAttribute("id");
477         if (id && style) {
478             style.id = id;
479         }
480
481         return style;
482     },
483
484     /**
485      * Method: parseStyleMaps
486      * Looks for <Style> nodes in the data and parses them
487      * Also parses <StyleMap> nodes, but only uses the 'normal' key
488      * 
489      * Parameters: 
490      * nodes    - {Array} of {DOMElement} data to read/parse.
491      * options  - {Object} Hash of options
492      * 
493      */
494     parseStyleMaps: function(nodes, options) {
495         // Only the default or "normal" part of the StyleMap is processed now
496         // To do the select or "highlight" bit, we'd need to change lots more
497
498         for(var i=0, len=nodes.length; i<len; i++) {
499             var node = nodes[i];
500             var pairs = this.getElementsByTagNameNS(node, "*", 
501                             "Pair");
502
503             var id = node.getAttribute("id");
504             for (var j=0, jlen=pairs.length; j<jlen; j++) {
505                 var pair = pairs[j];
506                 // Use the shortcut in the SLD format to quickly retrieve the 
507                 // value of a node. Maybe it's good to have a method in 
508                 // Format.XML to do this
509                 var key = this.parseProperty(pair, "*", "key");
510                 var styleUrl = this.parseProperty(pair, "*", "styleUrl");
511
512                 if (styleUrl && key == "normal") {
513                     this.styles[(options.styleBaseUrl || "") + "#" + id] =
514                         this.styles[(options.styleBaseUrl || "") + styleUrl];
515                 }
516
517                 if (styleUrl && key == "highlight") {
518                     // TODO: implement the "select" part
519                 }
520
521             }
522         }
523
524     },
525
526
527     /**
528      * Method: parseFeatures
529      * Loop through all Placemark nodes and parse them.
530      * Will create a list of features
531      * 
532      * Parameters: 
533      * nodes    - {Array} of {DOMElement} data to read/parse.
534      * options  - {Object} Hash of options
535      * 
536      */
537     parseFeatures: function(nodes, options) {
538         var features = new Array(nodes.length);
539         for(var i=0, len=nodes.length; i<len; i++) {
540             var featureNode = nodes[i];
541             var feature = this.parseFeature.apply(this,[featureNode]) ;
542             if(feature) {
543
544                 // Create reference to styleUrl 
545                 if (this.extractStyles && feature.attributes &&
546                     feature.attributes.styleUrl) {
547                     feature.style = this.getStyle(feature.attributes.styleUrl, options);
548                 }
549
550                 if (this.extractStyles) {
551                     // Make sure that <Style> nodes within a placemark are 
552                     // processed as well
553                     var inlineStyleNode = this.getElementsByTagNameNS(featureNode,
554                                                         "*",
555                                                         "Style")[0];
556                     if (inlineStyleNode) {
557                         var inlineStyle= this.parseStyle(inlineStyleNode);
558                         if (inlineStyle) {
559                             feature.style = OpenLayers.Util.extend(
560                                 feature.style, inlineStyle
561                             );
562                         }
563                     }
564                 }
565
566                 // add feature to list of features
567                 features[i] = feature;
568             } else {
569                 throw "Bad Placemark: " + i;
570             }
571         }
572
573         // add new features to existing feature list
574         this.features = this.features.concat(features);
575     },
576
577     /**
578      * Method: parseFeature
579      * This function is the core of the KML parsing code in OpenLayers.
580      *     It creates the geometries that are then attached to the returned
581      *     feature, and calls parseAttributes() to get attribute data out.
582      *
583      * Parameters:
584      * node - {DOMElement}
585      *
586      * Returns:
587      * {<OpenLayers.Feature.Vector>} A vector feature.
588      */
589     parseFeature: function(node) {
590         // only accept one geometry per feature - look for highest "order"
591         var order = ["MultiGeometry", "Polygon", "LineString", "Point"];
592         var type, nodeList, geometry, parser;
593         for(var i=0, len=order.length; i<len; ++i) {
594             type = order[i];
595             this.internalns = node.namespaceURI ? 
596                     node.namespaceURI : this.kmlns;
597             nodeList = this.getElementsByTagNameNS(node, 
598                                                    this.internalns, type);
599             if(nodeList.length > 0) {
600                 // only deal with first geometry of this type
601                 var parser = this.parseGeometry[type.toLowerCase()];
602                 if(parser) {
603                     geometry = parser.apply(this, [nodeList[0]]);
604                     if (this.internalProjection && this.externalProjection) {
605                         geometry.transform(this.externalProjection, 
606                                            this.internalProjection); 
607                     }                       
608                 } else {
609                     OpenLayers.Console.error(OpenLayers.i18n(
610                                 "unsupportedGeometryType", {'geomType':type}));
611                 }
612                 // stop looking for different geometry types
613                 break;
614             }
615         }
616
617         // construct feature (optionally with attributes)
618         var attributes;
619         if(this.extractAttributes) {
620             attributes = this.parseAttributes(node);
621         }
622         var feature = new OpenLayers.Feature.Vector(geometry, attributes);
623
624         var fid = node.getAttribute("id") || node.getAttribute("name");
625         if(fid != null) {
626             feature.fid = fid;
627         }
628
629         return feature;
630     },        
631     
632     /**
633      * Method: getStyle
634      * Retrieves a style from a style hash using styleUrl as the key
635      * If the styleUrl doesn't exist yet, we try to fetch it 
636      * Internet
637      * 
638      * Parameters: 
639      * styleUrl  - {String} URL of style
640      * options   - {Object} Hash of options 
641      *
642      * Returns:
643      * {Object}  - (reference to) Style hash
644      */
645     getStyle: function(styleUrl, options) {
646
647         var styleBaseUrl = OpenLayers.Util.removeTail(styleUrl);
648
649         var newOptions = OpenLayers.Util.extend({}, options);
650         newOptions.depth++;
651         newOptions.styleBaseUrl = styleBaseUrl;
652
653         // Fetch remote Style URLs (if not fetched before) 
654         if (!this.styles[styleUrl] 
655                 && !OpenLayers.String.startsWith(styleUrl, "#") 
656                 && newOptions.depth <= this.maxDepth
657                 && !this.fetched[styleBaseUrl] ) {
658
659             var data = this.fetchLink(styleBaseUrl);
660             if (data) {
661                 this.parseData(data, newOptions);
662             }
663
664         }
665
666         // return requested style
667         var style = OpenLayers.Util.extend({}, this.styles[styleUrl]);
668         return style;
669     },
670     
671     /**
672      * Property: parseGeometry
673      * Properties of this object are the functions that parse geometries based
674      *     on their type.
675      */
676     parseGeometry: {
677         
678         /**
679          * Method: parseGeometry.point
680          * Given a KML node representing a point geometry, create an OpenLayers
681          *     point geometry.
682          *
683          * Parameters:
684          * node - {DOMElement} A KML Point node.
685          *
686          * Returns:
687          * {<OpenLayers.Geometry.Point>} A point geometry.
688          */
689         point: function(node) {
690             var nodeList = this.getElementsByTagNameNS(node, this.internalns,
691                                                        "coordinates");
692             var coords = [];
693             if(nodeList.length > 0) {
694                 var coordString = nodeList[0].firstChild.nodeValue;
695                 coordString = coordString.replace(this.regExes.removeSpace, "");
696                 coords = coordString.split(",");
697             }
698
699             var point = null;
700             if(coords.length > 1) {
701                 // preserve third dimension
702                 if(coords.length == 2) {
703                     coords[2] = null;
704                 }
705                 point = new OpenLayers.Geometry.Point(coords[0], coords[1],
706                                                       coords[2]);
707             } else {
708                 throw "Bad coordinate string: " + coordString;
709             }
710             return point;
711         },
712         
713         /**
714          * Method: parseGeometry.linestring
715          * Given a KML node representing a linestring geometry, create an
716          *     OpenLayers linestring geometry.
717          *
718          * Parameters:
719          * node - {DOMElement} A KML LineString node.
720          *
721          * Returns:
722          * {<OpenLayers.Geometry.LineString>} A linestring geometry.
723          */
724         linestring: function(node, ring) {
725             var nodeList = this.getElementsByTagNameNS(node, this.internalns,
726                                                        "coordinates");
727             var line = null;
728             if(nodeList.length > 0) {
729                 var coordString = this.getChildValue(nodeList[0]);
730
731                 coordString = coordString.replace(this.regExes.trimSpace,
732                                                   "");
733                 coordString = coordString.replace(this.regExes.trimComma,
734                                                   ",");
735                 var pointList = coordString.split(this.regExes.splitSpace);
736                 var numPoints = pointList.length;
737                 var points = new Array(numPoints);
738                 var coords, numCoords;
739                 for(var i=0; i<numPoints; ++i) {
740                     coords = pointList[i].split(",");
741                     numCoords = coords.length;
742                     if(numCoords > 1) {
743                         if(coords.length == 2) {
744                             coords[2] = null;
745                         }
746                         points[i] = new OpenLayers.Geometry.Point(coords[0],
747                                                                   coords[1],
748                                                                   coords[2]);
749                     } else {
750                         throw "Bad LineString point coordinates: " +
751                               pointList[i];
752                     }
753                 }
754                 if(numPoints) {
755                     if(ring) {
756                         line = new OpenLayers.Geometry.LinearRing(points);
757                     } else {
758                         line = new OpenLayers.Geometry.LineString(points);
759                     }
760                 } else {
761                     throw "Bad LineString coordinates: " + coordString;
762                 }
763             }
764
765             return line;
766         },
767         
768         /**
769          * Method: parseGeometry.polygon
770          * Given a KML node representing a polygon geometry, create an
771          *     OpenLayers polygon geometry.
772          *
773          * Parameters:
774          * node - {DOMElement} A KML Polygon node.
775          *
776          * Returns:
777          * {<OpenLayers.Geometry.Polygon>} A polygon geometry.
778          */
779         polygon: function(node) {
780             var nodeList = this.getElementsByTagNameNS(node, this.internalns,
781                                                        "LinearRing");
782             var numRings = nodeList.length;
783             var components = new Array(numRings);
784             if(numRings > 0) {
785                 // this assumes exterior ring first, inner rings after
786                 var ring;
787                 for(var i=0, len=nodeList.length; i<len; ++i) {
788                     ring = this.parseGeometry.linestring.apply(this,
789                                                         [nodeList[i], true]);
790                     if(ring) {
791                         components[i] = ring;
792                     } else {
793                         throw "Bad LinearRing geometry: " + i;
794                     }
795                 }
796             }
797             return new OpenLayers.Geometry.Polygon(components);
798         },
799         
800         /**
801          * Method: parseGeometry.multigeometry
802          * Given a KML node representing a multigeometry, create an
803          *     OpenLayers geometry collection.
804          *
805          * Parameters:
806          * node - {DOMElement} A KML MultiGeometry node.
807          *
808          * Returns:
809          * {<OpenLayers.Geometry.Collection>} A geometry collection.
810          */
811         multigeometry: function(node) {
812             var child, parser;
813             var parts = [];
814             var children = node.childNodes;
815             for(var i=0, len=children.length; i<len; ++i ) {
816                 child = children[i];
817                 if(child.nodeType == 1) {
818                     var type = (child.prefix) ?
819                             child.nodeName.split(":")[1] :
820                             child.nodeName;
821                     var parser = this.parseGeometry[type.toLowerCase()];
822                     if(parser) {
823                         parts.push(parser.apply(this, [child]));
824                     }
825                 }
826             }
827             return new OpenLayers.Geometry.Collection(parts);
828         }
829         
830     },
831
832     /**
833      * Method: parseAttributes
834      *
835      * Parameters:
836      * node - {DOMElement}
837      *
838      * Returns:
839      * {Object} An attributes object.
840      */
841     parseAttributes: function(node) {
842         var attributes = {};
843        
844         // Extended Data is parsed first.
845         var edNodes = node.getElementsByTagName("ExtendedData");
846         if (edNodes.length) {
847             attributes = this.parseExtendedData(edNodes[0]);
848         }
849         
850         // assume attribute nodes are type 1 children with a type 3 or 4 child
851         var child, grandchildren, grandchild;
852         var children = node.childNodes;
853
854         for(var i=0, len=children.length; i<len; ++i) {
855             child = children[i];
856             if(child.nodeType == 1) {
857                 grandchildren = child.childNodes;
858                 if(grandchildren.length == 1 || grandchildren.length == 3) {
859                     var grandchild;
860                     switch (grandchildren.length) {
861                         case 1:
862                             grandchild = grandchildren[0];
863                             break;
864                         case 3:
865                         default:
866                             grandchild = grandchildren[1];
867                             break;
868                     }
869                     if(grandchild.nodeType == 3 || grandchild.nodeType == 4) {
870                         var name = (child.prefix) ?
871                                 child.nodeName.split(":")[1] :
872                                 child.nodeName;
873                         var value = OpenLayers.Util.getXmlNodeValue(grandchild);
874                         if (value) {
875                             value = value.replace(this.regExes.trimSpace, "");
876                             attributes[name] = value;
877                         }
878                     }
879                 } 
880             }
881         }
882         return attributes;
883     },
884
885     /**
886      * Method: parseExtendedData
887      * Parse ExtendedData from KML. No support for schemas/datatypes.
888      *     See http://code.google.com/apis/kml/documentation/kmlreference.html#extendeddata
889      *     for more information on extendeddata.
890      */
891     parseExtendedData: function(node) {
892         var attributes = {};
893         var dataNodes = node.getElementsByTagName("Data");
894         for (var i = 0, len = dataNodes.length; i < len; i++) {
895             var data = dataNodes[i];
896             var key = data.getAttribute("name");
897             var ed = {};
898             var valueNode = data.getElementsByTagName("value");
899             if (valueNode.length) {
900                 ed['value'] = this.getChildValue(valueNode[0]);
901             }    
902             var nameNode = data.getElementsByTagName("displayName");
903             if (nameNode.length) {
904                 ed['displayName'] = this.getChildValue(nameNode[0]);
905             }
906             attributes[key] = ed;
907         }
908         return attributes;    
909     },
910     
911     /**
912      * Method: parseProperty
913      * Convenience method to find a node and return its value
914      *
915      * Parameters:
916      * xmlNode    - {<DOMElement>}
917      * namespace  - {String} namespace of the node to find
918      * tagName    - {String} name of the property to parse
919      * 
920      * Returns:
921      * {String} The value for the requested property (defaults to null)
922      */    
923     parseProperty: function(xmlNode, namespace, tagName) {
924         var value;
925         var nodeList = this.getElementsByTagNameNS(xmlNode, namespace, tagName);
926         try {
927             value = OpenLayers.Util.getXmlNodeValue(nodeList[0]);
928         } catch(e) {
929             value = null;
930         }
931      
932         return value;
933     },                                                              
934
935     /**
936      * APIMethod: write
937      * Accept Feature Collection, and return a string. 
938      * 
939      * Parameters:
940      * features - {Array(<OpenLayers.Feature.Vector>} An array of features.
941      *
942      * Returns:
943      * {String} A KML string.
944      */
945     write: function(features) {
946         if(!(features instanceof Array)) {
947             features = [features];
948         }
949         var kml = this.createElementNS(this.kmlns, "kml");
950         var folder = this.createFolderXML();
951         for(var i=0, len=features.length; i<len; ++i) {
952             folder.appendChild(this.createPlacemarkXML(features[i]));
953         }
954         kml.appendChild(folder);
955         return OpenLayers.Format.XML.prototype.write.apply(this, [kml]);
956     },
957
958     /**
959      * Method: createFolderXML
960      * Creates and returns a KML folder node
961      * 
962      * Returns:
963      * {DOMElement}
964      */
965     createFolderXML: function() {
966         // Folder name
967         var folderName = this.createElementNS(this.kmlns, "name");
968         var folderNameText = this.createTextNode(this.foldersName); 
969         folderName.appendChild(folderNameText);
970
971         // Folder description
972         var folderDesc = this.createElementNS(this.kmlns, "description");        
973         var folderDescText = this.createTextNode(this.foldersDesc); 
974         folderDesc.appendChild(folderDescText);
975
976         // Folder
977         var folder = this.createElementNS(this.kmlns, "Folder");
978         folder.appendChild(folderName);
979         folder.appendChild(folderDesc);
980         
981         return folder;
982     },
983
984     /**
985      * Method: createPlacemarkXML
986      * Creates and returns a KML placemark node representing the given feature. 
987      * 
988      * Parameters:
989      * feature - {<OpenLayers.Feature.Vector>}
990      * 
991      * Returns:
992      * {DOMElement}
993      */
994     createPlacemarkXML: function(feature) {        
995         // Placemark name
996         var placemarkName = this.createElementNS(this.kmlns, "name");
997         var name = (feature.attributes.name) ?
998                     feature.attributes.name : feature.id;
999         placemarkName.appendChild(this.createTextNode(name));
1000
1001         // Placemark description
1002         var placemarkDesc = this.createElementNS(this.kmlns, "description");
1003         var desc = (feature.attributes.description) ?
1004                     feature.attributes.description : this.placemarksDesc;
1005         placemarkDesc.appendChild(this.createTextNode(desc));
1006         
1007         // Placemark
1008         var placemarkNode = this.createElementNS(this.kmlns, "Placemark");
1009         if(feature.fid != null) {
1010             placemarkNode.setAttribute("id", feature.fid);
1011         }
1012         placemarkNode.appendChild(placemarkName);
1013         placemarkNode.appendChild(placemarkDesc);
1014
1015         // Geometry node (Point, LineString, etc. nodes)
1016         var geometryNode = this.buildGeometryNode(feature.geometry);
1017         placemarkNode.appendChild(geometryNode);        
1018         
1019         // TBD - deal with remaining (non name/description) attributes.
1020         return placemarkNode;
1021     },    
1022
1023     /**
1024      * Method: buildGeometryNode
1025      * Builds and returns a KML geometry node with the given geometry.
1026      * 
1027      * Parameters:
1028      * geometry - {<OpenLayers.Geometry>}
1029      * 
1030      * Returns:
1031      * {DOMElement}
1032      */
1033     buildGeometryNode: function(geometry) {
1034         if (this.internalProjection && this.externalProjection) {
1035             geometry = geometry.clone();
1036             geometry.transform(this.internalProjection, 
1037                                this.externalProjection);
1038         }                       
1039         var className = geometry.CLASS_NAME;
1040         var type = className.substring(className.lastIndexOf(".") + 1);
1041         var builder = this.buildGeometry[type.toLowerCase()];
1042         var node = null;
1043         if(builder) {
1044             node = builder.apply(this, [geometry]);
1045         }
1046         return node;
1047     },
1048
1049     /**
1050      * Property: buildGeometry
1051      * Object containing methods to do the actual geometry node building
1052      *     based on geometry type.
1053      */
1054     buildGeometry: {
1055         // TBD: Anybody care about namespace aliases here (these nodes have
1056         //    no prefixes)?
1057
1058         /**
1059          * Method: buildGeometry.point
1060          * Given an OpenLayers point geometry, create a KML point.
1061          *
1062          * Parameters:
1063          * geometry - {<OpenLayers.Geometry.Point>} A point geometry.
1064          *
1065          * Returns:
1066          * {DOMElement} A KML point node.
1067          */
1068         point: function(geometry) {
1069             var kml = this.createElementNS(this.kmlns, "Point");
1070             kml.appendChild(this.buildCoordinatesNode(geometry));
1071             return kml;
1072         },
1073         
1074         /**
1075          * Method: buildGeometry.multipoint
1076          * Given an OpenLayers multipoint geometry, create a KML
1077          *     GeometryCollection.
1078          *
1079          * Parameters:
1080          * geometry - {<OpenLayers.Geometry.Point>} A multipoint geometry.
1081          *
1082          * Returns:
1083          * {DOMElement} A KML GeometryCollection node.
1084          */
1085         multipoint: function(geometry) {
1086             return this.buildGeometry.collection.apply(this, [geometry]);
1087         },
1088
1089         /**
1090          * Method: buildGeometry.linestring
1091          * Given an OpenLayers linestring geometry, create a KML linestring.
1092          *
1093          * Parameters:
1094          * geometry - {<OpenLayers.Geometry.LineString>} A linestring geometry.
1095          *
1096          * Returns:
1097          * {DOMElement} A KML linestring node.
1098          */
1099         linestring: function(geometry) {
1100             var kml = this.createElementNS(this.kmlns, "LineString");
1101             kml.appendChild(this.buildCoordinatesNode(geometry));
1102             return kml;
1103         },
1104         
1105         /**
1106          * Method: buildGeometry.multilinestring
1107          * Given an OpenLayers multilinestring geometry, create a KML
1108          *     GeometryCollection.
1109          *
1110          * Parameters:
1111          * geometry - {<OpenLayers.Geometry.Point>} A multilinestring geometry.
1112          *
1113          * Returns:
1114          * {DOMElement} A KML GeometryCollection node.
1115          */
1116         multilinestring: function(geometry) {
1117             return this.buildGeometry.collection.apply(this, [geometry]);
1118         },
1119
1120         /**
1121          * Method: buildGeometry.linearring
1122          * Given an OpenLayers linearring geometry, create a KML linearring.
1123          *
1124          * Parameters:
1125          * geometry - {<OpenLayers.Geometry.LinearRing>} A linearring geometry.
1126          *
1127          * Returns:
1128          * {DOMElement} A KML linearring node.
1129          */
1130         linearring: function(geometry) {
1131             var kml = this.createElementNS(this.kmlns, "LinearRing");
1132             kml.appendChild(this.buildCoordinatesNode(geometry));
1133             return kml;
1134         },
1135         
1136         /**
1137          * Method: buildGeometry.polygon
1138          * Given an OpenLayers polygon geometry, create a KML polygon.
1139          *
1140          * Parameters:
1141          * geometry - {<OpenLayers.Geometry.Polygon>} A polygon geometry.
1142          *
1143          * Returns:
1144          * {DOMElement} A KML polygon node.
1145          */
1146         polygon: function(geometry) {
1147             var kml = this.createElementNS(this.kmlns, "Polygon");
1148             var rings = geometry.components;
1149             var ringMember, ringGeom, type;
1150             for(var i=0, len=rings.length; i<len; ++i) {
1151                 type = (i==0) ? "outerBoundaryIs" : "innerBoundaryIs";
1152                 ringMember = this.createElementNS(this.kmlns, type);
1153                 ringGeom = this.buildGeometry.linearring.apply(this,
1154                                                                [rings[i]]);
1155                 ringMember.appendChild(ringGeom);
1156                 kml.appendChild(ringMember);
1157             }
1158             return kml;
1159         },
1160         
1161         /**
1162          * Method: buildGeometry.multipolygon
1163          * Given an OpenLayers multipolygon geometry, create a KML
1164          *     GeometryCollection.
1165          *
1166          * Parameters:
1167          * geometry - {<OpenLayers.Geometry.Point>} A multipolygon geometry.
1168          *
1169          * Returns:
1170          * {DOMElement} A KML GeometryCollection node.
1171          */
1172         multipolygon: function(geometry) {
1173             return this.buildGeometry.collection.apply(this, [geometry]);
1174         },
1175
1176         /**
1177          * Method: buildGeometry.collection
1178          * Given an OpenLayers geometry collection, create a KML MultiGeometry.
1179          *
1180          * Parameters:
1181          * geometry - {<OpenLayers.Geometry.Collection>} A geometry collection.
1182          *
1183          * Returns:
1184          * {DOMElement} A KML MultiGeometry node.
1185          */
1186         collection: function(geometry) {
1187             var kml = this.createElementNS(this.kmlns, "MultiGeometry");
1188             var child;
1189             for(var i=0, len=geometry.components.length; i<len; ++i) {
1190                 child = this.buildGeometryNode.apply(this,
1191                                                      [geometry.components[i]]);
1192                 if(child) {
1193                     kml.appendChild(child);
1194                 }
1195             }
1196             return kml;
1197         }
1198     },
1199
1200     /**
1201      * Method: buildCoordinatesNode
1202      * Builds and returns the KML coordinates node with the given geometry
1203      * <coordinates>...</coordinates>
1204      * 
1205      * Parameters:
1206      * geometry - {<OpenLayers.Geometry>}
1207      * 
1208      * Return:
1209      * {DOMElement}
1210      */     
1211     buildCoordinatesNode: function(geometry) {
1212         var coordinatesNode = this.createElementNS(this.kmlns, "coordinates");
1213         
1214         var path;
1215         var points = geometry.components;
1216         if(points) {
1217             // LineString or LinearRing
1218             var point;
1219             var numPoints = points.length;
1220             var parts = new Array(numPoints);
1221             for(var i=0; i<numPoints; ++i) {
1222                 point = points[i];
1223                 parts[i] = point.x + "," + point.y;
1224             }
1225             path = parts.join(" ");
1226         } else {
1227             // Point
1228             path = geometry.x + "," + geometry.y;
1229         }
1230         
1231         var txtNode = this.createTextNode(path);
1232         coordinatesNode.appendChild(txtNode);
1233         
1234         return coordinatesNode;
1235     },    
1236
1237     CLASS_NAME: "OpenLayers.Format.KML" 
1238 });