Files

304 lines
10 KiB
JavaScript

(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, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
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 = "<p>No forecast available.</p>";
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)) + "&deg;" + escapeHtml(p.temperatureUnit || ""))
: "—";
var shortF = escapeHtml(p.shortForecast || "");
var detail = escapeHtml(p.detailedForecast || "");
html += "<div class='forecastItem'>";
html += "<h3>" + name + "</h3>";
if (icon) html += "<img src='" + icon + "' alt='Forecast Icon'/>";
html += "<p><b>" + temp + "</b></p>";
html += "<p>" + shortF + "</p>";
html += "<p style='max-width: 700px; margin-left:auto; margin-right:auto;'>" + detail + "</p>";
html += "</div><hr>";
}
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) + "&deg;F (" + fmtNum(c) + "&deg;C)")
: "—";
$("humidity").innerHTML = (humidity !== null) ? (fmtNum(humidity) + "%") : "—";
$("wind").innerHTML =
(windMph !== null && windDir !== null)
? (fmtNum(windMph) + " mph @ " + fmtNum(windDir) + "&deg;")
: "—";
$("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);
});
});
})();