// urlstate.js — view state <-> location hash import { YEAR_DAYS, DAY_MOMENTS } from './sky.js'; import { MIN_ALT, MAX_ALT, MAX_ZOOM, MAX_PITCH, MAX_EXAG } from './limits.js'; export const VIEWS = ['globe', 'equal_earth', 'plate_carree']; export const DEFAULT = Object.freeze({ layer: 'relief', view: 'globe', lat: 20, lon: 0, alt: 2, zoom: 1, overlays: ['coast', 'lakes', 'rivers', 'graticule', 'regions', 'pins'], over: '', op: 0.5, sky: false, day: Math.round(YEAR_DAYS / 4), mo: Math.round(DAY_MOMENTS / 2), hd: 0, pt: 0, terrain: 1, era: '', water: true }); const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v)); export function parse(hash) { const s = { ...DEFAULT, overlays: [...DEFAULT.overlays] }; const q = new URLSearchParams((hash || '').replace(/^#/, '')); if (q.get('layer')) s.layer = q.get('layer'); if (VIEWS.includes(q.get('view'))) s.view = q.get('view'); for (const k of ['lat', 'lon', 'alt', 'zoom', 'op', 'day', 'mo', 'hd', 'pt']) { const v = parseFloat(q.get(k)); if (Number.isFinite(v)) s[k] = v; } s.lat = clamp(s.lat, -90, 90); s.lon = ((((s.lon + 180) % 360) + 360) % 360) - 180; s.alt = clamp(s.alt, MIN_ALT, MAX_ALT); s.zoom = clamp(s.zoom, 1, MAX_ZOOM); s.op = clamp(s.op, 0, 1); s.day = clamp(Math.round(s.day), 0, YEAR_DAYS - 1); s.mo = clamp(Math.round(s.mo), 0, DAY_MOMENTS - 1); s.hd = ((s.hd % 360) + 360) % 360; s.pt = clamp(s.pt, 0, MAX_PITCH); const t = parseFloat(q.get('3d')); if (Number.isFinite(t)) s.terrain = t <= 0 ? 0 : clamp(t, 1, MAX_EXAG); // 0 = flat tiles s.over = q.get('over') || ''; s.sky = q.get('sky') === '1'; s.era = q.get('era') || ''; s.water = q.get('water') !== '0'; if (q.has('overlays')) s.overlays = q.get('overlays').split(',').filter(Boolean); return s; } export const drawnLayer = (s, kind) => // Water off on the 3D globe: the sea floor in place of relief (kind === 'globe' && s.terrain > 0 && !s.water && s.layer === 'relief' ? 'seafloor' : s.layer); export function serialize(s) { const q = { layer: s.layer, view: s.view, lat: String(+s.lat.toFixed(4)), lon: String(+s.lon.toFixed(4)), alt: String(+s.alt.toPrecision(4)), zoom: String(+s.zoom.toPrecision(5)), overlays: s.overlays.join(',') }; if (s.over) Object.assign(q, { over: s.over, op: String(+s.op.toFixed(2)) }); if (s.sky) Object.assign(q, { sky: '1', day: String(s.day), mo: String(s.mo) }); if (s.view === 'globe' && (s.hd || s.pt)) Object.assign(q, { hd: String(+s.hd.toFixed(1)), pt: String(+s.pt.toFixed(1)) }); if (s.terrain !== 1) q['3d'] = String(s.terrain); if (s.era) q.era = s.era; if (!s.water) q.water = '0'; return '#' + new URLSearchParams(q).toString(); } export function carryHash(hash) { // to another world: the view carries over; era and time of year/day do not const q = new URLSearchParams((hash || '').replace(/^#/, '')); for (const k of ['era', 'day', 'mo']) q.delete(k); const s = q.toString(); return s ? '#' + s : ''; }