Files
TREK/client/src/components/Map/gpxTrackLayers.ts
T

104 lines
4.1 KiB
TypeScript

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);
}
}