Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/environment

src/weather-geocode.ts

79 lines · 2.2 KB · TypeScript
export type GeocodeParams = {
	location?: string
	latitude?: number
	longitude?: number
	label?: string
}

export type GeocodeResult = {
	latitude: number
	longitude: number
	displayName: string
	source: 'coordinates' | 'geocoded'
	query?: string
}

function clean(value: unknown): string {
	return String(value || '').trim()
}

function normalizeCoordinate(value: unknown, label: string): number {
	const numeric = Number(value)
	if (!Number.isFinite(numeric)) {
		throw new Error('Invalid ' + label + ' coordinate.')
	}
	return numeric
}

/** Resolve a free-form place name or direct coordinates into reusable coordinates. */
export async function resolveWeatherLocation(
	params: GeocodeParams = {},
): Promise<GeocodeResult> {
	const input = typeof params === 'object' && params ? params : {}
	const latitudeProvided = input.latitude !== undefined && input.latitude !== null
	const longitudeProvided =
		input.longitude !== undefined && input.longitude !== null
	if (latitudeProvided || longitudeProvided) {
		if (!latitudeProvided || !longitudeProvided) {
			throw new Error(
				'Provide both latitude and longitude when using direct coordinates.',
			)
		}
		const latitude = normalizeCoordinate(input.latitude, 'latitude')
		const longitude = normalizeCoordinate(input.longitude, 'longitude')
		return {
			latitude,
			longitude,
			displayName: clean(input.label) || latitude + ', ' + longitude,
			source: 'coordinates',
		}
	}
	const location = clean(input.location)
	if (!location) {
		throw new Error('Provide either a location string or direct coordinates.')
	}
	const response = await fetch(
		'https://nominatim.openstreetmap.org/search?q=' +
			encodeURIComponent(location) +
			'&format=json&limit=1',
		{
			headers: {
				'User-Agent': 'Kody/1.0 (kody.codes)',
			},
		},
	)
	const data = await response.json().catch(() => [])
	if (!response.ok || !Array.isArray(data) || data.length === 0) {
		throw new Error('Could not geocode location: "' + location + '"')
	}
	const first = data[0]
	return {
		latitude: Number(first.lat),
		longitude: Number(first.lon),
		displayName: clean(first.display_name) || location,
		source: 'geocoded',
		query: location,
	}
}

export default resolveWeatherLocation