Implement atomic operations on MapEditor

This commit is contained in:
Paul Chavard 2020-05-05 15:09:29 +02:00
parent d3ea20968e
commit 05e408225b
9 changed files with 258 additions and 101 deletions

View file

@ -40,6 +40,44 @@ class Champs::CarteController < ApplicationController
response.status = 503
end
def index
@selector = ".carte-#{params[:champ_id]}"
@champ = policy_scope(Champ).find(params[:champ_id])
@update_cadastres = params[:cadastres]
if @champ.cadastres? && @update_cadastres
@champ.geo_areas.cadastres.destroy_all
@champ.geo_areas += GeoArea.from_feature_collection(cadastres_features_collection(@champ.to_feature_collection))
@champ.save!
end
rescue ApiCarto::API::ResourceNotFound
flash.alert = 'Les données cartographiques sont temporairement indisponibles. Réessayez dans un instant.'
response.status = 503
end
def create
champ = policy_scope(Champ).find(params[:champ_id])
geo_area = champ.geo_areas.selections_utilisateur.new
save_geometry!(geo_area)
render json: { feature: geo_area.to_feature }, status: :created
end
def update
champ = policy_scope(Champ).find(params[:champ_id])
geo_area = champ.geo_areas.selections_utilisateur.find(params[:id])
save_geometry!(geo_area)
head :no_content
end
def destroy
champ = policy_scope(Champ).find(params[:champ_id])
champ.geo_areas.selections_utilisateur.find(params[:id]).destroy!
head :no_content
end
private
def populate_cadastres(feature_collection)
@ -61,4 +99,37 @@ class Champs::CarteController < ApplicationController
end
end
end
def save_geometry!(geo_area)
geo_area.geometry = params[:feature][:geometry]
geo_area.save!
end
def cadastres_features_collection(feature_collection)
coordinates = feature_collection[:features].filter do |feature|
feature[:properties][:source] == GeoArea.sources.fetch(:selection_utilisateur) && 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)
{
type: 'FeatureCollection',
features: cadastres.map do |cadastre|
{
type: 'Feature',
geometry: cadastre.delete(:geometry),
properties: cadastre.merge(source: GeoArea.sources.fetch(:cadastre))
}
end
}
else
{
type: 'FeatureCollection',
features: []
}
end
end
end

View file

