Add persistent location controls to astronomy page (ZIP, geolocation, manual)

This commit is contained in:
2026-03-09 02:41:35 +00:00
parent 196cf7557f
commit b0012d68d3
2 changed files with 139 additions and 8 deletions
+54
View File
@@ -117,6 +117,44 @@
font-size: 0.77rem; font-size: 0.77rem;
} }
.event-dual span:first-child { color: var(--text); } .event-dual span:first-child { color: var(--text); }
.loc-wrap {
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.7rem;
margin: 0.2rem 0 1rem;
background: #171717;
}
.loc-head {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--accent);
margin-bottom: 0.45rem;
font-weight: 700;
}
.loc-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem;
margin-bottom: 0.4rem;
}
.loc-input {
background: #111;
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
padding: 0.35rem 0.5rem;
font-size: 0.82rem;
min-width: 0;
}
.loc-status {
font-size: 0.76rem;
color: var(--muted);
min-height: 1.1em;
}
@media (max-width: 640px) {
.loc-row { grid-template-columns: 1fr 1fr; }
}
</style> </style>
</head> </head>
<body> <body>
@@ -132,6 +170,22 @@
<button class="nav-btn" id="nextDay">&#8250;</button> <button class="nav-btn" id="nextDay">&#8250;</button>
</div> </div>
<div class="loc-wrap">
<div class="loc-head">Astronomy Location</div>
<div class="loc-row">
<input id="locLabel" class="loc-input" type="text" placeholder="Label (Home)" maxlength="64">
<input id="locZip" class="loc-input" type="text" placeholder="ZIP" inputmode="numeric" maxlength="10">
<button class="nav-btn" id="locUseZip" type="button">Use ZIP</button>
</div>
<div class="loc-row">
<input id="locLat" class="loc-input" type="number" step="0.0001" min="-90" max="90" placeholder="Latitude">
<input id="locLon" class="loc-input" type="number" step="0.0001" min="-180" max="180" placeholder="Longitude">
<button class="nav-btn" id="locGeo" type="button">Use Current</button>
<button class="nav-btn" id="locSave" type="button">Save</button>
</div>
<div id="locStatus" class="loc-status">Using default location.</div>
</div>
<div class="moon-display"> <div class="moon-display">
<span class="moon-glyph" id="moonGlyph">🌑</span> <span class="moon-glyph" id="moonGlyph">🌑</span>
<div class="moon-name" id="moonName"></div> <div class="moon-name" id="moonName"></div>
+85 -8
View File
@@ -261,15 +261,25 @@ function formatDate(date) {
return date.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC" }); return date.toLocaleDateString("en-US", { weekday: "long", year: "numeric", month: "long", day: "numeric", timeZone: "UTC" });
} }
// Default lat/lon: roughly central US (no geolocation, just reasonable default) function loadLocation() {
// Will use user's timezone offset to pick a reasonable longitude try {
function guessLon() { const raw = localStorage.getItem("astro_location");
const offsetHours = -new Date().getTimezoneOffset() / 60; if (!raw) return { label: "Home", lat: 40.0, lon: -75.0 };
return offsetHours * 15; // rough: UTC offset * 15° per hour const d = JSON.parse(raw);
const lat = Number(d.lat);
const lon = Number(d.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) throw new Error("bad loc");
return { label: d.label || "Home", lat, lon };
} catch {
return { label: "Home", lat: 40.0, lon: -75.0 };
}
} }
const LAT = 40.0; // roughly mid-US latitude function saveLocation(loc) {
const LON = guessLon(); localStorage.setItem("astro_location", JSON.stringify(loc));
}
let locationState = loadLocation();
let currentDate = parseDate(); let currentDate = parseDate();
@@ -281,6 +291,7 @@ function render() {
history.replaceState(null, "", newUrl); history.replaceState(null, "", newUrl);
document.getElementById("dateTitle").textContent = formatDate(date); document.getElementById("dateTitle").textContent = formatDate(date);
document.getElementById("locStatus").textContent = `${locationState.label}: ${locationState.lat.toFixed(4)}, ${locationState.lon.toFixed(4)}`;
// Moon // Moon
const { age, illumination } = moonPhase(date); const { age, illumination } = moonPhase(date);
@@ -304,7 +315,7 @@ function render() {
document.getElementById("nextFullGreg").textContent = nextFullDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" }); document.getElementById("nextFullGreg").textContent = nextFullDate.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", timeZone: "UTC" });
// Sun // Sun
const sun = sunriseSunset(date, LAT, LON); const sun = sunriseSunset(date, locationState.lat, locationState.lon);
document.getElementById("sunriseSeth").textContent = sun.riseJD ? jdToDecimalTime(sun.riseJD) : "—"; document.getElementById("sunriseSeth").textContent = sun.riseJD ? jdToDecimalTime(sun.riseJD) : "—";
document.getElementById("sunriseGreg").textContent = sun.rise || (sun.alwaysUp ? "Midnight sun" : "Below horizon"); document.getElementById("sunriseGreg").textContent = sun.rise || (sun.alwaysUp ? "Midnight sun" : "Below horizon");
document.getElementById("sunsetSeth").textContent = sun.setJD ? jdToDecimalTime(sun.setJD) : "—"; document.getElementById("sunsetSeth").textContent = sun.setJD ? jdToDecimalTime(sun.setJD) : "—";
@@ -360,6 +371,71 @@ function render() {
}); });
} }
function initLocationUI() {
const labelEl = document.getElementById("locLabel");
const zipEl = document.getElementById("locZip");
const latEl = document.getElementById("locLat");
const lonEl = document.getElementById("locLon");
const statusEl = document.getElementById("locStatus");
labelEl.value = locationState.label || "Home";
latEl.value = String(locationState.lat);
lonEl.value = String(locationState.lon);
document.getElementById("locSave").addEventListener("click", () => {
const lat = Number(latEl.value);
const lon = Number(lonEl.value);
if (!Number.isFinite(lat) || !Number.isFinite(lon) || lat < -90 || lat > 90 || lon < -180 || lon > 180) {
statusEl.textContent = "Invalid latitude/longitude";
return;
}
locationState = { label: labelEl.value.trim() || "Home", lat, lon };
saveLocation(locationState);
statusEl.textContent = `Saved: ${locationState.label}`;
render();
});
document.getElementById("locUseZip").addEventListener("click", async () => {
const zip = (zipEl.value || "").trim();
if (!/^\d{5}(-\d{4})?$/.test(zip)) {
statusEl.textContent = "Enter valid US ZIP";
return;
}
statusEl.textContent = "Looking up ZIP...";
try {
const r = await fetch(`https://api.zippopotam.us/us/${encodeURIComponent(zip)}`);
if (!r.ok) throw new Error("zip");
const data = await r.json();
const p = data?.places?.[0];
if (!p) throw new Error("zip");
latEl.value = String(Number(p.latitude));
lonEl.value = String(Number(p.longitude));
if (!labelEl.value.trim()) labelEl.value = `${p["place name"]}, ${p["state abbreviation"]}`;
statusEl.textContent = "ZIP loaded";
} catch {
statusEl.textContent = "ZIP lookup failed";
}
});
document.getElementById("locGeo").addEventListener("click", () => {
if (!navigator.geolocation) {
statusEl.textContent = "Geolocation unsupported";
return;
}
statusEl.textContent = "Getting location...";
navigator.geolocation.getCurrentPosition(
(pos) => {
latEl.value = String(pos.coords.latitude.toFixed(6));
lonEl.value = String(pos.coords.longitude.toFixed(6));
if (!labelEl.value.trim()) labelEl.value = "Current Location";
statusEl.textContent = "Location loaded";
},
() => { statusEl.textContent = "Location denied/unavailable"; },
{ enableHighAccuracy: true, timeout: 10000 }
);
});
}
document.getElementById("prevDay").addEventListener("click", () => { document.getElementById("prevDay").addEventListener("click", () => {
currentDate = stepDate(currentDate, -1); currentDate = stepDate(currentDate, -1);
render(); render();
@@ -369,4 +445,5 @@ document.getElementById("nextDay").addEventListener("click", () => {
render(); render();
}); });
initLocationUI();
render(); render();