]> git.openstreetmap.org Git - rails.git/commitdiff
Merge remote-tracking branch 'upstream/master' into routing-merge
authorMatt Amos <zerebubuth@gmail.com>
Mon, 10 Nov 2014 16:14:06 +0000 (16:14 +0000)
committerMatt Amos <zerebubuth@gmail.com>
Mon, 10 Nov 2014 16:14:06 +0000 (16:14 +0000)
Conflicts:
app/assets/javascripts/index.js
config/locales/en.yml

29 files changed:
CONFIGURE.md
Gemfile
Gemfile.lock
app/assets/images/browse/path.20.png [new file with mode: 0644]
app/assets/images/browse/tertiary.20.png [new file with mode: 0644]
app/assets/images/browse/track.20.png [new file with mode: 0644]
app/assets/images/browse/wall.20.png [new file with mode: 0644]
app/assets/images/sotm.png [deleted file]
app/assets/images/sprite.png
app/assets/images/sprite.svg
app/assets/javascripts/index.js
app/assets/javascripts/index/query.js [new file with mode: 0644]
app/assets/javascripts/leaflet.query.js [new file with mode: 0644]
app/assets/javascripts/osm.js.erb
app/assets/javascripts/piwik.js
app/assets/javascripts/router.js
app/assets/stylesheets/browse.css.scss
app/assets/stylesheets/common.css.scss
app/controllers/changeset_controller.rb
app/views/browse/query.html.erb [new file with mode: 0644]
app/views/layouts/_head.html.erb
app/views/layouts/map.html.erb
config/example.application.yml
config/example.database.yml
config/i18n-js.yml
config/locales/en.yml
config/routes.rb
db/structure.sql
test/javascripts/osm_test.js

