From bbc2a764a3b752eab1e963baf3d54613bae1a94d Mon Sep 17 00:00:00 2001 From: viet nguyen Date: Wed, 17 Jan 2024 17:09:53 -0800 Subject: [PATCH] refactor: use precomputed polygons on area maps wip: show crags on hover --- package.json | 4 +- src/app/area/[[...slug]]/page.tsx | 2 +- src/components/area/areaMap.tsx | 118 ------------------------- src/components/maps/AreaMap.tsx | 98 ++++++++++++++++++++ src/components/maps/MouseoverPanel.tsx | 15 ++++ src/js/graphql/gql/areaById.ts | 3 + src/js/types.ts | 3 +- yarn.lock | 64 ++++++-------- 8 files changed, 146 insertions(+), 161 deletions(-) delete mode 100644 src/components/area/areaMap.tsx create mode 100644 src/components/maps/AreaMap.tsx create mode 100644 src/components/maps/MouseoverPanel.tsx diff --git a/package.json b/package.json index b1810a2f0..30f528cdc 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/app/area/[[...slug]]/page.tsx b/src/app/area/[[...slug]]/page.tsx index fe399d386..c3f1acd39 100644 --- a/src/app/area/[[...slug]]/page.tsx +++ b/src/app/area/[[...slug]]/page.tsx @@ -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' diff --git a/src/components/area/areaMap.tsx b/src/components/area/areaMap.tsx deleted file mode 100644 index c8572dabc..000000000 --- a/src/components/area/areaMap.tsx +++ /dev/null @@ -1,118 +0,0 @@ -'use client' -import * as React from 'react' -import { Map, ScaleControl, FullscreenControl, NavigationControl, LngLatBoundsLike, MapboxMap, Source, Layer, FillLayer } from 'react-map-gl' -import dynamic from 'next/dynamic' -import { Padding } from '@math.gl/web-mercator/dist/fit-bounds' -import { featureCollection } from '@turf/helpers' -import bbox2Polygon from '@turf/bbox-polygon' -import convexHull from '@turf/convex' - -import { AreaMetadataType, AreaType } from '../../js/types' -import { MAP_STYLES } from '../maps/BaseMap' - -type ChildArea = Pick & { metadata: Pick } -interface AreaMapProps { - subAreas: ChildArea[] - area: AreaType - focused: string | null - selected: string | null -} - -// TODO: use built-in getBounds() -/** get the bounds needed to display this map */ -function getBounds (area: AreaType): [[number, number], [number, number]] { - const initlat = area.metadata.lat - const initlng = area.metadata.lng - - let [minLat, maxLat] = [initlat, initlat] - let [minLon, maxLon] = [initlng, initlng] - - if (area.children.length > 0) { - area.children.forEach((area) => { - const { lat, lng } = area.metadata - if (lat > maxLat) { - maxLat = lat - } else if (lng > maxLon) { - maxLon = lng - } - if (lat < minLat) { - minLat = lat - } else if (lng < minLon) { - minLon = lng - } - }) - } - - return [ - [minLon, minLat], // SouthWest corner - [maxLon, maxLat] - ] // northeastern corner of the bounds -} - -function computeVS (area: AreaType): LngLatBoundsLike { - return getBounds(area) -} - -export default function AreaMap (props: AreaMapProps): JSX.Element { - const mapRef = React.useRef(null) - let padding: Padding = { top: 45, left: 45, bottom: 45, right: 45 } - if (props.subAreas.length === 0) { - padding = { top: 100, left: 100, bottom: 100, right: 100 } - } - - React.useEffect(() => { - // re-compute bounds whenever the area changes - if (mapRef.current !== null) { - const map: MapboxMap = mapRef.current as any - const bounds = computeVS(props.area) - map.fitBounds(bounds, { padding }) - } - }, [props.area.id]) - - const childAreas = props.subAreas.map((area) => { - const { metadata } = area - return bbox2Polygon(metadata.bbox) - }) - - const childAreasFC = featureCollection(childAreas) - - const boundary = convexHull(childAreasFC, { properties: { name: props.area.areaName } }) - - return ( -
- {boundary != null && - - - } - - - - -
- ) -} - -export const LazyAreaMap = dynamic(async () => await import('./areaMap').then( - module => module.default), { - ssr: true -}) - -const areaPolygonStyle: FillLayer = { - id: 'polygon', - type: 'fill', - paint: { - 'fill-color': '#F15E40', - 'fill-opacity': ['interpolate', ['linear'], ['zoom'], 8, 0.40, 16, 0.1] - } -} diff --git a/src/components/maps/AreaMap.tsx b/src/components/maps/AreaMap.tsx new file mode 100644 index 000000000..0255d307d --- /dev/null +++ b/src/components/maps/AreaMap.tsx @@ -0,0 +1,98 @@ +'use client' +import { useRef, useState } from 'react' +import { Map, ScaleControl, FullscreenControl, NavigationControl, Source, Layer, FillLayer, MapLayerMouseEvent } from 'react-map-gl' +import dynamic from 'next/dynamic' +import { Padding } from '@math.gl/web-mercator/dist/fit-bounds' +import { lineString } 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 { MouseoverPanel } from './MouseoverPanel' + +type ChildArea = Pick & { metadata: Pick } +interface AreaMapProps { + subAreas: ChildArea[] + area: AreaType + focused: string | null + selected: string | null +} + +export interface MapAreaFeatureProperties { + id: string + name: string + parent: { + id: string + name: string + } +} + +/** + * Area map + */ +const AreaMap: React.FC = ({ area, subAreas }) => { + const [hovered, setHovered] = useState(null) + const mapRef = useRef(null) + let padding: Padding = { top: 45, left: 45, bottom: 45, right: 45 } + if (subAreas.length === 0) { + padding = { top: 100, left: 100, bottom: 100, right: 100 } + } + + const { metadata } = area + + const boundary = metadata?.polygon == null ? null : lineToPolygon(lineString(metadata.polygon), { properties: { name: area.areaName } }) + + const onMouseEnter = (event: MapLayerMouseEvent): void => { + console.log('onMouseEnter', event) + const feature = event?.features?.[0] + if (feature != null) { + setHovered(feature.properties as MapAreaFeatureProperties) + } + } + + return ( +
+ + + + + {hovered != null && } + {boundary != null && + + + } + +
+ ) +} + +export default AreaMap + +export const LazyAreaMap = dynamic(async () => await import('./AreaMap').then( + module => module.default), { + ssr: false +}) + +const areaPolygonStyle: FillLayer = { + id: 'polygon', + type: 'fill', + paint: { + 'fill-antialias': true, + 'fill-color': 'rgb(236,72,153)', + 'fill-opacity': ['step', ['zoom'], 0.2, 15, 0] + } +} diff --git a/src/components/maps/MouseoverPanel.tsx b/src/components/maps/MouseoverPanel.tsx new file mode 100644 index 000000000..8c875204e --- /dev/null +++ b/src/components/maps/MouseoverPanel.tsx @@ -0,0 +1,15 @@ +import { MapAreaFeatureProperties } from './AreaMap' +import { getAreaPageFriendlyUrl } from '@/js/utils' + +/** + * Show the name of the area on hover + */ +export const MouseoverPanel: React.FC = ({ id, name }) => { + return ( +
+
+ {name} +
+
+ ) +} diff --git a/src/js/graphql/gql/areaById.ts b/src/js/graphql/gql/areaById.ts index 2815bae17..2f68376c1 100644 --- a/src/js/graphql/gql/areaById.ts +++ b/src/js/graphql/gql/areaById.ts @@ -46,6 +46,8 @@ export const QUERY_AREA_BY_ID = gql` lat lng leftRightIndex + polygon + bbox } pathTokens ancestors @@ -101,6 +103,7 @@ export const QUERY_AREA_BY_ID = gql` lat lng bbox + polygon } children { uuid diff --git a/src/js/types.ts b/src/js/types.ts index a2e296f9c..f5d9709a1 100644 --- a/src/js/types.ts +++ b/src/js/types.ts @@ -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' @@ -19,6 +19,7 @@ export interface AreaMetadataType { mp_id: string area_id: string areaId: string + polygon: Position[] // Pick } export enum SafetyType { diff --git a/yarn.lock b/yarn.lock index 4ddaa15a7..fb35ad3b8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2395,13 +2395,6 @@ resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== -"@turf/bbox-polygon@^6.5.0": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/bbox-polygon/-/bbox-polygon-6.5.0.tgz#f18128b012eedfa860a521d8f2b3779cc0801032" - integrity sha512-+/r0NyL1lOG3zKZmmf6L8ommU07HliP4dgYToMoTxqzsWzyLjaj/OzgQ8rBmv703WJX+aS6yCmLuIhYqyufyuw== - dependencies: - "@turf/helpers" "^6.5.0" - "@turf/bbox@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/bbox/-/bbox-6.5.0.tgz#bec30a744019eae420dac9ea46fb75caa44d8dc5" @@ -2410,20 +2403,35 @@ "@turf/helpers" "^6.5.0" "@turf/meta" "^6.5.0" -"@turf/convex@^6.5.0": +"@turf/clone@^6.5.0": version "6.5.0" - resolved "https://registry.yarnpkg.com/@turf/convex/-/convex-6.5.0.tgz#a7613e0d3795e2f5b9ce79a39271e86f54a3d354" - integrity sha512-x7ZwC5z7PJB0SBwNh7JCeCNx7Iu+QSrH7fYgK0RhhNop13TqUlvHMirMLRgf2db1DqUetrAO2qHJeIuasquUWg== + resolved "https://registry.yarnpkg.com/@turf/clone/-/clone-6.5.0.tgz#895860573881ae10a02dfff95f274388b1cda51a" + integrity sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw== dependencies: "@turf/helpers" "^6.5.0" - "@turf/meta" "^6.5.0" - concaveman "*" "@turf/helpers@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/helpers/-/helpers-6.5.0.tgz#f79af094bd6b8ce7ed2bd3e089a8493ee6cae82e" integrity sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw== +"@turf/invariant@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/invariant/-/invariant-6.5.0.tgz#970afc988023e39c7ccab2341bd06979ddc7463f" + integrity sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg== + dependencies: + "@turf/helpers" "^6.5.0" + +"@turf/line-to-polygon@^6.5.0": + version "6.5.0" + resolved "https://registry.yarnpkg.com/@turf/line-to-polygon/-/line-to-polygon-6.5.0.tgz#c919a03064a1cd5cef4c4e4d98dc786e12ffbc89" + integrity sha512-qYBuRCJJL8Gx27OwCD1TMijM/9XjRgXH/m/TyuND4OXedBpIWlK5VbTIO2gJ8OCfznBBddpjiObLBrkuxTpN4Q== + dependencies: + "@turf/bbox" "^6.5.0" + "@turf/clone" "^6.5.0" + "@turf/helpers" "^6.5.0" + "@turf/invariant" "^6.5.0" + "@turf/meta@^6.5.0": version "6.5.0" resolved "https://registry.yarnpkg.com/@turf/meta/-/meta-6.5.0.tgz#b725c3653c9f432133eaa04d3421f7e51e0418ca" @@ -3719,16 +3727,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concaveman@*: - version "1.2.1" - resolved "https://registry.yarnpkg.com/concaveman/-/concaveman-1.2.1.tgz#47d20b4521125c15fabf453653c2696d9ee41e0b" - integrity sha512-PwZYKaM/ckQSa8peP5JpVr7IMJ4Nn/MHIaWUjP4be+KoZ7Botgs8seAZGpmaOM+UZXawcdYRao/px9ycrCihHw== - dependencies: - point-in-polygon "^1.1.0" - rbush "^3.0.1" - robust-predicates "^2.0.4" - tinyqueue "^2.0.3" - constant-case@^1.1.0: version "1.1.2" resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-1.1.2.tgz#8ec2ca5ba343e00aa38dbf4e200fd5ac907efd63" @@ -7458,11 +7456,6 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" -point-in-polygon@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/point-in-polygon/-/point-in-polygon-1.1.0.tgz#b0af2616c01bdee341cbf2894df643387ca03357" - integrity sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw== - postcss-import@^14.1.0: version "14.1.0" resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" @@ -7691,13 +7684,6 @@ quickselect@^2.0.0: resolved "https://registry.yarnpkg.com/quickselect/-/quickselect-2.0.0.tgz#f19680a486a5eefb581303e023e98faaf25dd018" integrity sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw== -rbush@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/rbush/-/rbush-3.0.1.tgz#5fafa8a79b3b9afdfe5008403a720cc1de882ecf" - integrity sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w== - dependencies: - quickselect "^2.0.0" - rc-slider@^10.0.0-alpha.5: version "10.3.0" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.3.0.tgz#f5012bfc166b93cf79c5ccdd5f5bb560ab3ace57" @@ -8199,11 +8185,6 @@ rimraf@^3.0.2: dependencies: glob "^7.1.3" -robust-predicates@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-2.0.4.tgz#0a2367a93abd99676d075981707f29cfb402248b" - integrity sha512-l4NwboJM74Ilm4VKfbAtFeGq7aEjWL+5kVFcmgFA2MrdnQWx9iE/tUGvxY5HyMI7o/WpSIUFLbC5fbeaHgSCYg== - rtl-css-js@^1.14.0: version "1.16.1" resolved "https://registry.yarnpkg.com/rtl-css-js/-/rtl-css-js-1.16.1.tgz#4b48b4354b0ff917a30488d95100fbf7219a3e80" @@ -9276,6 +9257,11 @@ use-context-selector@1.4.1: resolved "https://registry.yarnpkg.com/use-context-selector/-/use-context-selector-1.4.1.tgz#eb96279965846b72915d7f899b8e6ef1d768b0ae" integrity sha512-Io2ArvcRO+6MWIhkdfMFt+WKQX+Vb++W8DS2l03z/Vw/rz3BclKpM0ynr4LYGyU85Eke+Yx5oIhTY++QR0ZDoA== +use-debounce@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.0.tgz#5091b18d6c16292605f588bae3c0d2cfae756ff2" + integrity sha512-XRjvlvCB46bah9IBXVnq/ACP2lxqXyZj0D9hj4K5OzNroMDpTEBg8Anuh1/UfRTRs7pLhQ+RiNxxwZu9+MVl1A== + use-sidecar@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2"