Merge pull request #4740 from betagouv/dev

2020-02-03-01
This commit is contained in:
Keirua 2020-02-03 15:23:28 +01:00 committed by GitHub
commit 0387d89187
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 1875 additions and 1321 deletions

View file

@ -1,14 +1,13 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { library } from '@fortawesome/fontawesome-svg-core'; import { library } from '@fortawesome/fontawesome-svg-core';
import {
faArrowCircleDown, import { faArrowCircleDown } from '@fortawesome/free-solid-svg-icons/faArrowCircleDown';
faArrowDown, import { faArrowDown } from '@fortawesome/free-solid-svg-icons/faArrowDown';
faArrowsAltV, import { faArrowsAltV } from '@fortawesome/free-solid-svg-icons/faArrowsAltV';
faArrowUp, import { faArrowUp } from '@fortawesome/free-solid-svg-icons/faArrowUp';
faPlus, import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus';
faTrash import { faTrash } from '@fortawesome/free-solid-svg-icons/faTrash';
} from '@fortawesome/free-solid-svg-icons';
import Flash from './Flash'; import Flash from './Flash';
import OperationsQueue from './OperationsQueue'; import OperationsQueue from './OperationsQueue';

View file

@ -1,47 +1,38 @@
import { initMap, drawPolygons, addFreeDrawEvents } from '../../shared/carte';
async function initialize() { async function initialize() {
const elements = document.querySelectorAll('.carte'); const elements = document.querySelectorAll('.carte');
if (elements.length) { if (elements.length) {
const editable = [...elements].find(element =>
element.classList.contains('edit')
);
await loadLeaflet(editable);
for (let element of elements) { for (let element of elements) {
diplayMap(element, null, true); loadAndDrawMap(element);
} }
} }
} }
// We load leaflet dynamically, ramda and freedraw and assign them to globals. async function loadAndDrawMap(element) {
// Latest freedraw version build needs globals. const data = JSON.parse(element.dataset.geo);
async function loadLeaflet(editable) { const editable = element.classList.contains('edit');
window.L = await import('leaflet').then(({ default: L }) => L);
if (editable) { if (editable) {
window.R = await import('ramda').then(({ default: R }) => R); const { drawEditableMap } = await import('../../shared/carte-editor');
await import('leaflet-freedraw/dist/leaflet-freedraw.web.js');
drawEditableMap(element, data);
} else {
const { drawMap } = await import('../../shared/carte');
drawMap(element, data);
} }
} }
function diplayMap(element, data, initial = false) { async function loadAndRedrawMap(element, data) {
data = data || JSON.parse(element.dataset.geo); const { redrawMap } = await import('../../shared/carte-editor');
const editable = element.classList.contains('edit');
const map = initMap(element, data.position, editable);
drawPolygons(map, data, { initial, editable }); redrawMap(element, data);
if (initial && editable) {
const input = element.parentElement.querySelector('input[data-remote]');
addFreeDrawEvents(map, input);
}
} }
addEventListener('turbolinks:load', initialize); addEventListener('turbolinks:load', initialize);
addEventListener('carte:update', ({ detail: { selector, data } }) => { addEventListener('carte:update', ({ detail: { selector, data } }) => {
const element = document.querySelector(selector); const element = document.querySelector(selector);
diplayMap(element, data);
loadAndRedrawMap(element, data);
}); });

View file

