From: Tom Hughes Date: Sun, 4 Aug 2013 11:38:59 +0000 (+0100) Subject: Merge remote-tracking branch 'osmlab/hash' X-Git-Tag: live~5367 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/f9d714dfd36fa56ced4c0a99a6e59c945ff89cca?hp=755585e2306c9384365f61b68c6c6c85565db098 Merge remote-tracking branch 'osmlab/hash' --- diff --git a/Vendorfile b/Vendorfile index 2c05a3ecb..f9e705b2e 100644 --- a/Vendorfile +++ b/Vendorfile @@ -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 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index cb670da34..cbea58cc1 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -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()); - } + 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) { @@ -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 diff --git a/app/assets/javascripts/changeset.js b/app/assets/javascripts/changeset.js index 6d4881547..d9c09bab4 100644 --- a/app/assets/javascripts/changeset.js +++ b/app/assets/javascripts/changeset.js @@ -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()); }); diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index 029c0bfd1..19e1f16de 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -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) { diff --git a/app/assets/javascripts/index/browse.js b/app/assets/javascripts/index/browse.js index 49090f69c..a9bdbf2b7 100644 --- a/app/assets/javascripts/index/browse.js +++ b/app/assets/javascripts/index/browse.js @@ -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, diff --git a/app/assets/javascripts/index/export.js b/app/assets/javascripts/index/export.js index 417dfabd3..47830f8be 100644 --- a/app/assets/javascripts/index/export.js +++ b/app/assets/javascripts/index/export.js @@ -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(); diff --git a/app/assets/javascripts/index/notes.js.erb b/app/assets/javascripts/index/notes.js.erb index 012538e1d..93b60c3e7 100644 --- a/app/assets/javascripts/index/notes.js.erb +++ b/app/assets/javascripts/index/notes.js.erb @@ -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); } diff --git a/app/assets/javascripts/leaflet.extend.js.erb b/app/assets/javascripts/leaflet.extend.js.erb index 3b505302f..a50df013e 100644 --- a/app/assets/javascripts/leaflet.extend.js.erb +++ b/app/assets/javascripts/leaflet.extend.js.erb @@ -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; diff --git a/app/assets/javascripts/leaflet.share.js b/app/assets/javascripts/leaflet.share.js index b808bc435..bea877e66 100644 --- a/app/assets/javascripts/leaflet.share.js +++ b/app/assets/javascripts/leaflet.share.js @@ -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() { diff --git a/app/assets/javascripts/osm.js.erb b/app/assets/javascripts/osm.js.erb index 81c1e3152..409cda8d8 100644 --- a/app/assets/javascripts/osm.js.erb +++ b/app/assets/javascripts/osm.js.erb @@ -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; } }; diff --git a/app/controllers/site_controller.rb b/app/controllers/site_controller.rb index 0e26185a1..b1239d0da 100644 --- a/app/controllers/site_controller.rb +++ b/app/controllers/site_controller.rb @@ -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 diff --git a/app/views/site/_potlatch2.html.erb b/app/views/site/_potlatch2.html.erb index 51c9aa905..9edf6b5f7 100644 --- a/app/views/site/_potlatch2.html.erb +++ b/app/views/site/_potlatch2.html.erb @@ -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); + } + }); diff --git a/app/views/site/id.html.erb b/app/views/site/id.html.erb index c0682258d..ccdad9557 100644 --- a/app/views/site/id.html.erb +++ b/app/views/site/id.html.erb @@ -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)); diff --git a/test/functional/site_controller_test.rb b/test/functional/site_controller_test.rb index 47bc27806..23e6c27a3 100644 --- a/test/functional/site_controller_test.rb +++ b/test/functional/site_controller_test.rb @@ -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 diff --git a/vendor/assets/jquery/jquery.throttle-debounce.js b/vendor/assets/jquery/jquery.throttle-debounce.js new file mode 100644 index 000000000..fa30bdfff --- /dev/null +++ b/vendor/assets/jquery/jquery.throttle-debounce.js @@ -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 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 + // 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 + // 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); diff --git a/vendor/assets/leaflet/leaflet.hash.js b/vendor/assets/leaflet/leaflet.hash.js new file mode 100644 index 000000000..26bb8abf6 --- /dev/null +++ b/vendor/assets/leaflet/leaflet.hash.js @@ -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);