mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Map: tracks drawn from their own modules on both maps; mobile filter chips on the type scale
This commit is contained in:
@@ -5,9 +5,9 @@
|
||||
"src/components/Admin/AdminPluginsPanel.tsx": 1667,
|
||||
"src/components/Budget/CostsPanel.test.tsx": 2764,
|
||||
"src/components/Budget/CostsPanel.tsx": 1816,
|
||||
"src/components/Map/MapView.tsx": 1450,
|
||||
"src/components/Map/MapView.tsx": 1409,
|
||||
"src/components/Map/MapViewGL.test.tsx": 2920,
|
||||
"src/components/Map/MapViewGL.tsx": 2414,
|
||||
"src/components/Map/MapViewGL.tsx": 2372,
|
||||
"src/components/Map/RouteCalculator.ts": 1195,
|
||||
"src/components/PDF/TripPDF.tsx": 1082,
|
||||
"src/components/Planner/DayDetailPanel.test.tsx": 2333,
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
"components/Map/LocationButton.tsx": 1,
|
||||
"components/Map/MapPluginMarkers.tsx": 2,
|
||||
"components/Map/MapTogglePill.tsx": 1,
|
||||
"components/Map/MapView.tsx": 9,
|
||||
"components/Map/MapView.tsx": 8,
|
||||
"components/Map/MapViewGL.tsx": 11,
|
||||
"components/Map/NightPauseTooltip.tsx": 1,
|
||||
"components/Map/PlaceHoverCard.tsx": 1,
|
||||
|
||||
@@ -60,7 +60,8 @@ import { NightPauseDrag } from './NightPauseDrag'
|
||||
import type { DayBoundaryControls } from './dayBoundaryDrag'
|
||||
import type { Poi } from './poiCategories'
|
||||
import { poiDetailRowKey, poiDetailRows, poiPinCacheKey, poiPinParts } from './poiMarker'
|
||||
import { resolveTrackColor, hasManualTrackColor, TRACK_CASING_COLOR } from './trackColors'
|
||||
import { resolveTrackColor, hasManualTrackColor } from './trackColors'
|
||||
import { SelectedTrackLine, TrackLines } from './TrackLines'
|
||||
import DawarichTrailLayer from './DawarichTrailLayer'
|
||||
import { OFM_POSITRON, DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM, MAP_MAX_ZOOM, OPENTOPOMAP_TILE_URL, OPENTOPOMAP_TILE_ATTRIBUTION, OPENTOPOMAP_TILE_MAXZOOM, SATELLITE_TILE_URL, SATELLITE_TILE_MAXZOOM, AMAP_SATELLITE, attributionForTile } from '../../constants/mapDefaults'
|
||||
import { crsForBasemap } from './gcj02Crs'
|
||||
@@ -1034,78 +1035,11 @@ export const MapView = memo(function MapView({
|
||||
return [{ place, coords, cased: hasManualTrackColor(place), color: resolveTrackColor(place) }]
|
||||
}), [places])
|
||||
|
||||
// Keeps the click handler out of the polyline memo below: `handleMarkerClick`
|
||||
// Keeps the click handler out of the track lines' memo (TrackLines): `handleMarkerClick`
|
||||
// changes on every selection, and depending on it would redraw all tracks.
|
||||
const markerClickRef = useRef(handleMarkerClick)
|
||||
markerClickRef.current = handleMarkerClick
|
||||
|
||||
const gpxPolylines = useMemo(() => (
|
||||
<>
|
||||
{/* Casings live in their own pane below the lines. Leaflet stacks paths by
|
||||
insertion order, so drawing them inline would put the casing of a track
|
||||
added later — or of one just given a colour — on top of an earlier
|
||||
track's line. Always rendered, hidden via opacity when there is no
|
||||
colour, so toggling one never remounts the path. */}
|
||||
{gpxTracks.map(({ place, coords, cased }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}-casing`}
|
||||
positions={coords}
|
||||
pane={hasCasingPane ? TRACK_CASING_PANE : undefined}
|
||||
pathOptions={{ color: TRACK_CASING_COLOR, weight: 6.5, opacity: cased ? 0.7 : 0, lineCap: 'round', lineJoin: 'round' }}
|
||||
interactive={false}
|
||||
/>
|
||||
))}
|
||||
{/* pathOptions, not bare color/weight props: react-leaflet only calls
|
||||
setStyle when the pathOptions reference changes, so bare props would
|
||||
stick at their mount-time colour and a repaint would never arrive. */}
|
||||
{gpxTracks.map(({ place, coords, cased, color }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}`}
|
||||
positions={coords}
|
||||
pathOptions={{ color, weight: 3.5, opacity: cased ? 0.9 : 0.75 }}
|
||||
interactive={false}
|
||||
/>
|
||||
))}
|
||||
{/* Invisible fat line on top so the track can actually be hit — 3.5px is
|
||||
not a target, least of all on touch, and the start markers cluster below
|
||||
zoom 11. bubblingMouseEvents is essential: paths bubble to the map by
|
||||
default (markers do not), and the map's own click handler clears the
|
||||
selection this one just made. */}
|
||||
{gpxTracks.map(({ place, coords }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}-hit`}
|
||||
positions={coords}
|
||||
pathOptions={{ color: '#000', weight: 14, opacity: 0, lineCap: 'round', lineJoin: 'round' }}
|
||||
bubblingMouseEvents={false}
|
||||
eventHandlers={{ click: () => markerClickRef.current(place.id) }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
), [gpxTracks, hasCasingPane])
|
||||
|
||||
// PR1-08: the track of the picked tour stands out, wider and with a light rim, so the line and
|
||||
// the list's row read as one. Drawn on its own, so a selection never redraws every track.
|
||||
const selectedTrack = useMemo(
|
||||
() => (selectedPlaceId == null ? null : gpxTracks.find(track => track.place.id === selectedPlaceId) ?? null),
|
||||
[gpxTracks, selectedPlaceId],
|
||||
)
|
||||
const selectedTrackLine = selectedTrack && (
|
||||
<>
|
||||
<Polyline
|
||||
key={`gpx-selected-${selectedTrack.place.id}-casing`}
|
||||
positions={selectedTrack.coords}
|
||||
pathOptions={{ color: TRACK_CASING_COLOR, weight: 9, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }}
|
||||
interactive={false}
|
||||
/>
|
||||
<Polyline
|
||||
key={`gpx-selected-${selectedTrack.place.id}`}
|
||||
positions={selectedTrack.coords}
|
||||
pathOptions={{ color: selectedTrack.color, weight: 5.5, opacity: 1, lineCap: 'round', lineJoin: 'round' }}
|
||||
interactive={false}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
const TooltipOverlay = !hoverDisabled && hoveredPlace && tooltipPos && !isTouchDevice
|
||||
&& (!hoveredPlace.routeVia || routeVias.includes(hoveredPlace.routeVia))
|
||||
|
||||
@@ -1335,8 +1269,8 @@ export const MapView = memo(function MapView({
|
||||
|
||||
{/* GPX imported route geometries */}
|
||||
<TrackCasingPane onReady={setHasCasingPane} />
|
||||
{gpxPolylines}
|
||||
{selectedTrackLine}
|
||||
<TrackLines tracks={gpxTracks} casingPane={hasCasingPane ? TRACK_CASING_PANE : undefined} selectRef={markerClickRef} />
|
||||
<SelectedTrackLine tracks={gpxTracks} selectedPlaceId={selectedPlaceId} />
|
||||
|
||||
{routeProfileFocus && (
|
||||
<CircleMarker
|
||||
|
||||
@@ -41,7 +41,7 @@ import PlaceHoverCard from './PlaceHoverCard'
|
||||
import { ratingBadgeHtml } from './ratingBadge'
|
||||
import type { Poi } from './poiCategories'
|
||||
import { poiPinParts } from './poiMarker'
|
||||
import { resolveTrackColor, hasManualTrackColor, TRACK_CASING_COLOR } from './trackColors'
|
||||
import { addGpxTrackLayers, GPX_HIT_LAYER_ID, hasGpxTrackLayers, selectGpxTrack, setGpxTracks } from './gpxTrackLayers'
|
||||
import { parseRenderableRouteGeometry } from '../../utils/routeGeometry'
|
||||
import { buildPoiPopupHtml } from './placePopup'
|
||||
import { pluginsApi, type PluginMapMarker, type PluginMapLayer } from '../../api/client'
|
||||
@@ -66,9 +66,6 @@ const PLACE_CLUSTER_SOURCE_ID = 'trip-place-clusters'
|
||||
const PLACE_CLUSTER_CIRCLE_LAYER_ID = 'trip-place-clusters-circle'
|
||||
const PLACE_CLUSTER_COUNT_LAYER_ID = 'trip-place-clusters-count'
|
||||
const PLACE_UNCLUSTERED_LAYER_ID = 'trip-place-unclustered-hit'
|
||||
const GPX_HIT_LAYER_ID = 'trip-gpx-hit'
|
||||
const GPX_SELECTED_CASING_LAYER_ID = 'trip-gpx-selected-casing'
|
||||
const GPX_SELECTED_LAYER_ID = 'trip-gpx-selected'
|
||||
/**
|
||||
* The satellite base layer, the GL twin of the Leaflet one.
|
||||
*
|
||||
@@ -1347,58 +1344,8 @@ export function MapViewGL({
|
||||
})
|
||||
}
|
||||
// gpx geometries source (place.route_geometry)
|
||||
if (!map.getSource('trip-gpx')) {
|
||||
map.addSource('trip-gpx', { type: 'geojson', data: { type: 'FeatureCollection', features: [] } })
|
||||
// Casing under the tracks that carry a picked colour (#776) — keeps them
|
||||
// legible on satellite and dark styles. Untouched tracks are filtered
|
||||
// out, so they look exactly as they did before.
|
||||
map.addLayer({
|
||||
id: 'trip-gpx-casing',
|
||||
type: 'line',
|
||||
source: 'trip-gpx',
|
||||
filter: ['==', ['get', 'cased'], true],
|
||||
paint: { 'line-color': '#ffffff', 'line-width': 6.5, 'line-opacity': 0.7 },
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
map.addLayer({
|
||||
id: 'trip-gpx-line',
|
||||
type: 'line',
|
||||
source: 'trip-gpx',
|
||||
paint: {
|
||||
'line-color': ['coalesce', ['get', 'color'], '#3b82f6'],
|
||||
'line-width': 3.5,
|
||||
'line-opacity': ['case', ['==', ['get', 'cased'], true], 0.9, 0.75],
|
||||
},
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
// PR1-08: the picked tour's track on top, wider with a light rim, as the Leaflet map draws
|
||||
// it. The filter follows the selection; until something is picked it matches no track.
|
||||
map.addLayer({
|
||||
id: GPX_SELECTED_CASING_LAYER_ID,
|
||||
type: 'line',
|
||||
source: 'trip-gpx',
|
||||
filter: ['==', ['get', 'place_id'], -1],
|
||||
paint: { 'line-color': TRACK_CASING_COLOR, 'line-width': 9, 'line-opacity': 0.95 },
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
map.addLayer({
|
||||
id: GPX_SELECTED_LAYER_ID,
|
||||
type: 'line',
|
||||
source: 'trip-gpx',
|
||||
filter: ['==', ['get', 'place_id'], -1],
|
||||
paint: { 'line-color': ['coalesce', ['get', 'color'], '#3b82f6'], 'line-width': 5.5 },
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
// Invisible fat line that catches the click — 3.5px is not a target,
|
||||
// and the start markers cluster below zoom 11, so without this a track
|
||||
// is unreachable at the very zoom where you compare walks side by side.
|
||||
map.addLayer({
|
||||
id: GPX_HIT_LAYER_ID,
|
||||
type: 'line',
|
||||
source: 'trip-gpx',
|
||||
paint: { 'line-color': '#000', 'line-width': 14, 'line-opacity': 0 },
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
if (!hasGpxTrackLayers(map)) {
|
||||
addGpxTrackLayers(map)
|
||||
const selectTrack = (e: any) => { // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
// A click on a cluster bubble sitting over a track belongs to the
|
||||
// cluster (zoom-to-expand), not to the line underneath it.
|
||||
@@ -2095,32 +2042,13 @@ export function MapViewGL({
|
||||
useEffect(() => {
|
||||
const map = mapRef.current
|
||||
if (!map) return
|
||||
const src = map.getSource('trip-gpx') as mapboxgl.GeoJSONSource | undefined
|
||||
if (!src) return
|
||||
const features = places.flatMap(place => {
|
||||
const coords = parseRenderableRouteGeometry(place.route_geometry)
|
||||
if (!coords) return []
|
||||
return [{
|
||||
type: 'Feature' as const,
|
||||
properties: {
|
||||
color: resolveTrackColor(place),
|
||||
cased: hasManualTrackColor(place),
|
||||
place_id: place.id,
|
||||
},
|
||||
geometry: { type: 'LineString' as const, coordinates: coords.map(([lat, lng]) => [lng, lat]) },
|
||||
}]
|
||||
})
|
||||
src.setData({ type: 'FeatureCollection', features })
|
||||
setGpxTracks(map, places)
|
||||
}, [places, mapReady])
|
||||
|
||||
// The picked tour's track stands out (PR1-08); -1 matches no track.
|
||||
// The picked tour's track stands out (PR1-08).
|
||||
useEffect(() => {
|
||||
const map = mapRef.current
|
||||
if (!map || !mapReady || typeof map.getLayer !== 'function') return
|
||||
const filter = ['==', ['get', 'place_id'], selectedPlaceId ?? -1] as mapboxgl.FilterSpecification
|
||||
for (const id of [GPX_SELECTED_CASING_LAYER_ID, GPX_SELECTED_LAYER_ID]) {
|
||||
if (map.getLayer(id)) map.setFilter(id, filter)
|
||||
}
|
||||
if (map && mapReady) selectGpxTrack(map, selectedPlaceId)
|
||||
}, [selectedPlaceId, mapReady])
|
||||
|
||||
// Reservation overlay — mirrors the Leaflet ReservationOverlay: great-
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { memo, type RefObject } from 'react';
|
||||
import { Polyline } from 'react-leaflet';
|
||||
import type { Place } from '../../types';
|
||||
import { TRACK_CASING_COLOR } from './trackColors';
|
||||
|
||||
/** A place's track as the Leaflet map draws it: its points, its colour, and whether it carries a rim. */
|
||||
export interface TrackLine {
|
||||
place: Place;
|
||||
coords: [number, number][];
|
||||
cased: boolean;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface TrackLinesProps {
|
||||
tracks: readonly TrackLine[];
|
||||
/** The pane the rims go into, below the lines; none while the map has not made it yet. */
|
||||
casingPane?: string;
|
||||
/** Read on click, so a new selection handler never redraws every track. */
|
||||
selectRef: RefObject<(placeId: number) => void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every track of the map: the rims, the lines and an invisible fat line on top
|
||||
* that takes the click. Memoised on the tracks alone; a selection changes
|
||||
* nothing here, the picked track is drawn by {@link SelectedTrackLine}.
|
||||
*/
|
||||
export const TrackLines = memo(function TrackLines({ tracks, casingPane, selectRef }: TrackLinesProps) {
|
||||
return (
|
||||
<>
|
||||
{/* Casings live in their own pane below the lines. Leaflet stacks paths by
|
||||
insertion order, so drawing them inline would put the casing of a track
|
||||
added later — or of one just given a colour — on top of an earlier
|
||||
track's line. Always rendered, hidden via opacity when there is no
|
||||
colour, so toggling one never remounts the path. */}
|
||||
{tracks.map(({ place, coords, cased }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}-casing`}
|
||||
positions={coords}
|
||||
pane={casingPane}
|
||||
pathOptions={{
|
||||
color: TRACK_CASING_COLOR,
|
||||
weight: 6.5,
|
||||
opacity: cased ? 0.7 : 0,
|
||||
lineCap: 'round',
|
||||
lineJoin: 'round',
|
||||
}}
|
||||
interactive={false}
|
||||
/>
|
||||
))}
|
||||
{/* pathOptions, not bare color/weight props: react-leaflet only calls
|
||||
setStyle when the pathOptions reference changes, so bare props would
|
||||
stick at their mount-time colour and a repaint would never arrive. */}
|
||||
{tracks.map(({ place, coords, cased, color }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}`}
|
||||
positions={coords}
|
||||
pathOptions={{ color, weight: 3.5, opacity: cased ? 0.9 : 0.75 }}
|
||||
interactive={false}
|
||||
/>
|
||||
))}
|
||||
{/* Invisible fat line on top so the track can actually be hit — 3.5px is
|
||||
not a target, least of all on touch, and the start markers cluster below
|
||||
zoom 11. bubblingMouseEvents is essential: paths bubble to the map by
|
||||
default (markers do not), and the map's own click handler clears the
|
||||
selection this one just made. */}
|
||||
{tracks.map(({ place, coords }) => (
|
||||
<Polyline
|
||||
key={`gpx-${place.id}-hit`}
|
||||
positions={coords}
|
||||
pathOptions={{ color: TRACK_CASING_COLOR, weight: 14, opacity: 0, lineCap: 'round', lineJoin: 'round' }}
|
||||
bubblingMouseEvents={false}
|
||||
eventHandlers={{ click: () => selectRef.current?.(place.id) }}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* PR1-08: the track of the picked tour, wider and with a light rim on top of
|
||||
* the others, so the line and the list's row read as one. It takes no click:
|
||||
* the hit line below still does.
|
||||
*/
|
||||
export function SelectedTrackLine({
|
||||
tracks,
|
||||
selectedPlaceId,
|
||||
}: {
|
||||
tracks: readonly TrackLine[];
|
||||
selectedPlaceId: number | null;
|
||||
}) {
|
||||
const track = selectedPlaceId == null ? null : tracks.find((item) => item.place.id === selectedPlaceId);
|
||||
if (!track) return null;
|
||||
return (
|
||||
<>
|
||||
<Polyline
|
||||
key={`gpx-selected-${track.place.id}-casing`}
|
||||
positions={track.coords}
|
||||
pathOptions={{ color: TRACK_CASING_COLOR, weight: 9, opacity: 0.95, lineCap: 'round', lineJoin: 'round' }}
|
||||
interactive={false}
|
||||
/>
|
||||
<Polyline
|
||||
key={`gpx-selected-${track.place.id}`}
|
||||
positions={track.coords}
|
||||
pathOptions={{ color: track.color, weight: 5.5, opacity: 1, lineCap: 'round', lineJoin: 'round' }}
|
||||
interactive={false}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { TRACK_COLOR_FALLBACK } from '@trek/shared';
|
||||
import type mapboxgl from 'mapbox-gl';
|
||||
import type { Place } from '../../types';
|
||||
import { parseRenderableRouteGeometry } from '../../utils/routeGeometry';
|
||||
import { hasManualTrackColor, resolveTrackColor, TRACK_CASING_COLOR } from './trackColors';
|
||||
|
||||
const SOURCE_ID = 'trip-gpx';
|
||||
/** The invisible fat line that takes a click on a track. */
|
||||
export const GPX_HIT_LAYER_ID = 'trip-gpx-hit';
|
||||
const SELECTED_CASING_LAYER_ID = 'trip-gpx-selected-casing';
|
||||
const SELECTED_LAYER_ID = 'trip-gpx-selected';
|
||||
|
||||
const ROUND = { 'line-cap': 'round', 'line-join': 'round' } as const;
|
||||
const NO_TRACK = ['==', ['get', 'place_id'], -1] as mapboxgl.FilterSpecification;
|
||||
|
||||
/**
|
||||
* The tracks of the vector map, as the Leaflet map draws them (TrackLines): a
|
||||
* rim under the tracks that carry a picked colour, the lines, the picked
|
||||
* track on top wider with a light rim (PR1-08), and an invisible fat line that
|
||||
* takes the click. Added once with the style; whoever adds it wires the click.
|
||||
*/
|
||||
export function addGpxTrackLayers(map: mapboxgl.Map): void {
|
||||
map.addSource(SOURCE_ID, { type: 'geojson', data: { type: 'FeatureCollection', features: [] } });
|
||||
// Casing under the tracks that carry a picked colour (#776): legible on satellite and dark styles.
|
||||
// Untouched tracks are filtered out, so they look exactly as they did before.
|
||||
map.addLayer({
|
||||
id: 'trip-gpx-casing',
|
||||
type: 'line',
|
||||
source: SOURCE_ID,
|
||||
filter: ['==', ['get', 'cased'], true],
|
||||
paint: { 'line-color': TRACK_CASING_COLOR, 'line-width': 6.5, 'line-opacity': 0.7 },
|
||||
layout: ROUND,
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'trip-gpx-line',
|
||||
type: 'line',
|
||||
source: SOURCE_ID,
|
||||
paint: {
|
||||
'line-color': ['coalesce', ['get', 'color'], TRACK_COLOR_FALLBACK],
|
||||
'line-width': 3.5,
|
||||
'line-opacity': ['case', ['==', ['get', 'cased'], true], 0.9, 0.75],
|
||||
},
|
||||
layout: ROUND,
|
||||
});
|
||||
// The filter follows the selection (selectGpxTrack); until something is picked it matches no track.
|
||||
map.addLayer({
|
||||
id: SELECTED_CASING_LAYER_ID,
|
||||
type: 'line',
|
||||
source: SOURCE_ID,
|
||||
filter: NO_TRACK,
|
||||
paint: { 'line-color': TRACK_CASING_COLOR, 'line-width': 9, 'line-opacity': 0.95 },
|
||||
layout: ROUND,
|
||||
});
|
||||
map.addLayer({
|
||||
id: SELECTED_LAYER_ID,
|
||||
type: 'line',
|
||||
source: SOURCE_ID,
|
||||
filter: NO_TRACK,
|
||||
paint: { 'line-color': ['coalesce', ['get', 'color'], TRACK_COLOR_FALLBACK], 'line-width': 5.5 },
|
||||
layout: ROUND,
|
||||
});
|
||||
// 3.5px is not a target, and the start markers cluster below zoom 11, so without this a track
|
||||
// is unreachable at the very zoom where you compare walks side by side.
|
||||
map.addLayer({
|
||||
id: GPX_HIT_LAYER_ID,
|
||||
type: 'line',
|
||||
source: SOURCE_ID,
|
||||
paint: { 'line-color': TRACK_CASING_COLOR, 'line-width': 14, 'line-opacity': 0 },
|
||||
layout: ROUND,
|
||||
});
|
||||
}
|
||||
|
||||
/** Whether the track layers are there yet (they come with the style). */
|
||||
export function hasGpxTrackLayers(map: mapboxgl.Map): boolean {
|
||||
return !!map.getSource(SOURCE_ID);
|
||||
}
|
||||
|
||||
/** The places' tracks into the source, each with its colour and whether it carries a rim. */
|
||||
export function setGpxTracks(map: mapboxgl.Map, places: readonly Place[]): void {
|
||||
const src = map.getSource(SOURCE_ID) as mapboxgl.GeoJSONSource | undefined;
|
||||
if (!src) return;
|
||||
const features = places.flatMap((place) => {
|
||||
const coords = parseRenderableRouteGeometry(place.route_geometry);
|
||||
if (!coords) return [];
|
||||
return [
|
||||
{
|
||||
type: 'Feature' as const,
|
||||
properties: { color: resolveTrackColor(place), cased: hasManualTrackColor(place), place_id: place.id },
|
||||
geometry: { type: 'LineString' as const, coordinates: coords.map(([lat, lng]) => [lng, lat]) },
|
||||
},
|
||||
];
|
||||
});
|
||||
src.setData({ type: 'FeatureCollection', features });
|
||||
}
|
||||
|
||||
/** PR1-08: the picked place's track stands out; none picked, none does. */
|
||||
export function selectGpxTrack(map: mapboxgl.Map, placeId: number | null): void {
|
||||
if (typeof map.getLayer !== 'function') return;
|
||||
const filter = ['==', ['get', 'place_id'], placeId ?? -1] as mapboxgl.FilterSpecification;
|
||||
for (const id of [SELECTED_CASING_LAYER_ID, SELECTED_LAYER_ID]) {
|
||||
if (map.getLayer(id)) map.setFilter(id, filter);
|
||||
}
|
||||
}
|
||||
@@ -22,12 +22,12 @@ function FacetChip({ active, label, count, onClick }: { active: boolean; label:
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-pressed={active}
|
||||
className={`flex h-8 flex-none items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-[0.75rem] font-semibold ${
|
||||
className={`flex h-8 flex-none items-center gap-1.5 whitespace-nowrap rounded-full px-3 text-caption font-semibold ${
|
||||
active ? 'bg-m-act text-m-actfg' : 'bg-[color:var(--m-ic)] text-m-ink'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
<span className="font-geist text-[0.6875rem] opacity-60">{count}</span>
|
||||
<span className="font-geist text-caption opacity-60">{count}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
@@ -69,8 +69,8 @@ export default function MToursSelectionList({ planner, shell, filters, canAssign
|
||||
<>
|
||||
{facetRow}
|
||||
<div role="status" className="mt-3 rounded-2xl border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] p-6 text-center">
|
||||
<p className="font-geist text-[0.78125rem] text-m-muted">{t('tours.filter.empty')}</p>
|
||||
<button type="button" onClick={filters.reset} className="mt-3 rounded-full bg-m-act px-4 py-2 text-[0.75rem] font-semibold text-m-actfg">
|
||||
<p className="font-geist text-caption text-m-muted">{t('tours.filter.empty')}</p>
|
||||
<button type="button" onClick={filters.reset} className="mt-3 rounded-full bg-m-act px-4 py-2 text-caption font-semibold text-m-actfg">
|
||||
{t('tours.filter.reset')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user