@ -0,0 +1,214 @@
import L from 'leaflet';
import FreeDraw from 'leaflet-freedraw';
import { fire, delegate } from '@utils';
import $ from 'jquery';
import polygonArea from './polygon_area';
const MAPS = new WeakMap();
export function drawEditableMap(element, data) {
const map = initMap(element, data);
drawCadastre(map, data);
drawQuartiersPrioritaires(map, data);
drawParcellesAgricoles(map, data);
drawUserSelectionEditor(map, data);
const input = element.parentElement.querySelector('input[data-remote]');
addFreeDrawEvents(map, input);
}
export function redrawMap(element, data) {
const map = initMap(element, data);
clearLayers(map);
drawCadastre(map, data);
drawQuartiersPrioritaires(map, data);
drawParcellesAgricoles(map, data);
bringToFrontUserSelection(map);
}
function initMap(element, { position }) {
if (MAPS.has(element)) {
return MAPS.get(element);
} else {
const map = L.map(element, {
scrollWheelZoom: false
}).setView([position.lat, position.lon], 18);
const loadTilesLayer = process.env.RAILS_ENV != 'test';
if (loadTilesLayer) {
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);
}
const freeDraw = new FreeDraw({
mode: FreeDraw.NONE,
smoothFactor: 4,
mergePolygons: false
});
map.addLayer(freeDraw);
map.freeDraw = freeDraw;
MAPS.set(element, map);
return map;
}
}
function toLatLngs({ coordinates }) {
return coordinates.map(polygon =>
polygon[0].map(point => L.GeoJSON.coordsToLatLng(point))
);
}
function drawUserSelectionEditor(map, { selection }) {
if (selection) {
const geoJSON = L.geoJSON(selection);
for (let polygon of toLatLngs(selection)) {
map.freeDraw.create(polygon);
}
map.fitBounds(geoJSON.getBounds());
}
}
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;
}
fire(input, 'change');
});
}
function drawCadastre(map, { cadastres }) {
drawLayer(map, cadastres, CADASTRE_POLYGON_STYLE);
}
function drawQuartiersPrioritaires(map, { quartiersPrioritaires }) {
drawLayer(map, quartiersPrioritaires, QP_POLYGON_STYLE);
}
function drawParcellesAgricoles(map, { parcellesAgricoles }) {
drawLayer(map, parcellesAgricoles, RPG_POLYGON_STYLE);
}
function getCurrentMap(element) {
if (!element.matches('.carte')) {
const closestCarteElement = element.closest('.carte');
const closestToolbarElement = element.closest('.toolbar');
element = closestCarteElement
? closestCarteElement
: closestToolbarElement.parentElement.querySelector('.carte');
}
if (MAPS.has(element)) {
return MAPS.get(element);
}
}
const EMPTY_GEO_JSON = '[]';
const ERROR_GEO_JSON = '';
function findInput(selector) {
return typeof selector === 'string'
? document.querySelector(selector)
: selector;
}
function clearLayers(map) {
map.eachLayer(layer => {
if (layer instanceof L.GeoJSON) {
map.removeLayer(layer);
}
});
}
function bringToFrontUserSelection(map) {
for (let layer of map.freeDraw.all()) {
layer.bringToFront();
}
}
function drawLayer(map, data, style) {
if (Array.isArray(data) && data.length > 0) {
const layer = new L.GeoJSON(undefined, {
interactive: false,
style
});
for (let { geometry } of data) {
layer.addData(geometry);
}
layer.addTo(map);
}
}
const POLYGON_STYLE = {
weight: 2,
opacity: 0.3,
color: 'white',
dashArray: '3',
fillOpacity: 0.7
};
const CADASTRE_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
fillColor: '#8a6d3b'
});
const QP_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
fillColor: '#31708f'
});
const RPG_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
fillColor: '#31708f'
});
delegate('click', '.carte.edit', event => {
const map = getCurrentMap(event.target);
if (map) {
const isPath = event.target.matches('.leaflet-container g path');
if (isPath) {
setTimeout(() => {
map.freeDraw.mode(FreeDraw.EDIT | FreeDraw.DELETE);
}, 50);
} else {
map.freeDraw.mode(FreeDraw.NONE);
}
}
});
delegate('click', '.toolbar .new-area', event => {
event.preventDefault();
const map = getCurrentMap(event.target);
if (map) {
map.freeDraw.mode(FreeDraw.CREATE);
}
});
$(document).on('select2:select', 'select[data-address]', event => {
const map = getCurrentMap(event.target);
const { geometry } = event.params.data;
if (map && geometry && geometry.type === 'Point') {
const [lon, lat] = geometry.coordinates;
map.setView(new L.LatLng(lat, lon), 14);
}
});

View file