@ -3,116 +3,99 @@ import PropTypes from 'prop-types';
import mapboxgl from 'mapbox-gl';
import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw';
import area from '@turf/area';
import SwitchMapStyle from './SwitchMapStyle';
import SearchInput from './SearchInput';
import { fire } from '@utils';
import { getJSON, ajax } from '@utils';
import ortho from './styles/ortho.json';
import vector from './styles/vector.json';
import {
createFeatureCollection,
polygonCadastresFill,
polygonCadastresLine,
ERROR_GEO_JSON
} from './utils';
import { polygonCadastresFill, polygonCadastresLine } from './utils';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
const Map = ReactMapboxGl({});
const MapEditor = ({ featureCollection: { features, bbox, id } }) => {
function filterFeatureCollection(featureCollection, source) {
return {
type: 'FeatureCollection',
features: featureCollection.features.filter(
feature => feature.properties.source === source
)
};
}
const MapEditor = ({ featureCollection, url }) => {
const drawControl = useRef(null);
const [style, setStyle] = useState('ortho');
const [coords, setCoords] = useState([1.7, 46.9]);
const [zoom, setZoom] = useState([5]);
const [currentMap, setCurrentMap] = useState({});
let input = document.querySelector(
`input[data-feature-collection-id="${id}"]`
);
let userSelections = features.filter(
feature => feature.properties.source === 'selection_utilisateur'
);
let cadastresFeatureCollection = {
type: 'FeatureCollection',
features: []
};
const constructCadastresFeatureCollection = features => {
for (let feature of features) {
switch (feature.properties.source) {
case 'cadastre':
cadastresFeatureCollection.features.push(feature);
break;
}
}
};
constructCadastresFeatureCollection(features);
const mapStyle = style === 'ortho' ? ortho : vector;
const bbox = featureCollection.bbox;
const cadastresFeatureCollection = filterFeatureCollection(
featureCollection,
'cadastre'
);
const saveFeatureCollection = featuresToSave => {
const featuresCollection = createFeatureCollection(featuresToSave);
if (area(featuresCollection) < 300000) {
input.value = JSON.stringify(featuresCollection);
} else {
input.value = ERROR_GEO_JSON;
}
fire(input, 'change');
};
const onDrawCreate = ({ features }) => {
const draw = drawControl.current.draw;
const featureId = features[0].id;
draw.setFeatureProperty(featureId, 'id', featureId);
draw.setFeatureProperty(featureId, 'source', 'selection_utilisateur');
userSelections.push(draw.get(featureId));
saveFeatureCollection(userSelections);
};
const onDrawUpdate = ({ features }) => {
let featureId = features[0].properties.id;
userSelections = userSelections.map(selection => {
if (selection.properties.id === featureId) {
selection = features[0];
}
return selection;
});
saveFeatureCollection(userSelections);
};
const onDrawDelete = ({ features }) => {
userSelections = userSelections.filter(
selection => selection.properties.id !== features[0].properties.id
function updateFeaturesList(features) {
const cadastres = features.find(
({ geometry }) => geometry.type === 'Polygon'
);
saveFeatureCollection(userSelections);
};
ajax({ url, type: 'get', data: cadastres ? 'cadastres=update' : '' });
}
function setFeatureId(lid, feature) {
const draw = drawControl.current.draw;
draw.setFeatureProperty(lid, 'id', feature.properties.id);
}
async function onDrawCreate({ features }) {
for (const feature of features) {
const data = await getJSON(url, { feature }, 'post');
setFeatureId(feature.id, data.feature);
}
updateFeaturesList(features);
}
async function onDrawUpdate({ features }) {
for (const feature of features) {
let { id } = feature.properties;
await getJSON(`${url}/${id}`, { feature }, 'patch');
}
updateFeaturesList(features);
}
async function onDrawDelete({ features }) {
for (const feature of features) {
const { id } = feature.properties;
await getJSON(`${url}/${id}`, null, 'delete');
}
updateFeaturesList(features);
}
const onMapLoad = map => {
setCurrentMap(map);
if (userSelections.length > 0) {
userSelections.map((selection, index) => {
selection.properties.id = index + 1;
drawControl.current.draw.add(selection);
});
}
drawControl.current.draw.set(
filterFeatureCollection(featureCollection, 'selection_utilisateur')
);
};
const onMapUpdate = evt => {
const onCadastresUpdate = evt => {
if (currentMap) {
cadastresFeatureCollection.features = [];
constructCadastresFeatureCollection(
evt.detail.featureCollection.features
);
currentMap
.getSource('cadastres-layer')
.setData(cadastresFeatureCollection);
.setData(
filterFeatureCollection(evt.detail.featureCollection, 'cadastre')
);
}
};
useEffect(() => {
addEventListener('map:update', onMapUpdate);
return () => removeEventListener('map:update', onMapUpdate);
addEventListener('cadastres:update', onCadastresUpdate);
return () => removeEventListener('cadastres:update', onCadastresUpdate);
});
if (!mapboxgl.supported()) {
@ -193,7 +176,8 @@ MapEditor.propTypes = {
bbox: PropTypes.array,
features: PropTypes.array,
id: PropTypes.number
})
}),
url: PropTypes.string
};
export default MapEditor;

View file

@ -1,11 +1,3 @@
export const ERROR_GEO_JSON = '';
export const createFeatureCollection = selectionsUtilisateur => {
return {
type: 'FeatureCollection',
features: selectionsUtilisateur
};
};
export const polygonCadastresFill = {
'fill-color': '#EC3323',
'fill-opacity': 0.3

View file

@ -68,7 +68,7 @@ export function ajax(options) {
}
export function getJSON(url, data, method = 'get') {
data = method !== 'get' ? JSON.stringify(data) : data;
data = method !== 'get' && data ? JSON.stringify(data) : data;
return Promise.resolve(
$.ajax({
method,

View file

@ -0,0 +1,9 @@
<%= render_flash(timeout: 5000, fixed: true) %>
<%= render_to_element("#{@selector} + .geo-areas",
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %>
<% if @update_cadastres %>
<%= fire_event('cadastres:update', { featureCollection: @champ.to_feature_collection }.to_json) %>
<% end %>

View file

@ -4,8 +4,4 @@
partial: 'shared/champs/carte/geo_areas',
locals: { champ: @champ, error: @error }) %>
<% if feature_enabled?(:new_map_editor) %>
<%= fire_event('map:update', { featureCollection: @champ.to_feature_collection }.to_json) %>
<% else %>
<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %>
<% end %>
<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %>

View file

@ -1,13 +1,13 @@
- if feature_enabled?(:new_map_editor)
= react_component("MapEditor", { featureCollection: champ.to_feature_collection }, class: "carte-#{form.index}")
= react_component("MapEditor", { featureCollection: champ.to_feature_collection, url: champs_carte_features_path(champ) }, class: "carte-#{champ.id}")
- else
.toolbar
%button.button.primary.new-area Ajouter une zone
%select.select2.adresse{ data: { address: true }, placeholder: 'Saisissez une adresse ou positionner la carte' }
.carte.edit{ data: { geo: geo_data(champ) }, class: "carte-#{form.index}" }
= form.hidden_field :value,
data: { remote: true, feature_collection_id: champ.stable_id, url: champs_carte_path(form.index), params: champ_carte_params(champ).to_query, method: 'post' }
.geo-areas
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }
= form.hidden_field :value,
data: { remote: true, feature_collection_id: champ.stable_id, url: champs_carte_path(form.index), params: champ_carte_params(champ).to_query, method: 'post' }

View file

@ -120,6 +120,12 @@ Rails.application.routes.draw do
get ':position/siret', to: 'siret#show', as: :siret
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
post ':position/carte', to: 'carte#show', as: :carte
get ':champ_id/carte/features', to: 'carte#index', as: :carte_features
post ':champ_id/carte/features', to: 'carte#create'
patch ':champ_id/carte/features/:id', to: 'carte#update'
delete ':champ_id/carte/features/:id', to: 'carte#destroy'
post ':position/repetition', to: 'repetition#show', as: :repetition
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
end

View file

@ -18,6 +18,105 @@ describe Champs::CarteController, type: :controller do
cadastres: true
}).champ.create(dossier: dossier)
end
describe 'features' do
let(:feature) { attributes_for(:geo_area, :polygon) }
let(:geo_area) { create(:geo_area, :selection_utilisateur, :polygon, champ: champ) }
let(:params) do
{
champ_id: champ.id,
feature: feature
}
end
before do
sign_in user
request.accept = "application/json"
request.content_type = "application/json"
end
describe 'POST #create' do
before do
post :create, params: params
end
it { expect(response.status).to eq 201 }
end
describe 'PATCH #update' do
let(:params) do
{
champ_id: champ.id,
id: geo_area.id,
feature: feature
}
end
before do
patch :update, params: params
end
it { expect(response.status).to eq 204 }
end
describe 'DELETE #destroy' do
let(:params) do
{
champ_id: champ.id,
id: geo_area.id
}
end
before do
delete :destroy, params: params
end
it { expect(response.status).to eq 204 }
end
describe 'GET #index' do
render_views
before do
request.accept = "application/javascript"
request.content_type = "application/javascript"
end
context 'with cadastres update' do
let(:params) do
{
champ_id: champ.id,
cadastres: 'update'
}
end
before do
get :index, params: params
end
it {
expect(response.status).to eq 200
expect(response.body).to include("DS.fire('cadastres:update'")
}
end
context 'without cadastres update' do
let(:params) do
{
champ_id: champ.id
}
end
before do
get :index, params: params
end
it {
expect(response.status).to eq 200
expect(response.body).not_to include("DS.fire('cadastres:update'")
}
end
end
end
describe 'POST #show' do
render_views