(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); }); }); })();