diff --git a/Gemfile b/Gemfile index a6c1dc71e..d4a2777a9 100644 --- a/Gemfile +++ b/Gemfile @@ -53,6 +53,9 @@ gem 'turnout', '~> 2.4.0' gem 'uglifier', '~> 3.2.0' gem 'unicorn', '~> 5.3.0' gem 'whenever', '~> 0.9.7', require: false +source 'https://rails-assets.org' do + gem 'rails-assets-leaflet' +end group :development, :test do gem "bullet", '~> 5.5.1' diff --git a/Gemfile.lock b/Gemfile.lock index f15f0c6e9..6435af58e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -334,6 +334,7 @@ GEM bundler (>= 1.3.0, < 2.0) railties (= 4.2.9) sprockets-rails + rails-assets-leaflet (1.1.0) rails-assets-markdown-it (8.2.2) rails-deprecated_sanitizer (1.0.3) activesupport (>= 4.2.0.alpha) @@ -542,6 +543,7 @@ DEPENDENCIES poltergeist (~> 1.15.0) quiet_assets (~> 1.1.0) rails (= 4.2.9) + rails-assets-leaflet! rails-assets-markdown-it (~> 8.2.1)! redcarpet (~> 3.4.0) responders (~> 2.4.0) diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 0b3ee0201..d7c36e944 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -67,6 +67,8 @@ //= require tree_navigator //= require custom //= require tag_autocomplete +//= require leaflet +//= require map var initialize_modules = function() { App.Comments.initialize(); @@ -103,6 +105,7 @@ var initialize_modules = function() { App.Documentable.initialize(); App.Imageable.initialize(); App.TagAutocomplete.initialize(); + App.Map.initialize(); }; $(function(){ diff --git a/app/assets/javascripts/map.js.coffee b/app/assets/javascripts/map.js.coffee new file mode 100644 index 000000000..3f28024a7 --- /dev/null +++ b/app/assets/javascripts/map.js.coffee @@ -0,0 +1,78 @@ +App.Map = + + initialize: -> + maps = $('*[data-map]') + + if maps.length > 0 + $.each maps, (index, map) -> + App.Map.initializeMap map + + initializeMap: (element) -> + + mapCenterLatitude = $(element).data('map-center-latitude') + mapCenterLongitude = $(element).data('map-center-longitude') + markerLatitude = $(element).data('marker-latitude') + markerLongitude = $(element).data('marker-longitude') + zoom = $(element).data('map-zoom') + mapTilesProvider = $(element).data('map-tiles-provider') + mapAttribution = $(element).data('map-tiles-provider-attribution') + latitudeInputSelector = $(element).data('latitude-input-selector') + longitudeInputSelector = $(element).data('longitude-input-selector') + zoomInputSelector = $(element).data('zoom-input-selector') + removeMarkerSelector = $(element).data('marker-remove-selector') + editable = $(element).data('marker-editable') + marker = null; + markerIcon = L.divIcon( + className: 'map-marker' + iconSize: [30, 30] + iconAnchor: [15, 40] + html: '
') + + createMarker = (latitude, longitude) -> + markerLatLng = new (L.LatLng)(latitude, longitude) + marker = L.marker(markerLatLng, { icon: markerIcon, draggable: editable }) + if editable + marker.on 'dragend', updateFormfields + marker.addTo(map) + return marker + + removeMarker = (e) -> + e.preventDefault() + if marker + map.removeLayer(marker) + marker = null; + clearFormfields() + return + + moveOrPlaceMarker = (e) -> + if marker + marker.setLatLng(e.latlng) + else + marker = createMarker(e.latlng.lat, e.latlng.lng) + + updateFormfields() + return + + updateFormfields = -> + $(latitudeInputSelector).val marker.getLatLng().lat + $(longitudeInputSelector).val marker.getLatLng().lng + $(zoomInputSelector).val map.getZoom() + return + + clearFormfields = -> + $(latitudeInputSelector).val '' + $(longitudeInputSelector).val '' + $(zoomInputSelector).val '' + return + + mapCenterLatLng = new (L.LatLng)(mapCenterLatitude, mapCenterLongitude) + map = L.map(element.id).setView(mapCenterLatLng, zoom) + L.tileLayer(mapTilesProvider, attribution: mapAttribution).addTo map + + if markerLatitude && markerLongitude + marker = createMarker(markerLatitude, markerLongitude) + + if editable + $(removeMarkerSelector).on 'click', removeMarker + map.on 'zoomend', updateFormfields + map.on 'click', moveOrPlaceMarker diff --git a/app/assets/stylesheets/admin.scss b/app/assets/stylesheets/admin.scss index fab661e8d..52c989f55 100644 --- a/app/assets/stylesheets/admin.scss +++ b/app/assets/stylesheets/admin.scss @@ -8,6 +8,7 @@ // 06. Polls // 07. Legislation // 08. CMS +// 09. Map // // 01. Global styles @@ -976,3 +977,51 @@ table { border: 0; } } + +// 09. Map +// -------------- + +.map { + width: 100%; + height: 350px; + + .map-marker { + visibility: visible; + position: absolute; + left: 50%; + top: 50%; + margin-top: -5px; + + .map-icon { + width: 30px; + height: 30px; + border-radius: 50% 50% 50% 0; + background: #00cae9; + transform: rotate(-45deg); + } + + .map-icon::after { + content: ''; + width: 14px; + height: 14px; + margin: 8px 0 0 8px; + background: #fff; + position: absolute; + border-radius: 50%; + } + } + + .map-attributtion { + visibility: visible; + height: auto; + } +} + +.map-marker { + visibility: hidden; +} + +.map-attributtion { + visibility: hidden; + height: 0; +} diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 361dcfde3..aff55c88c 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -18,3 +18,4 @@ @import 'datepicker_overrides'; @import 'jquery-ui/autocomplete'; @import 'autocomplete_overrides'; +@import 'leaflet'; diff --git a/app/controllers/admin/settings_controller.rb b/app/controllers/admin/settings_controller.rb index 70541a3a0..14dfc6658 100644 --- a/app/controllers/admin/settings_controller.rb +++ b/app/controllers/admin/settings_controller.rb @@ -14,6 +14,13 @@ class Admin::SettingsController < Admin::BaseController redirect_to admin_settings_path, notice: t("admin.settings.flash.updated") end + def update_map + Setting["map_latitude"] = params[:latitude].to_f + Setting["map_longitude"] = params[:longitude].to_f + Setting["map_zoom"] = params[:zoom].to_i + redirect_to admin_settings_path, notice: t("admin.settings.index.map.flash.update") + end + private def settings_params diff --git a/app/controllers/budgets/investments_controller.rb b/app/controllers/budgets/investments_controller.rb index fb82c51db..bb00f24af 100644 --- a/app/controllers/budgets/investments_controller.rb +++ b/app/controllers/budgets/investments_controller.rb @@ -107,7 +107,8 @@ module Budgets .permit(:title, :description, :external_url, :heading_id, :tag_list, :organization_name, :location, :terms_of_service, image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy]) + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def load_ballot diff --git a/app/controllers/proposals_controller.rb b/app/controllers/proposals_controller.rb index 9e6fc521d..d1db0e4a2 100644 --- a/app/controllers/proposals_controller.rb +++ b/app/controllers/proposals_controller.rb @@ -77,7 +77,8 @@ class ProposalsController < ApplicationController params.require(:proposal).permit(:title, :question, :summary, :description, :external_url, :video_url, :responsible_name, :tag_list, :terms_of_service, :geozone_id, image_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], - documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy] ) + documents_attributes: [:id, :title, :attachment, :cached_attachment, :user_id, :_destroy], + map_location_attributes: [:latitude, :longitude, :zoom]) end def retired_params diff --git a/app/helpers/map_locations_helper.rb b/app/helpers/map_locations_helper.rb new file mode 100644 index 000000000..ae45e7c1d --- /dev/null +++ b/app/helpers/map_locations_helper.rb @@ -0,0 +1,67 @@ +module MapLocationsHelper + + def map_location_available?(map_location) + map_location.present? && map_location.available? + end + + def map_location_latitude(map_location) + map_location.present? && map_location.latitude.present? ? map_location.latitude : Setting["map_latitude"] + end + + def map_location_longitude(map_location) + map_location.present? && map_location.longitude.present? ? map_location.longitude : Setting["map_longitude"] + end + + def map_location_zoom(map_location) + map_location.present? && map_location.zoom.present? ? map_location.zoom : Setting["map_zoom"] + end + + def map_location_input_id(prefix, attribute) + "#{prefix}_map_location_attributes_#{attribute}" + end + + def map_location_remove_marker_link_id(map_location) + "remove-marker-link-#{dom_id(map_location)}" + end + + def render_map(map_location, parent_class, editable, remove_marker_label) + map = content_tag_for :div, + map_location, + class: "map", + data: prepare_map_settings(map_location, editable, parent_class) + map += map_location_remove_marker(map_location, remove_marker_label) if editable + map + end + + def map_location_remove_marker(map_location, text) + content_tag :div, class: "text-right" do + content_tag :a, + id: map_location_remove_marker_link_id(map_location), + href: "#", + class: "location-map-remove-marker-button delete" do + text + end + end + end + + private + + def prepare_map_settings(map_location, editable, parent_class) + options = { + map: "", + map_center_latitude: map_location_latitude(map_location), + map_center_longitude: map_location_longitude(map_location), + map_zoom: map_location_zoom(map_location), + map_tiles_provider: Rails.application.secrets.map_tiles_provider, + map_tiles_provider_attribution: Rails.application.secrets.map_tiles_provider_attribution, + marker_editable: editable, + marker_latitude: map_location.latitude, + marker_longitude: map_location.longitude, + marker_remove_selector: "##{map_location_remove_marker_link_id(map_location)}", + latitude_input_selector: "##{map_location_input_id(parent_class, 'latitude')}", + longitude_input_selector: "##{map_location_input_id(parent_class, 'longitude')}", + zoom_input_selector: "##{map_location_input_id(parent_class, 'zoom')}" + } + end + +end diff --git a/app/models/budget/investment.rb b/app/models/budget/investment.rb index 857025375..ddf6e819c 100644 --- a/app/models/budget/investment.rb +++ b/app/models/budget/investment.rb @@ -8,6 +8,7 @@ class Budget include Followable include Communitable include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, diff --git a/app/models/concerns/mappable.rb b/app/models/concerns/mappable.rb new file mode 100644 index 000000000..c108bdca7 --- /dev/null +++ b/app/models/concerns/mappable.rb @@ -0,0 +1,9 @@ +module Mappable + extend ActiveSupport::Concern + + included do + has_one :map_location, dependent: :destroy + accepts_nested_attributes_for :map_location, allow_destroy: true + end + +end diff --git a/app/models/map_location.rb b/app/models/map_location.rb new file mode 100644 index 000000000..14e91d92c --- /dev/null +++ b/app/models/map_location.rb @@ -0,0 +1,10 @@ +class MapLocation < ActiveRecord::Base + + belongs_to :proposal + belongs_to :investment, class_name: Budget::Investment + + def available? + latitude.present? && longitude.present? && zoom.present? + end + +end diff --git a/app/models/proposal.rb b/app/models/proposal.rb index 7a0abcb6e..da0fc109f 100644 --- a/app/models/proposal.rb +++ b/app/models/proposal.rb @@ -11,10 +11,11 @@ class Proposal < ActiveRecord::Base include Followable include Communitable include Imageable + include Mappable include Documentable documentable max_documents_allowed: 3, max_file_size: 3.megabytes, - accepted_content_types: [ "application/pdf" ] + accepted_content_types: [ "application/pdf" ] include EmbedVideosHelper acts_as_votable diff --git a/app/views/admin/settings/_map_form.html.erb b/app/views/admin/settings/_map_form.html.erb new file mode 100644 index 000000000..9c411bb12 --- /dev/null +++ b/app/views/admin/settings/_map_form.html.erb @@ -0,0 +1,31 @@ +
+
+
" + data-map-center-longitude="<%= Setting["map_longitude"] %>" + data-map-zoom="<%= Setting["map_zoom"] %>" + data-map-tiles-provider="<%= Rails.application.secrets.map_tiles_provider %>" + data-map-tiles-provider-attribution="<%= Rails.application.secrets.map_tiles_provider_attribution %>" + data-marker-editable="true" + data-marker-latitude="<%= Setting["map_latitude"] %>" + data-marker-longitude="<%= Setting["map_longitude"] %>" + data-latitude-input-selector="#latitude" + data-longitude-input-selector="#longitude" + data-zoom-input-selector="#zoom"> +
+ + <%= form_tag admin_update_map_path, method: :put, id: 'map-form' do |f| %> + + <%= hidden_field_tag :latitude, Setting["map_latitude"] %> + <%= hidden_field_tag :longitude, Setting["map_longitude"] %> + <%= hidden_field_tag :zoom, Setting["map_zoom"] %> + +
+ <%= submit_tag t("admin.settings.index.map.form.submit"), + class: "button hollow expanded" %> +
+ + <% end %> +
+
\ No newline at end of file diff --git a/app/views/admin/settings/index.html.erb b/app/views/admin/settings/index.html.erb index 14c89b776..e49174fca 100644 --- a/app/views/admin/settings/index.html.erb +++ b/app/views/admin/settings/index.html.erb @@ -95,3 +95,11 @@ <% end %> + +<% if feature?(:map) %> +

