diff --git a/frontend/cache.js b/frontend/cache.js index 67a4685b5..aeeee869a 100644 --- a/frontend/cache.js +++ b/frontend/cache.js @@ -37,6 +37,7 @@ module.exports = [ && !url.host.includes('stamen-tiles') && !url.host.includes('wxs.ign.fr') && !url.host.includes('data.geopf.fr') + && !url.pathname.startsWith('/api/hdviewpoint/drf/hdviewpoints/') && request.destination === 'image' }, handler: 'NetworkFirst', diff --git a/frontend/config/details.json b/frontend/config/details.json index 22978b7e9..b143aa80c 100644 --- a/frontend/config/details.json +++ b/frontend/config/details.json @@ -7,6 +7,12 @@ "anchor": true, "order": 10 }, + { + "name": "medias", + "display": true, + "anchor": false, + "order": 15 + }, { "name": "itinerancySteps", "display": true, @@ -170,6 +176,12 @@ "anchor": true, "order": 10 }, + { + "name": "medias", + "display": true, + "anchor": false, + "order": 15 + }, { "name": "poi", "display": true, diff --git a/frontend/package.json b/frontend/package.json index dc74bb529..689e7f5be 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,6 +54,7 @@ "html-react-parser": "^2.0.0", "leaflet": "^1.7.1", "leaflet-boundary-canvas": "^1.0.0", + "leaflet-rastercoords": "^1.0.5", "leaflet.locatecontrol": "0.74.0", "leaflet.offline": "^3.0.1", "next": "^13.1.6", diff --git a/frontend/src/components/BackToMapButton/BackToMapButton.tsx b/frontend/src/components/BackToMapButton/BackToMapButton.tsx new file mode 100644 index 000000000..5e8ed2d18 --- /dev/null +++ b/frontend/src/components/BackToMapButton/BackToMapButton.tsx @@ -0,0 +1,34 @@ +import { useCallback } from 'react'; +import { Map } from 'components/Icons/Map'; +import { FormattedMessage } from 'react-intl'; +import { cn } from 'services/utils/cn'; + +export const BackToMapButton: React.FC< + React.ButtonHTMLAttributes & { + displayMap?: () => void; + setMapId?: (key: string) => void; + } +> = ({ displayMap, setMapId, ...nativeButtonProps }) => { + const handleClick = useCallback(() => { + displayMap?.(); + setMapId?.('default'); + }, [displayMap, setMapId]); + + return ( + + ); +}; diff --git a/frontend/src/components/BackToMapButton/index.ts b/frontend/src/components/BackToMapButton/index.ts new file mode 100644 index 000000000..0449c4f26 --- /dev/null +++ b/frontend/src/components/BackToMapButton/index.ts @@ -0,0 +1 @@ +export { BackToMapButton } from './BackToMapButton'; diff --git a/frontend/src/components/Icons/ViewPoint/index.tsx b/frontend/src/components/Icons/ViewPoint/index.tsx new file mode 100644 index 000000000..1c70bde15 --- /dev/null +++ b/frontend/src/components/Icons/ViewPoint/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { GenericIconProps } from '../types'; + +export const ViewPoint: React.FC = ({ + color = 'currentColor', + opacity, + className, + size = 24, +}) => { + return ( + + + + ); +}; diff --git a/frontend/src/components/Map/DetailsMap/DetailsMap.tsx b/frontend/src/components/Map/DetailsMap/DetailsMap.tsx index 44c6c2607..34bd4a797 100644 --- a/frontend/src/components/Map/DetailsMap/DetailsMap.tsx +++ b/frontend/src/components/Map/DetailsMap/DetailsMap.tsx @@ -1,9 +1,8 @@ import { GeometryList } from 'components/Map/DetailsMap/GeometryList'; import { LatLngBoundsExpression } from 'leaflet'; -import React, { useContext, useEffect } from 'react'; +import { useContext, useEffect } from 'react'; import { MapContainer, ScaleControl } from 'react-leaflet'; import 'leaflet/dist/leaflet.css'; -import styled, { css } from 'styled-components'; import { ArrowLeft } from 'components/Icons/ArrowLeft'; @@ -28,6 +27,8 @@ import { InformationDesk } from 'modules/informationDesk/interface'; import { SignageDictionary } from 'modules/signage/interface'; import { InfrastructureDictionary } from 'modules/infrastructure/interface'; import { cn } from 'services/utils/cn'; +import { ViewPoint } from 'modules/viewPoint/interface'; +import { BackToMapButton } from 'components/BackToMapButton'; import { BackButton } from '../components/BackButton'; import { TrekMarkersAndCourse } from './TrekMarkersAndCourse'; @@ -40,6 +41,9 @@ import DetailsMapDrawer from '../components/DetailsMapDrawer'; import { ResetView } from '../components/ResetView'; import TileLayerManager from '../components/TileLayerManager'; import FullscreenControl from '../components/FullScreenControl'; +import ViewPointHD from '../components/ViewPointHD'; +import { CRSPixel } from '../components/ViewPointHD/CRSPixel'; +import { AnnotationList } from '../components/ViewPointHD/AnnotationList'; export interface GeometryListProps { geometry: @@ -56,6 +60,7 @@ export interface GeometryListProps { } export type PropsType = { + mapId?: string; access?: any; experiences?: any; courses?: any; @@ -86,6 +91,9 @@ export type PropsType = { signage?: SignageDictionary | null; service?: PointWithIcon[]; infrastructure?: InfrastructureDictionary | null; + viewPoints?: ViewPoint[]; + displayMap?: () => void; + setMapId?: (id: string) => void; }; export const DetailsMap: React.FC = props => { const { reportVisibility, setReportVisibility } = useDetailsAndMapContext(); @@ -118,6 +126,10 @@ export const DetailsMap: React.FC = props => { toggleServiceVisibility, infrastructureVisibility, toggleInfrastructureVisibility, + viewPointVisibility, + toggleViewPointVisibility, + annotationViewpointVisibility, + toggleAnnotationViewpointVisibility, } = useDetailsMap(); const mapConfig = getMapConfig(); @@ -128,7 +140,9 @@ export const DetailsMap: React.FC = props => { [props.bbox.corner2.y, props.bbox.corner2.x], ]; - const { map, setMapInstance } = useTileLayer(props.trekId, bounds); + const mapToDisplay = props.viewPoints?.find(({ id }) => id === props.mapId) ?? 'default'; + + const { map, setMapInstance } = useTileLayer(props.trekId, bounds, mapToDisplay); useEffect(() => { if (map && center) { @@ -138,6 +152,12 @@ export const DetailsMap: React.FC = props => { const { visibleSection } = useContext(VisibleSectionContext); + useEffect(() => { + if (visibleSection === 'report' && reportVisibility) { + props.setMapId?.('default'); + } + }, [visibleSection, props.setMapId]); + const hasTitle = Boolean(props.title); return ( @@ -150,7 +170,11 @@ export const DetailsMap: React.FC = props => { )} > = props => { zoomControl={props.hasZoomControl} whenCreated={setMapInstance} bounds={bounds} + {...(mapToDisplay !== 'default' && { crs: CRSPixel(mapToDisplay) })} > - {reportVisibility && coordinatesReportTouched ? ( } onClick={hideMap}> @@ -173,103 +197,134 @@ export const DetailsMap: React.FC = props => { } onClick={hideMap} /> )} {props.hasZoomControl && } - - - 0 - ? trekChildrenMobileVisibility - : null - } - poiVisibility={props.poiPoints && props.poiPoints.length > 0 ? poiMobileVisibility : null} - referencePointsVisibility={ - props.pointsReference && props.pointsReference.length > 0 - ? referencePointsMobileVisibility - : null - } - touristicContentVisibility={ - props.touristicContentPoints && props.touristicContentPoints.length > 0 - ? touristicContentMobileVisibility - : null - } - informationDeskMobileVisibility={ - props.informationDesks && - props.informationDesks.some(({ longitude, latitude }) => longitude && latitude) - ? informationDeskMobileVisibility - : null - } - coursesVisibility={ - Boolean(props.courses) && props.courses.length > 0 ? coursesVisibility : null - } - experiencesVisibility={ - Boolean(props.experiences) && props.experiences.length > 0 - ? experiencesVisibility - : null - } - signageVisibility={props.signage ? signageVisibility : null} - serviceVisibility={props.service && props.service.length > 0 ? serviceVisibility : null} - infrastructureVisibility={props.infrastructure ? infrastructureVisibility : null} - toggleTrekChildrenVisibility={toggleTrekChildrenVisibility} - togglePoiVisibility={togglePoiVisibility} - toggleReferencePointsVisibility={toggleReferencePointsVisibility} - toggleTouristicContentVisibility={toggleTouristicContentVisibility} - toggleInformationDeskVisibility={toggleInformationDeskVisibility} - toggleCoursesVisibility={toggleCoursesVisibility} - toggleExperiencesVisibility={toggleExperiencesVisibility} - toggleSignageVisibility={toggleSignageVisibility} - toggleServiceVisibility={toggleServiceVisibility} - toggleInfrastructureVisibility={toggleInfrastructureVisibility} - /> - {props.trekGeometry && ( - - )} - {props.outdoorGeometry && } - {props.eventGeometry && ( - + {mapToDisplay !== 'default' && ( + <> + + {'type' in mapToDisplay.annotations && annotationViewpointVisibility && ( + <> + + {annotationViewpointVisibility === 'DISPLAYED' && ( + + )} + + )} + + )} - - {props.displayAltimetricProfile === true && props.trekGeoJSON && ( - - )} - {props.title !== undefined && ( -
- + + + + 0 + ? trekChildrenMobileVisibility + : null + } + poiVisibility={ + props.poiPoints && props.poiPoints.length > 0 ? poiMobileVisibility : null + } + referencePointsVisibility={ + props.pointsReference && props.pointsReference.length > 0 + ? referencePointsMobileVisibility + : null + } + touristicContentVisibility={ + props.touristicContentPoints && props.touristicContentPoints.length > 0 + ? touristicContentMobileVisibility + : null + } + informationDeskMobileVisibility={ + props.informationDesks && + props.informationDesks.some(({ longitude, latitude }) => longitude && latitude) + ? informationDeskMobileVisibility + : null + } + coursesVisibility={ + Boolean(props.courses) && props.courses.length > 0 ? coursesVisibility : null + } + experiencesVisibility={ + Boolean(props.experiences) && props.experiences.length > 0 + ? experiencesVisibility + : null + } + signageVisibility={props.signage ? signageVisibility : null} + serviceVisibility={ + props.service && props.service.length > 0 ? serviceVisibility : null + } + infrastructureVisibility={props.infrastructure ? infrastructureVisibility : null} + viewPointVisibility={props.viewPoints ? viewPointVisibility : null} + toggleTrekChildrenVisibility={toggleTrekChildrenVisibility} + togglePoiVisibility={togglePoiVisibility} + toggleReferencePointsVisibility={toggleReferencePointsVisibility} + toggleTouristicContentVisibility={toggleTouristicContentVisibility} + toggleInformationDeskVisibility={toggleInformationDeskVisibility} + toggleCoursesVisibility={toggleCoursesVisibility} + toggleExperiencesVisibility={toggleExperiencesVisibility} + toggleSignageVisibility={toggleSignageVisibility} + toggleServiceVisibility={toggleServiceVisibility} + toggleInfrastructureVisibility={toggleInfrastructureVisibility} + toggleViewPointVisiblity={toggleViewPointVisibility} + /> + {props.trekGeometry && ( + + )} + {props.outdoorGeometry && } + {props.eventGeometry && ( + + )} + -
+ {props.displayAltimetricProfile === true && props.trekGeoJSON && ( + + )} + {props.title !== undefined && ( +
+ +
+ )} + )}
diff --git a/frontend/src/components/Map/DetailsMap/MapChildren.tsx b/frontend/src/components/Map/DetailsMap/MapChildren.tsx index cb67f46f3..4c5889a2f 100644 --- a/frontend/src/components/Map/DetailsMap/MapChildren.tsx +++ b/frontend/src/components/Map/DetailsMap/MapChildren.tsx @@ -6,9 +6,10 @@ import { OutdoorSite } from 'modules/outdoorSite/interface'; import { SensitiveAreaGeometry } from 'modules/sensitiveArea/interface'; import { SignageDictionary } from 'modules/signage/interface'; import { InfrastructureDictionary } from 'modules/infrastructure/interface'; -import React, { useContext } from 'react'; +import { useContext } from 'react'; import { useMediaPredicate } from 'react-media-hook'; import { Infrastructure } from 'components/Icons/Infrastructure'; +import { ViewPoint } from 'modules/viewPoint/interface'; import { GeometryListProps } from './DetailsMap'; import { MarkersWithIcon } from './MarkersWithIcon'; @@ -20,6 +21,7 @@ import { SensitiveAreas } from './SensitiveAreas'; import { GeometryList } from './GeometryList'; import { TrekChildren } from './TrekChildren'; import { Visibility } from './useDetailsMap'; +import ViewPointMarkers from './ViewPointMarkers'; export interface PointWithIcon { location: { x: number; y: number }; @@ -52,6 +54,9 @@ type Props = { service?: PointWithIcon[]; infrastructureVisibility: Visibility; infrastructure?: InfrastructureDictionary | null; + viewPointVisibility?: Visibility; + viewPoints?: ViewPoint[]; + setMapId?: (id: string) => void; }; export const MapChildren: React.FC = props => { @@ -122,6 +127,10 @@ export const MapChildren: React.FC = props => { {props.serviceVisibility === 'DISPLAYED' && } {(isMobile || visibleSection === 'report') && props.reportVisibility && } + + {(props.viewPointVisibility === 'DISPLAYED' || visibleSection === 'medias') && ( + + )} ); }; diff --git a/frontend/src/components/Map/DetailsMap/ViewPointMarkers.tsx b/frontend/src/components/Map/DetailsMap/ViewPointMarkers.tsx new file mode 100644 index 000000000..457e0f642 --- /dev/null +++ b/frontend/src/components/Map/DetailsMap/ViewPointMarkers.tsx @@ -0,0 +1,43 @@ +import { ViewPoint } from 'modules/viewPoint/interface'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { ViewPoint as ViewPointIcon } from 'components/Icons/ViewPoint'; +import { ClickableMarker } from 'components/Map/components/ClickableMarker'; +import { useIntl } from 'react-intl'; + +interface ViewPointMarkersProps { + viewPoints?: ViewPoint[]; + setMapId?: (id: string) => void; +} + +export const ViewPointMarkers = ({ viewPoints, setMapId }: ViewPointMarkersProps) => { + const intl = useIntl(); + + if (!viewPoints?.length) { + return null; + } + + const points = viewPoints + .filter(({ geometry }) => geometry !== null) + .map(({ id, geometry, title, thumbnailUrl }) => ({ + id: `DETAILS-VIEWPOINT-${id}`, + // @ts-ignore geometry cannot be null because it's filtered above + location: { x: geometry.coordinates[0], y: geometry.coordinates[1] }, + name: title, + pictogramUri: renderToStaticMarkup(), + content: { + imgUrl: thumbnailUrl, + place: intl.formatMessage({ id: 'viewPoint.label' }), + title: title ?? '', + button: { + onClick: () => { + setMapId?.(id); + }, + label: 'viewPoint.displayPicture', + }, + }, + })); + + return ; +}; + +export default ViewPointMarkers; diff --git a/frontend/src/components/Map/DetailsMap/useDetailsMap.tsx b/frontend/src/components/Map/DetailsMap/useDetailsMap.tsx index cfd0cd6f9..3d26b73ba 100644 --- a/frontend/src/components/Map/DetailsMap/useDetailsMap.tsx +++ b/frontend/src/components/Map/DetailsMap/useDetailsMap.tsx @@ -21,6 +21,9 @@ export const useDetailsMap = () => { const [signageVisibility, setSignageVisibility] = useState('HIDDEN'); const [serviceVisibility, setServiceVisibility] = useState('HIDDEN'); const [infrastructureVisibility, setInfrastructureVisibility] = useState('HIDDEN'); + const [viewPointVisibility, setViewPointVisibility] = useState('HIDDEN'); + const [annotationViewpointVisibility, setAnnotationViewpointVisibility] = + useState('DISPLAYED'); const toggleTrekChildrenVisibility = () => setTrekChildrenVisibility(toggleVisibility); @@ -37,6 +40,9 @@ export const useDetailsMap = () => { const toggleSignageVisibility = () => setSignageVisibility(toggleVisibility); const toggleServiceVisibility = () => setServiceVisibility(toggleVisibility); const toggleInfrastructureVisibility = () => setInfrastructureVisibility(toggleVisibility); + const toggleViewPointVisibility = () => setViewPointVisibility(toggleVisibility); + const toggleAnnotationViewpointVisibility = () => + setAnnotationViewpointVisibility(toggleVisibility); return { trekChildrenMobileVisibility, @@ -59,5 +65,9 @@ export const useDetailsMap = () => { toggleServiceVisibility, infrastructureVisibility, toggleInfrastructureVisibility, + viewPointVisibility, + toggleViewPointVisibility, + annotationViewpointVisibility, + toggleAnnotationViewpointVisibility, }; }; diff --git a/frontend/src/components/Map/components/ClickableMarker/ClickableMarker.tsx b/frontend/src/components/Map/components/ClickableMarker/ClickableMarker.tsx new file mode 100644 index 000000000..570582663 --- /dev/null +++ b/frontend/src/components/Map/components/ClickableMarker/ClickableMarker.tsx @@ -0,0 +1,36 @@ +import { HoverableMarker } from 'components/Map/components/HoverableMarker'; +import { Popup } from 'components/Map/components/Popup'; +import { PopupResult } from 'modules/trekResult/interface'; + +interface ClickableMarkerProps { + points: { + location: { x: number; y: number }; + pictogramUri: string; + id: string; + content: PopupResult; + type?: 'TREK' | 'TOURISTIC_CONTENT' | 'OUTDOOR_SITE' | 'TOURISTIC_EVENT' | null; + }[]; +} + +export const ClickableMarker = ({ points, ...props }: ClickableMarkerProps) => { + if (!points?.length) { + return null; + } + + return ( + <> + {points.map(point => ( + + + + ))} + + ); +}; + +export default ClickableMarker; diff --git a/frontend/src/components/Map/components/ClickableMarker/index.tsx b/frontend/src/components/Map/components/ClickableMarker/index.tsx new file mode 100644 index 000000000..68ab31932 --- /dev/null +++ b/frontend/src/components/Map/components/ClickableMarker/index.tsx @@ -0,0 +1 @@ +export { ClickableMarker } from './ClickableMarker'; diff --git a/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx b/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx index 9f6e02d28..252441707 100644 --- a/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx +++ b/frontend/src/components/Map/components/ControlSection/ControlPanel/index.tsx @@ -2,6 +2,7 @@ import { Florist } from 'components/Icons/Florist'; import { Signage } from 'components/Icons/Signage'; import { Infrastructure } from 'components/Icons/Infrastructure'; import { MapPin } from 'components/Icons/MapPin'; +import { ViewPoint } from 'components/Icons/ViewPoint'; import { Line } from './Line'; import IconLocation from './IconLocation'; import IconInfo from './IconInfo'; @@ -33,6 +34,10 @@ export const ControlPanel: React.FC = ({ toggleServiceVisibility, infrastructureVisibility, toggleInfrastructureVisibility, + annotationViewpointVisibility, + toggleAnnotationViewpointVisibility, + viewPointVisibility, + toggleViewPointVisiblity, }) => { return (
@@ -116,6 +121,22 @@ export const ControlPanel: React.FC = ({ transKey="search.map.panel.service" /> )} + {viewPointVisibility && toggleViewPointVisiblity && ( + + )} + {annotationViewpointVisibility && toggleAnnotationViewpointVisibility && ( + + )}
); }; diff --git a/frontend/src/components/Map/components/ControlSection/ControlSection.tsx b/frontend/src/components/Map/components/ControlSection/ControlSection.tsx index bd5925591..710e03fd9 100644 --- a/frontend/src/components/Map/components/ControlSection/ControlSection.tsx +++ b/frontend/src/components/Map/components/ControlSection/ControlSection.tsx @@ -30,6 +30,10 @@ export interface ControlSectionProps { toggleServiceVisibility?: () => void; infrastructureVisibility?: Visibility; toggleInfrastructureVisibility?: () => void; + annotationViewpointVisibility?: Visibility; + toggleAnnotationViewpointVisibility?: () => void; + viewPointVisibility?: Visibility; + toggleViewPointVisiblity?: () => void; className?: string; position?: ControlPosition; } diff --git a/frontend/src/components/Map/components/ViewPointHD/AnnotationItem.tsx b/frontend/src/components/Map/components/ViewPointHD/AnnotationItem.tsx new file mode 100644 index 000000000..d2a794587 --- /dev/null +++ b/frontend/src/components/Map/components/ViewPointHD/AnnotationItem.tsx @@ -0,0 +1,118 @@ +import L from 'leaflet'; +import { GeoJsonProperties, Geometry } from 'geojson'; +import { Circle, CircleMarker, Polygon, Polyline, Tooltip, useMap } from 'react-leaflet'; + +type Props = { + geometry: Geometry; + properties: GeoJsonProperties; + id: string; +}; + +const MetaData = ({ properties }: { properties: GeoJsonProperties }) => { + if (properties === null || !properties.name) { + return null; + } + return ( + + {properties.name} + + ); +}; + +export const AnnotationItem = ({ geometry, properties, id }: Props) => { + const map = useMap(); + if (geometry.type === 'GeometryCollection') { + return ( + <> + {geometry.geometries.map((geom, index) => ( + + ))} + + ); + } + + if (geometry.type === 'Point' || geometry.type === 'MultiPoint') { + const coordinatesAsMultiPoint = + geometry.type === 'Point' ? [geometry.coordinates] : geometry.coordinates; + return ( + <> + {coordinatesAsMultiPoint.map((coordinates, index) => { + const [lat, lng] = coordinates; + return ( + + + + ); + })} + + ); + } + + if (geometry.type === 'LineString' || geometry.type === 'MultiLineString') { + const coordinatesAsMultiLineString = + geometry.type === 'LineString' ? [geometry.coordinates] : geometry.coordinates; + + return ( + <> + {coordinatesAsMultiLineString.map((group, index) => { + return ( + [lng, lat])} + > + + + ); + })} + + ); + } + + if (geometry.type === 'Polygon' || geometry.type === 'MultiPolygon') { + const coordinatesAsMultiPolygon = + geometry.type === 'Polygon' ? [geometry.coordinates] : geometry.coordinates; + + // Circle + if (properties?.annotationType === 'circle') { + const LPolygon = L.geoJSON(geometry); + const center = LPolygon.getBounds().getCenter(); + const [lat, lng] = geometry.coordinates[0][0] as [number, number]; + const diagonal = map.distance(center, [lng, lat]); + const radius = Math.sqrt(Math.pow(diagonal, 2) / 2); + + return ( + + + + ); + } + + // Square, Rectangle, Polygon + return ( + <> + {coordinatesAsMultiPolygon.map((group, index) => ( + line.map<[number, number]>(([lat, lng]) => [lng, lat]))} + > + + + ))} + + ); + } + + return null; +}; diff --git a/frontend/src/components/Map/components/ViewPointHD/AnnotationList.tsx b/frontend/src/components/Map/components/ViewPointHD/AnnotationList.tsx new file mode 100644 index 000000000..ebe596c4f --- /dev/null +++ b/frontend/src/components/Map/components/ViewPointHD/AnnotationList.tsx @@ -0,0 +1,23 @@ +import { GeoJsonProperties, Geometry } from 'geojson'; +import { AnnotationItem } from './AnnotationItem'; + +export type PropsType = { + contents?: { + geometry: Geometry; + properties: GeoJsonProperties; + }[]; +}; + +export const AnnotationList = ({ contents, ...props }: PropsType) => { + if (contents === undefined) { + return null; + } + + return ( + <> + {contents.map((contentProps, index) => ( + + ))} + + ); +}; diff --git a/frontend/src/components/Map/components/ViewPointHD/CRSPixel.ts b/frontend/src/components/Map/components/ViewPointHD/CRSPixel.ts new file mode 100644 index 000000000..6530c2a49 --- /dev/null +++ b/frontend/src/components/Map/components/ViewPointHD/CRSPixel.ts @@ -0,0 +1,18 @@ +import L from 'leaflet'; +import { ViewPoint } from 'modules/viewPoint/interface'; + +export const CRSPixel = ({ metadata }: ViewPoint) => { + if (metadata === null) { + return L.CRS.Simple; + } + const { sizeX, sizeY, tileWidth, levels } = metadata; + const step = 2 ** (levels - 1); + return L.Util.extend(L.CRS.Simple, { + transformation: new L.Transformation( + Math.min(step * tileWidth, sizeX) / step / sizeX, + 0, + Math.min(step * tileWidth, sizeY) / step / sizeY, + 0, + ), + }); +}; diff --git a/frontend/src/components/Map/components/ViewPointHD/index.tsx b/frontend/src/components/Map/components/ViewPointHD/index.tsx new file mode 100644 index 000000000..5aab0e82e --- /dev/null +++ b/frontend/src/components/Map/components/ViewPointHD/index.tsx @@ -0,0 +1,43 @@ +import L from 'leaflet'; +import { ViewPoint } from 'modules/viewPoint/interface'; +import { useEffect } from 'react'; +import { useMap } from 'react-leaflet'; +import 'leaflet-rastercoords'; + +const ViewPointHD: React.FC = ({ pictureTilesUrl, metadata }) => { + const map = useMap(); + useEffect(() => { + if (!metadata || !pictureTilesUrl) { + return; + } + const { sizeX, sizeY, tileWidth } = metadata; + + const raster = new L.RasterCoords(map, [sizeX, sizeY], tileWidth); + if (map === undefined) { + return; + } + + const { offsetHeight, offsetWidth } = map.getContainer(); + const southWest = raster.unproject([0, sizeY - (sizeY - offsetHeight) / 2]); + const northEast = raster.unproject([sizeX - (sizeX - offsetWidth) / 2, 0]); + const bounds = new L.LatLngBounds(southWest, northEast); + + map.fitBounds(bounds); + map.setMaxZoom(raster.zoomLevel()); + + const layer = new L.TileLayer(pictureTilesUrl, { + noWrap: false, + bounds: raster.getMaxBounds(), + maxNativeZoom: raster.zoomLevel(), + }); + map.addLayer(layer); + return () => { + if (layer !== null) { + map.removeLayer(layer); + } + }; + }, [map, pictureTilesUrl, metadata]); + + return null; +}; +export default ViewPointHD; diff --git a/frontend/src/components/OpenMapButton/OpenMapButton.tsx b/frontend/src/components/OpenMapButton/OpenMapButton.tsx index 3c000165c..e325acbca 100644 --- a/frontend/src/components/OpenMapButton/OpenMapButton.tsx +++ b/frontend/src/components/OpenMapButton/OpenMapButton.tsx @@ -1,26 +1,39 @@ +import { useCallback } from 'react'; import { Map } from 'components/Icons/Map'; import { FormattedMessage } from 'react-intl'; import { useHideOnScrollDown } from 'hooks/useHideOnScrollDown'; -import { Button } from 'components/Button'; import { cn } from 'services/utils/cn'; export const OpenMapButton: React.FC< - React.ButtonHTMLAttributes & { displayMap: () => void } -> = ({ displayMap, ...nativeButtonProps }) => { + React.ButtonHTMLAttributes & { + displayMap: () => void; + setMapId?: (key: string) => void; + } +> = ({ displayMap, setMapId, ...nativeButtonProps }) => { const buttonDisplayState = useHideOnScrollDown(); + const handleClick = useCallback(() => { + displayMap(); + setMapId?.('default'); + }, [displayMap, setMapId]); + return ( - + + ); }; diff --git a/frontend/src/components/Report/Report.tsx b/frontend/src/components/Report/Report.tsx index 0dda86954..ac883b5d4 100644 --- a/frontend/src/components/Report/Report.tsx +++ b/frontend/src/components/Report/Report.tsx @@ -19,11 +19,12 @@ import CoordinatesRow from './CoordinatesRow'; interface Props { displayMobileMap?: () => void; + setMapId?: (str: string) => void; trekId: number; startPoint: PointGeometry; } -const Report: React.FC = ({ displayMobileMap, startPoint, trekId }) => { +const Report: React.FC = ({ displayMobileMap, setMapId, startPoint, trekId }) => { const { state, coordinatesReportTouched, @@ -53,6 +54,7 @@ const Report: React.FC = ({ displayMobileMap, startPoint, trekId }) => { const isMobile = useMediaPredicate('(max-width: 1024px)'); const handleReportButtonClick = () => { + setMapId?.('default'); if (isMobile) { displayMobileMap?.(); setReportVisibility(true); diff --git a/frontend/src/components/pages/details/Details.tsx b/frontend/src/components/pages/details/Details.tsx index a06a85666..9aa47f26b 100644 --- a/frontend/src/components/pages/details/Details.tsx +++ b/frontend/src/components/pages/details/Details.tsx @@ -10,7 +10,7 @@ import { OpenMapButton } from 'components/OpenMapButton'; import { useShowOnScrollPosition } from 'hooks/useShowOnScrollPosition'; import { useMediaPredicate } from 'react-media-hook'; import { sizes } from 'stylesheet'; -import React, { useMemo, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { TrekChildGeometry } from 'modules/details/interface'; import { cleanHTMLElementsFromString } from 'modules/utils/string'; import Report from 'components/Report/Report'; @@ -51,6 +51,7 @@ import { DetailsSensitiveArea } from './components/DetailsSensitiveArea'; import { useOnScreenSection } from './hooks/useHighlightedSection'; import { DetailsGear } from './components/DetailsGear'; import { useDetailsSections } from './useDetailsSections'; +import { DetailsMedias } from './components/DetailsMedias'; interface Props { slug: string | string[] | undefined; @@ -72,6 +73,8 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu displayMobileMap, hideMobileMap, sectionRef, + mapId, + setMapId, } = useDetails(slug, parentId, language); const isMobile = useMediaPredicate('(max-width: 1024px)'); @@ -105,6 +108,14 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu (getGlobalConfig().minAltitudeDifferenceToDisplayElevationProfile ?? 0) < higherDifferenceElevation; + const handleViewPointClick = useCallback( + (viewPointId: string) => { + setMapId(viewPointId); + displayMobileMap(); + }, + [displayMobileMap, setMapId], + ); + return useMemo( () => ( <> @@ -135,7 +146,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu id="details_informationContainer" className="flex flex-col w-full relative -top-detailsHeaderMobile desktop:top-0 desktop:w-3/5" > - +
{({ isFullscreen, toggleFullscreen }) => ( @@ -197,6 +208,26 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu ); } + if ( + hasNavigator && + section.name === 'medias' && + details.viewPoints.length > 0 + ) { + return ( +
+ + + +
+ ); + } if (section.name === 'itinerancySteps' && details.children.length > 0) { return (
= ({ slug, parentId, langu attachments: poi.attachments, iconUri: poi.type.pictogramUri, iconName: poi.type.label, + viewPoints: poi.viewPoints, }))} type="POI" + handleViewPointClick={handleViewPointClick} />
); @@ -477,6 +510,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu > = ({ slug, parentId, langu )} > = ({ slug, parentId, langu id: `DETAILS-SERVICE-${service.id}`, }))} infrastructure={details.infrastructure} + viewPoints={[ + ...details.viewPoints, + ...details.pois.flatMap(({ viewPoints = [] }) => viewPoints).filter(Boolean), + ]} + displayMap={displayMobileMap} + setMapId={setMapId} />
@@ -679,6 +720,7 @@ export const DetailsUIWithoutContext: React.FC = ({ slug, parentId, langu refetch, sectionsReferences, trekFamily, + mapId, ], ); }; diff --git a/frontend/src/components/pages/details/__tests__/Details.test.tsx b/frontend/src/components/pages/details/__tests__/Details.test.tsx index d1306c429..ce197d9ec 100644 --- a/frontend/src/components/pages/details/__tests__/Details.test.tsx +++ b/frontend/src/components/pages/details/__tests__/Details.test.tsx @@ -48,7 +48,7 @@ describe('Details', () => { .query({ language: 'fr', fields: - 'id,name,departure,arrival,cities,attachments,practice,public_transport,access,advised_parking,description_teaser,ambiance,themes,duration,length_2d,ascent,descent,difficulty,route,networks,description,geometry,parking_location,pdf,gpx,kml,departure_city,disabled_infrastructure,accessibilities,source,information_desks,labels,advice,gear,points_reference,children,web_links,elevation_area_url,altimetric_profile,reservation_id,accessibility_signage,accessibility_slope,accessibility_width,accessibility_covering,accessibility_exposure,accessibility_advice,attachments_accessibility,accessibility_level,ratings,ratings_description', + 'id,name,departure,arrival,cities,attachments,practice,public_transport,access,advised_parking,description_teaser,ambiance,themes,duration,length_2d,ascent,descent,difficulty,route,networks,description,geometry,parking_location,pdf,gpx,kml,departure_city,disabled_infrastructure,accessibilities,source,information_desks,labels,advice,gear,points_reference,children,web_links,elevation_area_url,altimetric_profile,reservation_id,accessibility_signage,accessibility_slope,accessibility_width,accessibility_covering,accessibility_exposure,accessibility_advice,attachments_accessibility,accessibility_level,ratings,ratings_description,view_points', format: 'geojson', }) .reply(200, rawDetailsMock); diff --git a/frontend/src/components/pages/details/components/DetailsCard/DetailsCard.tsx b/frontend/src/components/pages/details/components/DetailsCard/DetailsCard.tsx index 3744d4942..fcac142e1 100644 --- a/frontend/src/components/pages/details/components/DetailsCard/DetailsCard.tsx +++ b/frontend/src/components/pages/details/components/DetailsCard/DetailsCard.tsx @@ -8,12 +8,13 @@ import useHasMounted from 'hooks/useHasMounted'; import parse from 'html-react-parser'; import { useListAndMapContext } from 'modules/map/ListAndMapContext'; import { FormattedMessage } from 'react-intl'; -import styled from 'styled-components'; -import { MAX_WIDTH_MOBILE } from 'stylesheet'; import { cn } from 'services/utils/cn'; import { Arrow } from 'components/Icons/Arrow'; +import { ViewPoint } from 'modules/viewPoint/interface'; import { Attachment } from '../../../../../modules/interface'; import { useDetailsCard } from './useDetailsCard'; +import { DetailsMedias } from '../DetailsMedias'; + export interface DetailsCardProps { id: string; name: string; @@ -26,6 +27,8 @@ export interface DetailsCardProps { className?: string; redirectionUrl?: string; type?: string; + viewPoints?: ViewPoint[]; + handleViewPointClick?: (id: string) => void; } export const DetailsCard: React.FC = ({ @@ -40,8 +43,11 @@ export const DetailsCard: React.FC = ({ className = '', redirectionUrl, type, + handleViewPointClick, + viewPoints, }) => { - const { truncateState, toggleTruncateState, heightState, detailsCardRef } = useDetailsCard(); + const hasMedia = Boolean(viewPoints?.length); + const { truncateState, toggleTruncateState, detailsCardRef } = useDetailsCard(hasMedia); const descriptionStyled = truncateState === 'TRUNCATE' ? ( @@ -54,97 +60,111 @@ export const DetailsCard: React.FC = ({ const { setHoveredCardId } = useListAndMapContext(); const hasNavigator = useHasMounted(typeof navigator !== 'undefined' && navigator.onLine); - return ( - { - setHoveredCardId(id); - }} - onMouseLeave={() => { - setHoveredCardId(null); - }} > -
-
- - {({ isFullscreen, toggleFullscreen }) => ( - <> - {type === 'TOURISTIC_CONTENT' && - redirectionUrl && - attachments.length > 0 && - hasNavigator && ( +
{ + setHoveredCardId(id); + }} + onMouseLeave={() => { + setHoveredCardId(null); + }} + > +
+
+ + {({ isFullscreen, toggleFullscreen }) => ( + <> + {type === 'TOURISTIC_CONTENT' && + redirectionUrl && + attachments.length > 0 && + hasNavigator && ( + + )} + {type !== 'TOURISTIC_CONTENT' && attachments.length > 0 && hasNavigator && ( )} - {type !== 'TOURISTIC_CONTENT' && attachments.length > 0 && hasNavigator && ( - - )} - - )} - - + + )} + + +
-
-
- {place && ( +
+ {place && ( + +

{place}

+
+ )} -

{place}

+

{name}

- )} - -

{name}

-
- {Boolean(description) && ( -
- {descriptionStyled} - {truncateState !== 'NONE' && ( - - )} -
- )} +
+ )} + {truncateState !== 'NONE' && ( + + )} + + )} +
- + ); }; @@ -160,9 +180,3 @@ const OptionalLink: React.FC = ({ redirectionUrl, children }) <>{children} ); }; - -const DetailsCardContainer = styled.li<{ height: number }>` - @media (min-width: ${MAX_WIDTH_MOBILE}px) { - height: ${props => props.height}px; - } -`; diff --git a/frontend/src/components/pages/details/components/DetailsCard/__tests__/__snapshots__/DetailsCard.test.tsx.snap b/frontend/src/components/pages/details/components/DetailsCard/__tests__/__snapshots__/DetailsCard.test.tsx.snap index 2ddf1ad3c..ba527d415 100644 --- a/frontend/src/components/pages/details/components/DetailsCard/__tests__/__snapshots__/DetailsCard.test.tsx.snap +++ b/frontend/src/components/pages/details/components/DetailsCard/__tests__/__snapshots__/DetailsCard.test.tsx.snap @@ -6,97 +6,95 @@ Object { "baseElement":
  • - -
    - Lorem ipsum -
    - +
    + - - Lorem ipsum - Lorem ipsum + + Lorem ipsum - Lorem ipsum + - - -
    -
    -
    + + Voir l'image en plein écran + + + + + +
    -
    -
    - Randonnée pédestre +
    + Randonnée pédestre +
    -
  • -
    -

    - Église St Louis -

    +

    + Église St Louis +

    @@ -110,218 +108,216 @@ Object {
  • - - - + + Voir l'image en plein écran + + + + +
    -
    - - -
  • -
  • - -
  • - + + +
  • + +
  • + +
    - -
    - Randonnée pédestre +
    + Randonnée pédestre +
    - -
    -

    - Église St Louis -

    +

    + Église St Louis +

    @@ -336,97 +332,95 @@ Object { , "container":
  • - -
    - Lorem ipsum -
    - +
    + - - Lorem ipsum - Lorem ipsum + + Lorem ipsum - Lorem ipsum + - - -
    -
    -
    + + Voir l'image en plein écran + + + + + +
    -
    -
    - Randonnée pédestre +
    + Randonnée pédestre +
    -
    -
    -
  • -
    -

    - Église St Louis -

    +
    +
    +

    + Église St Louis +

    @@ -498,97 +492,318 @@ Object { "baseElement":
  • - -
    - Lorem ipsum -
    - +
    + - - Lorem ipsum - Lorem ipsum + + Lorem ipsum - Lorem ipsum + - - -
    -
    -
    + + Voir l'image en plein écran + + + + + +
    -
    -
    - Randonnée pédestre +
    + Randonnée pédestre +
    +
    +

    + Église St Louis +

    +
    + + Pour espérer apercevoir cet oiseau, partir la nuit au printemps, parcourir un grand dénivelé afin d'arriver sur son terrain de prédilection à plus de 2000 m voire 3000 m d'altitude avant le lever du jour et là, entendre le chant guttural caractéristique qui trahit sa présence. Mais pour le voir, il faudra bien ouvrir les yeux ou se munir d'une paire de jumelles. Et alors là, quel bonheur ! Le lagopède alpin est l'espèce arctique par excellence, menacée entre autre par le réchauffement climatique. Il fait partie des espèces à protéger dans le cœur du Parc national des Ecrins. + +
    +
  • + +
    +
    +
  • -

    - Église St Louis -

    +
    +
    +
    +
    +
    + + + +
    +
    + + +
  • +
  • + +
  • + +
    +
    +
    +
    + +
    +
    + Randonnée pédestre +
    +
    + +
    +

    + Église St Louis +

    @@ -600,13 +815,16 @@ Object {
    -
    -
  • , + "container":
    +
  • +

    Église St Louis

    -
    -
    - - Pour espérer apercevoir cet oiseau, partir la nuit au printemps, parcourir un grand dénivelé afin d'arriver sur son terrain de prédilection à plus de 2000 m voire 3000 m d'altitude avant le lever du jour et là, entendre le chant guttural caractéristique qui trahit sa présence. Mais pour le voir, il faudra bien ouvrir les yeux ou se munir d'une paire de jumelles. Et alors là, quel bonheur ! Le lagopède alpin est l'espèce arctique par excellence, menacée entre autre par le réchauffement climatique. Il fait partie des espèces à protéger dans le cœur du Parc national des Ecrins. - -
    -
    -
    -
  • -
    - , - "container":
    -
  • -
    -
    -
    -
    -
    -
    - - - -
    -
    - - -
  • -
  • - -
  • - -
    - - - - -
    -
    - Randonnée pédestre -
    -
    - - -
    -

    - Église St Louis -

    -
    diff --git a/frontend/src/components/pages/details/components/DetailsCard/useDetailsCard.tsx b/frontend/src/components/pages/details/components/DetailsCard/useDetailsCard.tsx index 4f209e5e1..542ef04b6 100644 --- a/frontend/src/components/pages/details/components/DetailsCard/useDetailsCard.tsx +++ b/frontend/src/components/pages/details/components/DetailsCard/useDetailsCard.tsx @@ -2,63 +2,48 @@ import debounce from 'debounce'; import useIsomorphicLayoutEffect from 'hooks/useIsomorphicLayoutEffect'; import { useCallback, useEffect, useRef, useState } from 'react'; -const DETAILS_CARD_DEFAULT_HEIGHT = 200; +const DETAILS_CARD_DEFAULT_HEIGHT = 220; -export const useDetailsCard = () => { +export const useDetailsCard = (hasMedia = false) => { const detailsCardRef = useRef(null); - const [heightState, setHeightState] = useState(DETAILS_CARD_DEFAULT_HEIGHT); - const [truncateState, setTruncateState] = useState<'NONE' | 'TRUNCATE' | 'FULL'>( - () => 'TRUNCATE', - ); + const [truncateState, setTruncateState] = useState<'NONE' | 'TRUNCATE' | 'FULL'>('TRUNCATE'); const toggleTruncateState = () => setTruncateState(currentTruncateState => currentTruncateState === 'TRUNCATE' ? 'FULL' : 'TRUNCATE', ); - useEffect(() => { - if (truncateState === 'TRUNCATE') { - setHeightState(DETAILS_CARD_DEFAULT_HEIGHT); - } else { - const newHeight = detailsCardRef.current?.getBoundingClientRect().height; - if (newHeight !== undefined) { - setHeightState(Math.max(DETAILS_CARD_DEFAULT_HEIGHT, newHeight)); - } - } - }, [truncateState, setHeightState]); useEffect(() => { - if ( - detailsCardRef.current && - detailsCardRef.current.querySelector('.line-clamp-2')?.offsetHeight === - detailsCardRef.current.querySelector('.line-clamp-2')?.scrollHeight - ) { + const descriptionNode = detailsCardRef.current?.querySelector('.line-clamp-2'); + if (descriptionNode?.offsetHeight === descriptionNode?.scrollHeight && !hasMedia) { setTruncateState('NONE'); } - }, []); + }, [hasMedia]); const handleResize = useCallback( debounce( () => { setTruncateState(prevState => { - if (detailsCardRef.current === null) { + if (detailsCardRef.current === null || hasMedia) { return prevState; } + const descriptionNode = + detailsCardRef.current.querySelector('.line-clamp-2'); + if ( prevState === 'TRUNCATE' && - detailsCardRef.current.querySelector('.line-clamp-2')?.offsetHeight === - detailsCardRef.current.querySelector('.line-clamp-2')?.scrollHeight + (!descriptionNode || descriptionNode.offsetHeight <= descriptionNode.scrollHeight) ) { return 'NONE'; } else if ( prevState === 'FULL' && - heightState >= detailsCardRef.current?.getBoundingClientRect().height + DETAILS_CARD_DEFAULT_HEIGHT >= detailsCardRef.current.getBoundingClientRect().height ) { return 'NONE'; } else if ( - (prevState === 'NONE' || prevState === 'FULL') && - heightState < detailsCardRef.current?.getBoundingClientRect().height + prevState !== 'TRUNCATE' && + DETAILS_CARD_DEFAULT_HEIGHT < detailsCardRef.current.getBoundingClientRect().height ) { - setHeightState(DETAILS_CARD_DEFAULT_HEIGHT); return 'TRUNCATE'; } return prevState; @@ -67,7 +52,7 @@ export const useDetailsCard = () => { 1000, false, ), - [setTruncateState, setHeightState, detailsCardRef], + [setTruncateState, detailsCardRef], ); useIsomorphicLayoutEffect(() => { @@ -77,5 +62,5 @@ export const useDetailsCard = () => { }; }, []); - return { truncateState, toggleTruncateState, heightState, detailsCardRef }; + return { truncateState, toggleTruncateState, detailsCardRef }; }; diff --git a/frontend/src/components/pages/details/components/DetailsCardSection/DetailsCardSection.tsx b/frontend/src/components/pages/details/components/DetailsCardSection/DetailsCardSection.tsx index 05a817217..5387fbbad 100644 --- a/frontend/src/components/pages/details/components/DetailsCardSection/DetailsCardSection.tsx +++ b/frontend/src/components/pages/details/components/DetailsCardSection/DetailsCardSection.tsx @@ -12,6 +12,7 @@ interface DetailsCardSectionProps { generateUrlFunction?: (id: string | number, title: string) => string; type: 'POI' | 'TOURISTIC_CONTENT'; htmlId?: string; + handleViewPointClick?: (id: string) => void; } export const DetailsCardSection: React.FC = ({ @@ -21,6 +22,7 @@ export const DetailsCardSection: React.FC = ({ displayBadge = false, generateUrlFunction, type, + handleViewPointClick, }) => { return (
    @@ -33,7 +35,7 @@ export const DetailsCardSection: React.FC = ({ = ({ : undefined } type={type} + handleViewPointClick={handleViewPointClick} + viewPoints={card.viewPoints} /> ))} diff --git a/frontend/src/components/pages/details/components/DetailsMedias/DetailsMedias.tsx b/frontend/src/components/pages/details/components/DetailsMedias/DetailsMedias.tsx new file mode 100644 index 000000000..00cc186ee --- /dev/null +++ b/frontend/src/components/pages/details/components/DetailsMedias/DetailsMedias.tsx @@ -0,0 +1,147 @@ +import ImageWithLegend from 'components/ImageWithLegend'; +import ArrowRight from 'components/Map/components/DetailsMapDrawer/ArrowRight'; +import { ViewPoint } from 'modules/viewPoint/interface'; +import { useCallback, useId, useState } from 'react'; +import { FormattedMessage } from 'react-intl'; +import { cn } from 'services/utils/cn'; +import { Minus } from 'components/Icons/Minus'; +import { Plus } from 'components/Icons/Plus'; +import { useListAndMapContext } from 'modules/map/ListAndMapContext'; +import { ViewPoint as ViewPointIcon } from 'components/Icons/ViewPoint'; + +interface DetailsMediasProps { + className?: string; + viewPoints: ViewPoint[]; + handleViewPointClick?: (key: string) => void; + asAccordion?: boolean; + titleTag?: 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; +} + +export const DetailsMedias: React.FC = ({ + className, + viewPoints, + handleViewPointClick, + asAccordion = false, + titleTag: TitleTag = 'h2', +}) => { + const SubTitleTag = TitleTag === 'h2' ? 'h3' : 'h4'; + + const handleClick = useCallback( + (viewPointId: string) => { + handleViewPointClick?.(viewPointId); + }, + [handleViewPointClick], + ); + + const id = useId(); + const [isOpen, setOpen] = useState(true); + + if (viewPoints.length === 0) { + return null; + } + + const { setHoveredCardId } = useListAndMapContext(); + + return ( +
    + + + + {asAccordion && ( + + )} + +

    + +

    +
      + {viewPoints.map(viewPoint => { + const legend = [viewPoint.legend, viewPoint.author].filter(Boolean).join(' - '); + return ( +
    • { + !asAccordion && setHoveredCardId(`DETAILS-VIEWPOINT-${viewPoint.id}`); + }} + onMouseLeave={() => { + !asAccordion && setHoveredCardId(null); + }} + > +
      + +
      +
      +
      + + {viewPoint.title} + + {legend.length > 0 && ( +

      + {legend} +

      + )} +
      + +
      +
    • + ); + })} +
    +
    + ); +}; diff --git a/frontend/src/components/pages/details/components/DetailsMedias/index.ts b/frontend/src/components/pages/details/components/DetailsMedias/index.ts new file mode 100644 index 000000000..c6fbcc7f1 --- /dev/null +++ b/frontend/src/components/pages/details/components/DetailsMedias/index.ts @@ -0,0 +1 @@ +export { DetailsMedias } from './DetailsMedias'; diff --git a/frontend/src/components/pages/details/interface.ts b/frontend/src/components/pages/details/interface.ts index 001d4ca21..8931fb343 100644 --- a/frontend/src/components/pages/details/interface.ts +++ b/frontend/src/components/pages/details/interface.ts @@ -1,5 +1,6 @@ export type DetailsSectionTrekNames = | 'presentation' + | 'medias' | 'itinerancySteps' | 'poi' | 'description' @@ -32,6 +33,7 @@ export type DetailsSectionTouristicEventNames = export type DetailsSectionOutdoorSiteNames = | 'presentation' + | 'medias' | 'poi' | 'description' | 'subsites' diff --git a/frontend/src/components/pages/details/useDetails.tsx b/frontend/src/components/pages/details/useDetails.tsx index 98eaa405d..1f3d2eae0 100644 --- a/frontend/src/components/pages/details/useDetails.tsx +++ b/frontend/src/components/pages/details/useDetails.tsx @@ -91,6 +91,8 @@ export const useDetails = ( const intl = useIntl(); + // "default" is the world; reals "id" are related to HD viewpoints ids + const [mapId, setMapId] = useState('default'); const [mobileMapState, setMobileMapState] = useState<'DISPLAYED' | 'HIDDEN' | null>('HIDDEN'); const displayMobileMap = useCallback(() => setMobileMapState('DISPLAYED'), [setMobileMapState]); const hideMobileMap = useCallback(() => setMobileMapState('HIDDEN'), [setMobileMapState]); @@ -120,5 +122,7 @@ export const useDetails = ( hideMobileMap, path, sectionRef, + mapId, + setMapId, }; }; diff --git a/frontend/src/components/pages/site/OutdoorSiteUI.tsx b/frontend/src/components/pages/site/OutdoorSiteUI.tsx index dc4690ff9..3dde02ae4 100644 --- a/frontend/src/components/pages/site/OutdoorSiteUI.tsx +++ b/frontend/src/components/pages/site/OutdoorSiteUI.tsx @@ -18,7 +18,7 @@ import { } from 'components/pages/details/utils'; import { VisibleSectionProvider } from 'components/pages/details/VisibleSectionContext'; import { DetailsChildrenSection } from 'components/pages/details/components/DetailsChildrenSection'; -import { useMemo, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; import Loader from 'components/Loader'; import { useMediaPredicate } from 'react-media-hook'; @@ -42,6 +42,7 @@ import { DetailsCoverCarousel } from '../details/components/DetailsCoverCarousel import { DetailsSensitiveArea } from '../details/components/DetailsSensitiveArea'; import { DetailsAndMapProvider } from '../details/DetailsAndMapContext'; import { useDetailsSections } from '../details/useDetailsSections'; +import { DetailsMedias } from '../details/components/DetailsMedias'; interface Props { outdoorSiteUrl: string | string[] | undefined; @@ -60,6 +61,8 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language sectionsReferences, sectionsPositions, sectionRef, + mapId, + setMapId, } = useOutdoorSite(outdoorSiteUrl, language); const intl = useIntl(); @@ -85,6 +88,14 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language sizes.detailsHeaderDesktop, }); + const handleViewPointClick = useCallback( + (viewPointId: string) => { + setMapId(viewPointId); + displayMobileMap(); + }, + [displayMobileMap, setMapId], + ); + return useMemo( () => ( <> @@ -117,7 +128,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language id="outdoorSiteContent_informations" className="flex flex-col w-full relative -top-detailsHeaderMobile desktop:top-0 desktop:w-3/5" > - +
    {({ isFullscreen, toggleFullscreen }) => ( @@ -188,6 +199,26 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language ); } + if ( + hasNavigator && + section.name === 'medias' && + outdoorSiteContent.viewPoints.length > 0 + ) { + return ( +
    + + + +
    + ); + } if ( section.name === 'poi' && outdoorSiteContent?.pois?.length && @@ -213,8 +244,10 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language attachments: poi.attachments, iconUri: poi.type.pictogramUri, iconName: poi.type.label, + viewPoints: poi.viewPoints, }))} type="POI" + handleViewPointClick={handleViewPointClick} /> ); @@ -531,6 +564,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language )} > = ({ outdoorSiteUrl, language }))} infrastructure={outdoorSiteContent.infrastructure} hideMap={hideMobileMap} + viewPoints={[ + ...outdoorSiteContent.viewPoints, + ...outdoorSiteContent.pois + .flatMap(({ viewPoints = [] }) => viewPoints) + .filter(Boolean), + ]} + displayMap={displayMobileMap} + setMapId={setMapId} />
    @@ -583,7 +625,7 @@ const OutdoorSiteUIWithoutContext: React.FC = ({ outdoorSiteUrl, language )} ), - [outdoorSiteContent, isLoading, mobileMapState, sectionsReferences, hasNavigator], + [outdoorSiteContent, isLoading, mobileMapState, sectionsReferences, hasNavigator, mapId], ); }; diff --git a/frontend/src/components/pages/site/useOutdoorSite.tsx b/frontend/src/components/pages/site/useOutdoorSite.tsx index dd42ae771..468a0411a 100644 --- a/frontend/src/components/pages/site/useOutdoorSite.tsx +++ b/frontend/src/components/pages/site/useOutdoorSite.tsx @@ -44,7 +44,8 @@ export const useOutdoorSite = (outdoorSiteUrl: string | string[] | undefined, la (list, item) => ({ ...list, [item.name]: useSectionReferenceCallback(item.name) }), {} as Record void>, ); - + // "default" is the world; reals "id" are related to HD viewpoints ids + const [mapId, setMapId] = useState('default'); const [mobileMapState, setMobileMapState] = useState<'DISPLAYED' | 'HIDDEN'>('HIDDEN'); const displayMobileMap = () => setMobileMapState('DISPLAYED'); const hideMobileMap = () => setMobileMapState('HIDDEN'); @@ -62,5 +63,7 @@ export const useOutdoorSite = (outdoorSiteUrl: string | string[] | undefined, la hideMobileMap, path, sectionRef, + mapId, + setMapId, }; }; diff --git a/frontend/src/hooks/useTileLayer.ts b/frontend/src/hooks/useTileLayer.ts index c959c499f..c9475ca7a 100644 --- a/frontend/src/hooks/useTileLayer.ts +++ b/frontend/src/hooks/useTileLayer.ts @@ -6,10 +6,12 @@ import { useIntl } from 'react-intl'; require('leaflet.locatecontrol'); import 'leaflet.locatecontrol/dist/L.Control.Locate.min.css'; import injectOfflineMode from 'services/offline/injectOfflineMode'; +import { ViewPoint } from 'modules/viewPoint/interface'; export const useTileLayer = ( id?: number, center?: LatLngBoundsExpression | null, + mapToDisplay: ViewPoint | 'default' = 'default', ): { map: Map | null; setMapInstance: (newMap: Map) => void; @@ -25,19 +27,21 @@ export const useTileLayer = ( injectOfflineMode(newMap, id, center); } - L.control - // @ts-ignore no type available in this plugin - .locate({ - locateOptions: { - enableHighAccuracy: true, - }, - icon: 'gg-track', - strings: { - title: intl.formatMessage({ id: 'search.map.seeMe' }), - }, - position: 'bottomright', - }) - .addTo(newMap); + if (mapToDisplay === 'default') { + L.control + // @ts-ignore no type available in this plugin + .locate({ + locateOptions: { + enableHighAccuracy: true, + }, + icon: 'gg-track', + strings: { + title: intl.formatMessage({ id: 'search.map.seeMe' }), + }, + position: 'bottomright', + }) + .addTo(newMap); + } }; return { diff --git a/frontend/src/modules/details/adapter.ts b/frontend/src/modules/details/adapter.ts index a29f63a89..74dac4b0a 100644 --- a/frontend/src/modules/details/adapter.ts +++ b/frontend/src/modules/details/adapter.ts @@ -30,6 +30,7 @@ import { getTrekGeometryAsLineStringCoordinates, } from 'modules/utils/geometry'; import { formatHours } from 'modules/utils/time'; +import { ViewPoint } from 'modules/viewPoint/interface'; import { TrekRatingScale } from '../trekRatingScale/interface'; import { TrekRatingChoices } from '../trekRating/interface'; import { Details, RawDetails, Reservation, TrekChildGeometry, TrekFamily } from './interface'; @@ -58,6 +59,7 @@ export const adaptResults = ({ reservation, trekRating, trekRatingScale, + viewPoints, }: { accessbilityLevel: AccessibilityLevel | null; rawDetails: RawDetails; @@ -82,6 +84,7 @@ export const adaptResults = ({ reservation: Reservation | null; trekRating: TrekRatingChoices; trekRatingScale: TrekRatingScale[]; + viewPoints: ViewPoint[]; }): Details => { try { const coordinates = getTrekGeometryAsLineStringCoordinates(geometry); @@ -187,6 +190,7 @@ export const adaptResults = ({ signage, service, infrastructure, + viewPoints, }; } catch (e) { console.error('Error in details/adapter', e); diff --git a/frontend/src/modules/details/api.ts b/frontend/src/modules/details/api.ts index f80e9511c..33618cdfa 100644 --- a/frontend/src/modules/details/api.ts +++ b/frontend/src/modules/details/api.ts @@ -5,7 +5,7 @@ import { RawDetails, RawTrekChildGeometry, RawTrekChildIds, RawTrekName } from ' const fieldsParams = { fields: - 'id,name,departure,arrival,cities,attachments,practice,public_transport,access,advised_parking,description_teaser,ambiance,themes,duration,length_2d,ascent,descent,difficulty,route,networks,description,geometry,parking_location,pdf,gpx,kml,departure_city,disabled_infrastructure,accessibilities,source,information_desks,labels,advice,gear,points_reference,children,web_links,elevation_area_url,altimetric_profile,reservation_id,accessibility_signage,accessibility_slope,accessibility_width,accessibility_covering,accessibility_exposure,accessibility_advice,attachments_accessibility,accessibility_level,ratings,ratings_description', + 'id,name,departure,arrival,cities,attachments,practice,public_transport,access,advised_parking,description_teaser,ambiance,themes,duration,length_2d,ascent,descent,difficulty,route,networks,description,geometry,parking_location,pdf,gpx,kml,departure_city,disabled_infrastructure,accessibilities,source,information_desks,labels,advice,gear,points_reference,children,web_links,elevation_area_url,altimetric_profile,reservation_id,accessibility_signage,accessibility_slope,accessibility_width,accessibility_covering,accessibility_exposure,accessibility_advice,attachments_accessibility,accessibility_level,ratings,ratings_description,view_points', format: 'geojson', }; diff --git a/frontend/src/modules/details/connector.ts b/frontend/src/modules/details/connector.ts index b4dd3f8af..768476cbc 100644 --- a/frontend/src/modules/details/connector.ts +++ b/frontend/src/modules/details/connector.ts @@ -13,6 +13,7 @@ import { getInfrastructure } from 'modules/infrastructure/connector'; import { getGlobalConfig } from 'modules/utils/api.config'; import { getTouristicContentsNearTarget } from 'modules/touristicContent/connector'; import { CommonDictionaries } from 'modules/dictionaries/interface'; +import { adaptViewPoints } from 'modules/viewPoint/adapter'; import { getTrekRating } from '../trekRating/connector'; import { getTrekRatingScale } from '../trekRatingScale/connector'; import { adaptChildren, adaptResults, adaptTrekChildGeometry } from './adapter'; @@ -52,6 +53,8 @@ export const getDetails = async ( getAccessibilities(language), ]); + const viewPoints = await adaptViewPoints(rawDetails.properties.view_points ?? []); + const [ activity, difficulty, @@ -118,6 +121,7 @@ export const getDetails = async ( project: getGlobalConfig().reservationProject, } : null, + viewPoints, }); } catch (e) { console.error('Error in details/connector principal', e); diff --git a/frontend/src/modules/details/interface.ts b/frontend/src/modules/details/interface.ts index 41e1e663e..b8a17f0bb 100644 --- a/frontend/src/modules/details/interface.ts +++ b/frontend/src/modules/details/interface.ts @@ -24,6 +24,7 @@ import { SensitiveArea } from 'modules/sensitiveArea/interface'; import { SignageDictionary } from 'modules/signage/interface'; import { Service } from 'modules/service/interface'; import { InfrastructureDictionary } from 'modules/infrastructure/interface'; +import { RawViewPoint, ViewPoint } from 'modules/viewPoint/interface'; import { TrekRatingWithScale } from '../trekRating/interface'; export interface RawDetails { @@ -93,6 +94,7 @@ export interface RawDetailsProperties { gear: string | null; ratings: number[]; ratings_description: string; + view_points: RawViewPoint[]; } // Fields parsed with react-html-parser in page @@ -190,6 +192,7 @@ export interface Details extends DetailsHtml { signage: SignageDictionary | null; service: Service[] | null; infrastructure: InfrastructureDictionary | null; + viewPoints: ViewPoint[]; } export interface WebLink { diff --git a/frontend/src/modules/details/mocks/mocks.ts b/frontend/src/modules/details/mocks/mocks.ts index b8ed384fa..48c221f07 100644 --- a/frontend/src/modules/details/mocks/mocks.ts +++ b/frontend/src/modules/details/mocks/mocks.ts @@ -136,6 +136,7 @@ export const rawDetailsProperties: RawDetailsProperties = { altimetric_profile: 'https://geotrekdemo.ecrins-parcnational.fr/api/fr/treks/2/profile.json', ratings: [], ratings_description: '', + view_points: [], }; export const rawDetails: RawDetails = { diff --git a/frontend/src/modules/outdoorSite/adapter.ts b/frontend/src/modules/outdoorSite/adapter.ts index 5df5c41c8..ca485e767 100644 --- a/frontend/src/modules/outdoorSite/adapter.ts +++ b/frontend/src/modules/outdoorSite/adapter.ts @@ -4,6 +4,7 @@ import { Service } from 'modules/service/interface'; import { InfrastructureDictionary } from 'modules/infrastructure/interface'; import { getAttachments, getThumbnail, getThumbnails } from 'modules/utils/adapter'; import { adaptGeometry } from 'modules/utils/geometry'; +import { ViewPoint } from 'modules/viewPoint/interface'; import { CityDictionnary } from '../city/interface'; import { Choices } from '../filters/interface'; import { InformationDeskDictionnary } from '../informationDesk/interface'; @@ -101,6 +102,7 @@ export const adaptOutdoorSiteDetails = ({ signage, service, infrastructure, + viewPoints, }: { rawOutdoorSiteDetails: RawOutdoorSiteDetails; pois: Poi[]; @@ -122,6 +124,7 @@ export const adaptOutdoorSiteDetails = ({ signage: SignageDictionary | null; service: Service[] | null; infrastructure: InfrastructureDictionary | null; + viewPoints: ViewPoint[]; }): OutdoorSiteDetails => ({ ...adaptOutdoorSites({ rawOutdoorSites: [ @@ -179,6 +182,7 @@ export const adaptOutdoorSiteDetails = ({ signage, service, infrastructure, + viewPoints, }); export const adaptOutdoorSitePopupResults = ({ diff --git a/frontend/src/modules/outdoorSite/api.ts b/frontend/src/modules/outdoorSite/api.ts index 0e9eb4903..31c77fbfc 100644 --- a/frontend/src/modules/outdoorSite/api.ts +++ b/frontend/src/modules/outdoorSite/api.ts @@ -15,7 +15,7 @@ export const fetchOutdoorSites = (query: APIQuery): Promise - rawPoisResults.map(rawPoi => ({ - id: `${rawPoi.id}`, - name: rawPoi.name, - description: rawPoi.description, - thumbnails: getThumbnails(rawPoi.attachments), - attachments: getAttachments(rawPoi.attachments), - type: poiTypes[rawPoi.type], - geometry: { - x: rawPoi.geometry.coordinates[0], - y: rawPoi.geometry.coordinates[1], - z: rawPoi.geometry.coordinates[2], - }, - })); +}): Promise => + Promise.all( + rawPoisResults.map(async rawPoi => { + const viewPoints = + rawPoi.view_points?.length > 0 ? await adaptViewPoints(rawPoi.view_points) : []; + + return { + id: `${rawPoi.id}`, + name: rawPoi.name, + description: rawPoi.description, + thumbnails: getThumbnails(rawPoi.attachments), + attachments: getAttachments(rawPoi.attachments), + type: poiTypes[rawPoi.type], + geometry: { + x: rawPoi.geometry.coordinates[0], + y: rawPoi.geometry.coordinates[1], + z: rawPoi.geometry.coordinates[2], + }, + viewPoints, + }; + }), + ); diff --git a/frontend/src/modules/poi/api.ts b/frontend/src/modules/poi/api.ts index fc63f2f55..45dd9d014 100644 --- a/frontend/src/modules/poi/api.ts +++ b/frontend/src/modules/poi/api.ts @@ -3,7 +3,7 @@ import { APIQuery, APIResponseForList } from 'services/api/interface'; import { RawPoi } from './interface'; const fieldsParams = { - fields: 'id,name,description,attachments,type,geometry', + fields: 'id,name,description,attachments,type,geometry,view_points', }; export const fetchPois = (query: APIQuery): Promise> => diff --git a/frontend/src/modules/poi/interface.ts b/frontend/src/modules/poi/interface.ts index d70e3080c..a38b54271 100644 --- a/frontend/src/modules/poi/interface.ts +++ b/frontend/src/modules/poi/interface.ts @@ -1,5 +1,6 @@ import { Attachment, Coordinate3D, RawAttachment, RawPointGeometry3D } from 'modules/interface'; import { PoiType } from 'modules/poiType/interface'; +import { RawViewPoint, ViewPoint } from 'modules/viewPoint/interface'; export interface RawPoi { id: number; @@ -8,6 +9,7 @@ export interface RawPoi { type: number; attachments: RawAttachment[]; geometry: RawPointGeometry3D; + view_points: RawViewPoint[]; } export interface Poi { @@ -18,4 +20,5 @@ export interface Poi { attachments: Attachment[]; type: PoiType; geometry: Coordinate3D; + viewPoints?: ViewPoint[]; } diff --git a/frontend/src/modules/poi/mocks/index.ts b/frontend/src/modules/poi/mocks/index.ts index 2e53465f2..918de04c1 100644 --- a/frontend/src/modules/poi/mocks/index.ts +++ b/frontend/src/modules/poi/mocks/index.ts @@ -45,6 +45,7 @@ export const mockPois = (): PoisResponse => ({ type: 'Point', coordinates: [6.2061167, 44.8985958, 1787], }, + view_points: [], }, { description: 'Test refuge', @@ -56,6 +57,7 @@ export const mockPois = (): PoisResponse => ({ type: 'Point', coordinates: [6.1667321, 44.7604322, 2409], }, + view_points: [], }, ], }); @@ -66,7 +68,7 @@ export const mockPoiRoute = (times: number, trekId: number): void => mockData: mockPois(), additionalQueries: { near_trek: trekId, - fields: 'id,name,description,attachments,type,geometry', + fields: 'id,name,description,attachments,type,geometry,view_points', page_size: getGlobalConfig().maxPoiPerPage, }, times, diff --git a/frontend/src/modules/viewPoint/adapter.ts b/frontend/src/modules/viewPoint/adapter.ts new file mode 100644 index 000000000..2001ac19f --- /dev/null +++ b/frontend/src/modules/viewPoint/adapter.ts @@ -0,0 +1,27 @@ +import { getViewPointMetadata } from './connector'; +import { RawViewPoint, ViewPoint } from './interface'; + +export const adaptViewPoints = async (rawViewpoints: RawViewPoint[]): Promise => { + if (rawViewpoints.length === 0) { + return []; + } + const viewPoints = await Promise.all( + rawViewpoints.map(async viewpoint => { + const metadata = await getViewPointMetadata(viewpoint.metadata_url); + return { + annotations: viewpoint.annotations, + id: String(viewpoint.id), + author: viewpoint.author, + geometry: viewpoint.geometry ?? null, + legend: viewpoint.legend, + license: viewpoint.license, + metadata, + pictureTilesUrl: decodeURI(viewpoint.picture_tiles_url), + title: viewpoint.title, + thumbnailUrl: viewpoint.thumbnail_url, + }; + }), + ); + + return viewPoints.filter(({ metadata, pictureTilesUrl }) => metadata && pictureTilesUrl); +}; diff --git a/frontend/src/modules/viewPoint/api.ts b/frontend/src/modules/viewPoint/api.ts new file mode 100644 index 000000000..136813016 --- /dev/null +++ b/frontend/src/modules/viewPoint/api.ts @@ -0,0 +1,11 @@ +import { GeotrekAPI } from 'services/api/client'; +import { ViewPoint } from './interface'; + +export const fetchViewPointMetadata = (url: string): Promise => { + try { + return GeotrekAPI.get(url).then(r => r.data); + } catch (e) { + console.error('Error in viewpointsMetadata/api/fetch', e); + throw e; + } +}; diff --git a/frontend/src/modules/viewPoint/connector.ts b/frontend/src/modules/viewPoint/connector.ts new file mode 100644 index 000000000..975c73197 --- /dev/null +++ b/frontend/src/modules/viewPoint/connector.ts @@ -0,0 +1,10 @@ +import { fetchViewPointMetadata } from './api'; +import { ViewPoint } from './interface'; + +export const getViewPointMetadata = async (url: string): Promise => { + try { + return await fetchViewPointMetadata(url); + } catch (e) { + return null; + } +}; diff --git a/frontend/src/modules/viewPoint/interface.ts b/frontend/src/modules/viewPoint/interface.ts new file mode 100644 index 000000000..94aabe23a --- /dev/null +++ b/frontend/src/modules/viewPoint/interface.ts @@ -0,0 +1,34 @@ +import { FeatureCollection } from 'geojson'; +import { RawPointGeometry2D } from 'modules/interface'; + +export interface RawViewPoint { + annotations: FeatureCollection; + id: number; + author: string | null; + legend: string | null; + license: string | null; + metadata_url: string; + picture_tiles_url: string; + title: string | null; + thumbnail_url: string; + geometry?: RawPointGeometry2D; +} + +export interface ViewPoint { + annotations: FeatureCollection; + id: string; + author: string | null; + legend: string | null; + license: string | null; + metadata: { + levels: number; + sizeX: number; + sizeY: number; + tileWidth: number; + tileHeight: number; + } | null; + pictureTilesUrl: string; + title: string | null; + thumbnailUrl: string; + geometry: RawPointGeometry2D | null; +} diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css index 8a7c6e475..5af6aceef 100644 --- a/frontend/src/styles/global.css +++ b/frontend/src/styles/global.css @@ -76,3 +76,16 @@ .leaflet-container .leaflet-control-zoom { @apply order-first; } + +.leaflet-container:fullscreen #backToMapButton { + @apply hidden; +} + +.leaflet-interactive.annotation { + stroke: red; + fill: green; +} + +.leaflet-interactive.annotation-line { + fill: none; +} diff --git a/frontend/src/translations/ca.json b/frontend/src/translations/ca.json index cdcae3af6..b33b87b04 100644 --- a/frontend/src/translations/ca.json +++ b/frontend/src/translations/ca.json @@ -100,7 +100,8 @@ "experiences": "Lieux de pratique", "signage": "Senyalització", "infrastructure": "Infrastructures", - "service": "Others infos" + "service": "Others infos", + "annotations": "Annotations" }, "resetView": "Recenter el mapa" }, @@ -120,13 +121,14 @@ "poiFullTitle": "{count, plural, =0 {sense patrimoni} one {# patrimoni a descobrir} other {Els # patrimonis a descobrir}}", "source": "Font", "touristicContent": "Proper", - "readMore": "llegir més", "knowMore": "Més informació", - "readLess": "llegir menys", + "moreInformation": "More information", + "lessInformation": "Less information", "close": "Tancar", "description": "Descripció", "recommandations": "Recomanacions", "informationDesks": "Llocs d’informació", + "medias": "Mitjans de comunicació", "practicalInformations": "Infos pràctiques", "forecastWidget": "Informe del temps", "accessibility": "Accessibilitat", @@ -240,6 +242,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "Els {count} llocs de pràcticar a descobrir" }, + "viewPoint": { + "title": "Fotos HD", + "label": "Foto HD", + "description": "Navegueu per imatges enriquides i interactives \"ultra alta definició\"", + "credit": "Crèdit fotogràfic :", + "displayPicture": "Navega per la foto" + }, "Wind": { "N": "Nord", "S": "Sud", @@ -272,6 +281,10 @@ "courseType": "Tipus de cursa", "network": "Xarxa" }, + "accordion": { + "open": "Open", + "close": "Close" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/translations/de.json b/frontend/src/translations/de.json index 46d5b593c..bddb20cdb 100644 --- a/frontend/src/translations/de.json +++ b/frontend/src/translations/de.json @@ -100,7 +100,8 @@ "experiences": "Lieux de pratique", "signage": "Beschilderung", "infrastructure": "Infrastructures", - "service": "Others infos" + "service": "Others infos", + "annotations": "Annotations" }, "resetView": "Karte zentrieren" }, @@ -120,13 +121,14 @@ "poiFullTitle": "{count, plural, =0 {Kein Erbe} one {# Erbe zu entdecken} other {Die # Erbgüter, die es zu entdecken gilt}}", "source": "Quelle", "touristicContent": "In der Nähe", - "readMore": "weiterlesen", "knowMore": "Mehr darüber erfahren", - "readLess": "weniger lesen", + "moreInformation": "More information", + "lessInformation": "Less information", "close": "Schließen", "description": "Beschreibung", "recommandations": "Empfehlungen", "informationDesks": "Orte der Information", + "medias": "Medien", "practicalInformations": "Praktische Informationen", "forecastWidget": "Wetterbericht", "accessibility": "Zugänglichkeit", @@ -240,6 +242,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "Die {count} Orte der Praxis, die es zu entdecken gilt" }, + "viewPoint": { + "title": "HD-Fotos", + "label": "HD-Foto", + "description": "Navigieren Sie durch angereicherte, interaktive \"ultrahochauflösende\" Bilder", + "credit": "Fotogutschrift:", + "displayPicture": "Foto durchsuchen" + }, "Wind": { "N": "Norden", "S": "Süden", @@ -272,6 +281,10 @@ "courseType": "Art des Rennens", "network": "Netzwerk" }, + "accordion": { + "open": "Open", + "close": "Close" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/translations/en.json b/frontend/src/translations/en.json index 7d2cbafcb..5527490aa 100644 --- a/frontend/src/translations/en.json +++ b/frontend/src/translations/en.json @@ -103,7 +103,8 @@ "experiences": "Lieux de pratique", "signage": "Signage", "infrastructure": "Infrastructures", - "service": "Others infos" + "service": "Others infos", + "annotations": "Annotations" }, "resetView": "Recenter map" }, @@ -123,13 +124,14 @@ "poiFullTitle": "{count, plural, =0 {No point of interest} one {# point of interest} other {# points of interest}}", "source": "Source", "touristicContent": "Close by", - "readMore": "read more", "knowMore": "Find out more", - "readLess": "read less", + "moreInformation": "More information", + "lessInformation": "Less information", "close": "Close", "description": "Description", "recommandations": "Recommandations", "informationDesks": "Information desks", + "medias": "Medias", "practicalInformations": "Practical informations", "forecastWidget": "Forecast", "accessibility": "Accessibility", @@ -247,6 +249,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "The {count} places of practice to discover" }, + "viewPoint": { + "title": "HD pictures", + "label": "HD picture", + "description": "Navigate through enriched, interactive \"ultra High Definition\" pictures", + "credit": "Picture credit:", + "displayPicture": "Display picture" + }, "Wind": { "N": "North", "S": "South", @@ -318,6 +327,10 @@ "courseType": "Course type", "network": "Network" }, + "accordion": { + "open": "Open", + "close": "Close" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/translations/es.json b/frontend/src/translations/es.json index 33bbd62df..33b51d617 100644 --- a/frontend/src/translations/es.json +++ b/frontend/src/translations/es.json @@ -100,7 +100,8 @@ "experiences": "Lieux de pratique", "signage": "Señalización", "infrastructure": "Infrastructures", - "service": "Others infos" + "service": "Others infos", + "annotations": "Annotations" }, "resetView": "Centrarse la mapa" }, @@ -120,13 +121,14 @@ "poiFullTitle": "{count, plural, =0 {sin patrimonio} one {# patrimonio para descubrir} other {Els # patrimonios para descubrir}}", "source": "Fuente", "touristicContent": "Cercano", - "readMore": "lleer más", "knowMore": "Más información", - "readLess": "lleer menos", + "moreInformation": "More information", + "lessInformation": "Less information", "close": "Cerrar", "description": "Descripción", "recommandations": "Recomendaciones", "informationDesks": "Lugares de información", + "medias": "Medias", "practicalInformations": "Infos prácticas", "forecastWidget": "Reporte del clima", "accessibility": "Accesibilidad", @@ -240,6 +242,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "Los {count} lugares de práctica para descubrir" }, + "viewPoint": { + "title": "Fotos HD", + "description": "Navegue por imágenes enriquecidas e interactivas de \"ultra alta definición\"", + "label": "Foto HD", + "credit": "Crédito de la foto:", + "displayPicture": "Examinar foto" + }, "Wind": { "N": "Norte", "S": "Sur", @@ -272,6 +281,10 @@ "courseType": "Tipo de carrera", "network": "La red" }, + "accordion": { + "open": "Open", + "close": "Close" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/translations/fr.json b/frontend/src/translations/fr.json index 0aa8ad45a..eb85e1fa4 100644 --- a/frontend/src/translations/fr.json +++ b/frontend/src/translations/fr.json @@ -103,7 +103,8 @@ "experiences": "Lieux de pratique", "signage": "Signalétiques", "infrastructure": "Aménagements", - "service": "Autres infos" + "service": "Autres infos", + "annotations": "Annotations" }, "resetView": "Recentrer la carte" }, @@ -123,13 +124,14 @@ "poiFullTitle": "{count, plural, =0 {Pas de patrimoine} one {# patrimoine à découvrir} other {Les # patrimoines à découvrir}}", "source": "Source", "touristicContent": "À proximité", - "readMore": "lire la suite", "knowMore": "En savoir plus", - "readLess": "lire moins", + "moreInformation": "Plus d'informations", + "lessInformation": "Moins d'informations", "close": "Fermer", "description": "Description", "recommandations": "Recommandations", "informationDesks": "Lieux de renseignement", + "medias": "Medias", "practicalInformations": "Infos pratiques", "forecastWidget": "Météo", "accessibility": "Accessibilité", @@ -247,6 +249,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "Les {count} lieux de pratique à découvrir" }, + "viewPoint": { + "title": "Images HD", + "label": "Image HD", + "description": "Naviguez dans des images \"très haute définition\" enrichies et interactives", + "credit": "Crédit image :", + "displayPicture": "Afficher l'image" + }, "Wind": { "N": "Nord", "S": "Sud", @@ -318,6 +327,10 @@ "courseType": "Type de parcours", "network": "Réseau" }, + "accordion": { + "open": "Ouvrir", + "close": "Fermer" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/translations/it.json b/frontend/src/translations/it.json index a27c9f8fd..34689087d 100644 --- a/frontend/src/translations/it.json +++ b/frontend/src/translations/it.json @@ -103,7 +103,8 @@ "experiences": "Lieux de pratique", "signage": "Segnaletica", "infrastructure": "Infrastructures", - "service": "Others infos" + "service": "Others infos", + "annotations": "Annotations" }, "resetView": "Rimettere a fuoco la mappa" }, @@ -123,13 +124,14 @@ "poiFullTitle": "{count, plural, =0 {Nessun patrimonio} one {# Patrimonio da scoprire} other {# I patrimoni da scoprire}}", "source": "Autore", "touristicContent": "Vicino", - "readMore": "Continua a leggere", "knowMore": "Saperne di più", - "readLess": "Leggere meno", + "moreInformation": "More information", + "lessInformation": "Less information", "close": "Chiudere", "description": "Descrizione", "recommandations": "Raccomandazioni", "informationDesks": "Luoghi di informazione", + "medias": "Medias", "practicalInformations": "Informazioni pratiche", "forecastWidget": "Bollettino meteorologico", "accessibility": "Accessibilità", @@ -250,6 +252,13 @@ "coursesFullTitle": "Les {count} parcours à découvrir", "sitesFullTitle": "Il {conteggio} dei luoghi di pratica da scoprire" }, + "viewPoint": { + "title": "Foto HD", + "label": "Foto HD", + "description": "Navigare attraverso immagini arricchite e interattive ad \"altissima definizione\"", + "credit": "Credito foto:", + "displayPicture": "Sfoglia foto" + }, "Wind": { "N": "Nord", "S": "Sud", @@ -282,6 +291,10 @@ "courseType": "Tipo de gara", "network": "Rete" }, + "accordion": { + "open": "Open", + "close": "Close" + }, "consents": { "modal": { "purposes": { diff --git a/frontend/src/types/leaflet-rastercoords.d.ts b/frontend/src/types/leaflet-rastercoords.d.ts new file mode 100644 index 000000000..11acedd59 --- /dev/null +++ b/frontend/src/types/leaflet-rastercoords.d.ts @@ -0,0 +1,13 @@ +import { LatLng, LatLngBounds, LatLngExpression, Map, Point, PointExpression } from 'leaflet'; + +declare module 'leaflet' { + class RasterCoords { + constructor(map: Map, imgsize: number[], tilesize?: number, setmaxbounds?: boolean); + + zoomLevel(): number; + unproject(coords: PointExpression): LatLng; + project(coords: LatLngExpression): Point; + getMaxBounds(): LatLngBounds; + setMaxBounds(): void; + } +} diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 1b4867bfa..6139b6f33 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7739,6 +7739,13 @@ leaflet-i18n@^0.3.1: resolved "https://registry.yarnpkg.com/leaflet-i18n/-/leaflet-i18n-0.3.1.tgz#404e75dc6704f4a2399d1cd05f0dd1dd178300b5" integrity sha1-QE513GcE9KI5nRzQXw3R3ReDALU= +leaflet-rastercoords@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/leaflet-rastercoords/-/leaflet-rastercoords-1.0.5.tgz#6a188f8b88d5613333556a01cbd1eef68a60ebca" + integrity sha512-PGrxD6dhbChN45acRedZMfG1OvOKidwzUHU+lfD3Save9fNSHUeuL9c8BKg84t29Xp7mgvjoF0KIHzYoNNN+gQ== + dependencies: + leaflet "^1.7.1" + leaflet-textpath@^1.2.3: version "1.2.3" resolved "https://registry.yarnpkg.com/leaflet-textpath/-/leaflet-textpath-1.2.3.tgz#0adef3a2d438c1c781f4e1154b8fa2f963cfd8a7"