Note: After saving, you have to bypass your browser's cache to see the changes. Internet Explorer: press Ctrl-F5, Mozilla: hold down Shift while clicking Reload (or press Ctrl-Shift-R), Opera/Konqueror: press F5, Safari: hold down Shift + Alt while clicking Reload, Chrome: hold down Shift while clicking Reload.
/**
 * Geo-Edit aka geoMarker
 *
 * @description
 *  Create an interactive map where the user 
 *  can either select a rectangle or mark a point.
 *
 *  Depends Dschwen's WikiMiniAtlas, hosted at wmflabs.
 *  The map will be centered to the user's current position.
 *  If the current position of the user is unknown, the browser's
 *  geo-api will be queried.
 *
 * @author Rainer Rillke, 2013
 * 
 * @example
	// First you must make sure the code is available ;-)
	// Since users prefer different ways for doing so,
	// this step is not documented.
	
	// Create the object and initialize
	// You must first append the element to the DOM and then call init!
	// Make sure the containing element is visible before the iframe loads.
	var $myEdit = mw.libs.geoMarker.$getUI( config_object ).appendTo('myNode').init();
	
	// Listen to value changes
	$myEdit.on('geoValue', function(e, val) {
		alert('Left longitude:' + val.topleft.lon + '\n' +
			'Bottom latitude:' + val.rightbottom.lat);
	});
	
	// Obtain the value
	var val = $myEdit.val();
	
	// TODO: Implement a value-setter
	
 * @param Object config_object that will overwrite the default config (search this script for "geo-edit configuration")
 * @return jQuery a jQuery instance containing the "geo-edit"
 *
 *
 * @license
 *  Quadruple licensed GFDL 1.2, GPL v3, LGPL v3 and Creative Commons Attribution Share-Alike 3.0 (CC-BY-SA-3.0)
 *  Choose whichever license of these you like best :-)
 *
 * This script is jsHint valid.
 */


/*global blackberry:false, geo_position_js_simulator:false, bondi:false, google:false, Mojo:false, device:false, alert:false, Geo:false, jQuery:false, mediaWiki:false */
/*jshint curly:false, smarttabs:true*/

