OpenLayers OpenLayers

Ticket #1259: kml_styling_v3.diff

File kml_styling_v3.diff, 22.2 kB (added by rdewit, 1 year ago)

Newer version: Changes based on summary ahocevar. Tests pass in FF2/3b2,IE6 and Opera 9.5b1

  • lib/OpenLayers/Format/KML.js

    old new  
    4848    /** 
    4949     * APIProperty: extractAttributes 
    5050     * {Boolean} Extract attributes from KML.  Default is true. 
     51     *           Extracting styleUrls requires this to be set to true 
    5152     */ 
    5253    extractAttributes: true, 
    5354     
     
    5960    internalns: null, 
    6061 
    6162    /** 
     63     * Property: features 
     64     * {Array} Array of features 
     65     *      
     66     */ 
     67    features: null, 
     68 
     69    /** 
     70     * Property: styles 
     71     * {Object} Storage of style objects 
     72     *      
     73     */ 
     74    styles: null, 
     75 
     76    /** 
     77     * Property: fetched 
     78     * {Object} Storage of KML URLs that have been fetched before 
     79     *     in order to prevent reloading them. 
     80     */ 
     81    fetched: null, 
     82 
     83    /** 
     84     * APIProperty: maxDepth 
     85     * {Integer} Maximum depth for recursive loading external KML URLs  
     86     *           Defaults to 0: do no external fetching 
     87     */ 
     88    maxDepth: 0, 
     89 
     90    /** 
    6291     * Constructor: OpenLayers.Format.KML 
    6392     * Create a new parser for KML. 
    6493     * 
     
    72101            trimSpace: (/^\s*|\s*$/g), 
    73102            removeSpace: (/\s*/g), 
    74103            splitSpace: (/\s+/), 
    75             trimComma: (/\s*,\s*/g) 
     104            trimComma: (/\s*,\s*/g), 
     105            kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/), 
     106            kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/), 
     107            straightBracket: (/\$\[(.*?)\]/g) 
    76108        }; 
    77109        OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); 
    78110    }, 
     
    88120     * {Array(<OpenLayers.Feature.Vector>)} List of features. 
    89121     */ 
    90122    read: function(data) { 
     123        this.features = this.features || []; 
     124        this.styles   = this.styles   || {}; 
     125        this.fetched  = this.fetched  || {}; 
     126        return this.parseData(data); 
     127    }, 
     128 
     129    /** 
     130     * Method: parseData 
     131     * Read data from a string, and return a list of features.  
     132     *  
     133     * Parameters:  
     134     * data    - {String} or {DOMElement} data to read/parse. 
     135     * options - {Object} Hash of options 
     136     * 
     137     * Returns: 
     138     * {Array(<OpenLayers.Feature.Vector>)} List of features. 
     139     */ 
     140    parseData: function(data, options) { 
    91141        if(typeof data == "string") { 
    92142            data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); 
    93143        } 
    94         var featureNodes = this.getElementsByTagNameNS(data, 
    95                                                        '*', 
    96                                                        "Placemark"); 
    97         var numFeatures = featureNodes.length; 
    98         var features = new Array(numFeatures); 
    99         for(var i=0; i<numFeatures; i++) { 
    100             var feature = this.parseFeature(featureNodes[i]); 
     144         
     145        // create default options hash if none provided 
     146        if (!options) { 
     147            options = {}; 
     148        } 
     149 
     150        // Set default options  
     151        OpenLayers.Util.applyDefaults(options, { 
     152            depth: 0, 
     153            styleBaseUrl: "" 
     154        }); 
     155 
     156        // Loop throught the following node types in this order and 
     157        // process the nodes found  
     158        var types = ["Link", "NetworkLink", "Style", "StyleMap", "Placemark"]; 
     159        for(var i=0; i<types.length; ++i) { 
     160            var type = types[i]; 
     161 
     162            var nodes = this.getElementsByTagNameNS(data, "*", type); 
     163 
     164            // skip to next type if no nodes are found 
     165            if(nodes.length == 0) {  
     166                continue; 
     167            } 
     168 
     169            switch (type.toLowerCase()) { 
     170 
     171                // Fetch external links  
     172                case "link": 
     173                case "networklink": 
     174                    this.parseLinks(nodes, options); 
     175                    break; 
     176 
     177                // parse style information 
     178                case "style": 
     179                    this.parseStyles(nodes, options); 
     180                    break; 
     181                case "stylemap": 
     182                    this.parseStyleMaps(nodes, options); 
     183                    break; 
     184 
     185                // parse features 
     186                case "placemark": 
     187                    this.parseFeatures(nodes, options); 
     188                    break; 
     189            } 
     190        } 
     191         
     192        return this.features; 
     193    }, 
     194 
     195    /** 
     196     * Method: parseLinks 
     197     * Finds URLs of linked KML documents and fetches them 
     198     *  
     199     * Parameters:  
     200     * nodes   - {Array} of {DOMElement} data to read/parse. 
     201     * options - {Object} Hash of options 
     202     *  
     203     */ 
     204    parseLinks: function(nodes, options) { 
     205         
     206        // Fetch external links <NetworkLink> and <Link> 
     207        // Don't do anything if we have reached our maximum depth for recursion 
     208        if (options.depth >= this.maxDepth) { 
     209            return false; 
     210        } 
     211 
     212        // increase depth 
     213        var newOptions = OpenLayers.Util.extend({}, options); 
     214        newOptions.depth++; 
     215 
     216        for(var i=0; i < nodes.length; i++) { 
     217            var href = this.parseProperty(nodes[i], "*", "href"); 
     218            if(href && !this.fetched[href]) { 
     219                this.fetched[href] = true; // prevent reloading the same urls 
     220                var data = this.fetchLink(href); 
     221                if (data) { 
     222                    this.parseData(data, newOptions); 
     223                } 
     224            }  
     225        } 
     226 
     227    }, 
     228 
     229    /** 
     230     * Method: fetchLink 
     231     * Fetches a URL and returns the result 
     232     *  
     233     * Parameters:  
     234     * href  - {String} url to be fetched 
     235     *  
     236     */ 
     237    fetchLink: function(href) { 
     238 
     239        if (OpenLayers.ProxyHost  
     240                && OpenLayers.String.startsWith(href, "http")) { 
     241            href = OpenLayers.ProxyHost + escape(href); 
     242        } 
     243 
     244        var request = new OpenLayers.Ajax.Request(href,  
     245                      {method: 'get', asynchronous: false }); 
     246 
     247        if (request && request.transport) { 
     248            return request.transport.responseText; 
     249        } 
     250 
     251    }, 
     252 
     253    /** 
     254     * Method: parseStyles 
     255     * Looks for <Style> nodes in the data and parses them 
     256     * Also parses <StyleMap> nodes, but only uses the 'normal' key 
     257     *  
     258     * Parameters:  
     259     * nodes    - {Array} of {DOMElement} data to read/parse. 
     260     * options  - {Object} Hash of options 
     261     *  
     262     */ 
     263    parseStyles: function(nodes, options) { 
     264        for(var i=0; i < nodes.length; i++) { 
     265            var style = this.parseStyle(nodes[i]); 
     266            if(style) { 
     267                styleName = (options.styleBaseUrl || "") + "#" + style.id; 
     268                 
     269                this.styles[styleName] = style; 
     270            } 
     271        } 
     272    }, 
     273 
     274    /** 
     275     * Method: parseStyle 
     276     * Parses the children of a <Style> node and builds the style hash 
     277     * accordingly 
     278     *  
     279     * Parameters:  
     280     * node - {DOMElement} <Style> node 
     281     *  
     282     */ 
     283    parseStyle: function(node) { 
     284        var style = {}; 
     285         
     286        var types = ["LineStyle", "PolyStyle", "IconStyle", "BalloonStyle"]; 
     287        var type, nodeList, geometry, parser; 
     288        for(var i=0; i<types.length; ++i) { 
     289            type = types[i]; 
     290            styleTypeNode = this.getElementsByTagNameNS(node,  
     291                                                   "*", type)[0]; 
     292            if(!styleTypeNode) {  
     293                continue; 
     294            } 
     295 
     296            // only deal with first geometry of this type 
     297            switch (type.toLowerCase()) { 
     298                case "linestyle": 
     299                    var color = this.parseProperty(styleTypeNode, "*", "color"); 
     300                    if (color) { 
     301                        var matches = (color.toString()).match( 
     302                                                         this.regExes.kmlColor); 
     303 
     304                        // transparency 
     305                        var alpha = matches[1]; 
     306                        style["strokeOpacity"] = parseInt(alpha, 16) / 255; 
     307 
     308                        // rgb colors (google uses bgr) 
     309                        var b = matches[2];  
     310                        var g = matches[3];  
     311                        var r = matches[4];  
     312                        style["strokeColor"] = "#" + r + g + b; 
     313                    } 
     314                     
     315                    var width = this.parseProperty(styleTypeNode, "*", "width"); 
     316                    if (width) { 
     317                        style["strokeWidth"] = width; 
     318                    } 
     319 
     320                case "polystyle": 
     321                    var color = this.parseProperty(styleTypeNode, "*", "color"); 
     322                    if (color) { 
     323                        var matches = (color.toString()).match( 
     324                                                         this.regExes.kmlColor); 
     325 
     326                        // transparency 
     327                        var alpha = matches[1]; 
     328                        style["fillOpacity"] = parseInt(alpha, 16) / 255; 
     329 
     330                        // rgb colors (google uses bgr) 
     331                        var b = matches[2];  
     332                        var g = matches[3];  
     333                        var r = matches[4];  
     334                        style["fillColor"] = "#" + r + g + b; 
     335                    } 
     336                     
     337                    break; 
     338                case "iconstyle": 
     339                    var iconNode = this.getElementsByTagNameNS(styleTypeNode,  
     340                                               "*",  
     341                                               "Icon")[0]; 
     342 
     343                    // set default width and height of icon 
     344                    style["graphicWidth"] = 32; 
     345                    style["graphicHeight"] = 32; 
     346 
     347                    if (iconNode) { 
     348                        var href = this.parseProperty(iconNode, "*", "href"); 
     349                        if (href) {                                                    
     350 
     351                            // support for internal icons  
     352                            //    (/root://icons/palette-x.png) 
     353                            // x and y tell the position on the palette: 
     354                            // - in pixels 
     355                            // - starting from the left bottom 
     356                            // We translate that to a position in the list  
     357                            // and request the appropriate icon from the  
     358                            // google maps website 
     359                            var matches = href.match(this.regExes.kmlIconPalette); 
     360                            if (matches)  { 
     361                                var palette = matches[1]; 
     362                                var file_extension = matches[2]; 
     363 
     364                                var x = this.parseProperty(iconNode, "*", "x"); 
     365                                var y = this.parseProperty(iconNode, "*", "y"); 
     366 
     367                                var posX = x ? x/32 : 0; 
     368                                var posY = y ? (7 - y/32) : 7; 
     369 
     370                                var pos = posY * 8 + posX; 
     371                                href = "http://maps.google.com/mapfiles/kml/pal"  
     372                                     + palette + "/icon" + pos + file_extension; 
     373                            } 
     374 
     375 
     376                            var w = this.parseProperty(iconNode, "*", "w"); 
     377                            if (w) { 
     378                                style["graphicWidth"] = parseInt(w); 
     379                            } 
     380 
     381                            var h = this.parseProperty(iconNode, "*", "h"); 
     382                            if (h) { 
     383                                style["graphicHeight"] = parseInt(h); 
     384                            } 
     385 
     386                            style["graphicOpacity"] = 1; // fully opaque 
     387                            style["externalGraphic"] = href; 
     388                        } 
     389 
     390                    } 
     391 
     392 
     393                    // hotSpots define the offset for an Icon 
     394                    var hotSpotNode = this.getElementsByTagNameNS(styleTypeNode,  
     395                                               "*",  
     396                                               "hotSpot")[0]; 
     397                    if (hotSpotNode) { 
     398                        var x = hotSpotNode.getAttribute("x"); 
     399                        var y = hotSpotNode.getAttribute("y"); 
     400 
     401                        var xUnits = hotSpotNode.getAttribute("xunits"); 
     402                        if (xUnits == "pixels") { 
     403                            style["graphicXOffset"] = parseInt(x); 
     404                        } 
     405                        else if (xUnits == "insetPixels") { 
     406                            style["graphicXOffset"] = style["graphicWidth"]  
     407                                                          - parseInt(x); 
     408                        } 
     409                        else if (xUnits == "fraction") { 
     410                            style["graphicXOffset"] = style["graphicWidth"]  
     411                                                          * parseFloat(x); 
     412                        } 
     413 
     414                        var yUnits = hotSpotNode.getAttribute("yunits"); 
     415                        if (yUnits == "pixels") { 
     416                            style["graphicYOffset"] = parseInt(y); 
     417                        } 
     418                        else if (yUnits == "insetPixels") { 
     419                            style["graphicYOffset"] = style["graphicHeight"]  
     420                                                          - parseInt(y); 
     421                        } 
     422                        else if (yUnits == "fraction") { 
     423                            style["graphicYOffset"] = style["graphicHeight"]  
     424                                                          * parseFloat(y); 
     425                        } 
     426                    } 
     427                    break; 
     428                case "balloonstyle": 
     429                    var balloonStyle = OpenLayers.Util.getXmlNodeValue( 
     430                                            styleTypeNode); 
     431                    if (balloonStyle) { 
     432                        style["balloonStyle"] = balloonStyle.replace( 
     433                                       this.regExes.straightBracket, "${$1}"); 
     434                    } 
     435                    break; 
     436                default: 
     437            } 
     438        } 
     439 
     440        // Some polygons have no line color, so we use the fillColor for that 
     441        if (!style["strokeColor"] && style["fillColor"]) { 
     442            style["strokeColor"] = style["fillColor"]; 
     443        } 
     444 
     445        var id = node.getAttribute("id"); 
     446        if (id && style) { 
     447            style.id = id; 
     448        } 
     449 
     450        // Apply default styles 
     451        OpenLayers.Util.applyDefaults(style,  
     452                           OpenLayers.Feature.Vector.style["default"]); 
     453                  
     454        return style; 
     455    }, 
     456 
     457    /** 
     458     * Method: parseStyleMaps 
     459     * Looks for <Style> nodes in the data and parses them 
     460     * Also parses <StyleMap> nodes, but only uses the 'normal' key 
     461     *  
     462     * Parameters:  
     463     * nodes    - {Array} of {DOMElement} data to read/parse. 
     464     * options  - {Object} Hash of options 
     465     *  
     466     */ 
     467    parseStyleMaps: function(nodes, options) { 
     468        // Only the default or "normal" part of the StyleMap is processed now 
     469        // To do the select or "highlight" bit, we'd need to change lots more 
     470 
     471        for(var i=0; i < nodes.length; i++) { 
     472            var node = nodes[i]; 
     473            var pairs = this.getElementsByTagNameNS(node, "*",  
     474                            "Pair"); 
     475 
     476            var id = node.getAttribute("id"); 
     477            for (var j=0; j<pairs.length; j++) { 
     478                var pair = pairs[j]; 
     479                // Use the shortcut in the SLD format to quickly retrieve the  
     480                // value of a node. Maybe it's good to have a method in  
     481                // Format.XML to do this 
     482                var key = this.parseProperty(pair, "*", "key"); 
     483                var styleUrl = this.parseProperty(pair, "*", "styleUrl"); 
     484 
     485                if (styleUrl && key == "normal") { 
     486                    this.styles[(options.styleBaseUrl || "") + "#" + id] = 
     487                        this.styles[(options.styleBaseUrl || "") + styleUrl]; 
     488                } 
     489 
     490                if (styleUrl && key == "highlight") { 
     491                    // TODO: implement the "select" part 
     492                } 
     493 
     494            } 
     495        } 
     496 
     497    }, 
     498 
     499 
     500    /** 
     501     * Method: parseFeatures 
     502     * Loop through all Placemark nodes and parse them. 
     503     * Will create a list of features 
     504     *  
     505     * Parameters:  
     506     * nodes    - {Array} of {DOMElement} data to read/parse. 
     507     * options  - {Object} Hash of options 
     508     *  
     509     */ 
     510    parseFeatures: function(nodes, options) { 
     511        var features = new Array(nodes.length); 
     512        for(var i=0; i < nodes.length; i++) { 
     513            var featureNode = nodes[i]; 
     514            var feature = this.parseFeature.apply(this,[featureNode]) ; 
    101515            if(feature) { 
     516 
     517                // Create reference to styleUrl  
     518                if (feature.attributes && feature.attributes.styleUrl) { 
     519                    feature.style = this.getStyle(feature.attributes.styleUrl); 
     520                } 
     521 
     522                // Make sure that <Style> nodes within a placemark are  
     523                // processed as well 
     524                var inlineStyleNode = this.getElementsByTagNameNS(featureNode, 
     525                                                       "*", 
     526                                                       "Style")[0]; 
     527                if (inlineStyleNode) { 
     528                    var inlineStyle= this.parseStyle(styleNode); 
     529                    if (inlineStyle) { 
     530                        feature.style = OpenLayers.Util.extend({},  
     531                                            feature.style); 
     532                        OpenLayers.Util.extend(feature.style, inlineStyle); 
     533                    } 
     534                } 
     535 
     536                // add feature to list of features 
    102537                features[i] = feature; 
    103538            } else { 
    104539                throw "Bad Placemark: " + i; 
    105540            } 
    106541        } 
    107         return features; 
     542 
     543        // add new features to existing feature list 
     544        this.features = this.features.concat(features); 
    108545    }, 
    109546 
    110547    /** 
     
    154591        } 
    155592        var feature = new OpenLayers.Feature.Vector(geometry, attributes); 
    156593 
    157         var fid = node.getAttribute("id")
     594        var fid = node.getAttribute("id") || node.getAttribute("name")
    158595        if(fid != null) { 
    159596            feature.fid = fid; 
    160597        } 
     
    163600    },         
    164601     
    165602    /** 
     603     * Method: getStyle 
     604     * Retrieves a style from a style hash using styleUrl as the key 
     605     * If the styleUrl doesn't exist yet, we try to fetch it  
     606     * Internet 
     607     *  
     608     * Parameters:  
     609     * styleUrl  - {String} URL of style 
     610     * options   - {Object} Hash of options  
     611     * 
     612     * Returns: 
     613     * {Object}  - (reference to) Style hash 
     614     */ 
     615    getStyle: function(styleUrl, options) { 
     616 
     617        var styleBaseUrl = OpenLayers.Util.removeTail(styleUrl); 
     618 
     619        var newOptions = OpenLayers.Util.extend({}, options); 
     620        newOptions.depth++; 
     621        newOptions.styleBaseUrl = styleBaseUrl; 
     622 
     623        // Fetch remote Style URLs (if not fetched before)  
     624        if (!this.styles[styleUrl]  
     625                && !OpenLayers.String.startsWith(styleUrl, "#")  
     626                && newOptions.depth <= this.maxDepth 
     627                && !this.fetched[styleBaseUrl] ) { 
     628 
     629            var data = this.fetchLink(styleBaseUrl); 
     630            if (data) { 
     631                this.parseData(data, newOptions); 
     632            } 
     633 
     634        } 
     635 
     636        // return requested style 
     637        var style = this.styles[styleUrl]; 
     638        return style; 
     639    }, 
     640     
     641    /** 
    166642     * Property: parseGeometry 
    167643     * Properties of this object are the functions that parse geometries based 
    168644     *     on their type. 
     
    334810     */ 
    335811    parseAttributes: function(node) { 
    336812        var attributes = {}; 
    337         // assume attribute nodes are type 1 children with a type 3 child 
     813        // assume attribute nodes are type 1 children with a type 3 or 4 child 
    338814        var child, grandchildren, grandchild; 
    339815        var children = node.childNodes; 
    340816        for(var i=0; i<children.length; ++i) { 
    341817            child = children[i]; 
    342818            if(child.nodeType == 1) { 
    343819                grandchildren = child.childNodes; 
    344                 if(grandchildren.length == 1) { 
    345                     grandchild = grandchildren[0]; 
     820                if(grandchildren.length == 1 || grandchildren.length == 3) { 
     821                    var grandchild; 
     822                    switch (grandchildren.length) { 
     823                        case 1: 
     824                            grandchild = grandchildren[0]; 
     825                            break; 
     826                        case 3: 
     827                        default: 
     828                            grandchild = grandchildren[1]; 
     829                            break; 
     830                    } 
    346831                    if(grandchild.nodeType == 3 || grandchild.nodeType == 4) { 
    347832                        var name = (child.prefix) ? 
    348833                                child.nodeName.split(":")[1] : 
    349834                                child.nodeName; 
    350                         var value = grandchild.nodeValue.replace( 
    351                                                 this.regExes.trimSpace, ""); 
    352                         attributes[name] = value; 
     835                        var value = OpenLayers.Util.getXmlNodeValue(grandchild) 
     836                        if (value) { 
     837                            value = value.replace(this.regExes.trimSpace, ""); 
     838                            attributes[name] = value; 
     839                        } 
    353840                    } 
    354                 } 
     841                }  
    355842            } 
    356843        } 
    357844        return attributes; 
    358845    }, 
    359846 
     847     
    360848    /** 
     849     * Method: parseProperty 
     850     * Convenience method to find a node and return its value 
     851     * 
     852     * Parameters: 
     853     * xmlNode    - {<DOMElement>} 
     854     * namespace  - {String} namespace of the node to find 
     855     * tagName    - {String} name of the property to parse 
     856     *  
     857     * Returns: 
     858     * {String} The value for the requested property (defaults to null) 
     859     */     
     860    parseProperty: function(xmlNode, namespace, tagName) { 
     861        var value; 
     862        var nodeList = this.getElementsByTagNameNS(xmlNode, namespace, tagName); 
     863        try { 
     864            value = OpenLayers.Util.getXmlNodeValue(nodeList[0]); 
     865        } catch(e) { 
     866            value = null; 
     867        } 
     868      
     869        return value; 
     870    },                                                               
     871 
     872    /** 
    361873     * APIMethod: write 
    362874     * Accept Feature Collection, and return a string.  
    363875     *