(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 += "" + temp + "
"; html += "" + shortF + "
"; html += "" + detail + "
"; html += "