Merge branch 'master' into overpass
authorTom Hughes <tom@compton.nu>
Sun, 16 Mar 2014 16:20:39 +0000 (16:20 +0000)
committerTom Hughes <tom@compton.nu>
Sun, 16 Mar 2014 16:20:39 +0000 (16:20 +0000)
13 files changed:
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/stylesheets/common.css.scss
app/views/browse/query.html.erb [new file with mode: 0644]
config/example.application.yml
config/i18n-js.yml
config/locales/en.yml
config/routes.rb
test/javascripts/osm_test.js

index e7490c8..e3ed0e7 100644 (file)
Binary files a/app/assets/images/sprite.png and b/app/assets/images/sprite.png differ
index b61018d..b50b969 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 96e0353..54ccb88 100644 (file)
@@ -5,6 +5,7 @@
 //= require leaflet.key
 //= require leaflet.note
 //= require leaflet.share
+//= require leaflet.query
 //= require index/search
 //= require index/browse
 //= require index/export
@@ -12,6 +13,7 @@
 //= require index/history
 //= require index/note
 //= require index/new_note
+//= require index/query
 //= require router
 
 (function() {
@@ -123,6 +125,11 @@ $(document).ready(function () {
     sidebar: sidebar
   }).addTo(map);
 
+  L.OSM.query({
+    position: position,
+    sidebar: sidebar
+  }).addTo(map);
+
   L.control.scale()
     .addTo(map);
 
@@ -295,7 +302,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.Browse(map, 'changeset')
+    "/changeset/:id":              OSM.Browse(map, 'changeset'),
+    "/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..5ce9969
--- /dev/null
@@ -0,0 +1,277 @@
+//= require jquery.simulate
+
+OSM.Query = function(map) {
+  var queryButton = $(".control-query .control-button"),
+    uninterestingTags = ['source', 'source_ref', 'source:ref', 'history', 'attribution', 'created_by', 'tiger:county', 'tiger:tlid', 'tiger:upload_uuid'],
+    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("disabled")) return;
+
+    if (queryButton.hasClass("active")) {
+      disableQueryMode();
+
+      OSM.router.route("/");
+    } else {
+      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("click", ".query-results li.query-result", function (e) {
+      if (!$(e.target).is('a')) {
+        $(this).find("a").simulate("click", e);
+      }
+    });
+
+  function interestingFeature(feature, origin, radius) {
+    if (feature.tags) {
+      if (feature.type === "node" &&
+          OSM.distance(origin, L.latLng(feature.lat, feature.lon)) > radius) {
+        return false;
+      }
+
+      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") {
+      prefix = I18n.t("geocoder.search_osm_nominatim.admin_levels.level" + tags.admin_level)
+    } 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;
+
+    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, features) {
+    var geometry;
+
+    if (feature.type === "node") {
+      geometry = L.circleMarker([feature.lat, feature.lon], featureStyle);
+    } else if (feature.type === "way") {
+      geometry = L.polyline(feature.nodes.map(function (node) {
+        return features["node" + node].getLatLng();
+      }), featureStyle);
+    } else if (feature.type === "relation") {
+      geometry = L.featureGroup();
+
+      feature.members.forEach(function (member) {
+        if (features[member.type + member.ref]) {
+          geometry.addLayer(features[member.type + member.ref]);
+        }
+      });
+    }
+
+    if (geometry) {
+      features[feature.type + feature.id] = geometry;
+    }
+
+    return geometry;
+  }
+
+  function runQuery(latlng, radius, query, $section) {
+    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: OSM.OVERPASS_URL,
+      method: "POST",
+      data: {
+        data: "[timeout:5][out:json];" + query,
+      },
+      success: function(results) {
+        var features = {};
+
+        $section.find(".loader").stopTime("loading").hide();
+
+        for (var i = 0; i < results.elements.length; i++) {
+          var element = results.elements[i],
+            geometry = featureGeometry(element, features);
+
+          if (interestingFeature(element, latlng, radius)) {
+            var $li = $("<li>")
+              .addClass("query-result")
+              .data("geometry", geometry)
+              .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);
+        }
+      }
+    }));
+  }
+
+  function queryOverpass(lat, lng) {
+    var latlng = L.latLng(lat, lng),
+      radius = 10 * Math.pow(1.5, 19 - map.getZoom()),
+      around = "around:" + radius + "," + lat + "," + lng,
+      features = "(node(" + around + ");way(" + around + ");relation(" + around + "))",
+      nearby = "((" + features + ";way(bn));node(w));out;",
+      isin = "is_in(" + lat + "," + lng + ")->.a;(relation(pivot.a);way(pivot.a);node(w));out;";
+
+    $("#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"));
+  }
+
+  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);
+    });
+  };
+
+  page.load = function(path) {
+    var params = querystring.parse(path.substring(path.indexOf('?') + 1));
+
+    queryOverpass(params.lat, params.lon);
+    enableQueryMode();
+
+    return map.getState();
+  };
+
+  page.unload = function() {
+    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 033b2de..15c1682 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 533e91c..9e614f9 100644 (file)
@@ -171,7 +171,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 */
 
@@ -683,6 +683,14 @@ nav.secondary {
   #map {
     height: 100%;
     overflow: hidden;
+
+    &.query-active {
+      cursor: help;
+    }
+
+    &.query-disabled {
+      cursor: not-allowed;
+    }
   }
 
   #map-ui {
@@ -1119,6 +1127,38 @@ header .search_form {
     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;
+        }
+
+        a {
+          color: #000;
+        }
+      }
+    }
+  }
 }
 
 /* Rules for export sidebar */
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 d6a855e..702d411 100644 (file)
@@ -84,6 +84,8 @@ defaults: &defaults
   require_terms_seen: false
   # Whether to require users to agree to the CTs before editing
   require_terms_agreed: false
+  # URL of Overpass instance to use for feature queries
+  overpass_url: http://overpass-api.de/api/interpreter
 
 development:
   <<: *defaults
index 026ece6..369fa34 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 94537c1..c4f757b 100644 (file)
@@ -197,6 +197,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}"
@@ -507,7 +512,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"
@@ -718,6 +723,8 @@ en:
           tram: "Tramway"
           tram_stop: "Tram Stop"
           yard: "Railway Yard"
+        route:
+          bus: "Bus Route"
         shop:
           alcohol: "Off License"
           antiques: "Antiques"
@@ -2101,6 +2108,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
     notes:
       new:
         intro: "Spotted a mistake or something missing? Let other mappers know so we can fix it. Move the marker to the correct position and type a note to explain the problem. (Please don't enter personal information here.)"
@@ -2113,6 +2122,11 @@ en:
         comment_and_resolve: Comment & Resolve
         comment: Comment
     edit_help: Move the map and zoom in on a location you want to edit, then click here.
+    query:
+      node: Node
+      way: Way
+      relation: Relation
+      nothing_found: No features found
   redaction:
     edit:
       description: "Description"
index e9f593d..e03c5d6 100644 (file)
@@ -150,6 +150,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 d7fe4a2..51f74fe 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);
+    });
+  });
 });