/* geoxml3.js Renders KML on the Google Maps JavaScript API Version 3 http://code.google.com/p/geoxml3/ Copyright 2010 Sterling Udell, Larry Ross Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * A MultiGeometry object that will allow multiple polylines in a MultiGeometry * containing LineStrings to be treated as a single object * * @param {MutiGeometryOptions} anonymous object. Available properties: * map: The map on which to attach the MultiGeometry * paths: the individual polylines * polylineOptions: options to use when constructing all the polylines * * @constructor */ // only if Google Maps API included if (!!window.google && !! google.maps) { function MultiGeometry(multiGeometryOptions) { function createPolyline(polylineOptions, mg) { var polyline = new google.maps.Polyline(polylineOptions); google.maps.event.addListener(polyline,'click', function(evt) { google.maps.event.trigger(mg,'click',evt);}); google.maps.event.addListener(polyline,'dblclick', function(evt) { google.maps.event.trigger(mg, 'dblclick', evt);}); google.maps.event.addListener(polyline,'mousedown', function(evt) { google.maps.event.trigger(mg, 'mousedown', evt);}); google.maps.event.addListener(polyline,'mousemove', function(evt) { google.maps.event.trigger(mg, 'mousemove', evt);}); google.maps.event.addListener(polyline,'mouseout', function(evt) { google.maps.event.trigger(mg, 'mouseout', evt);}); google.maps.event.addListener(polyline,'mouseover', function(evt) { google.maps.event.trigger(mg, 'mouseover', evt);}); google.maps.event.addListener(polyline,'mouseup', function(evt) { google.maps.event.trigger(mg, 'mouseup', evt);}); google.maps.event.addListener(polyline,'rightclick', function(evt) { google.maps.event.trigger(mg, 'rightclick', evt);}); return polyline; } this.setValues(multiGeometryOptions); this.polylines = []; for (i=0; i= docs.length) { thisDoc = new Object(); thisDoc.baseUrl = baseUrl; internals.docSet.push(thisDoc); } thisDoc.url = urls[i]; thisDoc.internals = internals; var url = thisDoc.url; if (parserOptions.proxy) url = parserOptions.proxy+thisDoc.url; fetchDoc(url, thisDoc); } }; function fetchDoc(url, doc) { geoXML3.fetchXML(url, function (responseXML) { render(responseXML, doc);}) } var hideDocument = function (doc) { if (!doc) doc = docs[0]; // Hide the map objects associated with a document var i; if (!!window.google && !!google.maps) { if (!!doc.markers) { for (i = 0; i < doc.markers.length; i++) { if(!!doc.markers[i].infoWindow) doc.markers[i].infoWindow.close(); doc.markers[i].setVisible(false); } } if (!!doc.ggroundoverlays) { for (i = 0; i < doc.ggroundoverlays.length; i++) { doc.ggroundoverlays[i].setOpacity(0); } } if (!!doc.gpolylines) { for (i=0;i 0)) { styles[styleID].scale = parseFloat(nodeValue(styleNodes[0].getElementsByTagName('scale')[0])); } if (isNaN(styles[styleID].scale)) styles[styleID].scale = 1.0; styleNodes = thisNode.getElementsByTagName('Icon'); if (!!styleNodes && !!styleNodes.length && (styleNodes.length > 0)) { styles[styleID].href = nodeValue(styleNodes[0].getElementsByTagName('href')[0]); } styleNodes = thisNode.getElementsByTagName('LineStyle'); if (!!styleNodes && !!styleNodes.length && (styleNodes.length > 0)) { styles[styleID].color = nodeValue(styleNodes[0].getElementsByTagName('color')[0],defaultStyle.color); styles[styleID].colorMode = nodeValue(styleNodes[0].getElementsByTagName('colorMode')[0], defaultStyle.colorMode); styles[styleID].width = nodeValue(styleNodes[0].getElementsByTagName('width')[0],defaultStyle.width); } styleNodes = thisNode.getElementsByTagName('PolyStyle'); if (!!styleNodes && !!styleNodes.length && (styleNodes.length > 0)) { styles[styleID].outline = getBooleanValue(styleNodes[0].getElementsByTagName('outline')[0],defaultStyle.outline); styles[styleID].fill = getBooleanValue(styleNodes[0].getElementsByTagName('fill')[0],defaultStyle.fill); styles[styleID].colorMode = nodeValue(styleNodes[0].getElementsByTagName('colorMode')[0], defaultStyle.colorMode); styles[styleID].fillcolor = nodeValue(styleNodes[0].getElementsByTagName('color')[0],defaultStyle.fillcolor); } return styles[styleID]; } // from http://stackoverflow.com/questions/122102/what-is-the-most-efficient-way-to-clone-a-javascript-object // http://keithdevens.com/weblog/archive/2007/Jun/07/javascript.clone function clone(obj){ if(obj == null || typeof(obj) != 'object') return obj; var temp = new obj.constructor(); for(var key in obj) temp[key] = clone(obj[key]); return temp; } function processStyleMap(thisNode, styles, styleID) { var nodeValue = geoXML3.nodeValue; var pairs = thisNode.getElementsByTagName('Pair'); var map = new Object(); // add each key to the map for (var pr=0;pr 0) { break; } else { return [{coordinates: []}]; } } for (var j=0; j 0)) { var style = processStyle(node,doc.styles,"inline"); processStyleID(style); if (style) placemark.style = style; } if (/^https?:\/\//.test(placemark.description)) { placemark.description = ['', placemark.description, ''].join(''); } // process MultiGeometry var GeometryNodes = node.getElementsByTagName('coordinates'); var Geometry = null; if (!!GeometryNodes && (GeometryNodes.length > 0)) { for (var gn=0;gn= 0 ; i--) { if (!doc.markers[i].active) { if (!!doc.markers[i].infoWindow) { doc.markers[i].infoWindow.close(); } doc.markers[i].setMap(null); doc.markers.splice(i, 1); } } } // Parse ground overlays if (!!doc.reload && !!doc.groundoverlays) { for (i = 0; i < doc.groundoverlays.length; i++) { doc.groundoverlays[i].active = false; } } if (!!doc) { doc.groundoverlays = doc.groundoverlays || []; } // doc.groundoverlays =[]; var groundOverlay, color, transparency, overlay; var groundNodes = responseXML.getElementsByTagName('GroundOverlay'); for (i = 0; i < groundNodes.length; i++) { node = groundNodes[i]; // Init the ground overlay object groundOverlay = { name: geoXML3.nodeValue(node.getElementsByTagName('name')[0]), description: geoXML3.nodeValue(node.getElementsByTagName('description')[0]), icon: {href: geoXML3.nodeValue(node.getElementsByTagName('href')[0])}, latLonBox: { north: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('north')[0])), east: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('east')[0])), south: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('south')[0])), west: parseFloat(geoXML3.nodeValue(node.getElementsByTagName('west')[0])) } }; if (!!window.google && !!google.maps) { doc.bounds = doc.bounds || new google.maps.LatLngBounds(); doc.bounds.union(new google.maps.LatLngBounds( new google.maps.LatLng(groundOverlay.latLonBox.south, groundOverlay.latLonBox.west), new google.maps.LatLng(groundOverlay.latLonBox.north, groundOverlay.latLonBox.east) )); } // Opacity is encoded in the color node var colorNode = node.getElementsByTagName('color'); if ( colorNode && colorNode.length && (colorNode.length > 0)) { groundOverlay.opacity = geoXML3.getOpacity(nodeValue(colorNode[0])); } else { groundOverlay.opacity = 0.45; } doc.groundoverlays.push(groundOverlay); if (!!window.google && !!google.maps) { if (!!parserOptions.createOverlay) { // User-defined overlay handler parserOptions.createOverlay(groundOverlay, doc); } else { // ! user defined createOverlay // Check to see if this overlay was created on a previous load of this document var found = false; if (!!doc) { doc.groundoverlays = doc.groundoverlays || []; if (!!window.google && !!google.maps && doc.reload) { overlayBounds = new google.maps.LatLngBounds( new google.maps.LatLng(groundOverlay.latLonBox.south, groundOverlay.latLonBox.west), new google.maps.LatLng(groundOverlay.latLonBox.north, groundOverlay.latLonBox.east)); var overlays = doc.groundoverlays; for (i = overlays.length; i--;) { if ((overlays[i].bounds().equals(overlayBounds)) && (overlays.url_ === groundOverlay.icon.href)) { found = overlays[i].active = true; break; } } } } if (!found) { // Call the built-in overlay creator overlay = createOverlay(groundOverlay, doc); overlay.active = true; } } if (!!doc.reload && !!doc.groundoverlays && !!doc.groundoverlays.length) { var overlays = doc.groundoverlays; for (i = overlays.length; i--;) { if (!overlays[i].active) { overlays[i].remove(); overlays.splice(i, 1); } } doc.groundoverlays = overlays; } } } // Parse network links var networkLink; var docPath = document.location.pathname.split('/'); docPath = docPath.splice(0, docPath.length - 1).join('/'); var linkNodes = responseXML.getElementsByTagName('NetworkLink'); for (i = 0; i < linkNodes.length; i++) { node = linkNodes[i]; // Init the network link object networkLink = { name: geoXML3.nodeValue(node.getElementsByTagName('name')[0]), link: { href: geoXML3.nodeValue(node.getElementsByTagName('href')[0]), refreshMode: geoXML3.nodeValue(node.getElementsByTagName('refreshMode')[0]) } }; // Establish the specific refresh mode if (networkLink.link.refreshMode === '') { networkLink.link.refreshMode = 'onChange'; } if (networkLink.link.refreshMode === 'onInterval') { networkLink.link.refreshInterval = parseFloat(geoXML3.nodeValue(node.getElementsByTagName('refreshInterval')[0])); if (isNaN(networkLink.link.refreshInterval)) { networkLink.link.refreshInterval = 0; } } else if (networkLink.link.refreshMode === 'onChange') { networkLink.link.viewRefreshMode = geoXML3.nodeValue(node.getElementsByTagName('viewRefreshMode')[0]); if (networkLink.link.viewRefreshMode === '') { networkLink.link.viewRefreshMode = 'never'; } if (networkLink.link.viewRefreshMode === 'onStop') { networkLink.link.viewRefreshTime = geoXML3.nodeValue(node.getElementsByTagName('refreshMode')[0]); networkLink.link.viewFormat = geoXML3.nodeValue(node.getElementsByTagName('refreshMode')[0]); if (networkLink.link.viewFormat === '') { networkLink.link.viewFormat = 'BBOX=[bboxWest],[bboxSouth],[bboxEast],[bboxNorth]'; } } } if (!/^[\/|http]/.test(networkLink.link.href)) { // Fully-qualify the HREF networkLink.link.href = docPath + '/' + networkLink.link.href; } // Apply the link if ((networkLink.link.refreshMode === 'onInterval') && (networkLink.link.refreshInterval > 0)) { // Reload at regular intervals setInterval(parserName + '.parse("' + networkLink.link.href + '")', 1000 * networkLink.link.refreshInterval); } else if (networkLink.link.refreshMode === 'onChange') { if (networkLink.link.viewRefreshMode === 'never') { // Load the link just once doc.internals.parser.parse(networkLink.link.href, doc.internals.docSet); } else if (networkLink.link.viewRefreshMode === 'onStop') { // Reload when the map view changes } } } } if (!!doc.bounds && !!window.google && !!google.maps) { doc.internals.bounds = doc.internals.bounds || new google.maps.LatLngBounds(); doc.internals.bounds.union(doc.bounds); } if (!!doc.markers || !!doc.groundoverlays || !!doc.gpolylines || !!doc.gpolygons) { doc.internals.parseOnly = false; } doc.internals.remaining -= 1; if (doc.internals.remaining === 0) { // We're done processing this set of KML documents // Options that get invoked after parsing completes if (parserOptions.zoom && !!doc.internals.bounds && !doc.internals.bounds.isEmpty() && !!parserOptions.map) { parserOptions.map.fitBounds(doc.internals.bounds); } if (parserOptions.afterParse) { parserOptions.afterParse(doc.internals.docSet); } if (!doc.internals.parseOnly) { // geoXML3 is not being used only as a real-time parser, so keep the processed documents around for (var i=0;i specs var randomColor = function(rr, gg, bb) { var col = { rr: rr, gg: gg, bb: bb }; for (var k in col) { var v = col[k]; if (v == null) v = 'ff'; // RGB values are limiters for random numbers (ie: 7f would be a random value between 0 and 7f) v = Math.round(Math.random() * parseInt(rr, 16)).toString(16); if (v.length === 1) v = '0' + v; col[k] = v; } return '#' + col.rr + col.gg + col.bb; }; var processStyleID = function (style) { if (!!window.google && !!google.maps) { var zeroPoint = new google.maps.Point(0,0); if (!!style.href) { var markerRegEx = /\/(red|blue|green|yellow|lightblue|purple|pink|orange|pause|go|stop)(-dot)?\.png/; if (markerRegEx.test(style.href)) { //bottom middle var anchorPoint = new google.maps.Point(16*style.scale, 32*style.scale); } else { var anchorPoint = new google.maps.Point(16*style.scale, 16*style.scale); } // Init the style object with a standard KML icon style.icon = { url: style.href, size: new google.maps.Size(32*style.scale, 32*style.scale), origin: zeroPoint, // bottom middle anchor: anchorPoint, scaledSize: new google.maps.Size(32*style.scale, 32*style.scale)}; // Look for a predictable shadow var stdRegEx = /\/(red|blue|green|yellow|lightblue|purple|pink|orange)(-dot)?\.png/; var shadowSize = new google.maps.Size(59, 32); var shadowPoint = new google.maps.Point(16,32); if (stdRegEx.test(style.href)) { // A standard GMap-style marker icon style.shadow = { url: 'http://maps.google.com/mapfiles/ms/micons/msmarker.shadow.png', size: shadowSize, origin: zeroPoint, anchor: shadowPoint, scaledSize: shadowSize}; } else if (style.href.indexOf('-pushpin.png') > -1) { // Pushpin marker icon style.shadow = { url: 'http://maps.google.com/mapfiles/ms/micons/pushpin_shadow.png', size: shadowSize, origin: zeroPoint, anchor: shadowPoint, scaledSize: shadowSize}; } else { // Other MyMaps KML standard icon style.shadow = { url: style.href.replace('.png', '.shadow.png'), size: shadowSize, origin: zeroPoint, anchor: shadowPoint, scaledSize: shadowSize}; } } } } var processStyles = function (doc) { for (var styleID in doc.styles) { processStyleID(doc.styles[styleID]); } }; var createMarker = function (placemark, doc) { // create a Marker to the map from a placemark KML object // Load basic marker properties var markerOptions = geoXML3.combineOptions(parserOptions.markerOptions, { map: parserOptions.map, position: new google.maps.LatLng(placemark.Point.coordinates[0].lat, placemark.Point.coordinates[0].lng), title: placemark.name, zIndex: Math.round(placemark.Point.coordinates[0].lat * -100000)<<5, icon: placemark.style.icon, shadow: placemark.style.shadow }); // Create the marker on the map var marker = new google.maps.Marker(markerOptions); if (!!doc) { doc.markers.push(marker); } // Set up and create the infowindow if it is not suppressed if (!parserOptions.suppressInfoWindows) { var infoWindowOptions = geoXML3.combineOptions(parserOptions.infoWindowOptions, { content: '

