@fmidev/smartmet-alert-client
Version:
Web application for viewing weather and flood alerts
375 lines (325 loc) • 11.4 kB
text/typescript
/**
* 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,
}
}