();
+ useEffect(() => {
+ const timer = setTimeout(() => onError(undefined), 5000);
+ return () => clearTimeout(timer);
+ }, [error]);
+
+ return [error, onError];
+}
diff --git a/app/javascript/components/MapEditor/index.jsx b/app/javascript/components/MapEditor/index.jsx
deleted file mode 100644
index 20fa26dd4..000000000
--- a/app/javascript/components/MapEditor/index.jsx
+++ /dev/null
@@ -1,263 +0,0 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
-import ReactMapboxGl, { ZoomControl } from 'react-mapbox-gl';
-import DrawControl from 'react-mapbox-gl-draw';
-import {
- CursorClickIcon,
- PlusIcon,
- LocationMarkerIcon
-} from '@heroicons/react/outline';
-import CoordinateInput from 'react-coordinate-input';
-import { fire } from '@utils';
-import VisuallyHidden from '@reach/visually-hidden';
-import { useId } from '@reach/auto-id';
-import 'mapbox-gl/dist/mapbox-gl.css';
-import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
-
-import MapStyleControl, { useMapStyle } from '../shared/mapbox/MapStyleControl';
-import { FlashMessage } from '../shared/FlashMessage';
-
-import ComboAdresseSearch from '../ComboAdresseSearch';
-import { useMapboxEditor } from './useMapboxEditor';
-
-const Mapbox = ReactMapboxGl({});
-
-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 {
- isSupported,
- error,
- inputs,
- onLoad,
- onStyleChange,
- onFileChange,
- drawRef,
- createFeatures,
- updateFeatures,
- deleteFeatures,
- addInputFile,
- removeInputFile
- } = useMapboxEditor(featureCollection, {
- url,
- enabled: !preview,
- cadastreEnabled
- });
- const { style, layers, setStyle, setLayerEnabled, setLayerOpacity } =
- useMapStyle(options.layers, {
- onStyleChange,
- cadastreEnabled
- });
-
- if (!isSupported) {
- return (
-
- Nous ne pouvons pas afficher notre éditeur de carte car il est
- imcompatible avec votre navigateur. Nous vous conseillons de le mettre à
- jour ou utiliser les dernières versions de Chrome, Firefox ou Safari
-
- );
- }
-
- return (
- <>
- {error && }
-
-
-
- Ajouter un fichier GPX ou KML
-
-
- {inputs.map((input) => (
-
- onFileChange(e, input.id)}
- />
- {input.hasValue && (
- removeInputFile(e, input.id)}
- >
- )}
-
- ))}
-
-
-
- {
- setCoords(coordinates);
- setZoom([17]);
- }}
- />
-
- onLoad(map)}
- center={coords}
- zoom={zoom}
- style={style}
- containerStyle={{ height: '500px' }}
- >
- {!cadastreEnabled && (
-
- )}
-
-
- {options.layers.includes('cadastres') && (
-
-
- setCadastreEnabled((cadastreEnabled) => !cadastreEnabled)
- }
- title="Sélectionner les parcelles cadastrales"
- className={cadastreEnabled ? 'on' : ''}
- >
-
-
-
- )}
-
-
- >
- );
-}
-
-function PointInput() {
- const inputId = useId();
- const [value, setValue] = useState('');
- const [feature, setFeature] = useState(null);
- const getCurrentPosition = () => {
- navigator.geolocation &&
- navigator.geolocation.getCurrentPosition(({ coords }) => {
- setValue(
- `${coords.latitude.toPrecision(6)}, ${coords.longitude.toPrecision(
- 6
- )}`
- );
- });
- };
- const addPoint = () => {
- if (feature) {
- fire(document, 'map:feature:create', feature);
- setValue('');
- setFeature(null);
- }
- };
-
- return (
- <>
-
- Ajouter un point sur la carte
-
-
- {navigator.geolocation ? (
-
- Localiser votre position
-
-
- ) : null}
-
{
- setValue(value);
- setFeature(
- dd.length
- ? {
- type: 'Feature',
- geometry: {
- type: 'Point',
- coordinates: dd.reverse()
- },
- properties: {}
- }
- : null
- );
- }}
- />
-
-
- Ajouter le point avec les coordonnées saisies sur la carte
-
-
-
-
- >
- );
-}
-
-MapEditor.propTypes = {
- featureCollection: PropTypes.shape({
- bbox: PropTypes.array,
- features: PropTypes.array
- }),
- url: PropTypes.string,
- preview: PropTypes.bool,
- options: PropTypes.shape({ layers: PropTypes.array })
-};
-
-export default MapEditor;
diff --git a/app/javascript/components/MapEditor/index.tsx b/app/javascript/components/MapEditor/index.tsx
new file mode 100644
index 000000000..a84094835
--- /dev/null
+++ b/app/javascript/components/MapEditor/index.tsx
@@ -0,0 +1,91 @@
+import React, { useState } from 'react';
+import { CursorClickIcon } from '@heroicons/react/outline';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
+import type { FeatureCollection } from 'geojson';
+
+import { MapLibre } from '../shared/maplibre/MapLibre';
+import { useFeatureCollection } from './hooks';
+import { DrawLayer } from './components/DrawLayer';
+import { CadastreLayer } from './components/CadastreLayer';
+import { AddressInput } from './components/AddressInput';
+import { PointInput } from './components/PointInput';
+import { ImportFileInput } from './components/ImportFileInput';
+import { FlashMessage } from '../shared/FlashMessage';
+
+export default function MapEditor({
+ featureCollection: initialFeatureCollection,
+ url,
+ options,
+ preview
+}: {
+ featureCollection: FeatureCollection;
+ url: string;
+ preview: boolean;
+ options: { layers: string[] };
+}) {
+ const [cadastreEnabled, setCadastreEnabled] = useState(false);
+
+ const { featureCollection, error, ...actions } = useFeatureCollection(
+ initialFeatureCollection,
+ { url, enabled: !preview }
+ );
+
+ return (
+ <>
+
+ {error && }
+
+
+
+ >
+ }
+ footer={ }
+ >
+
+ {options.layers.includes('cadastres') ? (
+ <>
+
+
+
+ setCadastreEnabled((cadastreEnabled) => !cadastreEnabled)
+ }
+ title="Sélectionner les parcelles cadastrales"
+ className={cadastreEnabled ? 'on' : ''}
+ >
+
+
+
+ >
+ ) : null}
+
+ >
+ );
+}
diff --git a/app/javascript/components/MapEditor/readGeoFile.js b/app/javascript/components/MapEditor/readGeoFile.ts
similarity index 62%
rename from app/javascript/components/MapEditor/readGeoFile.js
rename to app/javascript/components/MapEditor/readGeoFile.ts
index 59f972f02..d9f66f10f 100644
--- a/app/javascript/components/MapEditor/readGeoFile.js
+++ b/app/javascript/components/MapEditor/readGeoFile.ts
@@ -1,29 +1,37 @@
import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js';
-import { generateId } from '../shared/mapbox/utils';
+import type { FeatureCollection, Feature } from 'geojson';
-export function readGeoFile(file) {
+import { generateId } from '../shared/maplibre/utils';
+
+export function readGeoFile(file: 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),
- file.name
- );
+ return new Promise>(
+ (resolve) => {
+ reader.onload = (event: FileReaderEventMap['load']) => {
+ const result = event.target?.result;
+ const xml = new DOMParser().parseFromString(
+ result as string,
+ 'text/xml'
+ );
+ const featureCollection = normalizeFeatureCollection(
+ isGpxFile ? gpx(xml) : kml(xml),
+ file.name
+ );
- resolve(featureCollection);
- };
- reader.readAsText(file, 'UTF-8');
- });
+ resolve(featureCollection);
+ };
+ reader.readAsText(file, 'UTF-8');
+ }
+ );
}
-function normalizeFeatureCollection(featureCollection, filename) {
- const features = [];
+function normalizeFeatureCollection(
+ featureCollection: FeatureCollection,
+ filename: string
+) {
+ const features: Feature[] = [];
for (const feature of featureCollection.features) {
switch (feature.geometry.type) {
case 'MultiPoint':
@@ -76,13 +84,13 @@ function normalizeFeatureCollection(featureCollection, filename) {
}
}
- featureCollection.filename = `${generateId()}-${filename}`;
+ const featureCollectionFilename = `${generateId()}-${filename}`;
featureCollection.features = features.map((feature) => ({
...feature,
properties: {
...feature.properties,
- filename: featureCollection.filename
+ filename: featureCollectionFilename
}
}));
- return featureCollection;
+ return { ...featureCollection, filename: featureCollectionFilename };
}
diff --git a/app/javascript/components/MapEditor/useMapboxEditor.js b/app/javascript/components/MapEditor/useMapboxEditor.js
deleted file mode 100644
index bc875b344..000000000
--- a/app/javascript/components/MapEditor/useMapboxEditor.js
+++ /dev/null
@@ -1,548 +0,0 @@
-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]);
-}
diff --git a/app/javascript/components/MapReader/components/CadastreLayer.tsx b/app/javascript/components/MapReader/components/CadastreLayer.tsx
new file mode 100644
index 000000000..243610eb1
--- /dev/null
+++ b/app/javascript/components/MapReader/components/CadastreLayer.tsx
@@ -0,0 +1,32 @@
+import { useRef } from 'react';
+import type { FeatureCollection } from 'geojson';
+
+import { useMapLibre } from '../../shared/maplibre/MapLibre';
+import { useMapEvent } from '../../shared/maplibre/hooks';
+import { filterFeatureCollection } from '../../shared/maplibre/utils';
+
+export function CadastreLayer({
+ featureCollection
+}: {
+ featureCollection: FeatureCollection;
+}) {
+ const map = useMapLibre();
+ const selectedCadastresRef = useRef>();
+
+ useMapEvent('styledata', () => {
+ selectedCadastresRef.current = new Set(
+ filterFeatureCollection(featureCollection, 'cadastre').features.map(
+ ({ properties }) => properties?.cid
+ )
+ );
+ if (selectedCadastresRef.current.size > 0) {
+ map.setFilter('parcelle-highlighted', [
+ 'in',
+ 'id',
+ ...selectedCadastresRef.current
+ ]);
+ }
+ });
+
+ return null;
+}
diff --git a/app/javascript/components/MapReader/components/GeoJSONLayer.tsx b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx
new file mode 100644
index 000000000..956a71fc4
--- /dev/null
+++ b/app/javascript/components/MapReader/components/GeoJSONLayer.tsx
@@ -0,0 +1,237 @@
+import React, { useCallback, useEffect, useMemo } from 'react';
+import { Popup, LngLatBoundsLike } from 'maplibre-gl';
+import type { Feature, FeatureCollection } from 'geojson';
+
+import { useMapLibre } from '../../shared/maplibre/MapLibre';
+import {
+ useFitBounds,
+ useEvent,
+ EventHandler,
+ useMapEvent
+} from '../../shared/maplibre/hooks';
+import {
+ filterFeatureCollection,
+ findFeature,
+ getBounds,
+ getCenter,
+ filterFeatureCollectionByGeometryType
+} from '../../shared/maplibre/utils';
+
+export function GeoJSONLayer({
+ featureCollection
+}: {
+ featureCollection: FeatureCollection;
+}) {
+ const map = useMapLibre();
+ const popup = useMemo(
+ () =>
+ new Popup({
+ closeButton: false,
+ closeOnClick: false
+ }),
+ []
+ );
+
+ const onMouseEnter = useCallback(
+ (event) => {
+ const feature = event.features && event.features[0];
+ if (feature?.properties && feature.properties.description) {
+ const coordinates = getCenter(feature.geometry, event.lngLat);
+ const description = feature.properties.description;
+ map.getCanvas().style.cursor = 'pointer';
+ popup.setLngLat(coordinates).setHTML(description).addTo(map);
+ } else {
+ popup.remove();
+ }
+ },
+ [popup]
+ );
+
+ const onMouseLeave = useCallback(() => {
+ map.getCanvas().style.cursor = '';
+ popup.remove();
+ }, [popup]);
+
+ useExternalEvents(featureCollection);
+
+ const polygons = filterFeatureCollectionByGeometryType(
+ filterFeatureCollection(featureCollection, 'selection_utilisateur'),
+ 'Polygon'
+ );
+ const lines = filterFeatureCollectionByGeometryType(
+ filterFeatureCollection(featureCollection, 'selection_utilisateur'),
+ 'LineString'
+ );
+ const points = filterFeatureCollectionByGeometryType(
+ filterFeatureCollection(featureCollection, 'selection_utilisateur'),
+ 'Point'
+ );
+
+ return (
+ <>
+ {polygons.features.map((feature) => (
+
+ ))}
+ {lines.features.map((feature) => (
+
+ ))}
+ {points.features.map((feature) => (
+
+ ))}
+ >
+ );
+}
+
+function useExternalEvents(featureCollection: FeatureCollection) {
+ const fitBounds = useFitBounds();
+ const onFeatureFocus = useCallback(({ detail }) => {
+ const { id } = detail;
+ const feature = findFeature(featureCollection, id);
+ if (feature) {
+ fitBounds(getBounds(feature.geometry));
+ }
+ }, []);
+
+ useEffect(() => {
+ fitBounds(featureCollection.bbox as LngLatBoundsLike);
+ }, []);
+
+ useEvent('map:feature:focus', onFeatureFocus);
+}
+
+function LineStringLayer({
+ feature,
+ onMouseEnter,
+ onMouseLeave
+}: {
+ feature: Feature;
+ onMouseEnter: EventHandler;
+ onMouseLeave: EventHandler;
+}) {
+ const map = useMapLibre();
+ const sourceId = String(feature.properties?.id);
+ const layerId = `${sourceId}-layer`;
+
+ useEffect(() => {
+ map
+ .addSource(sourceId, {
+ type: 'geojson',
+ data: feature
+ })
+ .addLayer({
+ id: layerId,
+ source: sourceId,
+ type: 'line',
+ paint: lineStringSelectionLine
+ });
+ }, []);
+
+ useMapEvent('mouseenter', onMouseEnter, layerId);
+ useMapEvent('mouseleave', onMouseLeave, layerId);
+
+ return null;
+}
+
+function PointLayer({
+ feature,
+ onMouseEnter,
+ onMouseLeave
+}: {
+ feature: Feature;
+ onMouseEnter: EventHandler;
+ onMouseLeave: EventHandler;
+}) {
+ const map = useMapLibre();
+ const sourceId = String(feature.properties?.id);
+ const layerId = `${sourceId}-layer`;
+
+ useEffect(() => {
+ map
+ .addSource(sourceId, {
+ type: 'geojson',
+ data: feature
+ })
+ .addLayer({
+ id: layerId,
+ source: sourceId,
+ type: 'circle',
+ paint: pointSelectionCircle
+ });
+ }, []);
+
+ useMapEvent('mouseenter', onMouseEnter, layerId);
+ useMapEvent('mouseleave', onMouseLeave, layerId);
+
+ return null;
+}
+
+function PolygonLayer({
+ feature,
+ onMouseEnter,
+ onMouseLeave
+}: {
+ feature: Feature;
+ onMouseEnter: EventHandler;
+ onMouseLeave: EventHandler;
+}) {
+ const map = useMapLibre();
+ const sourceId = String(feature.properties?.id);
+ const layerId = `${sourceId}-layer`;
+ const lineLayerId = `${sourceId}-line-layer`;
+
+ useEffect(() => {
+ map
+ .addSource(sourceId, {
+ type: 'geojson',
+ data: feature
+ })
+ .addLayer({
+ id: lineLayerId,
+ source: sourceId,
+ type: 'line',
+ paint: polygonSelectionLine
+ })
+ .addLayer({
+ id: layerId,
+ source: sourceId,
+ type: 'fill',
+ paint: polygonSelectionFill
+ });
+ }, []);
+
+ useMapEvent('mouseenter', onMouseEnter, layerId);
+ useMapEvent('mouseleave', onMouseLeave, layerId);
+
+ return null;
+}
+
+const polygonSelectionFill = {
+ 'fill-color': '#EC3323',
+ 'fill-opacity': 0.5
+};
+const polygonSelectionLine = {
+ 'line-color': 'rgba(255, 0, 0, 1)',
+ 'line-width': 4
+};
+const lineStringSelectionLine = {
+ 'line-color': 'rgba(55, 42, 127, 1.00)',
+ 'line-width': 3
+};
+const pointSelectionCircle = {
+ 'circle-color': '#EC3323'
+};
diff --git a/app/javascript/components/MapReader/index.jsx b/app/javascript/components/MapReader/index.jsx
deleted file mode 100644
index b20dde570..000000000
--- a/app/javascript/components/MapReader/index.jsx
+++ /dev/null
@@ -1,190 +0,0 @@
-import React, { useMemo } from 'react';
-import ReactMapboxGl, { ZoomControl, GeoJSONLayer } from 'react-mapbox-gl';
-import PropTypes from 'prop-types';
-import 'mapbox-gl/dist/mapbox-gl.css';
-
-import MapStyleControl, { useMapStyle } from '../shared/mapbox/MapStyleControl';
-import {
- filterFeatureCollection,
- filterFeatureCollectionByGeometryType
-} from '../shared/mapbox/utils';
-import { useMapbox } from './useMapbox';
-
-const Mapbox = ReactMapboxGl({});
-
-const MapReader = ({ featureCollection, options }) => {
- const { isSupported, onLoad, onStyleChange, onMouseEnter, onMouseLeave } =
- useMapbox(featureCollection);
- const { style, layers, setStyle, setLayerEnabled, setLayerOpacity } =
- useMapStyle(options.layers, { onStyleChange });
-
- if (!isSupported) {
- return (
-
- Nous ne pouvons pas afficher la carte car elle est imcompatible avec
- votre navigateur. Nous vous conseillons de le mettre à jour ou utiliser
- les dernières versions de Chrome, Firefox ou Safari
-
- );
- }
-
- return (
- onLoad(map)}
- style={style}
- containerStyle={{ height: '500px' }}
- >
-
-
-
-
-
-
-
- );
-};
-
-const polygonSelectionFill = {
- 'fill-color': '#EC3323',
- 'fill-opacity': 0.5
-};
-const polygonSelectionLine = {
- 'line-color': 'rgba(255, 0, 0, 1)',
- 'line-width': 4
-};
-const lineStringSelectionLine = {
- 'line-color': 'rgba(55, 42, 127, 1.00)',
- 'line-width': 3
-};
-const pointSelectionFill = {
- 'circle-color': '#EC3323'
-};
-
-function SelectionUtilisateurPolygonLayer({
- featureCollection,
- onMouseEnter,
- onMouseLeave
-}) {
- const data = useMemo(
- () =>
- filterFeatureCollectionByGeometryType(
- filterFeatureCollection(featureCollection, 'selection_utilisateur'),
- 'Polygon'
- ),
- [featureCollection]
- );
-
- return (
-
- );
-}
-
-function SelectionUtilisateurLineLayer({
- featureCollection,
- onMouseEnter,
- onMouseLeave
-}) {
- const data = useMemo(
- () =>
- filterFeatureCollectionByGeometryType(
- filterFeatureCollection(featureCollection, 'selection_utilisateur'),
- 'LineString'
- ),
- [featureCollection]
- );
- return (
-
- );
-}
-
-function SelectionUtilisateurPointLayer({
- featureCollection,
- onMouseEnter,
- onMouseLeave
-}) {
- const data = useMemo(
- () =>
- filterFeatureCollectionByGeometryType(
- filterFeatureCollection(featureCollection, 'selection_utilisateur'),
- 'Point'
- ),
- [featureCollection]
- );
- return (
-
- );
-}
-
-SelectionUtilisateurPolygonLayer.propTypes = {
- featureCollection: PropTypes.shape({
- type: PropTypes.string,
- bbox: PropTypes.array,
- features: PropTypes.array
- }),
- onMouseEnter: PropTypes.func,
- onMouseLeave: PropTypes.func
-};
-
-SelectionUtilisateurLineLayer.propTypes = {
- featureCollection: PropTypes.shape({
- type: PropTypes.string,
- bbox: PropTypes.array,
- features: PropTypes.array
- }),
- onMouseEnter: PropTypes.func,
- onMouseLeave: PropTypes.func
-};
-
-SelectionUtilisateurPointLayer.propTypes = {
- featureCollection: PropTypes.shape({
- type: PropTypes.string,
- bbox: PropTypes.array,
- features: PropTypes.array
- }),
- onMouseEnter: PropTypes.func,
- onMouseLeave: PropTypes.func
-};
-
-MapReader.propTypes = {
- featureCollection: PropTypes.shape({
- bbox: PropTypes.array,
- features: PropTypes.array
- }),
- options: PropTypes.shape({ layers: PropTypes.array })
-};
-
-export default MapReader;
diff --git a/app/javascript/components/MapReader/index.tsx b/app/javascript/components/MapReader/index.tsx
new file mode 100644
index 000000000..1f9fb07d4
--- /dev/null
+++ b/app/javascript/components/MapReader/index.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import 'maplibre-gl/dist/maplibre-gl.css';
+import type { FeatureCollection } from 'geojson';
+
+import { MapLibre } from '../shared/maplibre/MapLibre';
+import { CadastreLayer } from './components/CadastreLayer';
+import { GeoJSONLayer } from './components/GeoJSONLayer';
+
+const MapReader = ({
+ featureCollection,
+ options
+}: {
+ featureCollection: FeatureCollection;
+ options: { layers: string[] };
+}) => {
+ return (
+
+
+
+
+ );
+};
+
+export default MapReader;
diff --git a/app/javascript/components/MapReader/useMapbox.js b/app/javascript/components/MapReader/useMapbox.js
deleted file mode 100644
index d58b4e463..000000000
--- a/app/javascript/components/MapReader/useMapbox.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import { useCallback, useRef, useEffect, useMemo } from 'react';
-import mapboxgl, { Popup } from 'mapbox-gl';
-
-import {
- filterFeatureCollection,
- findFeature,
- getBounds,
- getCenter
-} from '../shared/mapbox/utils';
-
-const SOURCE_CADASTRE = 'cadastre';
-
-export function useMapbox(featureCollection) {
- const mapRef = useRef();
- const selectedCadastresRef = useRef(() => new Set());
- const isSupported = useMemo(() => mapboxgl.supported());
-
- const fitBounds = useCallback((bbox) => {
- mapRef.current.fitBounds(bbox, { padding: 100 });
- }, []);
-
- const onLoad = useCallback(
- (map) => {
- if (!mapRef.current) {
- mapRef.current = map;
- mapRef.current.fitBounds(featureCollection.bbox, { padding: 100 });
- onStyleChange();
- }
- },
- [featureCollection]
- );
-
- const onStyleChange = useCallback(() => {
- if (mapRef.current) {
- 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
- ]);
- }
- }
- }, [featureCollection]);
-
- const popup = useMemo(
- () =>
- new Popup({
- closeButton: false,
- closeOnClick: false
- })
- );
-
- const onMouseEnter = useCallback(
- (event) => {
- const feature = event.features[0];
- if (feature.properties && feature.properties.description) {
- const coordinates = getCenter(feature.geometry, event.lngLat);
- const description = feature.properties.description;
- mapRef.current.getCanvas().style.cursor = 'pointer';
- popup.setLngLat(coordinates).setHTML(description).addTo(mapRef.current);
- } else {
- popup.remove();
- }
- },
- [popup]
- );
-
- const onMouseLeave = useCallback(() => {
- mapRef.current.getCanvas().style.cursor = '';
- popup.remove();
- }, [popup]);
-
- useExternalEvents(featureCollection, { fitBounds });
-
- return { isSupported, onLoad, onStyleChange, onMouseEnter, onMouseLeave };
-}
-
-function useExternalEvents(featureCollection, { fitBounds }) {
- const onFeatureFocus = useCallback(
- ({ detail }) => {
- const { id } = detail;
- const feature = findFeature(featureCollection, id);
- if (feature) {
- fitBounds(getBounds(feature.geometry));
- }
- },
- [featureCollection, fitBounds]
- );
-
- useEvent('map:feature:focus', onFeatureFocus);
-}
-
-export function useEvent(eventName, callback) {
- return useEffect(() => {
- addEventListener(eventName, callback);
- return () => removeEventListener(eventName, callback);
- }, [eventName, callback]);
-}
diff --git a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx
index bce4e7103..68a83fc9c 100644
--- a/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx
+++ b/app/javascript/components/TypesDeChampEditor/components/TypeDeChamp.jsx
@@ -82,7 +82,7 @@ const TypeDeChamp = sortableElement(
}}
>
- Supprimer
+ Supprimer
diff --git a/app/javascript/components/shared/FlashMessage.jsx b/app/javascript/components/shared/FlashMessage.tsx
similarity index 57%
rename from app/javascript/components/shared/FlashMessage.jsx
rename to app/javascript/components/shared/FlashMessage.tsx
index 027b1c709..6a092ff60 100644
--- a/app/javascript/components/shared/FlashMessage.jsx
+++ b/app/javascript/components/shared/FlashMessage.tsx
@@ -1,17 +1,26 @@
import React from 'react';
import { createPortal } from 'react-dom';
-import PropTypes from 'prop-types';
-export function FlashMessage({ message, level, sticky, fixed }) {
+export function FlashMessage({
+ message,
+ level,
+ sticky,
+ fixed
+}: {
+ message: string;
+ level: string;
+ sticky?: boolean;
+ fixed?: boolean;
+}) {
return createPortal(
,
- document.getElementById('flash_messages')
+ document.getElementById('flash_messages')!
);
}
-function flashClassName(level, sticky = false, fixed = false) {
+function flashClassName(level: string, sticky = false, fixed = false) {
const className =
level == 'notice' ? ['alert', 'alert-success'] : ['alert', 'alert-danger'];
@@ -23,10 +32,3 @@ function flashClassName(level, sticky = false, fixed = false) {
}
return className.join(' ');
}
-
-FlashMessage.propTypes = {
- message: PropTypes.string,
- level: PropTypes.string,
- sticky: PropTypes.bool,
- fixed: PropTypes.bool
-};
diff --git a/app/javascript/components/shared/hooks.js b/app/javascript/components/shared/hooks.ts
similarity index 65%
rename from app/javascript/components/shared/hooks.js
rename to app/javascript/components/shared/hooks.ts
index a07191d75..483d5477f 100644
--- a/app/javascript/components/shared/hooks.js
+++ b/app/javascript/components/shared/hooks.ts
@@ -1,15 +1,18 @@
import { useRef, useCallback, useMemo, useState } from 'react';
import { fire } from '@utils';
-export function useDeferredSubmit(input) {
+export function useDeferredSubmit(input?: HTMLInputElement): {
+ (callback: () => void): void;
+ done: () => void;
+} {
const calledRef = useRef(false);
const awaitFormSubmit = useCallback(
- (callback) => {
+ (callback: () => void) => {
const form = input?.form;
if (!form) {
return;
}
- const interceptFormSubmit = (event) => {
+ const interceptFormSubmit = (event: Event) => {
event.preventDefault();
runCallback();
form.submit();
@@ -27,17 +30,24 @@ export function useDeferredSubmit(input) {
},
[input]
);
- awaitFormSubmit.done = () => {
+ const done = () => {
calledRef.current = true;
};
- return awaitFormSubmit;
+ return Object.assign(awaitFormSubmit, { done });
}
-export function groupId(id) {
+export function groupId(id: string) {
return `#champ-${id.replace(/-input$/, '')}`;
}
-export function useHiddenField(group, name = 'value') {
+export function useHiddenField(
+ group?: string,
+ name = 'value'
+): [
+ value: string | undefined,
+ setValue: (value: string) => void,
+ input: HTMLInputElement | undefined
+] {
const hiddenField = useMemo(
() => selectInputInGroup(group, name),
[group, name]
@@ -53,13 +63,16 @@ export function useHiddenField(group, name = 'value') {
fire(hiddenField, 'autosave:trigger');
}
},
- hiddenField
+ hiddenField ?? undefined
];
}
-function selectInputInGroup(group, name) {
+function selectInputInGroup(
+ group: string | undefined,
+ name: string
+): HTMLInputElement | undefined | null {
if (group) {
- return document.querySelector(
+ return document.querySelector(
`${group} input[type="hidden"][name$="[${name}]"], ${group} input[type="hidden"][name="${name}"]`
);
}
diff --git a/app/javascript/components/shared/mapbox/utils.js b/app/javascript/components/shared/mapbox/utils.js
deleted file mode 100644
index 721a0bfb5..000000000
--- a/app/javascript/components/shared/mapbox/utils.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { LngLatBounds } from 'mapbox-gl';
-
-export function getBounds(geometry) {
- const bbox = new LngLatBounds();
-
- if (geometry.type === 'Point') {
- return [geometry.coordinates, geometry.coordinates];
- } else if (geometry.type === 'LineString') {
- for (const coordinate of geometry.coordinates) {
- bbox.extend(coordinate);
- }
- } else {
- for (const coordinate of geometry.coordinates[0]) {
- bbox.extend(coordinate);
- }
- }
- return bbox;
-}
-
-export function findFeature(featureCollection, value, property = 'id') {
- return featureCollection.features.find(
- (feature) => feature.properties[property] === value
- );
-}
-
-export function filterFeatureCollection(featureCollection, source) {
- return {
- type: 'FeatureCollection',
- features: featureCollection.features.filter(
- (feature) => feature.properties.source === source
- )
- };
-}
-
-export function filterFeatureCollectionByGeometryType(featureCollection, type) {
- return {
- type: 'FeatureCollection',
- features: featureCollection.features.filter(
- (feature) => feature.geometry.type === type
- )
- };
-}
-
-export function generateId() {
- return Math.random().toString(20).substr(2, 6);
-}
-
-export function getCenter(geometry, lngLat) {
- const bbox = new LngLatBounds();
-
- switch (geometry.type) {
- case 'Point':
- return [...geometry.coordinates];
- case 'LineString':
- return [lngLat.lng, lngLat.lat];
- default:
- for (const coordinate of geometry.coordinates[0]) {
- bbox.extend(coordinate);
- }
- return bbox.getCenter();
- }
-}
-
-export function defer() {
- const deferred = {};
- const promise = new Promise(function (resolve, reject) {
- deferred.resolve = resolve;
- deferred.reject = reject;
- });
- deferred.promise = promise;
- return deferred;
-}
diff --git a/app/javascript/components/shared/maplibre/MapLibre.tsx b/app/javascript/components/shared/maplibre/MapLibre.tsx
new file mode 100644
index 000000000..b123733b2
--- /dev/null
+++ b/app/javascript/components/shared/maplibre/MapLibre.tsx
@@ -0,0 +1,104 @@
+import React, {
+ useState,
+ useContext,
+ useRef,
+ useEffect,
+ useMemo,
+ ReactNode,
+ createContext
+} from 'react';
+import maplibre, { Map, Style, NavigationControl } from 'maplibre-gl';
+
+import invariant from 'tiny-invariant';
+
+import { useStyle } from './hooks';
+import { StyleControl } from './StyleControl';
+
+const Context = createContext<{ map?: Map | null }>({});
+
+type MapLibreProps = {
+ layers: string[];
+ header?: ReactNode;
+ footer?: ReactNode;
+ children: ReactNode;
+};
+
+export function useMapLibre() {
+ const context = useContext(Context);
+ invariant(context.map, 'Maplibre not initialized');
+ return context.map;
+}
+
+export function MapLibre({ children, header, footer, layers }: MapLibreProps) {
+ const isSupported = useMemo(
+ () => maplibre.supported({ failIfMajorPerformanceCaveat: true }) && !isIE(),
+ []
+ );
+ const containerRef = useRef(null);
+ const [map, setMap] = useState();
+
+ const onStyleChange = (style: Style) => {
+ if (map) {
+ map.setStyle(style);
+ }
+ };
+ const { style, ...mapStyleProps } = useStyle(layers, onStyleChange);
+
+ useEffect(() => {
+ if (isSupported && !map) {
+ invariant(containerRef.current, 'Map container not found');
+ const map = new Map({
+ container: containerRef.current,
+ style
+ });
+ map.addControl(new NavigationControl({}), 'top-right');
+ map.on('load', () => {
+ setMap(map);
+ });
+ }
+ }, []);
+
+ if (!isSupported) {
+ return (
+
+
+
⚠️
+
+ Nous ne pouvons pas afficher la carte car elle est imcompatible avec
+ votre navigateur. Nous vous conseillons de le mettre à jour ou
+ d’utiliser{' '}
+
+ un navigateur plus récent
+
+ .
+
+
+
+ );
+ }
+
+ return (
+
+ {map ? header : null}
+
+
+ {map ? children : null}
+
+ {map ? footer : null}
+
+ );
+}
+
+function isIE() {
+ const ua = window.navigator.userAgent;
+ const msie = ua.indexOf('MSIE ');
+ const trident = ua.indexOf('Trident/');
+ return msie > 0 || trident > 0;
+}
diff --git a/app/javascript/components/shared/mapbox/MapStyleControl.jsx b/app/javascript/components/shared/maplibre/StyleControl.tsx
similarity index 64%
rename from app/javascript/components/shared/mapbox/MapStyleControl.jsx
rename to app/javascript/components/shared/maplibre/StyleControl.tsx
index f6bc98a04..d959e41d4 100644
--- a/app/javascript/components/shared/mapbox/MapStyleControl.jsx
+++ b/app/javascript/components/shared/maplibre/StyleControl.tsx
@@ -1,5 +1,4 @@
-import React, { useMemo, useState, useEffect } from 'react';
-import PropTypes from 'prop-types';
+import React, { useState } from 'react';
import { Popover, RadioGroup } from '@headlessui/react';
import { usePopper } from 'react-popper';
import { MapIcon } from '@heroicons/react/outline';
@@ -7,7 +6,7 @@ import { Slider } from '@reach/slider';
import { useId } from '@reach/auto-id';
import '@reach/slider/styles.css';
-import { getMapStyle, getLayerName, NBS } from './styles';
+import { LayersMap, NBS } from './styles';
const STYLES = {
ortho: 'Satellite',
@@ -15,68 +14,22 @@ const STYLES = {
ign: 'Carte IGN'
};
-function optionalLayersMap(optionalLayers) {
- return Object.fromEntries(
- optionalLayers.map((layer) => [
- layer,
- {
- configurable: layer != 'cadastres',
- enabled: true,
- opacity: 70,
- name: getLayerName(layer)
- }
- ])
- );
-}
-
-export function useMapStyle(
- optionalLayers,
- { onStyleChange, cadastreEnabled }
-) {
- const [styleId, setStyle] = useState('ortho');
- const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));
- const setLayerEnabled = (layer, enabled) =>
- setLayers((optionalLayers) => {
- optionalLayers[layer].enabled = enabled;
- return { ...optionalLayers };
- });
- const setLayerOpacity = (layer, opacity) =>
- setLayers((optionalLayers) => {
- optionalLayers[layer].opacity = opacity;
- return { ...optionalLayers };
- });
- const enabledLayers = Object.entries(layers).filter(
- ([, { enabled }]) => enabled
- );
- const layerIds = enabledLayers.map(
- ([layer, { opacity }]) => `${layer}-${opacity}`
- );
- const style = useMemo(
- () =>
- getMapStyle(
- styleId,
- enabledLayers.map(([layer]) => layer),
- Object.fromEntries(
- enabledLayers.map(([layer, { opacity }]) => [layer, opacity])
- )
- ),
- [styleId, layerIds]
- );
-
- useEffect(() => onStyleChange(), [styleId, layerIds, cadastreEnabled]);
-
- return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
-}
-
-function MapStyleControl({
- style,
+export function StyleControl({
+ styleId,
layers,
setStyle,
setLayerEnabled,
setLayerOpacity
+}: {
+ styleId: string;
+ setStyle: (style: string) => void;
+ layers: LayersMap;
+ setLayerEnabled: (layer: string, enabled: boolean) => void;
+ setLayerOpacity: (layer: string, opacity: number) => void;
}) {
- const [buttonElement, setButtonElement] = useState();
- const [panelElement, setPanelElement] = useState();
+ const [buttonElement, setButtonElement] =
+ useState();
+ const [panelElement, setPanelElement] = useState();
const { styles, attributes } = usePopper(buttonElement, panelElement, {
placement: 'bottom-end'
});
@@ -86,7 +39,10 @@ function MapStyleControl({
const mapId = useId();
return (
-
+
);
}
-
-MapStyleControl.propTypes = {
- style: PropTypes.string,
- layers: PropTypes.object,
- setStyle: PropTypes.func,
- setLayerEnabled: PropTypes.func,
- setLayerOpacity: PropTypes.func
-};
-
-export default MapStyleControl;
diff --git a/app/javascript/components/shared/maplibre/hooks.ts b/app/javascript/components/shared/maplibre/hooks.ts
new file mode 100644
index 000000000..dc6aaef97
--- /dev/null
+++ b/app/javascript/components/shared/maplibre/hooks.ts
@@ -0,0 +1,110 @@
+import { useCallback, useEffect, useState, useMemo } from 'react';
+import type {
+ LngLatBoundsLike,
+ LngLat,
+ MapLayerEventType,
+ Style
+} from 'maplibre-gl';
+import type { Feature, Geometry } from 'geojson';
+
+import { getMapStyle, getLayerName, LayersMap } from './styles';
+import { useMapLibre } from './MapLibre';
+
+export function useFitBounds() {
+ const map = useMapLibre();
+ return useCallback((bbox: LngLatBoundsLike) => {
+ map.fitBounds(bbox, { padding: 100 });
+ }, []);
+}
+
+export function useFlyTo() {
+ const map = useMapLibre();
+ return useCallback((zoom: number, center: [number, number]) => {
+ map.flyTo({ zoom, center });
+ }, []);
+}
+
+export function useEvent(eventName: string, callback: EventListener) {
+ return useEffect(() => {
+ addEventListener(eventName, callback);
+ return () => removeEventListener(eventName, callback);
+ }, [eventName, callback]);
+}
+
+export type EventHandler = (event: {
+ features: Feature[];
+ lngLat: LngLat;
+}) => void;
+
+export function useMapEvent(
+ eventName: string,
+ callback: EventHandler,
+ target?: string
+) {
+ const map = useMapLibre();
+ return useEffect(() => {
+ if (target) {
+ map.on(eventName as keyof MapLayerEventType, target, callback as any);
+ } else {
+ map.on(eventName, callback);
+ }
+ return () => {
+ if (target) {
+ map.off(eventName as keyof MapLayerEventType, target, callback as any);
+ } else {
+ map.off(eventName, callback);
+ }
+ };
+ }, [map, eventName, target, callback]);
+}
+
+function optionalLayersMap(optionalLayers: string[]): LayersMap {
+ return Object.fromEntries(
+ optionalLayers.map((layer) => [
+ layer,
+ {
+ configurable: layer != 'cadastres',
+ enabled: true,
+ opacity: 70,
+ name: getLayerName(layer)
+ }
+ ])
+ );
+}
+
+export function useStyle(
+ optionalLayers: string[],
+ onStyleChange: (style: Style) => void
+) {
+ const [styleId, setStyle] = useState('ortho');
+ const [layers, setLayers] = useState(() => optionalLayersMap(optionalLayers));
+ const setLayerEnabled = (layer: string, enabled: boolean) =>
+ setLayers((optionalLayers) => {
+ optionalLayers[layer].enabled = enabled;
+ return { ...optionalLayers };
+ });
+ const setLayerOpacity = (layer: string, opacity: number) =>
+ setLayers((optionalLayers) => {
+ optionalLayers[layer].opacity = opacity;
+ return { ...optionalLayers };
+ });
+ const enabledLayers = useMemo(
+ () => Object.entries(layers).filter(([, { enabled }]) => enabled),
+ [layers]
+ );
+ const style = useMemo(
+ () =>
+ getMapStyle(
+ styleId,
+ enabledLayers.map(([layer]) => layer),
+ Object.fromEntries(
+ enabledLayers.map(([layer, { opacity }]) => [layer, opacity])
+ )
+ ),
+ [styleId, enabledLayers]
+ );
+
+ useEffect(() => onStyleChange(style), [style]);
+
+ return { style, layers, setStyle, setLayerEnabled, setLayerOpacity };
+}
diff --git a/app/javascript/components/shared/mapbox/styles/base.js b/app/javascript/components/shared/maplibre/styles/base.ts
similarity index 85%
rename from app/javascript/components/shared/mapbox/styles/base.js
rename to app/javascript/components/shared/maplibre/styles/base.ts
index 8e7ecd0e1..cc7c880ee 100644
--- a/app/javascript/components/shared/mapbox/styles/base.js
+++ b/app/javascript/components/shared/maplibre/styles/base.ts
@@ -1,8 +1,11 @@
+import type { AnyLayer, Style, RasterLayer, RasterSource } from 'maplibre-gl';
+import invariant from 'tiny-invariant';
+
import cadastreLayers from './layers/cadastre';
-const IGN_TOKEN = 'rc1egnbeoss72hxvd143tbyk';
+const IGN_TOKEN = 'rc1egnbeoss72hxvd143tbyk'; // ggignore
-function ignServiceURL(layer, format = 'image/png') {
+function ignServiceURL(layer: string, format = 'image/png') {
const url = `https://wxs.ign.fr/${IGN_TOKEN}/geoportail/wmts`;
const query =
'service=WMTS&request=GetTile&version=1.0.0&tilematrixset=PM&tilematrix={z}&tilecol={x}&tilerow={y}&style=normal';
@@ -10,7 +13,7 @@ function ignServiceURL(layer, format = 'image/png') {
return `${url}?${query}&layer=${layer}&format=${format}`;
}
-const OPTIONAL_LAYERS = [
+const OPTIONAL_LAYERS: { label: string; id: string; layers: string[][] }[] = [
{
label: 'UNESCO',
id: 'unesco',
@@ -127,7 +130,7 @@ function buildSources() {
);
}
-function rasterSource(tiles, attribution) {
+function rasterSource(tiles: string[], attribution: string): RasterSource {
return {
type: 'raster',
tiles,
@@ -138,7 +141,7 @@ function rasterSource(tiles, attribution) {
};
}
-function rasterLayer(source, opacity) {
+function rasterLayer(source: string, opacity: number): RasterLayer {
return {
id: source,
source,
@@ -147,10 +150,13 @@ function rasterLayer(source, opacity) {
};
}
-export function buildOptionalLayers(ids, opacity) {
+export function buildOptionalLayers(
+ ids: string[],
+ opacity: Record
+): AnyLayer[] {
return OPTIONAL_LAYERS.filter(({ id }) => ids.includes(id))
.flatMap(({ layers, id }) =>
- layers.map(([, code]) => [code, opacity[id] / 100])
+ layers.map(([, code]) => [code, opacity[id] / 100] as const)
)
.flatMap(([code, opacity]) =>
code === 'CADASTRE'
@@ -159,16 +165,15 @@ export function buildOptionalLayers(ids, opacity) {
);
}
-export const NBS = ' ';
+export const NBS = ' ' as const;
-export function getLayerName(layer) {
- return OPTIONAL_LAYERS.find(({ id }) => id == layer).label.replace(
- /\s/g,
- NBS
- );
+export function getLayerName(layer: string): string {
+ const name = OPTIONAL_LAYERS.find(({ id }) => id == layer);
+ invariant(name, `Layer "${layer}" not found`);
+ return name.label.replace(/\s/g, NBS);
}
-function getLayerCode(code) {
+function getLayerCode(code: string) {
return code.toLowerCase().replace(/\./g, '-');
}
@@ -220,4 +225,4 @@ export default {
},
sprite: 'https://openmaptiles.github.io/osm-bright-gl-style/sprite',
glyphs: 'https://openmaptiles.geo.data.gouv.fr/fonts/{fontstack}/{range}.pbf'
-};
+} as Style;
diff --git a/app/javascript/components/shared/mapbox/styles/index.js b/app/javascript/components/shared/maplibre/styles/index.ts
similarity index 61%
rename from app/javascript/components/shared/mapbox/styles/index.js
rename to app/javascript/components/shared/maplibre/styles/index.ts
index ed97b2aee..b7d865e29 100644
--- a/app/javascript/components/shared/mapbox/styles/index.js
+++ b/app/javascript/components/shared/maplibre/styles/index.ts
@@ -1,3 +1,5 @@
+import type { Style } from 'maplibre-gl';
+
import baseStyle, { buildOptionalLayers, getLayerName, NBS } from './base';
import orthoStyle from './layers/ortho';
import vectorStyle from './layers/vector';
@@ -5,7 +7,21 @@ import ignLayers from './layers/ign';
export { getLayerName, NBS };
-export function getMapStyle(id, layers, opacity) {
+export type LayersMap = Record<
+ string,
+ {
+ configurable: boolean;
+ enabled: boolean;
+ opacity: number;
+ name: string;
+ }
+>;
+
+export function getMapStyle(
+ id: string,
+ layers: string[],
+ opacity: Record
+): Style & { id: string } {
const style = { ...baseStyle, id };
switch (id) {
@@ -23,7 +39,7 @@ export function getMapStyle(id, layers, opacity) {
break;
}
- style.layers = style.layers.concat(buildOptionalLayers(layers, opacity));
+ style.layers = style.layers?.concat(buildOptionalLayers(layers, opacity));
return style;
}
diff --git a/app/javascript/components/shared/mapbox/styles/layers/cadastre.js b/app/javascript/components/shared/maplibre/styles/layers/cadastre.ts
similarity index 95%
rename from app/javascript/components/shared/mapbox/styles/layers/cadastre.js
rename to app/javascript/components/shared/maplibre/styles/layers/cadastre.ts
index 0aed8996c..759504f97 100644
--- a/app/javascript/components/shared/mapbox/styles/layers/cadastre.js
+++ b/app/javascript/components/shared/maplibre/styles/layers/cadastre.ts
@@ -1,4 +1,6 @@
-export default [
+import { AnyLayer } from 'maplibre-gl';
+
+const layers: AnyLayer[] = [
{
id: 'batiments-line',
type: 'line',
@@ -104,3 +106,5 @@ export default [
}
}
];
+
+export default layers;
diff --git a/app/javascript/components/shared/mapbox/styles/layers/ign.js b/app/javascript/components/shared/maplibre/styles/layers/ign.ts
similarity index 52%
rename from app/javascript/components/shared/mapbox/styles/layers/ign.js
rename to app/javascript/components/shared/maplibre/styles/layers/ign.ts
index e7e7614a7..545d41911 100644
--- a/app/javascript/components/shared/mapbox/styles/layers/ign.js
+++ b/app/javascript/components/shared/maplibre/styles/layers/ign.ts
@@ -1,4 +1,6 @@
-export default [
+import type { RasterLayer } from 'maplibre-gl';
+
+const layers: RasterLayer[] = [
{
id: 'ign',
source: 'plan-ign',
@@ -6,3 +8,5 @@ export default [
paint: { 'raster-resampling': 'linear' }
}
];
+
+export default layers;
diff --git a/app/javascript/components/shared/mapbox/styles/layers/ortho.js b/app/javascript/components/shared/maplibre/styles/layers/ortho.ts
similarity index 99%
rename from app/javascript/components/shared/mapbox/styles/layers/ortho.js
rename to app/javascript/components/shared/maplibre/styles/layers/ortho.ts
index 0e255a426..5423d46ce 100644
--- a/app/javascript/components/shared/mapbox/styles/layers/ortho.js
+++ b/app/javascript/components/shared/maplibre/styles/layers/ortho.ts
@@ -1,4 +1,6 @@
-export default [
+import type { AnyLayer } from 'maplibre-gl';
+
+const layers: AnyLayer[] = [
{
id: 'photographies-aeriennes',
type: 'raster',
@@ -2129,7 +2131,7 @@ export default [
[10, 'point'],
[11, 'line']
]
- },
+ } as any,
'symbol-spacing': 200,
'text-field': '{ref}',
'text-font': ['Noto Sans Regular'],
@@ -2160,7 +2162,7 @@ export default [
[10, 'point'],
[11, 'line']
]
- },
+ } as any,
'symbol-spacing': 200,
'text-field': '{ref}',
'text-font': ['Noto Sans Regular'],
@@ -2262,7 +2264,7 @@ export default [
'text-letter-spacing': 0,
'icon-padding': 2,
'symbol-placement': 'point',
- 'symbol-z-order': 'auto',
+ 'symbol-z-order': 'auto' as any,
'text-line-height': 1.2,
'text-allow-overlap': false,
'text-ignore-placement': false,
@@ -2637,3 +2639,5 @@ export default [
}
}
];
+
+export default layers;
diff --git a/app/javascript/components/shared/mapbox/styles/layers/vector.js b/app/javascript/components/shared/maplibre/styles/layers/vector.ts
similarity index 99%
rename from app/javascript/components/shared/mapbox/styles/layers/vector.js
rename to app/javascript/components/shared/maplibre/styles/layers/vector.ts
index d69b812a2..b91bf9cb3 100644
--- a/app/javascript/components/shared/mapbox/styles/layers/vector.js
+++ b/app/javascript/components/shared/maplibre/styles/layers/vector.ts
@@ -1,4 +1,6 @@
-export default [
+import type { AnyLayer } from 'maplibre-gl';
+
+const layers: AnyLayer[] = [
{
id: 'background',
type: 'background',
@@ -113,7 +115,7 @@ export default [
[0, false],
[9, true]
]
- },
+ } as any,
'fill-color': '#6a4',
'fill-opacity': 0.1,
'fill-outline-color': 'hsla(0, 0%, 0%, 0.03)'
@@ -324,7 +326,7 @@ export default [
[6, [2, 0]],
[8, [0, 0]]
]
- }
+ } as any
}
},
{
@@ -427,7 +429,7 @@ export default [
[14, [0, 0]],
[16, [-2, -2]]
]
- }
+ } as any
}
},
{
@@ -2322,7 +2324,7 @@ export default [
[10, 'point'],
[11, 'line']
]
- },
+ } as any,
'symbol-spacing': 200,
'text-field': '{ref}',
'text-font': ['Noto Sans Regular'],
@@ -2354,7 +2356,7 @@ export default [
[7, 'line'],
[8, 'line']
]
- },
+ } as any,
'symbol-spacing': 200,
'text-field': '{ref}',
'text-font': ['Noto Sans Regular'],
@@ -2385,7 +2387,7 @@ export default [
[10, 'point'],
[11, 'line']
]
- },
+ } as any,
'symbol-spacing': 200,
'text-field': '{ref}',
'text-font': ['Noto Sans Regular'],
@@ -2837,3 +2839,5 @@ export default [
}
}
];
+
+export default layers;
diff --git a/app/javascript/components/shared/maplibre/utils.ts b/app/javascript/components/shared/maplibre/utils.ts
new file mode 100644
index 000000000..8ab08c181
--- /dev/null
+++ b/app/javascript/components/shared/maplibre/utils.ts
@@ -0,0 +1,93 @@
+import {
+ LngLatBounds,
+ LngLat,
+ LngLatLike,
+ LngLatBoundsLike
+} from 'maplibre-gl';
+import type { Geometry, FeatureCollection, Feature } from 'geojson';
+import invariant from 'tiny-invariant';
+
+export function getBounds(geometry: Geometry): LngLatBoundsLike {
+ const bbox = new LngLatBounds();
+
+ if (geometry.type === 'Point') {
+ return [geometry.coordinates, geometry.coordinates] as [
+ [number, number],
+ [number, number]
+ ];
+ } else if (geometry.type === 'LineString') {
+ for (const coordinate of geometry.coordinates) {
+ bbox.extend(coordinate as [number, number]);
+ }
+ } else {
+ invariant(
+ geometry.type != 'GeometryCollection',
+ 'GeometryCollection not supported'
+ );
+ for (const coordinate of geometry.coordinates[0]) {
+ bbox.extend(coordinate as [number, number]);
+ }
+ }
+ return bbox;
+}
+
+export function findFeature(
+ featureCollection: FeatureCollection,
+ value: unknown,
+ property = 'id'
+): Feature | null {
+ return (
+ featureCollection.features.find(
+ (feature) => feature.properties && feature.properties[property] === value
+ ) ?? null
+ );
+}
+
+export function filterFeatureCollection(
+ featureCollection: FeatureCollection,
+ source: string
+): FeatureCollection {
+ return {
+ type: 'FeatureCollection',
+ features: featureCollection.features.filter(
+ (feature) => feature.properties?.source === source
+ )
+ };
+}
+
+export function filterFeatureCollectionByGeometryType(
+ featureCollection: FeatureCollection,
+ type: Geometry['type']
+): FeatureCollection {
+ return {
+ type: 'FeatureCollection',
+ features: featureCollection.features.filter(
+ (feature) => feature.geometry.type === type
+ )
+ };
+}
+
+export function generateId(): string {
+ return Math.random().toString(20).substring(2, 6);
+}
+
+export function getCenter(geometry: Geometry, lngLat: LngLat): LngLatLike {
+ const bbox = new LngLatBounds();
+
+ invariant(
+ geometry.type != 'GeometryCollection',
+ 'GeometryCollection not supported'
+ );
+
+ switch (geometry.type) {
+ case 'Point':
+ return [...geometry.coordinates] as [number, number];
+ case 'LineString':
+ return [lngLat.lng, lngLat.lat];
+ default:
+ for (const coordinate of geometry.coordinates[0]) {
+ bbox.extend(coordinate as [number, number]);
+ }
+ return bbox.getCenter();
+ }
+}
diff --git a/app/javascript/components/shared/queryClient.js b/app/javascript/components/shared/queryClient.ts
similarity index 78%
rename from app/javascript/components/shared/queryClient.js
rename to app/javascript/components/shared/queryClient.ts
index e37e4fb7f..14164f831 100644
--- a/app/javascript/components/shared/queryClient.js
+++ b/app/javascript/components/shared/queryClient.ts
@@ -1,4 +1,4 @@
-import { QueryClient } from 'react-query';
+import { QueryClient, QueryFunction } from 'react-query';
import { getJSON, isNumeric } from '@utils';
import { matchSorter } from 'match-sorter';
@@ -16,17 +16,15 @@ const API_ADRESSE_QUERY_LIMIT = 5;
const API_GEO_COMMUNES_QUERY_LIMIT = 60;
const { api_geo_url, api_adresse_url, api_education_url } =
- gon.autocomplete || {};
+ (window as any).gon.autocomplete || {};
-export const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- queryFn: defaultQueryFn
- }
- }
-});
+type QueryKey = readonly [
+ scope: string,
+ term: string,
+ extra: string | undefined
+];
-function buildURL(scope, term, extra) {
+function buildURL(scope: string, term: string, extra?: string) {
term = encodeURIComponent(term.replace(/\(|\)/g, ''));
if (scope === 'adresse') {
return `${api_adresse_url}/search?q=${term}&limit=${API_ADRESSE_QUERY_LIMIT}`;
@@ -48,7 +46,7 @@ function buildURL(scope, term, extra) {
return `${api_geo_url}/${scope}?nom=${term}&limit=${API_GEO_QUERY_LIMIT}`;
}
-function buildOptions() {
+function buildOptions(): [RequestInit, AbortController | null] {
if (window.AbortController) {
const controller = new AbortController();
const signal = controller.signal;
@@ -57,7 +55,9 @@ function buildOptions() {
return [{}, null];
}
-async function defaultQueryFn({ queryKey: [scope, term, extra] }) {
+const defaultQueryFn: QueryFunction = async ({
+ queryKey: [scope, term, extra]
+}) => {
if (scope == 'pays') {
return matchSorter(await getPays(), term, { keys: ['label'] });
}
@@ -70,14 +70,22 @@ async function defaultQueryFn({ queryKey: [scope, term, extra] }) {
}
throw new Error(`Error fetching from "${scope}" API`);
});
- promise.cancel = () => controller && controller.abort();
+ (promise as any).cancel = () => controller && controller.abort();
return promise;
-}
+};
-let paysCache;
-async function getPays() {
+let paysCache: { label: string }[];
+async function getPays(): Promise<{ label: string }[]> {
if (!paysCache) {
paysCache = await getJSON('/api/pays', null);
}
return paysCache;
}
+
+export const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ queryFn: defaultQueryFn as any
+ }
+ }
+});
diff --git a/app/javascript/types.d.ts b/app/javascript/types.d.ts
index 27592bb47..200e8886e 100644
--- a/app/javascript/types.d.ts
+++ b/app/javascript/types.d.ts
@@ -1,4 +1,6 @@
-declare module '@tmcw/togeojson' {
+// This file contains type definitions for untyped packages. We are lucky to have only two ;)
+
+declare module '@tmcw/togeojson/dist/togeojson.es.js' {
import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson';
export function kml(doc: Document): FeatureCollection;
diff --git a/config/webpack/environment.js b/config/webpack/environment.js
index deb5c4c44..c5e252282 100644
--- a/config/webpack/environment.js
+++ b/config/webpack/environment.js
@@ -24,7 +24,7 @@ if (!Array.isArray(nodeModulesLoader.exclude)) {
nodeModulesLoader.exclude == null ? [] : [nodeModulesLoader.exclude];
}
nodeModulesLoader.exclude.push(
- path.resolve(__dirname, '..', '..', 'node_modules/mapbox-gl')
+ path.resolve(__dirname, '..', '..', 'node_modules/maplibre-gl')
);
// Uncoment next lines to run webpack-bundle-analyzer
diff --git a/package.json b/package.json
index aade98581..a65a2247b 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"@babel/preset-typescript": "^7.16.7",
"@headlessui/react": "^1.3.0",
"@heroicons/react": "^1.0.1",
- "@mapbox/mapbox-gl-draw": "^1.2.2",
+ "@mapbox/mapbox-gl-draw": "^1.3.0",
"@popperjs/core": "^2.9.2",
"@rails/actiontext": "^6.1.4-1",
"@rails/activestorage": "^6.1.4-1",
@@ -13,7 +13,6 @@
"@reach/auto-id": "^0.16.0",
"@reach/combobox": "^0.13.0",
"@reach/slider": "^0.15.0",
- "@reach/visually-hidden": "^0.15.2",
"@sentry/browser": "6.12.0",
"@tmcw/togeojson": "^4.3.0",
"babel-plugin-macros": "^2.8.0",
@@ -23,18 +22,17 @@
"debounce": "^1.2.1",
"dom4": "^2.1.6",
"email-butler": "^1.0.13",
+ "geojson": "^0.5.0",
"highcharts": "^9.0.0",
"intersection-observer": "^0.12.0",
"is-hotkey": "^0.2.0",
- "mapbox-gl": "^1.3.0",
+ "maplibre-gl": "^1.15.2",
"match-sorter": "^6.2.0",
"prop-types": "^15.7.2",
"react": "^17.0.1",
- "react-coordinate-input": "^1.0.0-rc.2",
+ "react-coordinate-input": "^1.0.0",
"react-dom": "^17.0.1",
"react-intersection-observer": "^8.31.0",
- "react-mapbox-gl": "^5.1.1",
- "react-mapbox-gl-draw": "^2.0.4",
"react-popper": "^2.2.5",
"react-query": "^3.9.7",
"react-sortable-hoc": "^1.11.0",
diff --git a/tsconfig.json b/tsconfig.json
index d0f042196..6995789e9 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,7 +1,6 @@
{
"compilerOptions": {
"declaration": false,
- "emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"module": "es6",
@@ -12,6 +11,7 @@
"noEmit": true,
"skipLibCheck": true,
"esModuleInterop": true,
+ "strict": true,
"paths": {
"~/*": ["./app/javascript/*"],
"@utils": ["./app/javascript/shared/utils.ts"]