= ({ geometry }) => {
- switch (geometry.type) {
- case 'Point':
- return
- case 'Polygon':
- return
+export const SelectedFeature: React.FC<{ feature: ActiveFeature }> = ({ feature }) => {
+ switch (feature.type) {
+ case 'crag-markers':
+ case 'crag-name-labels':
+ return
default: return null
}
}
@@ -19,26 +19,9 @@ 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
deleted file mode 100644
index 64f529647..000000000
--- a/src/components/maps/AreaInfoDrawer.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as Popover from '@radix-ui/react-popover'
-
-import { MapAreaFeatureProperties, SimpleClimbType } from './GlobalMap'
-import { getAreaPageFriendlyUrl } from '@/js/utils'
-import { Card } from '../core/Card'
-import { EntityIcon } from '@/app/(default)/editArea/[slug]/general/components/AreaItem'
-
-/**
- * Area info panel
- */
-export const AreaInfoDrawer: React.FC<{ data: MapAreaFeatureProperties | null, onClose?: () => void }> = ({ data, onClose }) => {
- return (
-
-
-
- {data != null && }
-
-
- )
-}
-
-export const Content: React.FC
= ({ id, areaName, climbs, content: { description }, media }) => {
- const friendlyUrl = getAreaPageFriendlyUrl(id, areaName)
- const editUrl = `/editArea/${id}/general`
- return (
-
-
-
-
-
Edit area
-
-
-
-
- {description == null || description.trim() === ''
- ? No description available. [Add]
- : {description}
}
-
-
-
-
-
-
-
- )
-}
-
-const MicroClimbList: React.FC<{ climbs: SimpleClimbType[] }> = ({ climbs }) => {
- return (
-
- Climbs
-
- {climbs.map((climb) => {
- const url = `/climb/${climb.id}`
- return (
- -
- {climb.name}
-
- )
- })}
-
-
- )
-}
diff --git a/src/components/maps/AreaInfoHover.tsx b/src/components/maps/AreaInfoHover.tsx
deleted file mode 100644
index 703e7e779..000000000
--- a/src/components/maps/AreaInfoHover.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import * as Popover from '@radix-ui/react-popover'
-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'
-import { MiniCarousel } from './CardGallery'
-
-/**
- * Area info panel.
- * By default a mouse click on the panel will select the
- * underlying feature and activate the side drawer. For links/buttons
- * we need to call event.stopPropagation() to prevent the panel from
- * receiving the click event.
- */
-export const AreaInfoHover: React.FC void
-}> = ({ data, geometry, mapInstance, onClick }) => {
- let screenXY
- if (geometry.type === 'Point') {
- screenXY = mapInstance.project(geometry.coordinates)
- } else {
- return
- }
-
- return (
-
-
- {
- e.stopPropagation()
- onClick({ data, geometry, mapInstance })
- }}
- >
- {data != null && }
-
-
- )
-}
-
-export const Content: React.FC = ({ id, areaName, climbs, media }) => {
- return (
- }>
-
-
- )
-}
diff --git a/src/components/maps/CardGallery.tsx b/src/components/maps/CardGallery.tsx
index 06edff89e..58cf6999f 100644
--- a/src/components/maps/CardGallery.tsx
+++ b/src/components/maps/CardGallery.tsx
@@ -3,7 +3,7 @@ import Image from 'next/image'
import clx from 'classnames'
import { usePrevNextButtons, PrevButton, NextButton } from '../carousel/useNextPrevButtons'
-import { MediaWithTagsInMapTile } from './GlobalMap'
+import { MediaWithTagsInMapTile } from './TileTypes'
export const CardGallery: React.FC<{ media: MediaWithTagsInMapTile[] }> = () => {
return (
@@ -25,8 +25,8 @@ export const MiniCarousel: React.FC<{ mediaList: MediaWithTagsInMapTile[] }> = (
const isSingle = mediaList.length === 1
return (
-
- {mediaList.map((m) => (
))}
+
+ {mediaList.map((m) => ())}
{!isSingle && (
@@ -46,7 +46,7 @@ export const MiniCarousel: React.FC<{ mediaList: MediaWithTagsInMapTile[] }> = (
const Slide: React.FC<{ media: MediaWithTagsInMapTile, isSingle: boolean }> = ({ media, isSingle }) => {
const { mediaUrl, width, height } = media
return (
-
diff --git a/src/components/maps/GlobalMap.tsx b/src/components/maps/GlobalMap.tsx
index 2841851a3..1a41463f6 100644
--- a/src/components/maps/GlobalMap.tsx
+++ b/src/components/maps/GlobalMap.tsx
@@ -1,33 +1,19 @@
'use client'
import { useCallback, useState } from 'react'
-import { Map, FullscreenControl, ScaleControl, NavigationControl, MapLayerMouseEvent, MapInstance, ViewStateChangeEvent } from 'react-map-gl/maplibre'
+import { Map, FullscreenControl, ScaleControl, NavigationControl, MapLayerMouseEvent, ViewStateChangeEvent } from 'react-map-gl/maplibre'
import maplibregl, { MapLibreEvent } from 'maplibre-gl'
-import { Point, Polygon } from '@turf/helpers'
import dynamic from 'next/dynamic'
import { MAP_STYLES, type MapStyles } from './MapSelector'
-import { AreaInfoDrawer } from './AreaInfoDrawer'
-import { AreaInfoHover } from './AreaInfoHover'
-import { SelectedFeature } from './AreaActiveMarker'
+import { Drawer } from './TileHandlers/Drawer'
+import { HoverCard } from './TileHandlers/HoverCard'
import { OBCustomLayers } from './OBCustomLayers'
-import { AreaType, ClimbType, MediaWithTags } from '@/js/types'
-import { TileProps, transformTileProps } from './utils'
+import { tileToFeature } from './utils'
+import { ActiveFeature, TileProps } from './TileTypes'
import MapLayersSelector from './MapLayersSelector'
import { debounce } from 'underscore'
-
-export type SimpleClimbType = Pick
-
-export type MediaWithTagsInMapTile = Omit & { _id: string }
-export type MapAreaFeatureProperties = Pick & {
- climbs: SimpleClimbType[]
- media: MediaWithTagsInMapTile[]
-}
-
-export interface HoverInfo {
- geometry: Point | Polygon
- data: MapAreaFeatureProperties
- mapInstance: MapInstance
-}
+import { MapToolbar } from './MapToolbar'
+import { SelectedFeature } from './AreaActiveMarker'
export interface CameraInfo {
center: {
@@ -37,6 +23,16 @@ export interface CameraInfo {
zoom: number
}
+interface FeatureState {
+ selected?: boolean
+ hover?: boolean
+}
+export interface DataLayersDisplayState {
+ areaBoundaries: boolean
+ organizations: boolean
+ heatmap: boolean
+ crags: boolean
+}
interface GlobalMapProps {
showFullscreenControl?: boolean
initialCenter?: [number, number]
@@ -55,12 +51,26 @@ interface GlobalMapProps {
export const GlobalMap: React.FC = ({
showFullscreenControl = true, initialCenter, initialZoom, initialViewState, onCameraMovement, children
}) => {
- const [clickInfo, setClickInfo] = useState(null)
- const [hoverInfo, setHoverInfo] = useState(null)
- const [selected, setSelected] = useState(null)
- const [mapInstance, setMapInstance] = useState(null)
+ const [clickInfo, setClickInfo] = useState(null)
+ const [hoverInfo, setHoverInfo] = useState(null)
+ const [mapInstance, setMapInstance] = useState(null)
const [cursor, setCursor] = useState('default')
- const [mapStyle, setMapStyle] = useState(MAP_STYLES.standard.style)
+ const [mapStyle, setMapStyle] = useState(MAP_STYLES.light.style)
+ const [dataLayersDisplayState, setDataLayersDisplayState] = useState({
+ areaBoundaries: false,
+ organizations: false,
+ heatmap: false,
+ crags: true
+ })
+
+ const setActiveFeatureVisual = (feature: ActiveFeature | null, fState: FeatureState): void => {
+ if (feature == null || mapInstance == null) return
+ mapInstance.setFeatureState({
+ source: 'areas',
+ sourceLayer: 'areas',
+ id: feature.data.id
+ }, fState)
+ }
const onMove = useCallback(debounce((e: ViewStateChangeEvent) => {
if (onCameraMovement != null) {
@@ -87,49 +97,61 @@ export const GlobalMap: React.FC = ({
/**
* Handle click event on the map. Place a market on the map and activate the side drawer.
*/
- const onClick = useCallback((event: MapLayerMouseEvent): void => {
+ const onClick = (event: MapLayerMouseEvent): void => {
+ if (mapInstance == null) return
const feature = event?.features?.[0]
if (feature == null) {
- setSelected(null)
setClickInfo(null)
} else {
- setSelected(feature.geometry as Point | Polygon)
- setClickInfo(transformTileProps(feature.properties as TileProps))
+ const { layer, geometry, properties } = feature
+
+ setClickInfo(prev => {
+ setActiveFeatureVisual(prev, { selected: false, hover: false })
+ const activeFeature = tileToFeature(layer.id, event.point, geometry, properties as TileProps, mapInstance)
+ setActiveFeatureVisual(activeFeature, { selected: true, hover: false })
+ return activeFeature
+ })
}
- }, [mapInstance])
+ }
/**
* Handle click event on the popover. Behave as if the user clicked on a feature on the map.
*/
- const onHoverCardClick = ({ geometry, data }: HoverInfo): void => {
- setSelected(geometry)
- setClickInfo(data)
+ const onHoverCardClick = (feature: ActiveFeature): void => {
+ setClickInfo(prevFeature => {
+ setHoverInfo(null)
+ setActiveFeatureVisual(prevFeature, { selected: false, hover: false })
+ if (feature.type === 'area-boundaries') {
+ setActiveFeatureVisual(feature, { selected: true, hover: false })
+ }
+ return feature
+ })
}
/**
- * Handle over event on the map. Show the popover with the area info.
+ * Handle mouseover event on the map. Show the popover with the area info.
*/
- const onHover = useCallback((event: MapLayerMouseEvent) => {
- const obLayerId = event.features?.findIndex((f) => f.layer.id === 'crags' || f.layer.id === 'crag-group-boundaries') ?? -1
+ const onHover = (event: MapLayerMouseEvent): void => {
+ const obLayerId = event.features?.findIndex((f) => f.layer.id === 'crag-markers' || f.layer.id === 'crag-name-labels' || f.layer.id === 'area-boundaries' || f.layer.id === 'area-background') ?? -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: transformTileProps(feature.properties as TileProps),
- mapInstance
- })
- }
+ const { layer, geometry, properties } = feature
+ setHoverInfo(prev => {
+ setActiveFeatureVisual(prev, { hover: false })
+ const feat = tileToFeature(layer.id, event.point, geometry, properties as TileProps, mapInstance)
+ setActiveFeatureVisual(feat, { hover: true })
+ return feat
+ })
}
} else {
setHoverInfo(null)
setCursor('default')
}
- }, [mapInstance])
+ }
const updateMapLayer = (key: keyof MapStyles): void => {
const style = MAP_STYLES[key]
@@ -150,27 +172,31 @@ export const GlobalMap: React.FC = ({
}}
onMouseEnter={onHover}
onMouseLeave={() => {
- setHoverInfo(null)
+ setHoverInfo(prev => {
+ setActiveFeatureVisual(prev, { hover: false })
+ return null
+ })
setCursor('default')
}}
onClick={onClick}
mapStyle={mapStyle}
cursor={cursor}
cooperativeGestures={showFullscreenControl}
- interactiveLayerIds={['crags', 'crag-group-boundaries']}
+ interactiveLayerIds={['crag-markers', 'crag-name-labels', 'area-boundaries', 'organizations']}
>
+
-
+
{showFullscreenControl && }
- {selected != null &&
- }
-
+ {clickInfo != null &&
+ }
+
{hoverInfo != null && (
- )}
diff --git a/src/components/maps/MapLayersSelector.tsx b/src/components/maps/MapLayersSelector.tsx
index 49598d53b..dbbd7c0c7 100644
--- a/src/components/maps/MapLayersSelector.tsx
+++ b/src/components/maps/MapLayersSelector.tsx
@@ -7,8 +7,8 @@ interface Props {
}
const MapLayersSelector: React.FC = ({ emit }) => {
- const [mapImgUrl, setMapImgUrl] = useState(MAP_STYLES.standard.imgUrl)
- const [mapName, setMapName] = useState('standard')
+ const [mapImgUrl, setMapImgUrl] = useState(MAP_STYLES.light.imgUrl)
+ const [mapName, setMapName] = useState('light')
const emitMap = (key: string): void => {
const styleKey = key as keyof MapStyles
diff --git a/src/components/maps/MapSelector.tsx b/src/components/maps/MapSelector.tsx
index 00e28777e..528fead1f 100644
--- a/src/components/maps/MapSelector.tsx
+++ b/src/components/maps/MapSelector.tsx
@@ -4,13 +4,13 @@ export const MAP_STYLES: MapStyles = {
style: `https://api.maptiler.com/maps/outdoor-v2/style.json?key=${MAPTILER_KEY}`,
imgUrl: 'https://docs.maptiler.com/sdk-js/api/map-styles/img/style-outdoor-v2.jpeg'
},
- minimal: {
+ light: {
style: `https://api.maptiler.com/maps/dataviz/style.json?key=${MAPTILER_KEY}`,
imgUrl: 'https://docs.maptiler.com/sdk-js/api/map-styles/img/style-bright-v2-pastel.jpeg'
},
- standard: {
- style: `https://api.maptiler.com/maps/basic/style.json?key=${MAPTILER_KEY}`,
- imgUrl: 'https://docs.maptiler.com/sdk-js/api/map-styles/img/style-basic-v2.jpeg'
+ dark: {
+ style: `https://api.maptiler.com/maps/dataviz-dark/style.json?key=${MAPTILER_KEY}`,
+ imgUrl: 'https://docs.maptiler.com/sdk-js/api/map-styles/img/style-dataviz-dark.jpeg'
},
satellite: {
style: `https://api.maptiler.com/maps/satellite/style.json?key=${MAPTILER_KEY}`,
@@ -22,11 +22,11 @@ export interface MapStyles {
style: string
imgUrl: string
}
- minimal: {
+ light: {
style: string
imgUrl: string
}
- standard: {
+ dark: {
style: string
imgUrl: string
}
diff --git a/src/components/maps/MapToolbar.tsx b/src/components/maps/MapToolbar.tsx
new file mode 100644
index 000000000..0f4d0f4de
--- /dev/null
+++ b/src/components/maps/MapToolbar.tsx
@@ -0,0 +1,43 @@
+import { ChangeEventHandler } from 'react'
+import { DataLayersDisplayState } from './GlobalMap'
+
+export interface MapToolbarProps {
+ layerState: DataLayersDisplayState
+ onChange: (newLayerState: DataLayersDisplayState) => void
+}
+
+/**
+ * Toolbar for filtering/toggling data layers
+ */
+export const MapToolbar: React.FC = ({ onChange, layerState }) => {
+ const { areaBoundaries, crags } = layerState
+ return (
+
+
+ onChange({ ...layerState, crags: !crags })}
+ />
+ onChange({ ...layerState, areaBoundaries: !areaBoundaries })}
+ />
+
+
+ )
+}
+
+const Checkbox: React.FC<{ value: boolean, label: string, onChange: ChangeEventHandler }> = ({ value, onChange, label }) => {
+ return (
+
+
+
+
+
+ )
+}
diff --git a/src/components/maps/OBCustomLayers.tsx b/src/components/maps/OBCustomLayers.tsx
index 1e732a7ed..c6c1d5d0c 100644
--- a/src/components/maps/OBCustomLayers.tsx
+++ b/src/components/maps/OBCustomLayers.tsx
@@ -1,45 +1,102 @@
import { Source, Layer } from 'react-map-gl'
+import { DataLayersDisplayState } from './GlobalMap'
+interface OBCustomLayersProps {
+ layersState: DataLayersDisplayState
+}
/**
* OpenBeta custom map tiles.
* - Crags: crag markers and labels
- * - Crag groups: polygon boundaries for crag groups (TBD)
+ * - Areas: polygon boundaries for areas
*/
-export const OBCustomLayers: React.FC = () => {
+export const OBCustomLayers: React.FC = ({ layersState }) => {
+ const { areaBoundaries, crags } = layersState
return (
-
+ <>
+
+
+ >
)
}
diff --git a/src/components/maps/TileHandlers/AreaContent.tsx b/src/components/maps/TileHandlers/AreaContent.tsx
new file mode 100644
index 000000000..22a8f8a42
--- /dev/null
+++ b/src/components/maps/TileHandlers/AreaContent.tsx
@@ -0,0 +1,71 @@
+import Link from 'next/link'
+import { CragGroupFeatureProps, SubArea } from '../TileTypes'
+import { getAreaPageFriendlyUrl } from '@/js/utils'
+import { EntityIcon } from '@/app/(default)/editArea/[slug]/general/components/AreaItem'
+import { BaseDrawerContent } from './Drawer'
+import { MiniCarousel } from '../CardGallery'
+
+export const CUSTOM_CLASS = 'max-h-screen lg:w-[420px] rounded-none'
+
+export const AreaDrawerContent: React.FC = ({ id, areaName: name, subareas, totalClimbs, media }) => {
+ const editUrl = `/editArea/${id}/general`
+ return (
+ }
+ heading={{name}}
+ subheading={}
+ cta={Edit area}
+ >
+
+
+ )
+}
+
+const SubAreas: React.FC<{ subareas: SubArea[] }> = ({ subareas }) => {
+ return (
+
+ {subareas.map(({ id, areaName, totalClimbs }, idx) => (
+ -
+ {idx + 1}.
+
+
{areaName}
+
+
+ {totalClimbs}
+
+
+ ))}
+
+ )
+}
+
+export const AreaHoverCardContent: React.FC = ({ id, areaName: name, subareas, media, totalClimbs }) => {
+ return (
+ <>
+ e.stopPropagation()}
+ >
+ {name}
+
+
+ >
+ )
+}
+
+const Subheading: React.FC<{ subareas: SubArea[], totalClimbs: number }> = ({ subareas, totalClimbs }) => {
+ return (
+
+
+
+
+
+ {subareas.length} Sub-areas
+
+
+ {Intl.NumberFormat().format(totalClimbs)} climbs
+
+
+ )
+}
diff --git a/src/components/maps/TileHandlers/CragContent.tsx b/src/components/maps/TileHandlers/CragContent.tsx
new file mode 100644
index 000000000..99cf012bf
--- /dev/null
+++ b/src/components/maps/TileHandlers/CragContent.tsx
@@ -0,0 +1,74 @@
+import Link from 'next/link'
+import { CragFeatureProperties, SimpleClimbType } from '../TileTypes'
+import { getAreaPageFriendlyUrl } from '@/js/utils'
+import { EntityIcon } from '@/app/(default)/editArea/[slug]/general/components/AreaItem'
+import { BaseDrawerContent } from './Drawer'
+import { MiniCarousel } from '../CardGallery'
+
+export const CragDrawerContent: React.FC = ({ id, areaName, climbs, content: { description }, media }) => {
+ const friendlyUrl = getAreaPageFriendlyUrl(id, areaName)
+ const editUrl = `/editArea/${id}/general`
+ return (
+ <>
+ }
+ heading={{areaName}}
+ subheading={}
+ cta={Edit area}
+ >
+
+ {description == null || description.trim() === ''
+ ? No description available. [Add]
+ : {description}
}
+
+
+
+
+
+ >
+ )
+}
+
+const Subheading: React.FC<{ id: string, totalClimbs: number }> = ({ id, totalClimbs }) => {
+ return (
+
+
+
+ {Intl.NumberFormat().format(totalClimbs)} climbs
+
+
+ )
+}
+
+const MicroClimbList: React.FC<{ climbs: SimpleClimbType[] }> = ({ climbs }) => {
+ return (
+
+ Climbs
+
+ {climbs.map((climb) => {
+ const url = `/climb/${climb.id}`
+ return (
+ -
+ {climb.name}
+
+ )
+ })}
+
+
+ )
+}
+
+export const CragHoverCardContent: React.FC = ({ id, areaName, climbs }) => {
+ return (
+
+ )
+}
diff --git a/src/components/maps/TileHandlers/Drawer.tsx b/src/components/maps/TileHandlers/Drawer.tsx
new file mode 100644
index 000000000..fe23843af
--- /dev/null
+++ b/src/components/maps/TileHandlers/Drawer.tsx
@@ -0,0 +1,58 @@
+import { ReactNode } from 'react'
+import * as Popover from '@radix-ui/react-popover'
+
+import { Card } from '@/components/core/Card'
+import { ActiveFeature, CragFeatureProperties, CragGroupFeatureProps } from '../TileTypes'
+import { CragDrawerContent } from './CragContent'
+import { AreaDrawerContent } from './AreaContent'
+
+/**
+ * Side drawer panel
+ */
+export const Drawer: React.FC<{ feature: ActiveFeature | null, onClose?: () => void }> = ({ feature, onClose }) => {
+ if (feature == null) return null
+ let ContentComponent = null
+ switch (feature.type) {
+ case 'crag-markers':
+ case 'crag-name-labels':
+ ContentComponent =
+ break
+ case 'area-boundaries':
+ ContentComponent =
+ break
+ default:
+ return null
+ }
+ return (
+
+
+
+ {ContentComponent}
+
+
+ )
+}
+
+export const BaseDrawerContent: React.FC<{ media: ReactNode, heading: ReactNode, subheading: ReactNode, cta: ReactNode, children: ReactNode }> = ({ media, heading, subheading, cta, children }) => {
+ return (
+
+
+ {media}
+
+
+
+
+ {heading}
+
+ {subheading}
+
+
+ {cta}
+
+
+
+ {children}
+
+
+ )
+}
diff --git a/src/components/maps/TileHandlers/HoverCard.tsx b/src/components/maps/TileHandlers/HoverCard.tsx
new file mode 100644
index 000000000..b130ea3bb
--- /dev/null
+++ b/src/components/maps/TileHandlers/HoverCard.tsx
@@ -0,0 +1,60 @@
+import * as Popover from '@radix-ui/react-popover'
+import { Card } from '../../core/Card'
+import { ActiveFeature, CragFeatureProperties, CragGroupFeatureProps } from '../TileTypes'
+import { MiniCarousel } from '../CardGallery'
+import { AreaHoverCardContent } from './AreaContent'
+import { CragHoverCardContent } from './CragContent'
+
+/**
+ * Hover card.
+ * By default a mouse click on the panel will select the
+ * underlying feature and activate the side drawer. For links/buttons
+ * we need to call event.stopPropagation() to prevent the drawer to open.
+ */
+export const HoverCard: React.FC void
+}> = (props) => {
+ const { type, data, point, geometry, mapInstance, onClick } = props
+ let screenXY
+ let ContentComponent = null
+
+ switch (type) {
+ case 'crag-markers':
+ case 'crag-name-labels':
+ screenXY = mapInstance.project(geometry.coordinates)
+ ContentComponent =
+ break
+ case 'area-boundaries':
+ screenXY = point
+ ContentComponent =
+ break
+ default:
+ return null
+ }
+
+ const { media } = data
+ return (
+
+
+ {
+ e.stopPropagation()
+ onClick(props)
+ }}
+ >
+ } className='hidden md:block'>
+ {ContentComponent}
+
+
+
+ )
+}
diff --git a/src/components/maps/TileTypes.ts b/src/components/maps/TileTypes.ts
new file mode 100644
index 000000000..8e4ba3c6b
--- /dev/null
+++ b/src/components/maps/TileTypes.ts
@@ -0,0 +1,59 @@
+import { Point, Polygon } from '@turf/helpers'
+import { MapInstance } from 'react-map-gl/maplibre'
+import { AreaType, ClimbType, MediaWithTags } from '@/js/types'
+
+export type LayerId = 'crag-markers' | 'crag-name-labels' | 'area-boundaries'
+
+export type TileProps = CragTileProps | CragGroupTileProps
+
+export interface CragTileProps {
+ id: string
+ name: string
+ ancestors: string
+ pathTokens: string
+ content: string
+ climbs: string
+ media: string
+ totalClimbs: number
+}
+
+export interface CragGroupTileProps {
+ id: string
+ areaName: string
+ pathTokens: string
+ ancestors: string
+ media: string // stringified json
+ children: string // stringified json
+ content: string // stringified json
+ totalClimbs: number
+ aggregate: string // stringified json
+}
+
+export type SimpleClimbType = Pick
+
+export type MediaWithTagsInMapTile = MediaWithTags
+
+type AreaFeatureProperties = Pick & {
+ media: MediaWithTagsInMapTile[]
+}
+
+export type CragFeatureProperties = AreaFeatureProperties & {
+ climbs: SimpleClimbType[]
+}
+
+export type SubArea = Pick
+
+export type CragGroupFeatureProps = Pick & AreaFeatureProperties & {
+ subareas: SubArea[]
+ media: MediaWithTagsInMapTile[]
+}
+
+export type FeatureProps = CragGroupFeatureProps | CragFeatureProperties
+
+export interface ActiveFeature {
+ type: LayerId
+ point: { x: number, y: number }
+ geometry: Point | Polygon
+ data: FeatureProps
+ mapInstance: MapInstance
+}
diff --git a/src/components/maps/utils.ts b/src/components/maps/utils.ts
index 56640da89..7b6689731 100644
--- a/src/components/maps/utils.ts
+++ b/src/components/maps/utils.ts
@@ -1,28 +1,62 @@
-import { MapAreaFeatureProperties, SimpleClimbType } from './GlobalMap'
+import { MapInstance } from 'react-map-gl/maplibre'
+import { Geometry } from 'geojson'
+import { CragFeatureProperties, CragGroupFeatureProps, SimpleClimbType, CragGroupTileProps, CragTileProps, ActiveFeature, SubArea, MediaWithTagsInMapTile } from './TileTypes'
-export interface TileProps {
- id: string
- name: string
- ancestors: string
- pathTokens: string
- content: string
- climbs: string
- media: string
+/**
+ * Convert maplibre tile feature to our model
+ * @param type
+ * @param geometry
+ * @param tile
+ * @param mapInstance
+ */
+export const tileToFeature = (type: string, point: { x: number, y: number }, geometry: Geometry, tile: CragTileProps | CragGroupTileProps, mapInstance: MapInstance): ActiveFeature | null => {
+ switch (type) {
+ case 'crag-markers':
+ case 'crag-name-labels':
+ return {
+ type,
+ point,
+ geometry: geometry as GeoJSON.Point,
+ data: transformCragTileProps(tile as CragTileProps),
+ mapInstance
+ }
+ case 'area-boundaries':
+ return {
+ type,
+ point,
+ geometry: geometry as GeoJSON.Polygon,
+ data: transformCragGroupTileProps(tile as CragGroupTileProps),
+ mapInstance
+ }
+ default: return null
+ }
}
-
/**
* 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, media, content } = p
+const transformCragTileProps = (props: CragTileProps): CragFeatureProperties => {
+ const { name, ancestors, pathTokens, climbs, media, content } = props
return {
- ...p,
+ ...props,
areaName: name,
ancestors: JSON.parse(ancestors) as string[],
pathTokens: JSON.parse(pathTokens) as string[],
- climbs: JSON.parse(p.climbs) as SimpleClimbType[],
+ climbs: JSON.parse(climbs) as SimpleClimbType[],
media: JSON.parse(media),
content: JSON.parse(content)
}
}
+
+const transformCragGroupTileProps = (props: CragGroupTileProps): CragGroupFeatureProps => {
+ const { children, media, ancestors, pathTokens } = props
+ return {
+ ...props,
+ ancestors: JSON.parse(ancestors) as string[],
+ pathTokens: JSON.parse(pathTokens) as string[],
+ content: JSON.parse(props?.content ?? {}),
+ subareas: JSON.parse(children) as SubArea[],
+ media: JSON.parse(media) as MediaWithTagsInMapTile[],
+ aggregate: JSON.parse(props.aggregate)
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index a9119d3e2..0d59a351e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1742,17 +1742,19 @@
rw "^1.3.3"
sort-object "^3.0.3"
-"@maplibre/maplibre-gl-style-spec@^20.1.1":
- version "20.1.1"
- resolved "https://registry.yarnpkg.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.1.1.tgz#94b644493723776c34813bd62a223e748390099b"
- integrity sha512-z85ARNPCBI2Cs5cPOS3DSbraTN+ue8zrcYVoSWBuNrD/mA+2SKAJ+hIzI22uN7gac6jBMnCdpPKRxS/V0KSZVQ==
+"@maplibre/maplibre-gl-style-spec@^20.2.0":
+ version "20.2.0"
+ resolved "https://registry.yarnpkg.com/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.2.0.tgz#5ae301e061725eff8a7a3facc0637445b6886123"
+ integrity sha512-BTw6/3ysowky22QMtNDjElp+YLwwvBDh3xxnq1izDFjTtUERm5nYSihlNZ6QaxXb+6lX2T2t0hBEjheAI+kBEQ==
dependencies:
"@mapbox/jsonlint-lines-primitives" "~2.0.2"
"@mapbox/unitbezier" "^0.0.1"
json-stringify-pretty-compact "^4.0.0"
minimist "^1.2.8"
+ quickselect "^2.0.0"
rw "^1.3.3"
sort-object "^3.0.3"
+ tinyqueue "^2.0.3"
"@math.gl/web-mercator@3.6.2":
version "3.6.2"
@@ -1843,10 +1845,10 @@
resolved "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.1.1.tgz"
integrity sha512-dhPeilub1NuIG0X5Kvhh9lH4iW3ZsHlnzwgwbOlgwQ2wG1IqFzsgHqmKPk3WzsdWAeaxKJxgM0+W433RmN45GA==
-"@phosphor-icons/react@^2.0.14":
- version "2.0.14"
- resolved "https://registry.npmjs.org/@phosphor-icons/react/-/react-2.0.14.tgz"
- integrity sha512-VaZ7/JEQ7dW+Up23l7t6lqJ3dPJupM03916Pat+ZOLX1vex9OeX9t8RZLJWt0oVrdc/GcrAyRD5FESDeP+M4tQ==
+"@phosphor-icons/react@^2.1.5":
+ version "2.1.5"
+ resolved "https://registry.yarnpkg.com/@phosphor-icons/react/-/react-2.1.5.tgz#762c368778a4040d52c5532b8af1692b66e16783"
+ integrity sha512-B7vRm/w+P/+eavWZP5CB5Ul0ffK4Y7fpd/auWKuGvm+8pVgAJzbOK8O0s+DqzR+TwWkh5pHtJTuoAtaSvgCPzg==
"@radix-ui/number@1.0.1":
version "1.0.1"
@@ -2747,6 +2749,11 @@
resolved "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz"
integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==
+"@types/junit-report-builder@^3.0.2":
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz#17cc131d14ceff59dcf14e5847bd971b96f2cbe0"
+ integrity sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==
+
"@types/leaflet@^1.9.8":
version "1.9.8"
resolved "https://registry.yarnpkg.com/@types/leaflet/-/leaflet-1.9.8.tgz#32162a8eaf305c63267e99470b9603b5883e63e8"
@@ -6828,10 +6835,10 @@ mapbox-gl@^2.7.0:
tinyqueue "^2.0.3"
vt-pbf "^3.1.3"
-maplibre-gl@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.1.1.tgz#8ca1d7d97e640817d91d52c721ef79dd32a05715"
- integrity sha512-DmHru9FTHCOngNHzIx9W2+MlUziYPfPxd2qjyeWwczBYNx2SDpmH394MkuCvSgnfUm5Zvs4NaYCqMu44jUga1Q==
+maplibre-gl@^4.3.2:
+ version "4.3.2"
+ resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-4.3.2.tgz#c5cfaf46067ba964b020fa6f3b0134cfc70c87fd"
+ integrity sha512-/oXDsb9I+LkjweL/28aFMLDZoIcXKNEhYNAZDLA4xgTNkfvKQmV/r0KZdxEMcVthincJzdyc6Y4N8YwZtHKNnQ==
dependencies:
"@mapbox/geojson-rewind" "^0.5.2"
"@mapbox/jsonlint-lines-primitives" "^2.0.2"
@@ -6840,9 +6847,10 @@ maplibre-gl@^4.1.1:
"@mapbox/unitbezier" "^0.0.1"
"@mapbox/vector-tile" "^1.3.1"
"@mapbox/whoots-js" "^3.1.0"
- "@maplibre/maplibre-gl-style-spec" "^20.1.1"
+ "@maplibre/maplibre-gl-style-spec" "^20.2.0"
"@types/geojson" "^7946.0.14"
"@types/geojson-vt" "3.2.5"
+ "@types/junit-report-builder" "^3.0.2"
"@types/mapbox__point-geometry" "^0.1.4"
"@types/mapbox__vector-tile" "^1.3.4"
"@types/pbf" "^3.0.5"
@@ -8162,9 +8170,9 @@ react-use@^17.4.0:
tslib "^2.1.0"
react@^18.2.0:
- version "18.2.0"
- resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
- integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
+ version "18.3.1"
+ resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
dependencies:
loose-envify "^1.1.0"