' + placemark.name + '

' + placemark.description + '
', pixelOffset: new google.maps.Size(0, 2) }); if (parserOptions.infoWindow) { marker.infoWindow = parserOptions.infoWindow; } else { marker.infoWindow = new google.maps.InfoWindow(infoWindowOptions); } marker.infoWindowOptions = infoWindowOptions; // Infowindow-opening event handler google.maps.event.addListener(marker, 'click', function() { this.infoWindow.close(); marker.infoWindow.setOptions(this.infoWindowOptions); this.infoWindow.open(this.map, this); }); } placemark.marker = marker; return marker; }; var createOverlay = function (groundOverlay, doc) { // Add a ProjectedOverlay to the map from a groundOverlay KML object if (!window.ProjectedOverlay) { throw 'geoXML3 error: ProjectedOverlay not found while rendering GroundOverlay from KML'; } var bounds = new google.maps.LatLngBounds( new google.maps.LatLng(groundOverlay.latLonBox.south, groundOverlay.latLonBox.west), new google.maps.LatLng(groundOverlay.latLonBox.north, groundOverlay.latLonBox.east) ); var overlayOptions = geoXML3.combineOptions(parserOptions.overlayOptions, {percentOpacity: groundOverlay.opacity*100}); var overlay = new ProjectedOverlay(parserOptions.map, groundOverlay.icon.href, bounds, overlayOptions); if (!!doc) { doc.ggroundoverlays = doc.ggroundoverlays || []; doc.ggroundoverlays.push(overlay); } return overlay; }; // Create Polyline var createPolyline = function(placemark, doc) { var paths = []; var bounds = new google.maps.LatLngBounds(); for (var j=0; j 1) { polyOptions.paths = paths; var p = new MultiGeometry(polyOptions); } else { polyOptions.path = paths[0]; var p = new google.maps.Polyline(polyOptions); } p.bounds = bounds; // setup and create the infoWindow if it is not suppressed if (!parserOptions.suppressInfoWindows) { var infoWindowOptions = geoXML3.combineOptions(parserOptions.infoWindowOptions, { content: '

