refactor(client): type both trip map renderers against one MapViewProps contract

MapViewAuto took props: any and the Leaflet MapView destructured any, so a prop
one renderer learned and the other never heard of went unnoticed.
components/Map/mapViewContract.ts holds MapViewProps, derived from the GL
renderer's props and what the callers pass; MapView and the GL bindings in
glLazy declare satisfies ComponentType against it, and the GL renderer extends
it with its engine. The file imports types only, so check:gl-split is not
affected. Types only, no runtime change.

The types show two known gaps, recorded in the contract rather than papered
over: onMapReady is called by the GL renderer only, and callers pass pins
without trip_id (assignment and collection places) while both renderers type
their pins as Place; MapViewAuto bridges the latter with one commented cast.
This commit is contained in:
Maurice
2026-10-10 12:42:31 +02:00
parent 5fcb307270
commit f51a64017d
8 changed files with 213 additions and 147 deletions
+4 -4
View File
@@ -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
}
}
+1 -1
View File
@@ -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,
+4 -1
View File
@@ -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<string, any> = {}) => ({ 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<Record<keyof RouteVia, unknown>> = {}) =>
({ 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(<MapView routeVias={[via({ tone: 'success' }), via({ tone: 'danger', lat: 48.7 })]} />)
+5 -5
View File
@@ -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<MapViewProps>
+5 -3
View File
@@ -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,
+6 -129
View File
@@ -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<number, number[] | null>
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<string> | null
/** Via points to draw as draggable handles, keyed by day (#1797). */
roadtripVias?: Record<number, RoadtripVia[]>
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
}
/**
+10 -4
View File
@@ -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) => <MapViewGL {...props} gl={engine.default} /> }
const bound = (props: MapViewGLProps) => <MapViewGL {...props} gl={engine.default} />
return { default: bound satisfies ComponentType<MapViewGLProps> }
})
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) => <MapViewGL {...props} gl={engine.default} /> }
const bound = (props: MapViewGLProps) => <MapViewGL {...props} gl={engine.default} />
return { default: bound satisfies ComponentType<MapViewGLProps> }
})
export const JourneyMapGLMapbox = lazyWithRetry(async () => {
@@ -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<Place, 'trip_id'> & { 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<number, number[] | null>;
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<string> | null;
/** Via points to draw as draggable handles, keyed by day (#1797). */
roadtripVias?: Record<number, RoadtripVia[]>;
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<MapViewProps, 'places' | 'dayPlaces'> & {
places?: MapPlace[];
dayPlaces?: MapPlace[];
};