Skip to content

Commit

Permalink
feat: add interactive to area boundaries
Browse files Browse the repository at this point in the history
  • Loading branch information
vnugent committed May 22, 2024
1 parent e79eba4 commit be20a2e
Show file tree
Hide file tree
Showing 14 changed files with 184 additions and 125 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"@lexical/react": "^0.7.5",
"@math.gl/web-mercator": "3.6.2",
"@openbeta/sandbag": "^0.0.51",
"@phosphor-icons/react": "^2.0.14",
"@phosphor-icons/react": "^2.1.5",
"@radix-ui/react-alert-dialog": "^1.0.0",
"@radix-ui/react-dialog": "^1.0.0",
"@radix-ui/react-dropdown-menu": "^2.0.1",
Expand Down Expand Up @@ -48,7 +48,7 @@
"immer": "^10.0.2",
"lexical": "^0.7.5",
"mapbox-gl": "^2.7.0",
"maplibre-gl": "^4.1.1",
"maplibre-gl": "^4.3.2",
"nanoid": "^4.0.0",
"nanoid-dictionary": "^4.3.0",
"next": "^13.5.6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export const RecentContributionsMap: React.FC<{ history: ChangesetType[] }> = ({
<div className='relative w-full h-full'>
<Map
reuseMaps
mapStyle={MAP_STYLES.standard.style}
mapStyle={MAP_STYLES.light.style}
cooperativeGestures
{...clickableLayer1 != null && clickableLayer2 != null &&
{ interactiveLayerIds: [clickableLayer2, clickableLayer1] }}
Expand Down
40 changes: 11 additions & 29 deletions src/components/maps/AreaActiveMarker.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
import { Marker, Source, Layer, LineLayer } from 'react-map-gl'
import { Geometry } from 'geojson'
import { Point, Polygon } from '@turf/helpers'
import { Marker } from 'react-map-gl'
import { Point } from '@turf/helpers'
import { MapPin } from '@phosphor-icons/react/dist/ssr'
import { ActiveFeature } from './TileTypes'

/**
* Highlight selected feature on the map
*/
export const SelectedFeature: React.FC<{ geometry: Geometry }> = ({ geometry }) => {
switch (geometry.type) {
case 'Point':
return <SelectedPoint geometry={geometry} />
case 'Polygon':
return <SelectedPolygon geometry={geometry} />
export const SelectedFeature: React.FC<{ feature: ActiveFeature }> = ({ feature }) => {
switch (feature.type) {
case 'crag-markers':
case 'crag-name-labels':
return <SelectedPoint geometry={feature.geometry as Point} />
default: return null
}
}
Expand All @@ -20,26 +19,9 @@ const SelectedPoint: React.FC<{ geometry: Point }> = ({ geometry }) => {
const { coordinates } = geometry
return (
<Marker longitude={coordinates[0]} latitude={coordinates[1]}>
<MapPin size={36} weight='fill' className='text-accent' />
<div className='absolute bottom-0 -translate-x-1/2'>
<MapPin size={48} weight='fill' className='text-accent' />
</div>
</Marker>
)
}

export const SelectedPolygon: React.FC<{ geometry: Polygon }> = ({ geometry }) => {
return (
<Source id='selected-polygon' type='geojson' data={geometry}>
<Layer {...selectedBoundary} />
</Source>
)
}

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
}
}
87 changes: 68 additions & 19 deletions src/components/maps/GlobalMap.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +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 dynamic from 'next/dynamic'
import { Geometry } from 'geojson'

import { MAP_STYLES, type MapStyles } from './MapSelector'
import { AreaInfoDrawer } from './TileHandlers/AreaInfoDrawer'
import { AreaInfoHover } from './TileHandlers/AreaInfoHover'
import { SelectedFeature } from './AreaActiveMarker'
import { OBCustomLayers } from './OBCustomLayers'
import { tileToFeature } from './utils'
import { ActiveFeature, TileProps } from './TileTypes'
import MapLayersSelector from './MapLayersSelector'
import { debounce } from 'underscore'
import { MapToolbar } from './MapToolbar'
import { SelectedFeature } from './AreaActiveMarker'

