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:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
28
components/ContentType/DestinationPage/Map/utils.ts
Normal file
28
components/ContentType/DestinationPage/Map/utils.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user