UNPKG

@fmidev/smartmet-alert-client

Version:

Web application for viewing weather and flood alerts

375 lines (325 loc) 11.4 kB
/** * Core composable for AlertClient wrapper components. * Contains shared logic for both web component (App.vue) and Vue component (AlertClientVue.vue). * * This composable provides: * - Reactive state management (loading, warningsData, themeClass, etc.) * - Computed properties for API queries * - Methods for fetching warnings and handling events * * Components using this composable must provide: * - Props with appropriate types */ import { ref, computed, type Ref, type ComputedRef } from 'vue' import crossFetch from 'cross-fetch' import type { Language, WarningsData } from '@/types' // ============================================================================ // Helper Functions (exported for use in components) // ============================================================================ /** * Normalize string|boolean to boolean */ export const toBool = (val: unknown, defaultVal = true): boolean => { if (typeof val === 'boolean') return val if (typeof val === 'string') return val.toLowerCase() !== 'false' return defaultVal } /** * Normalize string|number to number */ export const toNum = (val: unknown, defaultVal = 0): number => { if (typeof val === 'number') return val if (typeof val === 'string') return Number(val) return defaultVal } // ============================================================================ // Types // ============================================================================ export interface UseAlertClientOptions { /** Base URL for API requests */ baseUrl: Ref<string> | ComputedRef<string> /** Language code */ language: Ref<Language> | ComputedRef<Language> /** Theme name */ theme: Ref<string> | ComputedRef<string> /** Pre-loaded warnings data (optional) */ warnings?: | Ref<WarningsData | string | null> | ComputedRef<WarningsData | string | null> /** Current date for time calculations (optional) */ currentDate?: Ref<Date | string | null> | ComputedRef<Date | string | null> /** Font scale factor (optional) */ fontScale?: Ref<number | string> | ComputedRef<number | string> /** Debug mode (optional) */ debugMode?: Ref<boolean> | ComputedRef<boolean> /** Custom weather updated query (optional) */ weatherUpdated?: Ref<string> | ComputedRef<string> /** Custom flood updated query (optional) */ floodUpdated?: Ref<string> | ComputedRef<string> /** Custom weather warnings query (optional) */ weatherWarnings?: Ref<string> | ComputedRef<string> /** Custom flood warnings query (optional) */ floodWarnings?: Ref<string> | ComputedRef<string> } export interface UseAlertClientReturn { // State loading: Ref<number> updatedAt: Ref<number | null> refreshedAt: Ref<number | null> themeClass: Ref<string> warningsData: Ref<WarningsData | null> visible: Ref<boolean> // Computed currentTime: ComputedRef<number> weatherUpdatedQuery: ComputedRef<string> floodUpdatedQuery: ComputedRef<string> weatherWarningsQuery: ComputedRef<string> floodWarningsQuery: ComputedRef<string> // Methods onLoaded: (loaded: number) => void onThemeChanged: (newTheme: string | null) => void fetchWarnings: () => Promise<void> | undefined show: () => void hide: () => void initializeWarnings: () => void applyFontScale: () => void } // ============================================================================ // Constants // ============================================================================ const WEATHER_UPDATED_TYPE = 'weather_update_time' const FLOOD_UPDATED_TYPE = 'flood_update_time' const WEATHER_WARNINGS_TYPE = 'weather_finland_active_all' const FLOOD_WARNINGS_TYPE = 'flood_finland_active_all' const FLOOD_SUPPORTED_SEVERITIES = ['moderate', 'severe', 'extreme'] as const const QUERY_PREFIX = '?service=WFS&version=1.0.0&request=GetFeature&maxFeatures=1000&outputFormat=application%2Fjson&typeName=' // ============================================================================ // Composable // ============================================================================ export function useAlertClient( options: UseAlertClientOptions ): UseAlertClientReturn { const { baseUrl, language, theme, warnings, currentDate, fontScale, debugMode, weatherUpdated, floodUpdated, weatherWarnings, floodWarnings, } = options // ------------------------------------------------------------------------- // Reactive State // ------------------------------------------------------------------------- const loading = ref<number>(1) const updatedAt = ref<number | null>(null) const refreshedAt = ref<number | null>(null) const themeClass = ref<string>(`${theme.value}-theme`) const warningsData = ref<WarningsData | null>(null) const visible = ref<boolean>(true) const fetchInProgress = ref<boolean>(false) // ------------------------------------------------------------------------- // Helper: CAP Language mapping // ------------------------------------------------------------------------- const capLanguageMap: Record<Language, string> = { fi: 'fi-FI', sv: 'sv-SV', en: 'en-US', } const getCapLanguage = (): string => capLanguageMap[language.value] || 'fi-FI' // ------------------------------------------------------------------------- // Computed: Flood filter // ------------------------------------------------------------------------- const floodFilter = computed<string>(() => { const severityFilter = FLOOD_SUPPORTED_SEVERITIES.reduce( (filter, severity, index) => `${filter}${index === 0 ? '' : ','}%27${severity.toUpperCase()}%27`, '&cql_filter=severity%20IN%20(' ) return `${severityFilter})%20AND%20language=%27${getCapLanguage()}%27` }) // ------------------------------------------------------------------------- // Computed: Query URLs // ------------------------------------------------------------------------- const weatherUpdatedQuery = computed<string>(() => { return weatherUpdated?.value || `${QUERY_PREFIX}${WEATHER_UPDATED_TYPE}` }) const floodUpdatedQuery = computed<string>(() => { return floodUpdated?.value || `${QUERY_PREFIX}${FLOOD_UPDATED_TYPE}` }) const weatherWarningsQuery = computed<string>(() => { return weatherWarnings?.value || `${QUERY_PREFIX}${WEATHER_WARNINGS_TYPE}` }) const floodWarningsQuery = computed<string>(() => { return ( floodWarnings?.value || `${QUERY_PREFIX}${FLOOD_WARNINGS_TYPE}${floodFilter.value}` ) }) // ------------------------------------------------------------------------- // Computed: Current time // ------------------------------------------------------------------------- const currentTime = computed<number>(() => { if (refreshedAt.value) { return refreshedAt.value } if (currentDate?.value) { const date = currentDate.value instanceof Date ? currentDate.value : new Date(currentDate.value) return date.getTime() } return Date.now() }) // ------------------------------------------------------------------------- // Methods // ------------------------------------------------------------------------- /** * Initialize warnings from pre-loaded data (call in created/setup) */ const initializeWarnings = (): void => { if (warnings?.value) { warningsData.value = typeof warnings.value === 'string' ? JSON.parse(warnings.value) : warnings.value } } /** * Apply font scale to document (call in onMounted) */ const applyFontScale = (): void => { const fontScaleNum = toNum(fontScale?.value, 1) if (fontScaleNum !== 1) { let originalFontSize: number | undefined if ( typeof window !== 'undefined' && typeof document !== 'undefined' && document.documentElement && window.getComputedStyle ) { const htmlElement = document.documentElement const computedStyle = window.getComputedStyle(htmlElement) originalFontSize = parseFloat(computedStyle.fontSize) } if (originalFontSize == null || Number.isNaN(originalFontSize)) { originalFontSize = 16 // Fallback } const scaledFontSize = fontScaleNum * originalFontSize const newFontSize = Math.round(scaledFontSize * 100) / 100 document.documentElement.style.fontSize = `${newFontSize}px` } } /** * Handle loaded event from child component */ const onLoaded = (loaded: number): void => { if (loaded !== 0) { loading.value = loaded === -1 ? -1 : 0 } } /** * Handle theme change */ const onThemeChanged = (newTheme: string | null): void => { themeClass.value = `${ newTheme != null && newTheme.length > 0 ? newTheme : theme.value }-theme` } /** * Fetch warnings from API */ const fetchWarnings = (): Promise<void> | undefined => { if (warnings?.value) { return } if (fetchInProgress.value) { return } fetchInProgress.value = true loading.value = 1 if (debugMode?.value) { console.log(`Updating warnings at ${new Date()}`) } const cacheBuster = `_t=${Date.now()}` const appendCacheBuster = (url: string) => url.includes('?') ? `${url}&${cacheBuster}` : `${url}?${cacheBuster}` const queries = new Map<string, string>([ [`${baseUrl.value}${weatherUpdatedQuery.value}`, WEATHER_UPDATED_TYPE], [`${baseUrl.value}${floodUpdatedQuery.value}`, FLOOD_UPDATED_TYPE], [`${baseUrl.value}${weatherWarningsQuery.value}`, WEATHER_WARNINGS_TYPE], [`${baseUrl.value}${floodWarningsQuery.value}`, FLOOD_WARNINGS_TYPE], ]) const responseData: Record<string, unknown> = {} return Promise.allSettled( [...queries.keys()].map(async (queryUrl) => crossFetch(appendCacheBuster(queryUrl)).then((response) => response .json() .then((json: unknown) => { const currentTimeMs = Date.now() if (updatedAt.value != null) { refreshedAt.value = currentTimeMs } updatedAt.value = currentTimeMs responseData[queries.get(queryUrl)!] = json }) .catch((error: Error) => { loading.value = -1 console.log(error) }) ) ) ) .then(() => { warningsData.value = responseData as WarningsData }) .finally(() => { fetchInProgress.value = false }) } /** * Show the component */ const show = (): void => { visible.value = true } /** * Hide the component */ const hide = (): void => { visible.value = false } // ------------------------------------------------------------------------- // Return // ------------------------------------------------------------------------- return { // State loading, updatedAt, refreshedAt, themeClass, warningsData, visible, // Computed currentTime, weatherUpdatedQuery, floodUpdatedQuery, weatherWarningsQuery, floodWarningsQuery, // Methods onLoaded, onThemeChanged, fetchWarnings, show, hide, initializeWarnings, applyFontScale, } }