Files
omarchycn/shell/plugins/panels/weather/Model.js
T

285 lines
10 KiB
JavaScript

function parseWeatherStatus(raw) {
try {
var data = JSON.parse(String(raw || "{}"))
return {
label: data.text || "",
klass: data.class || ""
}
} catch (e) {
return { label: "", klass: "" }
}
}
// weather.json holds {"name": ..., "latitude": ..., "longitude": ...} (see
// omarchy-weather-location, which owns the format). Missing, blank, or
// unparseable means the location is auto-detected from the IP address.
function parseLocationFile(raw) {
var unset = { name: "", latitude: null, longitude: null }
try {
var data = JSON.parse(String(raw || ""))
if (!data || typeof data !== "object") return unset
var latitude = parseFloat(data.latitude)
var longitude = parseFloat(data.longitude)
var hasCoordinates = !isNaN(latitude) && !isNaN(longitude)
return {
name: typeof data.name === "string" ? data.name.replace(/^\s+|\s+$/g, "") : "",
latitude: hasCoordinates ? latitude : null,
longitude: hasCoordinates ? longitude : null
}
} catch (e) {
return unset
}
}
// wttr.in path segment for a configured location: exact coordinates when
// both are present, the URL-encoded name as a fallback (hand-edited
// weather.loc files may only carry a name), empty for IP auto-detect.
function wttrLocationQuery(location, latitude, longitude) {
var lat = parseFloat(String(latitude))
var lon = parseFloat(String(longitude))
if (!isNaN(lat) && !isNaN(lon)) return lat + "," + lon
var name = String(location || "").replace(/^\s+|\s+$/g, "")
return name === "" ? "" : encodeURIComponent(name)
}
// Open-Meteo geocoding response → suggestion rows for the location picker.
function parseGeocodingResults(raw) {
try {
var data = JSON.parse(String(raw || "{}"))
var results = data.results
if (!results || !results.length) return []
var out = []
for (var i = 0; i < results.length; i++) {
var r = results[i]
if (!r || !r.name || r.latitude === undefined || r.longitude === undefined) continue
var region = [r.admin1, r.country].filter(function(part) { return !!part }).join(", ")
out.push({
name: String(r.name),
description: region,
latitude: r.latitude,
longitude: r.longitude
})
}
return out
} catch (e) {
return []
}
}
function locationCommit(text, suggestions, selectedIndex) {
var name = String(text || "").replace(/^\s+|\s+$/g, "")
if (name === "") return { name: "", latitude: null, longitude: null }
var choices = suggestions || []
var index = Math.max(0, Math.min(parseInt(selectedIndex, 10) || 0, choices.length - 1))
var suggestion = choices[index]
if (suggestion) return suggestion
return { name: name, latitude: null, longitude: null }
}
function isFutureForecastDate(dateString, todayString) {
if (!dateString) return false
return String(dateString).slice(0, 10) > String(todayString || "")
}
function roundedTemp(value) {
if (value === undefined || value === null || value === "") return ""
var n = parseFloat(String(value))
return isNaN(n) ? "" : String(Math.round(n))
}
function celsiusToFahrenheit(value) {
if (value === undefined || value === null || value === "") return ""
var n = parseFloat(String(value))
return isNaN(n) ? "" : (n * 9 / 5) + 32
}
function formatTemp(value, useImperial) {
if (value === undefined || value === null || value === "") return ""
return value + "°" + (useImperial ? "F" : "C")
}
function normalizedUnit(value) {
return String(value || "").replace(/^\s+|\s+$/g, "").toLowerCase()
}
function localeUsesImperial(localeName) {
var name = String(localeName || "").replace(".", "_")
return /^en[_-]US($|[_.-])/.test(name) || /^en[_-]LR($|[_.-])/.test(name) || /^my($|[_.-])/.test(name)
}
function countryUsesImperial(countryName) {
var country = String(countryName || "")
.replace(/^\s+|\s+$/g, "")
.replace(/[._-]+/g, " ")
.toLowerCase()
if (!country) return null
if (country === "us" || country === "usa" || country === "united states" || country === "united states of america") return true
if (country === "liberia" || country === "myanmar" || country === "burma") return true
return false
}
function shouldUseImperial(unitOverride, localeName, countryName) {
var unit = normalizedUnit(unitOverride)
if (unit === "imperial") return true
if (unit === "metric") return false
var countryPreference = countryUsesImperial(countryName)
if (countryPreference !== null) return countryPreference
return localeUsesImperial(localeName)
}
function dayName(dateString, formatter) {
if (!dateString) return ""
var d = new Date(dateString + "T12:00:00")
if (isNaN(d.getTime())) return ""
if (formatter) return formatter(d)
return ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"][d.getDay()]
}
function openMeteoForecastDays(dailyForecastReport, todayString) {
var daily = dailyForecastReport && dailyForecastReport.daily ? dailyForecastReport.daily : null
if (!daily || !daily.time) return []
var result = []
for (var i = 0; i < daily.time.length && result.length < 3; ++i) {
var date = daily.time[i]
if (!isFutureForecastDate(date, todayString)) continue
var maxC = daily.temperature_2m_max ? daily.temperature_2m_max[i] : ""
var minC = daily.temperature_2m_min ? daily.temperature_2m_min[i] : ""
result.push({
date: date,
maxtempC: roundedTemp(maxC),
mintempC: roundedTemp(minC),
maxtempF: roundedTemp(celsiusToFahrenheit(maxC)),
mintempF: roundedTemp(celsiusToFahrenheit(minC)),
openMeteoWeatherCode: daily.weather_code ? daily.weather_code[i] : null
})
}
return result
}
// Open-Meteo bundles current conditions with the daily forecast request and
// answers far faster than wttr.in. Normalize them to wttr's
// current_condition shape so the panel can use either source
// interchangeably. Open-Meteo reports metric (°C, km/h).
function openMeteoCurrentCondition(dailyForecastReport) {
var current = dailyForecastReport && dailyForecastReport.current ? dailyForecastReport.current : null
if (!current || current.temperature_2m === undefined || current.temperature_2m === null) return null
return {
temp_C: roundedTemp(current.temperature_2m),
temp_F: roundedTemp(celsiusToFahrenheit(current.temperature_2m)),
FeelsLikeC: roundedTemp(current.apparent_temperature),
FeelsLikeF: roundedTemp(celsiusToFahrenheit(current.apparent_temperature)),
windspeedKmph: roundedTemp(current.wind_speed_10m),
windspeedMiles: roundedTemp(current.wind_speed_10m * 0.621371),
humidity: roundedTemp(current.relative_humidity_2m)
}
}
function wttrNextForecastDays(report, todayString) {
var days = report && report.weather ? report.weather : []
var result = []
for (var i = 0; i < days.length && result.length < 3; ++i) {
if (isFutureForecastDate(days[i].date, todayString)) result.push(days[i])
}
return result
}
function buildForecastDays(report, dailyForecastReport, todayString) {
var days = openMeteoForecastDays(dailyForecastReport, todayString)
return days.length > 0 ? days : wttrNextForecastDays(report, todayString)
}
function bareTempForDay(day, kind, useImperial) {
if (!day) return ""
var v = useImperial
? (kind === "max" ? day.maxtempF : day.mintempF)
: (kind === "max" ? day.maxtempC : day.mintempC)
if (v === undefined || v === null || v === "") return ""
return v + "°"
}
function dayIcon(day) {
if (!day) return ""
if (day.openMeteoWeatherCode !== undefined && day.openMeteoWeatherCode !== null)
return iconForOpenMeteoCode(day.openMeteoWeatherCode)
if (!day.hourly || day.hourly.length === 0) return ""
var best = day.hourly[0]
var bestDist = 9999
for (var i = 0; i < day.hourly.length; ++i) {
var t = parseInt(String(day.hourly[i].time || "0"), 10)
var dist = Math.abs(t - 1200)
if (dist < bestDist) {
bestDist = dist
best = day.hourly[i]
}
}
return iconForCode(best.weatherCode, false)
}
function iconForOpenMeteoCode(code) {
var c = parseInt(String(code || "0"), 10)
if (c === 0) return iconForCode(113, false)
if (c === 1 || c === 2) return iconForCode(116, false)
if (c === 3) return iconForCode(119, false)
if (c === 45 || c === 48) return iconForCode(143, false)
if (c === 51 || c === 53 || c === 55 || c === 56 || c === 57 || c === 61) return iconForCode(266, false)
if (c === 63 || c === 65 || c === 66 || c === 67 || c === 80 || c === 81 || c === 82) return iconForCode(308, false)
if (c === 71 || c === 73 || c === 75 || c === 77 || c === 85 || c === 86) return iconForCode(338, false)
if (c === 95 || c === 96 || c === 99) return iconForCode(389, false)
return iconForCode(119, false)
}
function iconForCode(code, night) {
var c = parseInt(String(code || "0"), 10)
switch (c) {
case 113: return night ? "" : ""
case 116: return night ? "" : ""
case 119: case 122: return ""
case 143: case 248: case 260: return ""
case 176: case 263: case 353: return night ? "" : ""
case 179: case 227: case 230: case 323: case 326: case 368: return night ? "" : ""
case 182: case 185: case 281: case 284: case 311: case 314:
case 317: case 320: case 350: case 362: case 365: case 374: case 377: return ""
case 200: case 386: case 389: case 392: case 395: return ""
case 266: case 293: case 296: case 299: case 302: case 305: case 308: case 356: case 359: return ""
case 329: case 332: case 335: case 338: case 371: return ""
default: return ""
}
}
if (typeof module !== "undefined") {
module.exports = {
parseWeatherStatus: parseWeatherStatus,
parseLocationFile: parseLocationFile,
wttrLocationQuery: wttrLocationQuery,
parseGeocodingResults: parseGeocodingResults,
locationCommit: locationCommit,
isFutureForecastDate: isFutureForecastDate,
roundedTemp: roundedTemp,
celsiusToFahrenheit: celsiusToFahrenheit,
formatTemp: formatTemp,
normalizedUnit: normalizedUnit,
localeUsesImperial: localeUsesImperial,
countryUsesImperial: countryUsesImperial,
shouldUseImperial: shouldUseImperial,
dayName: dayName,
openMeteoForecastDays: openMeteoForecastDays,
openMeteoCurrentCondition: openMeteoCurrentCondition,
wttrNextForecastDays: wttrNextForecastDays,
buildForecastDays: buildForecastDays,
bareTempForDay: bareTempForDay,
dayIcon: dayIcon,
iconForOpenMeteoCode: iconForOpenMeteoCode,
iconForCode: iconForCode
}
}