@ -1,18 +1,23 @@
/* globals FreeDraw L */ import L from 'leaflet';
import { fire, delegate } from '@utils';
import $ from 'jquery';
import polygonArea from './polygon_area';
const MAPS = new WeakMap(); const MAPS = new WeakMap();
export function initMap(element, position, editable = false) { export function drawMap(element, data) {
const map = initMap(element, data);
drawCadastre(map, data);
drawQuartiersPrioritaires(map, data);
drawParcellesAgricoles(map, data);
drawUserSelection(map, data);
}
function initMap(element, { position }) {
if (MAPS.has(element)) { if (MAPS.has(element)) {
return MAPS.get(element); return MAPS.get(element);
} else { } else {
const map = L.map(element, { const map = L.map(element, {
scrollWheelZoom: false scrollWheelZoom: false
}).setView([position.lat, position.lon], editable ? 18 : position.zoom); }).setView([position.lat, position.lon], position.zoom);
const loadTilesLayer = process.env.RAILS_ENV != 'test'; const loadTilesLayer = process.env.RAILS_ENV != 'test';
if (loadTilesLayer) { if (loadTilesLayer) {
@ -22,168 +27,47 @@ export function initMap(element, position, editable = false) {
}).addTo(map); }).addTo(map);
} }
if (editable) {
const freeDraw = new FreeDraw({
mode: FreeDraw.NONE,
smoothFactor: 4,
mergePolygons: false
});
map.addLayer(freeDraw);
map.freeDraw = freeDraw;
}
MAPS.set(element, map); MAPS.set(element, map);
return map; return map;
} }
} }
export function drawPolygons(map, data, { editable, initial }) { function drawUserSelection(map, { selection }) {
if (initial) {
drawUserSelection(map, data, editable);
}
clearLayers(map);
drawCadastre(map, data, editable);
drawQuartiersPrioritaires(map, data, editable);
drawParcellesAgricoles(map, data, editable);
bringToFrontUserSelection(map);
}
export function drawUserSelection(map, { selection }, editable = false) {
if (selection) { if (selection) {
const coordinates = toLatLngs(selection); const layer = L.geoJSON(selection, {
let polygon; style: USER_SELECTION_POLYGON_STYLE
if (editable) {
coordinates.forEach(polygon => map.freeDraw.create(polygon));
[polygon] = markFreeDrawLayers(map);
} else {
polygon = L.polygon(coordinates, {
color: 'red',
zIndex: 3
}); });
polygon.addTo(map);
}
if (polygon) {
map.fitBounds(polygon.getBounds());
}
}
}
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;
}
markFreeDrawLayers(map);
fire(input, 'change');
});
}
function drawCadastre(map, { cadastres }, editable = false) {
drawLayer(
map,
cadastres,
editable ? CADASTRE_POLYGON_STYLE : noEditStyle(CADASTRE_POLYGON_STYLE)
);
}
function drawQuartiersPrioritaires(
map,
{ quartiersPrioritaires },
editable = false
) {
drawLayer(
map,
quartiersPrioritaires,
editable ? QP_POLYGON_STYLE : noEditStyle(QP_POLYGON_STYLE)
);
}
function drawParcellesAgricoles(map, { parcellesAgricoles }, editable = false) {
drawLayer(
map,
parcellesAgricoles,
editable ? RPG_POLYGON_STYLE : noEditStyle(RPG_POLYGON_STYLE)
);
}
function getCurrentMap(element) {
if (!element.matches('.carte')) {
const closestCarteElement = element.closest('.carte');
const closestToolbarElement = element.closest('.toolbar');
element = closestCarteElement
? closestCarteElement
: closestToolbarElement.parentElement.querySelector('.carte');
}
if (MAPS.has(element)) {
return MAPS.get(element);
}
}
const EMPTY_GEO_JSON = '[]';
const ERROR_GEO_JSON = '';
function toLatLngs({ coordinates }) {
return coordinates.map(polygon =>
polygon[0].map(point => ({ lng: point[0], lat: point[1] }))
);
}
function findInput(selector) {
return typeof selector === 'string'
? document.querySelector(selector)
: selector;
}
function createLayer(map) {
const layer = new L.GeoJSON(undefined, {
interactive: false
});
layer.addTo(map); layer.addTo(map);
return layer;
}
function clearLayers(map) { map.fitBounds(layer.getBounds());
map.eachLayer(layer => {
if (layer instanceof L.GeoJSON) {
map.removeLayer(layer);
} }
});
} }
function bringToFrontUserSelection(map) { function drawCadastre(map, { cadastres }) {
map.eachLayer(layer => { drawLayer(map, cadastres, noEditStyle(CADASTRE_POLYGON_STYLE));
if (layer.isFreeDraw) {
layer.bringToFront();
}
});
} }
function markFreeDrawLayers(map) { function drawQuartiersPrioritaires(map, { quartiersPrioritaires }) {
return map.freeDraw.all().map(layer => { drawLayer(map, quartiersPrioritaires, noEditStyle(QP_POLYGON_STYLE));
layer.isFreeDraw = true; }
return layer;
}); function drawParcellesAgricoles(map, { parcellesAgricoles }) {
drawLayer(map, parcellesAgricoles, noEditStyle(RPG_POLYGON_STYLE));
} }
function drawLayer(map, data, style) { function drawLayer(map, data, style) {
if (Array.isArray(data) && data.length > 0) { if (Array.isArray(data) && data.length > 0) {
const layer = createLayer(map); const layer = new L.GeoJSON(undefined, {
interactive: false,
data.forEach(function(item) { style
layer.addData(item.geometry);
}); });
layer.setStyle(style).addTo(map); for (let { geometry } of data) {
layer.addData(geometry);
}
layer.addTo(map);
} }
} }
@ -215,36 +99,6 @@ const RPG_POLYGON_STYLE = Object.assign({}, POLYGON_STYLE, {
fillColor: '#31708f' fillColor: '#31708f'
}); });
delegate('click', '.carte.edit', event => { const USER_SELECTION_POLYGON_STYLE = {
const map = getCurrentMap(event.target); color: 'red'
};
if (map) {
const isPath = event.target.matches('.leaflet-container g path');
if (isPath) {
setTimeout(() => {
map.freeDraw.mode(FreeDraw.EDIT | FreeDraw.DELETE);
}, 50);
} else {
map.freeDraw.mode(FreeDraw.NONE);
}
}
});
delegate('click', '.toolbar .new-area', event => {
event.preventDefault();
const map = getCurrentMap(event.target);
if (map) {
map.freeDraw.mode(FreeDraw.CREATE);
}
});
$(document).on('select2:select', 'select[data-address]', event => {
const map = getCurrentMap(event.target);
const { geometry } = event.params.data;
if (map && geometry && geometry.type === 'Point') {
const [lon, lat] = geometry.coordinates;
map.setView(new L.LatLng(lat, lon), 14);
}
});

