mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
chore: apply prettier on the entire project
This commit is contained in:
@@ -1,55 +1,61 @@
|
||||
import { useEffect, useRef, useImperativeHandle, forwardRef, useCallback } from 'react'
|
||||
import mapboxgl from 'mapbox-gl'
|
||||
import 'mapbox-gl/dist/mapbox-gl.css'
|
||||
import { useSettingsStore } from '../../store/settingsStore'
|
||||
import { isStandardFamily, supportsCustom3d, wantsTerrain, addCustom3dBuildings, addTerrainAndSky } from '../Map/mapboxSetup'
|
||||
import mapboxgl from 'mapbox-gl';
|
||||
import 'mapbox-gl/dist/mapbox-gl.css';
|
||||
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { useSettingsStore } from '../../store/settingsStore';
|
||||
import {
|
||||
addCustom3dBuildings,
|
||||
addTerrainAndSky,
|
||||
isStandardFamily,
|
||||
supportsCustom3d,
|
||||
wantsTerrain,
|
||||
} from '../Map/mapboxSetup';
|
||||
|
||||
export interface JourneyMapGLHandle {
|
||||
highlightMarker: (id: string | null) => void
|
||||
focusMarker: (id: string) => void
|
||||
invalidateSize: () => void
|
||||
highlightMarker: (id: string | null) => void;
|
||||
focusMarker: (id: string) => void;
|
||||
invalidateSize: () => void;
|
||||
}
|
||||
|
||||
interface MapEntry {
|
||||
id: string
|
||||
lat: number
|
||||
lng: number
|
||||
title?: string | null
|
||||
location_name?: string | null
|
||||
mood?: string | null
|
||||
entry_date: string
|
||||
dayColor?: string
|
||||
dayLabel?: number
|
||||
id: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
title?: string | null;
|
||||
location_name?: string | null;
|
||||
mood?: string | null;
|
||||
entry_date: string;
|
||||
dayColor?: string;
|
||||
dayLabel?: number;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
checkins: unknown[]
|
||||
entries: MapEntry[]
|
||||
trail?: { lat: number; lng: number }[]
|
||||
height?: number
|
||||
dark?: boolean
|
||||
activeMarkerId?: string | null
|
||||
onMarkerClick?: (id: string, type?: string) => void
|
||||
fullScreen?: boolean
|
||||
paddingBottom?: number
|
||||
checkins: unknown[];
|
||||
entries: MapEntry[];
|
||||
trail?: { lat: number; lng: number }[];
|
||||
height?: number;
|
||||
dark?: boolean;
|
||||
activeMarkerId?: string | null;
|
||||
onMarkerClick?: (id: string, type?: string) => void;
|
||||
fullScreen?: boolean;
|
||||
paddingBottom?: number;
|
||||
}
|
||||
|
||||
interface Item {
|
||||
id: string
|
||||
lat: number
|
||||
lng: number
|
||||
label: string
|
||||
locationName: string
|
||||
time: string
|
||||
dayColor: string
|
||||
dayLabel: number
|
||||
id: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
label: string;
|
||||
locationName: string;
|
||||
time: string;
|
||||
dayColor: string;
|
||||
dayLabel: number;
|
||||
}
|
||||
|
||||
const MARKER_W = 28
|
||||
const MARKER_H = 36
|
||||
const MARKER_W = 28;
|
||||
const MARKER_H = 36;
|
||||
|
||||
function buildItems(entries: MapEntry[]): Item[] {
|
||||
const items: Item[] = []
|
||||
const items: Item[] = [];
|
||||
for (const e of entries) {
|
||||
if (e.lat && e.lng) {
|
||||
items.push({
|
||||
@@ -61,11 +67,11 @@ function buildItems(entries: MapEntry[]): Item[] {
|
||||
time: e.entry_date,
|
||||
dayColor: e.dayColor || '#52525B',
|
||||
dayLabel: e.dayLabel ?? 1,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
items.sort((a, b) => a.time.localeCompare(b.time))
|
||||
return items
|
||||
items.sort((a, b) => a.time.localeCompare(b.time));
|
||||
return items;
|
||||
}
|
||||
|
||||
function escapeHtml(s: string): string {
|
||||
@@ -74,26 +80,26 @@ function escapeHtml(s: string): string {
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function formatEntryDate(iso: string): string {
|
||||
if (!iso) return ''
|
||||
if (!iso) return '';
|
||||
try {
|
||||
const d = new Date(iso.includes('T') ? iso : iso + 'T00:00:00')
|
||||
if (Number.isNaN(d.getTime())) return iso
|
||||
return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' }).format(d)
|
||||
const d = new Date(iso.includes('T') ? iso : iso + 'T00:00:00');
|
||||
if (Number.isNaN(d.getTime())) return iso;
|
||||
return new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' }).format(d);
|
||||
} catch {
|
||||
return iso
|
||||
return iso;
|
||||
}
|
||||
}
|
||||
|
||||
// Inject the popup styles once per document. Two-line frosted-glass card in
|
||||
// the Apple/Google Maps idiom — title on top, location / date subtly below.
|
||||
function ensureJourneyPopupStyle() {
|
||||
if (document.getElementById('trek-journey-popup-style')) return
|
||||
const s = document.createElement('style')
|
||||
s.id = 'trek-journey-popup-style'
|
||||
if (document.getElementById('trek-journey-popup-style')) return;
|
||||
const s = document.createElement('style');
|
||||
s.id = 'trek-journey-popup-style';
|
||||
s.textContent = `
|
||||
.mapboxgl-popup.trek-journey-popup { pointer-events: none; animation: trek-journey-popup-in 180ms ease-out; }
|
||||
.mapboxgl-popup.trek-journey-popup .mapboxgl-popup-content {
|
||||
@@ -159,93 +165,94 @@ function ensureJourneyPopupStyle() {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
`
|
||||
document.head.appendChild(s)
|
||||
`;
|
||||
document.head.appendChild(s);
|
||||
}
|
||||
|
||||
function markerHtml(dayColor: string, dayLabel: number, highlighted: boolean): HTMLDivElement {
|
||||
const fill = dayColor
|
||||
const textColor = '#fff'
|
||||
const stroke = highlighted ? '#fff' : 'rgba(255,255,255,0.5)'
|
||||
const fill = dayColor;
|
||||
const textColor = '#fff';
|
||||
const stroke = highlighted ? '#fff' : 'rgba(255,255,255,0.5)';
|
||||
const shadow = highlighted
|
||||
? 'drop-shadow(0 0 10px rgba(0,0,0,0.4)) drop-shadow(0 2px 6px rgba(0,0,0,0.4))'
|
||||
: 'drop-shadow(0 2px 4px rgba(0,0,0,0.25))'
|
||||
const scale = highlighted ? 1.2 : 1
|
||||
const label = String(dayLabel)
|
||||
: 'drop-shadow(0 2px 4px rgba(0,0,0,0.25))';
|
||||
const scale = highlighted ? 1.2 : 1;
|
||||
const label = String(dayLabel);
|
||||
|
||||
// Outer wrap holds the element mapbox positions via `transform: translate(...)`.
|
||||
// Anything animated (scale, filter) has to live on an inner child — otherwise
|
||||
// the CSS transition would catch the map's per-frame translate updates and
|
||||
// the marker smears all over the viewport while scrolling / flying.
|
||||
const wrap = document.createElement('div')
|
||||
wrap.style.cssText = `width:${MARKER_W}px;height:${MARKER_H}px;cursor:pointer;`
|
||||
const inner = document.createElement('div')
|
||||
inner.className = 'trek-journey-marker-inner'
|
||||
inner.style.cssText = `width:100%;height:100%;transform:scale(${scale});transform-origin:bottom center;transition:transform 0.2s ease;filter:${shadow};`
|
||||
const wrap = document.createElement('div');
|
||||
wrap.style.cssText = `width:${MARKER_W}px;height:${MARKER_H}px;cursor:pointer;`;
|
||||
const inner = document.createElement('div');
|
||||
inner.className = 'trek-journey-marker-inner';
|
||||
inner.style.cssText = `width:100%;height:100%;transform:scale(${scale});transform-origin:bottom center;transition:transform 0.2s ease;filter:${shadow};`;
|
||||
inner.innerHTML = `<svg width="${MARKER_W}" height="${MARKER_H}" viewBox="0 0 28 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 34C14 34 26 22.36 26 13C26 6.37 20.63 1 14 1C7.37 1 2 6.37 2 13C2 22.36 14 34 14 34Z" fill="${fill}" stroke="${stroke}" stroke-width="1.5"/>
|
||||
<circle cx="14" cy="13" r="8" fill="${fill}"/>
|
||||
<text x="14" y="13" text-anchor="middle" dominant-baseline="central" fill="${textColor}" font-family="-apple-system,system-ui,sans-serif" font-size="11" font-weight="700">${label}</text>
|
||||
</svg>`
|
||||
wrap.appendChild(inner)
|
||||
return wrap
|
||||
</svg>`;
|
||||
wrap.appendChild(inner);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
const EMPTY_TRAIL: { lat: number; lng: number }[] = []
|
||||
const EMPTY_TRAIL: { lat: number; lng: number }[] = [];
|
||||
|
||||
const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL(
|
||||
{ entries, trail, height = 220, dark, activeMarkerId, onMarkerClick, fullScreen, paddingBottom },
|
||||
ref
|
||||
) {
|
||||
const stableTrail = trail || EMPTY_TRAIL
|
||||
const mapboxStyle = useSettingsStore(s => s.settings.mapbox_style || 'mapbox://styles/mapbox/standard')
|
||||
const mapboxToken = useSettingsStore(s => s.settings.mapbox_access_token || '')
|
||||
const mapbox3d = useSettingsStore(s => s.settings.mapbox_3d_enabled !== false)
|
||||
const mapboxQuality = useSettingsStore(s => s.settings.mapbox_quality_mode === true)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const mapRef = useRef<mapboxgl.Map | null>(null)
|
||||
const markersRef = useRef<Map<string, mapboxgl.Marker>>(new Map())
|
||||
const itemsRef = useRef<Item[]>([])
|
||||
const highlightedRef = useRef<string | null>(null)
|
||||
const popupRef = useRef<mapboxgl.Popup | null>(null)
|
||||
const onMarkerClickRef = useRef(onMarkerClick)
|
||||
onMarkerClickRef.current = onMarkerClick
|
||||
const darkRef = useRef(dark)
|
||||
darkRef.current = dark
|
||||
const stableTrail = trail || EMPTY_TRAIL;
|
||||
const mapboxStyle = useSettingsStore((s) => s.settings.mapbox_style || 'mapbox://styles/mapbox/standard');
|
||||
const mapboxToken = useSettingsStore((s) => s.settings.mapbox_access_token || '');
|
||||
const mapbox3d = useSettingsStore((s) => s.settings.mapbox_3d_enabled !== false);
|
||||
const mapboxQuality = useSettingsStore((s) => s.settings.mapbox_quality_mode === true);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const mapRef = useRef<mapboxgl.Map | null>(null);
|
||||
const markersRef = useRef<Map<string, mapboxgl.Marker>>(new Map());
|
||||
const itemsRef = useRef<Item[]>([]);
|
||||
const highlightedRef = useRef<string | null>(null);
|
||||
const popupRef = useRef<mapboxgl.Popup | null>(null);
|
||||
const onMarkerClickRef = useRef(onMarkerClick);
|
||||
onMarkerClickRef.current = onMarkerClick;
|
||||
const darkRef = useRef(dark);
|
||||
darkRef.current = dark;
|
||||
|
||||
const showPopup = useCallback((id: string) => {
|
||||
const item = itemsRef.current.find(i => i.id === id)
|
||||
if (!item || !mapRef.current) return
|
||||
ensureJourneyPopupStyle()
|
||||
const item = itemsRef.current.find((i) => i.id === id);
|
||||
if (!item || !mapRef.current) return;
|
||||
ensureJourneyPopupStyle();
|
||||
// Primary line: user-given title. If none, fall back to the location
|
||||
// name so we always show *something* useful on the top line.
|
||||
const primaryRaw = item.label || item.locationName || 'Entry'
|
||||
const secondaryPlace = item.label ? item.locationName : ''
|
||||
const dateStr = formatEntryDate(item.time)
|
||||
const primary = escapeHtml(primaryRaw)
|
||||
const place = escapeHtml(secondaryPlace)
|
||||
const date = escapeHtml(dateStr)
|
||||
const primaryRaw = item.label || item.locationName || 'Entry';
|
||||
const secondaryPlace = item.label ? item.locationName : '';
|
||||
const dateStr = formatEntryDate(item.time);
|
||||
const primary = escapeHtml(primaryRaw);
|
||||
const place = escapeHtml(secondaryPlace);
|
||||
const date = escapeHtml(dateStr);
|
||||
|
||||
const subParts: string[] = []
|
||||
if (place) subParts.push(`<span class="trek-journey-popup-place">${place}</span>`)
|
||||
if (date) subParts.push(`<span class="trek-journey-popup-date">${date}</span>`)
|
||||
const subline = subParts.length === 2
|
||||
? `${subParts[0]}<span class="trek-journey-popup-sep">\u00B7</span>${subParts[1]}`
|
||||
: subParts.join('')
|
||||
const subParts: string[] = [];
|
||||
if (place) subParts.push(`<span class="trek-journey-popup-place">${place}</span>`);
|
||||
if (date) subParts.push(`<span class="trek-journey-popup-date">${date}</span>`);
|
||||
const subline =
|
||||
subParts.length === 2
|
||||
? `${subParts[0]}<span class="trek-journey-popup-sep">\u00B7</span>${subParts[1]}`
|
||||
: subParts.join('');
|
||||
|
||||
const html = `
|
||||
<div class="trek-journey-popup-title">${primary}</div>
|
||||
${subline ? `<div class="trek-journey-popup-sub">${subline}</div>` : ''}
|
||||
`
|
||||
`;
|
||||
// Marker is bottom-anchored with a visible height of 36px (1.2× on
|
||||
// highlight ≈ 44px), so -46 keeps the popup just clear of the pin top.
|
||||
const offset: [number, number] = [0, -46]
|
||||
const offset: [number, number] = [0, -46];
|
||||
if (popupRef.current) {
|
||||
popupRef.current.setLngLat([item.lng, item.lat])
|
||||
popupRef.current.setHTML(html)
|
||||
popupRef.current.setOffset(offset)
|
||||
const el = popupRef.current.getElement()
|
||||
if (el) el.classList.toggle('trek-dark', !!darkRef.current)
|
||||
popupRef.current.setLngLat([item.lng, item.lat]);
|
||||
popupRef.current.setHTML(html);
|
||||
popupRef.current.setOffset(offset);
|
||||
const el = popupRef.current.getElement();
|
||||
if (el) el.classList.toggle('trek-dark', !!darkRef.current);
|
||||
} else {
|
||||
popupRef.current = new mapboxgl.Popup({
|
||||
closeButton: false,
|
||||
@@ -258,78 +265,98 @@ const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL
|
||||
})
|
||||
.setLngLat([item.lng, item.lat])
|
||||
.setHTML(html)
|
||||
.addTo(mapRef.current)
|
||||
.addTo(mapRef.current);
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const hidePopup = useCallback(() => {
|
||||
if (popupRef.current) {
|
||||
try { popupRef.current.remove() } catch { /* noop */ }
|
||||
popupRef.current = null
|
||||
try {
|
||||
popupRef.current.remove();
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
popupRef.current = null;
|
||||
}
|
||||
}, [])
|
||||
}, []);
|
||||
|
||||
const setMarkerStyle = useCallback((id: string, highlighted: boolean) => {
|
||||
const item = itemsRef.current.find(i => i.id === id)
|
||||
const marker = markersRef.current.get(id)
|
||||
if (!item || !marker) return
|
||||
const el = marker.getElement()
|
||||
const currentInner = el.querySelector('.trek-journey-marker-inner') as HTMLDivElement | null
|
||||
if (!currentInner) return
|
||||
const item = itemsRef.current.find((i) => i.id === id);
|
||||
const marker = markersRef.current.get(id);
|
||||
if (!item || !marker) return;
|
||||
const el = marker.getElement();
|
||||
const currentInner = el.querySelector('.trek-journey-marker-inner') as HTMLDivElement | null;
|
||||
if (!currentInner) return;
|
||||
// Only swap the inner element's styles/HTML. Touching `el.style.cssText`
|
||||
// would wipe mapbox's positional transform and make the marker flicker.
|
||||
const next = markerHtml(item.dayColor, item.dayLabel, highlighted)
|
||||
const nextInner = next.querySelector('.trek-journey-marker-inner') as HTMLDivElement
|
||||
currentInner.style.cssText = nextInner.style.cssText
|
||||
currentInner.innerHTML = nextInner.innerHTML
|
||||
el.style.zIndex = highlighted ? '1000' : '0'
|
||||
}, [])
|
||||
const next = markerHtml(item.dayColor, item.dayLabel, highlighted);
|
||||
const nextInner = next.querySelector('.trek-journey-marker-inner') as HTMLDivElement;
|
||||
currentInner.style.cssText = nextInner.style.cssText;
|
||||
currentInner.innerHTML = nextInner.innerHTML;
|
||||
el.style.zIndex = highlighted ? '1000' : '0';
|
||||
}, []);
|
||||
|
||||
const highlightMarker = useCallback((id: string | null) => {
|
||||
const prev = highlightedRef.current
|
||||
highlightedRef.current = id
|
||||
if (prev && prev !== id) setMarkerStyle(prev, false)
|
||||
if (id) {
|
||||
setMarkerStyle(id, true)
|
||||
showPopup(id)
|
||||
} else {
|
||||
hidePopup()
|
||||
}
|
||||
}, [setMarkerStyle, showPopup, hidePopup])
|
||||
const highlightMarker = useCallback(
|
||||
(id: string | null) => {
|
||||
const prev = highlightedRef.current;
|
||||
highlightedRef.current = id;
|
||||
if (prev && prev !== id) setMarkerStyle(prev, false);
|
||||
if (id) {
|
||||
setMarkerStyle(id, true);
|
||||
showPopup(id);
|
||||
} else {
|
||||
hidePopup();
|
||||
}
|
||||
},
|
||||
[setMarkerStyle, showPopup, hidePopup]
|
||||
);
|
||||
|
||||
const focusMarker = useCallback((id: string) => {
|
||||
highlightMarker(id)
|
||||
const marker = markersRef.current.get(id)
|
||||
if (!marker || !mapRef.current) return
|
||||
try {
|
||||
mapRef.current.flyTo({
|
||||
center: marker.getLngLat(),
|
||||
zoom: Math.max(mapRef.current.getZoom(), 14),
|
||||
pitch: mapbox3d ? 45 : 0,
|
||||
duration: 600,
|
||||
})
|
||||
} catch { /* map not yet ready */ }
|
||||
}, [highlightMarker, mapbox3d])
|
||||
const focusMarker = useCallback(
|
||||
(id: string) => {
|
||||
highlightMarker(id);
|
||||
const marker = markersRef.current.get(id);
|
||||
if (!marker || !mapRef.current) return;
|
||||
try {
|
||||
mapRef.current.flyTo({
|
||||
center: marker.getLngLat(),
|
||||
zoom: Math.max(mapRef.current.getZoom(), 14),
|
||||
pitch: mapbox3d ? 45 : 0,
|
||||
duration: 600,
|
||||
});
|
||||
} catch {
|
||||
/* map not yet ready */
|
||||
}
|
||||
},
|
||||
[highlightMarker, mapbox3d]
|
||||
);
|
||||
|
||||
const invalidateSize = useCallback(() => {
|
||||
try { mapRef.current?.resize() } catch { /* map not yet ready */ }
|
||||
}, [])
|
||||
try {
|
||||
mapRef.current?.resize();
|
||||
} catch {
|
||||
/* map not yet ready */
|
||||
}
|
||||
}, []);
|
||||
|
||||
useImperativeHandle(ref, () => ({ highlightMarker, focusMarker, invalidateSize }), [highlightMarker, focusMarker, invalidateSize])
|
||||
useImperativeHandle(ref, () => ({ highlightMarker, focusMarker, invalidateSize }), [
|
||||
highlightMarker,
|
||||
focusMarker,
|
||||
invalidateSize,
|
||||
]);
|
||||
|
||||
// Build map once per style/token change. Markers and layers are rebuilt
|
||||
// inside the same effect so they stay in sync with the active style.
|
||||
useEffect(() => {
|
||||
if (!containerRef.current || !mapboxToken) return
|
||||
mapboxgl.accessToken = mapboxToken
|
||||
if (!containerRef.current || !mapboxToken) return;
|
||||
mapboxgl.accessToken = mapboxToken;
|
||||
|
||||
const items = buildItems(entries)
|
||||
itemsRef.current = items
|
||||
const items = buildItems(entries);
|
||||
itemsRef.current = items;
|
||||
|
||||
const bounds = new mapboxgl.LngLatBounds()
|
||||
items.forEach(i => bounds.extend([i.lng, i.lat]))
|
||||
stableTrail.forEach(p => bounds.extend([p.lng, p.lat]))
|
||||
const hasPoints = items.length > 0 || stableTrail.length > 0
|
||||
const bounds = new mapboxgl.LngLatBounds();
|
||||
items.forEach((i) => bounds.extend([i.lng, i.lat]));
|
||||
stableTrail.forEach((p) => bounds.extend([p.lng, p.lat]));
|
||||
const hasPoints = items.length > 0 || stableTrail.length > 0;
|
||||
|
||||
const map = new mapboxgl.Map({
|
||||
container: containerRef.current,
|
||||
@@ -340,31 +367,42 @@ const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL
|
||||
attributionControl: true,
|
||||
antialias: mapboxQuality,
|
||||
projection: mapboxQuality ? 'globe' : 'mercator',
|
||||
})
|
||||
mapRef.current = map
|
||||
});
|
||||
mapRef.current = map;
|
||||
|
||||
map.on('load', () => {
|
||||
if (mapbox3d) {
|
||||
if (!isStandardFamily(mapboxStyle) && wantsTerrain(mapboxStyle)) addTerrainAndSky(map)
|
||||
if (supportsCustom3d(mapboxStyle)) addCustom3dBuildings(map, !!darkRef.current)
|
||||
if (!isStandardFamily(mapboxStyle) && wantsTerrain(mapboxStyle)) addTerrainAndSky(map);
|
||||
if (supportsCustom3d(mapboxStyle)) addCustom3dBuildings(map, !!darkRef.current);
|
||||
}
|
||||
// Flatten Mapbox Standard's built-in DEM so HTML markers (at Z=0)
|
||||
// stay pinned to their coordinates at every zoom and pitch.
|
||||
if (mapboxStyle === 'mapbox://styles/mapbox/standard') {
|
||||
try { map.setTerrain(null) } catch { /* noop */ }
|
||||
try {
|
||||
map.setTerrain(null);
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
}
|
||||
|
||||
// route trail — dashed line connecting entries in time order
|
||||
if (items.length > 1) {
|
||||
const coords = items.map(i => [i.lng, i.lat])
|
||||
if (map.getSource('journey-route')) (map.getSource('journey-route') as mapboxgl.GeoJSONSource).setData({
|
||||
type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } as GeoJSON.LineString,
|
||||
})
|
||||
const coords = items.map((i) => [i.lng, i.lat]);
|
||||
if (map.getSource('journey-route'))
|
||||
(map.getSource('journey-route') as mapboxgl.GeoJSONSource).setData({
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'LineString', coordinates: coords } as GeoJSON.LineString,
|
||||
});
|
||||
else {
|
||||
map.addSource('journey-route', {
|
||||
type: 'geojson',
|
||||
data: { type: 'Feature', properties: {}, geometry: { type: 'LineString', coordinates: coords } as GeoJSON.LineString },
|
||||
})
|
||||
data: {
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: { type: 'LineString', coordinates: coords } as GeoJSON.LineString,
|
||||
},
|
||||
});
|
||||
map.addLayer({
|
||||
id: 'journey-route-line',
|
||||
type: 'line',
|
||||
@@ -376,88 +414,115 @@ const JourneyMapGL = forwardRef<JourneyMapGLHandle, Props>(function JourneyMapGL
|
||||
'line-dasharray': [2, 3],
|
||||
},
|
||||
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// markers
|
||||
items.forEach((item) => {
|
||||
const el = markerHtml(item.dayColor, item.dayLabel, false)
|
||||
const el = markerHtml(item.dayColor, item.dayLabel, false);
|
||||
const marker = new mapboxgl.Marker({ element: el, anchor: 'bottom' })
|
||||
.setLngLat([item.lng, item.lat])
|
||||
.addTo(map)
|
||||
.addTo(map);
|
||||
el.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation()
|
||||
onMarkerClickRef.current?.(item.id)
|
||||
})
|
||||
markersRef.current.set(item.id, marker)
|
||||
})
|
||||
ev.stopPropagation();
|
||||
onMarkerClickRef.current?.(item.id);
|
||||
});
|
||||
markersRef.current.set(item.id, marker);
|
||||
});
|
||||
|
||||
// fit bounds to all points
|
||||
if (hasPoints) {
|
||||
const pb = paddingBottom || 50
|
||||
const pb = paddingBottom || 50;
|
||||
try {
|
||||
map.fitBounds(bounds, {
|
||||
padding: { top: 50, bottom: pb, left: 50, right: 50 },
|
||||
maxZoom: 16,
|
||||
pitch: mapbox3d && fullScreen ? 45 : 0,
|
||||
duration: 0,
|
||||
})
|
||||
} catch { /* empty bounds */ }
|
||||
});
|
||||
} catch {
|
||||
/* empty bounds */
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
return () => {
|
||||
markersRef.current.forEach(m => m.remove())
|
||||
markersRef.current.clear()
|
||||
markersRef.current.forEach((m) => m.remove());
|
||||
markersRef.current.clear();
|
||||
if (popupRef.current) {
|
||||
try { popupRef.current.remove() } catch { /* noop */ }
|
||||
popupRef.current = null
|
||||
try {
|
||||
popupRef.current.remove();
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
popupRef.current = null;
|
||||
}
|
||||
highlightedRef.current = null
|
||||
try { map.remove() } catch { /* noop */ }
|
||||
mapRef.current = null
|
||||
}
|
||||
}, [entries, stableTrail, mapboxStyle, mapboxToken, mapbox3d, mapboxQuality, fullScreen, paddingBottom])
|
||||
highlightedRef.current = null;
|
||||
try {
|
||||
map.remove();
|
||||
} catch {
|
||||
/* noop */
|
||||
}
|
||||
mapRef.current = null;
|
||||
};
|
||||
}, [entries, stableTrail, mapboxStyle, mapboxToken, mapbox3d, mapboxQuality, fullScreen, paddingBottom]);
|
||||
|
||||
// external activeMarkerId → highlight + flyTo
|
||||
useEffect(() => {
|
||||
if (!activeMarkerId || !mapRef.current) return
|
||||
if (!activeMarkerId || !mapRef.current) return;
|
||||
const t = setTimeout(() => {
|
||||
highlightMarker(activeMarkerId)
|
||||
const marker = markersRef.current.get(activeMarkerId)
|
||||
if (!marker || !mapRef.current) return
|
||||
highlightMarker(activeMarkerId);
|
||||
const marker = markersRef.current.get(activeMarkerId);
|
||||
if (!marker || !mapRef.current) return;
|
||||
try {
|
||||
mapRef.current.flyTo({
|
||||
center: marker.getLngLat(),
|
||||
zoom: Math.max(mapRef.current.getZoom(), 12),
|
||||
pitch: mapbox3d && fullScreen ? 45 : 0,
|
||||
duration: 500,
|
||||
})
|
||||
} catch { /* map not ready */ }
|
||||
}, 50)
|
||||
return () => clearTimeout(t)
|
||||
}, [activeMarkerId, highlightMarker, mapbox3d, fullScreen])
|
||||
});
|
||||
} catch {
|
||||
/* map not ready */
|
||||
}
|
||||
}, 50);
|
||||
return () => clearTimeout(t);
|
||||
}, [activeMarkerId, highlightMarker, mapbox3d, fullScreen]);
|
||||
|
||||
if (!mapboxToken) {
|
||||
return (
|
||||
<div
|
||||
style={{ position: 'relative', height: height === 9999 ? '100%' : height, width: '100%', borderRadius: 'inherit', overflow: 'hidden' }}
|
||||
className="flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 text-center px-6"
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: height === 9999 ? '100%' : height,
|
||||
width: '100%',
|
||||
borderRadius: 'inherit',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
className="flex items-center justify-center bg-zinc-100 px-6 text-center dark:bg-zinc-800"
|
||||
>
|
||||
<div className="text-sm text-zinc-500">
|
||||
No Mapbox access token configured.<br />
|
||||
No Mapbox access token configured.
|
||||
<br />
|
||||
<span className="text-xs">Settings → Map → Mapbox GL</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative', height: height === 9999 ? '100%' : height, width: '100%', borderRadius: 'inherit', overflow: 'hidden' }}>
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: height === 9999 ? '100%' : height,
|
||||
width: '100%',
|
||||
borderRadius: 'inherit',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
<div ref={containerRef} style={{ width: '100%', height: '100%' }} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
export default JourneyMapGL
|
||||
export default JourneyMapGL;
|
||||
|
||||
Reference in New Issue
Block a user