From: Tom Hughes Date: Sun, 15 Jun 2014 13:54:39 +0000 (+0100) Subject: Merge branch 'master' into overpass X-Git-Tag: live~4889^2~8 X-Git-Url: https://git.openstreetmap.org./rails.git/commitdiff_plain/a70b61315c2331c18123a0b36782d6ef6bb3ab3e?hp=399fe3c12a7612f1ad79d376f2859d9c6fd38839 Merge branch 'master' into overpass --- diff --git a/app/assets/images/sprite.png b/app/assets/images/sprite.png index e7490c84c..e3ed0e7f8 100644 Binary files a/app/assets/images/sprite.png and b/app/assets/images/sprite.png differ diff --git a/app/assets/images/sprite.svg b/app/assets/images/sprite.svg index b61018d13..b50b969e9 100644 --- a/app/assets/images/sprite.svg +++ b/app/assets/images/sprite.svg @@ -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"> @@ -27,16 +27,16 @@ 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" @@ -113,6 +113,10 @@ orientation="1,0" position="260,195" id="guide11761" /> + @@ -265,5 +269,16 @@ inkscape:export-filename="/Users/saman/work_repos/osm-redesign/renders/share-1.png" inkscape:export-xdpi="90" inkscape:export-ydpi="90" /> + ? diff --git a/app/assets/javascripts/index.js b/app/assets/javascripts/index.js index cfee031fd..236ed44fc 100644 --- a/app/assets/javascripts/index.js +++ b/app/assets/javascripts/index.js @@ -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 index 000000000..e2c8699ff --- /dev/null +++ b/app/assets/javascripts/index/query.js @@ -0,0 +1,327 @@ +//= 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'], + 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")) { + if ($("#content").hasClass("overlay-sidebar")) { + disableQueryMode(); + } + } 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("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) { + 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: 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 = $("
  • ") + .addClass("query-result") + .data("geometry", geometry) + .appendTo($ul); + var $p = $("

    ") + .text(featurePrefix(element) + " ") + .appendTo($li); + + $("") + .attr("href", "/" + element.type + "/" + element.id) + .text(featureName(element)) + .appendTo($p); + } + } + + if ($ul.find("li").length == 0) { + $("

  • ") + .text(I18n.t("javascripts.query.nothing_found")) + .appendTo($ul); + } + }, + error: function(xhr, status, error) { + $section.find(".loader").stopTime("loading").hide(); + + $("
  • ") + .text(I18n.t("javascripts.query." + status, { server: url, error: error })) + .appendTo($ul); + } + })); + } + + /* + * To find nearby objects we ask overpass for the union of the + * following sets: + * + * node(around:,,lng>) + * way(around:,,lng>) + * node(w) + * relation(around:,,lng>) + * + * to find enclosing objects we first find all the enclosing areas: + * + * is_in(,)->.a + * + * and then return the union of the following sets: + * + * relation(pivot.a) + * way(pivot.a) + * node(w) + * + * In order to avoid overly large responses we don't currently + * attempt to complete any relations and instead just show those + * ways and nodes which are returned for other reasons. + */ + function queryOverpass(lat, lng) { + var latlng = L.latLng(lat, lng), + radius = 10 * Math.pow(1.5, 19 - map.getZoom()), + around = "around:" + radius + "," + lat + "," + lng, + nodes = "node(" + around + ")", + ways = "way(" + around + ");node(w)", + relations = "relation(" + around + ")", + nearby = "(" + nodes + ";" + ways + ";" + relations + ");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, 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); + enableQueryMode(); + }; + + 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 index 000000000..906487274 --- /dev/null +++ b/app/assets/javascripts/leaflet.query.js @@ -0,0 +1,38 @@ +L.OSM.query = function (options) { + var control = L.control(options); + + control.onAdd = function (map) { + var $container = $('
    ') + .attr('class', 'control-query'); + + var link = $('') + .attr('class', 'control-button') + .attr('href', '#') + .html('') + .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; +}; diff --git a/app/assets/javascripts/osm.js.erb b/app/assets/javascripts/osm.js.erb index 033b2de81..15c1682d5 100644 --- a/app/assets/javascripts/osm.js.erb +++ b/app/assets/javascripts/osm.js.erb @@ -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) + )); } }; diff --git a/app/assets/stylesheets/common.css.scss b/app/assets/stylesheets/common.css.scss index 7c400b4f3..d3268f43b 100644 --- a/app/assets/stylesheets/common.css.scss +++ b/app/assets/stylesheets/common.css.scss @@ -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 { @@ -1121,6 +1129,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 index 000000000..629d84c05 --- /dev/null +++ b/app/views/browse/query.html.erb @@ -0,0 +1,22 @@ +<% set_title(t "browse.query.title") %> + +

    + + <%= t "browse.query.title" %> +

    + +
    +

    <%= t("browse.query.introduction") %>

    +
    + +
    +

    <%= t("browse.query.nearby") %>

    + <%= image_tag "searching.gif", :class => "loader" %> +
      +
      + +
      +

      <%= t("browse.query.enclosing") %>

      + <%= image_tag "searching.gif", :class => "loader" %> +
        +
        diff --git a/config/example.application.yml b/config/example.application.yml index d6a855ef5..7f6aaf298 100644 --- a/config/example.application.yml +++ b/config/example.application.yml @@ -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: "//overpass-api.de/api/interpreter" development: <<: *defaults diff --git a/config/i18n-js.yml b/config/i18n-js.yml index 026ece64c..369fa340a 100644 --- a/config/i18n-js.yml +++ b/config/i18n-js.yml @@ -31,3 +31,4 @@ translations: - "*.site.sidebar.search_results" - "*.diary_entry.edit.marker_text" - "*.layouts.project_name.title" + - "*.geocoder.search_osm_nominatim.*" diff --git a/config/locales/en.yml b/config/locales/en.yml index fe5f58d51..eb2991d4e 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -198,6 +198,11 @@ en: reopened_by: "Reactivated by %{user} %{when} ago" reopened_by_anonymous: "Reactivated by anonymous %{when} ago" hidden_by: "Hidden by %{user} %{when} ago" + 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}" @@ -508,7 +513,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" @@ -719,6 +724,8 @@ en: tram: "Tramway" tram_stop: "Tram Stop" yard: "Railway Yard" + route: + bus: "Bus Route" shop: alcohol: "Off License" antiques: "Antiques" @@ -2102,6 +2109,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 or information from copyrighted maps or directory listings.)" @@ -2114,6 +2123,13 @@ 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 + error: "Error contacting %{server}: %{error}" + timeout: "Timeout contacting %{server}" redaction: edit: description: "Description" diff --git a/config/routes.rb b/config/routes.rb index 3357f911d..9af0c1099 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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 diff --git a/test/javascripts/osm_test.js b/test/javascripts/osm_test.js index d7fe4a2fd..51f74fe7a 100644 --- a/test/javascripts/osm_test.js +++ b/test/javascripts/osm_test.js @@ -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); + }); + }); });