index 36c5586d852216ae5058a6cf5204c0958f25f431..9b7bb90e89e9cfef6af644966d447ed0f49e9c2b 100644 (file)
@@ -120,7 +120,7 @@ For information on contributing changes to the codes, see [CONTRIBUTING.md](CONT
 
 If you want to deploy The Rails Port for production use, you'll need to make a few changes.
 
-* It's not recommended to use `rails server` in production. Our recommended approach is to use [Phusion Passenger](https://www.phusionpassenger.com/).
+* It's not recommended to use `rails server` in production. Our recommended approach is to use [Phusion Passenger](https://www.phusionpassenger.com/). Instructions are available for [setting it up with most web servers](https://www.phusionpassenger.com/documentation_and_support#documentation).
 * Passenger will, by design, use the Production environment and therefore the production database - make sure it contains the appropriate data and user accounts.
 * Your production database will also need the extensions and functions installed - see [INSTALL.md](INSTALL.md)
 * The included version of the map call is quite slow and eats a lot of memory. You should consider using [CGIMap](https://github.com/zerebubuth/openstreetmap-cgimap) instead.
diff --git a/Gemfile b/Gemfile
index fd8e46b9facaac318003176b766082ba5d739bd8..ac030d38d44be8e89318fef6fcd54f8ab427f299 100644 (file)
--- a/Gemfile
+++ b/Gemfile
@@ -1,7 +1,7 @@
 source 'https://rubygems.org'
 
 # Require rails
-gem 'rails', '4.1.6'
+gem 'rails', '4.1.7'
 
 # Require things which have moved to gems in ruby 1.9
 gem 'bigdecimal', "~> 1.1.0", :platforms => :ruby_19
index 48d6013bf938ca071a3f96c3bd8adf7c20b3b8b6..3f66851755c0c68dda3f30c8a892b84361de5804 100644 (file)
@@ -2,40 +2,40 @@ GEM
   remote: https://rubygems.org/
   specs:
     SystemTimer (1.2.3)
-    actionmailer (4.1.6)
-      actionpack (= 4.1.6)
-      actionview (= 4.1.6)
+    actionmailer (4.1.7)
+      actionpack (= 4.1.7)
+      actionview (= 4.1.7)
       mail (~> 2.5, >= 2.5.4)
-    actionpack (4.1.6)
-      actionview (= 4.1.6)
-      activesupport (= 4.1.6)
+    actionpack (4.1.7)
+      actionview (= 4.1.7)
+      activesupport (= 4.1.7)
       rack (~> 1.5.2)
       rack-test (~> 0.6.2)
     actionpack-page_caching (1.0.2)
       actionpack (>= 4.0.0, < 5)
-    actionview (4.1.6)
-      activesupport (= 4.1.6)
+    actionview (4.1.7)
+      activesupport (= 4.1.7)
       builder (~> 3.1)
       erubis (~> 2.7.0)
-    activemodel (4.1.6)
-      activesupport (= 4.1.6)
+    activemodel (4.1.7)
+      activesupport (= 4.1.7)
       builder (~> 3.1)
-    activerecord (4.1.6)
-      activemodel (= 4.1.6)
-      activesupport (= 4.1.6)
+    activerecord (4.1.7)
+      activemodel (= 4.1.7)
+      activesupport (= 4.1.7)
       arel (~> 5.0.0)
-    activesupport (4.1.6)
+    activesupport (4.1.7)
       i18n (~> 0.6, >= 0.6.9)
       json (~> 1.7, >= 1.7.7)
       minitest (~> 5.1)
       thread_safe (~> 0.1)
       tzinfo (~> 1.1)
     arel (5.0.1.20140414130214)
-    autoprefixer-rails (3.1.1.20141001)
+    autoprefixer-rails (3.1.2.20141016)
       execjs
     bigdecimal (1.1.0)
     builder (3.2.2)
-    capybara (2.4.3)
+    capybara (2.4.4)
       mime-types (>= 1.16)
       nokogiri (>= 1.3.3)
       rack (>= 1.0.0)
@@ -54,20 +54,20 @@ GEM
       execjs
     coffee-script-source (1.8.0)
     colorize (0.7.3)
-    composite_primary_keys (7.0.11)
-      activerecord (= 4.1.6)
+    composite_primary_keys (7.0.12)
+      activerecord (~> 4.1.7)
     crass (0.2.1)
     dalli (2.7.2)
     deadlock_retry (1.2.0)
     dynamic_form (1.1.4)
     erubis (2.7.0)
-    execjs (2.2.1)
+    execjs (2.2.2)
     faraday (0.9.0)
       multipart-post (>= 1.2, < 3)
     hike (1.2.3)
     htmlentities (4.3.2)
     http_accept_language (2.0.2)
-    httpclient (2.4.0)
+    httpclient (2.5.3.2)
     i18n (0.6.11)
     iconv (0.1)
     jquery-rails (3.1.2)
@@ -81,23 +81,23 @@ GEM
       jsonify (< 0.4.0)
     jwt (1.0.0)
     kgio (2.9.2)
-    konacha (3.2.4)
+    konacha (3.3.0)
       actionpack (>= 3.1, < 5)
       capybara
       colorize
       railties (>= 3.1, < 5)
       sprockets
     libxml-ruby (2.7.0)
-    mail (2.6.1)
+    mail (2.6.3)
       mime-types (>= 1.16, < 3)
-    mime-types (2.4.1)
-    mini_portile (0.6.0)
+    mime-types (2.4.3)
+    mini_portile (0.6.1)
     minitest (5.4.2)
     multi_json (1.10.1)
     multi_xml (0.5.5)
     multipart-post (2.0.0)
-    nokogiri (1.6.3.1)
-      mini_portile (= 0.6.0)
+    nokogiri (1.6.4.1)
+      mini_portile (~> 0.6.0)
     nokogumbo (1.1.12)
       nokogiri
     oauth (0.4.7)
@@ -136,48 +136,48 @@ GEM
       ruby-openid (>= 2.1.8)
     rack-test (0.6.2)
       rack (>= 1.0)
-    rails (4.1.6)
-      actionmailer (= 4.1.6)
-      actionpack (= 4.1.6)
-      actionview (= 4.1.6)
-      activemodel (= 4.1.6)
-      activerecord (= 4.1.6)
-      activesupport (= 4.1.6)
+    rails (4.1.7)
+      actionmailer (= 4.1.7)
+      actionpack (= 4.1.7)
+      actionview (= 4.1.7)
+      activemodel (= 4.1.7)
+      activerecord (= 4.1.7)
+      activesupport (= 4.1.7)
       bundler (>= 1.3.0, < 2.0)
-      railties (= 4.1.6)
+      railties (= 4.1.7)
       sprockets-rails (~> 2.0)
     rails-i18n (4.0.3)
       i18n (~> 0.6)
       railties (~> 4.0)
-    railties (4.1.6)
-      actionpack (= 4.1.6)
-      activesupport (= 4.1.6)
+    railties (4.1.7)
+      actionpack (= 4.1.7)
+      activesupport (= 4.1.7)
       rake (>= 0.8.7)
       thor (>= 0.18.1, < 2.0)
     rake (10.3.2)
-    redcarpet (3.1.2)
+    redcarpet (3.2.0)
     rinku (1.7.3)
-    ruby-openid (2.5.0)
-    sanitize (3.0.2)
+    ruby-openid (2.6.0)
+    sanitize (3.0.3)
       crass (~> 0.2.0)
       nokogiri (>= 1.4.4)
       nokogumbo (= 1.1.12)
     sass (3.2.19)
-    sass-rails (4.0.3)
+    sass-rails (4.0.4)
       railties (>= 4.0.0, < 5.0)
-      sass (~> 3.2.0)
-      sprockets (~> 2.8, <= 2.11.0)
+      sass (~> 3.2.2)
+      sprockets (~> 2.8, < 2.12)
       sprockets-rails (~> 2.0)
     soap4r-ruby1.9 (2.0.5)
-    sprockets (2.11.0)
+    sprockets (2.11.3)
       hike (~> 1.2)
       multi_json (~> 1.0)
       rack (~> 1.0)
       tilt (~> 1.1, != 1.3.0)
-    sprockets-rails (2.1.4)
+    sprockets-rails (2.2.0)
       actionpack (>= 3.0)
       activesupport (>= 3.0)
-      sprockets (~> 2.8)
+      sprockets (>= 2.8, < 4.0)
     thor (0.19.1)
     thread_safe (0.3.4)
     tilt (1.4.1)
@@ -190,7 +190,7 @@ GEM
     validates_email_format_of (1.6.1)
       i18n
     vendorer (0.1.16)
-    websocket-driver (0.3.5)
+    websocket-driver (0.4.0)
     xpath (2.0.0)
       nokogiri (~> 1.3)
 
@@ -227,7 +227,7 @@ DEPENDENCIES
   psych
   r2
   rack-cors
-  rails (= 4.1.6)
+  rails (= 4.1.7)
   rails-i18n (~> 4.0.0)
   redcarpet
   rinku (>= 1.2.2)
diff --git a/app/assets/images/browse/path.20.png b/app/assets/images/browse/path.20.png
new file mode 100644 (file)
index 0000000..13a090e
Binary files /dev/null and b/app/assets/images/browse/path.20.png differ
diff --git a/app/assets/images/browse/tertiary.20.png b/app/assets/images/browse/tertiary.20.png
new file mode 100644 (file)
index 0000000..3dd7528
Binary files /dev/null and b/app/assets/images/browse/tertiary.20.png differ
diff --git a/app/assets/images/browse/track.20.png b/app/assets/images/browse/track.20.png
new file mode 100644 (file)
index 0000000..36e579e
Binary files /dev/null and b/app/assets/images/browse/track.20.png differ
diff --git a/app/assets/images/browse/wall.20.png b/app/assets/images/browse/wall.20.png
new file mode 100644 (file)
index 0000000..12dffce
Binary files /dev/null and b/app/assets/images/browse/wall.20.png differ
diff --git a/app/assets/images/sotm.png b/app/assets/images/sotm.png
deleted file mode 100644 (file)
index 3df0287..0000000
Binary files a/app/assets/images/sotm.png and /dev/null differ
index e7490c84cbca1a7852d89a3011b3c1f23d541e3f..e3ed0e7f81185d93ae6d75b8aab27d20d521242a 100644 (file)
Binary files a/app/assets/images/sprite.png and b/app/assets/images/sprite.png differ
index b61018d135276d031f0fb64a7e1611ac03305ecb..b50b969e909620ce71400c28d5c54c4dc409e1f4 100644 (file)
@@ -13,8 +13,8 @@
    height="200"
    id="svg2"
    version="1.1"
-   inkscape:version="0.48.2 r9819"
-   inkscape:export-filename="/Users/tmcw/src/openstreetmap-website/app/assets/images/sprite.png"
+   inkscape:version="0.48.4 r9939"
+   inkscape:export-filename="/home/tom/rails/app/assets/images/sprite.png"
    inkscape:export-xdpi="90"
    inkscape:export-ydpi="90"
    sodipodi:docname="sprite.svg">
      borderopacity="1.0"
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
-     inkscape:zoom="4"
-     inkscape:cx="210.42032"
-     inkscape:cy="175.54808"
+     inkscape:zoom="16"
+     inkscape:cx="258.2457"
+     inkscape:cy="193.60262"
      inkscape:document-units="px"
      inkscape:current-layer="layer1"
      showgrid="false"
-     inkscape:window-width="1436"
-     inkscape:window-height="856"
-     inkscape:window-x="4"
-     inkscape:window-y="0"
+     inkscape:window-width="1366"
+     inkscape:window-height="702"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
      inkscape:window-maximized="1"
      showguides="true"
      inkscape:guide-bbox="true"
        orientation="1,0"
        position="260,195"
        id="guide11761" />
+    <sodipodi:guide
+       orientation="1,0"
+       position="280,153.875"
+       id="guide3019" />
   </sodipodi:namedview>
   <metadata
      id="metadata7">
        inkscape:export-filename="/Users/saman/work_repos/osm-redesign/renders/share-1.png"
        inkscape:export-xdpi="90"
        inkscape:export-ydpi="90" />
+    <text
+       xml:space="preserve"
+       style="font-size:20px;font-style:normal;font-weight:bold;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans Bold"
+       x="264.8125"
+       y="869.62622"
+       id="text3021"
+       sodipodi:linespacing="125%"><tspan
+         sodipodi:role="line"
+         id="tspan3023"
+         x="264.8125"
+         y="869.62622">?</tspan></text>
   </g>
 </svg>
index b89d6c71d55cf9e56b5d53a40fc788c5803e7d67..66dfa5b6fac3975a2c00daa6b651d76034303ef0 100644 (file)
@@ -6,6 +6,7 @@
 //= require leaflet.note
 //= require leaflet.share
 //= require leaflet.polyline
+//= require leaflet.query
 //= require index/search
 //= require index/browse
 //= require index/export
@@ -15,6 +16,7 @@
 //= require index/new_note
 //= require index/directions
 //= require index/changeset
+//= require index/query
 //= require router
 
 $(document).ready(function () {
@@ -126,6 +128,11 @@ $(document).ready(function () {
     sidebar: sidebar
   }).addTo(map);
 
+  L.OSM.query({
+    position: position,
+    sidebar: sidebar
+  }).addTo(map);
+
   L.control.scale()
     .addTo(map);
 
@@ -157,15 +164,6 @@ $(document).ready(function () {
     $.cookie("_osm_location", OSM.locationCookie(map), { expires: expiry, path: "/" });
   });
 
-  if ($.cookie('_osm_sotm') == 'hide') {
-    $('#sotm').hide();
-  }
-
-  $('#sotm .close').on('click', function() {
-    $('#sotm').hide();
-    $.cookie("_osm_sotm", 'hide', { expires: expiry });
-  });
-
   if ($.cookie('_osm_welcome') == 'hide') {
     $('.welcome').hide();
   }
@@ -294,7 +292,8 @@ $(document).ready(function () {
     "/node/:id(/history)":         OSM.Browse(map, 'node'),
     "/way/:id(/history)":          OSM.Browse(map, 'way'),
     "/relation/:id(/history)":     OSM.Browse(map, 'relation'),
-    "/changeset/:id":              OSM.Changeset(map)
+    "/changeset/:id":              OSM.Changeset(map),
+    "/query":                      OSM.Query(map)
   });
 
   if (OSM.preferred_editor == "remote" && document.location.pathname == "/edit") {
diff --git a/app/assets/javascripts/index/query.js b/app/assets/javascripts/index/query.js
new file mode 100644 (file)
index 0000000..1f45a68
--- /dev/null
@@ -0,0 +1,337 @@
+//= require jquery.simulate
+
+OSM.Query = function(map) {
+  var protocol = document.location.protocol === "https:" ? "https:" : "http:",
+    url = protocol + OSM.OVERPASS_URL,
+    queryButton = $(".control-query .control-button"),
+    uninterestingTags = ['source', 'source_ref', 'source:ref', 'history', 'attribution', 'created_by', 'tiger:county', 'tiger:tlid', 'tiger:upload_uuid', 'KSJ2:curve_id', 'KSJ2:lat', 'KSJ2:lon', 'KSJ2:coordinate', 'KSJ2:filename', 'note:ja'],
+    marker;
+
+  var featureStyle = {
+    color: "#FF6200",
+    weight: 4,
+    opacity: 1,
+    fillOpacity: 0.5,
+    clickable: false
+  };
+
+  queryButton.on("click", function (e) {
+    e.preventDefault();
+    e.stopPropagation();
+
+    if (queryButton.hasClass("active")) {
+      disableQueryMode();
+    } else if (!queryButton.hasClass("disabled")) {
+      enableQueryMode();
+    }
+  }).on("disabled", function (e) {
+    if (queryButton.hasClass("active")) {
+      map.off("click", clickHandler);
+      $(map.getContainer()).removeClass("query-active").addClass("query-disabled");
+      $(this).tooltip("show");
+    }
+  }).on("enabled", function (e) {
+    if (queryButton.hasClass("active")) {
+      map.on("click", clickHandler);
+      $(map.getContainer()).removeClass("query-disabled").addClass("query-active");
+      $(this).tooltip("hide");
+    }
+  });
+
+  $("#sidebar_content")
+    .on("mouseover", ".query-results li.query-result", function () {
+      var geometry = $(this).data("geometry")
+      if (geometry) map.addLayer(geometry);
+      $(this).addClass("selected");
+    })
+    .on("mouseout", ".query-results li.query-result", function () {
+      var geometry = $(this).data("geometry")
+      if (geometry) map.removeLayer(geometry);
+      $(this).removeClass("selected");
+    })
+    .on("mousedown", ".query-results li.query-result", function (e) {
+      var moved = false;
+      $(this).one("click", function (e) {
+        if (!moved) {
+          var geometry = $(this).data("geometry")
+          if (geometry) map.removeLayer(geometry);
+
+          if (!$(e.target).is('a')) {
+            $(this).find("a").simulate("click", e);
+          }
+        }
+      }).one("mousemove", function () {
+        moved = true;
+      });
+    });
+
+  function interestingFeature(feature, origin, radius) {
+    if (feature.tags) {
+      for (var key in feature.tags) {
+        if (uninterestingTags.indexOf(key) < 0) {
+          return true;
+        }
+      }
+    }
+
+    return false;
+  }
+
+  function featurePrefix(feature) {
+    var tags = feature.tags;
+    var prefix = "";
+
+    if (tags.boundary === "administrative" && tags.admin_level) {
+      prefix =
+      I18n.t("geocoder.search_osm_nominatim.admin_levels.level" + tags.admin_level, {
+        defaultValue: I18n.t("geocoder.search_osm_nominatim.prefix.boundary.administrative")
+      })
+    } else {
+      var prefixes = I18n.t("geocoder.search_osm_nominatim.prefix");
+
+      for (var key in tags) {
+        var value = tags[key];
+
+        if (prefixes[key]) {
+          if (prefixes[key][value]) {
+            return prefixes[key][value];
+          } else {
+            var first = value.substr(0, 1).toUpperCase(),
+              rest = value.substr(1).replace(/_/g, " ");
+
+            return first + rest;
+          }
+        }
+      }
+    }
+
+    if (!prefix) {
+      prefix = I18n.t("javascripts.query." + feature.type);
+    }
+
+    return prefix;
+  }
+
+  function featureName(feature) {
+    var tags = feature.tags,
+      locales = I18n.locales.get();
+
+    for (var i = 0; i < locales.length; i++) {
+      if (tags["name:" + locales[i]]) {
+        return tags["name:" + locales[i]];
+      }
+    }
+
+    if (tags["name"]) {
+      return tags["name"];
+    } else if (tags["ref"]) {
+      return tags["ref"];
+    } else if (tags["addr:housename"]) {
+      return tags["addr:housename"];
+    } else if (tags["addr:housenumber"] && tags["addr:street"]) {
+      return tags["addr:housenumber"] + " " + tags["addr:street"];
+    } else {
+      return "#" + feature.id;
+    }
+  }
+
+  function featureGeometry(feature) {
+    var geometry;
+
+    if (feature.type === "node" && feature.lat && feature.lon) {
+      geometry = L.circleMarker([feature.lat, feature.lon], featureStyle);
+    } else if (feature.type === "way" && feature.geometry) {
+      geometry = L.polyline(feature.geometry.filter(function (point) {
+        return point !== null;
+      }).map(function (point) {
+        return [point.lat, point.lon];
+      }), featureStyle);
+    } else if (feature.type === "relation" && feature.members) {
+      geometry = L.featureGroup(feature.members.map(featureGeometry).filter(function (geometry) {
+        return geometry !== undefined;
+      }));
+    }
+
+    return geometry;
+  }
+
+  function runQuery(latlng, radius, query, $section, compare) {
+    var $ul = $section.find("ul");
+
+    $ul.empty();
+    $section.show();
+
+    $section.find(".loader").oneTime(1000, "loading", function () {
+      $(this).show();
+    });
+
+    if ($section.data("ajax")) {
+      $section.data("ajax").abort();
+    }
+
+    $section.data("ajax", $.ajax({
+      url: url,
+      method: "POST",
+      data: {
+        data: "[timeout:5][out:json];" + query,
+      },
+      success: function(results) {
+        var elements;
+
+        $section.find(".loader").stopTime("loading").hide();
+
+        if (compare) {
+          elements = results.elements.sort(compare);
+        } else {
+          elements = results.elements;
+        }
+
+        for (var i = 0; i < elements.length; i++) {
+          var element = elements[i];
+
+          if (interestingFeature(element, latlng, radius)) {
+            var $li = $("<li>")
+              .addClass("query-result")
+              .data("geometry", featureGeometry(element))
+              .appendTo($ul);
+            var $p = $("<p>")
+              .text(featurePrefix(element) + " ")
+              .appendTo($li);
+
+            $("<a>")
+              .attr("href", "/" + element.type + "/" + element.id)
+              .text(featureName(element))
+              .appendTo($p);
+          }
+        }
+
+        if ($ul.find("li").length == 0) {
+          $("<li>")
+            .text(I18n.t("javascripts.query.nothing_found"))
+            .appendTo($ul);
+        }
+      },
+      error: function(xhr, status, error) {
+        $section.find(".loader").stopTime("loading").hide();
+
+        $("<li>")
+          .text(I18n.t("javascripts.query." + status, { server: url, error: error }))
+          .appendTo($ul);
+      }
+    }));
+  }
+
+  function compareSize(feature1, feature2) {
+    var width1 = feature1.bounds.maxlon - feature1.bounds.minlon,
+      height1 = feature1.bounds.maxlat - feature1.bounds.minlat,
+      area1 = width1 * height1,
+      width2 = feature2.bounds.maxlat - feature2.bounds.minlat,
+      height2 = feature2.bounds.maxlat - feature2.bounds.minlat,
+      area2 = width2 * height2;
+
+    return area1 - area2;
+  }
+
+  /*
+   * To find nearby objects we ask overpass for the union of the
+   * following sets:
+   *
+   *   node(around:<radius>,<lat>,lng>)
+   *   way(around:<radius>,<lat>,lng>)
+   *   relation(around:<radius>,<lat>,lng>)
+   *
+   * to find enclosing objects we first find all the enclosing areas:
+   *
+   *   is_in(<lat>,<lng>)->.a
+   *
+   * and then return the union of the following sets:
+   *
+   *   relation(pivot.a)
+   *   way(pivot.a)
+   *
+   * In both cases we then ask to retrieve tags and the geometry
+   * for each object.
+   */
+  function queryOverpass(lat, lng) {
+    var latlng = L.latLng(lat, lng),
+      bounds = map.getBounds(),
+      bbox = bounds.getSouth() + "," + bounds.getWest() + "," + bounds.getNorth() + "," + bounds.getEast(),
+      radius = 10 * Math.pow(1.5, 19 - map.getZoom()),
+      around = "around:" + radius + "," + lat + "," + lng,
+      nodes = "node(" + around + ")",
+      ways = "way(" + around + ")",
+      relations = "relation(" + around + ")",
+      nearby = "(" + nodes + ";" + ways + ");out tags geom(" + bbox + ");" + relations + ";out geom(" + bbox + ");",
+      isin = "is_in(" + lat + "," + lng + ")->.a;way(pivot.a);out tags geom(" + bbox + ");relation(pivot.a);out tags bb;";
+
+    $("#sidebar_content .query-intro")
+      .hide();
+
+    if (marker) map.removeLayer(marker);
+    marker = L.circle(latlng, radius, featureStyle).addTo(map);
+
+    $(document).everyTime(75, "fadeQueryMarker", function (i) {
+      if (i == 10) {
+        map.removeLayer(marker);
+      } else {
+        marker.setStyle({
+          opacity: 1 - i * 0.1,
+          fillOpacity: 0.5 - i * 0.05
+        });
+      }
+    }, 10);
+
+    runQuery(latlng, radius, nearby, $("#query-nearby"));
+    runQuery(latlng, radius, isin, $("#query-isin"), compareSize);
+  }
+
+  function clickHandler(e) {
+    var precision = OSM.zoomPrecision(map.getZoom()),
+      lat = e.latlng.lat.toFixed(precision),
+      lng = e.latlng.lng.toFixed(precision);
+
+    OSM.router.route("/query?lat=" + lat + "&lon=" + lng);
+  }
+
+  function enableQueryMode() {
+    queryButton.addClass("active");
+    map.on("click", clickHandler);
+    $(map.getContainer()).addClass("query-active");
+  }
+
+  function disableQueryMode() {
+    if (marker) map.removeLayer(marker);
+    $(map.getContainer()).removeClass("query-active").removeClass("query-disabled");
+    map.off("click", clickHandler);
+    queryButton.removeClass("active");
+  }
+
+  var page = {};
+
+  page.pushstate = page.popstate = function(path) {
+    OSM.loadSidebarContent(path, function () {
+      page.load(path, true);
+    });
+  };
+
+  page.load = function(path, noCentre) {
+    var params = querystring.parse(path.substring(path.indexOf('?') + 1)),
+      latlng = L.latLng(params.lat, params.lon);
+
+    if (!window.location.hash && !noCentre && !map.getBounds().contains(latlng)) {
+      OSM.router.withoutMoveListener(function () {
+        map.setView(latlng, 15);
+      });
+    }
+
+    queryOverpass(params.lat, params.lon);
+  };
+
+  page.unload = function(sameController) {
+    if (!sameController) {
+      disableQueryMode();
+    }
+  };
+
+  return page;
+};
diff --git a/app/assets/javascripts/leaflet.query.js b/app/assets/javascripts/leaflet.query.js
new file mode 100644 (file)
index 0000000..9064872
--- /dev/null
@@ -0,0 +1,38 @@
+L.OSM.query = function (options) {
+  var control = L.control(options);
+
+  control.onAdd = function (map) {
+    var $container = $('<div>')
+      .attr('class', 'control-query');
+
+    var link = $('<a>')
+      .attr('class', 'control-button')
+      .attr('href', '#')
+      .html('<span class="icon query"></span>')
+      .appendTo($container);
+
+    map.on('zoomend', update);
+
+    update();
+
+    function update() {
+      var wasDisabled = link.hasClass('disabled'),
+        isDisabled = map.getZoom() < 14;
+      link
+        .toggleClass('disabled', isDisabled)
+        .attr('data-original-title', I18n.t(isDisabled ?
+          'javascripts.site.queryfeature_disabled_tooltip' :
+          'javascripts.site.queryfeature_tooltip'));
+
+      if (isDisabled && !wasDisabled) {
+        link.trigger('disabled');
+      } else if (wasDisabled && !isDisabled) {
+        link.trigger('enabled');
+      }
+    }
+
+    return $container[0];
+  };
+
+  return control;
+};
index 033b2de8122e991513ab9f431ba8a504b4b2c4aa..15c1682d5f0e14e925a8c5a4496da16ff577dc02 100644 (file)
@@ -8,6 +8,7 @@ OSM = {
   API_VERSION:           <%= API_VERSION.to_json %>,
   STATUS:                <%= STATUS.to_json %>,
   MAX_NOTE_REQUEST_AREA: <%= MAX_NOTE_REQUEST_AREA.to_json %>,
+  OVERPASS_URL:          <%= OVERPASS_URL.to_json %>,
 
   apiUrl: function (object) {
     var url = "/api/" + OSM.API_VERSION + "/" + object.type + "/" + object.id;
@@ -71,10 +72,6 @@ OSM = {
       mapParams.bounds = L.latLngBounds(
         [parseFloat(params.minlat), parseFloat(params.minlon)],
         [parseFloat(params.maxlat), parseFloat(params.maxlon)]);
-    } else if (params.lon && params.lat) {
-      mapParams.lon = parseFloat(params.lon);
-      mapParams.lat = parseFloat(params.lat);
-      mapParams.zoom = parseInt(params.zoom || 5);
     } else if (params.mlon && params.mlat) {
       mapParams.lon = parseFloat(params.mlon);
       mapParams.lat = parseFloat(params.mlat);
@@ -174,5 +171,20 @@ OSM = {
       zoom = map.getZoom(),
       precision = OSM.zoomPrecision(zoom);
     return [center.lng.toFixed(precision), center.lat.toFixed(precision), zoom, map.getLayersCode()].join('|');
+  },
+
+  distance: function(latlng1, latlng2) {
+    var lat1 = latlng1.lat * Math.PI / 180,
+      lng1 = latlng1.lng * Math.PI / 180,
+      lat2 = latlng2.lat * Math.PI / 180,
+      lng2 = latlng2.lng * Math.PI / 180,
+      latdiff = lat2 - lat1,
+      lngdiff = lng2 - lng1;
+
+    return 6372795 * 2 * Math.asin(
+      Math.sqrt(
+        Math.pow(Math.sin(latdiff / 2), 2) + 
+        Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(lngdiff / 2), 2)
+      ));
   }
 };
index a48e903add43ee638029805e80323307b20fc596..d327296f63621e97aacecd7e4b7bdbd043134540 100644 (file)
@@ -10,6 +10,10 @@ if (OSM.PIWIK) {
       success: function () {
         piwikTracker = Piwik.getTracker(base + "piwik.php", OSM.PIWIK.site);
       
+        if (OSM.user) {
+          piwikTracker.setUserId(OSM.user);
+        }
+
         piwikTracker.trackPageView();
         piwikTracker.enableLinkTracking();
       
index c3f13f9dffc1afc78aff160071b5657184b742e0..904134fc078630cedbd86c5f0771abafa950e366 100644 (file)
@@ -76,6 +76,8 @@ OSM.Router = function(map, rts) {
         });
       }
 
+      params = params.concat(Array.prototype.slice.call(arguments, 2));
+
       return (controller[action] || $.noop).apply(controller, params);
     };
 
@@ -101,11 +103,12 @@ OSM.Router = function(map, rts) {
   if (window.history && window.history.pushState) {
     $(window).on('popstate', function(e) {
       if (!e.originalEvent.state) return; // Is it a real popstate event or just a hash change?
-      var path = window.location.pathname + window.location.search;
+      var path = window.location.pathname + window.location.search,
+        route = routes.recognize(path);
       if (path === currentPath) return;
-      currentRoute.run('unload');
+      currentRoute.run('unload', null, route === currentRoute);
       currentPath = path;
-      currentRoute = routes.recognize(currentPath);
+      currentRoute = route;
       currentRoute.run('popstate', currentPath);
       map.setState(e.originalEvent.state, {animate: false});
     });
@@ -114,7 +117,7 @@ OSM.Router = function(map, rts) {
       var path = url.replace(/#.*/, ''),
         route = routes.recognize(path);
       if (!route) return false;
-      currentRoute.run('unload');
+      currentRoute.run('unload', null, route === currentRoute);
       var state = OSM.parseHash(url);
       map.setState(state);
       window.history.pushState(state, document.title, url);
index 0aa762a12c2150a3132aad594bc89117c149b98e..d30f5a8a144e40679b795c94b8f9d97dd6288bd6 100644 (file)
 .aeroway.runway::before { content: image-url('browse/runway.20.png'); }
 .aeroway.taxiway::before { content: image-url('browse/taxiway.20.png'); }
 
+.barrier.wall::before { content: image-url('browse/wall.20.png'); }
+
 .building::before { content: image-url('browse/building.png'); }
 
 .highway.bridleway::before { content: image-url('browse/bridleway.20.png'); }
 .highway.footway::before { content: image-url('browse/footway.20.png'); }
 .highway.motorway::before { content: image-url('browse/motorway.20.png'); }
 .highway.motorway_link::before { content: image-url('browse/motorway.20.png'); }
+.highway.path::before { content: image-url('browse/path.20.png'); }
 .highway.pedestrian::before { content: image-url('browse/service.20.png'); }
 .highway.primary::before { content: image-url('browse/primary.20.png'); }
 .highway.primary_link::before { content: image-url('browse/primary.20.png'); }
 .highway.secondary::before { content: image-url('browse/secondary.20.png'); }
 .highway.secondary_link::before { content: image-url('browse/secondary.20.png'); }
 .highway.service::before { content: image-url('browse/service.20.png'); }
+.highway.tertiary::before { content: image-url('browse/tertiary.20.png'); }
+.highway.track::before { content: image-url('browse/track.20.png'); }
 .highway.trunk::before { content: image-url('browse/trunk.20.png'); }
 .highway.trunk_link::before { content: image-url('browse/trunk.20.png'); }
 .highway.unclassified::before { content: image-url('browse/unclassified.20.png'); }
index aa729640eadabf7b2e61ca06a528f38a94d6f48a..95e5d8328dd144460847ed268e8135ea1068cd96 100644 (file)
@@ -169,7 +169,7 @@ small, aside {
 .icon.close:hover { background-position: -200px -20px; }
 .icon.check       { background-position: -220px 0; }
 .icon.note        { background-position: -240px 0; }
-.icon.gear        { background-position: -260px 0; }
+.icon.query       { background-position: -260px 0; }
 
 /* Rules for links */
 
@@ -566,16 +566,16 @@ nav.secondary {
     background-color: black;
   }
 
-  &.active {
-    background-color: #9ed485;
-  }
-
   &.disabled {
     background-color: #333;
     background-color: rgba(0,0,0,.5);
     cursor: default;
   }
 
+  &.active {
+    background-color: #9ed485;
+  }
+
   .icon {
     margin: 10px;
   }
@@ -680,6 +680,14 @@ nav.secondary {
   #map {
     height: 100%;
     overflow: hidden;
+
+    &.query-active {
+      cursor: help;
+    }
+
+    &.query-disabled {
+      cursor: not-allowed;
+    }
   }
 
   #map-ui {
@@ -1180,6 +1188,34 @@ tr.turn:hover {
     overflow: hidden;
     margin: 0 0 10px 10px;
   }
+
+  .query-intro p {
+    padding: $lineheight $lineheight $lineheight/2;
+  }
+
+  .query-results {
+    display: none;
+
+    h3 {
+      padding: $lineheight $lineheight $lineheight/2;
+      margin: 0;
+    }
+
+    ul {
+      li {
+        padding: 15px 20px;
+        border-bottom: 1px solid #ddd;
+
+        &.query-result {
+          cursor: pointer;
+        }
+
+        &.selected {
+          background: #FFFFE6;
+        }
+      }
+    }
+  }
 }
 
 /* Rules for export sidebar */
@@ -2618,36 +2654,6 @@ input.richtext_title[type="text"] {
   }
 }
 
-#sidebar #sotm {
-  padding: 10px;
-  min-height: 120px;
-
-  img {
-    float: left;
-    width: 100px;
-    height: 100px;
-  }
-
-  h2 {
-    margin-left: 100px;
-    padding: 7px 10px 6px 15px;
-  }
-
-  p {
-    margin-left: 100px;
-    padding: 6px 10px 7px 15px;
-  }
-
-  a {
-    color: $darkgrey;
-  }
-
-  :hover {
-    text-decoration: none;
-    color: darken($darkgrey, 25%);
-  }
-}
-
 @import 'browse';
 
 @media only screen and (max-width:960px) {
index 646ba2fa35a0160522bcfa69bf4b946edaf6f066..adda8c20c0fe536a882ad7894ef0240973725368 100644 (file)
@@ -5,18 +5,18 @@ class ChangesetController < ApplicationController
   require 'xml/libxml'
 
   skip_before_filter :verify_authenticity_token, :except => [:list]
-  before_filter :authorize_web, :only => [:list, :feed]
-  before_filter :set_locale, :only => [:list, :feed]
+  before_filter :authorize_web, :only => [:list, :feed, :comments_feed]
+  before_filter :set_locale, :only => [:list, :feed, :comments_feed]
   before_filter :authorize, :only => [:create, :update, :delete, :upload, :include, :close, :comment, :subscribe, :unsubscribe, :hide_comment, :unhide_comment]
   before_filter :require_moderator, :only => [:hide_comment, :unhide_comment]
   before_filter :require_allow_write_api, :only => [:create, :update, :delete, :upload, :include, :close, :comment, :subscribe, :unsubscribe, :hide_comment, :unhide_comment]
   before_filter :require_public_data, :only => [:create, :update, :delete, :upload, :include, :close, :comment, :subscribe, :unsubscribe]
   before_filter :check_api_writable, :only => [:create, :update, :delete, :upload, :include, :comment, :subscribe, :unsubscribe, :hide_comment, :unhide_comment]
   before_filter :check_api_readable, :except => [:create, :update, :delete, :upload, :download, :query, :list, :feed, :comment, :subscribe, :unsubscribe, :comments_feed]
-  before_filter(:only => [:list, :feed]) { |c| c.check_database_readable(true) }
+  before_filter(:only => [:list, :feed, :comments_feed]) { |c| c.check_database_readable(true) }
   after_filter :compress_output
-  around_filter :api_call_handle_error, :except => [:list, :feed]
-  around_filter :web_timeout, :only => [:list, :feed]
+  around_filter :api_call_handle_error, :except => [:list, :feed, :comments_feed]
+  around_filter :web_timeout, :only => [:list, :feed, :comments_feed]
 
   # Helper methods for checking consistency
   include ConsistencyValidations
diff --git a/app/views/browse/query.html.erb b/app/views/browse/query.html.erb
new file mode 100644 (file)
index 0000000..629d84c
--- /dev/null
@@ -0,0 +1,22 @@
+<% set_title(t "browse.query.title") %>
+
+<h2>
+  <a class="geolink" href="<%= root_path %>"><span class="icon close"></span></a>
+  <%= t "browse.query.title" %>
+</h2>
+
+<div class="query-intro">
+  <p><%= t("browse.query.introduction") %></p>
+</div>
+
+<div id="query-nearby" class="query-results">
+  <h3><%= t("browse.query.nearby") %></h3>
+  <%= image_tag "searching.gif", :class => "loader" %>
+  <ul class="query-results-list"></ul>
+</div>
+
+<div id="query-isin" class="query-results">
+  <h3><%= t("browse.query.enclosing") %></h3>
+  <%= image_tag "searching.gif", :class => "loader" %>
+  <ul class="query-results-list"></ul>
+</div>
index 1c91e68ec1aac9cd28cbbeb4eb7cbff753cc8209..b879d90bdec74318f3b387813fb37a0e0ebdd347 100644 (file)
     I18n.defaultLocale = "<%= I18n.default_locale %>";
     I18n.locale = "<%= I18n.locale %>";
     I18n.fallbacks = true;
-    <% if @user and !@user.home_lon.nil? and !@user.home_lat.nil? -%>
+    <% if @user -%>
+    OSM.user = <%= @user.id.to_json.html_safe %>;
+    <% unless @user.home_lon.nil? or @user.home_lat.nil? -%>
     OSM.home = <%= { :lat => @user.home_lat, :lon => @user.home_lon }.to_json.html_safe %>;
     <% end -%>
+    <% end -%>
     <% if session[:location] -%>
     OSM.location = <%= session[:location].to_json.html_safe %>;
     <% end -%>
index 4715570536f53cb3dcb6f002b30fd9635ffe70b0..e72214cbefe86fae1d9e6c816bd99766fec39c3c 100644 (file)
       <p class="error"><%= t 'layouts.osm_read_only' %></p>
     <% end %>
 
-    <div id="sotm">
-      <a href="http://www.stateofthemap.org/?l=en"><%= image_tag "sotm.png" %></a>
-      <h2>
-        <a><span class="icon close"></span></a>
-        <a href="http://www.stateofthemap.org/?l=en"><%= t 'layouts.sotm_header' %></a>
-      </h2>
-      <p><a href="http://www.stateofthemap.org/?l=en">
-        <%= t 'layouts.sotm_line_1' %>
-      <br />
-        <%= t 'layouts.sotm_line_2' %>
-      <br />
-        <%= t 'layouts.sotm_line_3' %>
-      </a></p>
-    </div>
-
     <div id="flash">
       <%= render :partial => "layouts/flash" %>
     </div>
index 3fbebdc766c5d82424682433381aeeb5246463ca..e7915c96b7699924de9f98a1a06e8479a0575c07 100644 (file)
@@ -89,6 +89,8 @@ defaults: &defaults
     - ".*\\.googleapis\\.com/.*"
     - ".*\\.google\\.com/.*"
     - ".*\\.google\\.ru/.*"
+  # URL of Overpass instance to use for feature queries
+  overpass_url: "//overpass-api.de/api/interpreter"
 
 development:
   <<: *defaults
index 7e1ffc07d02817f3473b386af159886eeabed4e8..4c77998da1d38fbe45a6b2766e3ebce7f26a0a86 100644 (file)
@@ -1,4 +1,4 @@
-# Using a recent release (8.3 or higher) of PostgreSQL (http://postgresql.org/) is recommended.
+# Using a recent release (9.1 or higher) of PostgreSQL (http://postgresql.org/) is recommended.
 # See https://github.com/openstreetmap/openstreetmap-website/blob/master/INSTALL.md#database-setup for detailed setup instructions.
 #
 development:
index 026ece64c11053f181dc1220921a187c683e5927..369fa340a2cd6611c538e9e93706ed0a7ea40154 100644 (file)
@@ -31,3 +31,4 @@ translations:
     - "*.site.sidebar.search_results"
     - "*.diary_entry.edit.marker_text"
     - "*.layouts.project_name.title"
+    - "*.geocoder.search_osm_nominatim.*"
index 7548833d855c2d62b4e9b2340247105eb7c80a15..d2797511bec70a6de1a59c17394111d514aefc62 100644 (file)
@@ -204,6 +204,11 @@ en:
       reopened_by: "Reactivated by %{user} <abbr title='%{exact_time}'>%{when} ago</abbr>"
       reopened_by_anonymous: "Reactivated by anonymous <abbr title='%{exact_time}'>%{when} ago</abbr>"
       hidden_by: "Hidden by %{user} <abbr title='%{exact_time}'>%{when} ago</abbr>"
+    query:
+      title: "Query Features"
+      introduction: "Click on the map to find nearby features."
+      nearby: "Nearby features"
+      enclosing: "Enclosing features"
   changeset:
     changeset_paging_nav:
       showing_page: "Page %{page}"
@@ -521,7 +526,7 @@ en:
           primary_link: "Primary Road"
           proposed: "Proposed Road"
           raceway: "Raceway"
-          residential: "Residential"
+          residential: "Residential Road"
           rest_area: "Rest Area"
           road: "Road"
           secondary: "Secondary Road"
@@ -732,6 +737,8 @@ en:
           tram: "Tramway"
           tram_stop: "Tram Stop"
           yard: "Railway Yard"
+        route:
+          bus: "Bus Route"
         shop:
           alcohol: "Off License"
           antiques: "Antiques"
@@ -937,10 +944,6 @@ en:
       text: Make a Donation
     learn_more: "Learn More"
     more: More
-    sotm_header: State of the Map 2014
-    sotm_line_1: 8th Annual Conference
-    sotm_line_2: November 7th-9th 2014
-    sotm_line_3: Buenos Aires, Argentina
   license_page:
     foreign:
       title: About this translation
@@ -2132,6 +2135,8 @@ en:
       createnote_disabled_tooltip: Zoom in to add a note to the map
       map_notes_zoom_in_tooltip: Zoom in to see map notes
       map_data_zoom_in_tooltip: Zoom in to see map data
+      queryfeature_tooltip: Query features
+      queryfeature_disabled_tooltip: Zoom in to query features
     changesets:
       show:
         comment: "Comment"
@@ -2185,6 +2190,13 @@ en:
         unnamed: "(unnamed)"
         courtesy: "Directions courtesy of %{link}"
       time: "Time"
+    query:
+      node: Node
+      way: Way
+      relation: Relation
+      nothing_found: No features found
+      error: "Error contacting %{server}: %{error}"
+      timeout: "Timeout contacting %{server}"
   redaction:
     edit:
       description: "Description"
index b28b6aff40d056e83e6a8a49270cc8c0990fba7d..3d47c6dcf9897632e4a3a4ad553cc061c1160b83 100644 (file)
@@ -157,6 +157,7 @@ OpenStreetMap::Application.routes.draw do
   match '/offline' => 'site#offline', :via => :get
   match '/key' => 'site#key', :via => :get
   match '/id' => 'site#id', :via => :get
+  match '/query' => 'browse#query', :via => :get
   match '/user/new' => 'user#new', :via => :get
   match '/user/new' => 'user#create', :via => :post
   match '/user/terms' => 'user#terms', :via => :get
index c287baea38197e16737ff0143af5866b58d14a0a..343aec084abbc9b1ac43026e1839ca5c41c5647b 100644 (file)
@@ -126,7 +126,7 @@ CREATE TYPE user_status_enum AS ENUM (
 
 CREATE FUNCTION maptile_for_point(bigint, bigint, integer) RETURNS integer
     LANGUAGE c STRICT
-    AS '/home/ukasiu/repos/openstreetmap-website/db/functions/libpgosm', 'maptile_for_point';
+    AS '/srv/www/overpass.osm.compton.nu/db/functions/libpgosm.so', 'maptile_for_point';
 
 
 --
@@ -135,7 +135,7 @@ CREATE FUNCTION maptile_for_point(bigint, bigint, integer) RETURNS integer
 
 CREATE FUNCTION tile_for_point(integer, integer) RETURNS bigint
     LANGUAGE c STRICT
-    AS '/home/ukasiu/repos/openstreetmap-website/db/functions/libpgosm', 'tile_for_point';
+    AS '/srv/www/overpass.osm.compton.nu/db/functions/libpgosm.so', 'tile_for_point';
 
 
 --
@@ -143,8 +143,8 @@ CREATE FUNCTION tile_for_point(integer, integer) RETURNS bigint
 --
 
 CREATE FUNCTION xid_to_int4(xid) RETURNS integer
-    LANGUAGE c STRICT
-    AS '/home/ukasiu/repos/openstreetmap-website/db/functions/libpgosm', 'xid_to_int4';
+    LANGUAGE c IMMUTABLE STRICT
+    AS '/srv/www/overpass.osm.compton.nu/db/functions/libpgosm.so', 'xid_to_int4';
 
 
 SET default_tablespace = '';
@@ -1795,13 +1795,6 @@ CREATE INDEX gpx_files_user_id_idx ON gpx_files USING btree (user_id);
 CREATE INDEX gpx_files_visible_visibility_idx ON gpx_files USING btree (visible, visibility);
 
 
---
--- Name: index_changeset_comments_on_body; Type: INDEX; Schema: public; Owner: -; Tablespace: 
---
-
-CREATE INDEX index_changeset_comments_on_body ON changeset_comments USING btree (body);
-
-
 --
 -- Name: index_changeset_comments_on_created_at; Type: INDEX; Schema: public; Owner: -; Tablespace: 
 --
@@ -2635,3 +2628,4 @@ INSERT INTO schema_migrations (version) VALUES ('7');
 INSERT INTO schema_migrations (version) VALUES ('8');
 
 INSERT INTO schema_migrations (version) VALUES ('9');
+
index d7fe4a2fd87ab41451f4dc139554a08f2cc3d4fd..51f74fe7a56b98087fba50772169642e0797d673 100644 (file)
@@ -73,18 +73,8 @@ describe("OSM", function () {
       expect(params).to.have.property("bounds").deep.equal(expected);
     });
 
-    it("parses lat/lon/zoom params", function () {
-      var params = OSM.mapParams("?lat=57.6247&lon=-3.6845");
-      expect(params).to.have.property("lat", 57.6247);
-      expect(params).to.have.property("lon", -3.6845);
-      expect(params).to.have.property("zoom", 5);
-
-      params = OSM.mapParams("?lat=57.6247&lon=-3.6845&zoom=10");
-      expect(params).to.have.property("lat", 57.6247);
-      expect(params).to.have.property("lon", -3.6845);
-      expect(params).to.have.property("zoom", 10);
-
-      params = OSM.mapParams("?mlat=57.6247&mlon=-3.6845");
+    it("parses mlat/mlon/zoom params", function () {
+      var params = OSM.mapParams("?mlat=57.6247&mlon=-3.6845");
       expect(params).to.have.property("lat", 57.6247);
       expect(params).to.have.property("lon", -3.6845);
       expect(params).to.have.property("zoom", 12);
@@ -249,4 +239,14 @@ describe("OSM", function () {
       expect(OSM.locationCookie(map)).to.eq("-3.685|57.625|5|M");
     });
   });
+
+  describe(".distance", function () {
+    it("computes distance between points", function () {
+      var latlng1 = L.latLng(51.76712,-0.00484),
+        latlng2 = L.latLng(51.7675159, -0.0078329);
+
+      expect(OSM.distance(latlng1, latlng2)).to.be.closeTo(210.664, 0.005);
+      expect(OSM.distance(latlng2, latlng1)).to.be.closeTo(210.664, 0.005);
+    });
+  });
 });