Skip to content

Commit

Permalink
feat: enable mouse interaction with crags/crag-groups on map (#1080)
Browse files Browse the repository at this point in the history
* refactor: use precomputed polygons on area maps
* feat: add active marker and info panel
  • Loading branch information
vnugent authored Jan 18, 2024
1 parent 9572162 commit 4bdfffe
Show file tree
Hide file tree
Showing 13 changed files with 208 additions and 167 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
"@radix-ui/react-tabs": "^1.0.1",
"@radix-ui/react-toggle": "^1.0.1",
"@turf/bbox": "^6.5.0",
"@turf/bbox-polygon": "^6.5.0",
"@turf/convex": "^6.5.0",
"@turf/line-to-polygon": "^6.5.0",
"@udecode/zustood": "^1.1.3",
"auth0": "^2.42.0",
"awesome-debounce-promise": "^2.1.0",
Expand Down Expand Up @@ -72,6 +71,7 @@
"tailwindcss-radix": "^2.5.0",
"typesense": "^1.2.1",
"underscore": "^1.13.3",
"use-debounce": "^10.0.0",
"uuid": "9.0.0",
"yup": "^1.2.0",
"zod": "^3.21.4",
Expand Down
2 changes: 1 addition & 1 deletion src/app/area/[[...slug]]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { StickyHeaderContainer } from '@/app/components/ui/StickyHeaderContainer
import { AreaCrumbs } from '@/components/breadcrumbs/AreaCrumbs'
import { ArticleLastUpdate } from '@/components/edit/ArticleLastUpdate'
import { getMapHref, getFriendlySlug, getAreaPageFriendlyUrl, sanitizeName } from '@/js/utils'
import { LazyAreaMap } from '@/components/area/areaMap'
import { LazyAreaMap } from '@/components/maps/AreaMap'
import { AreaPageContainer } from '@/app/components/ui/AreaPageContainer'
import { AreaPageActions } from '../../components/AreaPageActions'
import { SubAreasSection } from './sections/SubAreasSection'
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/PageFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Logo, LogoSize } from 'app/header'
*/
export const PageFooter: React.FC = () => {
return (
<footer className='footer p-10 bg-base-200 bg-base-content text-base-100'>
<footer className='footer p-10 bg-base-200 bg-base-content text-base-100 snap-start snap-normal'>
<aside>
<div className='border-2 border-accent py-3 pl-2 pr-4 rounded-full'><Logo size={LogoSize.md} className='fill-accent' /></div>
<p><span className='font-semibold text-lg'>OpenBeta</span><br /><span className='tracking-tight font-sm'>Free climbing database built & run by climbers.</span></p>
Expand Down
2 changes: 1 addition & 1 deletion src/app/components/ui/AreaPageContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const AreaPageContainer: React.FC<{
{breadcrumbs == null ? <BreadCrumbsSkeleton /> : breadcrumbs}
{children == null ? <ContentSkeleton /> : children}
</div>
<div id='#map' className='w-full mt-16 relative h-[90vh] border-t'>
<div id='#map' className='w-full mt-16 relative h-[90vh] border-t snap-start snap-normal'>
{map != null && map}
</div>
</article>
Expand Down
4 changes: 2 additions & 2 deletions src/app/editArea/[slug]/general/components/AreaItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ const IconMap: Record<EType, Icon> = {
climb: LineSegment
}

export const EntityIcon: React.FC<{ type: EType, withLabel?: boolean, size?: 20 | 24 | 28, className?: string }> = ({ type, withLabel = true, size = 24, className = '' }) => {
export const EntityIcon: React.FC<{ type: EType, withLabel?: boolean, size?: 16 | 20 | 24 | 28, className?: string }> = ({ type, withLabel = true, size = 24, className = '' }) => {
const IconComponent = IconMap?.[type]
if (IconComponent == null) return null
return (
<div className='flex gap-1.5 items-center'>
<div className='flex gap-1 items-center'>
<IconComponent size={size} weight='duotone' className={className} />
{withLabel && <span className='text-xs font-light'>{type.toUpperCase()}</span>}
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ export default function RootLayout ({
children: React.ReactNode
}): any {
return (
<html lang='en'>
<body className='mx-auto'>
<html lang='en' className='snap-proximity snap-y'>
<body>
<NextAuthProvider>
<Header />
<div>
Expand Down
118 changes: 0 additions & 118 deletions src/components/area/areaMap.tsx

This file was deleted.

12 changes: 12 additions & 0 deletions src/components/maps/AreaActiveMarker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Marker } from 'react-map-gl'
import { Point } from '@turf/helpers'
import { MapPin } from '@phosphor-icons/react/dist/ssr'

export const AreaActiveMarker: React.FC<{ point: Point }> = ({ point }) => {
const { coordinates } = point
return (
<Marker longitude={coordinates[0]} latitude={coordinates[1]} style={{ zIndex: 2000 }}>
<MapPin size={36} weight='fill' className='text-accent' />
</Marker>
)
}
45 changes: 45 additions & 0 deletions src/components/maps/AreaInfoDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
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'

/**
* 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 parentId = parent?.id ?? null
return (
<Popover.Root open={data != null}>
<Popover.Anchor className='absolute top-3 left-3 z-50' />
<Popover.Content align='start'>
{data != null && <Content {...data} parentName={parentName} parentId={parentId} />}
</Popover.Content>
</Popover.Root>
)
}

export const Content: React.FC<MapAreaFeatureProperties & { parentName: string, parentId: string | null }> = ({ id, name, parentName, parentId }) => {
const url = parentId == null
? parentName
: (
<a
href={getAreaPageFriendlyUrl(parentId, name)}
className='inline-flex items-center gap-1.5'
>
<EntityIcon type='area' size={16} /><span className='text-secondary font-medium hover:underline '>{parentName}</span>
</a>
)
return (
<Card>
<div className='flex flex-col gap-y-1 text-xs'>
<div>{url}</div>
<div className='ml-2'>
<span className='text-secondary'>&#8735;</span><a href={getAreaPageFriendlyUrl(id, name)} className='text-sm font-medium hover:underline'>{name}</a>
</div>
</div>
</Card>
)
}
112 changes: 112 additions & 0 deletions src/components/maps/AreaMap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Map, ScaleControl, FullscreenControl, NavigationControl, Source, Layer, MapLayerMouseEvent, LineLayer } from 'react-map-gl'
import dynamic from 'next/dynamic'
import { lineString, Point, 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'

type ChildArea = Pick<AreaType, 'uuid' | 'areaName'> & { metadata: Pick<AreaMetadataType, 'lat' | 'lng' | 'leaf' | 'bbox' | 'polygon'> }
interface AreaMapProps {
subAreas: ChildArea[]
area: AreaType
focused: string | null
selected: string | null
}

export interface MapAreaFeatureProperties {
id: string
name: string
parent: string // due to a backend backend bug, this is a string instead of a parent object
// parent: {
// id: string
// name: string
// }
}

/**
* Area map
*/
const AreaMap: React.FC<AreaMapProps> = ({ area, subAreas }) => {
const [hovered, setHovered] = useState<MapAreaFeatureProperties | null>(null)
const [selected, setSelected] = useState<Point | null>(null)
const mapRef = useRef(null)
let fitBoundOpts: any = { padding: { top: 45, left: 45, bottom: 45, right: 45 } }
if (subAreas.length === 0) {
fitBoundOpts = { maxZoom: 14 }
}

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(() => {
/**
* Show drop pin if viewing a leaf area
*/
if (metadata.leaf) {
setSelected(point([metadata.lng, metadata.lat]).geometry as unknown as Point)
}
}, [metadata.leaf])
return (
<div className='relative w-full h-full'>
<Map
ref={mapRef}
id='map'
initialViewState={{
bounds: metadata.bbox,
fitBoundsOptions: fitBoundOpts
}}
onClick={useDebouncedCallback(onClick, 200, { leading: true, maxWait: 200 })}
reuseMaps
mapboxAccessToken={process.env.NEXT_PUBLIC_MAPBOX_API_KEY}
mapStyle={MAP_STYLES.light}
cooperativeGestures
interactiveLayerIds={['crags']}
>
<ScaleControl />
<FullscreenControl />
<NavigationControl showCompass={false} />
{selected != null &&
<AreaActiveMarker point={selected} />}
<AreaInfoDrawer data={hovered} />
{boundary != null &&
<Source id='child-areas-polygon' type='geojson' data={boundary}>
<Layer {...areaPolygonStyle} />
</Source>}
</Map>
</div>
)
}

export default AreaMap

export const LazyAreaMap = dynamic<AreaMapProps>(async () => await import('./AreaMap').then(
module => module.default), {
ssr: false
})

const areaPolygonStyle: LineLayer = {
id: 'polygon',
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)'
}
}
3 changes: 3 additions & 0 deletions src/js/graphql/gql/areaById.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export const QUERY_AREA_BY_ID = gql`
lat
lng
leftRightIndex
polygon
bbox
}
pathTokens
ancestors
Expand Down Expand Up @@ -101,6 +103,7 @@ export const QUERY_AREA_BY_ID = gql`
lat
lng
bbox
polygon
}
children {
uuid
Expand Down
3 changes: 2 additions & 1 deletion src/js/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BBox, Feature } from '@turf/helpers'
import { BBox, Feature, Position } from '@turf/helpers'
import { ViewState } from 'react-map-gl'
import { BaseItem } from '@algolia/autocomplete-core'
import { RegisterOptions } from 'react-hook-form'
Expand All @@ -19,6 +19,7 @@ export interface AreaMetadataType {
mp_id: string
area_id: string
areaId: string
polygon: Position[]
}

export enum SafetyType {
Expand Down
Loading

1 comment on commit 4bdfffe

@vercel
Copy link

@vercel vercel bot commented on 4bdfffe Jan 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.