From 76aac92fc5a8dd86fd29d1e603b7f581d52e733b Mon Sep 17 00:00:00 2001 From: Carlton Smith Date: Mon, 19 Jan 2026 15:06:16 +0000 Subject: [PATCH] Upload files to "website/weather" --- website/weather/app.js | 304 +++++++++++++++++++++++++++++++++++++ website/weather/index.html | 55 +++++++ website/weather/style.css | 42 +++++ 3 files changed, 401 insertions(+) create mode 100644 website/weather/app.js create mode 100644 website/weather/index.html create mode 100644 website/weather/style.css diff --git a/website/weather/app.js b/website/weather/app.js new file mode 100644 index 0000000..45ea01b --- /dev/null +++ b/website/weather/app.js @@ -0,0 +1,304 @@ +(function () { + function $(id) { return document.getElementById(id); } + + function showResults(show) { + var r = $("results"); + if (r) r.style.display = show ? "" : "none"; + } + + function escapeHtml(str) { + return String(str) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function getParam(name) { + var qs = window.location.search || ""; + qs = qs.replace(/^\?/, ""); + if (!qs) return ""; + var parts = qs.split("&"); + for (var i = 0; i < parts.length; i++) { + var kv = parts[i].split("="); + var k = decodeURIComponent(kv[0] || ""); + if (k === name) return decodeURIComponent((kv[1] || "").replace(/\+/g, " ")); + } + return ""; + } + + function xhrJson(url, accept, cb) { + try { + var x = new XMLHttpRequest(); + x.open("GET", url, true); + if (accept) x.setRequestHeader("Accept", accept); + x.onreadystatechange = function () { + if (x.readyState !== 4) return; + if (x.status >= 200 && x.status < 300) { + var data; + try { data = JSON.parse(x.responseText); } + catch (e) { return cb("Bad JSON from: " + url); } + return cb(null, data); + } + cb("HTTP " + x.status + " from: " + url); + }; + x.send(null); + } catch (e2) { + cb("Request failed: " + e2.message); + } + } + + function isLatLon(s) { + return /^\s*-?\d+(\.\d+)?\s*,\s*-?\d+(\.\d+)?\s*$/.test(s); + } + + function parseLatLon(s) { + var parts = s.split(","); + return { lat: parseFloat(parts[0]), lon: parseFloat(parts[1]) }; + } + + function isZip(s) { + return /^\s*\d{5}\s*$/.test(s); + } + + // Resolve user input to lat/lon + // - lat,lon => direct + // - ZIP => zippopotam.us (for coordinates) + // - City, ST => OpenStreetMap Nominatim (for coordinates) + function resolveLocation(q, cb) { + q = (q || "").replace(/^\s+|\s+$/g, ""); + if (!q) return cb("Empty query"); + + if (isLatLon(q)) { + var ll = parseLatLon(q); + return cb(null, { input: q, display: q, lat: ll.lat, lon: ll.lon }); + } + + if (isZip(q)) { + var zip = q.replace(/\s+/g, ""); + var url = "https://api.zippopotam.us/us/" + encodeURIComponent(zip); + return xhrJson(url, "application/json", function (err, data) { + if (err) return cb("Could not resolve ZIP to coordinates. " + err); + try { + var place = data.places && data.places[0]; + var lat = parseFloat(place.latitude); + var lon = parseFloat(place.longitude); + var city = place["place name"]; + var st = place["state abbreviation"] || place.state; + return cb(null, { + input: q, + display: (city && st) ? (city + ", " + st + " " + zip) : zip, + lat: lat, + lon: lon + }); + } catch (e) { + cb("ZIP lookup returned unexpected data."); + } + }); + } + + var nom = "https://nominatim.openstreetmap.org/search?format=json&limit=1&countrycodes=us&q=" + encodeURIComponent(q); + xhrJson(nom, "application/json", function (err, arr) { + if (err) return cb("Could not resolve city/state to coordinates. " + err); + if (!arr || !arr.length) return cb("No results for: " + q); + + var r = arr[0]; + return cb(null, { + input: q, + display: r.display_name || q, + lat: parseFloat(r.lat), + lon: parseFloat(r.lon) + }); + }); + } + + function cToF(c) { + if (c === null || c === undefined) return null; + return (c * 9 / 5) + 32; + } + + function fmtNum(n) { + if (n === null || n === undefined || isNaN(n)) return "—"; + return String(Math.round(n)); + } + + function fmtLatLon(lat, lon) { + function f(x) { return (Math.round(x * 10000) / 10000).toFixed(4); } + return f(lat) + ", " + f(lon); + } + + function renderForecast(periods) { + var wrap = $("forecast"); + if (!wrap) return; + + if (!periods || !periods.length) { + wrap.innerHTML = "

No forecast available.

"; + return; + } + + var max = Math.min(periods.length, 8); + var html = ""; + + for (var i = 0; i < max; i++) { + var p = periods[i]; + + var name = escapeHtml(p.name || ""); + var icon = escapeHtml(p.icon || ""); + var temp = (p.temperature !== null && p.temperature !== undefined) + ? (escapeHtml(String(p.temperature)) + "°" + escapeHtml(p.temperatureUnit || "")) + : "—"; + var shortF = escapeHtml(p.shortForecast || ""); + var detail = escapeHtml(p.detailedForecast || ""); + + html += "
"; + html += "

" + name + "

"; + if (icon) html += "Forecast Icon"; + html += "

" + temp + "

"; + html += "

" + shortF + "

"; + html += "

" + detail + "

"; + html += "

"; + } + + wrap.innerHTML = html; + } + + function loadWeather(lat, lon, resolvedDisplay) { + showResults(false); + + var pointsUrl = "https://api.weather.gov/points/" + lat + "," + lon; + + xhrJson(pointsUrl, "application/geo+json", function (err, points) { + if (err) { + // No status box; just hide results + showResults(false); + return; + } + + var props = (points && points.properties) ? points.properties : {}; + var rel = props.relativeLocation && props.relativeLocation.properties ? props.relativeLocation.properties : null; + + var city = rel && rel.city ? rel.city : ""; + var state = rel && rel.state ? rel.state : ""; + + var placeText = (city && state) ? (city + ", " + state) : (resolvedDisplay || (lat + "," + lon)); + $("placeTitle").innerHTML = escapeHtml(placeText); + $("latlon").innerHTML = escapeHtml(fmtLatLon(lat, lon)); + + var forecastUrl = props.forecast; + var stationsUrl = props.observationStations || null; + + if (!forecastUrl) { + showResults(false); + return; + } + + // 1) Forecast + xhrJson(forecastUrl, "application/geo+json", function (errF, forecast) { + if (errF) { + showResults(false); + return; + } + + var periods = forecast && forecast.properties && forecast.properties.periods ? forecast.properties.periods : []; + renderForecast(periods); + + // 2) Current conditions (optional) + if (!stationsUrl) { + $("tempNow").innerHTML = "—"; + $("humidity").innerHTML = "—"; + $("wind").innerHTML = "—"; + $("observedAt").innerHTML = "—"; + showResults(true); + return; + } + + xhrJson(stationsUrl, "application/geo+json", function (errS, stations) { + if (errS) { + $("tempNow").innerHTML = "—"; + $("humidity").innerHTML = "—"; + $("wind").innerHTML = "—"; + $("observedAt").innerHTML = "—"; + showResults(true); + return; + } + + var feat = stations && stations.features && stations.features.length ? stations.features[0] : null; + var stationId = feat && feat.id ? feat.id : null; + + if (!stationId) { + $("tempNow").innerHTML = "—"; + $("humidity").innerHTML = "—"; + $("wind").innerHTML = "—"; + $("observedAt").innerHTML = "—"; + showResults(true); + return; + } + + var obsUrl = stationId.replace(/\/$/, "") + "/observations/latest"; + xhrJson(obsUrl, "application/geo+json", function (errO, obs) { + if (errO) { + $("tempNow").innerHTML = "—"; + $("humidity").innerHTML = "—"; + $("wind").innerHTML = "—"; + $("observedAt").innerHTML = "—"; + showResults(true); + return; + } + + var op = obs && obs.properties ? obs.properties : {}; + + // temperature is Celsius + var c = op.temperature && op.temperature.value !== null ? op.temperature.value : null; + var f = c !== null ? cToF(c) : null; + + var humidity = op.relativeHumidity && op.relativeHumidity.value !== null ? op.relativeHumidity.value : null; + + // windSpeed meters/sec => mph + var windMs = op.windSpeed && op.windSpeed.value !== null ? op.windSpeed.value : null; + var windMph = (windMs !== null) ? (windMs * 2.236936) : null; + + var windDir = op.windDirection && op.windDirection.value !== null ? op.windDirection.value : null; + var ts = op.timestamp || "—"; + + $("tempNow").innerHTML = + (f !== null && c !== null) + ? (fmtNum(f) + "°F (" + fmtNum(c) + "°C)") + : "—"; + + $("humidity").innerHTML = (humidity !== null) ? (fmtNum(humidity) + "%") : "—"; + $("wind").innerHTML = + (windMph !== null && windDir !== null) + ? (fmtNum(windMph) + " mph @ " + fmtNum(windDir) + "°") + : "—"; + + $("observedAt").innerHTML = escapeHtml(ts); + + showResults(true); + }); + }); + }); + }); + } + + window.addEventListener("DOMContentLoaded", function () { + var q = getParam("q"); + + if (!q) { + $("q").value = ""; + showResults(false); + return; + } + + $("q").value = q; + + resolveLocation(q, function (err, loc) { + if (err) { + showResults(false); + return; + } + loadWeather(loc.lat, loc.lon, loc.display); + }); + }); + })(); + \ No newline at end of file diff --git a/website/weather/index.html b/website/weather/index.html new file mode 100644 index 0000000..5b15e50 --- /dev/null +++ b/website/weather/index.html @@ -0,0 +1,55 @@ + + + + + + SumiWeather + + + + + + +
+ SumiWeather | Enter ZIP or City: + + +
+ Tips: ZIP (98112) or City, ST (Seattle, WA) works best. +
+
+ +
+ +
+ + + +
+ + + + diff --git a/website/weather/style.css b/website/weather/style.css new file mode 100644 index 0000000..1280ece --- /dev/null +++ b/website/weather/style.css @@ -0,0 +1,42 @@ +/* + Lunar-style simple portal CSS +*/ + +html, body { + font-family: 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; + color: #222; + background-color: #ddd; + } + + a, a:visited { + text-decoration: none; + color: #a22121; + } + + .center { + margin-left: auto; + margin-right: auto; + width: 100%; + max-width: 700px; + text-align: center; + } + + .forecastItem { + margin: 14px 0; + } + + .forecastItem img { + width: 64px; + height: 64px; + } + + hr { + border: none; + border-top: 1px solid #000; + } + + .footer { + font-size: 12px; + margin-top: 14px; + } + \ No newline at end of file