Merge pull request #5107 from tchak/map-geo-areas-api
Implement atomic operations on MapEditor
This commit is contained in:
commit
b7d511a6e2
9 changed files with 258 additions and 101 deletions
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
9
app/views/champs/carte/index.js.erb
Normal file
9
app/views/champs/carte/index.js.erb
Normal 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 %>
|
|
@ -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) %>
|
||||
|
|
|
@ -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' }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue