Add cadastres to MapEditor
This commit is contained in:
parent
19440afebf
commit
2244263b49
3 changed files with 653 additions and 312 deletions
|
@ -1,257 +1,47 @@
|
|||
import React, {
|
||||
useState,
|
||||
useCallback,
|
||||
useRef,
|
||||
useMemo,
|
||||
useEffect
|
||||
} from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
|
||||
import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl';
|
||||
import DrawControl from 'react-mapbox-gl-draw';
|
||||
import { MapIcon } from '@heroicons/react/outline';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
|
||||
|
||||
import { getJSON, ajax, fire } from '@utils';
|
||||
|
||||
import Mapbox from '../shared/mapbox/Mapbox';
|
||||
import { getMapStyle } from '../shared/mapbox/styles';
|
||||
import SwitchMapStyle from '../shared/mapbox/SwitchMapStyle';
|
||||
import MapStyleControl, { useMapStyle } from '../shared/mapbox/MapStyleControl';
|
||||
import { FlashMessage } from '../shared/FlashMessage';
|
||||
|
||||
import ComboAdresseSearch from '../ComboAdresseSearch';
|
||||
import {
|
||||
polygonCadastresFill,
|
||||
polygonCadastresLine,
|
||||
readGeoFile
|
||||
} from './utils';
|
||||
import {
|
||||
noop,
|
||||
filterFeatureCollection,
|
||||
fitBounds,
|
||||
generateId,
|
||||
useEvent,
|
||||
findFeature
|
||||
} from '../shared/mapbox/utils';
|
||||
import { useMapboxEditor } from './useMapboxEditor';
|
||||
|
||||
function MapEditor({ featureCollection, url, preview, options }) {
|
||||
const drawControl = useRef(null);
|
||||
const [currentMap, setCurrentMap] = useState(null);
|
||||
const Mapbox = ReactMapboxGl({});
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState();
|
||||
const [style, setStyle] = useState('ortho');
|
||||
function MapEditor({ featureCollection, url, options, preview }) {
|
||||
const [cadastreEnabled, setCadastreEnabled] = useState(false);
|
||||
const [coords, setCoords] = useState([1.7, 46.9]);
|
||||
const [zoom, setZoom] = useState([5]);
|
||||
const [bbox, setBbox] = useState(featureCollection.bbox);
|
||||
const [importInputs, setImportInputs] = useState([]);
|
||||
const [cadastresFeatureCollection, setCadastresFeatureCollection] = useState(
|
||||
filterFeatureCollection(featureCollection, 'cadastre')
|
||||
);
|
||||
const mapStyle = useMemo(() => getMapStyle(style, options.layers), [
|
||||
style,
|
||||
options
|
||||
]);
|
||||
const hasCadastres = useMemo(() => options.layers.includes('cadastres'));
|
||||
const {
|
||||
isSupported,
|
||||
error,
|
||||
inputs,
|
||||
onLoad,
|
||||
onStyleChange,
|
||||
onFileChange,
|
||||
drawRef,
|
||||
createFeatures,
|
||||
updateFeatures,
|
||||
deleteFeatures,
|
||||
addInputFile,
|
||||
removeInputFile
|
||||
} = useMapboxEditor(featureCollection, {
|
||||
url,
|
||||
enabled: !preview,
|
||||
cadastreEnabled
|
||||
});
|
||||
const [style, setStyle] = useMapStyle(options.layers, {
|
||||
onStyleChange,
|
||||
cadastreEnabled
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setErrorMessage(null), 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [errorMessage]);
|
||||
|
||||
const translations = [
|
||||
['.mapbox-gl-draw_line', 'Tracer une ligne'],
|
||||
['.mapbox-gl-draw_polygon', 'Dessiner un polygone'],
|
||||
['.mapbox-gl-draw_point', 'Ajouter un point'],
|
||||
['.mapbox-gl-draw_trash', 'Supprimer']
|
||||
];
|
||||
for (const [selector, translation] of translations) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.setAttribute('title', translation);
|
||||
}
|
||||
}
|
||||
|
||||
const onFeatureFocus = useCallback(
|
||||
({ detail }) => {
|
||||
const { id } = detail;
|
||||
const featureCollection = drawControl.current.draw.getAll();
|
||||
const feature = findFeature(featureCollection, id);
|
||||
if (feature) {
|
||||
fitBounds(currentMap, feature);
|
||||
}
|
||||
},
|
||||
[currentMap, drawControl.current]
|
||||
);
|
||||
|
||||
const onFeatureUpdate = useCallback(
|
||||
async ({ detail }) => {
|
||||
const { id, properties } = detail;
|
||||
const featureCollection = drawControl.current.draw.getAll();
|
||||
const feature = findFeature(featureCollection, id);
|
||||
|
||||
if (feature) {
|
||||
getJSON(`${url}/${id}`, { feature: { properties } }, 'patch');
|
||||
}
|
||||
},
|
||||
[url, drawControl.current]
|
||||
);
|
||||
|
||||
const updateFeaturesList = useCallback(
|
||||
async (features) => {
|
||||
const cadastres = features.find(
|
||||
({ geometry }) => geometry.type === 'Polygon'
|
||||
);
|
||||
await ajax({
|
||||
url,
|
||||
type: 'get',
|
||||
data: cadastres ? 'cadastres=update' : ''
|
||||
});
|
||||
fire(document, 'ds:page:update');
|
||||
},
|
||||
[url]
|
||||
);
|
||||
|
||||
const onCadastresUpdate = useCallback(({ detail }) => {
|
||||
setCadastresFeatureCollection(
|
||||
filterFeatureCollection(detail.featureCollection, 'cadastre')
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEvent('map:feature:focus', onFeatureFocus);
|
||||
useEvent('map:feature:update', onFeatureUpdate);
|
||||
useEvent('cadastres:update', onCadastresUpdate);
|
||||
|
||||
function setFeatureId(lid, feature) {
|
||||
const draw = drawControl.current.draw;
|
||||
draw.setFeatureProperty(lid, 'id', feature.properties.id);
|
||||
}
|
||||
|
||||
function updateImportInputs(inputs, inputId) {
|
||||
const updatedInputs = inputs.filter((input) => input.id !== inputId);
|
||||
setImportInputs(updatedInputs);
|
||||
}
|
||||
|
||||
async function onDrawCreate({ features }) {
|
||||
try {
|
||||
for (const feature of features) {
|
||||
const data = await getJSON(url, { feature }, 'post');
|
||||
setFeatureId(feature.id, data.feature);
|
||||
}
|
||||
|
||||
updateFeaturesList(features);
|
||||
} catch {
|
||||
setErrorMessage('Le polygone dessiné n’est pas valide.');
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrawUpdate({ features }) {
|
||||
try {
|
||||
for (const feature of features) {
|
||||
const { id } = feature.properties;
|
||||
if (id) {
|
||||
await getJSON(`${url}/${id}`, { feature }, 'patch');
|
||||
} else {
|
||||
const data = await getJSON(url, { feature }, 'post');
|
||||
setFeatureId(feature.id, data.feature);
|
||||
}
|
||||
}
|
||||
|
||||
updateFeaturesList(features);
|
||||
} catch {
|
||||
setErrorMessage('Le polygone dessiné n’est pas valide.');
|
||||
}
|
||||
}
|
||||
|
||||
async function onDrawDelete({ features }) {
|
||||
for (const feature of features) {
|
||||
const { id } = feature.properties;
|
||||
await getJSON(`${url}/${id}`, null, 'delete');
|
||||
}
|
||||
|
||||
updateFeaturesList(features);
|
||||
}
|
||||
|
||||
function onMapLoad(map) {
|
||||
setCurrentMap(map);
|
||||
|
||||
drawControl.current.draw.set(
|
||||
filterFeatureCollection(featureCollection, 'selection_utilisateur')
|
||||
);
|
||||
}
|
||||
|
||||
const onFileImport = async (e, inputId) => {
|
||||
try {
|
||||
const featureCollection = await readGeoFile(e.target.files[0]);
|
||||
const resultFeatureCollection = await getJSON(
|
||||
`${url}/import`,
|
||||
featureCollection,
|
||||
'post'
|
||||
);
|
||||
let inputs = [...importInputs];
|
||||
const setInputs = inputs.map((input) => {
|
||||
if (input.id === inputId) {
|
||||
input.disabled = true;
|
||||
input.hasValue = true;
|
||||
resultFeatureCollection.features.forEach((resultFeature) => {
|
||||
featureCollection.features.forEach((feature) => {
|
||||
if (
|
||||
JSON.stringify(resultFeature.geometry) ===
|
||||
JSON.stringify(feature.geometry)
|
||||
) {
|
||||
input.featureIds.push(resultFeature.properties.id);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
return input;
|
||||
});
|
||||
|
||||
drawControl.current.draw.set(
|
||||
filterFeatureCollection(
|
||||
resultFeatureCollection,
|
||||
'selection_utilisateur'
|
||||
)
|
||||
);
|
||||
|
||||
updateFeaturesList(resultFeatureCollection.features);
|
||||
setImportInputs(setInputs);
|
||||
setBbox(resultFeatureCollection.bbox);
|
||||
} catch {
|
||||
setErrorMessage('Le fichier importé contient des polygones invalides.');
|
||||
}
|
||||
};
|
||||
|
||||
const addInputFile = (e) => {
|
||||
e.preventDefault();
|
||||
let inputs = [...importInputs];
|
||||
inputs.push({
|
||||
id: generateId(),
|
||||
disabled: false,
|
||||
featureIds: [],
|
||||
hasValue: false
|
||||
});
|
||||
setImportInputs(inputs);
|
||||
};
|
||||
|
||||
const removeInputFile = async (e, inputId) => {
|
||||
e.preventDefault();
|
||||
const draw = drawControl.current.draw;
|
||||
const featureCollection = draw.getAll();
|
||||
let inputs = [...importInputs];
|
||||
const inputToRemove = inputs.find((input) => input.id === inputId);
|
||||
|
||||
for (const feature of featureCollection.features) {
|
||||
if (inputToRemove.featureIds.includes(feature.properties.id)) {
|
||||
const featureToRemove = draw.get(feature.id);
|
||||
await getJSON(`${url}/${feature.properties.id}`, null, 'delete');
|
||||
draw.delete(feature.id).getAll();
|
||||
updateFeaturesList([featureToRemove]);
|
||||
}
|
||||
}
|
||||
updateImportInputs(inputs, inputId);
|
||||
};
|
||||
|
||||
if (!mapboxgl.supported()) {
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<p>
|
||||
Nous ne pouvons pas afficher notre éditeur de carte car il est
|
||||
|
@ -263,9 +53,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
{errorMessage && (
|
||||
<FlashMessage message={errorMessage} level="alert" fixed={true} />
|
||||
)}
|
||||
{error && <FlashMessage message={error} level="alert" fixed={true} />}
|
||||
<div>
|
||||
<p style={{ marginBottom: '20px' }}>
|
||||
Besoin d'aide ?
|
||||
|
@ -278,12 +66,12 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="file-import" style={{ marginBottom: '20px' }}>
|
||||
<div className="file-import" style={{ marginBottom: '10px' }}>
|
||||
<button className="button send primary" onClick={addInputFile}>
|
||||
Ajouter un fichier GPX ou KML
|
||||
</button>
|
||||
<div>
|
||||
{importInputs.map((input) => (
|
||||
{inputs.map((input) => (
|
||||
<div key={input.id}>
|
||||
<input
|
||||
title="Choisir un fichier gpx ou kml"
|
||||
|
@ -292,7 +80,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
type="file"
|
||||
accept=".gpx, .kml"
|
||||
disabled={input.disabled}
|
||||
onChange={(e) => onFileImport(e, input.id)}
|
||||
onChange={(e) => onFileChange(e, input.id)}
|
||||
/>
|
||||
{input.hasValue && (
|
||||
<span
|
||||
|
@ -310,10 +98,11 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '50px'
|
||||
marginBottom: '10px'
|
||||
}}
|
||||
>
|
||||
<ComboAdresseSearch
|
||||
className="no-margin"
|
||||
placeholder="Rechercher une adresse : saisissez au moins 2 caractères"
|
||||
allowInputValues={false}
|
||||
onChange={(_, { geometry: { coordinates } }) => {
|
||||
|
@ -323,38 +112,43 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
/>
|
||||
</div>
|
||||
<Mapbox
|
||||
onStyleLoad={(map) => onMapLoad(map)}
|
||||
fitBounds={bbox}
|
||||
fitBoundsOptions={{ padding: 100 }}
|
||||
onStyleLoad={(map) => onLoad(map)}
|
||||
center={coords}
|
||||
zoom={zoom}
|
||||
style={mapStyle}
|
||||
containerStyle={{
|
||||
height: '500px'
|
||||
}}
|
||||
style={style}
|
||||
containerStyle={{ height: '500px' }}
|
||||
>
|
||||
{hasCadastres ? (
|
||||
<GeoJSONLayer
|
||||
data={cadastresFeatureCollection}
|
||||
fillPaint={polygonCadastresFill}
|
||||
linePaint={polygonCadastresLine}
|
||||
{!cadastreEnabled && (
|
||||
<DrawControl
|
||||
ref={drawRef}
|
||||
onDrawCreate={createFeatures}
|
||||
onDrawUpdate={updateFeatures}
|
||||
onDrawDelete={deleteFeatures}
|
||||
displayControlsDefault={false}
|
||||
controls={{
|
||||
point: true,
|
||||
line_string: true,
|
||||
polygon: true,
|
||||
trash: true
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
<DrawControl
|
||||
ref={drawControl}
|
||||
onDrawCreate={preview ? noop : onDrawCreate}
|
||||
onDrawUpdate={preview ? noop : onDrawUpdate}
|
||||
onDrawDelete={preview ? noop : onDrawDelete}
|
||||
displayControlsDefault={false}
|
||||
controls={{
|
||||
point: true,
|
||||
line_string: true,
|
||||
polygon: true,
|
||||
trash: true
|
||||
}}
|
||||
/>
|
||||
<SwitchMapStyle style={style} setStyle={setStyle} ign={options.ign} />
|
||||
)}
|
||||
<MapStyleControl style={style.id} setStyle={setStyle} />
|
||||
<ZoomControl />
|
||||
{options.layers.includes('cadastres') && (
|
||||
<div className="cadastres-selection-control mapboxgl-ctrl-group">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setCadastreEnabled((cadastreEnabled) => !cadastreEnabled)
|
||||
}
|
||||
title="Sélectionner les parcelles cadastrales"
|
||||
className={cadastreEnabled ? 'on' : ''}
|
||||
>
|
||||
<MapIcon className="icon-size" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Mapbox>
|
||||
</>
|
||||
);
|
||||
|
@ -363,15 +157,11 @@ function MapEditor({ featureCollection, url, preview, options }) {
|
|||
MapEditor.propTypes = {
|
||||
featureCollection: PropTypes.shape({
|
||||
bbox: PropTypes.array,
|
||||
features: PropTypes.array,
|
||||
id: PropTypes.number
|
||||
features: PropTypes.array
|
||||
}),
|
||||
url: PropTypes.string,
|
||||
preview: PropTypes.bool,
|
||||
options: PropTypes.shape({
|
||||
layers: PropTypes.array,
|
||||
ign: PropTypes.bool
|
||||
})
|
||||
options: PropTypes.shape({ layers: PropTypes.array })
|
||||
};
|
||||
|
||||
export default MapEditor;
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js';
|
||||
import { generateId } from '../shared/mapbox/utils';
|
||||
|
||||
export const polygonCadastresFill = {
|
||||
'fill-color': '#EC3323',
|
||||
'fill-opacity': 0.3
|
||||
};
|
||||
export function readGeoFile(file) {
|
||||
const isGpxFile = file.name.includes('.gpx');
|
||||
const reader = new FileReader();
|
||||
|
||||
export const polygonCadastresLine = {
|
||||
'line-color': 'rgba(255, 0, 0, 1)',
|
||||
'line-width': 4,
|
||||
'line-dasharray': [1, 1]
|
||||
};
|
||||
return new Promise((resolve) => {
|
||||
reader.onload = (event) => {
|
||||
const xml = new DOMParser().parseFromString(
|
||||
event.target.result,
|
||||
'text/xml'
|
||||
);
|
||||
const featureCollection = normalizeFeatureCollection(
|
||||
isGpxFile ? gpx(xml) : kml(xml),
|
||||
file.name
|
||||
);
|
||||
|
||||
export function normalizeFeatureCollection(featureCollection) {
|
||||
resolve(featureCollection);
|
||||
};
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeFeatureCollection(featureCollection, filename) {
|
||||
const features = [];
|
||||
for (const feature of featureCollection.features) {
|
||||
switch (feature.geometry.type) {
|
||||
|
@ -65,26 +76,13 @@ export function normalizeFeatureCollection(featureCollection) {
|
|||
}
|
||||
}
|
||||
|
||||
featureCollection.features = features;
|
||||
featureCollection.filename = `${generateId()}-${filename}`;
|
||||
featureCollection.features = features.map((feature) => ({
|
||||
...feature,
|
||||
properties: {
|
||||
...feature.properties,
|
||||
filename: featureCollection.filename
|
||||
}
|
||||
}));
|
||||
return featureCollection;
|
||||
}
|
||||
|
||||
export function readGeoFile(file) {
|
||||
const isGpxFile = file.name.includes('.gpx');
|
||||
const reader = new FileReader();
|
||||
|
||||
return new Promise((resolve) => {
|
||||
reader.onload = (event) => {
|
||||
const xml = new DOMParser().parseFromString(
|
||||
event.target.result,
|
||||
'text/xml'
|
||||
);
|
||||
const featureCollection = normalizeFeatureCollection(
|
||||
isGpxFile ? gpx(xml) : kml(xml)
|
||||
);
|
||||
|
||||
resolve(featureCollection);
|
||||
};
|
||||
reader.readAsText(file, 'UTF-8');
|
||||
});
|
||||
}
|
553
app/javascript/components/MapEditor/useMapboxEditor.js
Normal file
553
app/javascript/components/MapEditor/useMapboxEditor.js
Normal file
|
@ -0,0 +1,553 @@
|
|||
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import { getJSON, ajax, fire } from '@utils';
|
||||
|
||||
import { readGeoFile } from './readGeoFile';
|
||||
import {
|
||||
filterFeatureCollection,
|
||||
generateId,
|
||||
findFeature,
|
||||
getBounds,
|
||||
defer
|
||||
} from '../shared/mapbox/utils';
|
||||
|
||||
const SOURCE_SELECTION_UTILISATEUR = 'selection_utilisateur';
|
||||
const SOURCE_CADASTRE = 'cadastre';
|
||||
|
||||
export function useMapboxEditor(
|
||||
featureCollection,
|
||||
{ url, enabled = true, cadastreEnabled = true }
|
||||
) {
|
||||
const [isLoaded, setLoaded] = useState(false);
|
||||
const mapRef = useRef();
|
||||
const drawRef = useRef();
|
||||
const loadedRef = useRef(defer());
|
||||
const selectedCadastresRef = useRef(() => new Set());
|
||||
const isSupported = useMemo(() => mapboxgl.supported());
|
||||
|
||||
useEffect(() => {
|
||||
const translations = [
|
||||
['.mapbox-gl-draw_line', 'Tracer une ligne'],
|
||||
['.mapbox-gl-draw_polygon', 'Dessiner un polygone'],
|
||||
['.mapbox-gl-draw_point', 'Ajouter un point'],
|
||||
['.mapbox-gl-draw_trash', 'Supprimer']
|
||||
];
|
||||
for (const [selector, translation] of translations) {
|
||||
const element = document.querySelector(selector);
|
||||
if (element) {
|
||||
element.setAttribute('title', translation);
|
||||
}
|
||||
}
|
||||
}, [isLoaded]);
|
||||
|
||||
const addEventListener = useCallback((eventName, target, callback) => {
|
||||
loadedRef.current.promise.then(() => {
|
||||
mapRef.current.on(eventName, target, callback);
|
||||
});
|
||||
return () => {
|
||||
if (mapRef.current) {
|
||||
mapRef.current.off(eventName, target, callback);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const highlightFeature = useCallback((cid, highlight) => {
|
||||
if (highlight) {
|
||||
selectedCadastresRef.current.add(cid);
|
||||
} else {
|
||||
selectedCadastresRef.current.delete(cid);
|
||||
}
|
||||
if (selectedCadastresRef.current.size == 0) {
|
||||
mapRef.current.setFilter('parcelle-highlighted', ['in', 'id', '']);
|
||||
} else {
|
||||
mapRef.current.setFilter('parcelle-highlighted', [
|
||||
'in',
|
||||
'id',
|
||||
...selectedCadastresRef.current
|
||||
]);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const fitBounds = useCallback((bbox) => {
|
||||
mapRef.current.fitBounds(bbox, { padding: 100 });
|
||||
}, []);
|
||||
|
||||
const hoverFeature = useCallback((feature, hover) => {
|
||||
if (!selectedCadastresRef.current.has(feature.properties.id)) {
|
||||
mapRef.current.setFeatureState(
|
||||
{
|
||||
source: 'cadastre',
|
||||
sourceLayer: 'parcelles',
|
||||
id: feature.id
|
||||
},
|
||||
{ hover }
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const addFeatures = useCallback((features, external) => {
|
||||
for (const feature of features) {
|
||||
if (feature.lid) {
|
||||
drawRef.current?.draw.setFeatureProperty(
|
||||
feature.lid,
|
||||
'id',
|
||||
feature.properties.id
|
||||
);
|
||||
delete feature.lid;
|
||||
}
|
||||
if (external) {
|
||||
if (feature.properties.source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
drawRef.current?.draw.add({ id: feature.properties.id, ...feature });
|
||||
} else {
|
||||
highlightFeature(feature.properties.cid, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const removeFeatures = useCallback((features, external) => {
|
||||
if (external) {
|
||||
for (const feature of features) {
|
||||
if (feature.properties.source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
drawRef.current?.draw.delete(feature.id);
|
||||
} else {
|
||||
highlightFeature(feature.properties.cid, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onLoad = useCallback(
|
||||
(map) => {
|
||||
if (!mapRef.current) {
|
||||
mapRef.current = map;
|
||||
mapRef.current.fitBounds(props.featureCollection.bbox, {
|
||||
padding: 100
|
||||
});
|
||||
onStyleChange();
|
||||
setLoaded(true);
|
||||
loadedRef.current.resolve();
|
||||
}
|
||||
},
|
||||
[featureCollection]
|
||||
);
|
||||
|
||||
const addEventListeners = useCallback((events) => {
|
||||
const unsubscribe = Object.entries(
|
||||
events
|
||||
).map(([eventName, [target, callback]]) =>
|
||||
addEventListener(eventName, target, callback)
|
||||
);
|
||||
return () => unsubscribe.map((unsubscribe) => unsubscribe());
|
||||
}, []);
|
||||
|
||||
const {
|
||||
createFeatures,
|
||||
updateFeatures,
|
||||
deleteFeatures,
|
||||
...props
|
||||
} = useFeatureCollection(featureCollection, {
|
||||
url,
|
||||
enabled: isSupported && enabled,
|
||||
addFeatures,
|
||||
removeFeatures
|
||||
});
|
||||
|
||||
const onStyleChange = useCallback(() => {
|
||||
if (mapRef.current) {
|
||||
const featureCollection = props.featureCollection;
|
||||
if (!cadastreEnabled) {
|
||||
drawRef.current?.draw.set(
|
||||
filterFeatureCollection(
|
||||
featureCollection,
|
||||
SOURCE_SELECTION_UTILISATEUR
|
||||
)
|
||||
);
|
||||
}
|
||||
selectedCadastresRef.current = new Set(
|
||||
filterFeatureCollection(
|
||||
featureCollection,
|
||||
SOURCE_CADASTRE
|
||||
).features.map(({ properties }) => properties.cid)
|
||||
);
|
||||
if (selectedCadastresRef.current.size > 0) {
|
||||
mapRef.current.setFilter('parcelle-highlighted', [
|
||||
'in',
|
||||
'id',
|
||||
...selectedCadastresRef.current
|
||||
]);
|
||||
}
|
||||
}
|
||||
}, [props.featureCollection, cadastreEnabled]);
|
||||
|
||||
useExternalEvents(props.featureCollection, {
|
||||
fitBounds,
|
||||
createFeatures,
|
||||
updateFeatures,
|
||||
deleteFeatures
|
||||
});
|
||||
useCadastres(props.featureCollection, {
|
||||
addEventListeners,
|
||||
hoverFeature,
|
||||
createFeatures,
|
||||
deleteFeatures,
|
||||
enabled: cadastreEnabled
|
||||
});
|
||||
|
||||
return {
|
||||
isSupported,
|
||||
onLoad,
|
||||
onStyleChange,
|
||||
drawRef,
|
||||
createFeatures,
|
||||
updateFeatures,
|
||||
deleteFeatures,
|
||||
...props,
|
||||
...useImportFiles(props.featureCollection, {
|
||||
createFeatures,
|
||||
deleteFeatures
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
function useFeatureCollection(
|
||||
initialFeatureCollection,
|
||||
{ url, addFeatures, removeFeatures, enabled = true }
|
||||
) {
|
||||
const [error, onError] = useError();
|
||||
const [featureCollection, setFeatureCollection] = useState(
|
||||
initialFeatureCollection
|
||||
);
|
||||
const updateFeatureCollection = useCallback(
|
||||
(callback) => {
|
||||
setFeatureCollection(({ features }) => ({
|
||||
type: 'FeatureCollection',
|
||||
features: callback(features)
|
||||
}));
|
||||
ajax({ url, type: 'GET' })
|
||||
.then(() => fire(document, 'ds:page:update'))
|
||||
.catch(() => {});
|
||||
},
|
||||
[setFeatureCollection]
|
||||
);
|
||||
|
||||
const createFeatures = useCallback(
|
||||
async ({ features, source = SOURCE_SELECTION_UTILISATEUR, external }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newFeatures = [];
|
||||
for (const feature of features) {
|
||||
const data = await getJSON(url, { feature, source }, 'post');
|
||||
if (data) {
|
||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
data.feature.lid = feature.id;
|
||||
}
|
||||
newFeatures.push(data.feature);
|
||||
}
|
||||
}
|
||||
addFeatures(newFeatures, external);
|
||||
updateFeatureCollection(
|
||||
(features) => [...features, ...newFeatures],
|
||||
external
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
onError('Le polygone dessiné n’est pas valide.');
|
||||
}
|
||||
},
|
||||
[enabled, url, updateFeatureCollection, addFeatures]
|
||||
);
|
||||
|
||||
const updateFeatures = useCallback(
|
||||
async ({ features, source = SOURCE_SELECTION_UTILISATEUR, external }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const newFeatures = [];
|
||||
for (const feature of features) {
|
||||
const { id } = feature.properties;
|
||||
if (id) {
|
||||
await getJSON(`${url}/${id}`, { feature }, 'patch');
|
||||
} else {
|
||||
const data = await getJSON(url, { feature, source }, 'post');
|
||||
if (data) {
|
||||
if (source == SOURCE_SELECTION_UTILISATEUR) {
|
||||
data.feature.lid = feature.id;
|
||||
}
|
||||
newFeatures.push(data.feature);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newFeatures.length > 0) {
|
||||
addFeatures(newFeatures, external);
|
||||
updateFeatureCollection((features) => [...features, ...newFeatures]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
onError('Le polygone dessiné n’est pas valide.');
|
||||
}
|
||||
},
|
||||
[enabled, url, updateFeatureCollection, addFeatures]
|
||||
);
|
||||
|
||||
const deleteFeatures = useCallback(
|
||||
async ({ features, external }) => {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const deletedFeatures = [];
|
||||
for (const feature of features) {
|
||||
const { id } = feature.properties;
|
||||
await getJSON(`${url}/${id}`, null, 'delete');
|
||||
deletedFeatures.push(feature);
|
||||
}
|
||||
removeFeatures(deletedFeatures, external);
|
||||
const deletedFeatureIds = deletedFeatures.map(
|
||||
({ properties }) => properties.id
|
||||
);
|
||||
updateFeatureCollection(
|
||||
(features) =>
|
||||
features.filter(
|
||||
({ properties }) => !deletedFeatureIds.includes(properties.id)
|
||||
),
|
||||
external
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
onError('Le polygone n’a pas pu être supprimé.');
|
||||
}
|
||||
},
|
||||
[enabled, url, updateFeatureCollection, removeFeatures]
|
||||
);
|
||||
|
||||
return {
|
||||
featureCollection,
|
||||
createFeatures,
|
||||
updateFeatures,
|
||||
deleteFeatures,
|
||||
error
|
||||
};
|
||||
}
|
||||
|
||||
function useImportFiles(featureCollection, { createFeatures, deleteFeatures }) {
|
||||
const [inputs, setInputs] = useState([]);
|
||||
const addInput = useCallback(
|
||||
(input) => {
|
||||
setInputs((inputs) => [...inputs, input]);
|
||||
},
|
||||
[setInputs]
|
||||
);
|
||||
const removeInput = useCallback(
|
||||
(inputId) => {
|
||||
setInputs((inputs) => inputs.filter((input) => input.id !== inputId));
|
||||
},
|
||||
[setInputs]
|
||||
);
|
||||
|
||||
const onFileChange = useCallback(
|
||||
async (event, inputId) => {
|
||||
const { features, filename } = await readGeoFile(event.target.files[0]);
|
||||
createFeatures({ features, external: true });
|
||||
setInputs((inputs) => {
|
||||
return inputs.map((input) => {
|
||||
if (input.id === inputId) {
|
||||
return { ...input, disabled: true, hasValue: true, filename };
|
||||
}
|
||||
return input;
|
||||
});
|
||||
});
|
||||
},
|
||||
[setInputs, createFeatures, featureCollection]
|
||||
);
|
||||
|
||||
const addInputFile = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
addInput({
|
||||
id: generateId(),
|
||||
disabled: false,
|
||||
hasValue: false,
|
||||
filename: ''
|
||||
});
|
||||
},
|
||||
[addInput]
|
||||
);
|
||||
|
||||
const removeInputFile = useCallback(
|
||||
(event, inputId) => {
|
||||
event.preventDefault();
|
||||
const { filename } = inputs.find((input) => input.id === inputId);
|
||||
const features = featureCollection.features.filter(
|
||||
(feature) => feature.properties.filename == filename
|
||||
);
|
||||
deleteFeatures({ features, external: true });
|
||||
removeInput(inputId);
|
||||
},
|
||||
[removeInput, deleteFeatures, featureCollection]
|
||||
);
|
||||
|
||||
return {
|
||||
inputs,
|
||||
onFileChange,
|
||||
addInputFile,
|
||||
removeInputFile
|
||||
};
|
||||
}
|
||||
|
||||
function useExternalEvents(
|
||||
featureCollection,
|
||||
{ fitBounds, createFeatures, updateFeatures, deleteFeatures }
|
||||
) {
|
||||
const onFeatureFocus = useCallback(
|
||||
({ detail }) => {
|
||||
const { id, bbox } = detail;
|
||||
if (id) {
|
||||
const feature = findFeature(featureCollection, id);
|
||||
if (feature) {
|
||||
fitBounds(getBounds(feature.geometry));
|
||||
}
|
||||
} else if (bbox) {
|
||||
fitBounds(bbox);
|
||||
}
|
||||
},
|
||||
[featureCollection, fitBounds]
|
||||
);
|
||||
|
||||
const onFeatureCreate = useCallback(
|
||||
({ detail }) => {
|
||||
const { geometry, properties } = detail;
|
||||
|
||||
if (geometry) {
|
||||
createFeatures({
|
||||
features: [{ geometry, properties }],
|
||||
external: true
|
||||
});
|
||||
}
|
||||
},
|
||||
[createFeatures]
|
||||
);
|
||||
|
||||
const onFeatureUpdate = useCallback(
|
||||
({ detail }) => {
|
||||
const { id, properties } = detail;
|
||||
const feature = findFeature(featureCollection, id);
|
||||
|
||||
if (feature) {
|
||||
feature.properties = { ...feature.properties, ...properties };
|
||||
updateFeatures({ features: [feature], external: true });
|
||||
}
|
||||
},
|
||||
[featureCollection, updateFeatures]
|
||||
);
|
||||
|
||||
const onFeatureDelete = useCallback(
|
||||
({ detail }) => {
|
||||
const { id } = detail;
|
||||
const feature = findFeature(featureCollection, id);
|
||||
|
||||
if (feature) {
|
||||
deleteFeatures({ features: [feature], external: true });
|
||||
}
|
||||
},
|
||||
[featureCollection, deleteFeatures]
|
||||
);
|
||||
|
||||
useEvent('map:feature:focus', onFeatureFocus);
|
||||
useEvent('map:feature:create', onFeatureCreate);
|
||||
useEvent('map:feature:update', onFeatureUpdate);
|
||||
useEvent('map:feature:delete', onFeatureDelete);
|
||||
}
|
||||
|
||||
function useCadastres(
|
||||
featureCollection,
|
||||
{
|
||||
addEventListeners,
|
||||
hoverFeature,
|
||||
createFeatures,
|
||||
deleteFeatures,
|
||||
enabled = true
|
||||
}
|
||||
) {
|
||||
const hoveredFeature = useRef();
|
||||
|
||||
const onMouseMove = useCallback(
|
||||
(event) => {
|
||||
if (event.features.length > 0) {
|
||||
const feature = event.features[0];
|
||||
if (hoveredFeature.current?.id != feature.id) {
|
||||
if (hoveredFeature.current) {
|
||||
hoverFeature(hoveredFeature.current, false);
|
||||
}
|
||||
hoveredFeature.current = feature;
|
||||
hoverFeature(feature, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
[hoverFeature]
|
||||
);
|
||||
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if (hoveredFeature.current) {
|
||||
hoverFeature(hoveredFeature.current, false);
|
||||
}
|
||||
hoveredFeature.current = null;
|
||||
}, [hoverFeature]);
|
||||
|
||||
const onClick = useCallback(
|
||||
async (event) => {
|
||||
if (event.features.length > 0) {
|
||||
const currentId = event.features[0].properties.id;
|
||||
const feature = findFeature(
|
||||
filterFeatureCollection(featureCollection, SOURCE_CADASTRE),
|
||||
currentId,
|
||||
'cid'
|
||||
);
|
||||
if (feature) {
|
||||
deleteFeatures({
|
||||
features: [feature],
|
||||
source: SOURCE_CADASTRE,
|
||||
external: true
|
||||
});
|
||||
} else {
|
||||
createFeatures({
|
||||
features: event.features,
|
||||
source: SOURCE_CADASTRE,
|
||||
external: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[featureCollection, createFeatures, deleteFeatures]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (enabled) {
|
||||
return addEventListeners({
|
||||
click: ['parcelles-fill', onClick],
|
||||
mousemove: ['parcelles-fill', onMouseMove],
|
||||
mouseleave: ['parcelles-fill', onMouseLeave]
|
||||
});
|
||||
}
|
||||
}, [onClick, onMouseMove, onMouseLeave, enabled]);
|
||||
}
|
||||
|
||||
function useError() {
|
||||
const [error, onError] = useState();
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => onError(null), 5000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [error]);
|
||||
|
||||
return [error, onError];
|
||||
}
|
||||
|
||||
export function useEvent(eventName, callback) {
|
||||
return useEffect(() => {
|
||||
addEventListener(eventName, callback);
|
||||
return () => removeEventListener(eventName, callback);
|
||||
}, [eventName, callback]);
|
||||
}
|
Loading…
Add table
Reference in a new issue