' + placemark.name + '

' + placemark.description + '
', pixelOffset: new google.maps.Size(0, 2) }); if (parserOptions.infoWindow) { p.infoWindow = parserOptions.infoWindow; } else { p.infoWindow = new google.maps.InfoWindow(infoWindowOptions); } p.infoWindowOptions = infoWindowOptions; // Infowindow-opening event handler google.maps.event.addListener(p, 'click', function(e) { p.infoWindow.close(); p.infoWindow.setOptions(p.infoWindowOptions); if (e && e.latLng) { p.infoWindow.setPosition(e.latLng); } else { p.infoWindow.setPosition(point); } p.infoWindow.open(p.map || p.polylines[0].map); }); } if (!!doc) doc.gpolylines.push(p); placemark.polyline = p; return p; } // Create Polygon var createPolygon = function(placemark, doc) { var bounds = new google.maps.LatLngBounds(); var pathsLength = 0; var paths = []; for (var polygonPart=0;polygonPart

' + placemark.name + '

' + placemark.description + '
', pixelOffset: new google.maps.Size(0, 2) }); if (parserOptions.infoWindow) { p.infoWindow = parserOptions.infoWindow; } else { p.infoWindow = new google.maps.InfoWindow(infoWindowOptions); } p.infoWindowOptions = infoWindowOptions; // Infowindow-opening event handler google.maps.event.addListener(p, 'click', function(e) { p.infoWindow.close(); p.infoWindow.setOptions(p.infoWindowOptions); if (e && e.latLng) { p.infoWindow.setPosition(e.latLng); } else { p.infoWindow.setPosition(p.bounds.getCenter()); } p.infoWindow.open(this.map); }); } if (!!doc) doc.gpolygons.push(p); placemark.polygon = p; return p; } return { // Expose some properties and methods options: parserOptions, docs: docs, parse: parse, render: render, parseKmlString: parseKmlString, hideDocument: hideDocument, showDocument: showDocument, processStyles: processStyles, createMarker: createMarker, createOverlay: createOverlay, createPolyline: createPolyline, createPolygon: createPolygon }; }; // End of KML Parser // Helper objects and functions geoXML3.getOpacity = function (kmlColor) { // Extract opacity encoded in a KML color value. Returns a number between 0 and 1. if (!!kmlColor && (kmlColor !== '') && (kmlColor.length == 8)) { var transparency = parseInt(kmlColor.substr(0, 2), 16); return transparency / 255; } else { return 1; } }; // Log a message to the debugging console, if one exists geoXML3.log = function(msg) { if (!!window.console) { console.log(msg); } else { alert("log:"+msg); } }; // Combine two options objects: a set of default values and a set of override values geoXML3.combineOptions = function (overrides, defaults) { var result = {}; if (!!overrides) { for (var prop in overrides) { if (overrides.hasOwnProperty(prop)) { result[prop] = overrides[prop]; } } } if (!!defaults) { for (prop in defaults) { if (defaults.hasOwnProperty(prop) && (result[prop] === undefined)) { result[prop] = defaults[prop]; } } } return result; }; // Retrieve an XML document from url and pass it to callback as a DOM document geoXML3.fetchers = []; // parse text to XML doc /** * Parses the given XML string and returns the parsed document in a * DOM data structure. This function will return an empty DOM node if * XML parsing is not supported in this browser. * @param {string} str XML string. * @return {Element|Document} DOM. */ geoXML3.xmlParse = function (str) { if ((typeof ActiveXObject != 'undefined') || ("ActiveXObject" in window)) { var doc = new ActiveXObject('Microsoft.XMLDOM'); doc.loadXML(str); return doc; } if (typeof DOMParser != 'undefined') { return (new DOMParser()).parseFromString(str, 'text/xml'); } return document.createElement('div', null); } // from http://stackoverflow.com/questions/11563554/how-do-i-detect-xml-parsing-errors-when-using-javascripts-domparser-in-a-cross geoXML3.isParseError = function(parsedDocument) { if ((typeof ActiveXObject != 'undefined') || ("ActiveXObject" in window)) return false; // parser and parsererrorNS could be cached on startup for efficiency var p = new DOMParser(), errorneousParse = p.parseFromString('<', 'text/xml'), parsererrorNS = errorneousParse.getElementsByTagName("parsererror")[0].namespaceURI; if (parsererrorNS === 'http://www.w3.org/1999/xhtml') { // In PhantomJS the parseerror element doesn't seem to have a special namespace, so we are just guessing here :( return parsedDocument.getElementsByTagName("parsererror").length > 0; } return parsedDocument.getElementsByTagNameNS(parsererrorNS, 'parsererror').length > 0; }; geoXML3.fetchXML = function (url, callback) { function timeoutHandler() { geoXML3.log('XHR timeout'); callback(); }; var xhrFetcher = new Object(); if (!!geoXML3.fetchers.length) { xhrFetcher = geoXML3.fetchers.pop(); } else { if (!!window.XMLHttpRequest) { xhrFetcher.fetcher = new window.XMLHttpRequest(); // Most browsers } else if (!!window.ActiveXObject) { xhrFetcher.fetcher = new window.ActiveXObject('Microsoft.XMLHTTP'); // Some IE } } if (!xhrFetcher.fetcher) { geoXML3.log('Unable to create XHR object'); callback(null); } else { xhrFetcher.fetcher.open('GET', url, true); if (xhrFetcher.fetcher.overrideMimeType) { xhrFetcher.fetcher.overrideMimeType('text/xml'); } xhrFetcher.fetcher.onreadystatechange = function () { if (xhrFetcher.fetcher.readyState === 4) { // Retrieval complete if (!!xhrFetcher.xhrtimeout) clearTimeout(xhrFetcher.xhrtimeout); if (xhrFetcher.fetcher.status >= 400) { geoXML3.log('HTTP error ' + xhrFetcher.fetcher.status + ' retrieving ' + url); callback(); } else { // Returned successfully var xml = geoXML3.xmlParse(xhrFetcher.fetcher.responseText); if (xml.parseError && (xml.parseError.errorCode != 0)) { geoXML3.log("XML parse error "+xml.parseError.errorCode+", "+xml.parseError.reason+"\nLine:"+xml.parseError.line+", Position:"+xml.parseError.linepos+", srcText:"+xml.parseError.srcText); xml = "failed parse" } else if (geoXML3.isParseError(xml)) { geoXML3.log("XML parse error"); xml = "failed parse" } callback(xml); } // We're done with this fetcher object geoXML3.fetchers.push(xhrFetcher); } }; xhrFetcher.xhrtimeout = setTimeout(timeoutHandler, geoXML3.xhrTimeout); xhrFetcher.fetcher.send(null); } }; //nodeValue: Extract the text value of a DOM node, with leading and trailing whitespace trimmed geoXML3.nodeValue = function(node, defVal) { var retStr=""; if (!node) { return (typeof defVal === 'undefined' || defVal === null) ? '' : defVal; } if(node.nodeType==3||node.nodeType==4||node.nodeType==2){ retStr+=node.nodeValue; }else if(node.nodeType==1||node.nodeType==9||node.nodeType==11){ for(var i=0;i