mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
98cc57942b
The tours map gets a 3D mode on the same map surface between the side panels: a "3D" switch in the layer pill drapes the route over open Terrarium elevation tiles (AWS Open Data), colours it by grade, plays a flyover tied to the elevation profile in the rail, and keeps planning working by clicking and dragging waypoints on the terrain. The corridor around the route is preloaded before the flyover so the landscape never loads in front of the camera. GPX imports no longer get a made-up hiking difficulty: max_hiking_difficulty becomes nullable (new migration, waypoints kept), the import writes null and the client shows no T badge for it. Drawn tours still default to T2. Info hints in the planner show their tooltip only, and the discard confirmation reads "Discard changes".
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
import { Map as MapIcon, Mountain, Rotate3d, Satellite } from 'lucide-react'
|
|
import type { CSSProperties } from 'react'
|
|
import { useTranslation } from '../../i18n'
|
|
import { Tooltip } from '../shared/Tooltip'
|
|
import { MAP_CONTROL_SHADOW } from './mapControlShadow'
|
|
|
|
export type BaseLayer = 'default' | 'satellite'
|
|
export type TourBaseLayer = BaseLayer | 'topo'
|
|
|
|
const FROSTED_SHELL = {
|
|
display: 'inline-flex', alignItems: 'center', padding: 4, borderRadius: 999, pointerEvents: 'auto' as const,
|
|
background: 'var(--sidebar-bg)',
|
|
backdropFilter: 'blur(20px) saturate(180%)',
|
|
WebkitBackdropFilter: 'blur(20px) saturate(180%)',
|
|
boxShadow: MAP_CONTROL_SHADOW,
|
|
}
|
|
|
|
// Where the switcher sits and how much room it takes, so a control placed beside it
|
|
// (the phone's compass) is positioned off the same numbers instead of guessing them.
|
|
// Both map engines place the switcher with the inset, so they cannot drift apart either.
|
|
|
|
/** Distance from the map's left edge (plus any side panel) to the switcher. */
|
|
export const MAP_LAYER_SWITCHER_INSET = 20
|
|
/**
|
|
* Outer size of the round shell: a 34px button in 4px of padding, the same shell as
|
|
* MapCompassPill. The markup below keeps its literals, and FE-COMP-MAPLAYER-006 holds
|
|
* them to this number.
|
|
*/
|
|
export const MAP_ROUND_CONTROL_SIZE = 42
|
|
|
|
// Round base-layer switcher for both planner maps, Leaflet and GL (default street
|
|
// tiles ↔ satellite). Same frosted shell as MapCompassPill so it lines up with the
|
|
// other map controls; the icon shows the layer it switches to.
|
|
export function MapLayerSwitcher({ active, onToggle }: { active: BaseLayer; onToggle: () => void }) {
|
|
const { t } = useTranslation()
|
|
const isSatellite = active === 'satellite'
|
|
const Icon = isSatellite ? MapIcon : Satellite
|
|
const label = isSatellite ? t('map.baseLayer.switchToDefault') : t('map.baseLayer.switchToSatellite')
|
|
|
|
return (
|
|
<div style={FROSTED_SHELL}>
|
|
{/* Same tooltip as its neighbours on the map, for the same reasons — see
|
|
TripRouteOverviewPill. To the right, though: this one sits at the
|
|
bottom left of the map, where a tooltip to the left would land on the
|
|
day sidebar and one below would land off the screen. */}
|
|
<Tooltip label={label} placement="right">
|
|
<button
|
|
type="button"
|
|
onClick={onToggle}
|
|
aria-label={label}
|
|
aria-pressed={isSatellite}
|
|
className="text-content-muted"
|
|
style={{
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
|
|
width: 34, height: 34, borderRadius: 999, border: 'none', cursor: 'pointer',
|
|
background: 'transparent', padding: 0,
|
|
transition: 'background 0.14s, color 0.14s',
|
|
}}
|
|
onMouseEnter={e => { e.currentTarget.style.background = 'var(--bg-hover)' }}
|
|
onMouseLeave={e => { e.currentTarget.style.background = 'transparent' }}
|
|
>
|
|
<Icon size={17} strokeWidth={2} />
|
|
</button>
|
|
</Tooltip>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const TOUR_LAYERS = [
|
|
{ id: 'default', labelKey: 'map.baseLayer.default', Icon: MapIcon },
|
|
{ id: 'topo', labelKey: null, Icon: Mountain },
|
|
{ id: 'satellite', labelKey: 'map.baseLayer.satellite', Icon: Satellite },
|
|
] as const
|
|
|
|
/** A pill segment of the Tours control; `filled` lets its class paint the background. */
|
|
const segmentStyle = (filled: boolean): CSSProperties => ({
|
|
display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 5,
|
|
minHeight: 34, borderRadius: 999, border: 'none', cursor: 'pointer',
|
|
background: filled ? undefined : 'transparent', padding: '0 10px',
|
|
fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 600,
|
|
})
|
|
|
|
/** Per-view Tours control. It never reads or writes the global map setting. */
|
|
export function TourMapLayerSwitcher({
|
|
active,
|
|
onChange,
|
|
onToggleTerrain,
|
|
terrainActive = false,
|
|
}: {
|
|
active: TourBaseLayer
|
|
onChange: (layer: TourBaseLayer) => void
|
|
/** Offers the 3D terrain switch beside the base layers. */
|
|
onToggleTerrain?: () => void
|
|
terrainActive?: boolean
|
|
}) {
|
|
const { t } = useTranslation()
|
|
|
|
return (
|
|
<div style={{ ...FROSTED_SHELL, gap: 2 }} role="group" aria-label={t('settings.mapStyle')}>
|
|
{TOUR_LAYERS.map(({ id, labelKey, Icon }) => {
|
|
const selected = active === id
|
|
const label = labelKey ? t(labelKey) : 'Topo'
|
|
return (
|
|
<button
|
|
key={id}
|
|
type="button"
|
|
onClick={() => onChange(id)}
|
|
aria-pressed={selected}
|
|
className={selected ? 'bg-accent-soft text-accent' : 'text-content-muted'}
|
|
style={segmentStyle(selected)}
|
|
>
|
|
<Icon size={15} strokeWidth={2} />
|
|
<span>{label}</span>
|
|
</button>
|
|
)
|
|
})}
|
|
{onToggleTerrain && (
|
|
<>
|
|
<span aria-hidden className="mx-0.5 h-5 w-px bg-edge" />
|
|
<button
|
|
type="button"
|
|
onClick={onToggleTerrain}
|
|
aria-pressed={terrainActive}
|
|
className={terrainActive ? 'bg-accent text-accent-text' : 'text-content-muted'}
|
|
style={segmentStyle(terrainActive)}
|
|
>
|
|
<Rotate3d size={15} strokeWidth={2} />
|
|
<span>3D</span>
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|