Merge remote-tracking branch 'osmlab/hash'

This commit is contained in:
Tom Hughes 2013-08-04 12:38:59 +01:00
commit f9d714dfd3
16 changed files with 702 additions and 191 deletions

View file

@ -1,4 +1,8 @@
folder 'vendor/assets' do
folder 'jquery' do
file 'jquery.throttle-debounce.js', 'https://raw.github.com/cowboy/jquery-throttle-debounce/v1.1/jquery.ba-throttle-debounce.js'
end
folder 'leaflet' do
file 'leaflet.js', 'http://cdn.leafletjs.com/leaflet-0.6.3/leaflet-src.js'
file 'leaflet.css', 'http://cdn.leafletjs.com/leaflet-0.6.3/leaflet.css'
@ -8,7 +12,7 @@ folder 'vendor/assets' do
'marker-icon.png', 'marker-icon-2x.png',
'marker-shadow.png' ].each do |image|
file "images/#{image}", "http://cdn.leafletjs.com/leaflet-0.6.3/images/#{image}"
end
end
from 'git://github.com/kajic/leaflet-locationfilter.git' do
file 'leaflet.locationfilter.css', 'src/locationfilter.css'
@ -23,6 +27,10 @@ folder 'vendor/assets' do
from 'git://github.com/jfirebaugh/leaflet-osm.git' do
file 'leaflet.osm.js', 'leaflet-osm.js'
end
from 'git://github.com/mlevans/leaflet-hash.git' do
file 'leaflet.hash.js', 'leaflet-hash.js'
end
end
folder 'ohauth' do

View file

@ -2,15 +2,17 @@
//= require jquery_ujs
//= require jquery.timers
//= require jquery.cookie
//= require jquery.throttle-debounce
//= require augment
//= require osm
//= require leaflet
//= require leaflet.osm
//= require leaflet.hash
//= require leaflet.zoom
//= require leaflet.extend
//= require leaflet.locationfilter
//= require i18n/translations
//= require oauth
//= require osm
//= require piwik
//= require map
//= require menu
@ -22,10 +24,7 @@
var querystring = require('querystring-component');
function zoomPrecision(zoom) {
var decimals = Math.pow(10, Math.floor(zoom/3));
return function(x) {
return Math.round(x * decimals) / decimals;
};
return Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
}
function normalBounds(bounds) {
@ -61,41 +60,31 @@ function remoteEditHandler(bbox, select) {
* view tab and various other links
*/
function updatelinks(loc, zoom, layers, bounds, object) {
var toPrecision = zoomPrecision(zoom);
bounds = normalBounds(bounds);
var node;
$(".geolink").each(function(index, link) {
var href = link.href.split(/[?#]/)[0],
args = querystring.parse(link.search.substring(1));
var lat = toPrecision(loc.lat),
lon = toPrecision(loc.lon || loc.lng);
if (bounds && $(link).hasClass("bbox")) args.bbox = normalBounds(bounds).toBBoxString();
if (object && $(link).hasClass("object")) args[object.type] = object.id;
if (bounds) {
var minlon = toPrecision(bounds.getWest()),
minlat = toPrecision(bounds.getSouth()),
maxlon = toPrecision(bounds.getEast()),
maxlat = toPrecision(bounds.getNorth());
}
$(".geolink").each(setGeolink);
function setGeolink(index, link) {
var base = link.href.split('?')[0],
qs = link.href.split('?')[1],
args = querystring.parse(qs);
var query = querystring.stringify(args);
if (query) href += '?' + query;
if ($(link).hasClass("llz")) {
$.extend(args, {
lat: lat,
lon: lon,
zoom: zoom
});
} else if (minlon && $(link).hasClass("bbox")) {
$.extend(args, {
bbox: minlon + "," + minlat + "," + maxlon + "," + maxlat
});
args = {
lat: loc.lat,
lon: loc.lon || loc.lng,
zoom: zoom
};
if (layers && $(link).hasClass("layers")) {
args.layers = layers;
}
href += OSM.formatHash(args);
}
if (layers && $(link).hasClass("layers")) args.layers = layers;
if (object && $(link).hasClass("object")) args[object.type] = object.id;
link.href = href;
var minzoom = $(link).data("minzoom");
if (minzoom) {
@ -115,67 +104,7 @@ function updatelinks(loc, zoom, layers, bounds, object) {
});
}
}
link.href = base + '?' + querystring.stringify(args);
}
}
function getShortUrl(map) {
return (window.location.hostname.match(/^www\.openstreetmap\.org/i) ?
'http://osm.org/go/' : 'http://' + window.location.hostname + '/go/') +
makeShortCode(map);
}
function getUrl(map) {
var center = map.getCenter(),
zoom = map.getZoom(),
toZoom = zoomPrecision(zoom);
return (window.location.hostname.match(/^www\.openstreetmap\.org/i) ?
'http://openstreetmap.org/?' : 'http://' + window.location.hostname + '/?') +
querystring.stringify({
lat: toZoom(center.lat),
lon: toZoom(center.lng),
zoom: zoom,
layers: map.getLayersCode()
});
}
// Called to create a short code for the short link.
function makeShortCode(map) {
var zoom = map.getZoom(),
str = '',
char_array = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~",
x = Math.round((map.getCenter().lng + 180.0) * ((1 << 30) / 90.0)),
y = Math.round((map.getCenter().lat + 90.0) * ((1 << 30) / 45.0)),
// JavaScript only has to keep 32 bits of bitwise operators, so this has to be
// done in two parts. each of the parts c1/c2 has 30 bits of the total in it
// and drops the last 4 bits of the full 64 bit Morton code.
c1 = interlace(x >>> 17, y >>> 17), c2 = interlace((x >>> 2) & 0x7fff, (y >>> 2) & 0x7fff);
for (var i = 0; i < Math.ceil((zoom + 8) / 3.0) && i < 5; ++i) {
digit = (c1 >> (24 - 6 * i)) & 0x3f;
str += char_array.charAt(digit);
}
for (i = 5; i < Math.ceil((zoom + 8) / 3.0); ++i) {
digit = (c2 >> (24 - 6 * (i - 5))) & 0x3f;
str += char_array.charAt(digit);
}
for (i = 0; i < ((zoom + 8) % 3); ++i) str += "-";
// Called to interlace the bits in x and y, making a Morton code.
function interlace(x, y) {
x = (x | (x << 8)) & 0x00ff00ff;
x = (x | (x << 4)) & 0x0f0f0f0f;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00ff00ff;
y = (y | (y << 4)) & 0x0f0f0f0f;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return (x << 1) | y;
}
return str;
});
}
// generate a cookie-safe string of map state

View file

@ -69,11 +69,5 @@ $(document).ready(function () {
}
});
var params = OSM.mapParams();
if (params.bbox) {
map.fitBounds([[params.minlat, params.minlon],
[params.maxlat, params.maxlon]]);
} else {
map.fitBounds(group.getBounds());
}
map.fitBounds(OSM.mapParams().bounds || group.getBounds());
});

View file

@ -19,6 +19,8 @@ $(document).ready(function () {
map.attributionControl.setPrefix('');
map.hash = L.hash(map);
var layers = [
new L.OSM.Mapnik({
attribution: '',
@ -48,8 +50,11 @@ $(document).ready(function () {
layers[0].addTo(map);
map.noteLayer = new L.LayerGroup({code: 'N'});
map.noteLayer = new L.LayerGroup();
map.noteLayer.options = {code: 'N'};
map.dataLayer = new L.OSM.DataLayer(null);
map.dataLayer.options.code = 'D';
$("#sidebar").on("opened closed", function () {
map.invalidateSize();
@ -79,8 +84,6 @@ $(document).ready(function () {
L.OSM.share({
position: position,
getShortUrl: getShortUrl,
getUrl: getUrl,
sidebar: sidebar,
short: true
}).addTo(map);
@ -98,24 +101,21 @@ $(document).ready(function () {
map.markerLayer = L.layerGroup().addTo(map);
if (!params.object_zoom) {
if (params.bbox) {
var bbox = L.latLngBounds([params.minlat, params.minlon],
[params.maxlat, params.maxlon]);
map.fitBounds(bbox);
if (params.box) {
L.rectangle(bbox, {
weight: 2,
color: '#e90',
fillOpacity: 0
}).addTo(map);
}
if (params.bounds) {
map.fitBounds(params.bounds);
} else {
map.setView([params.lat, params.lon], params.zoom);
}
}
if (params.box) {
L.rectangle(params.box, {
weight: 2,
color: '#e90',
fillOpacity: 0
}).addTo(map);
}
if (params.layers) {
var foundLayer = false;
for (var i = 0; i < layers.length; i++) {
@ -164,8 +164,8 @@ $(document).ready(function () {
}
initializeExport(map);
initializeBrowse(map);
initializeNotes(map);
initializeBrowse(map, params);
initializeNotes(map, params);
});
function updateLocation() {
@ -177,6 +177,9 @@ function updateLocation() {
var expiry = new Date();
expiry.setYear(expiry.getFullYear() + 10);
$.cookie("_osm_location", cookieContent(this), { expires: expiry });
// Trigger hash update on layer changes.
this.hash.onMapMove();
}
function setPositionLink(map) {

View file

@ -2,7 +2,7 @@
//= require templates/browse/feature_list
//= require templates/browse/feature_history
function initializeBrowse(map) {
function initializeBrowse(map, params) {
var browseBounds;
var layersById;
var selectedLayer;
@ -49,6 +49,12 @@ function initializeBrowse(map) {
}
});
if (OSM.STATUS != 'api_offline' && OSM.STATUS != 'database_offline') {
if (params.layers.indexOf(dataLayer.options.code) >= 0) {
map.addLayer(dataLayer);
}
}
function startBrowse(sidebarHtml) {
locationFilter = new L.LocationFilter({
enableButton: false,

View file

@ -150,12 +150,12 @@ function initializeExport(map) {
}
function setBounds(bounds) {
var toPrecision = zoomPrecision(map.getZoom());
var precision = zoomPrecision(map.getZoom());
$("#minlon").val(toPrecision(bounds.getWest()));
$("#minlat").val(toPrecision(bounds.getSouth()));
$("#maxlon").val(toPrecision(bounds.getEast()));
$("#maxlat").val(toPrecision(bounds.getNorth()));
$("#minlon").val(bounds.getWest().toFixed(precision));
$("#minlat").val(bounds.getSouth().toFixed(precision));
$("#maxlon").val(bounds.getEast().toFixed(precision));
$("#maxlat").val(bounds.getNorth().toFixed(precision));
mapnikSizeChanged();
htmlUrlChanged();

View file

@ -1,9 +1,8 @@
//= require templates/notes/show
//= require templates/notes/new
function initializeNotes(map) {
var params = OSM.mapParams(),
noteLayer = map.noteLayer,
function initializeNotes(map, params) {
var noteLayer = map.noteLayer,
notes = {},
newNote;
@ -50,7 +49,7 @@ function initializeNotes(map) {
});
if (OSM.STATUS != 'api_offline' && OSM.STATUS != 'database_offline') {
if (params.notes || (params.layers && params.layers.indexOf('N')) >= 0) {
if (params.layers.indexOf(noteLayer.options.code) >= 0) {
map.addLayer(noteLayer);
}

View file

@ -10,22 +10,89 @@ L.extend(L.LatLngBounds.prototype, {
});
L.extend(L.Map.prototype, {
getLayersCode: function() {
var layerConfig = '';
for (var i in this._layers) { // TODO: map.eachLayer
var layer = this._layers[i];
if (layer.options && layer.options.code) {
layerConfig += layer.options.code;
}
}
return layerConfig;
},
getMapBaseLayerId: function() {
for (var i in this._layers) { // TODO: map.eachLayer
var layer = this._layers[i];
if (layer.options && layer.options.keyid) return layer.options.keyid;
}
getLayersCode: function () {
var layerConfig = '';
for (var i in this._layers) { // TODO: map.eachLayer
var layer = this._layers[i];
if (layer.options && layer.options.code) {
layerConfig += layer.options.code;
}
}
return layerConfig;
},
getMapBaseLayerId: function () {
for (var i in this._layers) { // TODO: map.eachLayer
var layer = this._layers[i];
if (layer.options && layer.options.keyid) return layer.options.keyid;
}
},
getUrl: function(marker) {
var precision = zoomPrecision(this.getZoom()),
params = {};
if (marker && this.hasLayer(marker)) {
params.mlat = marker.getLatLng().lat.toFixed(precision);
params.mlon = marker.getLatLng().lng.toFixed(precision);
}
var url = 'http://' + OSM.SERVER_URL + '/',
query = querystring.stringify(params),
hash = OSM.formatHash(this);
if (query) url += '?' + query;
if (hash) url += hash;
return url;
},
getShortUrl: function(marker) {
var zoom = this.getZoom(),
latLng = marker && this.hasLayer(marker) ? marker.getLatLng() : this.getCenter(),
str = '',
char_array = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~",
x = Math.round((latLng.lng + 180.0) * ((1 << 30) / 90.0)),
y = Math.round((latLng.lat + 90.0) * ((1 << 30) / 45.0)),
// JavaScript only has to keep 32 bits of bitwise operators, so this has to be
// done in two parts. each of the parts c1/c2 has 30 bits of the total in it
// and drops the last 4 bits of the full 64 bit Morton code.
c1 = interlace(x >>> 17, y >>> 17), c2 = interlace((x >>> 2) & 0x7fff, (y >>> 2) & 0x7fff),
digit;
for (var i = 0; i < Math.ceil((zoom + 8) / 3.0) && i < 5; ++i) {
digit = (c1 >> (24 - 6 * i)) & 0x3f;
str += char_array.charAt(digit);
}
for (i = 5; i < Math.ceil((zoom + 8) / 3.0); ++i) {
digit = (c2 >> (24 - 6 * (i - 5))) & 0x3f;
str += char_array.charAt(digit);
}
for (i = 0; i < ((zoom + 8) % 3); ++i) str += "-";
// Called to interlace the bits in x and y, making a Morton code.
function interlace(x, y) {
x = (x | (x << 8)) & 0x00ff00ff;
x = (x | (x << 4)) & 0x0f0f0f0f;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00ff00ff;
y = (y | (y << 4)) & 0x0f0f0f0f;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return (x << 1) | y;
}
if (marker && this.hasLayer(marker)) {
str += '?m'
}
return (window.location.hostname.match(/^www\.openstreetmap\.org/i) ?
'http://osm.org/go/' : 'http://' + window.location.hostname + '/go/') + str;
}
});
L.Icon.Default.imagePath = <%= "#{asset_prefix}/images".to_json %>;
L.Hash.prototype.parseHash = OSM.parseHash;
L.Hash.prototype.formatHash = OSM.formatHash;

View file

@ -60,8 +60,8 @@ L.OSM.share = function (options) {
}
function update() {
$shortLink.attr('href', options.getShortUrl(map));
$longLink.attr('href', options.getUrl(map));
$shortLink.attr('href', map.getShortUrl());
$longLink.attr('href', map.getUrl());
}
function select() {

View file

@ -23,7 +23,7 @@ OSM = {
},
mapParams: function (search) {
var params = {}, mapParams = {}, loc;
var params = {}, mapParams = {}, bounds, loc;
search = (search || window.location.search).replace('?', '').split(/&|;/);
@ -41,10 +41,6 @@ OSM = {
mapParams.mlat = parseFloat(params.mlat);
}
if (params.layers) {
mapParams.layers = params.layers;
}
if (params.node || params.way || params.relation) {
mapParams.object_zoom = true;
@ -57,21 +53,35 @@ OSM = {
}
}
// Decide on a lat lon to initialise the map with. Various ways of doing this
if (params.bbox) {
var bbox = params.bbox.split(",");
mapParams.bbox = true;
mapParams.minlon = parseFloat(bbox[0]);
mapParams.minlat = parseFloat(bbox[1]);
mapParams.maxlon = parseFloat(bbox[2]);
mapParams.maxlat = parseFloat(bbox[3]);
mapParams.object_zoom = false;
params.bbox = params.bbox.split(',');
bounds = L.latLngBounds(
[parseFloat(params.bbox[1]),
parseFloat(params.bbox[0])],
[parseFloat(params.bbox[3]),
parseFloat(params.bbox[2])]);
} else if (params.minlon && params.minlat && params.maxlon && params.maxlat) {
mapParams.bbox = true;
mapParams.minlon = parseFloat(params.minlon);
mapParams.minlat = parseFloat(params.minlat);
mapParams.maxlon = parseFloat(params.maxlon);
mapParams.maxlat = parseFloat(params.maxlat);
bounds = L.latLngBounds(
[parseFloat(params.minlat),
parseFloat(params.minlon)],
[parseFloat(params.maxlat),
parseFloat(params.maxlon)]);
}
if (params.box === 'yes') {
mapParams.box = bounds;
}
var hash = OSM.parseHash(location.hash);
// Decide on a map starting position. Various ways of doing this.
if (hash.lat && hash.lon) {
mapParams.lon = hash.center.lng;
mapParams.lat = hash.center.lat;
mapParams.zoom = hash.zoom;
mapParams.object_zoom = false;
} else if (bounds) {
mapParams.bounds = bounds;
mapParams.object_zoom = false;
} else if (params.lon && params.lat) {
mapParams.lon = parseFloat(params.lon);
@ -88,30 +98,23 @@ OSM = {
mapParams.lon = parseFloat(loc[0]);
mapParams.lat = parseFloat(loc[1]);
mapParams.zoom = parseInt(loc[2]);
mapParams.layers = loc[3];
} else if (OSM.home) {
mapParams.lon = OSM.home.lon;
mapParams.lat = OSM.home.lat;
mapParams.zoom = 10;
} else if (OSM.location) {
mapParams.bbox = true;
mapParams.minlon = OSM.location.minlon;
mapParams.minlat = OSM.location.minlat;
mapParams.maxlon = OSM.location.maxlon;
mapParams.maxlat = OSM.location.maxlat;
mapParams.bounds = L.latLngBounds(
[OSM.location.minlat,
OSM.location.minlon],
[OSM.location.maxlat,
OSM.location.maxlon]);
} else {
mapParams.lon = -0.1;
mapParams.lat = 51.5;
mapParams.zoom = parseInt(params.zoom || 5);
}
if (mapParams.bbox) {
mapParams.box = params.box == "yes";
mapParams.lon = (mapParams.minlon + mapParams.maxlon) / 2;
mapParams.lat = (mapParams.minlat + mapParams.maxlat) / 2;
}
mapParams.notes = params.notes == "yes";
mapParams.layers = hash.layers || (loc && loc[3]) || '';
if (params.note) {
mapParams.note = parseInt(params.note);
@ -123,5 +126,41 @@ OSM = {
}
return mapParams;
},
parseHash: function(hash) {
if (hash.indexOf('#') === 0) {
hash = hash.substr(1);
}
hash = querystring.parse(hash);
var args = L.Hash.parseHash(hash.map || '') || {};
if (hash.layers) args.layers = hash.layers;
return args;
},
formatHash: function(args) {
if (args instanceof L.Map) {
args = {
lat: args.getCenter().lat,
lon: args.getCenter().lng,
zoom: args.getZoom(),
layers: args.getLayersCode()
};
}
var precision = zoomPrecision(args.zoom),
hash = '#map=' + args.zoom +
'/' + args.lat.toFixed(precision) +
'/' + args.lon.toFixed(precision);
if (args.layers) {
args.layers = args.layers.replace('M', '');
}
if (args.layers) {
hash += '&layers=' + args.layers;
}
return hash;
}
};

View file

@ -8,6 +8,23 @@ class SiteController < ApplicationController
before_filter :require_oauth, :only => [:index]
def index
anchor = []
if params[:lat] && params[:lon]
anchor << "map=#{params.delete(:zoom) || 5}/#{params.delete(:lat)}/#{params.delete(:lon)}"
end
if params[:layers]
anchor << "layers=#{params.delete(:layers)}"
elsif params.delete(:notes) == 'yes'
anchor << "layers=N"
end
if anchor.present?
redirect_to params.merge(:anchor => anchor.join('&'))
return
end
unless STATUS == :database_readonly or STATUS == :database_offline
session[:location] ||= OSM::IPLocation(request.env['REMOTE_ADDR'])
end
@ -15,19 +32,18 @@ class SiteController < ApplicationController
def permalink
lon, lat, zoom = ShortLink::decode(params[:code])
new_params = params.clone
new_params.delete :code
new_params = params.except(:code, :lon, :lat, :zoom)
if new_params.has_key? :m
new_params.delete :m
new_params[:mlat] = lat
new_params[:mlon] = lon
else
new_params[:lat] = lat
new_params[:lon] = lon
end
new_params[:zoom] = zoom
new_params[:controller] = 'site'
new_params[:action] = 'index'
new_params[:anchor] = "#{zoom}/#{lat}/#{lon}"
redirect_to new_params
end

View file

@ -85,7 +85,12 @@
});
});
function mapMoved(lon, lat, zoom, minlon, minlat, maxlon, maxlat) {
var mapMoved = $.throttle(250, function(lon, lat, zoom, minlon, minlat, maxlon, maxlat) {
updatelinks({ lon: lon, lat: lat }, zoom, null, [[minlat, minlon], [maxlat, maxlon]]);
}
var hash = OSM.formatHash({ lon: lon, lat: lat, zoom: zoom });
if (hash !== location.hash) {
location.replace(hash);
}
});
</script>

View file

@ -31,7 +31,7 @@
oauth_token_secret: "<%= token.secret %>"
});
id.map().on('move.embed', function() {
id.map().on('move.embed', parent.$.throttle(250, function() {
var extent = id.map().extent(),
zoom = ~~id.map().zoom(),
center = id.map().center();
@ -46,7 +46,16 @@
extent[0][0]],
[extent[1][1],
extent[1][0]]]);
});
// 0ms timeout to avoid iframe JS context weirdness.
// http://bl.ocks.org/jfirebaugh/5439412
parent.setTimeout(function() {
var hash = parent.OSM.formatHash({ lon: center[0], lat: center[1], zoom: zoom });
if (hash !== parent.location.hash) {
parent.location.replace(hash);
}
}, 0);
}));
parent.$("body").on("click", "a.set_position", function (e) {
e.preventDefault();
@ -54,7 +63,7 @@
// 0ms timeout to avoid iframe JS context weirdness.
// http://bl.ocks.org/jfirebaugh/5439412
setTimeout(function() {
parent.setTimeout(function() {
id.map().centerZoom(
[data.lon, data.lat],
Math.max(data.zoom || 15, 13));

View file

@ -72,7 +72,29 @@ class SiteControllerTest < ActionController::TestCase
assert_template 'index'
assert_site_partials
end
def test_index_redirect
get :index, :lat => 4, :lon => 5
assert_redirected_to :controller => :site, :action => 'index', :anchor => 'map=5/4/5'
get :index, :lat => 4, :lon => 5, :zoom => 3
assert_redirected_to :controller => :site, :action => 'index', :anchor => 'map=3/4/5'
get :index, :layers => 'T'
assert_redirected_to :controller => :site, :action => 'index', :anchor => 'layers=T'
get :index, :notes => 'yes'
assert_redirected_to :controller => :site, :action => 'index', :anchor => 'layers=N'
get :index, :lat => 4, :lon => 5, :zoom => 3, :layers => 'T'
assert_redirected_to :controller => :site, :action => 'index', :anchor => 'map=3/4/5&layers=T'
end
def test_permalink
get :permalink, :code => 'wBz3--'
assert_redirected_to :controller => :site, :action => 'index', :anchor => '3/4.8779296875/3.955078125'
end
# Get the edit page
def test_edit
get :edit

View file

@ -0,0 +1,252 @@
/*!
* jQuery throttle / debounce - v1.1 - 3/7/2010
* http://benalman.com/projects/jquery-throttle-debounce-plugin/
*
* Copyright (c) 2010 "Cowboy" Ben Alman
* Dual licensed under the MIT and GPL licenses.
* http://benalman.com/about/license/
*/
// Script: jQuery throttle / debounce: Sometimes, less is more!
//
// *Version: 1.1, Last updated: 3/7/2010*
//
// Project Home - http://benalman.com/projects/jquery-throttle-debounce-plugin/
// GitHub - http://github.com/cowboy/jquery-throttle-debounce/
// Source - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.js
// (Minified) - http://github.com/cowboy/jquery-throttle-debounce/raw/master/jquery.ba-throttle-debounce.min.js (0.7kb)
//
// About: License
//
// Copyright (c) 2010 "Cowboy" Ben Alman,
// Dual licensed under the MIT and GPL licenses.
// http://benalman.com/about/license/
//
// About: Examples
//
// These working examples, complete with fully commented code, illustrate a few
// ways in which this plugin can be used.
//
// Throttle - http://benalman.com/code/projects/jquery-throttle-debounce/examples/throttle/
// Debounce - http://benalman.com/code/projects/jquery-throttle-debounce/examples/debounce/
//
// About: Support and Testing
//
// Information about what version or versions of jQuery this plugin has been
// tested with, what browsers it has been tested in, and where the unit tests
// reside (so you can test it yourself).
//
// jQuery Versions - none, 1.3.2, 1.4.2
// Browsers Tested - Internet Explorer 6-8, Firefox 2-3.6, Safari 3-4, Chrome 4-5, Opera 9.6-10.1.
// Unit Tests - http://benalman.com/code/projects/jquery-throttle-debounce/unit/
//
// About: Release History
//
// 1.1 - (3/7/2010) Fixed a bug in <jQuery.throttle> where trailing callbacks
// executed later than they should. Reworked a fair amount of internal
// logic as well.
// 1.0 - (3/6/2010) Initial release as a stand-alone project. Migrated over
// from jquery-misc repo v0.4 to jquery-throttle repo v1.0, added the
// no_trailing throttle parameter and debounce functionality.
//
// Topic: Note for non-jQuery users
//
// jQuery isn't actually required for this plugin, because nothing internal
// uses any jQuery methods or properties. jQuery is just used as a namespace
// under which these methods can exist.
//
// Since jQuery isn't actually required for this plugin, if jQuery doesn't exist
// when this plugin is loaded, the method described below will be created in
// the `Cowboy` namespace. Usage will be exactly the same, but instead of
// $.method() or jQuery.method(), you'll need to use Cowboy.method().
(function(window,undefined){
'$:nomunge'; // Used by YUI compressor.
// Since jQuery really isn't required for this plugin, use `jQuery` as the
// namespace only if it already exists, otherwise use the `Cowboy` namespace,
// creating it if necessary.
var $ = window.jQuery || window.Cowboy || ( window.Cowboy = {} ),
// Internal method reference.
jq_throttle;
// Method: jQuery.throttle
//
// Throttle execution of a function. Especially useful for rate limiting
// execution of handlers on events like resize and scroll. If you want to
// rate-limit execution of a function to a single time, see the
// <jQuery.debounce> method.
//
// In this visualization, | is a throttled-function call and X is the actual
// callback execution:
//
// > Throttled with `no_trailing` specified as false or unspecified:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X X X X X X X X X X X
// >
// > Throttled with `no_trailing` specified as true:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X X X X X X X X X
//
// Usage:
//
// > var throttled = jQuery.throttle( delay, [ no_trailing, ] callback );
// >
// > jQuery('selector').bind( 'someevent', throttled );
// > jQuery('selector').unbind( 'someevent', throttled );
//
// This also works in jQuery 1.4+:
//
// > jQuery('selector').bind( 'someevent', jQuery.throttle( delay, [ no_trailing, ] callback ) );
// > jQuery('selector').unbind( 'someevent', callback );
//
// Arguments:
//
// delay - (Number) A zero-or-greater delay in milliseconds. For event
// callbacks, values around 100 or 250 (or even higher) are most useful.
// no_trailing - (Boolean) Optional, defaults to false. If no_trailing is
// true, callback will only execute every `delay` milliseconds while the
// throttled-function is being called. If no_trailing is false or
// unspecified, callback will be executed one final time after the last
// throttled-function call. (After the throttled-function has not been
// called for `delay` milliseconds, the internal counter is reset)
// callback - (Function) A function to be executed after delay milliseconds.
// The `this` context and all arguments are passed through, as-is, to
// `callback` when the throttled-function is executed.
//
// Returns:
//
// (Function) A new, throttled, function.
$.throttle = jq_throttle = function( delay, no_trailing, callback, debounce_mode ) {
// After wrapper has stopped being called, this timeout ensures that
// `callback` is executed at the proper times in `throttle` and `end`
// debounce modes.
var timeout_id,
// Keep track of the last time `callback` was executed.
last_exec = 0;
// `no_trailing` defaults to falsy.
if ( typeof no_trailing !== 'boolean' ) {
debounce_mode = callback;
callback = no_trailing;
no_trailing = undefined;
}
// The `wrapper` function encapsulates all of the throttling / debouncing
// functionality and when executed will limit the rate at which `callback`
// is executed.
function wrapper() {
var that = this,
elapsed = +new Date() - last_exec,
args = arguments;
// Execute `callback` and update the `last_exec` timestamp.
function exec() {
last_exec = +new Date();
callback.apply( that, args );
};
// If `debounce_mode` is true (at_begin) this is used to clear the flag
// to allow future `callback` executions.
function clear() {
timeout_id = undefined;
};
if ( debounce_mode && !timeout_id ) {
// Since `wrapper` is being called for the first time and
// `debounce_mode` is true (at_begin), execute `callback`.
exec();
}
// Clear any existing timeout.
timeout_id && clearTimeout( timeout_id );
if ( debounce_mode === undefined && elapsed > delay ) {
// In throttle mode, if `delay` time has been exceeded, execute
// `callback`.
exec();
} else if ( no_trailing !== true ) {
// In trailing throttle mode, since `delay` time has not been
// exceeded, schedule `callback` to execute `delay` ms after most
// recent execution.
//
// If `debounce_mode` is true (at_begin), schedule `clear` to execute
// after `delay` ms.
//
// If `debounce_mode` is false (at end), schedule `callback` to
// execute after `delay` ms.
timeout_id = setTimeout( debounce_mode ? clear : exec, debounce_mode === undefined ? delay - elapsed : delay );
}
};
// Set the guid of `wrapper` function to the same of original callback, so
// it can be removed in jQuery 1.4+ .unbind or .die by using the original
// callback as a reference.
if ( $.guid ) {
wrapper.guid = callback.guid = callback.guid || $.guid++;
}
// Return the wrapper function.
return wrapper;
};
// Method: jQuery.debounce
//
// Debounce execution of a function. Debouncing, unlike throttling,
// guarantees that a function is only executed a single time, either at the
// very beginning of a series of calls, or at the very end. If you want to
// simply rate-limit execution of a function, see the <jQuery.throttle>
// method.
//
// In this visualization, | is a debounced-function call and X is the actual
// callback execution:
//
// > Debounced with `at_begin` specified as false or unspecified:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X
// >
// > Debounced with `at_begin` specified as true:
// > ||||||||||||||||||||||||| (pause) |||||||||||||||||||||||||
// > X X
//
// Usage:
//
// > var debounced = jQuery.debounce( delay, [ at_begin, ] callback );
// >
// > jQuery('selector').bind( 'someevent', debounced );
// > jQuery('selector').unbind( 'someevent', debounced );
//
// This also works in jQuery 1.4+:
//
// > jQuery('selector').bind( 'someevent', jQuery.debounce( delay, [ at_begin, ] callback ) );
// > jQuery('selector').unbind( 'someevent', callback );
//
// Arguments:
//
// delay - (Number) A zero-or-greater delay in milliseconds. For event
// callbacks, values around 100 or 250 (or even higher) are most useful.
// at_begin - (Boolean) Optional, defaults to false. If at_begin is false or
// unspecified, callback will only be executed `delay` milliseconds after
// the last debounced-function call. If at_begin is true, callback will be
// executed only at the first debounced-function call. (After the
// throttled-function has not been called for `delay` milliseconds, the
// internal counter is reset)
// callback - (Function) A function to be executed after delay milliseconds.
// The `this` context and all arguments are passed through, as-is, to
// `callback` when the debounced-function is executed.
//
// Returns:
//
// (Function) A new, debounced, function.
$.debounce = function( delay, at_begin, callback ) {
return callback === undefined
? jq_throttle( delay, at_begin, false )
: jq_throttle( delay, callback, at_begin !== false );
};
})(this);

162
vendor/assets/leaflet/leaflet.hash.js vendored Normal file
View file

@ -0,0 +1,162 @@
(function(window) {
var HAS_HASHCHANGE = (function() {
var doc_mode = window.documentMode;
return ('onhashchange' in window) &&
(doc_mode === undefined || doc_mode > 7);
})();
L.Hash = function(map) {
this.onHashChange = L.Util.bind(this.onHashChange, this);
if (map) {
this.init(map);
}
};
L.Hash.parseHash = function(hash) {
if(hash.indexOf('#') === 0) {
hash = hash.substr(1);
}
var args = hash.split("/");
if (args.length == 3) {
var zoom = parseInt(args[0], 10),
lat = parseFloat(args[1]),
lon = parseFloat(args[2]);
if (isNaN(zoom) || isNaN(lat) || isNaN(lon)) {
return false;
} else {
return {
center: new L.LatLng(lat, lon),
zoom: zoom
};
}
} else {
return false;
}
};
L.Hash.formatHash = function(map) {
var center = map.getCenter(),
zoom = map.getZoom(),
precision = Math.max(0, Math.ceil(Math.log(zoom) / Math.LN2));
return "#" + [zoom,
center.lat.toFixed(precision),
center.lng.toFixed(precision)
].join("/");
},
L.Hash.prototype = {
map: null,
lastHash: null,
parseHash: L.Hash.parseHash,
formatHash: L.Hash.formatHash,
init: function(map) {
this.map = map;
// reset the hash
this.lastHash = null;
this.onHashChange();
if (!this.isListening) {
this.startListening();
}
},
remove: function() {
if (this.changeTimeout) {
clearTimeout(this.changeTimeout);
}
if (this.isListening) {
this.stopListening();
}
this.map = null;
},
onMapMove: function() {
// bail if we're moving the map (updating from a hash),
// or if the map is not yet loaded
if (this.movingMap || !this.map._loaded) {
return false;
}
var hash = this.formatHash(this.map);
if (this.lastHash != hash) {
location.replace(hash);
this.lastHash = hash;
}
},
movingMap: false,
update: function() {
var hash = location.hash;
if (hash === this.lastHash) {
return;
}
var parsed = this.parseHash(hash);
if (parsed) {
this.movingMap = true;
this.map.setView(parsed.center, parsed.zoom);
this.movingMap = false;
} else {
this.onMapMove(this.map);
}
},
// defer hash change updates every 100ms
changeDefer: 100,
changeTimeout: null,
onHashChange: function() {
// throttle calls to update() so that they only happen every
// `changeDefer` ms
if (!this.changeTimeout) {
var that = this;
this.changeTimeout = setTimeout(function() {
that.update();
that.changeTimeout = null;
}, this.changeDefer);
}
},
isListening: false,
hashChangeInterval: null,
startListening: function() {
this.map.on("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.addListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
this.hashChangeInterval = setInterval(this.onHashChange, 50);
}
this.isListening = true;
},
stopListening: function() {
this.map.off("moveend", this.onMapMove, this);
if (HAS_HASHCHANGE) {
L.DomEvent.removeListener(window, "hashchange", this.onHashChange);
} else {
clearInterval(this.hashChangeInterval);
}
this.isListening = false;
}
};
L.hash = function(map) {
return new L.Hash(map);
};
L.Map.prototype.addHash = function() {
this._hash = L.hash(this);
};
L.Map.prototype.removeHash = function() {
this._hash.remove();
};
})(window);