diff --git a/src/app/(default)/area/[[...slug]]/page.tsx b/src/app/(default)/area/[[...slug]]/page.tsx index eda0061f8..d9ba5539f 100644 --- a/src/app/(default)/area/[[...slug]]/page.tsx +++ b/src/app/(default)/area/[[...slug]]/page.tsx @@ -3,7 +3,6 @@ import Link from 'next/link' import { Metadata } from 'next' import { validate } from 'uuid' import { MapPinLine, Lightbulb, ArrowRight } from '@phosphor-icons/react/dist/ssr' -import 'mapbox-gl/dist/mapbox-gl.css' import Markdown from 'react-markdown' import PhotoMontage, { UploadPhotoCTA } from '@/components/media/PhotoMontage' diff --git a/src/app/(default)/components/ui/AreaPageContainer.tsx b/src/app/(default)/components/ui/AreaPageContainer.tsx index f33316a7c..0e13e6496 100644 --- a/src/app/(default)/components/ui/AreaPageContainer.tsx +++ b/src/app/(default)/components/ui/AreaPageContainer.tsx @@ -26,7 +26,7 @@ export const AreaPageContainer: React.FC<{ {breadcrumbs == null ? : breadcrumbs} {children == null ? : children} -
+
{map != null && map}
diff --git a/src/app/(maps)/components/FullScreenMap.tsx b/src/app/(maps)/components/FullScreenMap.tsx new file mode 100644 index 000000000..1642b840f --- /dev/null +++ b/src/app/(maps)/components/FullScreenMap.tsx @@ -0,0 +1,34 @@ +'use client' +import { useEffect, useState } from 'react' +import { GlobalMap } from '@/components/maps/GlobalMap' + +export const FullScreenMap: React.FC = () => { + const [initialCenter, setInitialCenter] = useState<[number, number] | undefined>(undefined) + + useEffect(() => { + getVisitorLocation().then((visitorLocation) => { + if (visitorLocation != null) { + setInitialCenter([visitorLocation.longitude, visitorLocation.latitude]) + } + }).catch(() => { + console.log('Unable to determine user\'s location') + }) + }, []) + + return ( + + ) +} + +const getVisitorLocation = async (): Promise<{ longitude: number, latitude: number } | undefined> => { + try { + const res = await fetch('/api/geo') + return await res.json() + } catch (err) { + console.log('ERROR', err) + return undefined + } +} diff --git a/src/app/(maps)/maps/page.tsx b/src/app/(maps)/maps/page.tsx index a93212152..d6fef63c8 100644 --- a/src/app/(maps)/maps/page.tsx +++ b/src/app/(maps)/maps/page.tsx @@ -1,5 +1,5 @@ -import { GlobalMap } from '@/components/maps/GlobalMap' import { ProfileMenu } from '../components/ProfileMenu' +import { FullScreenMap } from '../components/FullScreenMap' export const dynamic = 'force-dynamic' @@ -7,9 +7,7 @@ export default async function MapPage (): Promise { return (
- +
) } diff --git a/src/app/global.css b/src/app/global.css index be875b059..f85899803 100644 --- a/src/app/global.css +++ b/src/app/global.css @@ -137,9 +137,12 @@ A slightly deemphasized dotted underline for a tag in order to not competing wit } /** - * Force mapbox-gl library to use our font otherwise components inside the map + * Force mapbox-gl/maplibre 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; +} +.maplibregl-map { + font-family: inherit !important; } \ No newline at end of file diff --git a/src/components/maps/AreaInfoHover.tsx b/src/components/maps/AreaInfoHover.tsx index be662434a..fb54a7b3f 100644 --- a/src/components/maps/AreaInfoHover.tsx +++ b/src/components/maps/AreaInfoHover.tsx @@ -1,16 +1,14 @@ 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/(default)/editArea/[slug]/general/components/AreaItem' import { SelectedPolygon } from './AreaActiveMarker' +import { HoverInfo, MapAreaFeatureProperties } from './GlobalMap' + /** * Area info panel */ export const AreaInfoHover: React.FC = ({ data, geometry, mapInstance }) => { - const ancestors = data?.ancestors == null ? null : JSON.parse(data.ancestors) - const pathTokens = data?.pathTokens == null ? null : JSON.parse(data.pathTokens) - let screenXY if (geometry.type === 'Point') { screenXY = mapInstance.project(geometry.coordinates) @@ -18,36 +16,25 @@ export const AreaInfoHover: React.FC = ({ data, geometry, mapInstance return } - const parentId = ancestors?.[ancestors.length - 2] ?? null - const parentName = pathTokens?.[pathTokens.length - 2] ?? 'Unknown' return ( - - {data != null && } - + + {data != null && } ) } -export const Content: React.FC = ({ id, name, parentName, parentId }) => { - const url = parentId == null - ? parentName - : ( - - {parentName} - - ) +export const Content: React.FC = ({ id, areaName, climbs }) => { return (
-
{url}
-
- {name} + {areaName} +
+ + ยท + {climbs.length} climbs
diff --git a/src/components/maps/AreaMap.tsx b/src/components/maps/AreaMap.tsx index bf75fd589..eb8d7cf90 100644 --- a/src/components/maps/AreaMap.tsx +++ b/src/components/maps/AreaMap.tsx @@ -1,15 +1,12 @@ 'use client' -import { useCallback, useEffect, useRef, useState } from 'react' -import { Map, ScaleControl, FullscreenControl, NavigationControl, Source, Layer, MapLayerMouseEvent, LineLayer, MapInstance } from 'react-map-gl' +import { Source, Layer, LineLayer, MapInstance } from 'react-map-gl' import dynamic from 'next/dynamic' -import { lineString, Point, Polygon, point } from '@turf/helpers' +import { lineString, Point, Polygon } from '@turf/helpers' import lineToPolygon from '@turf/line-to-polygon' +import 'maplibre-gl/dist/maplibre-gl.css' import { AreaMetadataType, AreaType } from '../../js/types' -import { MAP_STYLES } from './BaseMap' -import { AreaInfoDrawer } from './AreaInfoDrawer' -import { AreaInfoHover } from './AreaInfoHover' -import { SelectedFeature } from './AreaActiveMarker' +import { GlobalMap } from './GlobalMap' type ChildArea = Pick & { metadata: Pick } interface AreaMapProps { @@ -42,13 +39,6 @@ export interface HoverInfo { * Area map */ const AreaMap: React.FC = ({ area, subAreas }) => { - 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 } @@ -56,92 +46,20 @@ const AreaMap: React.FC = ({ area, subAreas }) => { const { metadata } = area const boundary = metadata?.polygon == null ? null : lineToPolygon(lineString(metadata.polygon), { properties: { name: area.areaName } }) - - 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, 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 (
- { - 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', 'crag-group-boundaries']} > - - - - {selected != null && - } - {boundary != null && } - {hoverInfo != null && } - +
) } @@ -158,7 +76,7 @@ const areaPolygonStyle: LineLayer = { type: 'line', paint: { 'line-opacity': ['step', ['zoom'], 0.85, 10, 0.5], - 'line-width': ['step', ['zoom'], 2, 10, 8], + 'line-width': ['step', ['zoom'], 4, 8, 6], 'line-color': 'rgb(219,39,119)', 'line-blur': 4 } diff --git a/src/components/maps/GlobalMap.tsx b/src/components/maps/GlobalMap.tsx index 016f05278..51c6405e1 100644 --- a/src/components/maps/GlobalMap.tsx +++ b/src/components/maps/GlobalMap.tsx @@ -1,27 +1,22 @@ 'use client' -import { useCallback, useEffect, useState } from 'react' +import { useCallback, useState } from 'react' import { Map, ScaleControl, FullscreenControl, NavigationControl, MapLayerMouseEvent, MapInstance } from 'react-map-gl/maplibre' import maplibregl, { MapLibreEvent } from 'maplibre-gl' -import { Protocol } from 'pmtiles' import { Point, Polygon } from '@turf/helpers' +import dynamic from 'next/dynamic' import { MAP_STYLES } from './BaseMap' import { AreaInfoDrawer } from './AreaInfoDrawer' import { AreaInfoHover } from './AreaInfoHover' import { SelectedFeature } from './AreaActiveMarker' import { OBCustomLayers } from './OBCustomLayers' +import { AreaType, ClimbType } from '@/js/types' +import { TileProps, transformTileProps } from './utils' -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 - // name: string - // } +export type SimpleClimbType = Pick + +export type MapAreaFeatureProperties = Pick & { + climbs: SimpleClimbType[] } export interface HoverInfo { @@ -32,34 +27,32 @@ export interface HoverInfo { interface GlobalMapProps { showFullscreenControl?: boolean - initialCenter?: { longitude: number, latitude: number } + initialCenter?: [number, number] + initialViewState?: { + bounds: any + fitBoundsOptions: any + } + children?: React.ReactNode } /** * Global map */ -export const GlobalMap: React.FC = ({ showFullscreenControl = true }) => { - const [initialCenter, setInitialCenter] = useState<[number, number] | undefined>(undefined) +export const GlobalMap: React.FC = ({ + showFullscreenControl = true, initialCenter, initialViewState, children +}) => { 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') - useEffect(() => { - getVisitorLocation().then((visitorLocation) => { - if (visitorLocation != null) { - setInitialCenter([visitorLocation.longitude, visitorLocation.latitude]) - } - }).catch(() => { - console.log('Unable to determine user\'s location') - }) - }, []) - const onLoad = useCallback((e: MapLibreEvent) => { setMapInstance(e.target) if (initialCenter != null) { e.target.jumpTo({ center: initialCenter, zoom: 6 }) + } else if (initialViewState != null) { + e.target.fitBounds(initialViewState.bounds, initialViewState.fitBoundsOptions) } }, [initialCenter]) @@ -85,7 +78,7 @@ export const GlobalMap: React.FC = ({ showFullscreenControl = tr if (geometry.type === 'Point' || geometry.type === 'Polygon') { setHoverInfo({ geometry: feature.geometry as Point | Polygon, - data: feature.properties as MapAreaFeatureProperties, + data: transformTileProps(feature.properties as TileProps), mapInstance }) } @@ -96,14 +89,6 @@ export const GlobalMap: React.FC = ({ showFullscreenControl = tr } }, [mapInstance]) - // useEffect(() => { - // const protocol = new Protocol() - // maplibregl.addProtocol('pmtiles', protocol.tile) - // return () => { - // maplibregl.removeProtocol('pmtiles') - // } - // }, []) - return (
= ({ showFullscreenControl = tr }} onClick={onClick} reuseMaps - mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_API_KEY} mapStyle={MAP_STYLES.dataviz} cursor={cursor} cooperativeGestures={showFullscreenControl} @@ -137,17 +121,13 @@ export const GlobalMap: React.FC = ({ showFullscreenControl = tr } {hoverInfo != null && } + {children}
) } -const getVisitorLocation = async (): Promise<{ longitude: number, latitude: number } | undefined> => { - try { - const res = await fetch('/api/geo') - return await res.json() - } catch (err) { - console.log('ERROR', err) - return undefined - } -} +export const LazyGlobalMap = dynamic(async () => await import('./GlobalMap').then( + module => module.GlobalMap), { + ssr: false +}) diff --git a/src/components/maps/utils.ts b/src/components/maps/utils.ts new file mode 100644 index 000000000..1493c76ac --- /dev/null +++ b/src/components/maps/utils.ts @@ -0,0 +1,27 @@ +import { MapAreaFeatureProperties, SimpleClimbType } from './GlobalMap' + +export interface TileProps { + id: string + name: string + ancestors: string + pathTokens: string + content: { + description: string + } + climbs: string +} + +/** + * Map tile properties can only contain primitive types. + * This function converts stringified json data back to json objects + */ +export const transformTileProps = (p: TileProps): MapAreaFeatureProperties => { + const { name, ancestors, pathTokens } = p + return { + ...p, + areaName: name, + ancestors: JSON.parse(ancestors) as string[], + pathTokens: JSON.parse(pathTokens) as string[], + climbs: JSON.parse(p.climbs) as SimpleClimbType[] + } +}