diff --git a/app/javascript/components/MapReader/index.jsx b/app/javascript/components/MapReader/index.jsx index 9a14202cb..c976464f8 100644 --- a/app/javascript/components/MapReader/index.jsx +++ b/app/javascript/components/MapReader/index.jsx @@ -1,141 +1,28 @@ -import React, { useState, useCallback, useMemo } from 'react'; -import { ZoomControl, GeoJSONLayer } from 'react-mapbox-gl'; -import mapboxgl, { Popup } from 'mapbox-gl'; +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 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 { filterFeatureCollection, - filterFeatureCollectionByGeometryType, - useEvent, - findFeature, - fitBounds, - getCenter + filterFeatureCollectionByGeometryType } from '../shared/mapbox/utils'; +import { useMapbox } from './useMapbox'; + +const Mapbox = ReactMapboxGl({}); const MapReader = ({ featureCollection, options }) => { - const [currentMap, setCurrentMap] = useState(null); - const [style, setStyle] = useState('ortho'); - const cadastresFeatureCollection = useMemo( - () => filterFeatureCollection(featureCollection, 'cadastre'), - [featureCollection] - ); - const selectionsUtilisateurFeatureCollection = useMemo( - () => filterFeatureCollection(featureCollection, 'selection_utilisateur'), - [featureCollection] - ); - const selectionsLineFeatureCollection = useMemo( - () => - filterFeatureCollectionByGeometryType( - selectionsUtilisateurFeatureCollection, - 'LineString' - ), - [selectionsUtilisateurFeatureCollection] - ); - const selectionsPolygonFeatureCollection = useMemo( - () => - filterFeatureCollectionByGeometryType( - selectionsUtilisateurFeatureCollection, - 'Polygon' - ), - [selectionsUtilisateurFeatureCollection] - ); - const selectionsPointFeatureCollection = useMemo( - () => - filterFeatureCollectionByGeometryType( - selectionsUtilisateurFeatureCollection, - 'Point' - ), - [selectionsUtilisateurFeatureCollection] - ); - const hasCadastres = useMemo(() => options.layers.includes('cadastres')); - const mapStyle = useMemo(() => getMapStyle(style, options.layers), [ - style, - options - ]); - const popup = useMemo( - () => - new Popup({ - closeButton: false, - closeOnClick: false - }) - ); + const { + isSupported, + onLoad, + onStyleChange, + onMouseEnter, + onMouseLeave + } = useMapbox(featureCollection); + const [style, setStyle] = useMapStyle(options.layers, { onStyleChange }); - 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; - currentMap.getCanvas().style.cursor = 'pointer'; - popup.setLngLat(coordinates).setHTML(description).addTo(currentMap); - } else { - popup.remove(); - } - }, - [currentMap, popup] - ); - - const onMouseLeave = useCallback(() => { - currentMap.getCanvas().style.cursor = ''; - popup.remove(); - }, [currentMap, popup]); - - const onFeatureFocus = useCallback( - ({ detail }) => { - const feature = findFeature(featureCollection, detail.id); - if (feature) { - fitBounds(currentMap, feature); - } - }, - [currentMap, featureCollection] - ); - - useEvent('map:feature:focus', onFeatureFocus); - - const [a1, a2, b1, b2] = featureCollection.bbox; - const boundData = [ - [a1, a2], - [b1, b2] - ]; - - 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' - }; - - const polygonCadastresFill = { - 'fill-color': '#FAD859', - 'fill-opacity': 0.5 - }; - - const polygonCadastresLine = { - 'line-color': 'rgba(156, 160, 144, 255)', - 'line-width': 2, - 'line-dasharray': [1, 1] - }; - - function onMapLoad(map) { - setCurrentMap(map); - } - - if (!mapboxgl.supported()) { + if (!isSupported) { return (

Nous ne pouvons pas afficher la carte car elle est imcompatible avec @@ -147,58 +34,155 @@ const MapReader = ({ featureCollection, options }) => { return ( onMapLoad(map)} - fitBounds={boundData} - fitBoundsOptions={{ padding: 100 }} - style={mapStyle} - containerStyle={{ - height: '400px', - width: '100%' - }} + onStyleLoad={(map) => onLoad(map)} + style={style} + containerStyle={{ height: '400px' }} > - - - - {hasCadastres ? ( - - ) : null} - + ); }; -MapReader.propTypes = { +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 }), - options: PropTypes.shape({ - layers: PropTypes.array, - ign: PropTypes.bool - }) + 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/useMapbox.js b/app/javascript/components/MapReader/useMapbox.js new file mode 100644 index 000000000..d58b4e463 --- /dev/null +++ b/app/javascript/components/MapReader/useMapbox.js @@ -0,0 +1,104 @@ +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]); +}