diff --git a/app/assets/stylesheets/new_design/carte.scss b/app/assets/stylesheets/new_design/carte.scss new file mode 100644 index 000000000..b23ec84e6 --- /dev/null +++ b/app/assets/stylesheets/new_design/carte.scss @@ -0,0 +1,13 @@ +.areas-title { + font-weight: bold; + margin-top: 5px; + margin-bottom: 5px; +} + +.areas { + margin-bottom: 10px; + + input { + margin-top: 5px; + } +} diff --git a/app/assets/stylesheets/new_design/map.scss b/app/assets/stylesheets/new_design/map.scss deleted file mode 100644 index 588030137..000000000 --- a/app/assets/stylesheets/new_design/map.scss +++ /dev/null @@ -1,80 +0,0 @@ -.carte { - height: 400px; - margin-bottom: 16px; - z-index: 0; -} - -.leaflet-container path { - cursor: url("/assets/edit.png"), default !important; -} - -.carte { - &.edit { - g path.leaflet-polygon { - transition: all 0.25s; - stroke-width: 4px; - stroke-opacity: 1; - stroke: #D7217E; - position: absolute; - z-index: 100; - fill: #D7217E; - fill-opacity: 0.75; - } - - div.leaflet-edge { - box-shadow: 0 0 0 2px #FFFFFF, 0 0 10px rgba(0, 0, 0, 0.35); - border: 5px solid #D7217E; - border-radius: 10px; - transition: opacity 0.25s; - cursor: move; - opacity: 0; - pointer-events: none; - box-sizing: border-box; - width: 0 !important; - height: 0 !important; - } - } - - &.mode-create { - cursor: url("/assets/pencil.png"), crosshair !important; - } - - &.mode-edit div.leaflet-edge { - opacity: 1; - pointer-events: all; - } - - &.mode-delete path.leaflet-polygon { - cursor: no-drop !important; - - &:hover { - fill: #4D4D4D !important; - } - } -} - -.editable-champ-carte { - .toolbar { - display: flex; - align-items: center; - margin-bottom: 10px; - - .button { - width: 200px; - margin-right: 10px; - } - - .select2-container { - margin-bottom: 0; - } - } -} - -.areas-title { - font-weight: bold; - margin-bottom: 5px; -} - -.areas { - margin-bottom: 10px; -} diff --git a/app/assets/stylesheets/new_design/utils.scss b/app/assets/stylesheets/new_design/utils.scss index 9e5b14290..62e44fab0 100644 --- a/app/assets/stylesheets/new_design/utils.scss +++ b/app/assets/stylesheets/new_design/utils.scss @@ -53,3 +53,7 @@ .mt-2 { margin-top: 2 * $default-spacer; } + +.mb-2 { + margin-bottom: 2 * $default-spacer; +} diff --git a/app/controllers/champs/carte_controller.rb b/app/controllers/champs/carte_controller.rb index 6790a9767..fc9a55833 100644 --- a/app/controllers/champs/carte_controller.rb +++ b/app/controllers/champs/carte_controller.rb @@ -19,7 +19,7 @@ class Champs::CarteController < ApplicationController def create champ = policy_scope(Champ).find(params[:champ_id]) geo_area = champ.geo_areas.selections_utilisateur.new - save_geometry!(geo_area, params_feature) + save_feature!(geo_area, params_feature) render json: { feature: geo_area.to_feature }, status: :created end @@ -27,7 +27,7 @@ class Champs::CarteController < ApplicationController def update champ = policy_scope(Champ).find(params[:champ_id]) geo_area = champ.geo_areas.selections_utilisateur.find(params[:id]) - save_geometry!(geo_area, params_feature) + save_feature!(geo_area, params_feature) head :no_content end @@ -43,7 +43,7 @@ class Champs::CarteController < ApplicationController champ = policy_scope(Champ).find(params[:champ_id]) params_features.each do |feature| geo_area = champ.geo_areas.selections_utilisateur.new - save_geometry!(geo_area, feature) + save_feature!(geo_area, feature) end render json: champ.to_feature_collection, status: :created @@ -59,8 +59,13 @@ class Champs::CarteController < ApplicationController params[:features] end - def save_geometry!(geo_area, feature) - geo_area.geometry = feature[:geometry] + def save_feature!(geo_area, feature) + if feature[:geometry] + geo_area.geometry = feature[:geometry] + end + if feature[:properties] && feature[:properties][:description] + geo_area.description = feature[:properties][:description] + end geo_area.save! end diff --git a/app/javascript/components/MapEditor/index.js b/app/javascript/components/MapEditor/index.js index d98fbdc13..7d3921fc7 100644 --- a/app/javascript/components/MapEditor/index.js +++ b/app/javascript/components/MapEditor/index.js @@ -1,69 +1,102 @@ -import React, { useState, useRef, useEffect } from 'react'; +import React, { useState, useCallback, useRef } from 'react'; import PropTypes from 'prop-types'; import mapboxgl from 'mapbox-gl'; import ReactMapboxGl, { GeoJSONLayer, ZoomControl } from 'react-mapbox-gl'; import DrawControl from 'react-mapbox-gl-draw'; -import SwitchMapStyle from './SwitchMapStyle'; -import SearchInput from './SearchInput'; -import { getJSON, ajax } from '@utils'; import { gpx, kml } from '@tmcw/togeojson/dist/togeojson.es.js'; -import ortho from '../MapStyles/ortho.json'; -import orthoCadastre from '../MapStyles/orthoCadastre.json'; -import vector from '../MapStyles/vector.json'; -import vectorCadastre from '../MapStyles/vectorCadastre.json'; -import { polygonCadastresFill, polygonCadastresLine } from './utils'; import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css'; +import { getJSON, ajax, fire } from '@utils'; + +import SwitchMapStyle from './SwitchMapStyle'; +import { getMapStyle } from '../MapStyles'; + +import SearchInput from './SearchInput'; +import { polygonCadastresFill, polygonCadastresLine } from './utils'; +import { + noop, + filterFeatureCollection, + fitBounds, + generateId, + useEvent, + findFeature +} from '../shared/map'; + const Map = ReactMapboxGl({}); -function filterFeatureCollection(featureCollection, source) { - return { - type: 'FeatureCollection', - features: featureCollection.features.filter( - (feature) => feature.properties.source === source - ) - }; -} - -function noop() {} - function MapEditor({ featureCollection, url, preview, hasCadastres }) { const drawControl = useRef(null); + const [currentMap, setCurrentMap] = useState(null); + const [style, setStyle] = useState('ortho'); const [coords, setCoords] = useState([1.7, 46.9]); const [zoom, setZoom] = useState([5]); - const [currentMap, setCurrentMap] = useState({}); const [bbox, setBbox] = useState(featureCollection.bbox); const [importInputs, setImportInputs] = useState([]); - let mapStyle = style === 'ortho' ? ortho : vector; + const [cadastresFeatureCollection, setCadastresFeatureCollection] = useState( + filterFeatureCollection(featureCollection, 'cadastre') + ); + const mapStyle = getMapStyle(style, hasCadastres); - if (hasCadastres) { - mapStyle = style === 'ortho' ? orthoCadastre : vectorCadastre; - } - - const cadastresFeatureCollection = filterFeatureCollection( - featureCollection, - 'cadastre' + 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] ); - function updateFeaturesList(features) { - const cadastres = features.find( - ({ geometry }) => geometry.type === 'Polygon' + 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') ); - ajax({ url, type: 'get', data: cadastres ? 'cadastres=update' : '' }); - } + }, []); + + 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); } - const generateId = () => Math.random().toString(20).substr(2, 6); - - const updateImportInputs = (inputs, inputId) => { + function updateImportInputs(inputs, inputId) { const updatedInputs = inputs.filter((input) => input.id !== inputId); setImportInputs(updatedInputs); - }; + } async function onDrawCreate({ features }) { for (const feature of features) { @@ -92,23 +125,13 @@ function MapEditor({ featureCollection, url, preview, hasCadastres }) { updateFeaturesList(features); } - const onMapLoad = (map) => { + function onMapLoad(map) { setCurrentMap(map); drawControl.current.draw.set( filterFeatureCollection(featureCollection, 'selection_utilisateur') ); - }; - - const onCadastresUpdate = (evt) => { - if (currentMap) { - currentMap - .getSource('cadastres-layer') - .setData( - filterFeatureCollection(evt.detail.featureCollection, 'cadastre') - ); - } - }; + } const onFileImport = (e, inputId) => { const isGpxFile = e.target.files[0].name.includes('.gpx'); @@ -190,11 +213,6 @@ function MapEditor({ featureCollection, url, preview, hasCadastres }) { updateImportInputs(inputs, inputId); }; - useEffect(() => { - addEventListener('cadastres:update', onCadastresUpdate); - return () => removeEventListener('cadastres:update', onCadastresUpdate); - }); - if (!mapboxgl.supported()) { return (
diff --git a/app/javascript/components/MapReader/index.js b/app/javascript/components/MapReader/index.js index c2d4e7d1c..145007b0a 100644 --- a/app/javascript/components/MapReader/index.js +++ b/app/javascript/components/MapReader/index.js @@ -1,25 +1,100 @@ -import React, { useState } from 'react'; +import React, { useState, useCallback, useMemo } from 'react'; import ReactMapboxGl, { ZoomControl, GeoJSONLayer } from 'react-mapbox-gl'; -import mapboxgl from 'mapbox-gl'; -import SwitchMapStyle from './SwitchMapStyle'; -import ortho from '../MapStyles/ortho.json'; -import orthoCadastre from '../MapStyles/orthoCadastre.json'; -import vector from '../MapStyles/vector.json'; -import vectorCadastre from '../MapStyles/vectorCadastre.json'; +import mapboxgl, { Popup } from 'mapbox-gl'; import PropTypes from 'prop-types'; +import SwitchMapStyle from './SwitchMapStyle'; +import { getMapStyle } from '../MapStyles'; + +import { + filterFeatureCollection, + filterFeatureCollectionByGeometryType, + useEvent, + findFeature, + fitBounds, + getCenter +} from '../shared/map'; + const Map = ReactMapboxGl({}); const MapReader = ({ featureCollection }) => { + const [currentMap, setCurrentMap] = useState(null); const [style, setStyle] = useState('ortho'); - const hasCadastres = featureCollection.features.find( - (feature) => feature.properties.source === 'cadastre' + 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 mapStyle = useMemo( + () => getMapStyle(style, cadastresFeatureCollection.length), + [style, cadastresFeatureCollection] + ); + const popup = useMemo( + () => + new Popup({ + closeButton: false, + closeOnClick: false + }) ); - let mapStyle = style === 'ortho' ? ortho : vector; - if (hasCadastres) { - mapStyle = style === 'ortho' ? orthoCadastre : vectorCadastre; - } + 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 = [ @@ -27,26 +102,6 @@ const MapReader = ({ featureCollection }) => { [b1, b2] ]; - const cadastresFeatureCollection = { - type: 'FeatureCollection', - features: [] - }; - - const selectionsLineFeatureCollection = { - type: 'FeatureCollection', - features: [] - }; - - const selectionsPolygonFeatureCollection = { - type: 'FeatureCollection', - features: [] - }; - - const selectionsPointFeatureCollection = { - type: 'FeatureCollection', - features: [] - }; - const polygonSelectionFill = { 'fill-color': '#EC3323', 'fill-opacity': 0.5 @@ -77,25 +132,8 @@ const MapReader = ({ featureCollection }) => { 'line-dasharray': [1, 1] }; - for (let feature of featureCollection.features) { - switch (feature.properties.source) { - case 'selection_utilisateur': - switch (feature.geometry.type) { - case 'LineString': - selectionsLineFeatureCollection.features.push(feature); - break; - case 'Polygon': - selectionsPolygonFeatureCollection.features.push(feature); - break; - case 'Point': - selectionsPointFeatureCollection.features.push(feature); - break; - } - break; - case 'cadastre': - cadastresFeatureCollection.features.push(feature); - break; - } + function onMapLoad(map) { + setCurrentMap(map); } if (!mapboxgl.supported()) { @@ -110,6 +148,7 @@ const MapReader = ({ featureCollection }) => { return (