fix(shared): cluster the public share page's markers

The share page drew every stop as its own pin while the planner drew the same
trip as a handful of clusters, so a shared link opened on three dozen
overlapping markers. On a 300px-tall map that covers a whole city, the pins
underneath can be neither read nor tapped (#2343).

The option block and the icon factory move to components/Map/markerCluster.ts,
which both maps import, so the two cannot drift apart again. The planner's
options are unchanged.

leaflet.markercluster's two stylesheets were imported by MapView.tsx alone.
The share page never mounts MapView, so it imports them itself: without them
the bubbles keep the library's default styling and spiderfy's legs are
invisible.
This commit is contained in:
Wenyin Wei
2026-09-13 01:11:14 +08:00
committed by Wenyin WEI
parent 11b69ae815
commit 12eabbb685
5 changed files with 212 additions and 27 deletions
+10
View File
@@ -24,6 +24,16 @@ vi.mock('react-leaflet', () => ({
}),
}));
// The markers now reach the map through a cluster group (#2343). The real wrapper
// asks React Leaflet's context for the map it belongs to, and the stubbed
// MapContainer above provides none — so it is stubbed for the same reason
// everything else here is.
vi.mock('react-leaflet-cluster', () => ({
default: ({ children }: { children?: React.ReactNode }) => (
<div data-testid="marker-cluster-group">{children}</div>
),
}));
// The basemap is a MapLibre style now, and the real component reaches for
// maplibre-gl through a dynamic import. The page test only cares that it is the
// thing being rendered.
+18 -5
View File
@@ -18,6 +18,13 @@ import {
import { createElement, useEffect, useRef } from 'react';
import { renderIconMarkup } from '../utils/iconMarkup';
import { MapContainer, Marker, Polyline, TileLayer, Tooltip, useMap } from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-cluster';
// leaflet.markercluster's own sheets travel with the planner's MapView, which this
// page never mounts: without them the bubbles keep the library's default styling
// and spiderfy's legs are invisible.
import 'leaflet.markercluster/dist/MarkerCluster.css';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
import { CLUSTER_OPTIONS, createClusterIcon } from '../components/Map/markerCluster';
import { getCategoryIcon } from '../components/shared/categoryIcons';
import PublicLanguagePicker from '../components/shared/PublicLanguagePicker';
import { OFM_POSITRON, DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM, MAP_MAX_ZOOM, attributionForTile } from '../constants/mapDefaults';
@@ -454,11 +461,17 @@ export default function SharedTripPage() {
interactive={false}
/>
)}
{mapPlaces.map((p: any) => (
<Marker key={p.id} position={[p.lat, p.lng]} icon={createMarkerIcon(p, dayOrderMap[p.id] ?? null)}>
<Tooltip>{p.name}</Tooltip>
</Marker>
))}
{/* Clustered like the planner's map (#2343). A shared link draws the
whole trip at once on a 300px-tall map, where a stop a few blocks
from the next one is the same few pixels: unclustered, the pins
cover each other and the ones underneath cannot be tapped. */}
<MarkerClusterGroup {...CLUSTER_OPTIONS} iconCreateFunction={createClusterIcon}>
{mapPlaces.map((p: any) => (
<Marker key={p.id} position={[p.lat, p.lng]} icon={createMarkerIcon(p, dayOrderMap[p.id] ?? null)}>
<Tooltip>{p.name}</Tooltip>
</Marker>
))}
</MarkerClusterGroup>
</MapContainer>
</div>