/** * @fileOverview Renders KML on the Google Maps JavaScript API Version 3 * @name GeoXML3 * @author Sterling Udell, Larry Ross, Brendan Byrd * @see http://code.google.com/p/geoxml3/ * * 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. * */ if (!String.prototype.trim) { /** * Remove leading and trailing whitespace. * * @augments String * @return {String} */ String.prototype.trim = function () { return this.replace(/^\s+|\s+$/g, ''); }; } /** * @namespace The GeoXML3 namespace. */ geoXML3 = window.geoXML3 || {instances: []}; /** * Constructor for the root KML parser object. * *

All top-level objects and functions are declared under a namespace of geoXML3. * The core object is geoXML3.parser; typically, you'll instantiate a one parser * per map.

* * @class Main XML parser. * @param {geoXML3.parserOptions} options */ geoXML3.parser = function (options) { // Inherit from Google MVC Object to include event handling google.maps.MVCObject.call(this); // Private variables var parserOptions = new geoXML3.parserOptions(options); var docs = []; // Individual KML documents var docsByUrl = {}; // Same docs as an hash by cleanURL var kmzMetaData = {}; // Extra files from KMZ data var styles = {}; // Global list of styles var lastPlacemark; var parserName; if (!parserOptions.infoWindow && parserOptions.singleInfoWindow) parserOptions.infoWindow = new google.maps.InfoWindow(); var parseKmlString = function (kmlString, docSet) { // Internal values for the set of documents as a whole var internals = { parser: this, docSet: docSet || [], remaining: 1, parseOnly: !(parserOptions.afterParse || parserOptions.processStyles) }; thisDoc = new Object(); thisDoc.internals = internals; internals.docSet.push(thisDoc); render(geoXML3.xmlParse(kmlString),thisDoc); } var parse = function (urls, docSet) { // Process one or more KML documents if (!parserName) { parserName = 'geoXML3.instances[' + (geoXML3.instances.push(this) - 1) + ']'; } if (typeof urls === 'string') { // Single KML document urls = [urls]; } // Internal values for the set of documents as a whole var internals = { parser: this, docSet: docSet || [], remaining: urls.length, parseOnly: !(parserOptions.afterParse || parserOptions.processStyles) }; var thisDoc, j; for (var i = 0; i < urls.length; i++) { var baseUrl = cleanURL(defileURL(location.pathname), urls[i]); if (docsByUrl[baseUrl]) { // Reloading an existing document thisDoc = docsByUrl[baseUrl]; thisDoc.reload = true; } else { thisDoc = new Object(); thisDoc.baseUrl = baseUrl; internals.docSet.push(thisDoc); } thisDoc.url = urls[i]; thisDoc.internals = internals; fetchDoc(thisDoc.url, thisDoc); } }; function fetchDoc(url, doc, resFunc) { resFunc = resFunc || function (responseXML) { render(responseXML, doc); }; if (typeof ZipFile === 'function' && typeof JSIO === 'object' && typeof JSIO.guessFileType === 'function') { // KMZ support requires these modules loaded contentType = JSIO.guessFileType(doc.baseUrl); if (contentType == JSIO.FileType.Binary || contentType == JSIO.FileType.Unknown) { doc.isCompressed = true; doc.baseDir = doc.baseUrl + '/'; geoXML3.fetchZIP(url, resFunc, doc.internals.parser); return; } } doc.isCompressed = false; doc.baseDir = defileURL(doc.baseUrl); geoXML3.fetchXML(url, resFunc); } var hideDocument = function (doc) { if (!doc) doc = docs[0]; // Hide the map objects associated with a document var i; 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$[name]\n
$[description]
\n
$[geDirections]
", displayMode: 'default' }, icon: { scale: 1.0, dim: { x: 0, y: 0, w: -1, h: -1 }, hotSpot: { x: 0.5, y: 0.5, xunits: 'fraction', yunits: 'fraction' } }, line: { color: 'ffffffff', // white (KML default) colorMode: 'normal', width: 1.0 }, poly: { color: 'ffffffff', // white (KML default) colorMode: 'normal', fill: true, outline: true } }; var kmlNS = 'http://www.opengis.net/kml/2.2'; var gxNS = 'http://www.google.com/kml/ext/2.2'; var nodeValue = geoXML3.nodeValue; var getBooleanValue = geoXML3.getBooleanValue; var getElementsByTagNameNS = geoXML3.getElementsByTagNameNS; var getElementsByTagName = geoXML3.getElementsByTagName; function processStyleUrl(node) { var styleUrlStr = nodeValue(getElementsByTagName(node, 'styleUrl')[0]); if (!!styleUrlStr && styleUrlStr.indexOf('#') != -1) var styleUrl = styleUrlStr.split('#'); else var styleUrl = ["",""]; return styleUrl; } function processStyle(thisNode, baseUrl, styleID, baseDir) { var style = (baseUrl === '{inline}') ? clone(defaultStyle) : (styles[baseUrl][styleID] = styles[baseUrl][styleID] || clone(defaultStyle)); var styleNodes = getElementsByTagName(thisNode, 'BalloonStyle'); if (!!styleNodes && styleNodes.length > 0) { style.balloon.bgColor = nodeValue(getElementsByTagName(styleNodes[0], 'bgColor')[0], style.balloon.bgColor); style.balloon.textColor = nodeValue(getElementsByTagName(styleNodes[0], 'textColor')[0], style.balloon.textColor); style.balloon.text = nodeValue(getElementsByTagName(styleNodes[0], 'text')[0], style.balloon.text); style.balloon.displayMode = nodeValue(getElementsByTagName(styleNodes[0], 'displayMode')[0], style.balloon.displayMode); } // style.list = (unsupported; doesn't make sense in Google Maps) var styleNodes = getElementsByTagName(thisNode, 'IconStyle'); if (!!styleNodes && styleNodes.length > 0) { var icon = style.icon; icon.scale = parseFloat(nodeValue(getElementsByTagName(styleNodes[0], 'scale')[0], icon.scale)); // style.icon.heading = (unsupported; not supported in API) // style.icon.color = (unsupported; not supported in API) // style.icon.colorMode = (unsupported; not supported in API) styleNodes = getElementsByTagName(styleNodes[0], 'hotSpot'); if (!!styleNodes && styleNodes.length > 0) { icon.hotSpot = { x: styleNodes[0].getAttribute('x'), y: styleNodes[0].getAttribute('y'), xunits: styleNodes[0].getAttribute('xunits'), yunits: styleNodes[0].getAttribute('yunits') }; } styleNodes = getElementsByTagName(thisNode, 'Icon'); if (!!styleNodes && styleNodes.length > 0) { icon.href = nodeValue(getElementsByTagName(styleNodes[0], 'href')[0]); icon.url = cleanURL(baseDir, icon.href); // Detect images buried in KMZ files (and use a base64 encoded URL) if (kmzMetaData[icon.url]) icon.url = kmzMetaData[icon.url].dataUrl; // Support for icon palettes and exact size dimensions icon.dim = { x: parseInt(nodeValue(getElementsByTagNameNS(styleNodes[0], gxNS, 'x')[0], icon.dim.x)), y: parseInt(nodeValue(getElementsByTagNameNS(styleNodes[0], gxNS, 'y')[0], icon.dim.y)), w: parseInt(nodeValue(getElementsByTagNameNS(styleNodes[0], gxNS, 'w')[0], icon.dim.w)), h: parseInt(nodeValue(getElementsByTagNameNS(styleNodes[0], gxNS, 'h')[0], icon.dim.h)) }; // certain occasions where we need the pixel size of the image (like the default settings...) // (NOTE: Scale is applied to entire image, not just the section of the icon palette. So, // if we need scaling, we'll need the img dimensions no matter what.) if (true /* (icon.dim.w < 0 || icon.dim.h < 0) && (icon.xunits != 'pixels' || icon.yunits == 'fraction') || icon.scale != 1.0 */) { // (hopefully, this will load by the time we need it...) icon.img = new Image(); icon.img.onload = function() { if (icon.dim.w < 0 || icon.dim.h < 0) { icon.dim.w = this.width; icon.dim.h = this.height; } else { icon.dim.th = this.height; } }; icon.img.src = icon.url; // sometimes the file is already cached and it never calls onLoad if (icon.img.width > 0) { if (icon.dim.w < 0 || icon.dim.h < 0) { icon.dim.w = icon.img.width; icon.dim.h = icon.img.height; } else { icon.dim.th = icon.img.height; } } } } } // style.label = (unsupported; may be possible but not with API) styleNodes = getElementsByTagName(thisNode, 'LineStyle'); if (!!styleNodes && styleNodes.length > 0) { style.line.color = nodeValue(getElementsByTagName(styleNodes[0], 'color')[0], style.line.color); style.line.colorMode = nodeValue(getElementsByTagName(styleNodes[0], 'colorMode')[0], style.line.colorMode); style.line.width = nodeValue(getElementsByTagName(styleNodes[0], 'width')[0], style.line.width); // style.line.outerColor = (unsupported; not supported in API) // style.line.outerWidth = (unsupported; not supported in API) // style.line.physicalWidth = (unsupported; unneccesary in Google Maps) // style.line.labelVisibility = (unsupported; possible to implement) } styleNodes = getElementsByTagName(thisNode, 'PolyStyle'); if (!!styleNodes && styleNodes.length > 0) { style.poly.color = nodeValue( getElementsByTagName(styleNodes[0], 'color')[0], style.poly.color); style.poly.colorMode = nodeValue( getElementsByTagName(styleNodes[0], 'colorMode')[0], style.poly.colorMode); style.poly.outline = getBooleanValue(getElementsByTagName(styleNodes[0], 'outline')[0], style.poly.outline); style.poly.fill = getBooleanValue(getElementsByTagName(styleNodes[0], 'fill')[0], style.poly.fill); } return style; } // 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; if (obj.cloneNode) return obj.cloneNode(true); var temp = new obj.constructor(); for(var key in obj) temp[key] = clone(obj[key]); return temp; } function processStyleMap(thisNode, baseUrl, styleID, baseDir) { var pairs = getElementsByTagName(thisNode, '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, '{inline}', '{inline}'); processStyleID(style); if (style) placemark.style = style; } if (/^https?:\/\//.test(placemark.description)) { placemark.description = ['', placemark.description, ''].join(''); } // record list of variables for substitution placemark.vars = { display: { name: 'Name', description: 'Description', address: 'Street Address', id: 'ID', Snippet: 'Snippet', geDirections: 'Directions' }, val: { name: placemark.name || '', description: placemark.description || '', address: nodeValue(getElementsByTagName(node, 'address')[0], ''), id: node.getAttribute('id') || '', Snippet: nodeValue(getElementsByTagName(node, 'Snippet')[0], '') }, directions: [ 'f=d', 'source=GeoXML3' ] }; // add extended data to variables var extDataNodes = getElementsByTagName(node, 'ExtendedData'); if (!!extDataNodes && extDataNodes.length > 0) { var dataNodes = getElementsByTagName(extDataNodes[0], 'Data'); for (var d = 0; d < dataNodes.length; d++) { var dn = dataNodes[d]; var name = dn.getAttribute('name'); if (!name) continue; var dName = nodeValue(getElementsByTagName(dn, 'displayName')[0], name); var val = nodeValue(getElementsByTagName(dn, 'value')[0]); placemark.vars.val[name] = val; placemark.vars.display[name] = dName; } } // process MultiGeometry var GeometryNodes = getElementsByTagName(node, '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 = getElementsByTagName(responseXML, 'GroundOverlay'); for (i = 0; i < groundNodes.length; i++) { node = groundNodes[i]; // Detect images buried in KMZ files (and use a base64 encoded URL) var gnUrl = cleanURL( doc.baseDir, nodeValue(getElementsByTagName(node, 'href')[0]) ); if (kmzMetaData[gnUrl]) gnUrl = kmzMetaData[gnUrl].dataUrl; // Init the ground overlay object groundOverlay = { name: nodeValue(getElementsByTagName(node, 'name')[0]), description: nodeValue(getElementsByTagName(node, 'description')[0]), icon: { href: gnUrl }, latLonBox: { north: parseFloat(nodeValue(getElementsByTagName(node, 'north')[0])), east: parseFloat(nodeValue(getElementsByTagName(node, 'east')[0])), south: parseFloat(nodeValue(getElementsByTagName(node, 'south')[0])), west: parseFloat(nodeValue(getElementsByTagName(node, 'west')[0])) } }; if (!!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 = getElementsByTagName(node, 'color'); if (colorNode && colorNode.length > 0) { groundOverlay.opacity = geoXML3.getOpacity(nodeValue(colorNode[0])); } else { groundOverlay.opacity = 1.0; // KML default } doc.groundoverlays.push(groundOverlay); if (!!parserOptions.createOverlay) { // User-defined overlay handler parserOptions.createOverlay(groundOverlay, doc); } else { // 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 (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 = getElementsByTagName(responseXML, 'NetworkLink'); for (i = 0; i < linkNodes.length; i++) { node = linkNodes[i]; // Init the network link object networkLink = { name: nodeValue(getElementsByTagName(node, 'name')[0]), link: { href: nodeValue(getElementsByTagName(node, 'href')[0]), refreshMode: nodeValue(getElementsByTagName(node, 'refreshMode')[0]) } }; // Establish the specific refresh mode if (!networkLink.link.refreshMode) { networkLink.link.refreshMode = 'onChange'; } if (networkLink.link.refreshMode === 'onInterval') { networkLink.link.refreshInterval = parseFloat(nodeValue(getElementsByTagName(node, 'refreshInterval')[0])); if (isNaN(networkLink.link.refreshInterval)) { networkLink.link.refreshInterval = 0; } } else if (networkLink.link.refreshMode === 'onChange') { networkLink.link.viewRefreshMode = nodeValue(getElementsByTagName(node, 'viewRefreshMode')[0]); if (!networkLink.link.viewRefreshMode) { networkLink.link.viewRefreshMode = 'never'; } if (networkLink.link.viewRefreshMode === 'onStop') { networkLink.link.viewRefreshTime = nodeValue(getElementsByTagName(node, 'refreshMode')[0]); networkLink.link.viewFormat = nodeValue(getElementsByTagName(node, '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) { 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; } if (!doc.internals.parseOnly) { // geoXML3 is not being used only as a real-time parser, so keep the processed documents around if (!docsByUrl[doc.baseUrl]) { docs.push(doc); docsByUrl[doc.baseUrl] = doc; } else { // internal replacement, which keeps the same memory ref loc in docs and docsByUrl for (var i in docsByUrl[doc.baseUrl]) { docsByUrl[doc.baseUrl][i] = doc[i]; } } } doc.internals.remaining--; 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); } google.maps.event.trigger(doc.internals.parser, 'parsed'); } }; var kmlColor = function (kmlIn, colorMode) { var kmlColor = {}; kmlIn = kmlIn || 'ffffffff'; // white (KML 2.2 default) var aa = kmlIn.substr(0,2); var bb = kmlIn.substr(2,2); var gg = kmlIn.substr(4,2); var rr = kmlIn.substr(6,2); kmlColor.opacity = parseInt(aa, 16) / 256; kmlColor.color = (colorMode === 'random') ? randomColor(rr, gg, bb) : '#' + rr + gg + bb; return kmlColor; }; // Implemented per KML 2.2 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) { var icon = style.icon; if (!icon || !icon.href) return; if (icon.img && !icon.img.complete && (icon.dim.w < 0) && (icon.dim.h < 0) ) { // we're still waiting on the image loading (probably because we've been blocking since the declaration) // so, let's queue this function on the onload stack icon.markerBacklog = []; icon.img.onload = function() { if (icon.dim.w < 0 || icon.dim.h < 0) { icon.dim.w = this.width; icon.dim.h = this.height; } else { icon.dim.th = this.height; } processStyleID(style); // we will undoubtedly get some createMarker queuing, so set this up in advance for (var i = 0; i < icon.markerBacklog.length; i++) { var p = icon.markerBacklog[i][0]; var d = icon.markerBacklog[i][1]; createMarker(p, d); if (p.marker) p.marker.active = true; } delete icon.markerBacklog; }; return; } else { //if (icon.dim.w < 0 || icon.dim.h < 0) { if (icon.img && icon.img.complete) { // sometimes the file is already cached and it never calls onLoad if (icon.dim.w < 0 || icon.dim.h < 0) { icon.dim.w = icon.img.width; icon.dim.h = icon.img.height; } else { icon.dim.th = icon.img.height; } } else { // settle for a default of 32x32 icon.dim.whGuess = true; icon.dim.w = 32; icon.dim.h = 32; icon.dim.th = 32; } } // pre-scaled variables var rnd = Math.round; var y = icon.dim.y; if (typeof icon.dim.th !== 'undefined' && icon.dim.th != icon.dim.h) { // palette - reverse kml y for maps y = Math.abs(y - (icon.dim.th - icon.dim.h)); } var scaled = { x: icon.dim.x * icon.scale, y: y * icon.scale, w: icon.dim.w * icon.scale, h: icon.dim.h * icon.scale, aX: icon.hotSpot.x * icon.scale, aY: icon.hotSpot.y * icon.scale, iW: (icon.img ? icon.img.width : icon.dim.w) * icon.scale, iH: (icon.img ? icon.img.height : icon.dim.h) * icon.scale }; // Figure out the anchor spot // Origins, anchor positions and coordinates of the marker increase in the X direction to the right and in // the Y direction down. var aX, aY; switch (icon.hotSpot.xunits) { case 'fraction': aX = rnd(scaled.aX * icon.dim.w); break; case 'insetPixels': aX = rnd(icon.dim.w * icon.scale - scaled.aX); break; default: aX = rnd(scaled.aX); break; // already pixels } aY = scaled.h - rnd( ((icon.hotSpot.yunits === 'fraction') ? icon.dim.h : 1) * scaled.aY ); // insetPixels Y = pixels Y var iconAnchor = new google.maps.Point(aX, aY); // Sizes // (NOTE: Scale is applied to entire image, not just the section of the icon palette.) var iconSize = icon.dim.whGuess ? null : new google.maps.Size(rnd(scaled.w), rnd(scaled.h)); var iconScale = icon.scale == 1.0 ? null : icon.dim.whGuess ? new google.maps.Size(rnd(scaled.w), rnd(scaled.h)) : new google.maps.Size(rnd(scaled.iW), rnd(scaled.iH)); var iconOrigin = new google.maps.Point(rnd(scaled.x), rnd(scaled.y)); // Detect images buried in KMZ files (and use a base64 encoded URL) if (kmzMetaData[icon.url]) icon.url = kmzMetaData[icon.url].dataUrl; // Init the style object with the KML icon icon.marker = { url: icon.url, // url size: iconSize, // size origin: iconOrigin, // origin anchor: iconAnchor, // anchor scaledSize: iconScale // scaledSize }; // 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(icon.href)) { // A standard GMap-style marker icon icon.shadow = { url: 'http://maps.google.com/mapfiles/ms/micons/msmarker.shadow.png', // url size: shadowSize, // size origin: null, // origin anchor: shadowPoint, // anchor scaledSize: shadowSize // scaledSize }; } else if (icon.href.indexOf('-pushpin.png') > -1) { // Pushpin marker icon icon.shadow = { url: 'http://maps.google.com/mapfiles/ms/micons/pushpin_shadow.png', // url size: shadowSize, // size origin: null, // origin anchor: shadowPoint, // anchor scaledSize: shadowSize // scaledSize }; } /* else { // Other MyMaps KML standard icon icon.shadow = new google.maps.MarkerImage( icon.href.replace('.png', '.shadow.png'), // url shadowSize, // size null, // origin anchorPoint, // anchor shadowSize // scaledSize ); } */ } 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 var icon = placemark.style.icon; if ( !icon.marker && icon.img ) { // yay, single point of failure is holding up multiple markers... icon.markerBacklog = icon.markerBacklog || []; icon.markerBacklog.push([placemark, doc]); return; } // 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: icon.marker, shadow: icon.shadow, flat: !icon.shadow, visible: placemark.visibility }); // 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 createInfoWindow(placemark, doc, marker); 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 path = []; var bounds = new google.maps.LatLngBounds(); for (var j=0; jTo Here - From Here'; } else vars.val.geDirections = ''; // add in the variables var iwText = bStyle.text.replace(/\$\[(\w+(\/displayName)?)\]/g, function(txt, n, dn) { return dn ? vars.display[n] : vars.val[n]; }); var classTxt = 'geoxml3_infowindow geoxml3_style_' + placemark.styleID; // color styles var styleArr = []; if (bStyle.bgColor != 'ffffffff') styleArr.push('background: ' + kmlColor(bStyle.bgColor ).color + ';'); if (bStyle.textColor != 'ff000000') styleArr.push('color: ' + kmlColor(bStyle.textColor).color + ';'); var styleProp = styleArr.length ? ' style="' + styleArr.join(' ') + '"' : ''; var infoWindowOptions = geoXML3.combineOptions(parserOptions.infoWindowOptions, { content: '
' + iwText + '
', pixelOffset: new google.maps.Size(0, 2) }); gObj.infoWindow = parserOptions.infoWindow || new google.maps.InfoWindow(infoWindowOptions); gObj.infoWindowOptions = infoWindowOptions; // Info Window-opening event handler google.maps.event.addListener(gObj, 'click', function(e) { var iW = this.infoWindow; iW.close(); iW.setOptions(this.infoWindowOptions); if (e && e.latLng) iW.setPosition(e.latLng); else if (this.bounds) iW.setPosition(this.bounds.getCenter()); iW.setContent("
"+iW.getContent()+"
"); google.maps.event.addListenerOnce(iW, "domready", function() { var node = document.getElementById('geoxml3_infowindow'); var imgArray = node.getElementsByTagName('img'); for (var i = 0; i < imgArray.length; i++) { var imgUrlIE = imgArray[i].getAttribute("src"); var imgUrl = cleanURL(doc.baseDir, imgUrlIE); if (kmzMetaData[imgUrl]) { imgArray[i].src = kmzMetaData[imgUrl].dataUrl; } else if (kmzMetaData[imgUrlIE]) { imgArray[i].src = kmzMetaData[imgUrlIE].dataUrl; } } }); iW.open(this.map, this.bounds ? null : this); }); } return { // Expose some properties and methods options: parserOptions, docs: docs, docsByUrl: docsByUrl, kmzMetaData: kmzMetaData, 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); } }; /** * Creates a new parserOptions object. * @class GeoXML3 parser options. * @param {Object} overrides Any options you want to declare outside of the defaults should be included here. * @property {google.maps.Map} map The API map on which geo objects should be rendered. * @property {google.maps.MarkerOptions} markerOptions If the parser is adding Markers to the map itself, any options specified here will be applied to them. * @property {google.maps.InfoWindowOptions} infoWindowOptions If the parser is adding Markers to the map itself, any options specified here will be applied to their attached InfoWindows. * @property {ProjectedOverlay.options} overlayOptions If the parser is adding ProjectedOverlays to the map itself, any options specified here will be applied to them. */ geoXML3.parserOptions = function (overrides) { this.map = null, /** If true, the parser will automatically move the map to a best-fit of the geodata after parsing of a KML document completes. * @type Boolean * @default true */ this.zoom = true, /**#@+ @type Boolean * @default false */ /** If true, only a single Marker created by the parser will be able to have its InfoWindow open at once (simulating the behavior of GMaps API v2). */ this.singleInfoWindow = false, /** If true, suppresses the rendering of info windows. */ this.suppressInfoWindows = false, /** * Control whether to process styles now or later. * *

By default, the parser only processes KML <Style> elements into their GMaps equivalents * if it will be creating its own Markers (the createMarker option is null). Setting this option * to true will force such processing to happen anyway, useful if you're going to be calling parser.createMarker * yourself later. OTOH, leaving this option false removes runtime dependency on the GMaps API, enabling * the use of geoXML3 as a standalone KML parser.

*/ this.processStyles = false, /**#@-*/ this.markerOptions = {}, this.infoWindowOptions = {}, this.overlayOptions = {}, /**#@+ @event */ /** This function will be called when parsing of a KML document is complete. * @param {geoXML3.parser#docs} doc Parsed KML data. */ this.afterParse = null, /** This function will be called when parsing of a KML document is complete. * @param {geoXML3.parser#docs} doc Parsed KML data. */ this.failedParse = null, /** * If supplied, this function will be called once for each marker in the KML document, instead of the parser adding its own Marker to the map. * @param {geoXML3.parser.render#placemark} placemark Placemark object. * @param {geoXML3.parser#docs} doc Parsed KML data. */ this.createMarker = null, /** * If supplied, this function will be called once for each in the KML document, instead of the parser adding its own ProjectedOverlay to the map. * @param {geoXML3.parser.render#groundOverlay} groundOverlay GroundOverlay object. * @param {geoXML3.parser#docs} doc Parsed KML data. */ this.createOverlay = null /**#@-*/ if (overrides) { for (var prop in overrides) { if (overrides.hasOwnProperty(prop)) this[prop] = overrides[prop]; } } return this; }; /** * Combine two options objects: a set of default values and a set of override values. * * @deprecated This has been replaced with {@link geoXML3.parserOptions#combineOptions}. * @param {geoXML3.parserOptions|Object} overrides Override values. * @param {geoXML3.parserOptions|Object} defaults Default values. * @return {geoXML3.parserOptions} Combined result. */ 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; }; /** * Combine two options objects: a set of default values and a set of override values. * * @function * @param {geoXML3.parserOptions|Object} overrides Override values. * @param {geoXML3.parserOptions|Object} defaults Default values. * @return {geoXML3.parserOptions} Combined result. */ geoXML3.parserOptions.prototype.combineOptions = geoXML3.combineOptions; // Retrieve an XML document from url and pass it to callback as a DOM document geoXML3.fetchers = []; /** * Parses a XML string. * *

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); } /** * Checks for XML parse error. * * @param {xmlDOM} XML DOM. * @return boolean. */ // 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; }; /** * Fetches a XML document. * *

Fetches/parses the given XML URL and passes the parsed document (in a * DOM data structure) to the given callback. Documents are downloaded * and parsed asynchronously.

* * @param {String} url URL of XML document. Must be uncompressed XML only. * @param {Function(Document)} callback Function to call when the document is processed. */ geoXML3.fetchXML = function (url, callback) { function timeoutHandler() { 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) { // Some IE // the many versions of IE's XML fetchers var AXOs = [ 'MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.5.0', 'MSXML2.XMLHTTP.4.0', 'MSXML2.XMLHTTP.3.0', 'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP', 'MSXML.XMLHTTP' ]; for (var i = 0; i < AXOs.length; i++) { try { xhrFetcher.fetcher = new ActiveXObject(AXOs[i]); break; } catch(e) { continue; } } if (!xhrFetcher.fetcher) { geoXML3.log('Unable to create XHR object'); callback(null); return null; } } 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(); } // Returned successfully else { if (xhrFetcher.fetcher.responseXML) { // Sometimes IE will get the data, but won't bother loading it as an XML doc var xml = xhrFetcher.fetcher.responseXML; if (xml && !xml.documentElement && !xml.ownerElement) { xml.loadXML(xhrFetcher.fetcher.responseText); } } else {// handle valid xml sent with wrong MIME type xml=geoXML3.xmlParse(xhrFetcher.fetcher.responseText); } // handle parse errors 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, 60000); xhrFetcher.fetcher.send(null); return null; }; var IEversion = function() { // http://msdn.microsoft.com/workshop/author/dhtml/overview/browserdetection.asp // Returns the version of Internet Explorer or a -1 // (indicating the use of another browser). var rv = -1; // Return value assumes failure if (navigator.appName == 'Microsoft Internet Explorer') { var ua = navigator.userAgent; var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})"); if (re.exec(ua) != null) { rv = parseFloat( RegExp.$1 ); } } return rv; }; /** * Fetches a KMZ document. * *

Fetches/parses the given ZIP URL, parses each image file, and passes * the parsed KML document to the given callback. Documents are downloaded * and parsed asynchronously, though the KML file is always passed after the * images have been processed, in case the callback requires the image data.

* * @requires ZipFile.complete.js * @param {String} url URL of KMZ document. Must be a valid KMZ/ZIP archive. * @param {Function(Document)} callback Function to call when the document is processed. * @param {geoXML3.parser} parser A geoXML3.parser object. This is used to populate the KMZ image data. * @author Brendan Byrd * @see http://code.google.com/apis/kml/documentation/kmzarchives.html */ geoXML3.fetchZIP = function (url, callback, parser) { // Just need a single 'new' declaration with a really long function... var zipFile = new ZipFile(url, function (zip) { // Retrieval complete // Check for ERRORs in zip.status for (var i = 0; i < zip.status.length; i++) { var msg = zip.status[i]; if (msg.indexOf("ERROR") == 0) { geoXML3.log('HTTP/ZIP error retrieving ' + url + ': ' + msg); callback(); return; } else if (msg.indexOf("WARNING") == 0) { // non-fatal, but still might be useful geoXML3.log('HTTP/ZIP warning retrieving ' + url + ': ' + msg); } } // Make sure KMZ structure is according to spec (with a single KML file in the root dir) var KMLCount = 0; var KML; for (var i = 0; i < zip.entries.length; i++) { var name = zip.entries[i].name; if (!/\.kml$/.test(name)) continue; KMLCount++; if (KMLCount == 1) KML = i; else { geoXML3.log('KMZ warning retrieving ' + url + ': found extra KML "' + name + '" in KMZ; discarding...'); } } // Returned successfully, but still needs extracting var baseUrl = cleanURL(defileURL(url), url) + '/'; var kmlProcessing = { // this is an object just so it gets passed properly timer: null, extractLeft: 0, timerCalls: 0 }; var extractCb = function(entry, entryContent) { var mdUrl = cleanURL(baseUrl, entry.name); var ext = entry.name.substring(entry.name.lastIndexOf(".") + 1).toLowerCase(); kmlProcessing.extractLeft--; if ((typeof entryContent.description == "string") && (entryContent.name == "Error")) { geoXML3.log('KMZ error extracting ' + mdUrl + ': ' + entryContent.description); callback(); return; } // MIME types that can be used in KML var mime; if (ext === 'jpg') ext = 'jpeg'; if (/^(gif|jpeg|png)$/.test(ext)) mime = 'image/' + ext; else if (ext === 'mp3') mime = 'audio/mpeg'; else if (ext === 'm4a') mime = 'audio/mp4'; else if (ext === 'm4a') mime = 'audio/MP4-LATM'; else mime = 'application/octet-stream'; parser.kmzMetaData[mdUrl] = {}; parser.kmzMetaData[mdUrl].entry = entry; // data:image/gif;base64,R0lGODlhEAAOALMA... parser.kmzMetaData[mdUrl].dataUrl = 'data:' + mime + ';base64,' + base64Encode(entryContent); // IE cannot handle GET requests beyond 2071 characters, even if it's an inline image if (/msie/i.test(navigator.userAgent) && !/opera/i.test(navigator.userAgent)) { if (((IEversion() < 8.0) && (parser.kmzMetaData[mdUrl].dataUrl.length > 2071)) || ((IEversion < 9.0) && (parser.kmzMetaData[mdUrl].dataUrl.length > 32767))) { parser.kmzMetaData[mdUrl].dataUrl = // this is a simple IE icon; to hint at the problem... 'data:image/gif;base64,R0lGODlhDwAQAOMPADBPvSpQ1Dpoyz1p6FhwvU2A6ECP63CM04CWxYCk+V6x+UK++Jao3rvC3fj7+v///yH5BAEKAA8ALAAAAAAPABAAAASC8Mk5mwCAUMlWwcLRHEelLA' + 'oGDMgzSsiyGCAhCETDPMh5XQCBwYBrNBIKWmg0MCQHj8MJU5IoroYCY6AAAgrDIbbQDGIK6DR5UPhlNo0JAlSUNAiDgH7eNAxEDWAKCQM2AAFheVxYAA0AIkFOJ1gBcQQaUQKKA5w7LpcEBwkJaKMUEQA7'; } } parser.kmzMetaData[internalSrc(entry.name)]=parser.kmzMetaData[mdUrl]; }; var kmlExtractCb = function(entry, entryContent) { if ((typeof entryContent.description == "string") && (entryContent.name == "Error")) { geoXML3.log('KMZ error extracting ' + mdUrl + ': ' + entryContent.description); callback(); return; } // check to see if the KML is the last file extracted clearTimeout(kmlProcessing.timer); if (kmlProcessing.extractLeft <= 1) { kmlProcessing.extractLeft--; callback(geoXML3.xmlParse(entryContent)); return; } else { // KML file isn't last yet; it may need to use those files, so wait a bit (100ms) kmlProcessing.timerCalls++; if (kmlProcessing.timerCalls < 100) { kmlProcessing.timer = setTimeout(function() { kmlExtractCb(entry, entryContent); }, 100); } else { geoXML3.log('KMZ warning extracting ' + url + ': entire ZIP has not been extracted after 10 seconds; running through KML, anyway...'); kmlProcessing.extractLeft--; callback(geoXML3.xmlParse(entryContent)); } } return; }; for (var i = 0; i < zip.entries.length; i++) { var entry = zip.entries[i]; var ext = entry.name.substring(entry.name.lastIndexOf(".") + 1).toLowerCase(); if (!/^(gif|jpe?g|png|kml)$/.test(ext)) continue; // not going to bother to extract files we don't support if (ext === "kml" && i != KML) continue; // extra KMLs get discarded if (!parser && ext != "kml") continue; // cannot store images without a parser object // extract asynchronously kmlProcessing.extractLeft++; if (ext === "kml") entry.extract(kmlExtractCb); else entry.extract(extractCb); } }); }; /** * Extract the text value of a DOM node, with leading and trailing whitespace trimmed. * * @param {Element} node XML node/element. * @param {Any} delVal Default value if the node doesn't exist. * @return {String|Null} */ geoXML3.nodeValue = function(node, defVal) { var retStr=""; if (!node) { return (typeof defVal === 'undefined' || defVal === null) ? 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;iRequired because IE8 doesn't define it.

* * @param {Element|Document} node DOM object. * @param {String} namespace Full namespace URL to search against. * @param {String} tagname XML local tag name. * @return {Array of Elements} * @author Brendan Byrd */ geoXML3.getElementsByTagNameNS = function(node, namespace, tagname) { if (node && typeof node.getElementsByTagNameNS != 'undefined') return node.getElementsByTagNameNS(namespace, tagname); if (!node) return []; var root = node.documentElement || node.ownerDocument && node.ownerDocument.documentElement; if (!root || !root.attributes) return []; // search for namespace prefix for (var i = 0; i < root.attributes.length; i++) { var attr = root.attributes[i]; if (attr.prefix === 'xmlns' && attr.nodeValue === namespace) return node.getElementsByTagName(attr.baseName + ':' + tagname); else if (attr.nodeName === 'xmlns' && attr.nodeValue === namespace) { // default namespace if (typeof node.selectNodes != 'undefined') { // Newer IEs have the SelectionNamespace property that can be used with selectNodes if (!root.ownerDocument.getProperty('SelectionNamespaces')) root.ownerDocument.setProperty('SelectionNamespaces', "xmlns:defaultNS='" + namespace + "'"); return node.selectNodes('.//defaultNS:' + tagname); } else { // Otherwise, you can still try to tack on the 'xmlns' attribute to root root.setAttribute('xmlns:defaultNS', namespace); return node.getElementsByTagName('defaultNS:' + tagname); } } } return geoXML3.getElementsByTagName(node, tagname); // try the unqualified version }; /** * Browser-normalized version of getElementsByTagName. * *

Required because MSXML 6.0 will treat this function as a NS-qualified function, * despite the missing NS parameter.

* * @param {Element|Document} node DOM object. * @param {String} tagname XML local tag name. * @return {Array of Elements} * @author Brendan Byrd */ geoXML3.getElementsByTagName = function(node, tagname) { if (node && typeof node.getElementsByTagNameNS != 'undefined') return node.getElementsByTagName(tagname); // if it has both functions, it should be accurate // if (node && typeof node.selectNodes != 'undefined') return node.selectNodes(".//*[local-name()='" + tagname + "']"); return node.getElementsByTagName(tagname); // hope for the best... } /** * Turn a directory + relative URL into an absolute one. * * @private * @param {String} d Base directory. * @param {String} s Relative URL. * @return {String} Absolute URL. * @author Brendan Byrd */ var toAbsURL = function (d, s) { var p, f, i; var h = location.protocol + "://" + location.host; if (!s.length) return ''; if (/^\w+:/.test(s)) return s; if (s.indexOf('/') == 0) return h + s; p = d.replace(/\/[^\/]*$/, ''); f = s.match(/\.\.\//g); if (f) { s = s.substring(f.length * 3); for (i = f.length; i--;) { p = p.substring(0, p.lastIndexOf('/')); } } return h + p + '/' + s; } var internalSrc = function(src) { //this gets the full url var url = document.location.href; //this removes everything after the last slash in the path url = url.substring(0,url.lastIndexOf("/") + 1); var internalPath= url+src; return internalPath; } /** * Remove current host from URL * * @private * @param {String} s Absolute or relative URL. * @return {String} Root-based relative URL. * @author Brendan Byrd */ var dehostURL = function (s) { var h = location.protocol + "://" + location.host; h = h.replace(/([\.\\\+\*\?\[\^\]\$\(\)])/g, '\\$1'); // quotemeta return s.replace(new RegExp('^' + h, 'i'), ''); } /** * Removes all query strings, #IDs, '../' references, and * hosts from a URL. * * @private * @param {String} d Base directory. * @param {String} s Absolute or relative URL. * @return {String} Root-based relative URL. * @author Brendan Byrd */ var cleanURL = function (d, s) { return dehostURL(toAbsURL(d ? d.split('#')[0].split('?')[0] : defileURL(location.pathname), s ? s.split('#')[0].split('?')[0] : '')); } /** * Remove filename from URL * * @private * @param {String} s Relative URL. * @return {String} Base directory. * @author Brendan Byrd */ var defileURL = function (s) { return s ? s.substr(0, s.lastIndexOf('/') + 1) : '/'; } // Some extra Array subs for ease of use // http://stackoverflow.com/questions/143847/best-way-to-find-an-item-in-a-javascript-array Array.prototype.hasObject = ( !Array.indexOf ? function (obj) { var l = this.length + 1; while (l--) { if (this[l - 1] === obj) return true; } return false; } : function (obj) { return (this.indexOf(obj) !== -1); } ); Array.prototype.hasItemInObj = function (name, item) { var l = this.length + 1; while (l--) { if (this[l - 1][name] === item) return true; } return false; }; if (!Array.prototype.indexOf) { Array.prototype.indexOf = function (obj, fromIndex) { if (fromIndex == null) { fromIndex = 0; } else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } for (var i = fromIndex, j = this.length; i < j; i++) { if (this[i] === obj) return i; } return -1; }; } Array.prototype.indexOfObjWithItem = function (name, item, fromIndex) { if (fromIndex == null) { fromIndex = 0; } else if (fromIndex < 0) { fromIndex = Math.max(0, this.length + fromIndex); } for (var i = fromIndex, j = this.length; i < j; i++) { if (this[i][name] === item) return i; } return -1; }; /** * Borrowed from jquery.base64.js, with some "Array as input" corrections * * @private * @param {Array of charCodes} input An array of byte ASCII codes (0-255). * @return {String} A base64-encoded string. * @author Brendan Byrd */ var base64Encode = function(input) { var keyString = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; var output = ""; var chr1, chr2, chr3, enc1, enc2, enc3, enc4; var i = 0; while (i < input.length) { chr1 = input[i++]; chr2 = input[i++]; chr3 = input[i++]; enc1 = chr1 >> 2; enc2 = ((chr1 & 3) << 4) | (chr2 >> 4); enc3 = ((chr2 & 15) << 2) | (chr3 >> 6); enc4 = chr3 & 63; if (chr2 == undefined) enc3 = enc4 = 64; else if (chr3 == undefined) enc4 = 64; output = output + keyString.charAt(enc1) + keyString.charAt(enc2) + keyString.charAt(enc3) + keyString.charAt(enc4); } return output; };