View file

@ -9,4 +9,9 @@ const resolve = {
environment.splitChunks(); environment.splitChunks();
environment.config.merge({ resolve }); environment.config.merge({ resolve });
// Uncoment next lines to run webpack-bundle-analyzer
// const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
// environment.plugins.append('BundleAnalyzer', new BundleAnalyzerPlugin());
module.exports = environment; module.exports = environment;

View file

@ -1,4 +1,4 @@
if ENV["RAILS_ENV"] == "development" if defined?(RuboCop)
module RuboCop module RuboCop
module Cop module Cop
module DS module DS

View file

@ -1,57 +1,58 @@
{ {
"dependencies": { "dependencies": {
"@babel/preset-react": "^7.6.3", "@babel/preset-react": "^7.8.3",
"@fortawesome/fontawesome-svg-core": "^1.2.25", "@fortawesome/fontawesome-svg-core": "^1.2.26",
"@fortawesome/free-solid-svg-icons": "^5.11.2", "@fortawesome/free-solid-svg-icons": "^5.12.0",
"@fortawesome/react-fontawesome": "^0.1.7", "@fortawesome/react-fontawesome": "^0.1.8",
"@rails/actiontext": "^6.0.0", "@rails/actiontext": "^6.0.2-1",
"@rails/activestorage": "^6.0.0", "@rails/activestorage": "^6.0.2-1",
"@rails/ujs": "^6.0.0", "@rails/ujs": "^6.0.2-1",
"@rails/webpacker": "4.0.7", "@rails/webpacker": "4.2.2",
"@sentry/browser": "^5.7.1", "@sentry/browser": "^5.11.2",
"@turf/area": "^6.0.1", "@turf/area": "^6.0.1",
"babel-plugin-macros": "^2.6.1", "babel-plugin-macros": "^2.8.0",
"babel-plugin-transform-react-remove-prop-types": "^0.4.24", "babel-plugin-transform-react-remove-prop-types": "^0.4.24",
"chartkick": "^3.1.3", "chartkick": "^3.2.0",
"core-js": "^2.0.0", "core-js": "^2.0.0",
"debounce": "^1.2.0", "debounce": "^1.2.0",
"dom4": "^2.1.5", "dom4": "^2.1.5",
"email-butler": "^1.0.12", "email-butler": "^1.0.13",
"highcharts": "^6.1.2", "highcharts": "^6.1.2",
"intersection-observer": "^0.7.0", "intersection-observer": "^0.7.0",
"jquery": "^3.4.1", "jquery": "^3.4.1",
"leaflet": "^1.4.0", "leaflet": "^1.6.0",
"leaflet-freedraw": "^2.10.0", "leaflet-freedraw": "^2.12.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"ramda": "=0.24.1", "react": "^16.12.0",
"react": "^16.11.0", "react-dom": "^16.12.0",
"react-dom": "^16.11.0", "react-intersection-observer": "^8.25.2",
"react-intersection-observer": "^8.25.1",
"react-loadable": "^5.5.0", "react-loadable": "^5.5.0",
"react-scroll-to-component": "^1.0.2", "react-scroll-to-component": "^1.0.2",
"react-sortable-hoc": "^1.10.1", "react-sortable-hoc": "^1.11.0",
"react_ujs": "^2.6.0", "react_ujs": "^2.6.1",
"select2": "^4.0.11", "select2": "^4.0.13",
"trix": "^1.2.1", "trix": "^1.2.2",
"turbolinks": "^5.2.0", "turbolinks": "^5.2.0",
"whatwg-fetch": "^3.0.0" "whatwg-fetch": "^3.0.0"
}, },
"devDependencies": { "devDependencies": {
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"eclint": "^2.8.1", "eclint": "^2.8.1",
"eslint": "^6.6.0", "eslint": "^6.7.2",
"eslint-config-prettier": "^6.5.0", "eslint-config-prettier": "^6.10.0",
"eslint-plugin-prettier": "^3.1.1", "eslint-plugin-prettier": "^3.1.2",
"eslint-plugin-react": "^7.16.0", "eslint-plugin-react": "^7.18.0",
"eslint-plugin-react-hooks": "^2.2.0", "eslint-plugin-react-hooks": "^2.3.0",
"prettier": "^1.18.2", "prettier": "^1.19.1",
"webpack-dev-server": "^3.9.0" "webpack-bundle-analyzer": "^3.6.0",
"webpack-dev-server": "^3.10.0"
}, },
"scripts": { "scripts": {
"lint:ec": "eclint check $({ git ls-files | grep -v app/graphql/schema.graphql ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)", "lint:ec": "eclint check $({ git ls-files | grep -v app/graphql/schema.graphql ; find vendor -type f ; echo 'db/schema.rb' ; } | sort | uniq -u)",
"lint:js": "eslint ./app/javascript ./app/assets/javascripts ./config/webpack" "lint:js": "eslint ./app/javascript ./app/assets/javascripts ./config/webpack",
"webpack:build": "NODE_ENV=production bin/webpack"
}, },
"engines": { "engines": {
"node": "8.* || >= 10.*" "node": ">= 12.*"
} }
} }

2640
yarn.lock

File diff suppressed because it is too large Load diff