]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'osmlab/hash'
authorTom Hughes <tom@compton.nu>
Sun, 4 Aug 2013 11:38:59 +0000 (12:38 +0100)
committerTom Hughes <tom@compton.nu>
Sun, 4 Aug 2013 11:38:59 +0000 (12:38 +0100)
16 files changed:
Vendorfile
app/assets/javascripts/application.js
app/assets/javascripts/changeset.js
app/assets/javascripts/index.js
app/assets/javascripts/index/browse.js
app/assets/javascripts/index/export.js
app/assets/javascripts/index/notes.js.erb
app/assets/javascripts/leaflet.extend.js.erb
app/assets/javascripts/leaflet.share.js
app/assets/javascripts/osm.js.erb
app/controllers/site_controller.rb
app/views/site/_potlatch2.html.erb
app/views/site/id.html.erb
test/functional/site_controller_test.rb
vendor/assets/jquery/jquery.throttle-debounce.js [new file with mode: 0644]
vendor/assets/leaflet/leaflet.hash.js [new file with mode: 0644]

index 2c05a3ecb7b24f0eb6c210706959c1c78540e2db..f9e705b2e6e272c1606d1006c1c82c55511883b3 100644 (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
index cb670da340d0feaf2fbead39da4086e643afef02..cbea58cc17628bc6f4daf90c432c635f8546df67 100644 (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
 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
index 6d4881547b4d07bba65eecaf661c64462b0aa2c0..d9c09bab49ee5a288d200eadad22978a2837a9e5 100644 (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());
 });
index 029c0bfd1f1046fe8ef0617b911a48bd6bb4a1d1..19e1f16de8ebd3b3a596fd5bbc95a7eb4b92c24a 100644 (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) {
index 49090f69cfe1a3a0b3742867d2bdb4d224e3cdca..a9bdbf2b7fac290dd561b9ac1e31b8396d8763ee 100644 (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,
index 417dfabd35be892cdf5e9ad391083d30c35de215..47830f8beea33111284db05b264eebdaeb694650 100644 (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();
index 012538e1d2a4bf98b887773837450b6b2ed343b6..93b60c3e7a09f6c1e907a36e5f71afb7579bba45 100644 (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);
     }
 
index 3b505302f300d23258de983307695f3c1ec5f9aa..a50df013e51799871b0f845744a3044c15c9e605 100644 (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;
index b808bc435b4ccf4648f3156ec15ddc71ce0aaedd..bea877e6686c0da8be68412fd808cc5c55f1e5e4 100644 (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() {
index 81c1e315217711a2c5d23631c4b8b0802dc2fb1d..409cda8d84fd51426c0a0e93feda73a297e38d80 100644 (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;
   }
 };
index 0e26185a1e34325a5fecb87ec3d50856e7a18a7a..b1239d0daa5f16e493e46a2568144d3fbd4dbf07 100644 (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
 
index 51c9aa905e01ae3212fc221d956a23082e40add9..9edf6b5f70a782a5c32736cdc6321ae766ce7988 100644 (file)
     });
   });
 
-  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>
index c0682258d3f9845febaa331cb9d1994a25e1b2eb..ccdad95572954100104e5526a5435c68a5154e2b 100644 (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();
         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));
index 47bc278066f2aa0a195467846977628e341ec6c2..23e6c27a375383bd57069b0d155e4aea5729b671 100644 (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
diff --git a/vendor/assets/jquery/jquery.throttle-debounce.js b/vendor/assets/jquery/jquery.throttle-debounce.js
new file mode 100644 (file)
index 0000000..fa30bdf
--- /dev/null
@@ -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);
diff --git a/vendor/assets/leaflet/leaflet.hash.js b/vendor/assets/leaflet/leaflet.hash.js
new file mode 100644 (file)
index 0000000..26bb8ab
--- /dev/null
@@ -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);