diff --git a/client/scripts/eslint-baseline.json b/client/scripts/eslint-baseline.json index 21f34886e..0a13d682d 100644 --- a/client/scripts/eslint-baseline.json +++ b/client/scripts/eslint-baseline.json @@ -1,6 +1,6 @@ { "src": { - "@typescript-eslint/no-explicit-any": 155, + "@typescript-eslint/no-explicit-any": 154, "@typescript-eslint/no-unused-expressions": 2, "@typescript-eslint/no-unused-vars": 155, "no-empty": 19, @@ -12,14 +12,14 @@ "unused-disable-directive": 6 }, "suppressed": { - "@typescript-eslint/no-explicit-any": 44, + "@typescript-eslint/no-explicit-any": 40, "react-hooks/exhaustive-deps": 46 }, "tests": { - "@typescript-eslint/no-explicit-any": 492, + "@typescript-eslint/no-explicit-any": 491, "@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-unsafe-function-type": 2, - "@typescript-eslint/no-unused-vars": 53, + "@typescript-eslint/no-unused-vars": 52, "react-refresh/only-export-components": 1 } } diff --git a/client/scripts/size-baseline.json b/client/scripts/size-baseline.json index 688069e45..005f789fd 100644 --- a/client/scripts/size-baseline.json +++ b/client/scripts/size-baseline.json @@ -7,7 +7,7 @@ "src/components/Budget/CostsPanel.tsx": 1854, "src/components/Map/MapView.tsx": 1461, "src/components/Map/MapViewGL.test.tsx": 2920, - "src/components/Map/MapViewGL.tsx": 2549, + "src/components/Map/MapViewGL.tsx": 2425, "src/components/Map/RouteCalculator.ts": 1195, "src/components/PDF/TripPDF.tsx": 1097, "src/components/Planner/DayDetailPanel.test.tsx": 2333, diff --git a/client/src/components/Map/MapView.test.tsx b/client/src/components/Map/MapView.test.tsx index 58f2d8588..b3c12b30b 100644 --- a/client/src/components/Map/MapView.test.tsx +++ b/client/src/components/Map/MapView.test.tsx @@ -209,6 +209,7 @@ vi.mock('./gcj02Crs', () => ({ })) import { MapView } from './MapView' +import type { RouteVia } from '../../types' // Helper: build a place with the extra fields MapView uses (category_name/color/icon) // that exist on joined DB rows but are not in the base Place TypeScript type. @@ -1025,7 +1026,9 @@ describe('MapView explore POIs', () => { }) describe('MapView plugin route vias', () => { - const via = (overrides: Record = {}) => ({ lat: 48.5, lng: 2.5, tone: 'default', ...overrides }) + // Cast, not typed: one case hands in a tone outside RouteVia's union on purpose. + const via = (overrides: Partial> = {}) => + ({ lat: 48.5, lng: 2.5, tone: 'default', ...overrides }) as RouteVia it('FE-COMP-MAPVIEW-037: draws a tone dot for each via point', () => { render() diff --git a/client/src/components/Map/MapView.tsx b/client/src/components/Map/MapView.tsx index 5d5dc4db6..4c468739a 100644 --- a/client/src/components/Map/MapView.tsx +++ b/client/src/components/Map/MapView.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useMemo, useCallback, createElement, memo } from 'react' +import { useEffect, useRef, useState, useMemo, useCallback, createElement, memo, type ComponentType } from 'react' import DOM from 'react-dom' import { renderIconMarkup } from '../../utils/iconMarkup' import { MapContainer, TileLayer, Marker, Polyline, CircleMarker, Circle, useMap, Tooltip } from 'react-leaflet' @@ -48,7 +48,7 @@ import { visibleRouteReservations } from '../../utils/reservationRoutes' import { safeHexColor } from '../../utils/safeColor' import { placeMarkerLook } from './markerLook' import { escapeHtml } from '@trek/shared' -import type { Day, Reservation, RouteVia } from '../../types' +import type { Day, Place, Reservation, RouteVia } from '../../types' import type { MapHoverInfo } from './mapHover' import { nightPauseMarker, NIGHT_PAUSE_MIN_ZOOM } from './nightPauseMarker' import NightPauseTooltip from './NightPauseTooltip' @@ -75,7 +75,7 @@ function categoryIconSvg(iconName: string | null | undefined, size: number): str return renderIconMarkup(createElement(IconComponent, { size, color: 'white', strokeWidth: 2.5 })) } catch { return '' } } -import type { Place } from '../../types' +import type { MapViewProps } from './mapViewContract' // Fix default marker icons for vite. `_getIconUrl` is a Leaflet-internal field // not present in the public typings, so narrow to delete it. @@ -839,7 +839,7 @@ export const MapView = memo(function MapView({ routeProfileFocus = null, viewBaseLayer, onViewBaseLayerChange, -}: any) { +}: MapViewProps) { const globalBaseLayer = useSettingsStore(s => s.settings.map_base_layer) || 'default' const updateSetting = useSettingsStore(s => s.updateSetting) const baseLayer = (viewBaseLayer ?? globalBaseLayer) as TourBaseLayer @@ -1415,4 +1415,4 @@ export const MapView = memo(function MapView({ )} ) -}) +}) satisfies ComponentType diff --git a/client/src/components/Map/MapViewAuto.tsx b/client/src/components/Map/MapViewAuto.tsx index 09ae481d3..eeb9d0bae 100644 --- a/client/src/components/Map/MapViewAuto.tsx +++ b/client/src/components/Map/MapViewAuto.tsx @@ -7,6 +7,7 @@ import ErrorBoundary from '../shared/ErrorBoundary' import { MapViewGLMapbox, MapViewGLMaplibre } from './glLazy' import { useRoadtripHazards } from './useRoadtripHazards' import { useTranslation } from '../../i18n/TranslationContext' +import type { MapViewAutoProps, MapViewProps } from './mapViewContract' // Auto-selects the map renderer based on user settings. Keeps the existing // Leaflet MapView untouched so the Mapbox GL variant can mature iteratively @@ -16,14 +17,15 @@ import { useTranslation } from '../../i18n/TranslationContext' // tiles via sync/tilePrefetcher.ts). GL maps are best-effort offline — their // vector tiles are cached opportunistically by the Service Worker as you view // them online (see the GL tile rules in vite.config.js), not prefetched. -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function MapViewAuto(props: any) { +export function MapViewAuto(props: MapViewAutoProps) { const { t } = useTranslation() const hazards = useRoadtripHazards(props.tripId, !!props.clusterLoosely) // `dawarichTrack` arrives as a prop rather than being fetched here: the pill // that switches it on lives at page level and needs the load status, so the // fetch sits in useTripPlanner and both shells read the same one. - const mapProps = { ...props, hazards: hazards.feed?.hazards } + // The one cast between what callers pass and what the renderers type: pins + // without trip_id (see MapViewAutoProps). Neither renderer reads trip_id. + const mapProps = { ...props, hazards: hazards.feed?.hazards } as MapViewProps const provider = useSettingsStore(s => s.settings.map_provider) const token = useSettingsStore(s => s.settings.mapbox_access_token) // Fall back to Leaflet when Mapbox is selected but no token is set, diff --git a/client/src/components/Map/MapViewGL.tsx b/client/src/components/Map/MapViewGL.tsx index 3f647695a..5547d26cd 100644 --- a/client/src/components/Map/MapViewGL.tsx +++ b/client/src/components/Map/MapViewGL.tsx @@ -1,9 +1,7 @@ import { useEffect, useRef, useMemo, useState, createElement, useCallback } from 'react' import { makeMarkerDraggable, makePoiDraggable, draggedPoiId } from './markerDrag' -import type { DawarichTrack, RoadtripVia } from '@trek/shared' import { useStableVias } from './viaMarkerState' import { ALT_CASING, ALT_LABEL_TEXT } from '../Roadtrip/alternativeColors' -import type { AlternativeOverlay } from '../Roadtrip/alternativeOverlays' import { serviceMarkerHtml, serviceMarkerOuter } from '../Roadtrip/serviceMarker' import { renderIconMarkup } from '../../utils/iconMarkup' import type mapboxgl from 'mapbox-gl' @@ -28,24 +26,24 @@ import { useIsPhone } from '../../mobile/useIsPhone' import { useGeolocation } from '../../hooks/useGeolocation' import type { Day, Place, Reservation, RouteVia } from '../../types' import type { MapHoverInfo } from './mapHover' +import type { MapViewProps, RouteSegment } from './mapViewContract' import { nightPauseMarker, NIGHT_PAUSE_MIN_ZOOM } from './nightPauseMarker' import { clusterPois, poiClusterMarkup, poiClusterList, POI_CLUSTER_DETAIL_ZOOM } from './poiClusters' import { groupCoincidentPlaces } from './coincidentPlaces' -import type { RoadtripHazard } from '@trek/shared' import { useHazardLayerGL } from './useHazardLayerGL' import { useDawarichTrailGL } from './useDawarichTrailGL' -import { bindDayBoundaryDrag, type DayBoundaryControls } from './dayBoundaryDrag' +import { bindDayBoundaryDrag } from './dayBoundaryDrag' import NightPauseTooltip from './NightPauseTooltip' import PlaceHoverCard from './PlaceHoverCard' import { ratingBadgeHtml } from './ratingBadge' import type { Poi } from './poiCategories' import { poiPinParts } from './poiMarker' import { resolveTrackColor, hasManualTrackColor } from './trackColors' -import { parseRenderableRouteGeometry, type RouteProfileFocus } from '../../utils/routeGeometry' +import { parseRenderableRouteGeometry } from '../../utils/routeGeometry' import { buildPoiPopupHtml } from './placePopup' import { pluginsApi, type PluginMapMarker, type PluginMapLayer } from '../../api/client' import { DEFAULT_MAP_CENTER, DEFAULT_MAP_ZOOM, SATELLITE_TILE_URL, SATELLITE_TILE_ATTRIBUTION, SATELLITE_TILE_MAXZOOM } from '../../constants/mapDefaults' -import { computeMapViewport, TILE_SIZE_GL, type ViewportPadding } from '../../utils/mapViewport' +import { computeMapViewport, TILE_SIZE_GL } from '../../utils/mapViewport' import { selectedPlaceTarget } from './selectedPlaceTarget' function categoryIconSvg(iconName: string | null | undefined, size: number): string { @@ -107,13 +105,6 @@ function buildPlaceClusterData(places: Place[]) { } } -interface RouteSegment { - mid: [number, number] - from: [number, number] - to: [number, number] - walkingText?: string - drivingText?: string -} // Stable identities for the omitted collection props. An inline `= []` / `= {}` // default allocates a fresh object on every render, and these props sit in the @@ -132,120 +123,8 @@ const NO_CONNECTION_IDS: number[] = [] const NO_POIS: Poi[] = [] const NO_DAYS: Day[] = [] -interface Props { - places: Place[] - dayPlaces?: Place[] - // Enables the plugin map contributions (markers + layers). Absent on surfaces - // without a trip (CollectionMap), which naturally excludes them — same rule as - // the Leaflet MapPluginMarkers. - tripId?: number | string - // Charging stops / rest areas a plugin route places on the drawn day route. - routeVias?: RouteVia[] - dayBoundaryControls?: DayBoundaryControls - /** The dashed last bit to a place the road network does not reach. */ - accessLines?: { line: [[number, number], [number, number]]; meters: number }[] - route?: [number, number][][] | null - /** - * One colour pair per entry of `route`, or absent for the blue the route has always - * been. Only the road trip passes these, and only while colouring by day is on. - */ - routeColors?: ({ line: string; casing: string } | undefined)[] | null - /** One flag per entry of `route`: true where the stretch is walked, drawn dashed (#2532). */ - routeWalking?: boolean[] | null - /** False while the map is locked (#2010): picking a place leaves the view alone. */ - followSelection?: boolean - /** Given, the lock that sets followSelection sits above the layer switcher. */ - onToggleFollow?: () => void - routeSegments?: RouteSegment[] - selectedPlaceId?: number | null - /** The selected place itself, for when no pin on this map stands for it. */ - selectedPlace?: Place | null - onMarkerClick?: (id: number) => void - hoverDisabled?: boolean - onMapClick?: (info: { latlng: { lat: number; lng: number } }) => void - onMapContextMenu?: ((e: { latlng: { lat: number; lng: number }; originalEvent: MouseEvent | TouchEvent }) => void) | null - center?: [number, number] - zoom?: number - fitKey?: number | null - dayOrderMap?: Record - leftWidth?: number - rightWidth?: number - hasInspector?: boolean - hasDayDetail?: boolean - reservations?: Reservation[] - visibleConnectionIds?: number[] - showTransitRoutes?: boolean - days?: Day[] - selectedDayId?: number | null - /** - * Whether a booking switched on by hand also has to run on the selected day to be - * drawn. Only the phone's plan map asks for it; see RouteVisibilityOptions. - */ - scopeConnectionsToDay?: boolean - showReservationStats?: boolean - onReservationClick?: (reservationId: number) => void - pois?: Poi[] - onPoiClick?: (poi: Poi) => void - /** - * A corridor hit dropped somewhere on the map, with the coordinate it landed on. - * The caller decides whether that point is near enough to the drive to mean anything. - */ - onPoiDropOnRoute?: (osmId: string, lat: number, lng: number) => void - /** A click on the drawn route, for putting a via point there (#1797). */ - onRouteClick?: (lat: number, lng: number) => void - /** The ways of driving one leg, drawn while the picker is open. */ - alternativeRoutes?: AlternativeOverlay[] - /** Which option is being considered, so it can be lit up in its own colour. */ - activeAlternative?: number | null - onChooseAlternative?: (index: number) => void - /** Reports which option the pointer is over, so the list and the map agree. */ - onHighlightAlternative?: (index: number | null) => void - /** Generic numbered control points for list-first route editors. */ - plannerWaypoints?: Array<{ id: string; lat: number; lng: number }> - selectedPlannerWaypointId?: string | null - onPlannerWaypointClick?: (id: string) => void - routeProfileFocus?: RouteProfileFocus | null - viewBaseLayer?: TourBaseLayer - onViewBaseLayerChange?: (layer: TourBaseLayer) => void - /** - * An explicit stretch of map to frame, independent of the day being shown. - * - * `fitKey` cannot express this: it carries no coordinates, and each renderer decides - * for itself that it means "the selected day". Weighing the ways of driving one leg - * needs that leg on screen, which is neither the day nor the trip. - */ - focusPoints?: [number, number][] - /** Changes only when the caller intentionally wants a new initial frame. */ - focusKey?: number - /** - * What the caller's own chrome covers while `focusPoints` is framed, in pixels per edge. - * - * The default padding knows this component's panels and nothing else, and on a phone it - * is a flat margin. A shell that lays its own bars over the map passes what they cover, - * so the frame lands in the part still visible. Only the fit on `focusPoints` reads it. - * Compared by value: the same numbers in a new object do not refit, while new numbers - * refit the points already handed over, because the chrome they must clear has moved. - */ - fitPadding?: ViewportPadding - /** - * Let markers stay apart longer than usual. - * - * A road trip is read along a line: two stops fifty kilometres apart on the same - * motorway are the shape of the day, and merging them into one dot hides it. - */ - clusterLoosely?: boolean - hazards?: RoadtripHazard[] - /** The route recorded in Dawarich, already fetched by MapViewAuto (#2279). */ - dawarichTrack?: DawarichTrack | null - /** Draw only this local day of the recording. */ - dawarichSelectedDate?: string | null - /** Local dates whose day is collapsed in the day plan; their recording is not drawn. */ - dawarichHiddenDates?: ReadonlySet | null - /** Via points to draw as draggable handles, keyed by day (#1797). */ - roadtripVias?: Record - onMoveVia?: (dayId: number, id: number, lat: number, lng: number) => void - onRemoveVia?: (dayId: number, id: number) => void - onViewportChange?: (bbox: { south: number; west: number; north: number; east: number }) => void +/** The GL renderer: the shared map contract plus the engine it is bound to. */ +interface Props extends MapViewProps { glProvider?: GlMapProvider /** * The GL engine, injected instead of imported. Both SDKs used to be pulled in @@ -254,8 +133,6 @@ interface Props { */ // eslint-disable-next-line @typescript-eslint/no-explicit-any gl: any - // eslint-disable-next-line @typescript-eslint/no-explicit-any - onMapReady?: (map: any | null) => void } /** diff --git a/client/src/components/Map/glLazy.tsx b/client/src/components/Map/glLazy.tsx index 7a5fd4e56..0015d8ebf 100644 --- a/client/src/components/Map/glLazy.tsx +++ b/client/src/components/Map/glLazy.tsx @@ -1,4 +1,10 @@ +import type { ComponentType } from 'react' import { lazyWithRetry } from '../../utils/lazyWithRetry' +import type { GlMapProvider } from './glProviders' +import type { MapViewProps } from './mapViewContract' + +/** What a caller hands the GL trip map: the shared map contract and which provider's style to load. */ +export type MapViewGLProps = MapViewProps & { glProvider?: GlMapProvider } /** * The six ways into a GL map, one chunk per engine. @@ -21,15 +27,15 @@ import { lazyWithRetry } from '../../utils/lazyWithRetry' export const MapViewGLMapbox = lazyWithRetry(async () => { const [component, engine] = await Promise.all([import('./MapViewGL'), import('./engines/mapbox')]) const MapViewGL = component.MapViewGL - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { default: (props: any) => } + const bound = (props: MapViewGLProps) => + return { default: bound satisfies ComponentType } }) export const MapViewGLMaplibre = lazyWithRetry(async () => { const [component, engine] = await Promise.all([import('./MapViewGL'), import('./engines/maplibre')]) const MapViewGL = component.MapViewGL - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { default: (props: any) => } + const bound = (props: MapViewGLProps) => + return { default: bound satisfies ComponentType } }) export const JourneyMapGLMapbox = lazyWithRetry(async () => { diff --git a/client/src/components/Map/mapViewContract.ts b/client/src/components/Map/mapViewContract.ts new file mode 100644 index 000000000..e6f147cfb --- /dev/null +++ b/client/src/components/Map/mapViewContract.ts @@ -0,0 +1,178 @@ +import type { DawarichTrack, RoadtripHazard, RoadtripVia } from '@trek/shared'; +import type { Day, Place, Reservation, RouteVia } from '../../types'; +import type { ViewportPadding } from '../../utils/mapViewport'; +import type { RouteProfileFocus } from '../../utils/routeGeometry'; +import type { AlternativeOverlay } from '../Roadtrip/alternativeOverlays'; +import type { DayBoundaryControls } from './dayBoundaryDrag'; +import type { CompassMap } from './MapCompassPill'; +import type { TourBaseLayer } from './MapLayerSwitcher'; +import type { Poi } from './poiCategories'; + +/* + * The props both trip map renderers take: the Leaflet MapView and the GL + * MapViewGL (Mapbox or MapLibre), between which MapViewAuto picks at runtime. + * + * Callers talk to MapViewAuto, so one prop type has to describe what either + * renderer does with it; before this file each side read `any`, and a prop one + * renderer learned and the other never heard of went unnoticed. Both renderers + * now declare `satisfies ComponentType<...>` against this type. + * + * Types only, on purpose: this module must stay free of leaflet, mapbox-gl and + * maplibre-gl (`check:gl-split`), so every import here is `import type`. + */ + +/** + * A pin as the callers of MapViewAuto hand it down. Besides trip places they + * pass the places inside a day's assignments and collection places, and those + * carry no trip_id. Neither renderer reads trip_id, but both type their pins + * as Place, which requires it; see MapViewAutoProps. + */ +export type MapPlace = Omit & { trip_id?: number }; + +/** A leg label between two consecutive stops: where it sits and what it says. */ +export interface RouteSegment { + mid: [number, number]; + from: [number, number]; + to: [number, number]; + walkingText?: string; + drivingText?: string; +} + +export interface MapViewProps { + places?: Place[]; + dayPlaces?: Place[]; + // Enables the plugin map contributions (markers + layers). Absent on surfaces + // without a trip (CollectionMap), which naturally excludes them — same rule as + // the Leaflet MapPluginMarkers. + tripId?: number; + // Charging stops / rest areas a plugin route places on the drawn day route. + routeVias?: RouteVia[]; + dayBoundaryControls?: DayBoundaryControls; + /** The dashed last bit to a place the road network does not reach. */ + accessLines?: { line: [[number, number], [number, number]]; meters: number }[]; + route?: [number, number][][] | null; + /** + * One colour pair per entry of `route`, or absent for the blue the route has always + * been. Only the road trip passes these, and only while colouring by day is on. + */ + routeColors?: ({ line: string; casing: string } | undefined)[] | null; + /** One flag per entry of `route`: true where the stretch is walked, drawn dashed (#2532). */ + routeWalking?: boolean[] | null; + /** False while the map is locked (#2010): picking a place leaves the view alone. */ + followSelection?: boolean; + /** Given, the lock that sets followSelection sits above the layer switcher. */ + onToggleFollow?: () => void; + routeSegments?: RouteSegment[]; + selectedPlaceId?: number | null; + /** The selected place itself, for when no pin on this map stands for it. */ + selectedPlace?: Place | null; + onMarkerClick?: (id: number) => void; + hoverDisabled?: boolean; + onMapClick?: (info: { latlng: { lat: number; lng: number } }) => void; + onMapContextMenu?: + ((e: { latlng: { lat: number; lng: number }; originalEvent: MouseEvent | TouchEvent }) => void) | null; + center?: [number, number]; + zoom?: number; + fitKey?: number | null; + dayOrderMap?: Record; + leftWidth?: number; + rightWidth?: number; + hasInspector?: boolean; + hasDayDetail?: boolean; + reservations?: Reservation[]; + visibleConnectionIds?: number[]; + showTransitRoutes?: boolean; + days?: Day[]; + selectedDayId?: number | null; + /** + * Whether a booking switched on by hand also has to run on the selected day to be + * drawn. Only the phone's plan map asks for it; see RouteVisibilityOptions. + */ + scopeConnectionsToDay?: boolean; + showReservationStats?: boolean; + onReservationClick?: (reservationId: number) => void; + pois?: Poi[]; + onPoiClick?: (poi: Poi) => void; + /** + * A corridor hit dropped somewhere on the map, with the coordinate it landed on. + * The caller decides whether that point is near enough to the drive to mean anything. + */ + onPoiDropOnRoute?: (osmId: string, lat: number, lng: number) => void; + /** A click on the drawn route, for putting a via point there (#1797). */ + onRouteClick?: (lat: number, lng: number) => void; + /** The ways of driving one leg, drawn while the picker is open. */ + alternativeRoutes?: AlternativeOverlay[]; + /** Which option is being considered, so it can be lit up in its own colour. */ + activeAlternative?: number | null; + onChooseAlternative?: (index: number) => void; + /** Reports which option the pointer is over, so the list and the map agree. */ + onHighlightAlternative?: (index: number | null) => void; + /** Generic numbered control points for list-first route editors. */ + plannerWaypoints?: Array<{ id: string; lat: number; lng: number }>; + selectedPlannerWaypointId?: string | null; + onPlannerWaypointClick?: (id: string) => void; + routeProfileFocus?: RouteProfileFocus | null; + viewBaseLayer?: TourBaseLayer; + onViewBaseLayerChange?: (layer: TourBaseLayer) => void; + /** + * An explicit stretch of map to frame, independent of the day being shown. + * + * `fitKey` cannot express this: it carries no coordinates, and each renderer decides + * for itself that it means "the selected day". Weighing the ways of driving one leg + * needs that leg on screen, which is neither the day nor the trip. + */ + focusPoints?: [number, number][]; + /** Changes only when the caller intentionally wants a new initial frame. */ + focusKey?: number; + /** + * What the caller's own chrome covers while `focusPoints` is framed, in pixels per edge. + * + * The default padding knows this component's panels and nothing else, and on a phone it + * is a flat margin. A shell that lays its own bars over the map passes what they cover, + * so the frame lands in the part still visible. Only the fit on `focusPoints` reads it. + * Compared by value: the same numbers in a new object do not refit, while new numbers + * refit the points already handed over, because the chrome they must clear has moved. + */ + fitPadding?: ViewportPadding; + /** + * Let markers stay apart longer than usual. + * + * A road trip is read along a line: two stops fifty kilometres apart on the same + * motorway are the shape of the day, and merging them into one dot hides it. + */ + clusterLoosely?: boolean; + hazards?: RoadtripHazard[]; + /** The route recorded in Dawarich, already fetched by MapViewAuto (#2279). */ + dawarichTrack?: DawarichTrack | null; + /** Draw only this local day of the recording. */ + dawarichSelectedDate?: string | null; + /** Local dates whose day is collapsed in the day plan; their recording is not drawn. */ + dawarichHiddenDates?: ReadonlySet | null; + /** Via points to draw as draggable handles, keyed by day (#1797). */ + roadtripVias?: Record; + onMoveVia?: (dayId: number, id: number, lat: number, lng: number) => void; + onRemoveVia?: (dayId: number, id: number) => void; + onViewportChange?: (bbox: { south: number; west: number; north: number; east: number }) => void; + /** + * The raster tile URL template. Only the Leaflet renderer draws raster tiles; + * the GL renderer draws the style of the chosen provider and ignores it. + */ + tileUrl?: string; + /** + * Hands the caller the live map once it is ready. Only the GL renderer calls + * it; the Leaflet renderer takes the prop and never calls it. + */ + onMapReady?: (map: CompassMap | null) => void; +} + +/** + * What MapViewAuto accepts: the renderer contract with pins as callers really + * pass them (MapPlace, possibly without trip_id). The gap between this and + * MapViewProps is known drift between the callers and the renderers' Place + * type; it is bridged in one place, in MapViewAuto, until the renderers type + * their pins by what they read. + */ +export type MapViewAutoProps = Omit & { + places?: MapPlace[]; + dayPlaces?: MapPlace[]; +};