Add cadastres to MapEditor

This commit is contained in:
Paul Chavard 2021-05-06 18:51:53 +02:00
parent 19440afebf
commit 2244263b49
3 changed files with 653 additions and 312 deletions

View file

@ -1,257 +1,47 @@
import React, { import React, { useState } from 'react';
useState,
useCallback,
useRef,
useMemo,
useEffect
} from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import mapboxgl from 'mapbox-gl'; import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl';
import { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl';
import DrawControl from 'react-mapbox-gl-draw'; import DrawControl from 'react-mapbox-gl-draw';
import { MapIcon } from '@heroicons/react/outline';
import 'mapbox-gl/dist/mapbox-gl.css'; import 'mapbox-gl/dist/mapbox-gl.css';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import { getJSON, ajax, fire } from '@utils'; import MapStyleControl, { useMapStyle } from '../shared/mapbox/MapStyleControl';
import Mapbox from '../shared/mapbox/Mapbox';
import { getMapStyle } from '../shared/mapbox/styles';
import SwitchMapStyle from '../shared/mapbox/SwitchMapStyle';
import { FlashMessage } from '../shared/FlashMessage'; import { FlashMessage } from '../shared/FlashMessage';
import ComboAdresseSearch from '../ComboAdresseSearch'; import ComboAdresseSearch from '../ComboAdresseSearch';
import { import { useMapboxEditor } from './useMapboxEditor';
polygonCadastresFill,
polygonCadastresLine,
readGeoFile
} from './utils';
import {
noop,
filterFeatureCollection,
fitBounds,
generateId,
useEvent,
findFeature
} from '../shared/mapbox/utils';
function MapEditor({ featureCollection, url, preview, options }) { const Mapbox = ReactMapboxGl({});
const drawControl = useRef(null);
const [currentMap, setCurrentMap] = useState(null);
const [errorMessage, setErrorMessage] = useState(); function MapEditor({ featureCollection, url, options, preview }) {
const [style, setStyle] = useState('ortho'); const [cadastreEnabled, setCadastreEnabled] = useState(false);
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 [bbox, setBbox] = useState(featureCollection.bbox); const {
const [importInputs, setImportInputs] = useState([]); isSupported,
const [cadastresFeatureCollection, setCadastresFeatureCollection] = useState( error,
filterFeatureCollection(featureCollection, 'cadastre') inputs,
); onLoad,
const mapStyle = useMemo(() => getMapStyle(style, options.layers), [ onStyleChange,
style, onFileChange,
options drawRef,
]); createFeatures,
const hasCadastres = useMemo(() => options.layers.includes('cadastres')); updateFeatures,
deleteFeatures,
addInputFile,
removeInputFile
} = useMapboxEditor(featureCollection, {
url,
enabled: !preview,
cadastreEnabled
});
const [style, setStyle] = useMapStyle(options.layers, {
onStyleChange,
cadastreEnabled
});
useEffect(() => { if (!isSupported) {
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é nest 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é nest 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()) {
return ( return (
<p> <p>
Nous ne pouvons pas afficher notre éditeur de carte car il est Nous ne pouvons pas afficher notre éditeur de carte car il est
@ -263,9 +53,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
return ( return (
<> <>
{errorMessage && ( {error && <FlashMessage message={error} level="alert" fixed={true} />}
<FlashMessage message={errorMessage} level="alert" fixed={true} />
)}
<div> <div>
<p style={{ marginBottom: '20px' }}> <p style={{ marginBottom: '20px' }}>
Besoin d&apos;aide ?&nbsp; Besoin d&apos;aide ?&nbsp;
@ -278,12 +66,12 @@ function MapEditor({ featureCollection, url, preview, options }) {
</a> </a>
</p> </p>
</div> </div>
<div className="file-import" style={{ marginBottom: '20px' }}> <div className="file-import" style={{ marginBottom: '10px' }}>
<button className="button send primary" onClick={addInputFile}> <button className="button send primary" onClick={addInputFile}>
Ajouter un fichier GPX ou KML Ajouter un fichier GPX ou KML
</button> </button>
<div> <div>
{importInputs.map((input) => ( {inputs.map((input) => (
<div key={input.id}> <div key={input.id}>
<input <input
title="Choisir un fichier gpx ou kml" title="Choisir un fichier gpx ou kml"
@ -292,7 +80,7 @@ function MapEditor({ featureCollection, url, preview, options }) {
type="file" type="file"
accept=".gpx, .kml" accept=".gpx, .kml"
disabled={input.disabled} disabled={input.disabled}
onChange={(e) => onFileImport(e, input.id)} onChange={(e) => onFileChange(e, input.id)}
/> />
{input.hasValue && ( {input.hasValue && (
<span <span
@ -310,10 +98,11 @@ function MapEditor({ featureCollection, url, preview, options }) {
</div> </div>
<div <div
style={{ style={{
marginBottom: '50px' marginBottom: '10px'
}} }}
> >
<ComboAdresseSearch <ComboAdresseSearch
className="no-margin"
placeholder="Rechercher une adresse : saisissez au moins 2 caractères" placeholder="Rechercher une adresse : saisissez au moins 2 caractères"
allowInputValues={false} allowInputValues={false}
onChange={(_, { geometry: { coordinates } }) => { onChange={(_, { geometry: { coordinates } }) => {
@ -323,38 +112,43 @@ function MapEditor({ featureCollection, url, preview, options }) {
/> />
</div> </div>
<Mapbox <Mapbox
onStyleLoad={(map) => onMapLoad(map)} onStyleLoad={(map) => onLoad(map)}
fitBounds={bbox}
fitBoundsOptions={{ padding: 100 }}
center={coords} center={coords}
zoom={zoom} zoom={zoom}
style={mapStyle} style={style}
containerStyle={{ containerStyle={{ height: '500px' }}
height: '500px'
}}
> >
{hasCadastres ? ( {!cadastreEnabled && (
<GeoJSONLayer <DrawControl
data={cadastresFeatureCollection} ref={drawRef}
fillPaint={polygonCadastresFill} onDrawCreate={createFeatures}
linePaint={polygonCadastresLine} onDrawUpdate={updateFeatures}
onDrawDelete={deleteFeatures}
displayControlsDefault={false}
controls={{
point: true,
line_string: true,
polygon: true,
trash: true
}}
/> />
) : null} )}
<DrawControl <MapStyleControl style={style.id} setStyle={setStyle} />
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} />
<ZoomControl /> <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> </Mapbox>
</> </>
); );
@ -363,15 +157,11 @@ function MapEditor({ featureCollection, url, preview, options }) {
MapEditor.propTypes = { MapEditor.propTypes = {
featureCollection: PropTypes.shape({ featureCollection: PropTypes.shape({
bbox: PropTypes.array, bbox: PropTypes.array,
features: PropTypes.array, features: PropTypes.array
id: PropTypes.number
}), }),
url: PropTypes.string, url: PropTypes.string,
preview: PropTypes.bool, preview: PropTypes.bool,
options: PropTypes.shape({ options: PropTypes.shape({ layers: PropTypes.array })
layers: PropTypes.array,
ign: PropTypes.bool
})
}; };
export default MapEditor; export default MapEditor;

View file

@ -1,17 +1,28 @@
import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js'; import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js';
import { generateId } from '../shared/mapbox/utils';
export const polygonCadastresFill = { export function readGeoFile(file) {
'fill-color': '#EC3323', const isGpxFile = file.name.includes('.gpx');
'fill-opacity': 0.3 const reader = new FileReader();
};
export const polygonCadastresLine = { return new Promise((resolve) => {
'line-color': 'rgba(255, 0, 0, 1)', reader.onload = (event) => {
'line-width': 4, const xml = new DOMParser().parseFromString(
'line-dasharray': [1, 1] 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 = []; const features = [];
for (const feature of featureCollection.features) { for (const feature of featureCollection.features) {
switch (feature.geometry.type) { 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; 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');
});
}

View 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é nest 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é nest 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 na 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]);
}