// 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 : '';
}