<%= t("admin.settings.index.map.title") %>

+

<%= t("admin.settings.index.map.help") %>

+ + <%= render "map_form" %> + +<% end %> diff --git a/app/views/budgets/investments/_form.html.erb b/app/views/budgets/investments/_form.html.erb index 0d0bdd7f1..8a19c188a 100644 --- a/app/views/budgets/investments/_form.html.erb +++ b/app/views/budgets/investments/_form.html.erb @@ -29,6 +29,20 @@ <%= render 'documents/nested_documents', documentable: @investment, f: f %> + <% if feature?(:map) %> +
+ + <%= render 'map_locations/form_fields', + form: f, + map_location: @investment.map_location || MapLocation.new, + label: t("budgets.investments.form.map_location"), + help: t("budgets.investments.form.map_location_instructions"), + remove_marker_label: t("budgets.investments.form.map_remove_marker"), + parent_class: "budget_investment" %> + +
+ <% end %> +
<%= f.text_field :location %>
diff --git a/app/views/budgets/investments/_investment_show.html.erb b/app/views/budgets/investments/_investment_show.html.erb index e257de0bb..eab4de525 100644 --- a/app/views/budgets/investments/_investment_show.html.erb +++ b/app/views/budgets/investments/_investment_show.html.erb @@ -45,6 +45,10 @@ <%= safe_html_with_links investment.description.html_safe %> + <% if feature?(:map) && map_location_available?(@investment.map_location) %> + <%= render_map(@investment.map_location, "budget_investment", false, nil) %> + <% end %> + <% if investment.external_url.present? %> + <% if feature?(:map) %> +
+ + <%= render 'map_locations/form_fields', + form: f, + map_location: @proposal.map_location || MapLocation.new, + label: t("proposals.form.map_location"), + help: t("proposals.form.map_location_instructions"), + remove_marker_label: t("proposals.form.map_remove_marker"), + parent_class: "proposal" %> + +
+ <% end %> +
<%= f.label :tag_list, t("proposals.form.tags_label") %>

<%= t("proposals.form.tags_instructions") %>

diff --git a/app/views/proposals/show.html.erb b/app/views/proposals/show.html.erb index 96999ca7d..e25bdf9f7 100644 --- a/app/views/proposals/show.html.erb +++ b/app/views/proposals/show.html.erb @@ -68,6 +68,10 @@ <%= safe_html_with_links @proposal.description %> + <% if feature?(:map) && map_location_available?(@proposal.map_location) %> + <%= render_map(@proposal.map_location, "proposal", false, nil) %> + <% end %> + <% if @proposal.external_url.present? %>