Add avg lat/lon per H7 cell to map endpoint response
This commit is contained in:
@@ -12,13 +12,10 @@ const DIST = join(ROOT, 'dist')
|
|||||||
const SAVES_DIR = join(ROOT, 'data', 'saves')
|
const SAVES_DIR = join(ROOT, 'data', 'saves')
|
||||||
const DB_PATH = join(ROOT, 'data', 'otivm.sqlite3')
|
const DB_PATH = join(ROOT, 'data', 'otivm.sqlite3')
|
||||||
|
|
||||||
// Ensure saves directory exists
|
|
||||||
await mkdir(SAVES_DIR, { recursive: true })
|
await mkdir(SAVES_DIR, { recursive: true })
|
||||||
|
|
||||||
// Open TESSERA database — read-only
|
|
||||||
const db = new Database(DB_PATH, { readonly: true })
|
const db = new Database(DB_PATH, { readonly: true })
|
||||||
|
|
||||||
// Prepared statements
|
|
||||||
const stmtEpoch = db.prepare(
|
const stmtEpoch = db.prepare(
|
||||||
'SELECT sl_offset_cm FROM paleo_epochs WHERE epoch_key = ?'
|
'SELECT sl_offset_cm FROM paleo_epochs WHERE epoch_key = ?'
|
||||||
)
|
)
|
||||||
@@ -26,45 +23,41 @@ const stmtEpoch = db.prepare(
|
|||||||
const stmtH7 = db.prepare(`
|
const stmtH7 = db.prepare(`
|
||||||
SELECT
|
SELECT
|
||||||
h7,
|
h7,
|
||||||
SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land,
|
AVG(lat) AS lat,
|
||||||
COUNT(*) AS h9_total
|
AVG(lon) AS lon,
|
||||||
|
SUM(CASE WHEN elev_cm > ? THEN 1 ELSE 0 END) AS h7_land,
|
||||||
|
COUNT(*) AS h9_total
|
||||||
FROM tessera_cells
|
FROM tessera_cells
|
||||||
WHERE h5 = ? AND status = 2
|
WHERE h5 = ? AND status = 2
|
||||||
GROUP BY h7
|
GROUP BY h7
|
||||||
`)
|
`)
|
||||||
|
|
||||||
// Convert hex string H3 ID to BigInt integer for DB lookup
|
|
||||||
function h3HexToInt(hexStr) {
|
function h3HexToInt(hexStr) {
|
||||||
return BigInt('0x' + hexStr)
|
return BigInt('0x' + hexStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fastify = Fastify({ logger: false })
|
const fastify = Fastify({ logger: false })
|
||||||
|
|
||||||
// Serve built frontend
|
|
||||||
await fastify.register(fastifyStatic, {
|
await fastify.register(fastifyStatic, {
|
||||||
root: DIST,
|
root: DIST,
|
||||||
prefix: '/',
|
prefix: '/',
|
||||||
})
|
})
|
||||||
|
|
||||||
// --- TESSERA map endpoint ---
|
// GET /api/map/:h5/:epoch
|
||||||
// Returns H7-aggregated land/sea classification for one H5 waypoint at one epoch.
|
// Returns H7-aggregated land/sea classification with real centroids.
|
||||||
// h5param: H3 res-5 cell ID as hex string (e.g. '851e805bfffffff')
|
// h5param: H3 res-5 hex string (e.g. '851e805bfffffff')
|
||||||
// epochKey: named epoch from paleo_epochs (e.g. 'roman_14bce')
|
// 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) => {
|
fastify.get('/api/map/:h5/:epoch', async (req, reply) => {
|
||||||
const { h5: h5param, epoch: epochKey } = req.params
|
const { h5: h5param, epoch: epochKey } = req.params
|
||||||
|
|
||||||
// Validate H5 — must be 15-char lowercase hex
|
|
||||||
if (!/^[0-9a-f]{15}$/.test(h5param)) {
|
if (!/^[0-9a-f]{15}$/.test(h5param)) {
|
||||||
return reply.code(400).send({ error: 'Invalid H5 ID' })
|
return reply.code(400).send({ error: 'Invalid H5 ID' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate epoch key — alphanumeric and underscores only
|
|
||||||
if (!/^[a-z0-9_]+$/.test(epochKey)) {
|
if (!/^[a-z0-9_]+$/.test(epochKey)) {
|
||||||
return reply.code(400).send({ error: 'Invalid epoch key' })
|
return reply.code(400).send({ error: 'Invalid epoch key' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve epoch to sea level offset
|
|
||||||
const epoch = stmtEpoch.get(epochKey)
|
const epoch = stmtEpoch.get(epochKey)
|
||||||
if (!epoch) {
|
if (!epoch) {
|
||||||
return reply.code(404).send({ error: `Unknown epoch: ${epochKey}` })
|
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
|
const slOffsetCm = epoch.sl_offset_cm
|
||||||
|
|
||||||
// Convert H5 hex string to integer for DB query
|
|
||||||
let h5int
|
let h5int
|
||||||
try {
|
try {
|
||||||
h5int = h3HexToInt(h5param)
|
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' })
|
return reply.code(400).send({ error: 'Malformed H5 ID' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query H7 land/sea classification
|
|
||||||
const rows = stmtH7.all(slOffsetCm, h5int)
|
const rows = stmtH7.all(slOffsetCm, h5int)
|
||||||
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
return reply.code(404).send({ error: `No data for H5: ${h5param}` })
|
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 => ({
|
const cells = rows.map(row => ({
|
||||||
h7: row.h7.toString(16), // return as hex string
|
h7: row.h7.toString(16),
|
||||||
h7_land: row.h7_land,
|
lat: row.lat,
|
||||||
|
lon: row.lon,
|
||||||
|
h7_land: row.h7_land,
|
||||||
h9_total: row.h9_total,
|
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({
|
return reply.send({
|
||||||
epoch_key: epochKey,
|
epoch_key: epochKey,
|
||||||
sl_offset_cm: slOffsetCm,
|
sl_offset_cm: slOffsetCm,
|
||||||
h5: h5param,
|
h5: h5param,
|
||||||
cells,
|
cells,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// Load save state
|
|
||||||
fastify.get('/api/save/:token', async (req, reply) => {
|
fastify.get('/api/save/:token', async (req, reply) => {
|
||||||
const { token } = req.params
|
const { token } = req.params
|
||||||
if (!/^[0-9a-f]{8}$/.test(token)) {
|
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) => {
|
fastify.post('/api/save/:token', async (req, reply) => {
|
||||||
const { token } = req.params
|
const { token } = req.params
|
||||||
if (!/^[0-9a-f]{8}$/.test(token)) {
|
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) => {
|
fastify.setNotFoundHandler((req, reply) => {
|
||||||
reply.sendFile('index.html')
|
reply.sendFile('index.html')
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user