Worldwide coverage: grade any point on Earth (#32)
Remove the US+Canada bounding box so every endpoint accepts any lat/lon. The grading pipeline was already global-ready (ERA5 archive, timezone=auto, day-of-year climatology), so opening it up is mostly deleting the guard — plus the edge cases that only exist once the whole globe is in play: - grid.py: snap() wraps longitude into [-180, 180) and clamps latitude, and cell centers are normalized so the polar row and the cells straddling the antimeridian always report valid coordinates to the weather/geocoding APIs. snap() and from_id() now share one _cell() builder, making id round-trips exact by construction (verified with a 300k-point global sweep). - nav.js: neighbor-cell prefetch skips rows past the poles and wraps longitudes across the dateline instead of sending out-of-range queries. - Nominatim reverse geocoding requests accept-language=en so place labels render in one script worldwide (matching the forward geocoder). - mappicker: search suggestions are no longer filtered to US/CA, the placeholder and default map view are worldwide. - calendar: season filter labels flip for southern-hemisphere locations (Dec-Feb shows as Summer); the underlying month groups are unchanged, so saved filter selections keep meaning the same months. Verified end-to-end on a scratch server: Tokyo and Sydney grade with real labels, a Fiji cell on the antimeridian's east edge builds and serves warm hits from the derived store, and prefetch=1 on a cold cell still answers 204 without spending weather-API quota.
This commit is contained in:
parent
2f38fb8b51
commit
475e0ee7a2
4 changed files with 15 additions and 10 deletions
|
|
@ -802,7 +802,7 @@ function updateHash() {
|
|||
}
|
||||
|
||||
// Start where you left off: an explicit link (hash) wins, otherwise the last
|
||||
// place you looked at (remembered across views), otherwise the default US map.
|
||||
// place you looked at (remembered across views), otherwise the default world map.
|
||||
function restoreFromHash() {
|
||||
const loc = window.Thermograph.loadLastLocation();
|
||||
if (!loc) return;
|
||||
|
|
|
|||
|
|
@ -135,8 +135,11 @@ let data = null; // last /api/v2/calendar response
|
|||
let metric = localStorage.getItem("thermograph:calMetric") || "tmax"; // tmax|tmin|feels|wind|gust|precip|dsr
|
||||
|
||||
// Season/year filters: only months whose season AND year are checked are shown.
|
||||
const SEASONS = [["winter", "Winter"], ["spring", "Spring"], ["summer", "Summer"], ["fall", "Fall"]];
|
||||
// Keys group months (Dec–Feb = "winter", etc.); in the southern hemisphere those
|
||||
// same months are the opposite season, so only the display label flips (s[2]).
|
||||
const SEASONS = [["winter", "Winter", "Summer"], ["spring", "Spring", "Fall"], ["summer", "Summer", "Winter"], ["fall", "Fall", "Spring"]];
|
||||
const seasonOf = (m) => (m === 11 || m <= 1) ? "winter" : m <= 4 ? "spring" : m <= 7 ? "summer" : "fall";
|
||||
const seasonLabel = (s) => (selected && selected.lat < 0 ? s[2] : s[1]);
|
||||
let checkedSeasons = new Set(SEASONS.map((s) => s[0])); // persists across reloads
|
||||
let checkedYears = null; // reset to all available on each fetch
|
||||
|
||||
|
|
@ -264,7 +267,7 @@ let filterYears = []; // the year set behind the Years dropdown (for its summa
|
|||
function seasonSummaryText() {
|
||||
if (checkedSeasons.size === SEASONS.length) return "All seasons";
|
||||
if (checkedSeasons.size === 0) return "None";
|
||||
return SEASONS.filter((s) => checkedSeasons.has(s[0])).map((s) => s[1]).join(", ");
|
||||
return SEASONS.filter((s) => checkedSeasons.has(s[0])).map(seasonLabel).join(", ");
|
||||
}
|
||||
function yearSummaryText() {
|
||||
const n = checkedYears ? checkedYears.size : 0;
|
||||
|
|
@ -335,7 +338,7 @@ function renderFilters() {
|
|||
// the span (the weather sets survive the rebuild — they're module state).
|
||||
seasonYearEl.innerHTML =
|
||||
filterDropdown("season", "Seasons", "season",
|
||||
SEASONS.map((s) => [s[0], s[1], checkedSeasons.has(s[0])]), seasonSummaryText()) +
|
||||
SEASONS.map((s) => [s[0], seasonLabel(s), checkedSeasons.has(s[0])]), seasonSummaryText()) +
|
||||
filterDropdown("year", "Years", "year",
|
||||
years.map((y) => [y, y, checkedYears.has(y)]), yearSummaryText()) +
|
||||
weatherDropdown();
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@
|
|||
<button type="button" class="mp-close" aria-label="Close">×</button>
|
||||
</div>
|
||||
<form class="mp-search" autocomplete="off">
|
||||
<input type="text" placeholder="Search a US or Canadian place…" autocomplete="off" />
|
||||
<input type="text" placeholder="Search for a place…" autocomplete="off" />
|
||||
<button type="submit">Search</button>
|
||||
<ul class="mp-sug" hidden></ul>
|
||||
</form>
|
||||
|
|
@ -70,8 +70,7 @@
|
|||
}
|
||||
|
||||
function renderSug(list) {
|
||||
const usca = list.filter((r) => ["US", "CA"].includes(r.country_code));
|
||||
const shown = usca.length ? usca : list;
|
||||
const shown = list;
|
||||
sugEl.innerHTML = "";
|
||||
if (!shown.length) { sugEl.hidden = true; return; }
|
||||
shown.forEach((r) => {
|
||||
|
|
@ -117,7 +116,7 @@
|
|||
overlay.hidden = false;
|
||||
|
||||
if (!map) {
|
||||
map = L.map(mapEl, { worldCopyJump: true, zoomControl: true }).setView([44.5, -95], 4);
|
||||
map = L.map(mapEl, { worldCopyJump: true, zoomControl: true }).setView([25, 0], 2);
|
||||
// Colorful CARTO "Voyager" basemap (no key needed), split into two raster
|
||||
// layers so road weight and label size can be tuned independently:
|
||||
// • the label-free base is requested one zoom lower and drawn at 512px
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
// (map, calendar, day) before that page's own script. It remembers the most
|
||||
// recently selected spot in localStorage and keeps the header's view links
|
||||
// (Map · Calendar · Day) pointing at that spot — so switching views, or coming
|
||||
// back to the map, lands you where you were instead of an empty US-wide map.
|
||||
// back to the map, lands you where you were instead of an empty world map.
|
||||
|
||||
const LOC_KEY = "thermograph:loc";
|
||||
|
||||
|
|
@ -327,10 +327,13 @@ function prefetchNeighbors(lat, lon) {
|
|||
for (const di of [-1, 0, 1]) {
|
||||
for (const dj of [-1, 0, 1]) {
|
||||
if (!di && !dj) continue;
|
||||
const nLat = centerLat + di * latStep;
|
||||
if (Math.abs(nLat) > 90) continue; // no rows past the poles
|
||||
const nLon = ((centerLon + dj * lonStep + 540) % 360) - 180; // wrap across the antimeridian
|
||||
const cellTag = `${i + di}_${j + dj}`;
|
||||
if (_warmedCells.has(cellTag)) continue;
|
||||
_warmedCells.add(cellTag);
|
||||
const nq = `lat=${(centerLat + di * latStep).toFixed(5)}&lon=${(centerLon + dj * lonStep).toFixed(5)}`;
|
||||
const nq = `lat=${nLat.toFixed(5)}&lon=${nLon.toFixed(5)}`;
|
||||
setTimeout(() => { fetch(`api/v2/cell?${nq}&prefetch=1`).catch(() => {}); }, 1500 + 1100 * k++);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue