Raw Source
RaknalB / Geocaching Map Enhancements

// ==UserScript==
// @name        Geocaching Map Enhancements
// @version     0.8.0
// @author      JRI
// @oujs:author JRI
// @namespace   inge.org.uk/userscripts
// @description Adds extra maps and grid reference search to Geocaching.com, along with several other enhancements.
// @include     https://www.geocaching.com/*
// @license     MIT; http://www.opensource.org/licenses/mit-license.php
// @copyright   2011-17, James Inge (http://geo.inge.org.uk/)
// @attribution GeoNames (http://www.geonames.org/)
// @attribution Postcodes.io (https://postcodes.io/)
// @attribution Chris Veness (http://www.movable-type.co.uk/scripts/latlong-gridref.html)
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlHttpRequest
// @connect     geograph.org.uk
// @connect     channel-islands.geographs.org
// @connect     geo-en.hlipp.de
// @connect     api.geonames.org
// @connect     api.postcodes.io
// @connect     www.geocaching.com
// @icon        https://geo.inge.org.uk/userscripts/GeocachingMap48.png
// @icon64      https://geo.inge.org.uk/userscripts/GeocachingMap64.png
// @updateURL   https://geo.inge.org.uk/userscripts/GeocachingMapEnhancements.meta.js
// @downloadURL https://openuserjs.org/install/JRI/Geocaching_Map_Enhancements.user.js
// ==/UserScript==

/* jshint multistr: true */
/* global $, amplify, DMM, FileReader, GM, GM_xmlhttpRequest, Groundspeak, L, LatLon, mapLatLng, MapSettings */

(function () {
"use strict";

var gmeResources = {
	parameters: {
		// Defaults
		version: "0.8.0",
		versionMsg: "This is a maintenance update to remove the user survey (thanks for all your responses!) and make various bugfixes. There is also a new tool to let you hide or show all the caches on the map.",
		brightness: 1,	// Default brightness for maps (0-1), can be overridden by custom map parameters.
		filterFinds: false, // True filters finds out of list searches.
		follow: false,	// Locator widget follows current location (moving map mode)
		labels: "codes", // Label caches on the map with their GC code. Or "names" to use long name.
		measure: "metric",	// Or "imperial" - used for the scale indicators
		osgbSearch: true,	// Enhance search box with OSGB grid references, zooming, etc. (may interfere with postal code searches)
		defaultMap: "OpenStreetMap",
		maps: [
	//	{alt:"Readable Name", tileUrl: "URL template including {s} (subdomain) and either {q} (quadkey) or {x},{y},{z} (Google/TMS tile coordinates + zoom)", subdomains: "0123", minZoom: 0, maxZoom: 24, attribution: "Copyright message (HTML allowed)", name: "shortname", overlay:false }
			{alt:"OpenStreetMap",tileUrl:"https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",name:"osm",subdomains:"abc"},
			{alt:"OpenCycleMap",tileUrl:"https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png",name:"ocm"},
			{alt:"Bing Maps", tileUrl: "https://ecn.t{s}.tiles.virtualearth.net/tiles/r{q}?g=864&mkt=en-gb&lbl=l1&stl=h&shading=hill&n=z", subdomains: "0123", minZoom: 1, maxZoom: 20, attribution: "<a href=\'https://www.bing.com/maps/\'>Bing</a> map data copyright Microsoft and its suppliers", name: "bingmap",ignore:true},
			{alt:"Bing Aerial View", tileUrl:"https://ecn.t{s}.tiles.virtualearth.net/tiles/a{q}?g=737&n=z", subdomains: "0123", minZoom: 1, maxZoom: 20, attribution: "<a href=\'https://www.bing.com/maps/\'>Bing</a> map data copyright Microsoft and its suppliers", name: "bingaerial" },
			{alt:"Google Maps",tileUrl:"https://mt.google.com/vt?&x={x}&y={y}&z={z}",name:"googlemaps",attribution:"<a href=\'https://maps.google.com/\'>Google</a> Maps",subdomains:"1234",tileSize:256,maxZoom:22},
			{alt:"Google Satellite",tileUrl:"https://mt.google.com/vt?lyrs=s&x={x}&y={y}&z={z}",name:"googlemapssat",attribution:"<a href=\'https://maps.google.com/\'>Google</a> Maps Satellite",subdomains:"1234",tileSize:256,maxZoom:22},
			{alt:"Freemap Slovakia Hiking", tileUrl: "http://t{s}.freemap.sk/T/{z}/{x}/{y}.jpeg", attribution: "Map &copy; <a href='http://www.freemap.sk/'>Freemap Slovakia</a>, data &copy; <a href='http://openstreetmap.org'>OpenStreetMap</a> contributors", subdomains: "1234", minZoom: 8, maxZoom: 16, ignore: true},
			{alt:"Freemap Slovakia Bicycle", tileUrl: "http://t{s}.freemap.sk/C/{z}/{x}/{y}.jpeg", attribution: "Map &copy; <a href='http://www.freemap.sk/'>Freemap Slovakia</a>, data &copy; <a href='http://openstreetmap.org'>OpenStreetMap</a> contributors", subdomains: "1234", minZoom: 8, maxZoom: 16, ignore: true},
			{alt:"Freemap Slovakia Car", tileUrl: "http://t{s}.freemap.sk/A/{z}/{x}/{y}.jpeg", attribution: "Map &copy; <a href='http://www.freemap.sk/'>Freemap Slovakia</a>, data &copy; <a href='http://openstreetmap.org'>OpenStreetMap</a> contributors", subdomains: "1234", minZoom: 8, maxZoom: 16, ignore: true},
			{alt:"Hillshading", tileUrl:"http://{s}.tiles.wmflabs.org/hillshading/{z}/{x}/{y}.png", subdomains: "abc", attribution:"Hillshading by <a	 href=\'https://wiki.openstreetmap.org/wiki/Hike_%26_Bike_Map\'>Colin Marquardt</a> from NASA SRTM data", overlay: true}
		]
	},
	css: {
		main: '.leaflet-control-gme,.leaflet-control-zoomwarning {border-radius:7px; filter: progid:DXImageTransform.Microsoft.gradient(startColorStr="#3F000000",EndColorStr="#3F000000"); padding:5px;z-index:8;}\
			.leaflet-control-gme {display: inline-block; padding: 0; background: rgba(0, 0, 0, 0.2); box-shadow: 0 0 8px rgba(0, 0, 0, 0.4);}\
			.gme-control-scale {bottom:5em !important;margin-left:13px !important; left: 385px;}\
			.gme-left {left: 385px; margin-left:13px !important;}\
			div.gme-identify-layer {margin-top:-1em;margin-left:1em;padding-left:0.1em;font-weight:bold;background:rgba(255,255,255,0.57);}\
			#gme_caches table { margin-top: 0.5em; }\
			.GME_search_list { border: 1px solid #679300; border-radius: 7px; padding: 0.5em; }\
			div.GME_search_results { margin-right: -65px; }\
			.GME_search_results.hidden { display: none; }\
			.groundspeak-control-findmylocation { border: 1px solid #888; border-radius: 5px; box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); padding:0; background:rgba(255,255,255,0.8);}\
			.groundspeak-control-findmylocation a { padding: 3px; }\
			.gme-button { display: inline-block; box-sizing: content-box; -moz-box-sizing: content-box; padding:3px; vertical-align:middle; background:url(https://www.geocaching.com/map/css/themes/images/icons-18-black.png) no-repeat #eee; background-color: rgba(255,255,255,0.8); border: 1px solid #888; border-right:0; height:19px; width:19px; text-decoration: none; }\
			.gme-button-l { border-bottom-left-radius:5px; border-top-left-radius:5px; }\
			.gme-button-r { border-right: 1px solid #888; border-bottom-right-radius: 5px; border-top-right-radius:5px; margin-right:0.5em;}\
			.gme-button:hover { background-color: #fff; }\
			.gme-button-active {border:solid 3px #02b; padding:1px 0 1px 1px; background-color:#fff;}\
			.gme-button-active:hover {border-color:#63f;filter:alpha(opacity=100);}\
			span.gme-button, .gme-button-wide { padding-left:5px; padding-right:5px; font-size:12px; font-weight:bold; width:auto; background-image:none; color: #424242; }\
			.GME_home { background-position: -572px 4px;}\
			.GME_config { background-position: -284px 4px;}\
			.GME_route, .GME_hide {background: url(https://geo.inge.org.uk/userscripts/gme_icons_0_8_0.png) no-repeat #eee;}\
			.GME_route { background-position: 7px 3px;}\
			.GME_route.gme-button-active { background-position: 5px 1px;}\
			.GME_hide { background-position: -17px 3px;}\
			.GME_hide.gme-button-active { background-position: -19px 1px;}\
			.gme-button-refresh-labels { background-position: -320px 4px;}\
			.gme-button-clear-labels { background-position: -69px 4px;}\
			span.gme-distance-container { display: none; }\
			span.gme-distance-container.show { display: inline-block; }\
			.GME_info { background-position: -537px 4px;}\
			.GME_info.gme-button-active {background-position: -540px 1px;}\
			#GME_loc, a.gme-button.leaflet-active {outline: none;}\
			.leaflet-control-zoomwarning { top: 94px; }\
			.leaflet-control-zoomwarning a { filter: progid:DXImageTransform.Microsoft.gradient(startColorStr="#BFC80000",EndColorStr="#BFC80000"); background-color:rgba(200,0,0,0.75); margin-left: -4px; background-position: -502px 2px;height:14px;width:14px; border-color: #b00; box-shadow: 0 0 8px rgba(0, 0, 0, 0.4); }\
			.leaflet-control-zoomwarning a:hover { background-color:rgba(230,0,0,0.75); }\
			.gme-event { cursor: pointer; }\
			.gme-modalDialog {position: fixed; top: 0; right: 0; bottom: 0; left: 0; background: rgba(0,0,0,0.5); z-index: 1000; opacity:.5; -webkit-transition: opacity 400ms ease-in; -moz-transition: opacity 400ms ease-in; transition: opacity 400ms ease-in; pointer-events: none; display:none; }\
			.gme-modalDialog:target, .gme-modalDialog.gme-targetted { opacity:1; display:block; pointer-events: auto; }\
			.gme-modalDialog > div { position: relative; margin: 4% 12.5%; height: 30em; max-height: 75%; padding: 0 0 13px 0; border: 1px solid #000; border-radius: 10px; background: #fff; background: -moz-linear-gradient(#fff, #999); background: -webkit-linear-gradient(#fff, #999); background: -o-linear-gradient(#fff, #999); }\
			.gme-modalDialog header { color: #eee; background: none #454545; font-size: 15px; text-align: center; border-top-left-radius: 10px; padding: 0.5em 0; font-weight: bold; text-shadow: none; height: auto; min-height: auto; min-width: auto; }\
			.gme-modal-content { position: absolute; top: 3.5em; left: 0.75em; right: 0.75em; bottom: 0.5em; overflow: auto; }\
			.gme-modal-content > .leaflet-control-gme { position: absolute; left: 0.5em; bottom: 0.5em; top: auto; }\
			.gme-close-dialog { background: #606061; color: #fff; line-height: 25px; position: absolute; right: -12px; text-align: center; top: -10px; width: 24px; text-decoration: none; font-weight: bold; -webkit-border-radius: 12px; -moz-border-radius: 12px; border-radius: 12px; -moz-box-shadow: 1px 1px 3px #000; -webkit-box-shadow: 1px 1px 3px #000; box-shadow: 1px 1px 3px #000; }\
			.gme-close-dialog:hover { background: #00d9ff; }\
			#searchtabs li a { padding: 1em 0.5em; }\
			@media print { #search { display: none !important}}\
			.tab-switcher { position: relative; font-family: Arial, sans-serif; font-size: 14px; }\
			.gme-tab { float: left; }\
			.gme-tab-label { border-radius: 8px 8px 0 0; border: 1px solid #ccc; color: #454545; background: #ddd; display: block; position: relative; margin-left: 15px; padding: 3px 0; font-weight: bold; z-index: 0; }\
			.gme-tab-label:after { border-bottom: 1px solid #ccc; border-bottom-left-radius: 8px; border-left: 1px solid #ccc; box-shadow: -2px 2px 0 #ddd; bottom: -8px; content: ""; display: inline-block; height: 8px; left: 9px; position: relative; width: 8px; z-index: 3;}\
			.gme-tab-label:before { border-bottom: 1px solid #ccc; border-bottom-right-radius: 8px; border-right: 1px solid #ccc; box-shadow: 2px 2px 0 #ddd; bottom: -8px; content: ""; display: inline-block; height: 8px; left: -9px; position: relative; width: 8px; z-index: 3; }\
			.gme-tab-label:hover { cursor: pointer; }\
			.gme-tab-content { position: absolute; top: 25px; bottom: 3.5em; left: 0; right: 0; padding: 0.5em; background: #000; border: 1px solid #ccc; border-radius: 8px; color: #555; z-index: 1; opacity: 0; overflow: auto; }\
			.gme-tab-content ul { margin: 0.5em 0; }\
			.gme-tab input[type=radio] { display: none; }\
			.gme-tab input[type=radio]:checked ~ .gme-tab-content { z-index: 2; opacity: 1; background: #fff; color: #454545; }\
			.gme-tab input[type=radio]:checked ~ .gme-tab-label { background: #fff; color: #454545; border-bottom: 1px solid #fff; z-index: 3; }\
			.gme-tab input[type=radio]:checked ~ .gme-tab-label:after { box-shadow: -2px 2px 0 #fff; }\
			.gme-tab input[type=radio]:checked ~ .gme-tab-label:before { box-shadow: 2px 2px 0 #fff; }\
			.gme-fieldgroup { position: relative; border: 1px solid #ccc; border-radius: 6px; background: #eee; margin: 0.5em 0 1.5em; padding: 0.5em; }\
			.gme-fieldgroup h3 { position:absolute; top: -0.7em; left: 0.5em; padding: 0 0.5em; background: #eee; border-top: 1px solid #ccc; border-radius: 6px; z-index:1; display:inline-block; font-weight: bold; font-size: 12px; }\
			.gme-fieldgroup ul { margin: 0.5em 0; padding: 0; }\
			.gme-fieldgroup li { display: inline-block; margin: 0 -1px -1px 0; background: #ddd; border: 1px solid #ccc; border-radius: 6px; padding: 0 0.5em; }\
			.gme-xhair { cursor: crosshair; }\
			.map-button-container { margin-right: 5em; }',
		drag: '#cacheDetails .cacheImage { border: solid 1px #ccc; border-radius: 7px; padding-left: 5px; }\
			.moveable { cursor: move; box-shadow: 0 1px 4px rgba(102, 51, 255, 0.3); }'
	},
	env: {
		dragdrop: (document.createElement('span').draggable !== undefined),
		geolocation: !!navigator.geolocation,
		init: [],
		loggedin: (!!document.getElementById("ctl00_uxLoginStatus_divSignedIn") || !!document.getElementById("uxLoginStatus_divSignedIn")),
		page: "default",
		storage: false,
		xhr: (typeof GM_xmlhttpRequest === 'function') ? 'GM' : ((typeof GM === 'object' && typeof GM.xmlHttpRequest === 'function') ? 'GM4': '')
	},
	html: {
		config: '<section class="gme-tab">\
				<input type="radio" name="gme-tab-row" id="gme-tab-maps" checked />\
				<label class="gme-tab-label" for="gme-tab-maps">Map display</label>\
				<div class="gme-tab-content">\
					<div class="gme-fieldgroup">\
						<h3>Maps to show in selector widget</h3>\
						<ul id="GME_mapfields"></ul>\
						<label>Default map source: &nbsp;<select name="GME_map_default" id="GME_map_default"></select></label>\
					</div>\
				</div>\
			</section>\
			<section class="gme-tab">\
				<input type="radio" name="gme-tab-row" id="gme-tab-manage" />\
				<label class="gme-tab-label" for="gme-tab-manage">Manage maps</label>\
				<div class="gme-tab-content">\
					<div class="gme-fieldgroup">\
						<h3>Add map sources</h3>\
						<label>Mapsource: <input type="text" name="GME_map_custom" id="GME_map_custom">&nbsp;</label>\
						<div class="leaflet-control-gme"><button type="button" id="GME_custom_add" class="gme-button gme-button-wide gme-button-l gme-button-r" title="Add custom map source">Add</button> <a href="#GME_format" title="Map source format info" class="gme-button gme-button-wide gme-button-l">Mapsource format info</a><button type="button" id="GME_custom_export" title="Export custom map source JSON" class="gme-button gme-button-wide gme-button-r">Export custom maps</button></div>\
					</div>\
					<div class="gme-fieldgroup">\
						<h3>Remove map sources</h3>\
						<ul id="GME_mapfields_del"></ul>\
					</div>\
				</div>\
			</section>\
			<section class="gme-tab">\
				<input type="radio" name="gme-tab-row" id="gme-tab-other" />\
				<label class="gme-tab-label" for="gme-tab-other">Other</label>\
				<div class="gme-tab-content">\
					<div class="gme-fieldgroup">\
						<h3>Miscellaneous settings</h3>\
						<ul>\
							<li><label title="Only list unfound caches in search"><input type="checkbox" name="GME_filterFinds" id="GME_filterFinds" /> Filter finds</label></li>\
							<li><label><input type="checkbox" checked="checked" name="GME_osgbSearch" id="GME_osgbSearch" /> Enhance search</label></li>\
							<li><label title="Location widget constantly updates position"><input type="checkbox" name="GME_follow" id="GME_follow" /> FollowMe Mode</label></li>\
						</ul>\
						<label>Labels:\
							<select name="GME_labelStyle" id="GME_labelStyle">\
								<option value="names">Names</option>\
								<option value="codes" selected="selected">Codes</option>\
							</select>\
						</label>\
						<label>Scale:\
							<select name="GME_measure" id="GME_measure">\
								<option value="metric" selected="selected">Metric</option>\
								<option value="imperial">Imperial</option>\
							</select>\
						</label>\
						<label>Map brightness:\
							<input type="range" name="GME_brightness" id="GME_brightness" value="100" min="0" max="100" />\
						</label>\
					</div>\
				</div>\
			</section>\
			<section class="gme-tab">\
				<input type="radio" name="gme-tab-row" id="gme-tab-about" />\
				<label class="gme-tab-label" for="gme-tab-about">About</label>\
				<div class="gme-tab-content">\
					<div class="gme-fieldgroup">\
						<h3>Geocaching Map Enhancements</h3><br />\
						<p>v<span id="GME_version"></span> &copy; 2011-2016 James Inge. Geocaching Map Enhancements is licensed for reuse under the <a target="_blank" rel="noopener noreferrer" href="http://www.opensource.org/licenses/mit-license.php">MIT License</a>. For documentation, see <a target="_blank" rel="noopener noreferrer" href="http://geo.inge.org.uk/gme.htm">http://geo.inge.org.uk/gme.htm</a></p>\
						<p>Elevation and reverse geocoding data provided by <a target="_blank" rel="noopener noreferrer" href="http://www.geonames.org/">GeoNames</a> and used under a <a target="_blank" rel="noopener noreferrer" href="https://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution 3.0</a> (CC-BY) License.</p>\
						<p>Grid reference manipulation is adapted from code &copy; 2005-2014 Chris Veness (<a target="_blank" rel="noopener noreferrer" href="http://www.movable-type.co.uk/scripts/latlong-gridref.html">www.movable-type.co.uk/scripts/latlong-gridref.html</a>, used under a <a target="_blank" rel="noopener noreferrer" href="https://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution 3.0</a> (CC-BY) License.</p>\
						<p>Photos provided by Geograph are copyright their respective owners - hover mouse over thumbnails or click through for attribution details. They may be re-used under a <a target="_blank" rel="noopener noreferrer" href="https://creativecommons.org/licenses/by-sa/2.0/">Creative Commons Attribution-ShareAlike 2.0</a> (CC-BY-SA) License.</p>\
					</div>\
				</div>\
			</section>\
			<div class="leaflet-control-gme">\
				<a href="#" class="gme-button gme-button-wide gme-button-l" rel="back" title="Cancel">Cancel</a><button type="button" class="gme-button gme-button-wide" id="GME_default" title="Reset to defaults">Defaults</button><button type="button" class="gme-button gme-button-wide gme-button-r" id="GME_set" title="Confirm settings">Save</button>\
			</div>',
		customInfo: '<p>Custom mapsources can be added by supplying entering a <a rel="external" href="http://www.json.org/">JSON</a> configuration string that tells GME what to call the map, where to find it, and how it is set up. e.g.</p>\
			<p><code>{"alt":"OS NPE (GB only)","tileUrl":"https://ooc.openstreetmap.org/npe/{z}/{x}/{y}.png", "minZoom":6, "maxZoom": 15, "attribution": "OpenStreetMap NPE" }</code></p>\
			<p>The <code>"alt"</code> and <code>"tileUrl"</code> parameters are mandatory. <code>"tileUrl"</code> can contain {x}, {y} and {z} for Google-style coordinate systems (also works with TMS systems like Eniro, but needs the <code>"scheme":"tms"</code> parameter), or {q} for Bing-style quadkeys. GME can also connect with WMS servers, in which case a <code>"layers"</code> parameter is required.</p>\
			<p>The other parameters are the same as those used by the <a rel="external" href="http://leafletjs.com/reference-versions.html">Leaflet API</a>, with the addition of a <code>"overlay":true</code> option, that makes the mapsource appear as a selectable overlay.</p>\
			<ul><li><a rel="external" href="http://geo.inge.org.uk/gme_config.htm">Detailed documentation</a></li><li><a rel="external" href="http://geo.inge.org.uk/gme_maps.htm">More mapsource examples</a></li></ul>',
		search: '<input type="text" placeholder="Address, coordinates, GC-code, keyword, etc." id="SearchBox_Text" title="Jump to a specific zoom level by typing zoom then a number. Zoom 1 shows the whole world, maxiumum zoom is normally 18-22. To search using a British National Grid reference, just type it in the search box and hit the button! You can use 2, 4, 6, 8 or 10-digit grid refs with the 2-letter prefix but no spaces in the number (e.g. SU12344225) or absolute grid refs with a comma but no prefix (e.g. 439668,1175316)." />\
			<button id="SearchBox_OS" title="Search">Search</button>\
			<div class="GME_search_results hidden">\
				<h3 class="GME_search_heading">GeoNames search results</h3>\
				<ul class="GME_search_list"></ul>\
				<p>Or try the <a href="#" class="GME_link_GSSearch">Geocaching.com search</a>\
			</div>'
	},
	script: {
		common: function () {
			var that = this, callbackCount = 0, load_count = 0, JSONP;
			function setEnv() {
				// The script waits for the Leaflet API to load, and will abort if it does not find it after a minute.
				var maxTries = 60,
					wait = 1000;
				switch (gmeConfig.env.page) {
				case "seek":
					if (typeof $ === "function") {
						gmeInit(gmeConfig.env.init);
						load();
						return;
					}
					break;
				case "hide":
					maxTries = Infinity;
					wait = 3000;
					if (window.map !== null && window.map !== undefined && typeof L === "object" && typeof $ === "function") {
						gmeInit(gmeConfig.env.init);
						window.setTimeout(load,500);
						return;
					}
					break;
				case "type":
					if (window.map !== null && window.map !== undefined && typeof L === "object" && typeof $ === "function") {
						gmeInit(gmeConfig.env.init);
						load();
						reload();
						$(".cache-type-selector button").click(reload);
						return;
					}
					break;
				case "maps":
					// Wait for the map to load and the default map selector to be added.
					if (typeof L === "object" && typeof $ === "function" && window.MapSettings && window.MapSettings.Map && window.MapSettings.Map._loaded && $(".leaflet-control-layers").length > 0) {
						gmeInit(gmeConfig.env.init);
						window.setTimeout(load,500);
						return;
					}
					break;
				default:
					if (typeof L === "object" && typeof $ === "function") {
						gmeInit(gmeConfig.env.init);
						window.setTimeout(load,500);
						return;
					}
					break;
				}

				if (load_count < maxTries) {
					window.setTimeout(setEnv, wait);
					load_count++;
					console.log("GME: Waiting for map API to load: " + load_count + "...");
				}
			}
			function gmeInit(scriptArray) {
				// Init routines that need either JQuery or Leaflet API, so must be run from load() rather than on script insertion.
				var initScripts = {
					"config": function () {
						if (gmeConfig.env.storage) {
							setConfig();
							$("#GME_set").bind("click", storeSettings);
							$("#GME_default").bind("click", setDefault);
							$("#GME_custom_add").bind("click", addCustom);
							$("#GME_custom_export").bind("click", exportCustom);
							$("li.li-user ul").append("<li class='li-settings'><a class='icon-settings' id='gme-config-link' href='#GME_config' title='Configure Geocaching Map Enhancements extension'>Geocaching Map Enhancements</a></li>");
						}
					},
					"drop": function () {
						$.fn.filterNode = function(name) {
							return this.find("*").filter(function () {
								return this.nodeName === name;
							});
						};
						L.GME_dropHandler = L.Control.extend(dropHandlerObj);
					},
					"map": function () {
						bounds_GB = new L.LatLngBounds(new L.LatLng(49,-9.5),new L.LatLng(62,2.3));
						bounds_IE = new L.LatLngBounds(new L.LatLng(51.2,-12.2),new L.LatLng(55.73,-5.366));
						bounds_NI = new L.LatLngBounds(new L.LatLng(54,-8.25),new L.LatLng(55.73,-5.25));
						bounds_CI = new L.LatLngBounds(new L.LatLng(49.1,-2.8),new L.LatLng(49.8,-1.8));
						bounds_DE = new L.LatLngBounds(new L.LatLng(47.24941,5.95459),new L.LatLng(55.14121,14.89746));
						L.GME_DistLine = L.Polyline.extend(polylineObj); 
						L.GME_QuadkeyLayer = L.TileLayer.extend(quadkeyLayerObj);
						L.GME_complexLayer = L.TileLayer.extend(complexLayerObj);
						L.GME_genericLayer = genericLayerFn;
					},
					"widget": function () {
						L.GME_Widget=L.Control.extend(widgetControlObj);
						if (window.Groundspeak && Groundspeak.Map && Groundspeak.Map.Control && Groundspeak.Map.Control.FindMyLocation) {
							L.GME_FollowMyLocationControl=Groundspeak.Map.Control.FindMyLocation.extend(locationControlObj);
						}
						L.GME_ZoomWarning=L.Control.extend(zoomWarningObj);
						if (L.LatLng.prototype.toUrl === undefined) {
							L.LatLng.prototype.toUrl = function() { return this.lat.toFixed(6) + "," + this.lng.toFixed(6); };
						}
						if ($.fancybox === undefined) {
							console.info("GME: Fetching Fancybox");
							$("head").append("<link rel='stylesheet' type='text/css' href='https://cdnjs.cloudflare.com/ajax/libs/fancybox/2.1.5/jquery.fancybox.min.css'><script type='text/javascript' src='https://cdnjs.cloudflare.com/ajax/libs/fancybox/2.1.5/jquery.fancybox.min.js'></script>");
						}
					}
				},
				j;
				
				for (j = 0; j < scriptArray.length; j++) {
					if (initScripts.hasOwnProperty(scriptArray[j]) && typeof initScripts[scriptArray[j]] === "function") {
						initScripts[scriptArray[j]]();
					}
				}
				console.log("GME init: " + scriptArray.join());
			}
			function b64encode(str) {
				if (typeof window.btoa === "function") {
					return btoa(encodeURIComponent(str));
				} else {
					return encodeURIComponent(str);
				}
			}
			function b64decode(str) {
				if (typeof window.atob === "function") {
					return decodeURIComponent(window.atob(str));
				} else {
					return decodeURIComponent(str);
				}
			}
			function DMM(ll) {
				var latDeg = ll.lat < 0 ? Math.ceil(ll.lat) : Math.floor(ll.lat),
					lngDeg = ll.lng < 0 ? Math.ceil(ll.lng) : Math.floor(ll.lng);
				return (ll.lat < 0 ? "S" : "N") + Math.abs(latDeg) + " " + (60 * Math.abs((ll.lat - latDeg))).toFixed(3) + (ll.lng < 0 ? " W" : " E") + Math.abs(lngDeg) + " " + (60 * Math.abs((ll.lng - lngDeg))).toFixed(3);
			}
			function formatDistance(dist) {
				var formatted = 0;
				if (that.parameters.measure === "metric") {
					if (dist > 10000) {
						formatted = Math.round(dist/1000) + " km";
					} else {
						if (dist > 1000) {
							formatted = (dist/1000).toFixed(1) + " km";
						} else {
							formatted = Math.round(dist)+" m";
						}
					}
				} else {
					if (dist > 16093.44) {
						formatted = Math.round(dist/1609.344) + " mi";
					} else {
						if (dist > 1609.344) {
							formatted = (dist/1609.344).toFixed(1) + " mi";
						} else {
							formatted = Math.round(dist * 3.2808)+" ft";
						}
					}
				}
				return formatted;
			}
			function htmlEntities(text) {
				return text
					.replace(/&/g, "&amp;")
					.replace(/\"/g, "&quot;")
					.replace(/'/g, "&apos;")
					.replace(/</g, "&lt;")
					.replace(/>/g, "&gt;");
			}
			function validCoords(c1, c2) {
				var lat, lng;
				if (c1 === undefined) {
					return false;
				}
				if (c1.hasOwnProperty("lat") && c1.hasOwnProperty("lng")) {
					lat = c1.lat;
					lng = c1.lng;
				} else {
					if (c2 !== undefined) {
						lat = c1;
						lng = c2;
					}
				}
				if (lat !== null && lng !== null && !isNaN(+lat) && !isNaN(+lng) && lat >= -90 && lat <= 90) {
					return true;
				}
				return false;
			}
			function parseCoords(text) {
				var lat=0, lng=0, num=0,
				c = text.replace(/[^\-SsWw0-9\.\s]/g," ").trim().match(/^([S\-])?\s*(\d{1,2}(\.\d*){0,1}|\.\d*)(\s+(\d{0,2}(\.\d*){0,1})){0,1}(\s+(\d{0,2}(\.\d*){0,1})){0,1}\s*([S\-])?\s+([W\-])?\s*(\d{1,3}(\.\d*){0,1}|\.\d*)(\s+(\d{0,2}(\.\d*){0,1})){0,1}(\s+(\d{0,2}(\.\d*){0,1})){0,1}\s*([W\-])?$/i);
				if (c) {
					num = (c[2]?1:0) + (c[5]?1:0) + (c[8]?1:0) + (c[12]?1:0) + (c[15]?1:0) + (c[18]?1:0);
					switch(num) {
						case 6:
							break;
						case 4:
							if (c[15] === undefined) { c[15] = c[12]; c[12] = c[8]; c[8] = undefined; }
							break;
						case 2:
							if (c[12] === undefined && c[5]) { c[12] = c[5]; c[5] = undefined; }
							break;
						default:
							alert("Couldnt understand coordinates");
							return false;
					}
					if (c[2] !== undefined) { lat = +c[2]; }
					if (c[5] !== undefined) { lat += c[5]/60; }
					if (c[8] !== undefined) { lat += c[8]/3600; }
					if (c[1] !== undefined || c[10] !== undefined) { lat *= -1; }
					if (c[12] !== undefined) { lng = +c[12]; }
					if (c[15] !== undefined) { lng += c[15]/60; }
					if (c[18] !== undefined) { lng += c[18]/3600; }
					if (c[11] !== undefined || c[20] !== undefined) { lng *= -1; }
				}
				if (validCoords(lat, lng)) {
					return {lat:lat,lng:lng};
				}
				alert("Invalid coordinates");
				return false;
			}
			function getHomeCoords() {
				var c, h = document.getElementById("ctl00_ContentBody_lnkPrintDirectionsSimple");
				if (window.MapSettings && MapSettings.User && validCoords(MapSettings.User.Home)) {
					return new L.LatLng(MapSettings.User.Home.lat, MapSettings.User.Home.lng);
				}
				if (validCoords(window.homeLat, window.homeLon)) {
					return new L.LatLng(window.homeLat, window.homeLon);
				}
				if (h && h.href) {
					c = h.href.match(/(?:saddr=)(-?\d{1,2}\.\d*),(-?\d{1,3}\.\d*)/);
					if (c !== null && c.length === 3 && validCoords(c[1], c[2])) {
						return new L.LatLng(c[1], c[2]);
					}
				}
				return false;
			}
			function validURL(url) {
				return (/^(http|https|ftp)\:\/\/([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&amp;%\$\-]+)*@)*((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|localhost|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(\:[0-9]+)*(\/($|[a-zA-Z0-9\.\,\?'\\\+&amp;%\$#\=~_\-]+))*$/).test(url);
			}

			if (window.console === undefined) {
                var logFn = function (text) {};
				window.console = {
                    error: logFn,
                    log: logFn,
                    info: logFn,
                    warn: logFn
                };
			}
			
			if (gmeConfig.env.xhr) {
				JSONP = function (url, id) {
					console.log("GME: Using GM_xhr to fetch " + url);
					var s = document.getElementById("gme_jsonp_node");
					if (!s) {
						s = document.createElement("script");
						s.id = "gme_jsonp_node";
						document.documentElement.firstChild.appendChild(s);
					}
					s.type = "text/x-gme-jsonp";
					s.text = url;
					s.setAttribute("data-gme-callback", id);
					document.dispatchEvent(new Event("GME_XHR_event"));
				};
				document.addEventListener("GME_XHR_callback", function (e) {
					var s = document.getElementById("gme_jsonp_node"),
						callback = s.getAttribute("data-gme-callback");
					if (typeof window[callback] === "function") {
						try {
							window[callback](JSON.parse(s.text));
						} catch (e) {
							console.error("Error processing JSON callback " + callback + ": " + e);
						}
					} else {
						console.warn("Unexpected request to JSON callback handler: Couldn't find callback function " + callback);
					}
					return false;
				});
			} else {
				JSONP = function (url, id) {
					console.log("GME: Using JSONP to fetch " + url);
					if (validURL(url)) {
						var s=document.createElement("script");
						s.type="text/javascript";
						if (id) { s.id=id; }
						s.src=url;
						document.documentElement.firstChild.appendChild(s);
					}
				};
			}

			gmeConfig.env.home = getHomeCoords();
			
			this.parameters = gmeConfig.parameters;
			this.getVersion = function() { return gmeConfig.parameters.version; };
			this.getGeograph = function(coords) {
				var callprefix="GME_geograph_callback", call, host = "";
				function searchLink(coords) {
					// URIs for website search pages.
					if (coords === undefined) { return false; }
					var host="";
					if (bounds_GB.contains(coords) || bounds_IE.contains(coords)) {
						host="https://geograph.org.uk/";
					}
					if (bounds_CI.contains(coords)) {
						host="https://www.geograph.org.gg/";
					}
					if (bounds_DE.contains(coords)) {
						host="https://geo-en.hlipp.de/";
					}
					return host?[host,"search.php?location=", coords.toUrl()].join(""):false;
				}
				function makeCallback(callname) { callbackCount++; return function(json) {
					var html, i, p;
					if (json.items && json.items.length>0) {
						html = ["<h3>Geograph images near ", DMM(coords), "</h3><p>"].join("");
						for (i=json.items.length-1;i>=0;i--) {
							p = json.items[i];
							html += ["<a target='_blank' rel='noopener noreferrer' href='",encodeURI(p.link),"' style='margin-right:0.5em;' title='", htmlEntities(p.title) + " by " + htmlEntities(p.author), "'>",p.thumbTag,"</a>"].join("");
						}
						html += ["</p><p><a target='_blank' rel='noopener noreferrer' href='",searchLink(coords),"'>Search for more photos nearby on Geograph</a></p><p style='font-size:90%;'>Geograph photos are copyrighted by their owners and available under a <a href='https://creativecommons.org/licenses/by-sa/2.0/'>Creative Commons licence</a>. Hover mouse over thumbnails for more details, or click through for full images.</p>"].join("");
						$.fancybox(html);
					} else {
						$.fancybox(["<p>No photos found nearby. <a target='_blank' rel='noopener noreferrer' href='",searchLink(coords),"'>Search on Geograph</a></p>"].join(""));
					}
					$("#"+callname).remove();
					if (window[callname] !== undefined) { delete window[callname]; }
				};}
				if (validCoords(coords) && this.isGeographAvailable(coords)) {
					if (!bounds_CI.contains(coords) && (bounds_GB.contains(coords) || bounds_IE.contains(coords))) {
						host="https://api.geograph.org.uk/";
                        call = callprefix + callbackCount;
                        window[call] = makeCallback(call);
                        JSONP(host + "syndicator.php?key=geo.inge.org.uk&location=" + coords.toUrl() + "&format=JSON&callback=" + call, call);
					} else {
						window.open(searchLink(coords), "_blank");
					}
				} else {
					console.error("GME: Bad coordinates to getGeograph");
				}
			};
			this.getHeight = function(coords) {
				var callprefix="GME_height_callback",call;
				function makeCallback(callname) { callbackCount++; return function (json) {
					if (typeof json.astergdem === "number" && typeof json.lat === "number" && typeof json.lng === "number") {
						var h, m;
						if (json.astergdem === -9999) {
							m = "<p><strong>Spot Height</strong><br/>(Ocean)</p>";
						} else {
							h = that.parameters.measure==="metric"?json.astergdem+" m":Math.round(json.astergdem*3.2808)+" ft";
							m = ["<p><strong>Spot Height</strong><br/>Approx ",h," above sea level</p>"].join("");
						}
						$.fancybox(m);
					}
					$("#"+callname).remove();
					if (window[callname] !== undefined) { delete window[callname]; }
				};}
				if (validCoords(coords)) {
					call = callprefix + callbackCount;
					window[call] = makeCallback(call);
					JSONP(["http://api.geonames.org/astergdemJSON?lat=",coords.lat,"&lng=",coords.lng,"&username=gme&callback=",call].join(""), call);
				} else {
					console.error("GME: Bad coordinates to getHeight");
				}
			};
			this.isGeographAvailable = function(coords) {
				return	bounds_GB.contains(coords) || bounds_DE.contains(coords) || bounds_IE.contains(coords) || bounds_CI.contains(coords);
			};
			this.isInUK = function(coords) {
				if (bounds_GB.contains(coords)) {
					if (bounds_IE.contains(coords)) {
						if (bounds_NI.contains(coords)) {
							return true;
						}
						return false;
					}
					return true;
				}
				return false;
			};
			if (gmeConfig.env.geolocation) {
				this.seekHere = function() {
					function hereCallback(pos) {
						that.seekByLatLng({lat:pos.coords.latitude, lng:pos.coords.longitude});
						$("#GME_hereSub").val("Go");
					}
					function hereError(err) {
						if (err.code === 2) {
							alert("Current location not available");
						}
						if (err.code === 3) {
							alert("Timed out finding current location");
						}
						$("#GME_hereSub").val("Go");
					}
					$("#GME_hereSub").val("Waiting for location...");
					navigator.geolocation.getCurrentPosition(hereCallback, hereError, {timeout: 60000, maximumAge: 30000});
					return false;
				};
			}
			this.seekByLatLng = function(latlng) {
				if (validCoords(latlng)) {
					var url = ["https://www.geocaching.com/seek/nearest.aspx?origin_lat=",latlng.lat,"&origin_long=",latlng.lng, that.parameters.filterFinds?"&f=1":""].join("");
					window.open(url, "_blank");
				} else {
					console.error("GME: Invalid coordinates for search");
				}
			};
		},
		config: function () {
			function addSources(json){
				function setSrc(src) {
					if (src.alt && src.tileUrl) {
						var m = that.parameters.maps.concat(src);
						that.parameters.maps = m;
						return 1;
					}
					alert("Map source must include at least \"alt\" and \"tileUrl\" parameters");
					return 0;
				}
				var i,updated=0;
				if (json.length === undefined) {
					updated += setSrc(json);
				} else {
					for (i = 0; i < json.length; i++) {
						updated += setSrc(json[i]);
					}
				}
				if (updated > 0) {
					setConfig();
					$("#gme-tab-maps")[0].checked = true;
				}
			}
			function addCustom(){
				try{
					var n = JSON.parse(document.getElementById("GME_map_custom").value);
					addSources(n);
				} catch (e) {
					alert("Map source string must be valid JSON.");
					return;
				}
			}
			function exportCustom(){
				$.fancybox($("<p/>").text(JSON.stringify(that.parameters.maps)).html());
			}
			function setDefault(){
				if (localStorage.GME_custom) { delete localStorage.GME_custom; }
				if (localStorage.GME_parameters) { delete localStorage.GME_parameters; }
				if (localStorage.GME_cache) { delete localStorage.GME_cache; }
				refresh();
			}
			function refresh(config) {
				var dest = "https://www.geocaching.com/map/#",
					mapLink = document.getElementById("map_linkto"),
					uri;
				if (config) {
					dest += "GME_config";
				}
				if (mapLink) {
					uri = mapLink.value;
					if (uri) {
						dest += uri.replace(/^http:\/\/coord.info\/map/, "");
					}
					document.location.href = dest;
				} else {
					document.location.hash = "";
				}
				window.location.reload(false);
				return false;
			}
			function setConfig() {
				var i, mapfields = "", mapfields_del = "", mapselect = "", alt = "", overlay, sel, allMaps = that.parameters.maps;
				for (i = 0; i < allMaps.length; i++) {
					alt = allMaps[i].alt;
					overlay = allMaps[i].overlay;
					if (!overlay) { mapselect += "<option value='" + htmlEntities(alt) + "'>" + htmlEntities(alt) + "</option>"; }
					mapfields += "<li><label><input type='checkbox' " + (allMaps[i].ignore ? "" : "checked='checked' ") + "name='" + htmlEntities(alt) + "' id='checkbox-" + i + "' /> " + htmlEntities(alt) + (overlay ? " (Overlay)" : "") + "</label></li>";
				}
				if (allMaps.length > 0) {
					for (i = 0; i < allMaps.length; i++) {
						alt = allMaps[i].alt;
						mapfields_del += "<li><label><input type='checkbox' name='" + htmlEntities(alt) + "' id='checkbox-del-" + i + "' /> " + htmlEntities(alt) + (allMaps[i].overlay ? " (Overlay)" : "") + "</label></li>";
					}
				} else {
					mapfields_del = "&lt; No custom maps installed &gt;";
				}
				$("#GME_mapfields").html(mapfields);
				$("#GME_mapfields_del").html(mapfields_del);
				$("#GME_map_default").html(mapselect);
				sel = $("#GME_map_default").children();
				for (i = sel.length - 1; i > -1; i--) {
					if (sel[i].value === that.parameters.defaultMap) {
						sel[i].selected = "selected";
					}
				}
				$("#GME_filterFinds").attr("checked", that.parameters.filterFinds);
				$("#GME_osgbSearch").attr("checked", that.parameters.osgbSearch);
				$("#GME_follow").attr("checked", that.parameters.follow);
				$("#GME_labelStyle").val(that.parameters.labels);
				$("#GME_measure").val(that.parameters.measure);
				$("#GME_brightness").val(that.parameters.brightness * 100);
				$("#GME_version").html(that.parameters.version);
			}
			function storeSettings(){
				var i, j, list;
				that.parameters.defaultMap = $("#GME_map_default")[0].value;
				list = $("#GME_mapfields input");
				for (i = list.length - 1; i >= 0; i--) {
					for (j = that.parameters.maps.length - 1; j >= 0; j--) {
						if (that.parameters.maps[j].alt === list[i].name) {
							that.parameters.maps[j].ignore = !list[i].checked;
						}
					}
				}
				for (j = that.parameters.maps.length - 1;j >= 0; j--) {
					if (that.parameters.maps[j].alt === that.parameters.defaultMap) {
						that.parameters.maps[j].ignore = false;
					}
				}
				list = $("#GME_mapfields_del input");
				for (i = list.length - 1; i >= 0; i--) {
					if (list[i].checked === true) {
						for (j = that.parameters.maps.length - 1; j >= 0; j--) {
							if (that.parameters.maps[j].alt === list[i].name) {
								that.parameters.maps.splice(j,1);
								break;
							}
						}
					}
				}
				that.parameters.brightness = $("#GME_brightness").val() / 100;
				that.parameters.filterFinds = $("#GME_filterFinds")[0].checked ? true : false;
				that.parameters.follow = $("#GME_follow")[0].checked ? true : false;
				that.parameters.labels = $("#GME_labelStyle")[0].value;
				that.parameters.measure = $("#GME_measure")[0].value;
				that.parameters.osgbSearch = $("#GME_osgbSearch")[0].checked? true : false;
				localStorage.setItem("GME_parameters", JSON.stringify(that.parameters));
				refresh();
			}
		},
		cssTransitionsFix: function () {
		//	<bugfix>
			// Work around bug that breaks JQuery Mobile dialog boxes in Opera 12.
			if (window.$ && $.support) {
				$.support.cssTransitions = false;
			}
		//	</bugfix>
		},
		dist: function () {
			$("#lblDistFromHome").parent().append("<br/><span id='gme-dist'><a href='#' id='gme-dist-link'>Check distance from here</a></span>");
			$("#gme-dist-link").click(function () {
				var there = new LatLon(mapLatLng.lat, mapLatLng.lng),
					rose = [[22.5,67.5,112.5,157.5,202.5,247.5,292.5,337.5],["N","NE","E","SE","S","SW","W","NW"]],
					watcher;
				function found(pos) {
					var here = new LatLon(pos.coords.latitude, pos.coords.longitude),
					bearing = here.bearingTo(there),
					dir = "N", i;
					for (i = 0; i < 8; i++){
						if (bearing < rose[0][i]){
							dir = rose[1][i];
							break;
						}
					}
					$("#gme-dist").html("<img style='vertical-align:text-bottom' alt='" + dir + "' src='/images/icons/compass/" + dir + ".gif'> " + dir + " " + formatDistance(here.distanceTo(there)*1000) + " from here at bearing " + Math.round(bearing) + "&deg;");
				}
				function lost() {
					if (watcher) {
						navigator.geolocation.clearWatch(watcher);
					}
					alert("GME couldn't detect your location.\nDisable FollowMe mode in Geocaching Map Enhancements if this error pops up repeatedly.");
				}
				if (that.parameters.follow) {
					watcher = navigator.geolocation.watchPosition(found, lost, {timeout: 60000, maximumAge: 30000});
				} else {
					navigator.geolocation.getCurrentPosition(found, lost, {timeout: 60000, maximumAge: 30000});
				}
				return false;
			});
		},
		drag: function () {
			that.dragStart = function (event) {
				function GME_formatLOC(wpts) {
					return wpts ? ['<?xml version="1.0" encoding="UTF-8"?>\n<loc version="1.0" src="Geocaching Map Enhancements v' + that.getVersion() + '">' + wpts.join('\n') + '</loc>'].join('\n'):null;
				}
				function GME_formatLOC_wpt(id, desc, coords, type, link) {
					if (id && desc && coords) {
						var t="Geocache",
							l=link ? ('\n\t<link text="' + link.desc + '">' + link.href + '</link>') : "";
						switch(type) {
							case "Original Coordinates": t=type; break;
							case 217: t="Parking Area"; break;
							case 218: t="Question to Answer"; break;
							case 219: t="Stages of a Multicache"; break;
							case 220: t="Final Location"; break;
							case 221: t="Trailhead"; break;
							case 452: t="Reference Point"; break;
						}
						return ('<waypoint>\n\t<name id="' + id + '"><![CDATA[' + desc + ']]></name>\n\t<coord lat="' + coords.lat + '" lon="' + coords.lng + '"/>\n\t<type>' + t + '</type>' + l + '\n</waypoint>');
					}
					console.warn("GME: Missing cache data - id:", id , "desc:", desc, "coords:", coords);
					return null;
				}
				var c, dataURI, dt, i, locfmt,
					id = $("#ctl00_ContentBody_CoordInfoLinkControl1_uxCoordInfoCode")[0].innerHTML,
					loc = [GME_formatLOC_wpt(id, cache_coords.primary[0].name, cache_coords.primary[0], cache_coords.primary[0].type,{desc:"Cache Details",href:"https://coord.info/"+id})];
				for (i = cache_coords.additional.length-1; i >= 0; i--) {
					c = cache_coords.additional[i];
					loc.push(GME_formatLOC_wpt(c.pf + id.slice(2), [c.name,$("#awpt_"+c.pf).parent().parent().next().children()[2].innerHTML.trim()].join(" "), c, c.type));
				}
				if (cache_coords.primary[0].isUserDefined) {
					loc.push(GME_formatLOC_wpt("GO"+id.slice(2), cache_coords.primary[0].name, {lat:cache_coords.primary[0].oldLatLng[0], lng:cache_coords.primary[0].oldLatLng[1]}, "Original Coordinates",{desc:"Cache Details",href:"https://coord.info/"+id}));
				}
				locfmt = GME_formatLOC(loc);
				dataURI = "data:application/xml-loc," + encodeURIComponent(locfmt);
				dt = event.originalEvent.dataTransfer;
				if (window.DataTransfer !== undefined && dt.constructor === window.DataTransfer) {
					dt.setData("application/gme-cache-coords",JSON.stringify(cache_coords));
					dt.setData("application/xml-loc", locfmt);
					dt.setData("text/x-moz-url",dataURI+"\nGME_waypoints.loc");
					dt.setData("DownloadURL","application/xml-loc:GME_waypoints.loc:"+dataURI);
				}
				dt.setData("text/uri-list",dataURI);
				dt.setData("Text",locfmt);
				dt.effectAllowed="copy";
				dt.setDragImage($('a[title="About Cache Types"] img')[0],0,0);
			};
		},
		drop: function () {
			var dropHandlerObj = {
				onAdd:function (map) {
					var container = $(map.getContainer());
					this._map = map;
					container.on("drop",this.drop(map));
					container.on("dragover",this.dragOver);
					return document.createElement("div");
				},
				onRemove:function (map) {
					var container = $(map.getContainer());
					container.off("drop",this.drop(map));
					container.off("dragover",this.dragOver);
				},
				drop:function (map) { return function (e) {
					function typeToIcon(t) {
						var j, type=t;
						for (j = wptTypes.length - 1; j>=0; j--) {
							type = type.replace(wptTypes[j][0],wptTypes[j][1]);
						}
						return type;
					}
					function parseLOC(text) {
						var i, l, w, t, len, lat, lng, name, points={primary:[], additional:[]}, wpts = $($.parseXML(text)).find("waypoint");
						for (i=0,len=wpts.length;i<len;i++){
							w=$(wpts[i]);
							lat=w.find("coord").attr("lat");
							lng=w.find("coord").attr("lon");
							name=w.find("name").attr("id") + ": " + w.find("name").text().trim();
							if (isNaN(+lat)||isNaN(+lng)||lat < -90||lat >90) { return false; }
							t = w.find("type").text();
							if (/Geocache/i.test(t)) {
								points.primary.push({ lat:lat, lng:lng, name:name, type:2 });
							} else {
								l = points.primary.length;
								if (l && /Original Coordinates/i.test(t)) {
									points.primary[l-1].oldLatLng = [lat, lng];
									points.primary[l-1].isUserDefined = true;
								} else {
									points.additional.push({ lat:lat, lng:lng, name:name, type:typeToIcon(t) });
								}
							}
						}
						return(points.additional.length + points.primary.length > 0)?points:false;
					}
					function readLOC(e) {
						var data = e.target.result, pts = parseLOC(data);
						if (pts) {
							console.info("GME: Received LOC file");
							GME_displayPoints(pts,map,"dragdrop");
						}
					}
					function parseGPX(text) {
						var d, i, j, k, w, r, t, lat, lng, len, n, name="", poly, type, points={primary:[], additional:[], routes:[]}, gpx = $($.parseXML(text)), wpts = gpx.find("wpt"), tracks = gpx.find("trk"), segs, routes = gpx.find("rte");
						for (i=0,len=wpts.length;i<len;i++){
							w=$(wpts[i]);
							lat=w.attr("lat");
							lng=w.attr("lon");
							n=w.filterNode("name");
							d=w.filterNode("desc");
							name=n.length>0?n[0].textContent:"Point "+i;
							name+=(n.length>0&&d.length>0)?" : ":"";
							name+=d.length>0?d[0].textContent:"";
							if (isNaN(+lat)||isNaN(+lng)||lat < -90||lat >90) { return false;}
							t = w.find("sym").text();
							if (/Geocache/i.test(t)) {
								t = w.filterNode("groundspeak:type");
								if (t.length > 0) {
									type = t[0].textContent;
								} else {
									type = "Geocache";
								}
							} else {
								type = t;
							}
							points[/Geocache/i.test(t)?"primary":"additional"].push({ lat:lat, lng:lng, name:name, type:typeToIcon(type) });
						}
						for (i=routes.length-1;i>=0;i--) {
							poly = [];
							r = $(routes[i]);
							n=r.filterNode("name");
							name=n.length>0?n[0].textContent:"Route "+i;
							wpts = r.find("rtept");
							for (j = wpts.length-1;j>=0;j--) {
								w = $(wpts[j]);
								poly.push(new L.LatLng(w.attr("lat"), w.attr("lon")));
							}
							points.routes.push({name:name, points:poly});
						}
						for (i=tracks.length-1;i>=0;i--) {
							poly = [];
							r = $(tracks[i]);
							segs = r.find("trkseg");
							for (j=segs.length-1;j>=0;j--) {
								n = r.filterNode("name");
								name =[(n.length>0)?n[0].textContent:"Track " + i, " segment ", j].join("");
								wpts = $(segs[j]).find("trkpt");
								for (k = wpts.length-1;k>=0;k--) {
									w = $(wpts[k]);
									poly.push(new L.LatLng(w.attr("lat"), w.attr("lon")));
								}
								points.routes.push({name:name, points:poly});
							}
						}
						return(points.additional.length + points.primary.length + points.routes.length> 0)?points:false;
					}
					function readGPX(e) {
						var data = e.target.result, pts = parseGPX(data);
						if (pts) {
							console.info("GME: Received GPX file");
							GME_displayPoints(pts,map,"dragdrop");
						}
					}
					e.stopPropagation();
					e.preventDefault();
					var i, data, dt=e.originalEvent.dataTransfer, file, files = dt.files, pts, reader;
					try {
						data = dt.getData("application/gme-cache-coords");
						if (data) {
							console.info("GME: Received GME data");
							GME_displayPoints(JSON.parse(data),map,"dragdrop");
							return;
						}
						data = dt.getData("text/plain");
						if (data) {
							pts = parseLOC(data);
							if (pts) {
								console.info("GME: Received LOC text");
								GME_displayPoints(pts,map,"dragdrop");
								return;
							}
							pts = parseGPX(data);
							if (pts) {
								console.info("GME: Received GPX text");
								GME_displayPoints(pts,map,"dragdrop");
								return;
							}
						}
					} catch (E) { console.warn("GME: Drop: " + E); }
					for (i=files.length-1; i>=0; i--){
						file = files[i];
						if (/application\/xml-loc/.test(file.type)||/\.loc$/i.test(file.name)) {
							reader = new FileReader();
							reader.onload = readLOC;
							reader.readAsText(file);
						} else {
							if (/application\/xml-gpx/.test(file.type)||/\.gpx$/i.test(file.name)) {
								reader = new FileReader();
								reader.onload = readGPX;
								reader.readAsText(file);
							} else {
								console.warn("GME: Dropped file not recognised: " + file.name + ", (type: " + file.type + ")");
							}
						}
					}
				};},
				dragOver:function (e) {
					var dt=e.originalEvent.dataTransfer;
					function contains(array, value) {
						if (array.indexOf) {
							return array.indexOf(value) >= 0;
						}
						if (array.contains) {
							return array.contains(value);
						}
						console.warn("GME: couldn\'t determine type of dragged data. Accepting anyway.");
						return true;
					}
					if (dt && dt.types) {
						try {
							if (contains(dt.types, "application/gme-cache-coords") || contains(dt.types,"application/xml-gpx") || contains(dt.types,"application/xml-loc") || contains(dt.types,"text/plain") || contains(dt.types,"Files")){
								e.preventDefault();
								return false;
							}
						} catch (e) {
							console.error("GME: dragOver:", e);
						}
					}
				}
			};
		},
		loadDefault: function () {
			if (typeof window.$ === "function") {
				gmeInit(gmeConfig.env.init);
			}
		},
		labels: function () {
			function GME_load_labels(control, div) {
				function labelHandler() {
					var action = this.getAttribute("data-gme-action"), cache = this.getAttribute("data-gme-cache");
					switch (action) {
						case "panTo":
							if (control.labels.labels[cache]) {
								control._map.panTo(control.labels.labels[cache][2]);
							}
							break;
						case "refresh":
							control.labels.refresh();
							break;
						case "clear":
							control.labels.removeLabels();
							break;
						case "auto":
							control.labels.toggleAuto();
							break;
						case "show":
							control.labels.toggleShow();
							break;
					}
					return false;
				}
				L.GME_identifyLayer = L.Class.extend({
					initialize: function (latlng, options) {
						L.Util.setOptions(this, options);
						this._latlng = latlng;
					},
					onAdd: function (map) {
						this._map = map;
						this._el = L.DomUtil.create("div", "gme-identify-layer leaflet-zoom-hide");
						this._el.innerHTML = this.options.label;
						this._el.title = this.options.desc;
						this._el.style.position = "absolute";
						map.getPanes().overlayPane.appendChild(this._el);
						map.on("viewreset", this._reset, this);
						this._reset();
					},
					onRemove: function (map) {
						map.getPanes().overlayPane.removeChild(this._el);
						map.off("viewreset", this._reset, this);
					},
					options: {
						label: "Cache",
						desc: "Long cache name"
					},
					setPosition: function (ll) {
						this._latlng = ll;
						this._reset();
					},
					_reset: function () {
						if (this._map) {
							var pos = this._map.latLngToLayerPoint(this._latlng);
							L.DomUtil.setPosition(this._el, pos);
						}
					}
				});
				control.labels = {
					showLabels: false,
					autoUpdate: false,
					labels: {},
					labelLayer: new L.LayerGroup(),
					clearLabels:function () {
						control._map.removeLayer(control.labels.labelLayer);
					},
					displayLabels:function () {
						control._map.addLayer(control.labels.labelLayer);
					},
					refresh:function () {
						if (!(window.MapSettings && MapSettings.MapLayers && MapSettings.MapLayers.UTFGrid)) {
							return;
						}
						var i, coords, tiles = $(".leaflet-tile-pane .leaflet-layer img[src*='geocaching.com/map.png']");
						for (i = tiles.length-1; i>=0; i--) {
							coords = tiles[i].src.match(/x=(\d+)&y=(\d+)&z=(\d+)/);
							if (coords && !MapSettings.MapLayers.UTFGrid._cache.hasOwnProperty(([coords[3],coords[1],coords[2]].join("_")))) {
								MapSettings.MapLayers.UTFGrid._loadTile(coords[3],coords[1],coords[2]);
							}
						}
						setTimeout(control.labels.refreshLabels, 500);
					},
					refreshLabels:function () {
						var c, p, q, r, tile, tilepos, tileref, gridref, zoom = control._map.getZoom();
						if (!(window.MapSettings && MapSettings.MapLayers && MapSettings.MapLayers.UTFGrid)) {
							return;
						}
						for (p in MapSettings.MapLayers.UTFGrid._cache) {
							if (MapSettings.MapLayers.UTFGrid._cache.hasOwnProperty(p)) {
								tileref = p.split("_");
								if (tileref.length === 3) {
									tile = $("img[src*='x=" + tileref[1] + "&y=" + tileref[2] + "&z=" + tileref[0] + "']")[0];
									if (tile) {
										tilepos = L.DomUtil.getPosition(tile);
										if (MapSettings.MapLayers.UTFGrid._cache[p]) {
											for (q in MapSettings.MapLayers.UTFGrid._cache[p].data) {
												if (MapSettings.MapLayers.UTFGrid._cache[p].data.hasOwnProperty(q)) {
													for (r in MapSettings.MapLayers.UTFGrid._cache[p].data[q]) {
														if (MapSettings.MapLayers.UTFGrid._cache[p].data[q].hasOwnProperty(r)) {
															c = MapSettings.MapLayers.UTFGrid._cache[p].data[q][r];
															if (!control.labels.labels[c.i]) {
																gridref = q.match(/\((\d+), (\d+)\)/);
																if (gridref) {
																	control.labels.labels[c.i] = [ c.i, c.n, control._map.layerPointToLatLng(tilepos.add(new L.Point(4*gridref[1], 4*gridref[2]))), zoom];
																	control.labels.labels[c.i][4] = new L.GME_identifyLayer(control.labels.labels[c.i][2], (that.parameters.labels === "names") ? {label:c.n, desc:c.i} : {label:c.i, desc:c.n});
																}
															} else {
																if (zoom > control.labels.labels[c.i][3]) {
																	gridref = q.match(/\((\d+), (\d+)\)/);
																	if (gridref) {
																		control.labels.labels[c.i][2] = control._map.layerPointToLatLng(tilepos.add(new L.Point(4*gridref[1], 4*gridref[2])));
																		control.labels.labels[c.i][3] = zoom;
																		control.labels.labels[c.i][4].setPosition(control.labels.labels[c.i][2]);
																	}
																}
															}
															control.labels.labelLayer.addLayer(control.labels.labels[c.i][4]);
														}
													}
												}
											}
										}
									}
								}
							}
						}
						control.labels.updateCachePanel();
						if (control.labels.showLabels) {
							control.labels.clearLabels();
							control.labels.displayLabels();
						}
					},
					removeLabels:function () {
						$("#gme_cachelist").html("");
						control.labels.labelLayer.clearLayers();
						control.labels.labels = {};
					},
					toggleAuto:function () {
						if (control.labels.autoUpdate) {
							control.labels.autoUpdate = false;
							control._map.off("moveend",control.labels.refresh);
							$(".gme-button-labels-auto").removeClass("gme-button-active");
						} else {
							control.labels.autoUpdate = true;
							$(".gme-button-labels-auto").addClass("gme-button-active");
							control._map.on("moveend",control.labels.refresh);
							control.labels.refresh();
						}
					},
					toggleShow:function () {
						if (control.labels.showLabels) {
							control.labels.showLabels = false;
							$(".gme-button-labels-show").removeClass("gme-button-active");
							control.labels.clearLabels();
						} else {
							control.labels.showLabels = true;
							$(".gme-button-labels-show").addClass("gme-button-active");
							control.labels.refresh();
						}
					},
					updateCachePanel:function () {
						var i, j, sortorder =[], html = "";
						for (i in control.labels.labels) {
							if (control.labels.labels.hasOwnProperty(i)) {
								sortorder.push(i);
							}
						}
						sortorder.sort();
						j = sortorder.length;
						for (i =0; i<j; i++){
							html += "<tr><td><a href='https://coord.info/" + sortorder[i] + "' target='_blank' rel='noopener noreferrer'>" + control.labels.labels[sortorder[i]][1] + "</a></td><td class='gme-cache-code'>&nbsp;" + sortorder[i] + "</td><td><a class='gme-event' title='Pan map to cache location' data-gme-action='panTo' data-gme-cache='" + sortorder[i] + "'><img src='../images/silk/map.png' width='16' height='16' alt='Pan' /></a></td></tr>";
						}
						$("#gme_cachelist").html(html);
					}
				};
				$("#searchtabs ul").append("<li id='gme_caches_button'><a href='#gme_caches' title='GME Cache Label List' id='gme_caches_link'>GME</a></li>");
				$("#searchtabs li").css("width", 100/$("#searchtabs li").length+"%");
				$("#pqlink").html("PQs");
				$("#clistButton").html("GCVote");
				document.getElementById("pqlink").innerHTML = "PQs";
				$(div).append("<div id='gme_caches'>\
					<div class='leaflet-control-gme'>\
						<a title='Refresh cache labels' class='gme-event gme-button-refresh-labels gme-button gme-button-l' data-gme-action='refresh'></a><a title='Empty cache list and remove labels from map' class='gme-event gme-button gme-button-clear-labels' data-gme-action='clear'></a><a class='gme-event gme-button gme-button-wide gme-button-labels-show' data-gme-action='show'>Show labels</a><a class='gme-event gme-button gme-button-r gme-button-wide gme-button-labels-auto' data-gme-action='auto'>Auto update</a>\
					</div>\
					<table><tbody id='gme_cachelist'><tr><td colspan='3'>Hit the refresh button above to populate the list.</td></tr></tbody></table></div>");
				$("#gme_caches").css("display","none").on("click", ".gme-event", labelHandler);
				$("#gme-labels-show").on("change", control.labels.toggleShow);
				$("#gme-labels-auto").on("change", control.labels.toggleAuto);
			}
		},
		loadHide: function () {
			function load() {
				window.GME_control = new L.GME_Widget().addTo(map);
				GME_control._layerControl = GME_load_map(map);
				if (gmeConfig.env.dragdrop) {
					map.addControl(new L.GME_dropHandler());
				}
			}
			window.setTimeout(setEnv, 3000);
		},
		loadListing: function () {
			var cache_coords = {},
				mapLink = document.getElementById("ctl00_ContentBody_uxViewLargerMap");
			function load() {
				var parkUrl="", label="", i, parking, uri="&pop=";
				if (L.LatLng.prototype.toUrl === undefined) {
					L.LatLng.prototype.toUrl = function () {var obj=this; if(!(obj instanceof L.LatLng)) {return false;} return [L.Util.formatNum(obj.lat,5),L.Util.formatNum(obj.lng,5)].join(",");};
				}
				$("#map_canvas").replaceWith("<div style=\'width: 325px; height: 325px; position: relative;\' id=\'map_canvas2\'></div>");
				if (gmeConfig.env.dragdrop) {
					$("#cacheDetails .cacheImage").hover(function(e) { $("#cacheDetails .cacheImage").addClass("moveable"); },function(e) { $("#cacheDetails .cacheImage").removeClass("moveable"); });
					$("#cacheDetails .cacheImage").attr("draggable","true").on("dragstart", that.dragStart);
					$("#cacheDetails .cacheImage a").removeAttr("href");
				}
				window.GME_Map = new L.Map("map_canvas2",{center: new L.LatLng(mapLatLng.lat, mapLatLng.lng), zoom:14});
				GME_Map.addControl(new L.control.scale());
				GME_load_map(GME_Map);
				cache_coords = { primary:[mapLatLng], additional:[] };
				if (cmapAdditionalWaypoints && cmapAdditionalWaypoints.length > 0) {
					cache_coords.additional = cmapAdditionalWaypoints;
					if (gmeConfig.env.home) {
						for (i=cmapAdditionalWaypoints.length-1;i>=0;i--){
							if (cmapAdditionalWaypoints[i].hasOwnProperty("editurl")) {
								delete cache_coords.additional[i].editurl;
							}
							parking = cmapAdditionalWaypoints[i];
							if (parking.type === 217 || parking.type === 221) {
								label = parking.type===217?"Parking Area":"Trailhead";
								parkUrl = "https://www.google.com/maps/dir/?api=1&origin=" + gmeConfig.env.home.toUrl() + "&destination=" + parking.lat + "," + parking.lng;
$("#awpt_"+parking.pf)[0].parentNode.parentNode.children[5].innerHTML += '<a target="_blank" rel="noopener noreferrer" href="' + parkUrl + '"><img width="16" height="16" title="Directions to ' + label + '" alt="' + label + '" src="https://www.geocaching.com/images/wpttypes/sm/' + (parking.type===217?'pkg.jpg':'trailhead.jpg') + '" /></a>';						}
					}
				}
				if (gmeConfig.env.dragdrop) {
					GME_Map.addControl(new L.GME_dropHandler());
				}
				if (cache_coords.primary[0].oldLatLng || cache_coords.primary.length + cache_coords.additional.length > 1) {
					uri += b64encode(JSON.stringify(cache_coords));
					mapLink.href = mapLink.href.replace("http:", "https:") + uri;
					$('#ctl00_ContentBody_MapLinks_MapLinks a[href*="geocaching.com"]').attr("href", function(i, val) {return val + uri;});
				}
				GME_displayPoints(cache_coords, GME_Map, "listing");
			}
			setEnv();
		},
		loadMap: function() {
			function load() {
				function goSearch(e) {
					if (e.type === "click" || (e.which || e.keyCode) === 13) {
						e.preventDefault();
						e.stopImmediatePropagation();
						return GME_control.search($.trim($("#SearchBox_Text").val() || ""));
					}
				}
				var jsonURI;
				GME_load_map(MapSettings.Map);
				if (gmeConfig.env.dragdrop) {
					MapSettings.Map.addControl(new L.GME_dropHandler());
				}
				window.GME_control = GME_load_widget(MapSettings.Map);
				GME_load_labels(GME_control,"#scroller");
				if (gmeConfig.parameters.osgbSearch) {
					$("#SearchBox_OS").on("click keypress", goSearch);
					$(".SearchBox").on("keydown", goSearch);
					$("#search p")[0].innerHTML = "Search by <span style='cursor:help;' title='Enhanced by Geonames'>Address</span>, Coordinates, GC-code,<br/><span style='cursor:help;' title='Jump to a specific zoom level by typing zoom then a number. Zoom 1 shows the whole world, maxiumum zoom is normally 18-22.'>zoom</span> or <span style='cursor:help;' title='To search using a British National Grid reference, just type it in the search box and hit the button! You can use 2, 4, 6, 8 or 10-digit grid refs with the 2-letter prefix but no spaces in the number (e.g. SU12344225) or absolute grid refs with a comma but no prefix (e.g. 439668,1175316).'>Grid Ref</span>";
				}
				if (window.pnlOpen === false) {
					$(".leaflet-control-toolbar,.groundspeak-control-findmylocation,.leaflet-control-scale,.gme-left").css("left","30px");
				}
				if (gmeConfig.env.storage) {
					if (localStorage.GME_cache) {
						try {
							GME_displayPoints(JSON.parse(b64decode(localStorage.GME_cache)), GME_control._map, "clickthru");
							delete localStorage.GME_cache;
						} catch (e) {
							console.error("GME Can't pop cache: " + e);
						}
					}
				}
			}
			setEnv();
		},
		loadSeek: function () {
			function load() {
				function goGR(e) {
					if (e.type === "click" || (e.type === "keypress" && (e.which || e.keyCode) === 13)) {
						e.preventDefault();
						e.stopImmediatePropagation();
						that.seekGR($.trim($("#grRef").val()));
						return false;
					}
				}
				function goCoords(e) {
					var c, coords;
					if (e.type === "click" || (e.type === "keypress" && (e.which || e.keyCode) === 13)) {
						e.preventDefault();
						e.stopImmediatePropagation();
						c=document.getElementById("gme_coords").value;
						if (c) {
							coords = parseCoords(c);
							if (coords) {
								that.seekByLatLng(coords);
								return false;
							}
						}
					}
				}
				function goGoogle(e) {
					var q = document.getElementById("gme_google").value.trim(), url;
					e.preventDefault();
					e.stopImmediatePropagation();
					if (q) {
						url = "https://www.google.co.uk/search?q=allintitle%3A" + encodeURIComponent(q) + "+site%3Awww.geocaching.com%2Fgeocache%2F+OR+site%3Awww.geocaching.com%2Fseek%2Fcache_details.aspx";
						window.open(url, "_blank");
					}
					return false;
				}
				$("#grRef").keypress(goGR);
				$("#grSub").click(goGR);
				$("#gme_coords").keypress(goCoords);
				$("#gme_coords_sub").click(goCoords);
				$("#GME_hereSub").click(that.seekHere);
				$("#GME_googleSub").click(goGoogle);
			}
			setEnv();
		},
		loadType: function () {
			function load() {
				window.GME_control = new L.GME_Widget();
				GME_control._layerControl = GME_get_layerControl();
			}
			function reload() {
				$($(".leaflet-control-layers")[0]).remove();
				GME_control.addTo(map);
				map.eachLayer(function(layer) {
					if (layer._url) {
						map.removeLayer(layer);
					}
				});
				GME_control._layerControl.addTo(map);
				GME_control._layerControl.setDefault();
				if (gmeConfig.env.dragdrop) {
					map.addControl(new L.GME_dropHandler());
				}
			}
			window.setTimeout(setEnv, 3000);
		},
		loadTrack: function () {
			function load() {
				var caches, coords, i, name;
				function getLogPoints (layer) {
					if (layer._latlng && layer._popup && layer._popup._content && ~layer._popup._content.indexOf(name)) {
						coords = layer._latlng;
						$(caches[i].parentNode.parentNode.children[0]).append("<br/><a href='#GME_map_anchor'><img src='https://www.geocaching.com/images/silk/map.png' width='16' height='16' alt='Map' class='gme-action' data-gme-ref='" + coords.lat + "," + coords.lng + "' title='Centre map on this geocache'/></a> #" + layer._icon.innerHTML);
						return;
					}
				}
				GME_load_map(map);
				map.addControl(new L.GME_Widget());
				$("#ctl00_ContentBody_lbHeading").append("<a id=\'GME_map_anchor\'></a>");
				caches = $(".TrackableLogTable a[href*=cache_details]");
				for (i = caches.length - 1; i >= 0; i--) {
					name = caches[i].textContent.trim();
					map.eachLayer(getLogPoints, this);
				}
				$(".TrackableLogTable").on("click", ".gme-action", function(e) { map.panTo(L.latLng(this.getAttribute("data-gme-ref").split(","))); });
			}
			setEnv();
		},
		map: function () {
			var bounds_CI,
				bounds_DE,
				bounds_GB,
				bounds_IE,
				bounds_NI,
				icons = {
					marker: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA8AAAAZCAYAAADuWXTMAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAOQSURBVDiNhdRPSCNXHAfw7xs1mc5MjP9itE5ISjVGQrGxKgu6FIzx1CCskKunsj1YKN68tRdPBQ89hdKLl8JmLRR6sGzAPXRB0VVML2Y3kpDBJkHNv3EmTZjx14tZdM3aHzx4vPf78OP9Hu8xIsJNMABfxuPx7xKJxMT5%2BXn3xcWF0NfXpzscjtL4%2BPjrYDD4I4BXTQAiAhF1xWKxg%2BHhYRoYGCCfz0d%2Bv58CgQD5%2FX7y%2BXw0ODhIY2NjFIvFdonITkRgRMQ2NjZO19fXPxkdHUVPTw8AQFVVqKoKm80Gm80GACgWi0gmk1hbW0uvrq5%2BynZ2dlaWlpZ%2BmpychNVqRalUwv7%2B%2FhUR%2FQPgbwCfMcY%2Bnpqakrq7u1Gv13FwcICtra1v2fLy8sHe3t4XIyMjKBQKdHh4eG4YxhMienc2xthMe3v7bxMTEw6n08lSqRSmp6dfc7lcThZFEYZh4Pj4%2BMowjMe34U1fXhmG8fhmH4IgIJfLyVw%2Bn%2B8SBAGKopBhGDEieoMWQURvDMOIKYpCgiCgUCjYuUKhYBEEAaqq1k3TfNEKNsM0zReqqtZvsJUTBKHRaDSgqqoJ4O1DGMBbVVXNRqMBnucbnNvtzpbLZYii2AZA%2Fh8si6LYVqlU4Ha7FW5oaGi3VqtBkiQrx3GPHpIcxz2SJMlaq9Ugy%2FIu19nZua3rOmRZZhaLZYUx1tcKMsb6LBbLiizLTNd1SJL0J5fJZLaq1eo1Ywxer1fkef4lY2zoPTjE8%2FxLr9crchyHSqVynUwmnzMiwsLCwmE%2Bnw94PB4oikInJye1tra2PzRN%2B0sUxVnTNL%2Fy%2BXwfuVwulslk0N%2FffxyPxz9vBwCXy%2FV9IpH43ePxwOVyMYfDIVxeXkY0TYuIooje3l7wPA8AuLi4QCAQ%2BAEAWPNJut3ufz0ej9Vut3%2BwYdVqFaenp3VFUXgA4Jobs7Ozz3K53IP3lMvlMDMz8%2Fxd95sTSZK%2BKZfLpqZpLaGu6ygWi6bFYnl6D0ejUX1%2Bfv7XbDbbEmezWczNzT3b3NzU7mEAsNlsTyuVinl1dXUHapqGUqlkWq3Wr2%2Bv38HRaFQPh8M%2Fp1KpOziVSiEcDv9yuypwq9u3IxAIlBljdlmWcXZ2huvr68rR0VHX%2B3ncPQkgFAotptNpVKtVpNNphEKhxVZ5zd%2Fz3ohEItsdHR0UiUS2P5TTsjIAOJ3OxWAwmHQ6na2rAvgPIb3JdHxMgbEAAAAASUVORK5CYII%3D',
					tick:'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAXCAYAAAAYyi9XAAAALHRFWHRDcmVhdGlvbiBUaW1lAFRodSAxNyBNYXkgMjAxMiAyMjoxMjo1MiAtMDAwMP%2F5zBkAAAAHdElNRQfcBREVNTVnAq5wAAAACXBIWXMAAAsSAAALEgHS3X78AAAABGdBTUEAALGPC%2FxhBQAAAnRJREFUeNq9lMlrFEEUh19VdU3P1j0koujFmYzE4B8gCaIXTx6SgODFBUTw4lkv4kHxKuLFs4JHEZQkF0GM%2B4KIATHE4DATBMmiSetktq7NVxM7DB4n03nQdHdtv%2FreRmAbrFAo%2BM4e55477Oad7RAkaTKmq%2FqYrMgE3Q464pJL7kGXAQMRu6ClAwEHUodSjlpUzVhdGtElR5IU6QC%2FF2IltHSEkP3%2BOZ833zQFSZKnsRFGdJnxjOPsciD8GoZA4X5shG06Rga9k14inAtxANbkgvwci%2BAm3WiG830c6k%2Fqkqboq0qlUo3FpZ10%2Bo%2BG1kxLgoRJO9dzwn90F%2F2zvsMHuY0d6HXdkEvyWSyClo56tJA9kXUxbtB43tCEk3nTMMt2vmuXIkkaXz4%2BSxgb00nnnfZSbDcDFIHmBywHTqZwjexaEA9m2Ixv6po%2BjkRvi35xAmP1kqTI4TbdeDZp14mSAPldShR%2BHO3tShBvq4peccY0zXm2k42xPDuCBwuMFfPObNBZw2SxrxVs3HNbErSGh9xl%2FWxEr%2BlT%2Fdf6d%2FA8B13Xmu%2Fl7bwwytj4CZqmL0qVUjXa13XSIGWoVtVlY8xscCOwbQsSQwmKbm3Pqx%2FKEiq82GTnvi1lKYoumqq5IMqiGtwOlBFmcy6cbXeXmvqlXvdM0Fr5W%2FkdGLi6%2FmBd1x7VNgZR13YXkiCf8G%2Bxp4LWMFnu0CSdCG4FYbvQA%2BwuH1sS4zgZlUxkrBeCQRCInJt7DxxGxbzIsT7GalO1lvltrgSrwXLnWtILwcgGhgaOYpE%2FpH3Uw5L5olbUMBLWe%2B7SyEzLTOOJ19VP1cLmPf2%2FmLW%2FylgqETpxl%2BsAAAAASUVORK5CYII%3D'
				},
				wptTypes = [[/Geocache/i,"2"],[/Traditional Cache/i,"2"],[/Multi-cache/i,"3"],[/Virtual Cache/i,"4"],[/Letterbox Hybrid/i,"5"],[/Event Cache/i,"6"],[/Unknown cache/i,"8"],[/Webcam Cache/i,"11"],[/Cache In Trash Out Event/i,"13"],[/Wherigo Cache/i,"1858"],[/Locationless \(Reverse\) Cache/i,"12"],[/Mega-Event Cache/i,"453"],[/GPS Adventures Exhibit/i,"1304"],[/Groundspeak Block Party/i,"4738"],[/Groundspeak HQ/i,"3773"],[/Groundspeak Lost and Found Celebration/i,"3774"],[/Lost and Found Event Cache/i,"3653"],[/Project APE Cache/i,"9"],[/Earthcache/i,"137"],[/Question to Answer/i,"218"],[/Parking Area/i,"217"],[/Stages of a Multicache/i,"219"],[/Final Location/i,"220"],[/Trailhead/i,"221"],[/Reference Point/i,"452"]],
				polylineObj = {
					initialize: function (pts, ops) {
						L.Polyline.prototype.initialize.call(this, pts, ops);
						this._length = 0;
						this._markers = L.layerGroup();
						this._updateMarkers();
					},
					addLatLng: function (pt) {
						L.Polyline.prototype.addLatLng.call(this, pt);
						var len = this._latlngs.length;
						this._addMarker(pt, len);
						if (len > 1) {
							this._length += this._latlngs[len-2].distanceTo(this._latlngs[len-1]);
							this.fire("gme-length", {length: this._length});
						}
						return this;
					},
					getData:function() {
						return ((typeof window.btoa === "function") ? "data:application/xml-gpx;base64," : "data:application/xml-gpx,") + b64encode(this.getGPX());
					},
					getGPX:function() {
						var name = $(".CommonUsername").attr("title"),
							author = name ?
								(name + '</name>\r\n\t\t\t<link href="https://www.geocaching.com/profile/?u=' + name + '"><text>' + name + '\'s profile</text></link>\r\n') :
								"Geocaching.com user</name>\r\n",
							date = !!Date.prototype.toISOString ? ["\t\t<time>", new Date().toISOString(), "</time>\r\n"].join("") : "",
							i, l,
							gpx = ["<?xml version=\"1.0\" encoding=\"UTF-8\"?>\r\n<gpx creator=\"Geocaching Map Enhancements v", that.getVersion(), "\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" version=\"1.1\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\" xmlns=\"http://www.topografix.com/GPX/1/1\">\r\n\t<metadata>\r\n\t\t<name>GME Export</name>\r\n\t\t<desc>Route file exported from Geocaching.com using Geocaching Map Enhancements.</desc>\r\n\t\t<author>\r\n\t\t\t<name>", author, "\t\t</author>\r\n", date, "\t</metadata>\r\n"].join("");
						for (i = 0, l = this._latlngs.length; i < l; i++) {
							gpx += [ "\t<wpt lat=\"", this._latlngs[i].lat, "\" lon=\"", this._latlngs[i].lng, "\">\r\n\t\t<name>P", i, "</name>\r\n\t\t<type>Waypoint</type>\r\n\t</wpt>\r\n"].join("");
						}
						gpx += "\t<rte>\r\n\t\t<name>GME exported route</name>\r\n\t\t<src>Manual entry</src>\r\n\t\t<number>1</number>\r\n";
						for (i = 0, l = this._latlngs.length; i < l; i++) {
							gpx += [ "\t\t<rtept lat=\"", this._latlngs[i].lat, "\" lon=\"", this._latlngs[i].lng, "\">\r\n\t\t\t<name>P", i, "</name>\r\n\t\t\t<type>Waypoint</type>\r\n\t\t</rtept>\r\n"].join("");
						}
						gpx += "\t</rte>\r\n</gpx>";
						return gpx;
					},
					getLength: function() {
						return this._length;
					},
					onAdd: function(map) {
						map.addLayer(this._markers);
						this._markers.eachLayer(function(a) {a.dragging.enable();});
						return L.Polyline.prototype.onAdd.call(this, map);
					},
					onRemove: function(map) {
						map.removeLayer(this._markers);
						return L.Polyline.prototype.onRemove.call(this, map);
					},
					removePt: function(num) {
						this.spliceLatLngs(num-1,1);
						this._updateMarkers();
					},
					setLatLngs: function(pts) {
						L.Polyline.prototype.setLatLngs.call(this, pts);
						this._updateMarkers();
						return this;
					},
					_addMarker: function(pt, num) {
						var mark = new L.Marker(pt, {
							icon: new L.Icon({draggable: "true", iconUrl: icons.marker, iconSize: new L.Point(15, 25), iconAnchor: new L.Point(8,25)}),
							zIndexOffset:99, title: "Route Point #"+num
						});
						mark._routeNum = num;
						mark.bindPopup(["<p><strong>Route Point #", num, "</strong><br/>Centre: ",pt.toUrl(),"<br/><strong>",DMM(pt),"</strong><br/><span style='float:right;'><a class='gme-event' data-gme-action='removeDistMarker' data-gme-layer='", this._leaflet_id, "' data-gme-ref='",num,"'>Clear</a>, <a class='gme-event' data-gme-layer='", this._leaflet_id, "' data-gme-action='clearDist'>Clear All</a>, <a class='gme-event gme-draggable-gpx' data-gme-action='exportDist' data-gme-layer='", this._leaflet_id, "' draggable='true'>GPX</a></span></p>"].join(""));
						mark.on("dragend", this._moveMarker, this);
						this._markers.addLayer(mark);
						if (mark.dragging) {
							mark.dragging.enable();
						}
					},
					_moveMarker: function(e) {
						this.spliceLatLngs(e.target._routeNum - 1, 1, e.target.getLatLng());
						this._updateLength();
					},
					_updateLength: function() {
						var i;
						this._length = 0;
						for (i=1; i< this._latlngs.length; i++) {
							this._length += this._latlngs[i-1].distanceTo(this._latlngs[i]);
						}
						this.fire("gme-length", {length: this._length});
						return this._length;
					},
					_updateMarkers: function() {
						var i;
						this._markers.clearLayers();
						if (this._latlngs.length > 0) {
							this._addMarker(this._latlngs[0],1);
							for (i=1; i< this._latlngs.length; i++) {
								this._addMarker(this._latlngs[i], i+1);
							}
						}
						this._updateLength();
					}
				},
				quadkeyLayerObj = {
					tile2quad: function(x, y, z) {
						var i, digit, mask, quad = "";
						for (i = z; i > 0; i--) {
							digit = 0;
							mask = 1 << (i - 1);
							if ((x & mask) !== 0) { digit += 1; }
							if ((y & mask) !== 0) { digit += 2; }
							quad = quad + digit;
						}
						return quad;
					},
					getTileUrl: function(tilePoint) {
						return L.Util.template(this._url, L.extend({
							s: this._getSubdomain(tilePoint),
							q: this.tile2quad(tilePoint.x, tilePoint.y, this._getZoomForUrl()),
							z: this._getZoomForUrl()
						}, this.options));
					}
				},
				complexLayerObj = {
					getTileUrl: function(tilePoint) {
						return L.Util.template(this._url, L.extend({
							s4: tilePoint.x%4 + 4*(tilePoint.y%4),
							x100: this._getZoomForUrl()<=13?"":Math.floor(tilePoint.x/100)+"/",
							z: this._getZoomForUrl(),
							x: tilePoint.x,
							y: tilePoint.y
						}, this.options));
					}
				};
				
			function GME_displayPoints(plist, map, context) {
				var bounds = new L.LatLngBounds(), i, p, layers = L.featureGroup(), ll, op, PinIcon = L.Icon.extend({iconSize: new L.Point(20, 23),iconAnchor: new L.Point(10,23)});
				function checkType(t) {
					var j;
					for (j = wptTypes.length-1; j>=0; j--) {
						if (t==wptTypes[j][1]) {
							return wptTypes[j][1];
						}
					}
					return 452;
				}
				for (i=plist.primary.length-1; i>= 0; i--) {
					p = plist.primary[i];
					ll = L.latLng(p.lat, p.lng);
					if (context === "listing" || context === "dragdrop" || p.isUserDefined){
						layers.addLayer(L.marker(ll, {icon: new PinIcon({iconUrl:"/images/wpttypes/pins/" + checkType(p.type) + ".png",iconAnchor: L.point(10,23)}),clickable: false, zIndexOffset:98, title: p.name + (p.isUserDefined?" (Corrected coordinates)":"")}));
						if (p.isUserDefined) {
							layers.addLayer(L.marker(ll, {icon: new PinIcon({iconSize: new L.Point(28,23), iconAnchor: L.point(10,23), iconUrl:icons.tick}),clickable: false, zIndexOffset:99, title: p.name + " (Corrected coordinates)"}));
						}
					} else {
						bounds.extend(ll);
					}
					if (p.isUserDefined) {
						op = L.latLng(p.oldLatLng[0], p.oldLatLng[1]);
						layers.addLayer(L.polyline([op, ll], { clickable:false, weight:3 }));
						if (context === "listing") {
							layers.addLayer(L.circleMarker(op, {clickable:false, weight:3, radius:6}));
						}
					}
				}
				for (i=plist.additional.length-1; i>= 0; i--) {
					p = plist.additional[i];
					ll = L.latLng(p.lat, p.lng);
					layers.addLayer(L.marker(ll, {
						icon: new PinIcon({iconUrl:"/images/wpttypes/pins/" + checkType(p.type) + ".png", iconAnchor: new L.Point(10,23)}),
						title: p.name, clickable:false
					}));
				}
				if (plist.routes) {
					for (i=plist.routes.length-1; i>=0; i--) {
						if (plist.routes[i].points && plist.routes[i].points.length > 0) {
							layers.addLayer(L.polyline(plist.routes[i].points));
						}
					}
				}
				bounds.extend(layers.getBounds());
				if (bounds.isValid()) {
					switch (context) {
					case "listing":
						map.fitBounds(bounds, {padding:[10, 10], maxZoom: 15});
						break;
					case "clickthru":
						if (map.getBoundsZoom(bounds) > 15) {
							map.panTo(bounds.getCenter()).setZoom(15);
						} else {
							map.fitBounds(bounds);
						}
						break;
					default:
						map.panTo(bounds.getCenter());
					}
				}
				map.addLayer(layers);
				return bounds;
			}
			function genericLayerFn(url, options) {
                function filterOpts(opts) {
                    /* Remove GME's internal options, so they don't get passed to servers (WMS in particular). */
                    var opt, filtered = {}, exclude = ["tileUrl", "ignore", "alt"];
                    for (opt in opts) {
                        if (exclude.indexOf(opt) === -1) {
                            filtered[opt] = options[opt];
                        }
                    }
                    return filtered;
                }
                var filteredOpts = filterOpts(options);
                
				if (typeof url === "string") {
					return (/\{q\}/).test(url)?(new L.GME_QuadkeyLayer(url, filteredOpts)):((/\{s4\}|\{x100\}/).test(url)?(new L.GME_complexLayer(url,filteredOpts)):((/\{x\}/).test(url)?(new L.TileLayer(url, filteredOpts)):(new L.TileLayer.WMS(url, filteredOpts))));
				}
				console.error("GME: Bad map source: " + JSON.stringify(options));
				return undefined;
			}
			function setBrightness(e) {
				var brightness=this.brightness||that.parameters.brightness;
				if (brightness<1) {
					$(".leaflet-container").css("backgroundColor","#000");
				} else {
					$(".leaflet-container").css("backgroundColor","#ddd");
				}
				if (e.layer._url && /^http/.test(e.layer._url) && e.layer.options && !e.layer.options.overlay) {
					e.layer.setOpacity(brightness);
				}
			}
			function switchLayer(e) {
				var layer = e.layer;
				if (layer.options && layer.options.tileUrl && !layer.options.overlay) {
					this.layersMaxZoom = layer.options.maxZoom;
					this.layersMinZoom = layer.options.minZoom;
					if (isNaN(layer.options.maxZoom) || !isNaN(layer.options.minZoom)) {
						this._zoomBoundLayers[L.stamp(layer)] = layer;
						this._updateZoomLevels();
					}
					if (this.getZoom() > this.layersMaxZoom) { this.setZoom(this.layersMaxZoom); }
					if (this.getZoom() < this.layersMinZoom) { this.setZoom(this.layersMinZoom); }
					this.brightness = e.layer.options.brightness||that.parameters.brightness;
				}
			}
			function GME_get_layerControl(map){
				var maps={},overlays={},allMaps=that.parameters.maps,baseMaps,control,i,layer,src;
				for (baseMaps=0,i=0;i<allMaps.length;i++) {
					src = allMaps[i];
					if (!src.ignore) {
						layer=L.GME_genericLayer(src.tileUrl,src);
						if (layer) {
							if (src.overlay) {
								overlays[src.alt]=layer;
							} else {
								if (src.alt===that.parameters.defaultMap) {
									layer.default = true;
								}
								maps[src.alt]=layer;
								baseMaps++;
							}
						}
					}
				}
				if (baseMaps > 0) {
					// Only return a new control if we have some basemaps.
					control = L.control.layers(maps, overlays);
					control.setDefault = function () {
						var defLayer, j;
						for (j in this._layers) {
							if (this._layers.hasOwnProperty(j) && this._layers[j].layer.default) {
								defLayer = j;
							}
						}
						if (!defLayer) {
							defLayer = Object.keys(this._layers)[0];
						}
						if (this._map && defLayer !== undefined) {
							this._map.addLayer(this._layers[defLayer].layer, true);
						}
						return defLayer;
					};
				}
				return control;
			}
			function GME_load_map(map){
				var control = GME_get_layerControl(),
					layer;

				map.on("layeradd", switchLayer);
				if (document.createElement("div").style.opacity!==undefined) {
					map.on("layeradd", setBrightness);
				}

				/* If we're adding our own map selector control, we need to manually remove any pre-existing map layers.  Otherwise, they persist in the background underneath
				 * the layers provided by GME.	We check for the _url or _google attribute to distinguish map layers from other Leaflet layers like controls or popups */
				if (control) {
					if (gmeConfig.env.page === "maps" || gmeConfig.env.page === "track" || gmeConfig.env.page === "hide" || gmeConfig.env.page === "hide") {
						$($(".leaflet-control-layers")[0]).remove();
						for (layer in map._layers) {
							if (map._layers.hasOwnProperty(layer) && (map._layers[layer]._url || map._layers[layer]._google)) {
								map.removeLayer(map._layers[layer]);
							}
						}
						if (window.MapSettings !== undefined && MapSettings.MapLayers !== undefined && typeof MapSettings.MapLayers.ResetGeocacheLayer === "function") {
							MapSettings.MapLayers.ResetGeocacheLayer();
						}
					}
					map.addControl(control);
					control.setDefault();
				}
			}
		},
		osgb: function () {
			function OSGridToLatLng(E,N) {
				var a = 6377563.396,
					b = 6356256.910,
					F0 = 0.9996012717,
					lat0 = 49*Math.PI/180,
					lon0 = -2*Math.PI/180,
					N0 = -100000,
					E0 = 400000,
					e2 = 1 - (b*b)/(a*a),
					n = (a-b)/(a+b),
					n2 = n * n,
					n3 = n * n2,
					lat = lat0,
					lon,
					M = 0,
					Ma, Mb, Mc, Md, cosLat, sinLat, nu, nu3, nu5, nu7, rho, eta2, tanLat, tan2lat, tan4lat, tan6lat, secLat, VII, VIII, IX, X, XI, XII, XIIA, dE, dE2, dE3, dE4, dE5, dE6, dE7, tx, ty, tz, rx, ry, rz, s1, sinPhi, cosPhi, sinLambda, cosLambda, eSq, nu2, x1, y1, z1, x2, y2, z2, p, phi, phiP, precision, lambda;
				do {
					lat = (N-N0-M)/(a*F0) + lat;
					Ma = (1 + n + (5/4)*n2 + (5/4)*n3) * (lat-lat0);
					Mb = (3*n + 3*n*n + (21/8)*n3) * Math.sin(lat-lat0) * Math.cos(lat+lat0);
					Mc = ((15/8)*n2 + (15/8)*n3) * Math.sin(2*(lat-lat0)) * Math.cos(2*(lat+lat0));
					Md = (35/24)*n3 * Math.sin(3*(lat-lat0)) * Math.cos(3*(lat+lat0));
					M = b * F0 * (Ma - Mb + Mc - Md);
				} while (N-N0-M >= 0.01);
				cosLat = Math.cos(lat);
				sinLat = Math.sin(lat);
				nu = a*F0/Math.sqrt(1-e2*sinLat*sinLat);
				rho = a*F0*(1-e2)/Math.pow(1-e2*sinLat*sinLat, 1.5);
				eta2 = nu/rho-1;
				tanLat = Math.tan(lat);
				tan2lat = tanLat*tanLat;
				tan4lat = tan2lat*tan2lat;
				tan6lat = tan4lat*tan2lat;
				secLat = 1/cosLat;
				nu3 = nu*nu*nu;
				nu5 = nu3*nu*nu;
				nu7 = nu5*nu*nu;
				VII = tanLat/(2*rho*nu);
				VIII = tanLat/(24*rho*nu3)*(5+3*tan2lat+eta2-9*tan2lat*eta2);
				IX = tanLat/(720*rho*nu5)*(61+90*tan2lat+45*tan4lat);
				X = secLat/nu;
				XI = secLat/(6*nu3)*(nu/rho+2*tan2lat);
				XII = secLat/(120*nu5)*(5+28*tan2lat+24*tan4lat);
				XIIA = secLat/(5040*nu7)*(61+662*tan2lat+1320*tan4lat+720*tan6lat);
				dE = (E-E0);
				dE2 = dE*dE;
				dE3 = dE2*dE;
				dE4 = dE2*dE2;
				dE5 = dE3*dE2;
				dE6 = dE4*dE2;
				dE7 = dE5*dE2;
				lat = lat - VII*dE2 + VIII*dE4 - IX*dE6;
				lon = lon0 + X*dE - XI*dE3 + XII*dE5 - XIIA*dE7;
				tx=446.448;
				ty=-125.157;
				tz=542.060;
				rx=7.2819014902652306237205098174164e-7;
				ry=1.1974897923405539041670878328241e-6;
				rz=4.0826160086234026020206666559563e-6;
				s1=0.9999795106;
				sinPhi = Math.sin(lat);
				cosPhi = Math.cos(lat);
				sinLambda = Math.sin(lon);
				cosLambda = Math.cos(lon);
				eSq = (a*a - b*b) / (a*a);
				nu2 = a / Math.sqrt(1 - eSq*sinPhi*sinPhi);
				x1 = nu2 * cosPhi * cosLambda;
				y1 = nu2 * cosPhi * sinLambda;
				z1 = (1-eSq)*nu2 * sinPhi;
				x2 = tx + x1*s1 - y1*rz + z1*ry;
				y2 = ty + x1*rz + y1*s1 - z1*rx;
				z2 = tz - x1*ry + y1*rx + z1*s1;
				a = 6378137;
				b = 6356752.3142;
				eSq = (a*a - b*b) / (a*a);
				p = Math.sqrt(x2*x2 + y2*y2);
				phi = Math.atan2(z2, p*(1-eSq));
				phiP = 2*Math.PI;
				precision = 4 / a;
				while (Math.abs(phi-phiP) > precision) {
					nu = a / Math.sqrt(1 - eSq*Math.sin(phi)*Math.sin(phi));
					phiP = phi;
					phi = Math.atan2(z2 + eSq*nu*Math.sin(phi), p);
				}
				lambda = Math.atan2(y2, x2);
				return {lat: phi*180/Math.PI, lng:lambda*180/Math.PI};
			}
			function gridrefLetToNum(letters, numbers) {
				letters = letters.toUpperCase();
				var e, n,
					l1 = letters.charCodeAt(0) - "A".charCodeAt(0),
					l2 = letters.charCodeAt(1) - "A".charCodeAt(0);
				if (l1 > 7) { l1--; }
				if (l2 > 7) { l2--; }
				e = ((l1-2)%5)*5 + (l2%5);
				n = (19-Math.floor(l1/5)*5) - Math.floor(l2/5);
				e += numbers.slice(0, numbers.length/2);
				n += numbers.slice(numbers.length/2);
				switch (numbers.length) {
					case 2: e += "5000"; n += "5000"; break;
					case 4: e += "500"; n += "500"; break;
					case 6: e += "50"; n += "50"; break;
					case 8: e += "5"; n += "5"; break;
				}
				return [e, n];
			}
			function parseGR(searchVal) {
				var ngr, gr = searchVal.match(/^\s*([hnstHNST][A-Ha-hJ-Zj-z])\s*((?:\d\d){1,5})\s*$/);
				if (gr) {
					if (gr.length===3){
						if (2* Math.floor(gr[2].length / 2) === gr[2].length){
							ngr = gridrefLetToNum(gr[1], gr[2]);
							return OSGridToLatLng(ngr[0], ngr[1]);
						}
					}
					return null;
				}
				gr = searchVal.match(/^\s*(\d{3,6})\s*,\s*(\d{4,7})\s*$/);
				if (gr) {
					if (gr.length===3){
						return OSGridToLatLng(gr[1], gr[2]);
					}
				}
				return null;
			}
		},
		seek: function () {
			this.seekGR = function(searchVal) {
				if (searchVal.length > 0) {
					var coords = parseGR(searchVal);
					if (coords !== null) {
						that.seekByLatLng(coords);
					} else {
						alert("Could not recognise grid reference.");
					}
				}
			};
		},
		widget: function () {
			var locationControlObj = {
				onAdd:function(map){
					var el, tracking=false, container=L.DomUtil.create("div", "leaflet-control-toolbar groundspeak-control-findmylocation gme-left");
					function located(l){
						this.panTo(l.latlng);
					}
					function click(e){
						L.DomEvent.stopPropagation(e);
						if (tracking) {
							map.stopLocate();
							map.off("locationfound",located);
							tracking = false;
							$(".groundspeak-control-findmylocation-lnk").removeClass("gme-button-active");
							$("#GME_loc").attr("title","Follow My Location");
						} else {
							map.on("locationfound",located);
							map.locate({enableHighAccuracy:true,watch:true,timeout:60000});
							tracking = true;
							$(".groundspeak-control-findmylocation-lnk").addClass("gme-button-active");
							$("#GME_loc").attr("title","Stop following");
						}
					}
					function click_once(e) {
						L.DomEvent.stopPropagation(e);
						this.locate({setView:true, maxZoom:this.getZoom(), minZoom:this.getZoom(), enableHighAccuracy:true, timeout:60000});
					}
					el = document.createElement("a");
					el.id="GME_loc";
					el.title=that.parameters.follow?"Follow My Location":"Find My Location";
					el.className="groundspeak-control-findmylocation-lnk";
					if (that.parameters.follow) {
						L.DomEvent.addListener(el,"click",click,map);
					} else {
						L.DomEvent.addListener(el,"click",click_once,map);
					}
					container.appendChild(el);
					return container;
				}
			},
			widgetControlObj = {
				options:{position:"bottomleft"},
				onAdd:function(contextmap){
					var elem, container=L.DomUtil.create("div","leaflet-control-gme"), control=this, html="";
					function onPopup(e) {
						if (e.layer._container && /leaflet-popup/.test(e.layer._container.className)) {
							$(e.layer._contentNode).on("click", ".gme-event", contextmap, mapHandler);
							$(e.layer._contentNode).on("dragstart", ".gme-draggable-gpx", {line: control._dist_line}, dragGPXHandler);
						}
					}
					function offPopup(e) {
						if (e.layer._container && /leaflet-popup/.test(e.layer._container.className)) {
							$(e.layer._contentNode).off("click", ".gme-event", contextmap, mapHandler);
							$(e.layer._contentNode).off("dragstart", ".gme-draggable-gpx", {line: control._dist_line}, dragGPXHandler);
						}
					}
					function mapHandler(e) {
						var action = this.getAttribute("data-gme-action"),
							c1 = this.getAttribute("data-gme-coords"),
							c2, coords,
							data = this.getAttribute("data-gme-ref"),
							layer = this.getAttribute("data-gme-layer");
						e.stopPropagation();
						if (action !== "exportDist") {
							e.preventDefault(e);
						}
						if (c1) {
							c2 = c1.match(/(-?\d{1,2}(\.\d+)?),(-?\d{1,3}(\.\d+)?)/);
							if (c2 && c2.length === 5 && validCoords(c2[1], c2[3])) {
								coords = new L.LatLng(c2[1], c2[3]);
							}
						}
						if (action === "clearDist") { control.clearDist(); }
						if (action === "clearMarkers") { control.clearMarkers(); }
						if (action === "dropDist" && coords) { control.dropDist(coords); }
						if (action === "dropMarker" && coords) { control.dropMarker(coords); }
						if (action === "exportDist") { control.exportDist(this); }
						if (action === "getGeograph" && coords) { that.getGeograph(coords); }
						if (action === "getHeight" && coords) { that.getHeight(coords); }
						if (action === "getPostcode" && coords) { control.getPostcode(coords); }
						if (action === "panTo" && coords) { e.data.panTo(coords); }
						if (action === "removeMarker" && data) { control.removeMarker(data); }
						if (action === "removeDistMarker" && data) { control.removeDistMarker(data); }
						if (action === "toggleCaches") { control.toggleCaches(); }
						$(".leaflet-popup-close-button").each(function() {this.click();});
					}
					function dragGPXHandler(e) {
						e.originalEvent.dataTransfer.effectAllowed = "copy";
						var plain = e.data.line.getGPX(), data = e.data.line.getData();
						e.originalEvent.dataTransfer.setData("application/xml-gpx", plain);
						e.originalEvent.dataTransfer.setData("text/uri-list", data);
						e.originalEvent.dataTransfer.setData("DownloadURL", "application/xml-gpx:gme-export.gpx:" + data);
						e.originalEvent.dataTransfer.setData("text/plain", plain);
					}
					function widgetHandler(e) {
						var action = this.getAttribute("data-gme-action");
						e.stopPropagation();
						if (action === "panToHome") { control.panToHome(); }
						if (action === "toggleInfo") { control.toggleTool("info"); }
						if (action === "toggleRoute") { control.toggleTool("route"); }
						if (action === "toggleCaches") { control.toggleCaches(); }
					}
					this._map = contextmap;
					this._map.infoMode = false;
					this._map.routeMode = false;
					this._markers = L.layerGroup().addTo(contextmap);
					html = "<button type='button' class=\'GME_info gme-button gme-button-l\' title=\'Enable location info tool\' data-gme-action=\'toggleInfo\'></button>";
					html += "<button type='button' class=\'GME_hide gme-button\' title=\'Hide caches\' data-gme-action=\'toggleCaches\'></button>";
					html += "<button type='button' class=\'GME_route gme-button\' title=\'Enable route tool\' data-gme-action=\'toggleRoute\'></button>";
					if (gmeConfig.env.home) {
						html += "<button type='button' title=\'Go to home location\' class=\'GME_home gme-button\' data-gme-action=\'panToHome\'></button>";
					}
					if (gmeConfig.parameters.osgbSearch) {
						$(".GME_search_results").on("click", ".gme-event", contextmap, mapHandler);
					}
					if (gmeConfig.env.storage) {
						html += "<a class=\'GME_config gme-button\' title=\'Configure Geocaching Map Enhancements\' href=\'#GME_config\'></a>";
					}
					container.innerHTML = html;
					$(container.lastChild).addClass("gme-button-r");
					container.innerHTML += "<span class=\'gme-button gme-button-l gme-button-r gme-scale-container\' title=\'Approximate width of the full map view\' style=\'cursor:help;\'>Width: <span class=\'gme-scale\'>-</span></span><span class=\'gme-distance-container gme-button gme-button-r\' title=\'Measured distance\'>Route: <span class=\'gme-distance\'>"+ formatDistance(0) +"</span></span>";
					contextmap.addControl(new L.GME_ZoomWarning()).on("layeradd", onPopup).on("layerremove", offPopup).on("viewreset", this.updateScale, this);
					$(container).on("click", ".gme-button", this, widgetHandler);
					$(window).on("resize", this, (function (context) { var t = {timer: null}; return function () { context.updateScale(context._map, t);}; } (this)));
					return container;
				},
				clearDist:function () {
					this._dist_line.off("gme-length");
					this._map.removeLayer(this._dist_line);
					delete this._dist_line;
					$(".gme-distance-container").removeClass("show");
					$(".gme-distance").html(formatDistance(0));
					$(".gme-scale-container").addClass("gme-button-r");
				},
				clearMarkers:function () {
					this._markers.clearLayers();
				},
				dropDist:function(ll) {
					if (!validCoords(ll)) { return; }
					var dist, formatted;
					if (this._dist_line === undefined) {
						this._dist_line = new L.GME_DistLine([ll], {clickable:false});
						this._dist_line.on("gme-length", function (e) { $(this._map._container).find(".gme-distance").html(formatDistance(e.length)); });
						this._map.addLayer(this._dist_line);
						$(this._map._container).find(".gme-distance-container").addClass("show");
						$(this._map._container).find(".gme-scale-container").removeClass("gme-button-r");
					} else {
						this._dist_line.addLatLng(ll);
					}
				},
				exportDist:function(e) {
					if (!this._dist_line) { return; }
					e.download = "ExportedRoute.gpx";
					e.href = "data:application/xml-gpx," + encodeURIComponent(this._dist_line.getGPX());
					return false;
				},
				dropMarker:function(ll, rad) {
					if (!validCoords(ll)) { return; }
					var circle,
						defaultRadius = 0.161,
						group,
						label = "Marker",
						m = 1000,
						r, radius, raw,
						unit = "km";
					if (that.parameters.measure !== "metric") {
						unit = "miles";
						defaultRadius = 0.1;
						m = 1609.344;
					}
					radius = defaultRadius;
					if (!isNaN(rad)) {
						radius = rad * 1;
					} else {
						raw = window.prompt("Radius in " + unit + " [, label]", defaultRadius).match(/([\d]*\.?[\d]*)\s*,?\s*(.*)/);
						if (raw) {
							if (raw.length === 3) {
								radius = raw[1] * 1;
								label = raw[2] || label;
							} else {
								label = raw;
							}
						}
					}
					if (radius) {
						radius *= m;
						if (isNaN(radius)) { radius = 161; }
					} else {
						radius = 161;
					}
					circle = new L.Circle(ll, radius, {weight:2});
					group = new L.LayerGroup([circle, new L.CircleMarker(ll, {weight:2, radius:3})]);
					this._markers.addLayer(group);
					r = (radius / m).toFixed(3) + " " + unit;
					circle.bindPopup("<p><strong>" + label + "</strong><br/>Radius: " + r + "<br/>Centre: decimal " + ll.toUrl() + "<br/><strong>" + DMM(ll) + "</strong><br/><span style='float:right;'><a class='gme-event' data-gme-action='removeMarker' data-gme-ref='" + group._leaflet_id + "'>Clear</a>, <a class='gme-event' data-gme-action='clearMarkers'>Clear All</a></span></p>");
				},
				getPostcode:function(coords) {
					var that = this, callprefix="GME_postcode_callback",call;
					function makeCallback(callname) { callbackCount++; return function (json) {
						var m;
						if (json !== undefined && json.status === 200) {
							if (json.result && json.result.length > 0) {
								m = "<p>" + json.result[0].postcode + (json.result[0].parish ? (", " + json.result[0].parish) :	 "") + (json.result[0].admin_ward ? (", " + json.result[0].admin_ward) :	"") + "</p>";
							} else {
								m = "<p>No postcode found for this location.<br />Is it within 500m of an occupied building?</p>";
							}
						} else {
							m = "<p>Error fetching data from postcodes.io</p>";
						}
						if (json.result && !isNaN(json.result[0].latitude) && !isNaN(json.result[0].longitude)) {
							L.popup().setLatLng({lat: json.result[0].latitude, lng: json.result[0].longitude}).setContent(m).openOn(that._map);
						} else {
							$.fancybox(m);
						}
						$("#"+callname).remove();
						if (window[callname] !== undefined) { delete window[callname]; }
					};}
					if (validCoords(coords)) {
						call = callprefix + callbackCount;
						window[call] = makeCallback(call);
						JSONP("https://api.postcodes.io/postcodes/lon/" + coords.lng + "/lat/" + coords.lat + "?radius=500&limit=1&callback=" + call, call);
					} else {
						console.error("GME: Bad coordinates to getPostcode");
					}
				},
				panToHome:function () {
					if (gmeConfig.env.home) {
						this._map.panTo(gmeConfig.env.home);
						return true;
					}
					return false;
				},
				removeDistMarker:function(mark){
					if (this._dist_line) {
						this._dist_line.removePt(mark);
						$(this._map._container).find(".gme-distance").html(formatDistance(this._dist_line.getLength()));
					}
				},
				removeMarker:function(mark){
					this._markers.removeLayer(this._markers._layers[mark]);
				},
				removeMarkers:function(mark){
					this._markers.clearLayer(this._markers._layers[mark]);
				},
				showInfo:function (e) {
					var control=this, popupContent="<p>", popup = new L.Popup(), i;

					for(i = 0; i < this.tools.length; i++) {
                        if( this.tools[i].isValid(e.latlng, control._map.getZoom())) {
                            popupContent += this.tools[i].getHTML(e.latlng, control._map.getZoom(), control._map) + " ";
                        }
					}
					popupContent += "</p>";
						
					popup.setLatLng(e.latlng);
					popup.setContent(popupContent);
					control._map.addLayer(popup);
				},
                tools: [
                    {
                        name: "Coords",
                        getHTML: function(coords, zoom, map) {
                            var ll = coords.toUrl();
                            return "<strong>" + DMM(coords) + "</strong><br/>Dec: <a href='geo:" + ll + "?z=" + zoom + "'>" + ll + "</a></br>";
                        },
                        isValid: function(coords, zoom) { return true; }
                    },
                    {
                        name: "List caches",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='List " + (that.parameters.filterFinds ? "unfound " : "") + "caches near point' href='https://www.geocaching.com/seek/nearest.aspx?lat=" + coords.lat + "&lng=" + coords.lng + (that.parameters.filterFinds ? "&f=1" : "") + "' target='_blank' rel='noopener noreferrer'>List caches</a>";
                        },
                        isValid: function(coords, zoom) { return true; }
                    },
                    {
                        name: "Geograph",
                        action: "getGeograph",
                        getHTML: function(coords, zoom, map) {
                            return "<a href='#' title='Show Geograph images near this point' class='gme-event' data-gme-action='getGeograph' data-gme-coords='" + coords.toUrl() + "'>Geograph</a>";
                        },
                        isValid: function(coords, zoom) {
                            return	bounds_GB.contains(coords) || bounds_DE.contains(coords) || bounds_IE.contains(coords) || bounds_CI.contains(coords);
                        }
                    },
                    {
                        name: "Directions",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='Launch Google Directions from home to this point' target='_blank' rel='noopener noreferrer' href='https://www.google.com/maps/dir/?api=1&origin=" + gmeConfig.env.home.toUrl() + "&destination=" + coords.toUrl() + "'>Directions</a>";
                        },
                        isValid: function(coords, zoom) {
                            return !!gmeConfig.env.home;
                        }
                    },
                    {
                        name: "Wikimapia",
                        getHTML: function(coords, zoom, map) {
                            var centre = map.getCenter();
                            return "<a title='Go to wikimapia' target='_blank' rel='noopener noreferrer' href='http://wikimapia.org/#lat=" + centre.lat + "&lon=" + centre.lng + "&z=" + zoom + "'>Wikimapia</a>";
                        },
                        isValid: function(coords, zoom) {
                            return true;
                        }
                    },
                    {
                        name: "Marker",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='Drop route marker onto map' href='#' class='gme-event' data-gme-action='dropMarker' data-gme-coords='" + coords.toUrl() + "'>Marker</a>";
                        },
                        isValid: function(coords, zoom) {
                            return true;
                        }
                    },
                    {
                        name: "MAGIC",
                        getHTML: function(coords, zoom, map) {
                            var b = map.getBounds();
                            return "<a title='Show MAGIC map of environmentally sensitive areas' target='_blank' rel='noopener noreferrer' href='http://magic.defra.gov.uk/MagicMap.aspx?srs=WGS84&startscale=" + (Math.cos(map.getCenter().lat * L.LatLng.DEG_TO_RAD) * 684090188 * Math.abs(b.getSouthWest().lng - b.getSouthEast().lng)) / map.getSize().x +	"&layers=LandBasedSchemes,12,24:HabitatsAndSpecies,38:Designations,6,10,13,16,34,37,40,72,94&box=" + b.toBBoxString().replace(/,/g,":") + "'>MAGIC</a>";
                        },
                        isValid: function(coords, zoom) {
                            return that.isInUK(coords);
                        }
                    },
                    {
                        name: "Postcode",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='Fetch location data from postcodes.io' href='#' class='gme-event' data-gme-action='getPostcode' data-gme-coords='" + coords.toUrl() + "'>Postcode</a>";
                        },
                        isValid: function(coords, zoom) {
                            return that.isInUK(coords);
                        }
                    },
                    {
                        name: "Height",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='Height of point above sea level' href='#' class='gme-event' data-gme-action='getHeight' data-gme-coords='" + coords.toUrl() + "'>Height</a>";
                        },
                        isValid: function(coords, zoom) {
                            return (coords.lat > -65 && coords.lat < 83);
                        }
                    },
                    {
                        name: "StreetView",
                        getHTML: function(coords, zoom, map) {
                            return "<a title='Launch Google Streetview' target='_blank' rel='noopener noreferrer' href='https://www.google.com/maps/@?api=1&map_action=pano&viewpoint=" + coords.toUrl() + "'>Streetview</a>";
                        },
                        isValid: function(coords, zoom) {
                            return true;
                        }
                    },
                    {
                        name: "MapApp",
                        getHTML: function(coords, zoom, map) {
                            /* Open Bing Maps app if available, otherwise use a cross-platform Google Maps URI */
                            return "<a title='Launch Bing Maps' href='bingmaps:?cp=" + coords.lat + "~" + coords.lng + "' target='_blank' rel='noopener noreferrer'><a title='Launch Google Maps' href='https://www.google.com/maps/@?api=1&map_action=map&center=" + coords.toUrl() + "&zoom=" + zoom + "' target='_blank' rel='noopener noreferrer'>Maps</a></a>";
                        },
                        isValid: function(coords, zoom) {
                            return true;
                        }
                    }
                ],
				showRoute:function(e) {
					L.DomEvent.stopPropagation(e);
					this.dropDist(e.latlng);
				},
				toggleCaches:function() {
					if (window.MapSettings && MapSettings.MapLayers && MapSettings.MapLayers.AddGeocacheLayer && MapSettings.MapLayers.RemoveGeocacheLayer) {
						if (MapSettings.MapLayers.Geocache) {
							MapSettings.MapLayers.RemoveGeocacheLayer();
							$(".GME_hide").addClass("gme-button-active").attr("title","Show caches");
						} else {
							MapSettings.MapLayers.AddGeocacheLayer();
							$(".GME_hide").removeClass("gme-button-active").attr("title","Hide caches");
						}
					}
				},
				toggleTool:function(mode) {
					var that = this, widgets = {
						info: {
							on: function() {
								that._map.on("click contextmenu",that.showInfo,that);
								$("#map_canvas").addClass("gme-xhair");
								$(".GME_info").addClass("gme-button-active").attr("title","Disable location info tool");
							},
							off: function() {
								that._map.off("click contextmenu",that.showInfo,that);
								$("#map_canvas").removeClass("gme-xhair");
								$(".GME_info").removeClass("gme-button-active").attr("title","Enable location info tool");
							}
						},
						none: {on: function() {}, off: function() {}},
						route: {
							on: function() {
								that._map.on("click contextmenu",that.showRoute,that);
								$("#map_canvas").addClass("gme-xhair");
								$(".GME_route").addClass("gme-button-active").attr("title","Disable route tool");
							},
							off: function() {
								that._map.off("click contextmenu",that.showRoute,that);
								$("#map_canvas").removeClass("gme-xhair");
								$(".GME_route").removeClass("gme-button-active").attr("title","Enable route tool");
							}
						}
					};
					if (!widgets[mode]) {
						return;
					}
					widgets[this._clickMode].off();
					if (mode == this._clickMode) {
						this._clickMode = "none";
					} else {
						this._clickMode = mode;
						widgets[mode].on();
					}
				},
				search:function (searchVal) {
					var gr, m, call, callbackPrefix = "GME_search_callback", coords=false, marker, that=this;
					function searchGS(searchVal) {
						$(".GME_search_results").addClass("hidden");
						$.getJSON("/api/geocode",{q:searchVal},function(a){
							if (a.status==="success") {
								that._map.panTo(new L.LatLng(a.data.lat,a.data.lng));
							} else {
								alert("Sorry, no results found for "+searchVal);
							}
						});
					}
					function makeCallback2(callname) { callbackCount++; return function(json) {
						var i, j;
						if (json.geonames && json.geonames.length > 0) {
							$(".GME_search_list").empty();
							for (i=0,j=json.geonames.length;i<j;i++) {
								$(".GME_search_list").append("<li><a class='gme-event' data-gme-action='panTo' data-gme-coords='" + json.geonames[i].lat + "," + json.geonames[i].lng + "'>" + json.geonames[i].name + ", " + json.geonames[i].adminName1 + ", " + json.geonames[i].countryCode + "</a></li>");
							}
							$(".GME_search_results").removeClass("hidden");
							$(".GME_search_results.ui-collapsible-collapsed a.ui-collapsible-heading-toggle").click();
							$(".GME_link_GSSearch").off("click");
							$(".GME_link_GSSearch").click(function () { searchGS(searchVal); });
							that._map.panTo(new L.LatLng(json.geonames[0].lat,json.geonames[0].lng));
						} else {
							searchGS(searchVal);
						}
						$("#"+callname).remove();
						if (window[callname] !== undefined) { delete window[callname]; }
					};}
					function makeCallback1(callname) { callbackCount++; return function(json) {
						var newCall=callbackPrefix+callbackCount;
						if (json.countryCode) {
							window[newCall] = makeCallback2(newCall);
							JSONP("http://api.geonames.org/searchJSON?q=" + encodeURIComponent(searchVal) + "&countryBias=" + json.countryCode + "&maxRows=10&username=gme&callback=" + newCall, newCall);
						} else {
							searchGS(searchVal);
						}
						$("#"+callname).remove();
						if (window[callname] !== undefined) { delete window[callname]; }
					};}
					if (searchVal.length > 0) {
						m = searchVal.match(/^\s*(?:z|zoom)\s*(\d\d?)\s*$/i);
						if (m && m.length === 2) {
							this._map.setZoom(m[1]);
							return false;
						}
						m = searchVal.match(/^\s*(?:p|plot)(?:\s+r\s?(\d*\.?\d*))?\s+(.*)/);
						if (m && m.length === 3) {
							coords = parseCoords(m[2]);
							if (coords) {
								if (!isNaN(m[1])) {
									this.dropMarker(L.latLng(coords.lat, coords.lng), m[1]);
								} else {
									L.marker(coords, {icon: L.divIcon()}).addTo(this._map).bindPopup(DMM(coords));
								}
								this._map.panTo(coords);
								return false;
							}
						}
						m = searchVal.match(/^\s*(GC[0123456789ABCDEFGHJKMNOPQRSTVWXYZ]{1,7})\s*$/i);
						if (m && m.length === 2) {
							if (gmeConfig.env.loggedin) {
								this.panToGC(m[1]);
								$(".GME_search_results").addClass("hidden");
								return false;
							}
							alert("You must be logged in to allow GME to get cache coordinates");
							return false;
						}
						gr = parseGR(searchVal);
						if (gr) {
							this._map.panTo(new L.LatLng(gr.lat,gr.lng));
						} else {
							call = callbackPrefix + callbackCount;
							window[call] = makeCallback1(call);
							JSONP("http://api.geonames.org/countryCodeJSON?lat=" + this._map.getCenter().lat + "&lng=" + this._map.getCenter().lng + "&username=gme&radius=100&callback=" + call, call);
						}
					}
					return false;
				},
				panToGC:function(gc) {
					var s = document.getElementById("gme_jsonp_node");
					if (!s) {
						s = document.createElement("script");
						s.id = "gme_jsonp_node";
						document.documentElement.firstChild.appendChild(s);
					}
					s.type = "text/plain";
					s.text = gc;
					if (gmeConfig.env.xhr === "GM") {
						document.dispatchEvent(new Event("GME_GCsearch_event"));
					} else {
						$.fancybox("Search by GC-code is not available in this browser.<br />You can <a target='_blank' rel='noopener noreferrer' title='gc' href='https://coord.info/" + gc + "'>open the cache page</a> for " + gc + " instead");
					}
					return false;
				},
				updateScale:function (e, timer) {
					var map = this._map || e;

					if (!map.getBounds) {
						console.warn("updateScale didn't have working map");
						return;
					}
					
					function updateMap() {
						var bound = map.getBounds();
						var width = formatDistance(Math.cos(map.getCenter().lat * L.LatLng.DEG_TO_RAD) * 111319.49079327358 * Math.abs(bound.getSouthWest().lng - bound.getSouthEast().lng));
						$(this._container).find(".gme-scale").html(width);
					}

					if (timer !== undefined) {
						window.clearTimeout(timer.timer);
						timer.timer = window.setTimeout(function() { map.whenReady(updateMap); return false; }, 200);
					} else {
						map.whenReady(updateMap);
					}
				},
				_clickMode: "none"
			},
			zoomWarningObj = {
				options:{position:"topleft"},
				onAdd:function(map){
					var c = L.DomUtil.create("div","leaflet-control-zoomwarning gme-left");
					function checkZoom() {
						if (map.getZoom() > map.layersMaxZoom) {
							map.setZoom(map.layersMaxZoom);
						}
						if (map.getZoom() < map.layersMinZoom) {
							map.setZoom(map.layersMinZoom);
						}
						if (this.getZoom() > 18) {
							c.style.display = "block";
							if (typeof amplify === "object" && typeof amplify.store==="function" && amplify.store("ShowPanel") === false) {
								$(".leaflet-control-zoomwarning").css("left","30px");
							}
						} else {
							c.style.display = "none";
						}
					}
					c.innerHTML = "<a class='gme-button gme-button-l gme-button-r' title='Caches not visible at this zoom level'></a>";
					c.style.display = (map.getZoom() > 18)?"block":"none";
					map.on("zoomend", checkZoom);
					return c;
				}
			};
			
			function GME_load_widget(map) {
				var control = new L.GME_Widget().addTo(map);
				$(control._container).addClass("gme-left").css("top", "20px");
				$(".groundspeak-control-findmylocation").remove();
				if (L.GME_FollowMyLocationControl) {
					map.addControl(new L.GME_FollowMyLocationControl());
				}
				$(".leaflet-control-scale").addClass("gme-control-scale");
				$("a.ToggleSidebar").unbind();
				$("a.ToggleSidebar").click(function(a) {
					a.preventDefault();
					if (window.pnlOpen) {
						window.pnlOpen=false;
						$(".Sidebar").animate({left:"-355px"},500);
						$(".leaflet-control-zoom,.leaflet-control-toolbar,.leaflet-control-scale,.gme-left").animate({left:"30px"},500);
						$(this).removeClass("Open");
					} else {
						window.pnlOpen=true;
						$(".Sidebar").animate({left:"0"},500);
						$(".leaflet-control-zoom,.leaflet-control-toolbar,.leaflet-control-scale,.gme-left").animate({left:"385px"},500);
						$(this).addClass("Open");
					}
					if (typeof amplify === "object" && typeof amplify.store==="function") {
						amplify.store("ShowPanel", window.pnlOpen);
					}
					return false;
				});
				// Trigger reset to update scale and width controls.
				map.fireEvent("viewreset");
				return control;
			}
		},
		xhr: function (e) {
			var node = document.getElementById("gme_jsonp_node"),
				callback = node.getAttribute("data-gme-callback"),
				url = node.text,
				details = {
                    "method": "GET",
                    "url": url,
                    "onload": function(response) {
                        var x = response.responseText,
                            call = x.match(/([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\(/),
                            s;
                        if (call && call.length === 2 && call[1] === callback) {
                            s = document.getElementById("gme_jsonp_node");
                            s.setAttribute("data-gme-callback", callback);
                            s.text = x.substring(x.indexOf("(")+1, x.lastIndexOf(")"));
                            document.dispatchEvent(new Event("GME_XHR_callback"));
                        } else {
                            console.warn("Received: " + x);
                        }
                    }
                };
			if (gmeResources.env.xhr === 'GM4') {
                // GreaseMonkey 4+
                GM.xmlHttpRequest(details);
			} else {
                // Other userscript engines
                setTimeout(function() {
                    GM_xmlhttpRequest(details);
                }, 0);
            }
		}
	}
},
pageTests = [
	["listing", /\/geocache\/GC|\/seek\/cache_details\.aspx|\/seek\/cache_details2\.aspx/],
	["maps", /\/map\//],
	["hide", /\/hide\/planning\.aspx/],
	["type", /\/hide\/typelocation\.aspx/],
	["hide", /\/hide\/waypoints\.aspx/],
	["seek", /\/seek\/$|\/seek\/default\.aspx/],
	["track", /\/track\/map_gm\.aspx/]
],
i, target, target2, targets;

function buildScript() {
	var j, script = "";
	for (j = 1; j < arguments.length; j++) {
		if (typeof arguments[j] === "string" && gmeResources.script.hasOwnProperty(arguments[j])) {
			script += unwrapFunction(gmeResources.script[arguments[j]]);
			gmeResources.env.init.push(arguments[j]);
		}
	}
	insertScript(
		'var GME;\
		(function () {\
			"use strict";\
			function GeocachingMapEnhancements() {\
				var gmeConfig = ' + JSON.stringify({env: gmeResources.env, parameters: gmeResources.parameters}) +	";" +
				script +
			'}\
			GME = new GeocachingMapEnhancements();\
			console.info("Geocaching Map Enhancements v' + gmeResources.parameters.version + ' loaded.");\
		}());',
		arguments[0]
	);
}

function insertCSS(css) {
	if (typeof css !== "string") { console.warn("GME: insertCSS not called with string: " + typeof css); return;	}
	var style = document.createElement('style');
	style.type = 'text/css';
	if (style.styleSheet){
		style.styleSheet.cssText = css;
	} else {
		style.appendChild(document.createTextNode(css));
	}
	document.documentElement.firstChild.appendChild(style);
}

function insertPage(div, src, title, back) {
	if (div && typeof src === 'string') {
		var d = document.createElement('div');
		d.id = div;
		d.title = title||'';
		d.innerHTML = ['<div><a href="#',back||'','" title="Close" class="gme-close-dialog">X</a><header>',d.title, '</header><div class="gme-modal-content">', src, '</div></div>'].join('');
		d.className = 'gme-modalDialog';
		document.documentElement.lastChild.appendChild(d);
	} else {
		console.warn("GME: insertPage not called with correct parameters.");
	}
}

function insertScript(src, id) {
	console.log("GME: Inserting script: " + id);
//	console.debug(src);
	if (typeof src !== "string") { console.warn("GME: insertScript not called with string."); return; }
	var s = document.createElement("script");
	s.type = "text/javascript";
	s.text = src;
	if (id) {
		s.id = id;
	}
	document.documentElement.firstChild.appendChild(s);
	document.documentElement.firstChild.removeChild(s);
}

function unwrapFunction(fn) {
	var text;
	if (typeof fn === "function") {
		text = fn.toString();
		return text.slice(text.indexOf("{") + 1, text.lastIndexOf("}"));
	}
	return fn;
}

function xhr(e) {
	var node = document.getElementById("gme_jsonp_node"),
		callback = node.getAttribute("data-gme-callback"),
		url = node.text,
		details = {
            "method": "GET",
            "url": url,
            "onload": function(response) {
                var x = response.responseText,
                    call = x.match(/([a-zA-Z_$][0-9a-zA-Z_$]*)\s*\(/),
                    s;
                if (call && call.length === 2 && call[1] === callback) {
                    s = document.getElementById("gme_jsonp_node");
                    s.setAttribute("data-gme-callback", callback);
                    s.text = x.substring(x.indexOf("(")+1, x.lastIndexOf(")"));
                    document.dispatchEvent(new Event("GME_XHR_callback"));
                } else {
                    console.warn("Received: " + x);
                }
            }
        };
    if (gmeResources.env.xhr === 'GM4') {
        GM.xmlHttpRequest(details);
    } else {
        setTimeout(function() { GM_xmlhttpRequest(details);}, 0);
    }
}

//don't run on frames or iframes
if (window.top !== window.self) { return; }

if(!(typeof JSON === 'object' && typeof JSON.parse === 'function')) {
	console.error("Geocaching Map Enhancements requires a browser with JSON support.");
	return;
}

if(document.querySelector("head[data-gme-version]")) {
	console.error("Aborting: GME already running on page: " + document.location);
	return;
}
document.documentElement.firstChild.setAttribute("data-gme-version", gmeResources.parameters.version); 

for (i = 0; i < pageTests.length; i++) {
	if (pageTests[i][1].test(document.location.pathname)) {
		gmeResources.env.page = pageTests[i][0];
		break;
	}
}

try {
	if (window.localStorage !== undefined && window.localStorage !== null) { gmeResources.env.storage = true; }
} catch (e) {
	/*Potential security exception*/
	console.warn("No localStorage capability - GME cannot set configuration");
}

if(gmeResources.env.storage) {
	var a, b, customJSON, GME_custom, paramsJSON, storedParams;
	/* List of defunct tileUrls to remove from settings */
	var blacklist = [
		"https://ecn.t{s}.tiles.virtualearth.net/tiles/r{q}?g=737&productSet=mmOS",
		"https://ecn.t{s}.tiles.virtualearth.net/tiles/r{q}?g=864&productSet=mmCB",
		"https://otile{s}-s.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.jpg",
		"https://otile{s}-s.mqcdn.com/tiles/1.0.0/sat/{z}/{x}/{y}.jpg"
	];

	try {
		paramsJSON = localStorage.getItem("GME_parameters");
		if (paramsJSON) {
			try {
				storedParams = JSON.parse(paramsJSON);
				if (storedParams.version !== gmeResources.parameters.version) {
					for (a in gmeResources.parameters) {
						if (gmeResources.parameters.hasOwnProperty(a)) {
							if (storedParams[a]===undefined){storedParams[a]=gmeResources.parameters[a];}
						}
					}
					alert("Geocaching Map Enhancements has been updated to v" + gmeResources.parameters.version + ". " + gmeResources.parameters.versionMsg);
					storedParams.version = gmeResources.parameters.version;
					localStorage.setItem("GME_parameters",JSON.stringify(storedParams));
				}
				if (typeof storedParams.maps === "string") {
					console.info("GME: Trying to fix corrupted map settings.");
					storedParams.maps = JSON.parse(storedParams.maps);
				}
				gmeResources.parameters = storedParams;
			} catch (e) {
				console.warn("GME: Could not parse stored configuration parameters.");
			}
		} 
		/* Import old-style custom maps */
		customJSON = localStorage.getItem("GME_custom");
		if (customJSON) {
			console.info("GME: Found stored custom settings");
			try {
				GME_custom = JSON.parse(customJSON);
				if (GME_custom.maps && GME_custom.maps.length > 0) {
					gmeResources.parameters.maps = gmeResources.parameters.maps.concat(GME_custom.maps);
				}
				delete localStorage.GME_custom;
			} catch (e) {
				console.warn("GME: Could not parse stored custom maps.");
			} 
		}
		/* Remove old-style builtin maps */
		if (gmeResources.parameters.includeMaps) {
			delete gmeResources.parameters.includeMaps;
		}
		if (gmeResources.parameters.excludeMaps) {
			for (a = gmeResources.parameters.excludeMaps.length - 1; a >= 0; a--) {
				for (b = gmeResources.parameters.maps.length -1; b >= 0; b--) {
					if (gmeResources.parameters.maps[b].alt === gmeResources.parameters.excludeMaps[a]) {
						gmeResources.parameters.maps[b].ignore = true;
					}
				}
			}
			delete gmeResources.parameters.excludeMaps;
		}

		/* Remove broken map sources */
		for (a = gmeResources.parameters.maps.length - 1;  a >= 0; a--) {
			for(b = 0; b < blacklist.length; b++) {
				if (gmeResources.parameters.maps[a].tileUrl === blacklist[b]) {
					gmeResources.parameters.maps.splice(a,1);
				}
			}
		}

		localStorage.setItem("GME_parameters",JSON.stringify(gmeResources.parameters));
	} catch (e){
		console.error("GME: Bad Exception: " + e);
		/* Potential security exception. Carry on with default parameters, but block localstorage */
		gmeResources.env.storage = false;
	}
}

document.addEventListener("GME_XHR_event", xhr);

if (!gmeResources.env.geolocation) {
	gmeResources.script.dist = function () { console.warn("GME: Geolocation not available"); };
}
if (!gmeResources.env.dragdrop) {
	gmeResources.script.drag = function () { console.warn("GME: Drag and Drop not available"); };
	gmeResources.script.drop = gmeResources.script.drag;
}

insertCSS(gmeResources.css.main); 
if(gmeResources.env.storage) {
	insertPage('GME_config', gmeResources.html.config, 'Configure GME v' + gmeResources.parameters.version);
	insertPage('GME_format', gmeResources.html.customInfo, 'Custom Mapsource Format', 'GME_config');
}

//	<bugfix>
	// Trixie treats jQuery Mobile dialogs as new page loads, resetting GME's functions
	if (window.GME !== undefined) { return; }
//	</bugfix>

switch(gmeResources.env.page) {
	case "listing":
		// On a geocache listing
		if (!gmeResources.env.loggedin) {
			// Not logged in, so no maps or coordinates...
			console.log("GME: Couldn't detect log-in.  Exiting...");
			return;
		}
		if (gmeResources.env.dragdrop) { insertCSS(gmeResources.css.drag); }
		buildScript("GME_page_listing", "common", gmeResources.env.storage ? "config" : "", "map", "dist", "drag", "drop", "loadListing");
		break;
	case "seek":
		// On the Hide & Seek page
		target2 = document.querySelector(".SeekCacheWidget h4");
		targets = document.getElementsByTagName("h5");
		for (i = 0; i < targets.length; i++) {
			if (targets[i].innerHTML.match(/WGS84/)) {
				target = targets[i];
				break;
			}
		}
		
		if (target && target2) {
			var grDiv = document.createElement("div"), hereDiv = document.createElement("div");
			grDiv.innerHTML = '<h5>Ordnance Survey Grid Reference :</h5><dl><dt>Grid reference : </dt><dd><form name="grForm" id="grForm"><input type="text" class="Text EqualWidthInput" maxlength="50" size="15" name="grRef" id="grRef" placeholder="SU122422">&nbsp;<input type="submit" class="Button blockWithModalSpinner" name="submitGR" value="Go" id="grSub"></form></dd></dl><h5>Freeform coordinates</h5><dl><dt>Coordinates :</dt><dd><form name="coordForm" id="coordForm"><input type="text" class="Text EqualWidthInput" maxlength="50" size="15" name="gme_coords" id="gme_coords" placeholder="N 51° 10.683′ W 001° 49.604′"/>&nbsp;<input type="submit" class="Button blockWithModalSpinner" name="gme_coords_sub" value="Go" id="gme_coords_sub"/></form></dd></dl>';
			hereDiv.innerHTML = '<h4>Where you are...</h4><dl><dt>Use GeoLocation :</dt><dd><form name="hereForm" id="hereForm"><input type="submit" class="Button blockWithModalSpinner" name="GME_hereSub" value="Go" id="GME_hereSub"></form></dd></dl><h4>By keyword...</h4><dl><dt>Google search :</dt><dd><form name="googleForm" id="googleForm"><input type="text" class="Text EqualWidthInput" maxlength="50" size="15" name="gme_google" id="gme_google"/><input type="submit" class="Button blockWithModalSpinner" name="GME_googleSub" value="Go" id="GME_googleSub"></form></dd></dl>';
			target.parentNode.insertBefore(grDiv,target);
			target2.parentNode.insertBefore(hereDiv,target2);

			buildScript("GME_page_seek", "common", gmeResources.env.storage ? "config" : "", "osgb", "seek", "loadSeek");
		}
		break;
	case "track":
		// On a TB tracking map
		if (!gmeResources.env.loggedin) {
			// Not logged in, so no maps or coordinates...
			return;
		}
		buildScript("GME_page_track", "common", gmeResources.env.storage ? "config" : "", "map", "widget", "loadTrack");
		break;
	case "maps":
		// On a Geocaching Maps page
		// TODO: Detect if the Google Maps API is being used instead of Leaflet, and quit gracefully
/*		if (document.querySelector("script[src*='//maps.googleapis.com/']")){
			console.warn("Geocaching Map Enhancements requires Leaflet Maps to be enabled.");
			return;
	f	}
*/
		// Check for click-thru cache data in URI
		var pop = location.search.match(/pop=([A-Za-z0-9+\/=]+)[\?&]?/);
		if (pop && pop.length === 2) {
			try {
				localStorage.setItem("GME_cache", pop[1]);
				location.search = location.search.replace(/&pop=[A-Za-z0-9+\/=]+[\?&]?/, "");
			} catch (e) {
				console.error(e + "GME couldn't decode click-through data: " + pop[1]);
			}
			return;
		}

		if (gmeResources.env.xhr) {
			document.addEventListener("GME_GCsearch_event", function (e) {
				var node = document.getElementById("gme_jsonp_node"),
					gc = node.text,
					details = {
                        method: "GET",
                        url: "https://www.geocaching.com/geocache/" + gc,
                        onload: function (data) {
                            try {
                                var r = data.responseText,
                                    k = r.indexOf("mapLatLng = {"),
                                    c = JSON.parse(r.substring(k + 12, r.indexOf("}", k) + 1));
                                insertScript("GME_control._map.panTo(new L.LatLng(" + c.lat + ", " + c.lng + "));", "GC-search result");
                            } catch (e) {
                                console.warn("GME: Couldn't fetch cache coordinates:" + e + "\nReceived " + r.length + " bytes, coords at " + k);
                            }
                        }
                    };
                if (gmeResources.env.xhr === 'GM4') {
                    GM.xmlHttpRequest(details);
                } else {
                    setTimeout(function () {GM_xmlhttpRequest(details); }, 0);
                }
            });
		}
		if (gmeResources.parameters.osgbSearch) {
			targets = document.getElementsByClassName("SearchBox");
			if (targets[0]) {
				targets[0].innerHTML = gmeResources.html.search;
			}
		}

		buildScript("GME_page_map", "common", gmeResources.env.storage ? "config" : "", "cssTransitionsFix", "map", "widget", "labels", "drop", gmeResources.parameters.osgbSearch ? "osgb" : "", "loadMap");
		break;
	case "type":
		buildScript("GME_page_type", "common", gmeResources.env.storage ? "config" : "", "map", "widget", "drop", "loadType");
		break;
	case "hide":
		buildScript("GME_page_hide", "common", gmeResources.env.storage ? "config" : "", "map", "widget", "drop", "loadHide");
		break;
	default:
		// Somewhere random on the main website
		if (gmeResources.env.storage) {
			buildScript("Generic config", "common", "config", "loadDefault");
		}
	}
}());