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
|
response.status = 503
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def populate_cadastres(feature_collection)
|
def populate_cadastres(feature_collection)
|
||||||
|
@ -61,4 +99,37 @@ class Champs::CarteController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -3,116 +3,99 @@ import PropTypes from 'prop-types';
|
||||||
import mapboxgl from 'mapbox-gl';
|
import mapboxgl from 'mapbox-gl';
|
||||||
import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
|
import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
|
||||||
import DrawControl from 'react-mapbox-gl-draw';
|
import DrawControl from 'react-mapbox-gl-draw';
|
||||||
import area from '@turf/area';
|
|
||||||
import SwitchMapStyle from './SwitchMapStyle';
|
import SwitchMapStyle from './SwitchMapStyle';
|
||||||
import SearchInput from './SearchInput';
|
import SearchInput from './SearchInput';
|
||||||
import { fire } from '@utils';
|
import { getJSON, ajax } from '@utils';
|
||||||
import ortho from './styles/ortho.json';
|
import ortho from './styles/ortho.json';
|
||||||
import vector from './styles/vector.json';
|
import vector from './styles/vector.json';
|
||||||
import {
|
import { polygonCadastresFill, polygonCadastresLine } from './utils';
|
||||||
createFeatureCollection,
|
|
||||||
polygonCadastresFill,
|
|
||||||
polygonCadastresLine,
|
|
||||||
ERROR_GEO_JSON
|
|
||||||
} from './utils';
|
|
||||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||||
|
|
||||||
const Map = ReactMapboxGl({});
|
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 drawControl = useRef(null);
|
||||||
const [style, setStyle] = useState('ortho');
|
const [style, setStyle] = useState('ortho');
|
||||||
const [coords, setCoords] = useState([1.7, 46.9]);
|
const [coords, setCoords] = useState([1.7, 46.9]);
|
||||||
const [zoom, setZoom] = useState([5]);
|
const [zoom, setZoom] = useState([5]);
|
||||||
const [currentMap, setCurrentMap] = useState({});
|
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 mapStyle = style === 'ortho' ? ortho : vector;
|
||||||
|
const bbox = featureCollection.bbox;
|
||||||
|
const cadastresFeatureCollection = filterFeatureCollection(
|
||||||
|
featureCollection,
|
||||||
|
'cadastre'
|
||||||
|
);
|
||||||
|
|
||||||
const saveFeatureCollection = featuresToSave => {
|
function updateFeaturesList(features) {
|
||||||
const featuresCollection = createFeatureCollection(featuresToSave);
|
const cadastres = features.find(
|
||||||
if (area(featuresCollection) < 300000) {
|
({ geometry }) => geometry.type === 'Polygon'
|
||||||
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
|
|
||||||
);
|
);
|
||||||
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 => {
|
const onMapLoad = map => {
|
||||||
setCurrentMap(map);
|
setCurrentMap(map);
|
||||||
if (userSelections.length > 0) {
|
|
||||||
userSelections.map((selection, index) => {
|
drawControl.current.draw.set(
|
||||||
selection.properties.id = index + 1;
|
filterFeatureCollection(featureCollection, 'selection_utilisateur')
|
||||||
drawControl.current.draw.add(selection);
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMapUpdate = evt => {
|
const onCadastresUpdate = evt => {
|
||||||
if (currentMap) {
|
if (currentMap) {
|
||||||
cadastresFeatureCollection.features = [];
|
|
||||||
constructCadastresFeatureCollection(
|
|
||||||
evt.detail.featureCollection.features
|
|
||||||
);
|
|
||||||
currentMap
|
currentMap
|
||||||
.getSource('cadastres-layer')
|
.getSource('cadastres-layer')
|
||||||
.setData(cadastresFeatureCollection);
|
.setData(
|
||||||
|
filterFeatureCollection(evt.detail.featureCollection, 'cadastre')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addEventListener('map:update', onMapUpdate);
|
addEventListener('cadastres:update', onCadastresUpdate);
|
||||||
return () => removeEventListener('map:update', onMapUpdate);
|
return () => removeEventListener('cadastres:update', onCadastresUpdate);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!mapboxgl.supported()) {
|
if (!mapboxgl.supported()) {
|
||||||
|
@ -193,7 +176,8 @@ MapEditor.propTypes = {
|
||||||
bbox: PropTypes.array,
|
bbox: PropTypes.array,
|
||||||
features: PropTypes.array,
|
features: PropTypes.array,
|
||||||
id: PropTypes.number
|
id: PropTypes.number
|
||||||
})
|
}),
|
||||||
|
url: PropTypes.string
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MapEditor;
|
export default MapEditor;
|
||||||
|
|
|
@ -1,11 +1,3 @@
|
||||||
export const ERROR_GEO_JSON = '';
|
|
||||||
export const createFeatureCollection = selectionsUtilisateur => {
|
|
||||||
return {
|
|
||||||
type: 'FeatureCollection',
|
|
||||||
features: selectionsUtilisateur
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const polygonCadastresFill = {
|
export const polygonCadastresFill = {
|
||||||
'fill-color': '#EC3323',
|
'fill-color': '#EC3323',
|
||||||
'fill-opacity': 0.3
|
'fill-opacity': 0.3
|
||||||
|
|
|
@ -68,7 +68,7 @@ export function ajax(options) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getJSON(url, data, method = 'get') {
|
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(
|
return Promise.resolve(
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method,
|
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',
|
partial: 'shared/champs/carte/geo_areas',
|
||||||
locals: { champ: @champ, error: @error }) %>
|
locals: { champ: @champ, error: @error }) %>
|
||||||
|
|
||||||
<% if feature_enabled?(:new_map_editor) %>
|
<%= fire_event('carte:update', { selector: @selector, data: @champ.to_render_data }.to_json) %>
|
||||||
<%= 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 %>
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
|
|
||||||
- if feature_enabled?(:new_map_editor)
|
- 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
|
- else
|
||||||
.toolbar
|
.toolbar
|
||||||
%button.button.primary.new-area Ajouter une zone
|
%button.button.primary.new-area Ajouter une zone
|
||||||
%select.select2.adresse{ data: { address: true }, placeholder: 'Saisissez une adresse ou positionner la carte' }
|
%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}" }
|
.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
|
.geo-areas
|
||||||
= render partial: 'shared/champs/carte/geo_areas', locals: { champ: champ, error: false }
|
= 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/siret', to: 'siret#show', as: :siret
|
||||||
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
get ':position/dossier_link', to: 'dossier_link#show', as: :dossier_link
|
||||||
post ':position/carte', to: 'carte#show', as: :carte
|
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
|
post ':position/repetition', to: 'repetition#show', as: :repetition
|
||||||
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
|
put 'piece_justificative/:champ_id', to: 'piece_justificative#update', as: :piece_justificative
|
||||||
end
|
end
|
||||||
|
|
|
@ -18,6 +18,105 @@ describe Champs::CarteController, type: :controller do
|
||||||
cadastres: true
|
cadastres: true
|
||||||
}).champ.create(dossier: dossier)
|
}).champ.create(dossier: dossier)
|
||||||
end
|
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
|
describe 'POST #show' do
|
||||||
render_views
|
render_views
|
||||||
|
|
Loading…
Reference in a new issue