Merged in feat/SW-1616-clustering (pull request #1330)

feat(SW-1616): Added clustering on destination country/city pages

* feat(SW-1616): Added clustering on destination country/city pages


Approved-by: Fredrik Thorsson
Approved-by: Matilda Landström
This commit is contained in:
Erik Tiekstra
2025-02-14 06:31:01 +00:00
parent 7fac673fbc
commit f9a03052b1
13 changed files with 342 additions and 22 deletions

View File

@@ -61,10 +61,13 @@ export default function DynamicMap({
}
const mapOptions: MapProps = {
defaultCenter: markers[0].coordinates, // Default center will be overridden by the bounds
defaultCenter: markers[0]?.coordinates || {
lat: 59.3293,
lng: 18.0686,
}, // Default center will be overridden by the bounds
minZoom: 3,
maxZoom: 18,
defaultZoom: 8,
defaultZoom: 5,
disableDefaultUI: true,
clickableIcons: false,
gestureHandling: "greedy",

View File

@@ -0,0 +1,20 @@
.clusterMarker {
display: flex;
justify-content: center;
align-items: center;
width: 42px !important;
height: 42px !important;
background-color: var(--Base-Text-High-contrast);
border: 4px solid var(--Base-Surface-Primary-light-Normal);
color: var(--Base-Text-Inverted);
border-radius: var(--Corner-radius-Rounded);
box-shadow: 0 0 4px 2px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.3s;
}
.count {
font-family: var(--typography-Body-Regular-fontFamily);
font-size: var(--typography-Subtitle-2-fontSize);
font-weight: var(--typography-Subtitle-2-fontWeight);
}

View File

@@ -0,0 +1,41 @@
"use client"
import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
} from "@vis.gl/react-google-maps"
import { useCallback } from "react"
import styles from "./clusterMarker.module.css"
interface ClusterMarkerProps {
position: google.maps.LatLngLiteral
size: number
sizeAsText: string
onMarkerClick?: (position: google.maps.LatLngLiteral) => void
}
export default function ClusterMarker({
position,
size,
sizeAsText,
onMarkerClick,
}: ClusterMarkerProps) {
const handleClick = useCallback(() => {
if (onMarkerClick) {
onMarkerClick(position)
}
}, [onMarkerClick, position])
return (
<AdvancedMarker
position={position}
zIndex={size}
onClick={handleClick}
className={styles.clusterMarker}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
>
<span className={styles.count}>{sizeAsText}</span>
</AdvancedMarker>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
useAdvancedMarkerRef,
} from "@vis.gl/react-google-maps"
import { useCallback } from "react"
import HotelMarkerByType from "@/components/Maps/Markers"
import type { MarkerProperties } from "@/types/components/maps/destinationMarkers"
interface MarkerProps {
position: google.maps.LatLngLiteral
properties: MarkerProperties
onMarkerClick?: (
position: google.maps.LatLngLiteral,
properties: MarkerProperties
) => void
}
export default function Marker({
position,
properties,
onMarkerClick,
}: MarkerProps) {
const [markerRef, marker] = useAdvancedMarkerRef()
const handleClick = useCallback(() => {
if (onMarkerClick) {
onMarkerClick(position, properties)
}
}, [onMarkerClick, position, properties])
return (
<AdvancedMarker
ref={markerRef}
position={position}
onClick={handleClick}
anchorPoint={AdvancedMarkerAnchorPoint.CENTER}
>
<HotelMarkerByType hotelId={properties.id} hotelType={properties.type} />
</AdvancedMarker>
)
}

View File

@@ -1,29 +1,75 @@
"use client"
import {
AdvancedMarker,
AdvancedMarkerAnchorPoint,
} from "@vis.gl/react-google-maps"
import { useMap } from "@vis.gl/react-google-maps"
import HotelMarkerByType from "@/components/Maps/Markers"
import { useSupercluster } from "@/hooks/maps/use-supercluster"
import styles from "./mapContent.module.css"
import ClusterMarker from "./ClusterMarker"
import Marker from "./Marker"
import type { DestinationMarker } from "@/types/components/maps/destinationMarkers"
import type { ClusterProperties } from "supercluster"
import type {
MarkerGeojson,
MarkerProperties,
} from "@/types/components/maps/destinationMarkers"
interface MapContentProps {
markers: DestinationMarker[]
geojson: MarkerGeojson
}
export default function MapContent({ markers }: MapContentProps) {
return markers.map((item) => (
<AdvancedMarker
key={item.name}
className={styles.advancedMarker}
position={item.coordinates}
anchorPoint={AdvancedMarkerAnchorPoint.BOTTOM_CENTER}
>
<HotelMarkerByType hotelId={item.id} hotelType={item.type} />
</AdvancedMarker>
))
// Important this is outside the component to avoid re-creating the object on each render
// which is making the useSupercluster hook fail
const CLUSTER_OPTIONS = {
extent: 256,
radius: 80,
maxZoom: 14,
}
export default function MapContent({ geojson }: MapContentProps) {
const map = useMap()
const { clusters } = useSupercluster<MarkerProperties>(
geojson,
CLUSTER_OPTIONS
)
function handleClusterClick(position: google.maps.LatLngLiteral) {
const currentZoom = map && map.getZoom()
if (currentZoom) {
map.panTo(position)
map.setZoom(currentZoom + 2)
}
}
function handleMarkerClick(
position: google.maps.LatLngLiteral,
properties: MarkerProperties
) {
console.log("Marker clicked", position, properties)
}
return clusters.map((feature) => {
const [lng, lat] = feature.geometry.coordinates
const clusterProperties = feature.properties as ClusterProperties
const markerProperties = feature.properties as MarkerProperties
const isCluster = clusterProperties.cluster
return isCluster ? (
<ClusterMarker
key={feature.id}
position={{ lat, lng }}
size={clusterProperties.point_count}
sizeAsText={String(clusterProperties.point_count_abbreviated)}
onMarkerClick={handleClusterClick}
/>
) : (
<Marker
key={feature.id}
position={{ lat, lng }}
properties={markerProperties}
onMarkerClick={handleMarkerClick}
/>
)
})
}

View File

@@ -16,6 +16,7 @@ import { debounce } from "@/utils/debounce"
import DynamicMap from "./DynamicMap"
import MapContent from "./MapContent"
import MapProvider from "./MapProvider"
import { mapMarkerDataToGeoJson } from "./utils"
import styles from "./map.module.css"
@@ -57,6 +58,8 @@ export default function Map({
}))
.filter((item): item is DestinationMarker => !!item.coordinates)
const geoJson = mapMarkerDataToGeoJson(markers)
// Calculate the height of the map based on the viewport height from the start-point (below the header and booking widget)
const handleMapHeight = useCallback(() => {
const topPosition = rootDiv.current?.getBoundingClientRect().top ?? 0
@@ -119,7 +122,7 @@ export default function Map({
>
<aside className={styles.sidebar}>{children}</aside>
<DynamicMap markers={markers} mapId={mapId} onClose={handleClose}>
<MapContent markers={markers} />
<MapContent geojson={geoJson} />
</DynamicMap>
</Dialog>
</Modal>

View File

@@ -0,0 +1,28 @@
import type {
DestinationMarker,
MarkerFeature,
MarkerGeojson,
} from "@/types/components/maps/destinationMarkers"
export function mapMarkerDataToGeoJson(markers: DestinationMarker[]) {
const features = markers.map<MarkerFeature>(
({ coordinates, ...properties }) => {
return {
type: "Feature",
id: properties.id,
geometry: {
type: "Point",
coordinates: [coordinates.lng, coordinates.lat],
},
properties,
}
}
)
const geoJson: MarkerGeojson = {
type: "FeatureCollection",
features,
}
return geoJson
}