/**
* 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));