diff --git a/server/index.js b/server/index.js index bc32455..9ceb0e5 100644 --- a/server/index.js +++ b/server/index.js @@ -12,13 +12,10 @@ const DIST = join(ROOT, 'dist') const SAVES_DIR = join(ROOT, 'data', 'saves') const DB_PATH = join(ROOT, 'data', 'otivm.sqlite3') -// Ensure saves directory exists await mkdir(SAVES_DIR, { recursive: true }) -// Open TESSERA database — read-only const db = new Database(DB_PATH, { readonly: true }) -// Prepared statements const stmtEpoch = db.prepare( 'SELECT sl_offset_cm FROM paleo_epochs WHERE epoch_key = ?' ) @@ -26,45 +23,41 @@ const stmtEpoch = db.prepare( const stmtH7 = db.prepare(` SELECT h7, - SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land, - COUNT(*) AS h9_total + AVG(lat) AS lat, + AVG(lon) AS lon, + SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land, + COUNT(*) AS h9_total FROM tessera_cells WHERE h5 = ? AND status = 2 GROUP BY h7 `) -// Convert hex string H3 ID to BigInt integer for DB lookup function h3HexToInt(hexStr) { return BigInt('0x' + hexStr) } const fastify = Fastify({ logger: false }) -// Serve built frontend await fastify.register(fastifyStatic, { root: DIST, prefix: '/', }) -// --- TESSERA map endpoint --- -// Returns H7-aggregated land/sea classification for one H5 waypoint at one epoch. -// h5param: H3 res-5 cell ID as hex string (e.g. '851e805bfffffff') +// GET /api/map/:h5/:epoch +// Returns H7-aggregated land/sea classification with real centroids. +// h5param: H3 res-5 hex string (e.g. '851e805bfffffff') // epochKey: named epoch from paleo_epochs (e.g. 'roman_14bce') -// Response: { epoch_key, sl_offset_cm, h5, cells: [{ h7, h7_land, h9_total, is_land }] } +// Response: { epoch_key, sl_offset_cm, h5, cells: [{ h7, lat, lon, h7_land, h9_total, is_land }] } fastify.get('/api/map/:h5/:epoch', async (req, reply) => { const { h5: h5param, epoch: epochKey } = req.params - // Validate H5 — must be 15-char lowercase hex if (!/^[0-9a-f]{15}$/.test(h5param)) { return reply.code(400).send({ error: 'Invalid H5 ID' }) } - - // Validate epoch key — alphanumeric and underscores only if (!/^[a-z0-9_]+$/.test(epochKey)) { return reply.code(400).send({ error: 'Invalid epoch key' }) } - // Resolve epoch to sea level offset const epoch = stmtEpoch.get(epochKey) if (!epoch) { return reply.code(404).send({ error: `Unknown epoch: ${epochKey}` }) @@ -72,7 +65,6 @@ fastify.get('/api/map/:h5/:epoch', async (req, reply) => { const slOffsetCm = epoch.sl_offset_cm - // Convert H5 hex string to integer for DB query let h5int try { h5int = h3HexToInt(h5param) @@ -80,30 +72,29 @@ fastify.get('/api/map/:h5/:epoch', async (req, reply) => { return reply.code(400).send({ error: 'Malformed H5 ID' }) } - // Query H7 land/sea classification const rows = stmtH7.all(slOffsetCm, h5int) if (!rows.length) { return reply.code(404).send({ error: `No data for H5: ${h5param}` }) } - // Apply majority rule: H7 cell is land if >50% of H9 children are land const cells = rows.map(row => ({ - h7: row.h7.toString(16), // return as hex string - h7_land: row.h7_land, + h7: row.h7.toString(16), + lat: row.lat, + lon: row.lon, + h7_land: row.h7_land, h9_total: row.h9_total, - is_land: row.h7_land * 2 > row.h9_total ? 1 : 0, + is_land: row.h7_land * 2 > row.h9_total ? 1 : 0, })) return reply.send({ - epoch_key: epochKey, + epoch_key: epochKey, sl_offset_cm: slOffsetCm, - h5: h5param, + h5: h5param, cells, }) }) -// Load save state fastify.get('/api/save/:token', async (req, reply) => { const { token } = req.params if (!/^[0-9a-f]{8}$/.test(token)) { @@ -121,7 +112,6 @@ fastify.get('/api/save/:token', async (req, reply) => { } }) -// Write save state fastify.post('/api/save/:token', async (req, reply) => { const { token } = req.params if (!/^[0-9a-f]{8}$/.test(token)) { @@ -139,7 +129,6 @@ fastify.post('/api/save/:token', async (req, reply) => { } }) -// Fallback — serve index.html for client-side routing fastify.setNotFoundHandler((req, reply) => { reply.sendFile('index.html') })