diff --git a/src/app/global.css b/src/app/global.css index 29131c03c..be875b059 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -135,3 +135,11 @@ A slightly deemphasized dotted underline for a tag in order to not competing wit @apply bg-neutral-300 rounded-full; } } + +/** + * Force mapbox-gl library to use our font otherwise components inside the map + * will use their font and look out of place. + */ +.mapboxgl-map { + font-family: inherit !important; +} \ No newline at end of file diff --git a/src/components/maps/AreaActiveMarker.tsx b/src/components/maps/AreaActiveMarker.tsx index 4feaaac45..d9fe6f667 100644 --- a/src/components/maps/AreaActiveMarker.tsx +++ b/src/components/maps/AreaActiveMarker.tsx @@ -1,12 +1,44 @@ -import { Marker } from 'react-map-gl' -import { Point } from '@turf/helpers' +import { Marker, Source, Layer, LineLayer } from 'react-map-gl' +import { Point, Polygon } from '@turf/helpers' import { MapPin } from '@phosphor-icons/react/dist/ssr' -export const AreaActiveMarker: React.FC<{ point: Point }> = ({ point }) => { - const { coordinates } = point +/** + * Highlight selected feature on the map + */ +export const SelectedFeature: React.FC<{ geometry: Point | Polygon }> = ({ geometry }) => { + switch (geometry.type) { + case 'Point': + return + case 'Polygon': + return + default: return null + } +} + +const SelectedPoint: React.FC<{ geometry: Point }> = ({ geometry }) => { + const { coordinates } = geometry return ( - + ) } + +export const SelectedPolygon: React.FC<{ geometry: Polygon }> = ({ geometry }) => { + return ( + + + + ) +} + +const selectedBoundary: LineLayer = { + id: 'polygon2', + type: 'line', + paint: { + 'line-opacity': ['step', ['zoom'], 0.85, 10, 0.5], + 'line-width': ['step', ['zoom'], 2, 10, 10], + 'line-color': '#004F6E', // See 'area-cue' in tailwind.config.js + 'line-blur': 4 + } +} diff --git a/src/components/maps/AreaInfoDrawer.tsx b/src/components/maps/AreaInfoDrawer.tsx index c5d86cd08..2d83eb1d6 100644 --- a/src/components/maps/AreaInfoDrawer.tsx +++ b/src/components/maps/AreaInfoDrawer.tsx @@ -1,15 +1,17 @@ import * as Popover from '@radix-ui/react-popover' + import { MapAreaFeatureProperties } from './AreaMap' import { getAreaPageFriendlyUrl } from '@/js/utils' import { Card } from '../core/Card' import { EntityIcon } from '@/app/editArea/[slug]/general/components/AreaItem' +import { ArrowRight } from '@phosphor-icons/react/dist/ssr' /** * Area info panel */ export const AreaInfoDrawer: React.FC<{ data: MapAreaFeatureProperties | null, onClose?: () => void }> = ({ data, onClose }) => { const parent = data?.parent == null ? null : JSON.parse(data.parent) - const parentName = parent?.name ?? 'Unknown' + const parentName = parent?.name ?? '' const parentId = parent?.id ?? null return ( @@ -21,9 +23,14 @@ export const AreaInfoDrawer: React.FC<{ data: MapAreaFeatureProperties | null, o ) } -export const Content: React.FC = ({ id, name, parentName, parentId }) => { +export const Content: React.FC = ({ id, name, parentName, parentId, content }) => { const url = parentId == null - ? parentName + ? ( + + + {parentName} + + ) : ( {parentName} ) + + const friendlyUrl = getAreaPageFriendlyUrl(id, name) return ( @@ -40,6 +49,11 @@ export const Content: React.FC∟{name} + + + Edit + Visit area + ) } diff --git a/src/components/maps/AreaInfoHover.tsx b/src/components/maps/AreaInfoHover.tsx new file mode 100644 index 000000000..a5a3b6588 --- /dev/null +++ b/src/components/maps/AreaInfoHover.tsx @@ -0,0 +1,53 @@ +import * as Popover from '@radix-ui/react-popover' +import { HoverInfo, MapAreaFeatureProperties } from './AreaMap' +import { getAreaPageFriendlyUrl } from '@/js/utils' +import { Card } from '../core/Card' +import { EntityIcon } from '@/app/editArea/[slug]/general/components/AreaItem' +import { SelectedPolygon } from './AreaActiveMarker' +/** + * Area info panel + */ +export const AreaInfoHover: React.FC = ({ data, geometry, mapInstance }) => { + const parent = data?.parent == null ? null : JSON.parse(data.parent) + const parentName = parent?.name ?? 'Unknown' + const parentId = parent?.id ?? null + + let screenXY + if (geometry.type === 'Point') { + screenXY = mapInstance.project(geometry.coordinates) + } else { + return + } + return ( + + + + {data != null && } + + + + ) +} + +export const Content: React.FC = ({ id, name, parentName, parentId }) => { + const url = parentId == null + ? parentName + : ( + + {parentName} + + ) + return ( + + + {url} + + ∟{name} + + + + ) +} diff --git a/src/components/maps/AreaMap.tsx b/src/components/maps/AreaMap.tsx index 4d9a3b60f..bf75fd589 100644 --- a/src/components/maps/AreaMap.tsx +++ b/src/components/maps/AreaMap.tsx @@ -1,15 +1,15 @@ 'use client' -import { useEffect, useRef, useState } from 'react' -import { Map, ScaleControl, FullscreenControl, NavigationControl, Source, Layer, MapLayerMouseEvent, LineLayer } from 'react-map-gl' +import { useCallback, useEffect, useRef, useState } from 'react' +import { Map, ScaleControl, FullscreenControl, NavigationControl, Source, Layer, MapLayerMouseEvent, LineLayer, MapInstance } from 'react-map-gl' import dynamic from 'next/dynamic' -import { lineString, Point, point } from '@turf/helpers' +import { lineString, Point, Polygon, point } from '@turf/helpers' import lineToPolygon from '@turf/line-to-polygon' -import { useDebouncedCallback } from 'use-debounce' import { AreaMetadataType, AreaType } from '../../js/types' import { MAP_STYLES } from './BaseMap' import { AreaInfoDrawer } from './AreaInfoDrawer' -import { AreaActiveMarker } from './AreaActiveMarker' +import { AreaInfoHover } from './AreaInfoHover' +import { SelectedFeature } from './AreaActiveMarker' type ChildArea = Pick & { metadata: Pick } interface AreaMapProps { @@ -22,6 +22,9 @@ interface AreaMapProps { export interface MapAreaFeatureProperties { id: string name: string + content: { + description: string + } parent: string // due to a backend backend bug, this is a string instead of a parent object // parent: { // id: string @@ -29,13 +32,23 @@ export interface MapAreaFeatureProperties { // } } +export interface HoverInfo { + geometry: Point | Polygon + data: MapAreaFeatureProperties + mapInstance: MapInstance +} + /** * Area map */ const AreaMap: React.FC = ({ area, subAreas }) => { - const [hovered, setHovered] = useState(null) - const [selected, setSelected] = useState(null) - const mapRef = useRef(null) + const [clickInfo, setClickInfo] = useState(null) + const [hoverInfo, setHoverInfo] = useState(null) + const [selected, setSelected] = useState(null) + const [mapInstance, setMapInstance] = useState(null) + const [cursor, setCursor] = useState('default') + const mapRef = useRef(null) + let fitBoundOpts: any = { padding: { top: 45, left: 45, bottom: 45, right: 45 } } if (subAreas.length === 0) { fitBoundOpts = { maxZoom: 14 } @@ -44,25 +57,51 @@ const AreaMap: React.FC = ({ area, subAreas }) => { const { metadata } = area const boundary = metadata?.polygon == null ? null : lineToPolygon(lineString(metadata.polygon), { properties: { name: area.areaName } }) - const onClick = (event: MapLayerMouseEvent): void => { - const feature = event?.features?.[0] - if (feature == null) { - setSelected(null) - setHovered(null) - } else { - setSelected(feature.geometry as unknown as Point) - setHovered(feature.properties as MapAreaFeatureProperties) - } - } - useEffect(() => { + if (mapRef.current != null) { + setMapInstance(mapRef.current) + } /** * Show drop pin if viewing a leaf area */ if (metadata.leaf) { setSelected(point([metadata.lng, metadata.lat]).geometry as unknown as Point) } - }, [metadata.leaf]) + }, [metadata.leaf, mapRef?.current]) + + const onClick = useCallback((event: MapLayerMouseEvent): void => { + const feature = event?.features?.[0] + if (feature == null) { + setSelected(null) + setClickInfo(null) + } else { + setSelected(feature.geometry as Point | Polygon) + setClickInfo(feature.properties as MapAreaFeatureProperties) + } + }, [mapInstance]) + + const onHover = useCallback((event: MapLayerMouseEvent) => { + const obLayerId = event.features?.findIndex((f) => f.layer.id === 'crags' || f.layer.id === 'crag-group-boundaries') ?? -1 + + if (obLayerId !== -1) { + setCursor('pointer') + const feature = event.features?.[obLayerId] + if (feature != null && mapInstance != null) { + const { geometry } = feature + if (geometry.type === 'Point' || geometry.type === 'Polygon') { + setHoverInfo({ + geometry: feature.geometry as Point | Polygon, + data: feature.properties as MapAreaFeatureProperties, + mapInstance + }) + } + } + } else { + setHoverInfo(null) + setCursor('default') + } + }, [mapInstance]) + return ( = ({ area, subAreas }) => { bounds: metadata.bbox, fitBoundsOptions: fitBoundOpts }} - onClick={useDebouncedCallback(onClick, 200, { leading: true, maxWait: 200 })} + onDragStart={() => { + setCursor('move') + }} + onDragEnd={() => { + setCursor('default') + }} + onMouseEnter={onHover} + onMouseLeave={() => { + setHoverInfo(null) + setCursor('default') + }} + onClick={onClick} reuseMaps mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_API_KEY} mapStyle={MAP_STYLES.light} + cursor={cursor} cooperativeGestures - interactiveLayerIds={['crags']} + interactiveLayerIds={['crags', 'crag-group-boundaries']} > {selected != null && - } - + } + {boundary != null && } + {hoverInfo != null && } ) @@ -106,7 +158,8 @@ const areaPolygonStyle: LineLayer = { type: 'line', paint: { 'line-opacity': ['step', ['zoom'], 0.85, 10, 0.5], - 'line-width': ['step', ['zoom'], 2, 10, 6], - 'line-color': 'rgb(219,39,119)' + 'line-width': ['step', ['zoom'], 2, 10, 8], + 'line-color': 'rgb(219,39,119)', + 'line-blur': 4 } }