From bc8217e030346999113ff9b79676e27b4d4f26b1 Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 14 Apr 2020 10:24:30 +0200 Subject: [PATCH 1/3] Carte Editor sends FeatureCollection to the server --- app/controllers/champs/carte_controller.rb | 59 ++++++++----------- app/javascript/shared/carte-editor.js | 16 +++-- ...n_area.js => create-feature-collection.js} | 12 ++-- app/models/champs/carte_champ.rb | 1 + app/models/geo_area.rb | 10 ++++ app/views/champs/carte/show.js.erb | 1 + .../champs/carte_controller_spec.rb | 51 +++++++++++++--- spec/models/champs/carte_champ_spec.rb | 3 +- 8 files changed, 98 insertions(+), 55 deletions(-) rename app/javascript/shared/{polygon_area.js => create-feature-collection.js} (66%) diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index d1be197af..9a22d821a 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -1,16 +1,15 @@ class Champs::CarteController < ApplicationController before_action :authenticate_logged_user! - EMPTY_GEO_JSON = '[]' ERROR_GEO_JSON = '' def show @selector = ".carte-#{params[:position]}" - if params[:dossier].key?(:champs_attributes) - coordinates = params[:dossier][:champs_attributes][params[:position]][:value] + feature_collection = if params[:dossier].key?(:champs_attributes) + params[:dossier][:champs_attributes][params[:position]][:value] else - coordinates = params[:dossier][:champs_private_attributes][params[:position]][:value] + params[:dossier][:champs_private_attributes][params[:position]][:value] end @champ = if params[:champ_id].present? @@ -21,40 +20,21 @@ class Champs::CarteController < ApplicationController geo_areas = [] - if coordinates == EMPTY_GEO_JSON - @champ.value = nil - @champ.geo_areas = [] - elsif coordinates == ERROR_GEO_JSON + if feature_collection == ERROR_GEO_JSON @error = true - @champ.value = nil - @champ.geo_areas = [] else - coordinates = JSON.parse(coordinates) + feature_collection = JSON.parse(feature_collection, symbolize_names: true) if @champ.cadastres? - cadastres = ApiCartoService.generate_cadastre(coordinates) - geo_areas += cadastres.map do |cadastre| - cadastre[:source] = GeoArea.sources.fetch(:cadastre) - cadastre - end + populate_cadastres(feature_collection) end - selections_utilisateur = legacy_selections_utilisateur_to_polygons(coordinates) - geo_areas += selections_utilisateur.map do |selection_utilisateur| - selection_utilisateur.merge(source: GeoArea.sources.fetch(:selection_utilisateur)) - end - - @champ.geo_areas = geo_areas.map do |geo_area| - GeoArea.new(geo_area) - end - - @champ.value = coordinates.to_json + geo_areas = GeoArea.from_feature_collection(feature_collection) end if @champ.persisted? - @champ.save + @champ.update(value: nil, geo_areas: geo_areas) end - rescue ApiCarto::API::ResourceNotFound flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.' response.status = 503 @@ -62,14 +42,23 @@ class Champs::CarteController < ApplicationController private - def legacy_selections_utilisateur_to_polygons(coordinates) - coordinates.map do |lat_longs| - { - geometry: { - type: 'Polygon', - coordinates: [lat_longs.map { |lat_long| [lat_long['lng'], lat_long['lat']] }] + def populate_cadastres(feature_collection) + coordinates = feature_collection[:features].filter do |feature| + feature[:geometry][:type] == 'Polygon' + end.map do |feature| + feature[:geometry][:coordinates][0].map { |(lng, lat)| { 'lng' => lng, 'lat' => lat } } + end + + if coordinates.present? + cadastres = ApiCartoService.generate_cadastre(coordinates) + + feature_collection[:features] += cadastres.map do |cadastre| + { + type: 'Feature', + geometry: cadastre.delete(:geometry), + properties: cadastre.merge(source: GeoArea.sources.fetch(:cadastre)) } - } + end end end end diff --git a/app/javascript/shared/carte-editor.js b/app/javascript/shared/carte-editor.js index 7bd52e3d4..9849a6312 100644 --- a/app/javascript/shared/carte-editor.js +++ b/app/javascript/shared/carte-editor.js @@ -1,9 +1,10 @@ import L from 'leaflet'; import FreeDraw from 'leaflet-freedraw'; +import area from '@turf/area'; import { fire, delegate } from '@utils'; import $ from 'jquery'; -import polygonArea from './polygon_area'; +import createFeatureCollection from './create-feature-collection'; const MAPS = new WeakMap(); @@ -81,13 +82,18 @@ function drawUserSelectionEditor(map, { selection }) { export function addFreeDrawEvents(map, selector) { const input = findInput(selector); + map.freeDraw.on('markers', ({ latLngs }) => { if (latLngs.length === 0) { input.value = EMPTY_GEO_JSON; - } else if (polygonArea(latLngs) < 300000) { - input.value = JSON.stringify(latLngs); } else { - input.value = ERROR_GEO_JSON; + const featureCollection = createFeatureCollection(latLngs); + + if (area(featureCollection) < 300000) { + input.value = JSON.stringify(featureCollection); + } else { + input.value = ERROR_GEO_JSON; + } } fire(input, 'change'); @@ -121,7 +127,7 @@ function getCurrentMap(element) { } } -const EMPTY_GEO_JSON = '[]'; +const EMPTY_GEO_JSON = '{ "type": "FeatureCollection", "features": [] }'; const ERROR_GEO_JSON = ''; function findInput(selector) { diff --git a/app/javascript/shared/polygon_area.js b/app/javascript/shared/create-feature-collection.js similarity index 66% rename from app/javascript/shared/polygon_area.js rename to app/javascript/shared/create-feature-collection.js index d0384a310..c5dc09a79 100644 --- a/app/javascript/shared/polygon_area.js +++ b/app/javascript/shared/create-feature-collection.js @@ -1,16 +1,16 @@ -import area from '@turf/area'; - -export default function polygonArea(latLngs) { - return area({ +export default function createFeatureCollection(latLngs) { + return { type: 'FeatureCollection', features: latLngs.map(featurePolygonLatLngs) - }); + }; } function featurePolygonLatLngs(latLngs) { return { type: 'Feature', - properties: {}, + properties: { + source: 'selection_utilisateur' + }, geometry: { type: 'Polygon', coordinates: [latLngs.map(({ lng, lat }) => [lng, lat])] diff --git a/app/models/champs/carte_champ.rb b/app/models/champs/carte_champ.rb index 23269ac02..7d54d9880 100644 --- a/app/models/champs/carte_champ.rb +++ b/app/models/champs/carte_champ.rb @@ -74,6 +74,7 @@ class Champs::CarteChamp < Champ def to_feature_collection { type: 'FeatureCollection', + id: type_de_champ.stable_id, bbox: bounding_box, features: (legacy_selections_utilisateur + except_selections_utilisateur).map(&:to_feature) } diff --git a/app/models/geo_area.rb b/app/models/geo_area.rb index 6ff08e68a..ce89d7e03 100644 --- a/app/models/geo_area.rb +++ b/app/models/geo_area.rb @@ -43,4 +43,14 @@ class GeoArea < ApplicationRecord def rgeo_geometry RGeo::GeoJSON.decode(geometry.to_json, geo_factory: RGeo::Geographic.simple_mercator_factory) end + + def self.from_feature_collection(feature_collection) + feature_collection[:features].map do |feature| + GeoArea.new( + source: feature[:properties].delete(:source), + properties: feature[:properties], + geometry: feature[:geometry] + ) + end + end end diff --git a/app/views/champs/carte/show.js.erb b/app/views/champs/carte/show.js.erb index 7b80491fe..a34b4d2b4 100644 --- a/app/views/champs/carte/show.js.erb +++ b/app/views/champs/carte/show.js.erb @@ -5,3 +5,4 @@ locals: { champ: @champ, error: @error }) %> <%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %> +<%= fire_event('map:update', { featureCollection: @champ.to_feature_collection }.to_json) %> diff --git a/spec/controllers/champs/carte_controller_spec.rb b/spec/controllers/champs/carte_controller_spec.rb index dc483d39b..69493fd05 100644 --- a/spec/controllers/champs/carte_controller_spec.rb +++ b/spec/controllers/champs/carte_controller_spec.rb @@ -36,7 +36,12 @@ describe Champs::CarteController, type: :controller do end context 'when coordinates are empty' do - let(:value) { '[]' } + let(:value) do + { + type: 'FeatureCollection', + features: [] + }.to_json + end it { expect(assigns(:error)).to eq(nil) @@ -47,11 +52,26 @@ describe Champs::CarteController, type: :controller do end context 'when coordinates are informed' do - let(:value) { [[{ "lat": 48.87442541960633, "lng": 2.3859214782714844 }, { "lat": 48.87273183590832, "lng": 2.3850631713867183 }, { "lat": 48.87081237174292, "lng": 2.3809432983398438 }, { "lat": 48.8712640169951, "lng": 2.377510070800781 }, { "lat": 48.87510283703279, "lng": 2.3778533935546875 }, { "lat": 48.87544154230615, "lng": 2.382831573486328 }, { "lat": 48.87442541960633, "lng": 2.3859214782714844 }]].to_json } + let(:value) do + { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + source: 'selection_utilisateur' + }, + geometry: { type: 'Polygon', coordinates: [[[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.377510070800781, 48.8712640169951], [2.3859214782714844, 48.87442541960633]]] } + } + ] + }.to_json + end - it { expect(response.body).not_to be_nil } - it { expect(response.body).to include('MultiPolygon') } - it { expect(response.body).to include('[2.38715792094576,48.8723062632126]') } + it { + expect(response.body).not_to be_nil + expect(response.body).to include('MultiPolygon') + expect(response.body).to include('[2.38715792094576,48.8723062632126]') + } end context 'when error' do @@ -76,10 +96,25 @@ describe Champs::CarteController, type: :controller do post :show, params: params, format: 'js' end - let(:value) { [[{ "lat": 48.87442541960633, "lng": 2.3859214782714844 }, { "lat": 48.87273183590832, "lng": 2.3850631713867183 }, { "lat": 48.87081237174292, "lng": 2.3809432983398438 }, { "lat": 48.8712640169951, "lng": 2.377510070800781 }, { "lat": 48.87510283703279, "lng": 2.3778533935546875 }, { "lat": 48.87544154230615, "lng": 2.382831573486328 }, { "lat": 48.87442541960633, "lng": 2.3859214782714844 }]].to_json } + let(:value) do + { + type: 'FeatureCollection', + features: [ + { + type: 'Feature', + properties: { + source: 'selection_utilisateur' + }, + geometry: { type: 'Polygon', coordinates: [[[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.377510070800781, 48.8712640169951], [2.3859214782714844, 48.87442541960633]]] } + } + ] + }.to_json + end - it { expect(response.status).to eq 503 } - it { expect(response.body).to include('Les données cartographiques sont temporairement indisponibles') } + it { + expect(response.status).to eq 503 + expect(response.body).to include('Les données cartographiques sont temporairement indisponibles') + } end end end diff --git a/spec/models/champs/carte_champ_spec.rb b/spec/models/champs/carte_champ_spec.rb index 5f286e6f7..03131df5f 100644 --- a/spec/models/champs/carte_champ_spec.rb +++ b/spec/models/champs/carte_champ_spec.rb @@ -1,5 +1,5 @@ describe Champs::CarteChamp do - let(:champ) { Champs::CarteChamp.new(geo_areas: geo_areas) } + let(:champ) { Champs::CarteChamp.new(geo_areas: geo_areas, type_de_champ: create(:type_de_champ_carte)) } let(:value) { '' } let(:coordinates) { [[2.3859214782714844, 48.87442541960633], [2.3850631713867183, 48.87273183590832], [2.3809432983398438, 48.87081237174292], [2.3859214782714844, 48.87442541960633]] } let(:geo_json) do @@ -49,6 +49,7 @@ describe Champs::CarteChamp do let(:feature_collection) { { type: 'FeatureCollection', + id: champ.type_de_champ.stable_id, bbox: champ.bounding_box, features: features } From e512870fd2085963b750fa9906d199853db80ccf Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Tue, 14 Apr 2020 15:49:54 +0200 Subject: [PATCH 2/3] Cleanup procedure duree_conservation_dossiers_dans_ds validations --- app/models/procedure.rb | 13 ++----------- spec/models/procedure_spec.rb | 36 ----------------------------------- 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/app/models/procedure.rb b/app/models/procedure.rb index 581b3e616..099961c4a 100644 --- a/app/models/procedure.rb +++ b/app/models/procedure.rb @@ -85,11 +85,8 @@ class Procedure < ApplicationRecord validate :validate_for_publication, on: :publication validate :check_juridique validates :path, presence: true, format: { with: /\A[a-z0-9_\-]{3,50}\z/ }, uniqueness: { scope: [:path, :closed_at, :hidden_at, :unpublished_at], case_sensitive: false } - # FIXME: remove duree_conservation_required flag once all procedures are converted to the new style - validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }, if: :durees_conservation_required - validates :duree_conservation_dossiers_hors_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, if: :durees_conservation_required - validates :duree_conservation_dossiers_dans_ds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION }, unless: :durees_conservation_required - validates :duree_conservation_dossiers_hors_ds, allow_nil: true, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, unless: :durees_conservation_required + validates :duree_conservation_dossiers_dans_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: MAX_DUREE_CONSERVATION } + validates :duree_conservation_dossiers_hors_ds, allow_nil: false, numericality: { only_integer: true, greater_than_or_equal_to: 0 } validates_with MonAvisEmbedValidator validates :notice, content_type: [ "application/msword", @@ -112,7 +109,6 @@ class Procedure < ApplicationRecord validates :logo, content_type: ['image/png', 'image/jpg', 'image/jpeg'], size: { less_than: 5.megabytes } before_save :update_juridique_required - before_save :update_durees_conservation_required after_initialize :ensure_path_exists before_save :ensure_path_exists after_create :ensure_default_groupe_instructeur @@ -597,11 +593,6 @@ class Procedure < ApplicationRecord end end - def update_durees_conservation_required - self.durees_conservation_required ||= duree_conservation_dossiers_hors_ds.present? && duree_conservation_dossiers_dans_ds.present? - true - end - def percentile_time(start_attribute, end_attribute, p) times = dossiers .where.not(start_attribute => nil, end_attribute => nil) diff --git a/spec/models/procedure_spec.rb b/spec/models/procedure_spec.rb index 5fe8370d6..17cf2fd82 100644 --- a/spec/models/procedure_spec.rb +++ b/spec/models/procedure_spec.rb @@ -252,22 +252,11 @@ describe Procedure do shared_examples 'duree de conservation' do context 'duree_conservation_required it true, the field gets validated' do - before { subject.durees_conservation_required = true } - it { is_expected.not_to allow_value(nil).for(field_name) } it { is_expected.not_to allow_value('').for(field_name) } it { is_expected.not_to allow_value('trois').for(field_name) } it { is_expected.to allow_value(3).for(field_name) } end - - context 'duree_conservation_required is false, the field doesn’t get validated' do - before { subject.durees_conservation_required = false } - - it { is_expected.to allow_value(nil).for(field_name) } - it { is_expected.to allow_value('').for(field_name) } - it { is_expected.not_to allow_value('trois').for(field_name) } - it { is_expected.to allow_value(3).for(field_name) } - end end describe 'duree de conservation dans ds' do @@ -283,31 +272,6 @@ describe Procedure do end end - describe '#duree_de_conservation_required' do - it 'automatically jumps to true once both durees de conservation have been set' do - p = build( - :procedure, - durees_conservation_required: false, - duree_conservation_dossiers_dans_ds: nil, - duree_conservation_dossiers_hors_ds: nil - ) - p.save - expect(p.durees_conservation_required).to be_falsey - - p.duree_conservation_dossiers_hors_ds = 3 - p.save - expect(p.durees_conservation_required).to be_falsey - - p.duree_conservation_dossiers_dans_ds = 6 - p.save - expect(p.durees_conservation_required).to be_truthy - - p.duree_conservation_dossiers_dans_ds = nil - p.save - expect(p.durees_conservation_required).to be_truthy - end - end - describe '#types_de_champ (ordered)' do let(:procedure) { create(:procedure) } let!(:type_de_champ_0) { create(:type_de_champ, procedure: procedure, order_place: 1) } From 8f01eebf1fa1ca736b8308890db42af3774da7eb Mon Sep 17 00:00:00 2001 From: Paul Chavard Date: Fri, 17 Apr 2020 12:46:07 +0200 Subject: [PATCH 3/3] fix legacy api carte serializer --- app/serializers/dossier_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/dossier_serializer.rb b/app/serializers/dossier_serializer.rb index b5a4beeb8..d4b4a90d6 100644 --- a/app/serializers/dossier_serializer.rb +++ b/app/serializers/dossier_serializer.rb @@ -38,7 +38,7 @@ class DossierSerializer < ActiveModel::Serializer end if champ_carte.present? - champs_geo_areas = geo_areas.filter do |geo_area| + champs_geo_areas = champ_carte.geo_areas.filter do |geo_area| geo_area.source != GeoArea.sources.fetch(:selection_utilisateur) end champs_geo_areas << champ_carte.selection_utilisateur_legacy_geo_area