(function($, mw) {
	'use strict';

	var myModuleName = 'mediawiki.commons.geoedit';

	/*! Derivative work of:
	 * geo-location-javascript v0.4.3
	 * http://code.google.com/p/geo-location-javascript/
	 *
	 * Copyright (c) 2009 Stan Wiechers
	 * Licensed under the MIT licenses.
	 *
	 * Revision: $Rev: 68 $:
	 * Author: $Author: whoisstan $:
	 * Date: $Date: 2010-02-15 13:42:19 +0100 (Mon, 15 Feb 2010) $:
	 */
	var bb_successCallback;
	var bb_errorCallback;
	var bb_blackberryTimeout_id = -1;

	function handleBlackBerryLocationTimeout() {
		if (bb_blackberryTimeout_id !== -1) {
			bb_errorCallback({
				message: "Timeout error",
				code: 3
			});
		}
	}

	window.handleBlackBerryLocation = function() {
		clearTimeout(bb_blackberryTimeout_id);
		bb_blackberryTimeout_id = -1;
		if (bb_successCallback && bb_errorCallback) {
			if (blackberry.location.latitude === 0 && blackberry.location.longitude === 0) {
				//http://dev.w3.org/geo/api/spec-source.html#position_unavailable_error
				//POSITION_UNAVAILABLE (numeric value 2)
				bb_errorCallback({
					message: "Position unavailable",
					code: 2
				});
			} else {
				var timestamp = null;
				//only available with 4.6 and later
				//http://na.blackberry.com/eng/deliverables/8861/blackberry_location_568404_11.jsp
				if (blackberry.location.timestamp) {
					timestamp = new Date(blackberry.location.timestamp);
				}
				bb_successCallback({
					timestamp: timestamp,
					coords: {
						latitude: blackberry.location.latitude,
						longitude: blackberry.location.longitude
					}
				});
			}
			//since blackberry.location.removeLocationUpdate();
			//is not working as described http://na.blackberry.com/eng/deliverables/8861/blackberry_location_removeLocationUpdate_568409_11.jsp
			//the callback are set to null to indicate that the job is done

			bb_successCallback = null;
			bb_errorCallback = null;
		}
	};

	var geo_position_js = function() {

		var pub = {};
		var provider = null;

		pub.getCurrentPosition = function(successCallback, errorCallback, options) {
			provider.getCurrentPosition(successCallback, errorCallback, options);
		};

		pub.init = function() {
			try {
				if (window.geo_position_js_simulator) {
					provider = geo_position_js_simulator;
				} else if (window.bondi && bondi.geolocation) {
					provider = bondi.geolocation;
				} else if (navigator.geolocation) {
					provider = navigator.geolocation;
					pub.getCurrentPosition = function(successCallback, errorCallback, options) {
						function _successCallback(p) {
							//for mozilla geode,it returns the coordinates slightly differently
							if (p.latitude !== undefined) {
								successCallback({
									timestamp: p.timestamp,
									coords: {
										latitude: p.latitude,
										longitude: p.longitude
									}
								});
							} else {
								successCallback(p);
							}
						}
						provider.getCurrentPosition(_successCallback, errorCallback, options);
					};
				} else if (window.google && google.gears) {
					provider = google.gears.factory.create('beta.geolocation');
					// A service is an on-device "server" for any resource, data, or configuration that can be exposed for developers 
					// to use with their applications. They are called "services" instead of "servers" 
					// to make it clear that they are on the device rather than "in the cloud". 
				} else if (window.Mojo && Mojo.Service.Request) {
					provider = true;
					pub.getCurrentPosition = function(successCallback, errorCallback, options) {

						var parameters = {};
						if (options) {
							//http://developer.palm.com/index.php?option=com_content&view=article&id=1673#GPS-getCurrentPosition
							if (options.enableHighAccuracy && options.enableHighAccuracy === true) {
								parameters.accuracy = 1;
							}
							if (options.maximumAge) {
								parameters.maximumAge = options.maximumAge;
							}
							if (options.responseTime) {
								if (options.responseTime < 5) {
									parameters.responseTime = 1;
								} else if (options.responseTime < 20) {
									parameters.responseTime = 2;
								} else {
									parameters.timeout = 3;
								}
							}
						}


						new Mojo.Service.Request('palm://com.palm.location', {
							method: "getCurrentPosition",
							parameters: parameters,
							onSuccess: function(p) {
								successCallback({
									timestamp: p.timestamp,
									coords: {
										latitude: p.latitude,
										longitude: p.longitude,
										heading: p.heading
									}
								});
							},
							onFailure: function(e) {
								if (e.errorCode === 1) {
									errorCallback({
										code: 3,
										message: "Timeout"
									});
								} else if (e.errorCode === 2) {
									errorCallback({
										code: 2,
										message: "Position Unavailable"
									});
								} else {
									errorCallback({
										code: 0,
										message: "Unknown Error: webOS-code" + e.errorCode
									});
								}
							}
						});
					};

				} else if (window.device && device.getServiceObject) {
					provider = device.getServiceObject("Service.Location", "ILocation");

					//override default method implementation
					pub.getCurrentPosition = function(successCallback, errorCallback) {
						function callback(transId, eventCode, result) {
							if (eventCode === 4) {
								errorCallback({
									message: "Position unavailable",
									code: 2
								});
							} else {
								//no timestamp of location given?
								successCallback({
									timestamp: null,
									coords: {
										latitude: result.ReturnValue.Latitude,
										longitude: result.ReturnValue.Longitude,
										altitude: result.ReturnValue.Altitude,
										heading: result.ReturnValue.Heading
									}
								});
							}
						}
						//location criteria
						var criteria = {};
						criteria.LocationInformationClass = "BasicLocationInformation";
						//make the call
						provider.ILocation.GetLocation(criteria, callback);
					};
				} else if (window.blackberry && blackberry.location.GPSSupported) {

					// set to autonomous mode
					if (blackberry.location.setAidMode === undefined) {
						return false;
					}
					blackberry.location.setAidMode(2);
					//override default method implementation
					pub.getCurrentPosition = function(successCallback, errorCallback, options) {
						//alert(parseFloat(navigator.appVersion));
						//passing over callbacks as parameter didn't work consistently
						//in the onLocationUpdate method, thats why they have to be set
						//outside
						bb_successCallback = successCallback;
						bb_errorCallback = errorCallback;

						if (options.timeout) {
							bb_blackberryTimeout_id = setTimeout(handleBlackBerryLocationTimeout, options.timeout);
						} else
						//default timeout when none is given to prevent a hanging script
						{
							bb_blackberryTimeout_id = setTimeout(handleBlackBerryLocationTimeout, 60000);
						}
						//function needs to be a string according to
						//http://www.tonybunce.com/2008/05/08/Blackberry-Browser-Amp-GPS.aspx						
						blackberry.location.onLocationUpdate("handleBlackBerryLocation()");
						blackberry.location.refreshLocation();
					};
					provider = blackberry.location;
				}
			} catch (e) {
				alert("error=" + e);
				mw.log(e);
				return false;
			}
			return !!provider;
		};
		return pub;
	}();


	// Since we do not register this as a gadget, we can not
	// take the advantage of CSS-Janus
	// Therefore we have to flip ourserlf (nothing complicated, however)
	var isRTL = $(document.body).hasClass('rtl'),
		right = 'right',
		left = 'left',
		ltr = 'ltr',
		css;
	if (isRTL) {
		right = 'left';
		left = 'right';
		ltr = 'rtl';
	}

	css = [
			'.gma-input-wrap { display:inline-block; margin:1.5em }',
			'.gma-input-label { display:block }',
			'#gma-maincontainer input { padding: 5px }',
			'div.gma-selectionrect { border: 1px solid #99f; background: #bbf; position: absolute; z-index: 50; cursor: move; }',
			'div.gma-selectionmarker { position: absolute; z-index: 50; cursor: move; }',
			'div.gma-mapcontainer { position:relative; overflow: hidden; float:' + left + '; margin: 0em 0.5em }',
			'iframe.gma-dschwen { background: url(\'//upload.wikimedia.org/wikipedia/commons/thumb/b/b0/Openstreetmap_logo.svg/256px-Openstreetmap_logo.svg.png\') ' +
			'no-repeat center #def; margin: 0; padding: 0; }',
			'div.gma-geoctrl { margin:1.5em 0.5em; display:block; }',
			'div.gma-map-overlay { background:#eee; position:absolute; top:0; ' + left + ':0; cursor:crosshair }'
	].join('\n');
	mw.util.addCSS(css);

	// These messages will be overwritten by localized versions
	// The loacalized versions are shiped with the page ([[:commons:Help:Watchlist messages/Wizard]])
	// They are inlcuded in a hidden div and make use of [[Help:Autotranslate]]
	var i18n = {
		'gma-float-placeholder': "Number (e.g. 12.84)",
		'gma-geo-loading-map': "Retrieving map from $1 …",
		'gma-geo-usage': "Note that when an area is selected, you can\'t zoom or move the map.",
		'gma-geo-info': "Your GeoIP information: Lat: $1, Lon: $2, City: $3, Country: $4"
	};
	mw.messages.set(i18n);

	var _msg = function(params) {
		var args = Array.prototype.slice.call(arguments, 0);
		args[0] = 'gma-' + args[0];
		return mw.msg.apply(mw, args);
	},
		_msgp = function(params) {
			var args = Array.prototype.slice.call(arguments, 0);
			args[0] = 'gma-' + args[0];
			var msg = mw.message.apply(mw, args);
			return msg.parse();
		},
		userlang = mw.config.get('wgUserLanguage'),
		$win = $(window),
		delay = function() {
			var timeoutID = 0;
			return function(cb, ms) {
				if (timeoutID) clearTimeout(timeoutID);
				timeoutID = setTimeout(cb, ms);
			};
		},
		geoDelay = delay();


	var internal = {
		floatRE: /^\-?\d+(?:\.\d+)?$/,
		_numbersonly: function($i) {
			return $i.on('input', function() {
				var o_val = $i.val(),
					n_val = o_val.replace(/[^\d.]/g, '');
				if (o_val !== n_val) $i.val(n_val);
			});
		},
		_rch: null,
		_requestCoords: function($iframe, $rect, $ctrl, cfg) {
			var _g360 = function(x) {
				return x % 360;
			},
				_g180 = function(x) {
					if (x > 180) x -= 360;
					return x;
				},
				_l0 = function(x) {
					if (x < 0) x = 360 - x;
					return x;
				},
				isMarker = ('marker' === cfg.type);

			geoDelay(function() {
				if (internal._rch) $win.off('message', internal._rch);
				internal._rch = function(e) {
					var r = JSON.parse(e.originalEvent.data).response,
						tl = r.topleft,
						rb = r.rightbottom,
						londiff = _l0(rb.lon - tl.lon),
						latdiff = rb.lat - tl.lat,
						iw = $iframe.width(),
						ih = $iframe.height(),
						lonPerPx = londiff / iw,
						latPerPx = latdiff / ih,
						rw = $rect.width(),
						rh = $rect.height(),
						rpos = $rect.position(),
						spotL = (isMarker ? cfg.marker.spot[0] : 0),
						spotT = (isMarker ? cfg.marker.spot[1] : 0),
						rl = rpos.left + spotL,
						rt = rpos.top + spotT,
						lonLeft = tl.lon + lonPerPx * rl,
						lonRight = _g180(_g360(lonLeft + lonPerPx * rw)),
						latTop = tl.lat + latPerPx * rt,
						latBottom = latTop + latPerPx * rh;

					lonLeft = _g180(_g360(lonLeft));
					$ctrl.lon.from.val(lonLeft);
					$ctrl.lon.to.val(lonRight);
					$ctrl.lat.from.val(latTop);
					$ctrl.lat.to.val(latBottom);
					$ctrl.$ctrl.triggerHandler('geoValue', $ctrl.$ctrl.val());
				};
				$win.on('message', internal._rch);

				// Fetch the coords of the iframe borders
				$iframe[0].contentWindow.postMessage(JSON.stringify({
					getcoords: 1
				}), location.protocol + $iframe.attr('src'));
			}, 500);
		},
		initGeo: function(cb) {
			if (window.Geo && Geo.lon) return cb();
			//determine if the handset has client side geo location capabilities
			if (geo_position_js.init()) {
				geo_position_js.getCurrentPosition(function(p) {
					var c = p.coords;
					window.Geo = {
						lon: c.longitude,
						lat: c.latitude
					};
					cb();
				}, cb);
			} else {
				cb();
			}

		},
		labels: {
			rect: {
				selBtn: "Select area",
				selTooltip: "You can drag me around and resize me"
			},
			marker: {
				selBtn: "Mark point",
				selTooltip: "You can drag me around"
			}
		}

	};

	var geoMarker = {
		// geo-edit configuration
		config: {
			type: 'rect', // rect|marker
			marker: {
				url: '//upload.wikimedia.org/wikipedia/commons/a/ac/Flag-export_lightblue.png',
				width: 32,
				height: 37,
				spot: [16, 36]
			}
		},
		$getUI: function(cfg) {
			cfg = $.extend(true, {}, geoMarker.config, cfg);

			var lat, lon,
				type = cfg.type,
				i18nType = internal.labels[type],
				isMarker = ('marker' === type),
				isRect = ('rect' === type),
				w = 600,
				h = 400;

			var $createInputs = function(id, label, key, $geoCtrl) {
				var $l = $('<label class="gma-input-label"></label>').text(label),
					idn = 'gma-ip-' + id + '-from',
					idx = 'gma-ip-' + id + '-to',
					$d1 = $('<div>').css('display', 'inline-block'),
					$d2 = $('<div>').css('display', 'inline-block'),
					$l1 = $('<label>').attr('for', idn).text('From '),
					$l2 = $('<label>').attr('for', idx).text('To '),
					$i1 = internal._numbersonly($('<input type="text" size="20"/>').attr({
						'id': idn,
						pattern: internal.floatRE.source,
						placeholder: _msg('float-placeholder')
					})),
					$i2 = internal._numbersonly($('<input type="text" size="20"/>').attr({
						'id': idx,
						pattern: internal.floatRE.source,
						placeholder: _msg('float-placeholder')
					}));

				switch (type) {
					case 'rect':
						$l1.appendTo($d1);
						$l2.appendTo($d2);
						$i1.appendTo($d1);
						$i2.appendTo($d2);
						break;
					case 'marker':
						$i1.appendTo($d1);
						break;
				}
				$geoCtrl[key] = {
					from: $i1,
					to: $i2
				};
				return $('<div>').append($l, ' ', $d1, ' ', $d2);
			};

			var $ctrl = $('<div>').attr({
				'class': 'gma-input-wrap',
				'dir': ltr
			}).css('display', 'block'),
				$map = $('<div class="gma-mapcontainer"></div>').height(h).width(w).prependTo($ctrl),
				$iframe, $button,
				$usage = $('<div>').text(_msg('geo-usage')).hide().appendTo($ctrl),
				$waiting = $('<div class="gma-geoctrl gma-input-wrap"></div>').text(_msgp('geo-loading-map', 'WMF labs (OSM/Dschwen/Dispenser)')).appendTo($ctrl),
				$selection, $overlay, _requestCoords,
				$geoCtrl = $('<div class="gma-geoctrl gma-input-wrap"></div>').appendTo($ctrl),
				$lat = $createInputs('lat', "Latitude", 'lat', $geoCtrl).appendTo($geoCtrl),
				$lon = $createInputs('lon', "Longitude", 'lon', $geoCtrl).appendTo($geoCtrl),
				$geoCtrls = $geoCtrl.find('input'),
				$info = $('<div>').appendTo($ctrl),
				$clear = $('<div>').css('clear', 'both').appendTo($ctrl);

			$geoCtrl.$ctrl = $ctrl;

			// Ident_ify as Wikipedia (we don't want to see an image collection but labels!)
			$iframe = $('<iframe>').attr({
				scrolling: 'no',
				frameBorder: 0,
				'class': 'gma-dschwen'
			}).css({
				width: w,
				height: h
			}).appendTo($map);

			var init = function() {
				$info.text(_msgp('geo-info', Geo.lat, Geo.lon, Geo.city, Geo.country));
				$iframe.on( 'load', function() {
					$waiting.fadeOut();
					if ($selection) _requestCoords();
				}).attr('src', '//wma.wmflabs.org/iframe.html?' + $.param({
					wma: lat + '_' + lon + '_' + w + '_' + h + '_' + userlang + '_3_' + userlang,
					globe: 'Earth',
					lang: userlang,
					page: '',
					awt: 0
				}));
				var _cleanUp = function(e, resetCtrls) {
					$('body').off('mouseup.gma-selection');
					if ($selection) $selection.remove();
					if ($overlay) $overlay.remove();
					$overlay = $selection = null;
					if (resetCtrls !== false) $geoCtrls.val('');
					$button.button({
						disabled: false
					});
					$usage.hide();
					return false;
				};
				$geoCtrls.on('input change', function() {
					_cleanUp(null, false);
					$ctrl.triggerHandler('geoValue', $ctrl.val());
				});
				$('<button role="button" type="button"></button>').text("Remove selection").button({
					icons: {
						primary: 'ui-icon-circle-close'
					}
				}).addClass('ui-button-red').click(_cleanUp).insertAfter($map);

				$button = $('<button role="button" type="button"></button>').text(i18nType.selBtn).button({
					icons: {
						primary: 'ui-icon-circle-plus'
					}
				}).addClass('ui-button-blue').click(function() {
					$button.button({
						disabled: true
					});
					_cleanUp();

					$usage.show();

					_requestCoords = function() {
						internal._requestCoords($iframe, $selection, $geoCtrl, cfg);
					};
					var parentOffset = $map.offset(),
						relXs, relYs, relX, relY, mousedown, isNegX, isNegY,
						_registerMouseUp = function() {
							$('body').one('mouseup.gma-selection', function() {
								mousedown = false;
								$button.button({
									disabled: false
								});
								if ($overlay) $overlay.css('cursor', 'default');

								_requestCoords();
							});
						};

					$selection = $('<div>').addClass('gma-selection' + type)
						.attr('title', i18nType.selTooltip).prependTo($map);

					if (isMarker) {
						$('<img>').attr('src', cfg.marker.url).width(cfg.marker.width).height(cfg.marker.height).appendTo($selection);
					} else {
						$selection.fadeTo(0, 0.7);
					}

					$overlay = $('<div class="gma-map-overlay"></div>')
						.height(h).width(w).fadeTo(0, 0).prependTo($map).one('mousedown', function(e) {
						e.preventDefault();
						if (e.which !== 1) return;

						_registerMouseUp();

						relXs = e.pageX - parentOffset.left;
						relYs = e.pageY - parentOffset.top;

						if (isMarker) {
							relXs -= cfg.marker.spot[0];
							relYs -= cfg.marker.spot[1];
						}
						$selection.css({
							top: relYs,
							left: relXs
						});
						mousedown = true;
					}).mousemove(function(e) {
						if (!mousedown || isMarker) return;
						relX = e.pageX - parentOffset.left;
						relY = e.pageY - parentOffset.top;
						var diffX = relX - relXs,
							diffY = relY - relYs;

						if (diffX < 0) {
							isNegX = true;
							$selection.css('left', relX);
						} else if (isNegX) {
							isNegX = false;
							$selection.css('left', relXs);
						}
						if (diffY < 0) {
							isNegY = true;
							$selection.css('top', relY);
						} else if (isNegY) {
							isNegY = false;
							$selection.css('top', relYs);
						}
						$selection.width(Math.abs(diffX)).height(Math.abs(diffY));
					});
					$selection.draggable({
						containment: 'parent',
						stop: _requestCoords
					});
					if (isRect) {
						$selection.resizable({
							stop: _requestCoords
						});
					}
				}).insertAfter($map);

				$ctrl.val = function() {
					return {
						topleft: {
							lon: $geoCtrl.lon.from.val(),
							lat: $geoCtrl.lat.from.val()
						},
						rightbottom: {
							lon: $geoCtrl.lon.to.val(),
							lat: $geoCtrl.lat.to.val()
						},
						cfg: cfg
					};
				};
			};
			$ctrl.init = function() {
				internal.initGeo(function() {
					lat = Geo.lat || 0;
					lon = Geo.lon || 0;
					init();
				});
				return $ctrl;
			};
			return $ctrl;
		}
	};

	mw.libs.geoMarker = geoMarker;

	var h = {};
	h[myModuleName] = 'ready';
	mw.loader.state(h);
	$(document).triggerHandler('scriptLoaded', myModuleName);
}(jQuery, mediaWiki));