diff --git a/app/assets/javascripts/forms.js b/app/assets/javascripts/forms.js
index 0a2dfe107..23f2b4c83 100644
--- a/app/assets/javascripts/forms.js
+++ b/app/assets/javascripts/forms.js
@@ -27,11 +27,12 @@
});
},
synchronizeInputs: function() {
- var banners, inputs, processes, progress_bar;
+ var banners, geozones, inputs, processes, progress_bar;
progress_bar = "[name='progress_bar[percentage]']";
processes = "[name='legislation_process[background_color]'], [name='legislation_process[font_color]']";
banners = "[name='banner[background_color]'], [name='banner[font_color]']";
- inputs = $(progress_bar + ", " + processes + ", " + banners);
+ geozones = "[name='geozone[color]']";
+ inputs = $(progress_bar + ", " + processes + ", " + banners + ", " + geozones);
inputs.on({
input: function() {
$("[name='" + this.name + "']").val($(this).val());
diff --git a/app/assets/javascripts/map.js b/app/assets/javascripts/map.js
index 80a8c6af6..2315ae1cf 100644
--- a/app/assets/javascripts/map.js
+++ b/app/assets/javascripts/map.js
@@ -78,6 +78,7 @@
}
App.Map.addInvestmentsMarkers(investmentsMarkers, createMarker);
+ App.Map.addGeozones(map);
},
leafletMap: function(element) {
var centerData, mapCenterLatLng;
@@ -194,6 +195,25 @@
map.attributionControl.setPrefix(App.Map.attributionPrefix());
L.tileLayer(mapTilesProvider, { attribution: mapAttribution }).addTo(map);
},
+ addGeozones: function(map) {
+ var geozones = $(map._container).data("geozones");
+
+ if (geozones) {
+ geozones.forEach(function(geozone) {
+ App.Map.addGeozone(geozone, map);
+ });
+ }
+ },
+ addGeozone: function(geozone, map) {
+ var polygon = L.polygon(geozone.outline_points, {
+ color: geozone.color,
+ fillOpacity: 0.3,
+ className: "map-polygon"
+ });
+
+ polygon.bindPopup(geozone.headings.join("
"));
+ polygon.addTo(map);
+ },
openMarkerPopup: function(e) {
var marker = e.target;
$.ajax("/investments/" + marker.options.id + "/json_data", {
diff --git a/app/components/admin/budget_headings/form_component.html.erb b/app/components/admin/budget_headings/form_component.html.erb
index bcae27da5..af488f177 100644
--- a/app/components/admin/budget_headings/form_component.html.erb
+++ b/app/components/admin/budget_headings/form_component.html.erb
@@ -18,6 +18,13 @@
<%= f.hidden_field :price, value: 0 %>
<% end %>
+ <% if feature?(:map) %>
+ <%= f.select :geozone_id,
+ geozone_options,
+ include_blank: t("geozones.none"),
+ hint: t("admin.budget_headings.form.geozone_info") %>
+ <% end %>
+
<% if heading.budget.approval_voting? %>
<%= f.number_field :max_ballot_lines,
hint: t("admin.budget_headings.form.max_ballot_lines_info") %>
diff --git a/app/components/admin/budget_headings/form_component.rb b/app/components/admin/budget_headings/form_component.rb
index 96f08a3c0..16dd3cbfd 100644
--- a/app/components/admin/budget_headings/form_component.rb
+++ b/app/components/admin/budget_headings/form_component.rb
@@ -18,4 +18,8 @@ class Admin::BudgetHeadings::FormComponent < ApplicationComponent
def single_heading?
helpers.respond_to?(:single_heading?) && helpers.single_heading?
end
+
+ def geozone_options
+ Geozone.all.map { |geozone| [geozone.name, geozone.id] }
+ end
end
diff --git a/app/components/admin/budget_headings/headings_component.html.erb b/app/components/admin/budget_headings/headings_component.html.erb
index a9269ab26..93f42a88f 100644
--- a/app/components/admin/budget_headings/headings_component.html.erb
+++ b/app/components/admin/budget_headings/headings_component.html.erb
@@ -10,6 +10,7 @@
<% if budget.approval_voting? %>
<%= Budget::Heading.human_attribute_name(:max_ballot_lines) %> |
<% end %>
+ <%= Budget::Heading.human_attribute_name(:geozone_id) %> |
<%= t("admin.actions.actions") %> |
@@ -23,6 +24,9 @@
<% if budget.approval_voting? %>
<%= heading.max_ballot_lines %> |
<% end %>
+
+ <%= geozone_for(heading) %>
+ |
<%= render Admin::TableActionsComponent.new(heading) %>
|
diff --git a/app/components/admin/budget_headings/headings_component.rb b/app/components/admin/budget_headings/headings_component.rb
index 396944392..ea2c251ca 100644
--- a/app/components/admin/budget_headings/headings_component.rb
+++ b/app/components/admin/budget_headings/headings_component.rb
@@ -14,4 +14,12 @@ class Admin::BudgetHeadings::HeadingsComponent < ApplicationComponent
def budget
@budget ||= group.budget
end
+
+ def geozone_for(heading)
+ if heading.geozone
+ link_to heading.geozone.name, edit_admin_geozone_path(heading.geozone)
+ else
+ t("geozones.none")
+ end
+ end
end
diff --git a/app/components/admin/geozones/form_component.html.erb b/app/components/admin/geozones/form_component.html.erb
index b36153e8a..7d55ecf00 100644
--- a/app/components/admin/geozones/form_component.html.erb
+++ b/app/components/admin/geozones/form_component.html.erb
@@ -20,6 +20,25 @@
<%= f.text_field :html_map_coordinates, hint: t("admin.geozones.geozone.coordinates_help") %>
+
+ <%= f.text_area :geojson, rows: "10", hint: t("admin.geozones.geozone.geojson_help") %>
+
+
+
+ <%= f.label :color, nil, for: "color_input", id: "color_input_label" %>
+
+ <%= t("admin.geozones.geozone.color_help", format_help: t("admin.shared.color_help")) %>
+
+
+
+ <%= f.text_field :color, label: false, type: :color %>
+
+
+ <%= f.text_field :color, label: false, id: "color_input" %>
+
+
+
+
<%= f.submit(value: t("admin.geozones.edit.form.submit_button"),
class: "button success") %>
diff --git a/app/components/admin/geozones/index_component.html.erb b/app/components/admin/geozones/index_component.html.erb
index 5ed12f1c1..74aeca62d 100644
--- a/app/components/admin/geozones/index_component.html.erb
+++ b/app/components/admin/geozones/index_component.html.erb
@@ -9,6 +9,7 @@
<%= t("admin.geozones.geozone.external_code") %> |
<%= t("admin.geozones.geozone.census_code") %> |
<%= t("admin.geozones.geozone.coordinates") %> |
+
<%= t("admin.geozones.geozone.geojson") %> |
<%= t("admin.actions.actions") %> |
@@ -19,7 +20,8 @@
<%= geozone.name %> |
<%= geozone.external_code %> |
<%= geozone.census_code %> |
-
<%= geozone.html_map_coordinates %> |
+
<%= yes_no_text(geozone.html_map_coordinates.present?) %> |
+
<%= yes_no_text(geozone.geojson.present?) %> |
<%= render Admin::TableActionsComponent.new(geozone) %>
|
diff --git a/app/components/admin/geozones/index_component.rb b/app/components/admin/geozones/index_component.rb
index 4701c84b8..d9cee9e84 100644
--- a/app/components/admin/geozones/index_component.rb
+++ b/app/components/admin/geozones/index_component.rb
@@ -11,4 +11,12 @@ class Admin::Geozones::IndexComponent < ApplicationComponent
def title
t("admin.geozones.index.title")
end
+
+ def yes_no_text(condition)
+ if condition
+ t("shared.yes")
+ else
+ t("shared.no")
+ end
+ end
end
diff --git a/app/components/budgets/map_component.html.erb b/app/components/budgets/map_component.html.erb
index fb89cd1f0..33aea81a7 100644
--- a/app/components/budgets/map_component.html.erb
+++ b/app/components/budgets/map_component.html.erb
@@ -1,4 +1,4 @@
<%= t("budgets.index.map") %>
- <%= render_map(nil, investments_coordinates: coordinates) %>
+ <%= render_map(nil, investments_coordinates: coordinates, geozones_data: geozones_data) %>
diff --git a/app/components/budgets/map_component.rb b/app/components/budgets/map_component.rb
index 103a97808..dcbc427d6 100644
--- a/app/components/budgets/map_component.rb
+++ b/app/components/budgets/map_component.rb
@@ -21,4 +21,16 @@ class Budgets::MapComponent < ApplicationComponent
MapLocation.where(investment_id: investments).map(&:json_data)
end
+
+ def geozones_data
+ budget.geozones.map do |geozone|
+ {
+ outline_points: geozone.outline_points,
+ color: geozone.color,
+ headings: budget.headings.where(geozone: geozone).map do |heading|
+ link_to heading.name, budget_investments_path(budget, heading_id: heading.id)
+ end
+ }
+ end
+ end
end
diff --git a/app/components/shared/map_location_component.rb b/app/components/shared/map_location_component.rb
index 69371228b..e82f1f8a1 100644
--- a/app/components/shared/map_location_component.rb
+++ b/app/components/shared/map_location_component.rb
@@ -1,10 +1,11 @@
class Shared::MapLocationComponent < ApplicationComponent
- attr_reader :investments_coordinates, :form
+ attr_reader :investments_coordinates, :form, :geozones_data
- def initialize(map_location, investments_coordinates: nil, form: nil)
+ def initialize(map_location, investments_coordinates: nil, form: nil, geozones_data: nil)
@map_location = map_location
@investments_coordinates = investments_coordinates
@form = form
+ @geozones_data = geozones_data
end
def map_location
@@ -56,7 +57,8 @@ class Shared::MapLocationComponent < ApplicationComponent
marker_remove_selector: "##{remove_marker_id}",
marker_investments_coordinates: investments_coordinates,
marker_latitude: map_location.latitude.presence,
- marker_longitude: map_location.longitude.presence
+ marker_longitude: map_location.longitude.presence,
+ geozones: geozones_data
}.merge(input_selectors)
end
diff --git a/app/controllers/admin/geozones_controller.rb b/app/controllers/admin/geozones_controller.rb
index be2f01ecb..794a5985f 100644
--- a/app/controllers/admin/geozones_controller.rb
+++ b/app/controllers/admin/geozones_controller.rb
@@ -47,6 +47,6 @@ class Admin::GeozonesController < Admin::BaseController
end
def allowed_params
- [:name, :external_code, :census_code, :html_map_coordinates]
+ [:name, :external_code, :census_code, :html_map_coordinates, :geojson, :color]
end
end
diff --git a/app/controllers/concerns/admin/budget_headings_actions.rb b/app/controllers/concerns/admin/budget_headings_actions.rb
index 202aa5ff1..1a1db8314 100644
--- a/app/controllers/concerns/admin/budget_headings_actions.rb
+++ b/app/controllers/concerns/admin/budget_headings_actions.rb
@@ -59,7 +59,7 @@ module Admin::BudgetHeadingsActions
end
def allowed_params
- valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines]
+ valid_attributes = [:price, :population, :allow_custom_content, :latitude, :longitude, :max_ballot_lines, :geozone_id]
[*valid_attributes, translation_params(Budget::Heading)]
end
diff --git a/app/models/budget.rb b/app/models/budget.rb
index 6cbe93b0d..4fc250bf4 100644
--- a/app/models/budget.rb
+++ b/app/models/budget.rb
@@ -34,6 +34,7 @@ class Budget < ApplicationRecord
has_many :ballots, dependent: :destroy
has_many :groups, dependent: :destroy
has_many :headings, through: :groups
+ has_many :geozones, through: :headings
has_many :lines, through: :ballots, class_name: "Budget::Ballot::Line"
has_many :phases, class_name: "Budget::Phase"
has_many :budget_administrators, dependent: :destroy
diff --git a/app/models/budget/heading.rb b/app/models/budget/heading.rb
index 94c61511e..7b8fbde57 100644
--- a/app/models/budget/heading.rb
+++ b/app/models/budget/heading.rb
@@ -22,6 +22,7 @@ class Budget
end
belongs_to :group
+ belongs_to :geozone
has_many :investments
has_many :content_blocks
diff --git a/app/models/concerns/geojson_format_validator.rb b/app/models/concerns/geojson_format_validator.rb
new file mode 100644
index 000000000..e2a88f0e8
--- /dev/null
+++ b/app/models/concerns/geojson_format_validator.rb
@@ -0,0 +1,23 @@
+class GeojsonFormatValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ if value.present?
+ geojson = parse_json(value)
+
+ unless geojson?(geojson)
+ record.errors.add(attribute, :invalid)
+ end
+ end
+ end
+
+ private
+
+ def parse_json(geojson_data)
+ JSON.parse(geojson_data) rescue nil
+ end
+
+ def geojson?(geojson)
+ return false unless geojson.is_a?(Hash)
+
+ geojson.dig("geometry", "coordinates").is_a?(Array)
+ end
+end
diff --git a/app/models/geozone.rb b/app/models/geozone.rb
index 57b5e6cb4..be2930ca8 100644
--- a/app/models/geozone.rb
+++ b/app/models/geozone.rb
@@ -4,7 +4,9 @@ class Geozone < ApplicationRecord
has_many :proposals
has_many :debates
has_many :users
+ has_many :headings, class_name: "Budget::Heading", dependent: :nullify
validates :name, presence: true
+ validates :geojson, geojson_format: true
scope :public_for_api, -> { all }
@@ -17,4 +19,28 @@ class Geozone < ApplicationRecord
association.klass.where(geozone: self).empty?
end
end
+
+ def outline_points
+ normalized_coordinates.map { |longlat| [longlat.last, longlat.first] }
+ end
+
+ private
+
+ def normalized_coordinates
+ if geojson.present?
+ if geojson.match(/"coordinates"\s*:\s*\[{4}/)
+ coordinates.reduce([], :concat).reduce([], :concat)
+ elsif geojson.match(/"coordinates"\s*:\s*\[{3}/)
+ coordinates.reduce([], :concat)
+ else
+ coordinates
+ end
+ else
+ []
+ end
+ end
+
+ def coordinates
+ JSON.parse(geojson)["geometry"]["coordinates"]
+ end
end
diff --git a/config/locales/en/activerecord.yml b/config/locales/en/activerecord.yml
index d6563e507..f2089ca9c 100644
--- a/config/locales/en/activerecord.yml
+++ b/config/locales/en/activerecord.yml
@@ -201,7 +201,9 @@ en:
geozone:
name: Name
external_code: "External code (optional)"
+ color: "Color (optional)"
census_code: "Census code (optional)"
+ geojson: "GeoJSON data (optional)"
html_map_coordinates: "HTML