OpenLayers OpenLayers

Ticket #1259: kml_styling_v2.diff

File kml_styling_v2.diff, 23.1 kB (added by rdewit, 1 year ago)

New version: cleaned up and uses OL.Style. Tests pass in FF2,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 styles requires this to be set to true 
    5152     */ 
    5253    extractAttributes: true, 
    5354     
     55    /* APIProperty: reverseFeatures  
     56     * {Boolean} Reverse drawing order of features. Default is false, but 
     57     *           it seems like it should be true. 
     58     * */ 
     59    reverseFeatures: false, 
     60 
    5461    /** 
    5562     * Property: internalns 
    5663     * {String} KML Namespace to use -- defaults to the namespace of the 
     
    5966    internalns: null, 
    6067 
    6168    /** 
     69     * Property: features 
     70     * {Array} Array of features 
     71     *      
     72     */ 
     73    features: [], 
     74 
     75    /** 
     76     * Property: styles 
     77     * {Object} Storage of style objects 
     78     *      
     79     */ 
     80    styles: {}, 
     81 
     82    /** 
     83     * Property: fetched 
     84     * {Object} Storage of KML URLs that have been fetched before 
     85     *     in order to prevent reloading them. 
     86     */ 
     87    fetched: {}, 
     88 
     89    /** 
     90     * APIProperty: maxDepth 
     91     * {Integer} Maximum depth for recursive loading external KML URLs  
     92     *           Defaults to 1: fetch 1 level deep  
     93     *           To disable any external fetching: set this to 0 
     94     */ 
     95    maxDepth: 1, 
     96 
     97    /** 
    6298     * Constructor: OpenLayers.Format.KML 
    6399     * Create a new parser for KML. 
    64100     * 
     
    72108            trimSpace: (/^\s*|\s*$/g), 
    73109            removeSpace: (/\s*/g), 
    74110            splitSpace: (/\s+/), 
    75             trimComma: (/\s*,\s*/g) 
     111            trimComma: (/\s*,\s*/g), 
     112            kmlColor: (/(\w{2})(\w{2})(\w{2})(\w{2})/), 
     113            kmlIconPalette: (/root:\/\/icons\/palette-(\d+)(\.\w+)/), 
     114            straightBracket: (/\$\[(.*?)\]/g) 
    76115        }; 
    77116        OpenLayers.Format.XML.prototype.initialize.apply(this, [options]); 
    78117    }, 
     
    82121     * Read data from a string, and return a list of features.  
    83122     *  
    84123     * Parameters:  
    85      * data - {String} or {DOMElement} data to read/parse. 
     124     * data    - {String} or {DOMElement} data to read/parse. 
     125     * options - {Object} Hash of options 
    86126     * 
    87127     * Returns: 
    88128     * {Array(<OpenLayers.Feature.Vector>)} List of features. 
    89129     */ 
    90     read: function(data) { 
     130    read: function(data, options) { 
    91131        if(typeof data == "string") { 
    92132            data = OpenLayers.Format.XML.prototype.read.apply(this, [data]); 
    93133        } 
    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]); 
     134 
     135        // create default options if none provided 
     136        options = options || {}; 
     137 
     138        // Set default options  
     139        OpenLayers.Util.applyDefaults(options, { 
     140            depth: 0, 
     141            styleBaseUrl: "" 
     142        }); 
     143 
     144        // Loop throught the following node types in this order and 
     145        // process the nodes found  
     146        var types = ["Link", "NetworkLink", "Style", "StyleMap", "Placemark"]; 
     147        for(var i=0; i<types.length; ++i) { 
     148            var type = types[i]; 
     149 
     150            var nodes = this.getElementsByTagNameNS(data, "*", type); 
     151 
     152            if(nodes.length == 0) {  
     153                continue; 
     154            } 
     155 
     156            switch (type.toLowerCase()) { 
     157 
     158                // Fetch external links  
     159                case "link": 
     160                case "networklink": 
     161                    this.parseLinks(nodes, options); 
     162                    break; 
     163 
     164                // parse style information 
     165                case "style": 
     166                    this.parseStyles(nodes, options); 
     167                    break; 
     168                case "stylemap": 
     169                    this.parseStyleMaps(nodes, options); 
     170                    break; 
     171 
     172                // parse features 
     173                case "placemark": 
     174                    this.parseFeatures(nodes, options); 
     175                    break; 
     176            } 
     177        } 
     178         
     179        // Reversing the features, only on highest recursion level  
     180        if (this.reverseFeatures && options.depth == 0) { 
     181            this.features.reverse(); 
     182        } 
     183 
     184        return this.features; 
     185    }, 
     186 
     187    /** 
     188     * Method: parseLinks 
     189     * Finds URLs of linked KML documents and fetches them 
     190     *  
     191     * Parameters:  
     192     * nodes   - {Array} of {DOMElement} data to read/parse. 
     193     * options - {Object} Hash of options 
     194     *  
     195     */ 
     196    parseLinks: function(nodes, options) { 
     197         
     198        // Fetch external links <NetworkLink> and <Link> 
     199        // Don't do anything if we have reached our maximum depth for recursion 
     200        if (options.depth >= this.maxDepth) { 
     201            return false; 
     202        } 
     203 
     204        // increase depth 
     205        var newOptions = OpenLayers.Util.extend({}, options); 
     206        newOptions.depth++; 
     207 
     208        for(var i=0; i < nodes.length; i++) { 
     209            var href = this.parseProperty(nodes[i], "*", "href"); 
     210            if(href && !this.fetched[href]) { 
     211                this.fetched[href] = true; // prevent reloading the same urls 
     212                var data = this.fetchLink(href); 
     213                if (data) { 
     214                    this.read(data, newOptions); 
     215                } 
     216            }  
     217        } 
     218 
     219    }, 
     220 
     221    /** 
     222     * Method: fetchLink 
     223     * Fetches a URL and returns the result 
     224     *  
     225     * Parameters:  
     226     * href  - {String} url to be fetched 
     227     *  
     228     */ 
     229    fetchLink: function(href) { 
     230 
     231        if (OpenLayers.ProxyHost  
     232                && OpenLayers.String.startsWith(href, "http")) { 
     233            href = OpenLayers.ProxyHost + escape(href); 
     234        } 
     235 
     236        var request = new OpenLayers.Ajax.Request(href,  
     237                      {method: 'get', asynchronous: false }); 
     238 
     239        if (request && request.transport) { 
     240            return request.transport.responseText; 
     241        } 
     242 
     243    }, 
     244 
     245    /** 
     246     * Method: parseStyles 
     247     * Looks for <Style> nodes in the data and parses them 
     248     * Also parses <StyleMap> nodes, but only uses the 'normal' key 
     249     *  
     250     * Parameters:  
     251     * nodes    - {Array} of {DOMElement} data to read/parse. 
     252     * options  - {Object} Hash of options 
     253     *  
     254     */ 
     255    parseStyles: function(nodes, options) { 
     256        for(var i=0; i < nodes.length; i++) { 
     257            var style = this.parseStyle(nodes[i]); 
     258            if(style && style.name) { 
     259                styleName = (options.styleBaseUrl || "") + "#" + style.name; 
     260                this.styles[styleName] = style; 
     261            } 
     262        } 
     263    }, 
     264 
     265    /** 
     266     * Method: parseStyleMaps 
     267     * Looks for <Style> nodes in the data and parses them 
     268     * Also parses <StyleMap> nodes, but only uses the 'normal' key 
     269     *  
     270     * Parameters:  
     271     * nodes    - {Array} of {DOMElement} data to read/parse. 
     272     * options  - {Object} Hash of options 
     273     *  
     274     */ 
     275    parseStyleMaps: function(nodes, options) { 
     276        // Only the default or "normal" part of the StyleMap is processed now 
     277        // To do the select or "highlight" bit, we'd need to change lots more 
     278 
     279        for(var i=0; i < nodes.length; i++) { 
     280            var node = nodes[i]; 
     281            var pairs = this.getElementsByTagNameNS(node, "*",  
     282                            "Pair"); 
     283 
     284            var id = node.getAttribute("id"); 
     285            for (var j=0; j<pairs.length; j++) { 
     286                var pair = pairs[j]; 
     287                // Use the shortcut in the SLD format to quickly retrieve the  
     288                // value of a node. Maybe it's good to have a method in  
     289                // Format.XML to do this 
     290                var key = this.parseProperty(pair, "*", "key"); 
     291                var styleUrl = this.parseProperty(pair, "*", "styleUrl"); 
     292 
     293                if (styleUrl && key == "normal") { 
     294                    this.styles[(options.styleBaseUrl || "") + "#" + id] = 
     295                        this.styles[(options.styleBaseUrl || "") + styleUrl]; 
     296                } 
     297 
     298                if (styleUrl && key == "highlight") { 
     299                    // TODO: implement the "select" part 
     300                } 
     301 
     302            } 
     303        } 
     304 
     305    }, 
     306 
     307    /** 
     308     * Method: parseStyle 
     309     * Parses the children of a <Style> node and builds the style hash 
     310     * accordingly 
     311     *  
     312     * Parameters:  
     313     * node - {DOMElement} <Style> node 
     314     *  
     315     */ 
     316    parseStyle: function(node) { 
     317        var style = {}; 
     318         
     319        var types = ["LineStyle", "PolyStyle", "IconStyle", "BalloonStyle"]; 
     320        var type, nodeList, geometry, parser; 
     321        for(var i=0; i<types.length; ++i) { 
     322            type = types[i]; 
     323            //"*" = node.namespaceURI ?  
     324                    //node.namespaceURI : this.kmlns; 
     325            styleTypeNode = this.getElementsByTagNameNS(node,  
     326                                                   "*", type)[0]; 
     327            if(!styleTypeNode) {  
     328                continue; 
     329            } 
     330 
     331            // only deal with first geometry of this type 
     332            switch (type.toLowerCase()) { 
     333                case "linestyle": 
     334                    var color = this.parseProperty(styleTypeNode, "*", "color"); 
     335                    if (color) { 
     336                        var matches = (color.toString()).match( 
     337                                                         this.regExes.kmlColor); 
     338 
     339                        // transparency 
     340                        var alpha = matches[1]; 
     341                        style["strokeOpacity"] = parseInt(alpha, 16) / 255; 
     342 
     343                        // rgb colors (google uses bgr) 
     344                        var b = matches[2];  
     345                        var g = matches[3];  
     346                        var r = matches[4];  
     347                        style["strokeColor"] = "#" + r + g + b; 
     348                    } 
     349                     
     350                    var width = this.parseProperty(styleTypeNode, "*", "width"); 
     351                    if (width) { 
     352                        style["strokeWidth"] = width; 
     353                    } 
     354 
     355                case "polystyle": 
     356                    var color = this.parseProperty(styleTypeNode, "*", "color"); 
     357                    if (color) { 
     358                        var matches = (color.toString()).match( 
     359                                                         this.regExes.kmlColor); 
     360 
     361                        // transparency 
     362                        var alpha = matches[1]; 
     363                        style["fillOpacity"] = parseInt(alpha, 16) / 255; 
     364 
     365                        // rgb colors (google uses bgr) 
     366                        var b = matches[2];  
     367                        var g = matches[3];  
     368                        var r = matches[4];  
     369                        style["fillColor"] = "#" + r + g + b; 
     370                    } 
     371                     
     372                    break; 
     373                case "iconstyle": 
     374                    var iconNode = this.getElementsByTagNameNS(styleTypeNode,  
     375                                               "*",  
     376                                               "Icon")[0]; 
     377 
     378                    // set default width and height of icon 
     379                    style["graphicWidth"] = 32; 
     380                    style["graphicHeight"] = 32; 
     381 
     382                    if (iconNode) { 
     383                        var href = this.parseProperty(iconNode, "*", "href"); 
     384                        if (href) {                                                    
     385 
     386                            // support for internal icons  
     387                            //    (/root://icons/palette-x.png) 
     388                            // x and y tell the position on the palette: 
     389                            // - in pixels 
     390                            // - starting from the left bottom 
     391                            // We need to translate that to a position in the  
     392                            // list and request the appropriate icon from the  
     393                            // google maps website 
     394                            var matches = href.match(this.regExes.kmlIconPalette); 
     395                            if (matches)  { 
     396                                var palette = matches[1]; 
     397                                var file_extension = matches[2]; 
     398 
     399                                var x = this.parseProperty(iconNode, "*", "x"); 
     400                                var y = this.parseProperty(iconNode, "*", "y"); 
     401 
     402                                var posX = x ? x/32 : 0; 
     403                                var posY = y ? (7 - y/32) : 7; 
     404 
     405                                var pos = posY * 8 + posX; 
     406                                var href = "http://maps.google.com/mapfiles/kml/pal"  
     407                                     + palette + "/icon" + pos + file_extension; 
     408                            } 
     409 
     410 
     411                            var w = this.parseProperty(iconNode, "*", "w"); 
     412                            if (w) { 
     413                                style["graphicWidth"] = parseInt(w); 
     414                            } 
     415 
     416                            var h = this.parseProperty(iconNode, "*", "h"); 
     417                            if (h) { 
     418                                style["graphicHeight"] = parseInt(h); 
     419                            } 
     420 
     421                            style["graphicOpacity"] = 1; // fully opaque 
     422                            style["externalGraphic"] = href; 
     423                        } 
     424 
     425                    } 
     426 
     427 
     428                    // hotSpots define the offset for an Icon 
     429                    var hotSpotNode = this.getElementsByTagNameNS(styleTypeNode,  
     430                                               "*",  
     431                                               "hotSpot")[0]; 
     432                    if (hotSpotNode) { 
     433                        var x = hotSpotNode.getAttribute("x"); 
     434                        var y = hotSpotNode.getAttribute("y"); 
     435 
     436                        var xUnits = hotSpotNode.getAttribute("xunits"); 
     437                        if (xUnits == "pixels") { 
     438                            style["graphicXOffset"] = parseInt(x); 
     439                        } 
     440                        else if (xUnits == "insetPixels") { 
     441                            style["graphicXOffset"] = style["graphicWidth"]  
     442                                                          - parseInt(x); 
     443                        } 
     444                        else if (xUnits == "fraction") { 
     445                            style["graphicXOffset"] = style["graphicWidth"]  
     446                                                          * parseFloat(x); 
     447                        } 
     448 
     449                        var yUnits = hotSpotNode.getAttribute("yunits"); 
     450                        if (yUnits == "pixels") { 
     451                            style["graphicYOffset"] = parseInt(y); 
     452                        } 
     453                        else if (yUnits == "insetPixels") { 
     454                            style["graphicYOffset"] = style["graphicHeight"]  
     455                                                          - parseInt(y); 
     456                        } 
     457                        else if (yUnits == "fraction") { 
     458                            style["graphicYOffset"] = style["graphicHeight"]  
     459                                                          * parseFloat(y); 
     460                        } 
     461                    } 
     462                    break; 
     463                case "balloonstyle": 
     464                    var balloonStyle = OpenLayers.Util.getXmlNodeValue( 
     465                                            styleTypeNode); 
     466                    if (balloonStyle) { 
     467                        style["balloonStyle"] = balloonStyle.replace( 
     468                                       this.regExes.straightBracket, "${$1}"); 
     469                    } 
     470                    break; 
     471                default: 
     472            } 
     473        } 
     474 
     475        // Some polygons have no line color, so we use the fillColor for that 
     476        if (!style["strokeColor"] && style["fillColor"]) { 
     477            style["strokeColor"] = style["fillColor"]; 
     478        } 
     479 
     480        // set default options for style object 
     481        var styleOptions = { 
     482                name: node.getAttribute("id") 
     483            }; 
     484 
     485        // convert style into OpenLayers.Style 
     486        var styleObject = new OpenLayers.Style(style, styleOptions); 
     487 
     488        return styleObject; 
     489    }, 
     490 
     491 
     492    /** 
     493     * Method: parseFeatures 
     494     * Loop through all Placemark nodes and parse them. 
     495     * Will create a list of features 
     496     *  
     497     * Parameters:  
     498     * nodes    - {Array} of {DOMElement} data to read/parse. 
     499     * options  - {Object} Hash of options 
     500     *  
     501     */ 
     502    parseFeatures: function(nodes, options) { 
     503        var features = new Array(nodes.length); 
     504        for(var i=0; i < nodes.length; i++) { 
     505            //var feature = this.parseFeature(featureNodes[i]); 
     506            var featureNode = nodes[i]; 
     507            var feature = this.parseFeature.apply(this,[featureNode]) ; 
    101508            if(feature) { 
     509                // give feature default style 
     510                var styleHash = OpenLayers.Util.extend({},  
     511                                   OpenLayers.Feature.Vector.style["default"]); 
     512                  
     513                if (feature.attributes.styleUrl) { 
     514                    var urlStyleObject = this.getStyle( 
     515                                         feature.attributes.styleUrl, options); 
     516                    if (urlStyleObject) { 
     517                        OpenLayers.Util.extend(styleHash,  
     518                                           urlStyleObject.createStyle(feature)); 
     519                    } 
     520                } 
     521 
     522                // Make sure that <Style> nodes within a placemark are  
     523                // processed as well: add a styleUrl attribute to the feature. 
     524                var styleNode = this.getElementsByTagNameNS(featureNode, 
     525                                                       "*", 
     526                                                       "Style")[0]; 
     527                if (styleNode) { 
     528                    var inlineStyleObject = this.parseStyle(styleNode); 
     529                    if (inlineStyleObject) { 
     530                        OpenLayers.Util.extend(styleHash,  
     531                                        inlineStyleObject.createStyle(feature)); 
     532                    } 
     533                } 
     534 
     535                // Give feature the created style 
     536                feature.style = new OpenLayers.Style(styleHash); 
     537 
     538                // add feature to list of features 
    102539                features[i] = feature; 
    103540            } else { 
    104541                throw "Bad Placemark: " + i; 
    105542            } 
    106543        } 
    107         return features; 
     544 
     545        // add new features to existing feature list 
     546        this.features = this.features.concat(features); 
    108547    }, 
    109548 
    110549    /** 
     
    154593        } 
    155594        var feature = new OpenLayers.Feature.Vector(geometry, attributes); 
    156595 
    157         var fid = node.getAttribute("id")
     596        var fid = node.getAttribute("id") || node.getAttribute("name")
    158597        if(fid != null) { 
    159598            feature.fid = fid; 
    160599        } 
     
    163602    },         
    164603     
    165604    /** 
     605     * Method: getStyle 
     606     * Retrieves a style from a style hash using styleUrl as the key 
     607     * If the styleUrl doesn't exist yet, we try to fetch it  
     608     * Internet 
     609     *  
     610     * Parameters:  
     611     * styleUrl  - {String} URL of style 
     612     * optinos   - {Object} Hash of options  
     613     */ 
     614    getStyle: function(styleUrl, options) { 
     615 
     616        var styleBaseUrl = OpenLayers.Util.removeTail(styleUrl); 
     617 
     618        var newOptions = OpenLayers.Util.extend({}, options); 
     619        newOptions.depth++; 
     620        newOptions.styleBaseUrl = styleBaseUrl; 
     621 
     622        // Fetch remote Style URLs (if not fetched before)  
     623        if (!this.styles[styleUrl]  
     624                && !OpenLayers.String.startsWith(styleUrl, "#")  
     625                && newOptions.depth <= this.maxDepth 
     626                && !this.fetched[styleBaseUrl] ) { 
     627 
     628            var data = this.fetchLink(styleBaseUrl); 
     629            if (data) { 
     630                this.read(data, newOptions); 
     631            } 
     632 
     633        } 
     634 
     635        // return requested style 
     636        var style = this.styles[styleUrl]; 
     637        return style; 
     638    }, 
     639     
     640    /** 
    166641     * Property: parseGeometry 
    167642     * Properties of this object are the functions that parse geometries based 
    168643     *     on their type. 
     
    334809     */ 
    335810    parseAttributes: function(node) { 
    336811        var attributes = {}; 
    337         // assume attribute nodes are type 1 children with a type 3 child 
     812        // assume attribute nodes are type 1 children with a type 3 or 4 child 
    338813        var child, grandchildren, grandchild; 
    339814        var children = node.childNodes; 
    340815        for(var i=0; i<children.length; ++i) { 
    341816            child = children[i]; 
    342817            if(child.nodeType == 1) { 
    343818                grandchildren = child.childNodes; 
    344                 if(grandchildren.length == 1) { 
    345                     grandchild = grandchildren[0]; 
     819                if(grandchildren.length == 1 || grandchildren.length == 3) { 
     820                    var grandchild; 
     821                    switch (grandchildren.length) { 
     822                        case 1: 
     823                            grandchild = grandchildren[0]; 
     824                            break; 
     825                        case 3: 
     826                        default: 
     827                            grandchild = grandchildren[1]; 
     828                            break; 
     829                    } 
    346830                    if(grandchild.nodeType == 3 || grandchild.nodeType == 4) { 
    347831                        var name = (child.prefix) ? 
    348832                                child.nodeName.split(":")[1] : 
    349833                                child.nodeName; 
    350                         var value = grandchild.nodeValue.replace( 
    351                                                 this.regExes.trimSpace, ""); 
    352                         attributes[name] = value; 
     834                        var value = OpenLayers.Util.getXmlNodeValue(grandchild) 
     835                        if (value) { 
     836                            value = value.replace(this.regExes.trimSpace, ""); 
     837                            attributes[name] = value; 
     838                        } 
    353839                    } 
    354                 } 
     840                }  
    355841            } 
    356842        } 
    357843        return attributes; 
    358844    }, 
    359845 
     846     
    360847    /** 
     848     * Method: parseProperty 
     849     * Convenience method to parse the different kinds of properties 
     850     *     found in the sld and ogc namespace. 
     851     * 
     852     * Calls OpenLayers.Format.SLD.parseProperty and returns its output 
     853     * 
     854     * Parameters: 
     855     * xmlNode        - {<DOMElement>} 
     856     * namespace      - {String} namespace of the node to find 
     857     * propertyName   - {String} name of the property to parse 
     858     *  
     859     * Returns: 
     860     * {String} The value for the requested property. 
     861     */     
     862    parseProperty: function(xmlNode, namespace, propertyName, attributeName, 
     863                                                              attributeValue) { 
     864        return OpenLayers.Format.SLD.prototype.parseProperty(xmlNode,  
     865                  namespace, propertyName, attributeName, attributeValue); 
     866    },                                                               
     867 
     868    /** 
    361869     * APIMethod: write 
    362870     * Accept Feature Collection, and return a string.  
    363871     *