export interface CameraInfo {
center: {
Expand Down Expand Up @@ -47,11 +46,12 @@ export const GlobalMap: React.FC<GlobalMapProps> = ({
showFullscreenControl = true, initialCenter, initialZoom, initialViewState, onCameraMovement, children
}) => {
const [clickInfo, setClickInfo] = useState<ActiveFeature | null>(null)
const [hoverInfo, setHoverInfo] = useState < ActiveFeature | null>(null)
const [selected, setSelected] = useState<Geometry | null>(null)
const [mapInstance, setMapInstance] = useState<MapInstance | null>(null)
const [hoverInfo, setHoverInfo] = useState<ActiveFeature | null>(null)
const [hoverStateId, setHoverStateId] = useState<string | null>(null)
const [selectedStateId, setSelectedStateId] = useState<string | null>(null)
const [mapInstance, setMapInstance] = useState<maplibregl.Map | null>(null)
const [cursor, setCursor] = useState<string>('default')
const [mapStyle, setMapStyle] = useState<string>(MAP_STYLES.standard.style)
const [mapStyle, setMapStyle] = useState<string>(MAP_STYLES.light.style)
const [dataLayersDisplayState, setDataLayersDisplayState] = useState<DataLayersDisplayState>({
cragGroups: false,
organizations: false
Expand Down Expand Up @@ -82,44 +82,85 @@ export const GlobalMap: React.FC<GlobalMapProps> = ({
/**
* 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 || mapInstance == null) {
setSelected(null)
if (feature == null) {
if (selectedStateId != null) {
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: selectedStateId
}, { selected: false })
}
setSelectedStateId(null)
setClickInfo(null)
} else {
const { layer, geometry, properties } = feature
setSelected(feature.geometry)

if (selectedStateId != null) {
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: selectedStateId
}, { selected: false })
}

setSelectedStateId(feature.id as string)
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: feature.id
}, { selected: true })

setClickInfo(tileToFeature(layer.id, event.point, geometry, properties as TileProps, mapInstance))
}
}, [mapInstance])
}

/**
* Handle click event on the popover. Behave as if the user clicked on a feature on the map.
*/
const onHoverCardClick = (feature: ActiveFeature): void => {
setSelected(feature.geometry)
// setSelected(feature)
setClickInfo(feature)
}

/**
* Handle over 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 === 'crag-markers' || f.layer.id === 'crag-name-labels' || 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]
console.log('#Hover', feature)

if (feature != null && mapInstance != null) {
const { layer, geometry, properties } = feature

if (hoverStateId != null) {
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: feature.id
}, { hover: false })
}

setHoverStateId(feature.id as string)
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: feature.id
}, { hover: true })

setHoverInfo(tileToFeature(layer.id, event.point, geometry, properties as TileProps, mapInstance))
}
} else {
setHoverInfo(null)
setCursor('default')
}
}, [mapInstance])
}

const updateMapLayer = (key: keyof MapStyles): void => {
const style = MAP_STYLES[key]
Expand All @@ -140,14 +181,22 @@ export const GlobalMap: React.FC<GlobalMapProps> = ({
}}
onMouseEnter={onHover}
onMouseLeave={() => {
if (hoverStateId != null && mapInstance != null) {
mapInstance.setFeatureState({
source: 'areas',
sourceLayer: 'areas',
id: hoverStateId
}, { hover: false })
}
setHoverStateId(null)
setHoverInfo(null)
setCursor('default')
}}
onClick={onClick}
mapStyle={mapStyle}
cursor={cursor}
cooperativeGestures={showFullscreenControl}
interactiveLayerIds={['crag-markers', 'crag-name-labels', 'crag-group-boundaries', 'organizations']}
interactiveLayerIds={['crag-markers', 'crag-name-labels', 'area-boundaries', 'organizations']}
>
<MapToolbar layerState={dataLayersDisplayState} onChange={setDataLayersDisplayState} />
<MapLayersSelector emit={updateMapLayer} />
Expand All @@ -157,8 +206,8 @@ export const GlobalMap: React.FC<GlobalMapProps> = ({
<OBCustomLayers layersState={dataLayersDisplayState} />
{showFullscreenControl && <FullscreenControl />}
<NavigationControl showCompass={false} position='bottom-right' />
{selected != null &&
<SelectedFeature geometry={selected} />}
{clickInfo != null &&
<SelectedFeature feature={clickInfo} />}
<AreaInfoDrawer feature={clickInfo} />
{hoverInfo != null && (
<AreaInfoHover
Expand Down
4 changes: 2 additions & 2 deletions src/components/maps/MapLayersSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ interface Props {
}

const MapLayersSelector: React.FC<Props> = ({ emit }) => {
const [mapImgUrl, setMapImgUrl] = useState<string>(MAP_STYLES.standard.imgUrl)
const [mapName, setMapName] = useState<string>('standard')
const [mapImgUrl, setMapImgUrl] = useState<string>(MAP_STYLES.light.imgUrl)
const [mapName, setMapName] = useState<string>('light')

const emitMap = (key: string): void => {
const styleKey = key as keyof MapStyles
Expand Down
12 changes: 6 additions & 6 deletions src/components/maps/MapSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}`,
Expand All @@ -22,11 +22,11 @@ export interface MapStyles {
style: string
imgUrl: string
}
minimal: {
light: {
style: string
imgUrl: string
}
standard: {
dark: {
style: string
imgUrl: string
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/maps/MapToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const MapToolbar: React.FC<MapToolbarProps> = ({ onChange, layerState })
<input
type='checkbox' className='checkbox' checked={cragGroups}
onChange={() => onChange({ ...layerState, cragGroups: !cragGroups })}
/> Crag groups
/> Area boundaries
</li>
</ul>
</div>
Expand Down
Loading

0 comments on commit be20a2e

Please sign in to comment.