442 lines
15 KiB
JavaScript
442 lines
15 KiB
JavaScript
/*
|
|
geoXML3.js
|
|
|
|
Renders KML on the Google Maps JavaScript API Version 3
|
|
http://code.google.com/p/geoxml3/
|
|
|
|
|
|
This program is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
// Extend the global String with a method to remove leading and trailing whitespace
|
|
if (!String.prototype.trim) {
|
|
String.prototype.trim = function () {
|
|
return this.replace(/^\s+|\s+$/g, '');
|
|
};
|
|
}
|
|
|
|
// Declare namespace
|
|
geoXML3 = window.geoXML3 || {};
|
|
|
|
// Constructor for the root KML parser object
|
|
geoXML3.parser = function (options) {
|
|
// Private variables
|
|
var parserOptions = geoXML3.combineOptions(options, {
|
|
singleInfoWindow: false,
|
|
processStyles: true,
|
|
zoom: true
|
|
});
|
|
var docs = []; // Individual KML documents
|
|
var lastMarker;
|
|
|
|
// Private methods
|
|
|
|
var parse = function (urls) {
|
|
// Process one or more KML documents
|
|
|
|
if (typeof urls === 'string') {
|
|
// Single KML document
|
|
urls = [urls];
|
|
}
|
|
|
|
// Internal values for the set of documents as a whole
|
|
var internals = {
|
|
docSet: [],
|
|
remaining: urls.length,
|
|
parserOnly: !parserOptions.afterParse
|
|
};
|
|
|
|
var thisDoc;
|
|
for (var i = 0; i < urls.length; i++) {
|
|
thisDoc = {
|
|
url: urls[i],
|
|
internals: internals
|
|
};
|
|
internals.docSet.push(thisDoc);
|
|
geoXML3.fetchXML(thisDoc.url, function (responseXML) {render(responseXML, thisDoc);});
|
|
}
|
|
};
|
|
|
|
var hideDocument = function (doc) {
|
|
// Hide the map objects associated with a document
|
|
var i;
|
|
for (i = 0; i < doc.markers.length; i++) {
|
|
this.markers[i].set_visible(false);
|
|
}
|
|
for (i = 0; i < doc.overlays.length; i++) {
|
|
doc.overlays[i].setOpacity(0);
|
|
}
|
|
};
|
|
|
|
var showDocument = function (doc) {
|
|
// Show the map objects associated with a document
|
|
var i;
|
|
for (i = 0; i < doc.markers.length; i++) {
|
|
doc.markers[i].set_visible(true);
|
|
}
|
|
for (i = 0; i < doc.overlays.length; i++) {
|
|
doc.overlays[i].setOpacity(doc.overlays[i].percentOpacity_);
|
|
}
|
|
};
|
|
|
|
var render = function (responseXML, doc) {
|
|
// Callback for retrieving a KML document: parse the KML and display it on the map
|
|
|
|
if (!responseXML) {
|
|
// Error retrieving the data
|
|
geoXML3.log('Unable to retrieve ' + doc.url);
|
|
if (parserOptions.failedParse) {
|
|
parserOptions.failedParse(doc);
|
|
}
|
|
} else if (!doc) {
|
|
throw 'geoXML3 internal error: render called with null document';
|
|
} else {
|
|
doc.styles = {};
|
|
doc.placemarks = [];
|
|
doc.groundOverlays = [];
|
|
if (parserOptions.zoom && !!parserOptions.map)
|
|
doc.bounds = new google.maps.LatLngBounds();
|
|
|
|
// Parse styles
|
|
var styleID, iconNodes, i;
|
|
var styleNodes = responseXML.getElementsByTagName('Style');
|
|
for (i = 0; i < styleNodes.length; i++) {
|
|
styleID = styleNodes[i].getAttribute('id');
|
|
iconNodes = styleNodes[i].getElementsByTagName('Icon');
|
|
if (!!iconNodes.length) {
|
|
doc.styles['#' + styleID] = {
|
|
href: geoXML3.nodeValue(iconNodes[0].getElementsByTagName('href')[0])
|
|
};
|
|
}
|
|
}
|
|
if (!!parserOptions.processStyles || !parserOptions.createMarker) {
|
|
// Convert parsed styles into GMaps equivalents
|
|
processStyles(doc);
|
|
}
|
|
|
|
// Parse placemarks
|
|
var placemark, node, coords, path;
|
|
var placemarkNodes = responseXML.getElementsByTagName('Placemark');
|
|
for (i = 0; i < placemarkNodes.length; i++) {
|
|
// Init the placemark object
|
|
node = placemarkNodes[i];
|
|
placemark = {
|
|
name: geoXML3.nodeValue(node.getElementsByTagName('name')[0]),
|
|
description: geoXML3.nodeValue(node.getElementsByTagName('description')[0]),
|
|
styleUrl: geoXML3.nodeValue(node.getElementsByTagName('styleUrl')[0])
|
|
};
|
|
placemark.style = doc.styles[placemark.styleUrl] || {};
|
|
if (/^https?:\/\//.test(placemark.description)) {
|
|
placemark.description = '<a href="' + placemark.description + '">' + placemark.description + '</a>';
|
|
}
|
|
|
|
// Extract the coordinates
|
|
coords = geoXML3.nodeValue(node.getElementsByTagName('coordinates')[0]).trim();
|
|
coords = coords.replace(/\s+/g, ' ').replace(/, /g, ',');
|
|
path = coords.split(' ');
|
|
|
|
// What sort of placemark?
|
|
if (path.length === 1) {
|
|
// Polygons/lines not supported in v3, so only plot markers
|
|
coords = path[0].split(',');
|
|
placemark.point = {
|
|
lat: parseFloat(coords[1]),
|
|
lng: parseFloat(coords[0]),
|
|
alt: parseFloat(coords[2])
|
|
};
|
|
if (!!doc.bounds) {
|
|
doc.bounds.extend(new google.maps.LatLng(placemark.point.lat, placemark.point.lng));
|
|
}
|
|
|
|
// Call the appropriate function to create the marker
|
|
if (!!parserOptions.createMarker) {
|
|
parserOptions.createMarker(placemark, doc);
|
|
} else {
|
|
createMarker(placemark, doc);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Parse ground overlays
|
|
var groundOverlay, color, transparency;
|
|
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 (!!doc.bounds) {
|
|
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
|
|
color = geoXML3.nodeValue(node.getElementsByTagName('color')[0]);
|
|
if ((color !== '') && (color.length == 8)) {
|
|
transparency = parseInt(color.substring(0, 2), 16);
|
|
groundOverlay.opacity = Math.round((255 - transparency) / 2.55);
|
|
} else {
|
|
groundOverlay.opacity = 100;
|
|
}
|
|
|
|
// Call the appropriate function to create the overlay
|
|
if (!!parserOptions.createOverlay) {
|
|
parserOptions.createOverlay(groundOverlay, doc);
|
|
} else {
|
|
createOverlay(groundOverlay, doc);
|
|
}
|
|
}
|
|
|
|
if (!!doc.bounds) {
|
|
doc.internals.bounds = doc.internals.bounds || new google.maps.LatLngBounds();
|
|
doc.internals.bounds.union(doc.bounds);
|
|
}
|
|
if (!!doc.styles || !!doc.markers || !!doc.overlays) {
|
|
doc.internals.parserOnly = 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 (!!doc.internals.bounds) {
|
|
parserOptions.map.fitBounds(doc.internals.bounds);
|
|
}
|
|
if (parserOptions.afterParse) {
|
|
parserOptions.afterParse(doc.internals.docSet);
|
|
}
|
|
|
|
if (!doc.internals.parserOnly) {
|
|
// geoXML3 is not being used only as a real-time parser, so keep the parsed documents around
|
|
docs.concat(doc.internals.docSet);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
var processStyles = function (doc) {
|
|
var stdRegEx = /\/(red|blue|green|yellow|lightblue|purple|pink|orange)(-dot)?\.png/;
|
|
for (var styleID in doc.styles) {
|
|
if (!!doc.styles[styleID].href) {
|
|
// Init the style object with a standard KML icon
|
|
doc.styles[styleID].icon = new google.maps.MarkerImage(
|
|
doc.styles[styleID].href,
|
|
new google.maps.Size(32, 32),
|
|
new google.maps.Point(0, 0),
|
|
new google.maps.Point(16, 12)
|
|
);
|
|
|
|
// Look for a predictable shadow
|
|
if (stdRegEx.test(doc.styles[styleID].href)) {
|
|
// A standard GMap-style marker icon
|
|
doc.styles[styleID].shadow = new google.maps.MarkerImage(
|
|
'http://maps.google.com/mapfiles/ms/micons/msmarker.shadow.png',
|
|
new google.maps.Size(59, 32),
|
|
new google.maps.Point(0, 0),
|
|
new google.maps.Point(16, 12));
|
|
} else if (doc.styles[styleID].href.indexOf('-pushpin.png') > -1) {
|
|
// Pushpin marker icon
|
|
doc.styles[styleID].shadow = new google.maps.MarkerImage(
|
|
'http://maps.google.com/mapfiles/ms/micons/pushpin_shadow.png',
|
|
new google.maps.Size(59, 32),
|
|
new google.maps.Point(0, 0),
|
|
new google.maps.Point(16, 12));
|
|
} else {
|
|
// Other MyMaps KML standard icon
|
|
doc.styles[styleID].shadow = new google.maps.MarkerImage(
|
|
doc.styles[styleID].href.replace('.png', '.shadow.png'),
|
|
new google.maps.Size(59, 32),
|
|
new google.maps.Point(0, 0),
|
|
new google.maps.Point(16, 12));
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
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.lat, placemark.point.lng),
|
|
title: placemark.name,
|
|
zIndex: Math.round(-placemark.point.lat * 100000),
|
|
icon: placemark.style.icon,
|
|
shadow: placemark.style.shadow
|
|
});
|
|
|
|
// Create the marker on the map
|
|
var marker = new google.maps.Marker(markerOptions);
|
|
|
|
// Set up and create the infowindow
|
|
var infoWindowOptions = geoXML3.combineOptions(parserOptions.infoWindowOptions, {
|
|
content: '<div class="infowindow"><h3>' + placemark.name +
|
|
'</h3><div>' + placemark.description + '</div></div>',
|
|
pixelOffset: new google.maps.Size(0, 2)
|
|
});
|
|
marker.infoWindow = new google.maps.InfoWindow(infoWindowOptions);
|
|
|
|
// Infowindow-opening event handler
|
|
google.maps.event.addListener(marker, 'click', function() {
|
|
if (!!parserOptions.singleInfoWindow) {
|
|
if (!!lastMarker && !!lastMarker.infoWindow) {
|
|
lastMarker.infoWindow.close();
|
|
}
|
|
lastMarker = this;
|
|
}
|
|
this.infoWindow.open(this.map, this);
|
|
});
|
|
|
|
if (!!doc) {
|
|
doc.markers = doc.markers || [];
|
|
doc.markers.push(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});
|
|
var overlay = new ProjectedOverlay(parserOptions.map, groundOverlay.icon.href, bounds, overlayOptions);
|
|
|
|
if (!!doc) {
|
|
doc.overlays = doc.overlays || [];
|
|
doc.overlays.push(overlay);
|
|
}
|
|
|
|
return
|
|
};
|
|
|
|
return {
|
|
// Expose some properties and methods
|
|
|
|
options: parserOptions,
|
|
docs: docs,
|
|
|
|
parse: parse,
|
|
hideDocument: hideDocument,
|
|
showDocument: showDocument,
|
|
processStyles: processStyles,
|
|
createMarker: createMarker,
|
|
createOverlay: createOverlay
|
|
};
|
|
};
|
|
// End of KML Parser
|
|
|
|
// Helper objects and functions
|
|
|
|
// Log a message to the debugging console, if one exists
|
|
geoXML3.log = function(msg) {
|
|
if (!!window.console) {
|
|
console.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 a text document from url and pass it to callback as a string
|
|
geoXML3.fetchers = [];
|
|
geoXML3.fetchXML = function (url, callback) {
|
|
function timeoutHandler() {
|
|
callback();
|
|
};
|
|
|
|
var xhrFetcher;
|
|
if (!!geoXML3.fetchers.length) {
|
|
xhrFetcher = geoXML3.fetchers.pop();
|
|
} else {
|
|
if (!!window.XMLHttpRequest) {
|
|
xhrFetcher = new window.XMLHttpRequest(); // Most browsers
|
|
} else if (!!window.ActiveXObject) {
|
|
xhrFetcher = new window.ActiveXObject('Microsoft.XMLHTTP'); // Some IE
|
|
}
|
|
}
|
|
|
|
if (!xhrFetcher) {
|
|
geoXML3.log('Unable to create XHR object');
|
|
callback(null);
|
|
} else {
|
|
xhrFetcher.open('GET', url, true);
|
|
xhrFetcher.onreadystatechange = function () {
|
|
if (xhrFetcher.readyState === 4) {
|
|
// Retrieval complete
|
|
if (!!xhrFetcher.timeout)
|
|
clearTimeout(xhrFetcher.timeout);
|
|
if (xhrFetcher.status >= 400) {
|
|
geoXML3.log('HTTP error ' + xhrFetcher.status + ' retrieving ' + url);
|
|
callback();
|
|
} else {
|
|
// Returned successfully
|
|
callback(xhrFetcher.responseXML);
|
|
}
|
|
// We're done with this fetcher object
|
|
geoXML3.fetchers.push(xhrFetcher);
|
|
}
|
|
};
|
|
xhrFetcher.timeout = setTimeout(timeoutHandler, 60000);
|
|
xhrFetcher.send(null);
|
|
}
|
|
};
|
|
|
|
//nodeValue: Extract the text value of a DOM node, with leading and trailing whitespace trimmed
|
|
geoXML3.nodeValue = function(node) {
|
|
if (!node) {
|
|
return '';
|
|
} else {
|
|
return (node.innerText || node.text || node.textContent).trim();
|
|
}
|
|
};
|