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'
'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'
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
//= 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
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) {
* 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());
- }
+ var query = querystring.stringify(args);
+ if (query) href += '?' + query;
- $(".geolink").each(setGeolink);
+ if ($(link).hasClass("llz")) {
+ args = {
+ lat: loc.lat,
+ lon: loc.lon || loc.lng,
+ zoom: zoom
+ };
- function setGeolink(index, link) {
- var base = link.href.split('?')[0],
- qs = link.href.split('?')[1],
- args = querystring.parse(qs);
+ if (layers && $(link).hasClass("layers")) {
+ args.layers = layers;
+ }
- 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
- });
+ 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) {
});
}
}
- 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
}
});
- 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());
});
map.attributionControl.setPrefix('');
+ map.hash = L.hash(map);
+
var layers = [
new L.OSM.Mapnik({
attribution: '',
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();
L.OSM.share({
position: position,
- getShortUrl: getShortUrl,
- getUrl: getUrl,
sidebar: sidebar,
short: true
}).addTo(map);
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++) {
}
initializeExport(map);
- initializeBrowse(map);
- initializeNotes(map);
+ initializeBrowse(map, params);
+ initializeNotes(map, params);
});
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) {
//= require templates/browse/feature_list
//= require templates/browse/feature_history
-function initializeBrowse(map) {
+function initializeBrowse(map, params) {
var browseBounds;
var layersById;
var selectedLayer;
}
});
+ 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,
}
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();
//= 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;
});
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);
}
});
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;
}
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() {
},
mapParams: function (search) {
- var params = {}, mapParams = {}, loc;
+ var params = {}, mapParams = {}, bounds, loc;
search = (search || window.location.search).replace('?', '').split(/&|;/);
mapParams.mlat = parseFloat(params.mlat);
}
- if (params.layers) {
- mapParams.layers = params.layers;
- }
-
if (params.node || params.way || params.relation) {
mapParams.object_zoom = true;
}
}
- // 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);
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);
}
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;
}
};
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
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
});
});
- 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>
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();
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();
// 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));
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
--- /dev/null
+/*!
+ * 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);
--- /dev/null
+(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);