Files
kapteins-daagbok/server/src/utils/tideProvider.ts
T

135 lines
3.7 KiB
TypeScript

import {
fetchBshTidesForCoordinates,
fetchBshTidesForStation,
listNearbyBshStations,
loadBshStationIndex,
MAX_BSH_DISTANCE_KM,
type BshStationSuggestion
} from './bshTides.js'
import {
fetchTidesForCoordinates as fetchOpenMeteoTidesForCoordinates,
fetchTidesForPlace as fetchOpenMeteoTidesForPlace,
geocodePlace,
type TideLookupResult
} from './openMeteoTides.js'
export type TideProviderResult = TideLookupResult & {
distanceKm?: number
fallback?: 'open_meteo'
}
export async function fetchTidesForCoordinates(
lat: number,
lon: number,
options?: { name?: string; source?: 'coordinates' | 'geocoded' }
): Promise<TideProviderResult> {
try {
const bsh = await fetchBshTidesForCoordinates(lat, lon)
return bsh
} catch (error: unknown) {
const message = error instanceof Error ? error.message : ''
const tooFar = message === 'bsh_station_too_far'
const noStation = message === 'no_bsh_station' || message === 'bsh_empty_station_list'
const noData = message === 'no_tide_data'
if (!tooFar && !noStation && !noData) {
console.warn('BSH tide lookup failed, trying Open-Meteo fallback:', error)
}
const fallback = await fetchOpenMeteoTidesForCoordinates(lat, lon, options)
return {
...fallback,
fallback: 'open_meteo',
tides: {
data: {
...fallback.tides.data,
source: `${fallback.tides.data.source} (Fallback — keine BSH-Station innerhalb ${MAX_BSH_DISTANCE_KM} km)`
}
}
}
}
}
export async function listNearbyTideStations(
lat: number,
lon: number,
limit = 8
): Promise<BshStationSuggestion[]> {
try {
return await listNearbyBshStations(lat, lon, limit)
} catch {
return []
}
}
export async function fetchTidesForStation(
stationId: string,
options?: { queryLat?: number; queryLon?: number }
): Promise<TideProviderResult> {
try {
return await fetchBshTidesForStation(stationId, options)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : ''
if (message === 'bsh_invalid_station' || message === 'no_tide_data') {
throw error
}
console.warn('BSH station tide lookup failed:', error)
throw new Error('no_tide_data')
}
}
function normalizeForMatching(s: string): string {
return s
.toLowerCase()
.trim()
.replace(/ae/g, 'ä')
.replace(/oe/g, 'ö')
.replace(/ue/g, 'ü')
.replace(/ss/g, 'ß');
}
export async function fetchTidesForPlace(query: string): Promise<TideProviderResult> {
const normQuery = normalizeForMatching(query)
if (normQuery) {
try {
const stations = await loadBshStationIndex()
let match = stations.find(s => normalizeForMatching(s.name) === normQuery)
if (!match) {
match = stations.find(s => normalizeForMatching(s.name).startsWith(normQuery))
}
if (match) {
return await fetchTidesForStation(match.id)
}
} catch (err) {
console.warn('[tideProvider] Direct BSH station lookup failed:', err)
}
}
const place = await geocodePlace(query)
if (!place) {
if (normQuery) {
try {
const stations = await loadBshStationIndex()
const match = stations.find(s =>
normalizeForMatching(s.name).includes(normQuery) ||
normQuery.includes(normalizeForMatching(s.name))
)
if (match) {
return await fetchTidesForStation(match.id)
}
} catch (err) {
console.warn('[tideProvider] Fallback BSH station lookup failed:', err)
}
}
const err = new Error('place_not_found') as Error & { status?: number }
err.status = 404
throw err
}
return fetchTidesForCoordinates(place.latitude, place.longitude, {
name: place.name,
source: 'geocoded'
})
}