Use real H7 centroids from API — remove spiral approximation

This commit is contained in:
otivm
2026-04-28 05:30:10 +00:00
parent 8e57f8c3a3
commit 4f5f4510a7

View File

@@ -2,11 +2,18 @@ import { useState, useEffect } from 'react'
import { WAYPOINTS, ROUTES } from '../constants.js' import { WAYPOINTS, ROUTES } from '../constants.js'
import { fetchMapCells } from '../api.js' import { fetchMapCells } from '../api.js'
// Bounding box — Mediterranean basin
const BBOX = { minLng: 5, maxLng: 38, minLat: 28, maxLat: 48 } const BBOX = { minLng: 5, maxLng: 38, minLat: 28, maxLat: 48 }
const W = 800 const W = 800
const H = 460 const H = 460
const H5_IDS = {
ostia: '851e805bfffffff',
capua: '851e8333fffffff',
brundisium: '851e8ba3fffffff',
carthago: '85386e23fffffff',
alexandria: '853f5ba7fffffff',
}
// H3 res-5 cell centre coordinates — hardcoded, h3-js not in browser bundle // H3 res-5 cell centre coordinates — hardcoded, h3-js not in browser bundle
const CENTRES = { const CENTRES = {
ostia: { lat: 41.73, lng: 12.23 }, ostia: { lat: 41.73, lng: 12.23 },
@@ -16,62 +23,29 @@ const CENTRES = {
alexandria: { lat: 31.20, lng: 29.92 }, alexandria: { lat: 31.20, lng: 29.92 },
} }
// H3 res-5 hex string IDs — used for API calls
const H5_IDS = {
ostia: '851e805bfffffff',
capua: '851e8333fffffff',
brundisium: '851e8ba3fffffff',
carthago: '85386e23fffffff',
alexandria: '853f5ba7fffffff',
}
const WAYPOINT_IDS = ['ostia', 'capua', 'brundisium', 'carthago', 'alexandria'] const WAYPOINT_IDS = ['ostia', 'capua', 'brundisium', 'carthago', 'alexandria']
const ROUTE_ORDER = ['olive', 'wine', 'grain', 'linen'] const ROUTE_ORDER = ['olive', 'wine', 'grain', 'linen']
// Active epoch — Roman period for OTIVM-II
// sl_offset_cm = -10 (effectively zero). Coastline matches modern sea level.
const EPOCH = 'roman_14bce' const EPOCH = 'roman_14bce'
// H7 cell approximate radius in SVG pixels — H7 inradius ~10km
// At our projection (33° = 800px wide), 1° lng ≈ 24px, 10km ≈ 9px
const H7_RADIUS = 9
function project(lat, lng) { function project(lat, lng) {
const x = ((lng - BBOX.minLng) / (BBOX.maxLng - BBOX.minLng)) * W const x = ((lng - BBOX.minLng) / (BBOX.maxLng - BBOX.minLng)) * W
const y = ((BBOX.maxLat - lat) / (BBOX.maxLat - BBOX.minLat)) * H const y = ((BBOX.maxLat - lat) / (BBOX.maxLat - BBOX.minLat)) * H
return { x, y } return { x, y }
} }
// Approximate H7 cell position within its H5 parent using a sunflower spiral.
// H7 cells are rendered as circles — exact hex geometry requires h3-js (server-side only).
// The spiral distributes 49 cells across the H5 area. Visual approximation only.
// Will be replaced when the API returns per-cell centroids.
function h7ApproxPos(h5Id, cellIndex) {
const cx = CENTRES[h5Id]
if (!cx) return null
const { x: cx_px, y: cy_px } = project(cx.lat, cx.lng)
// Golden angle spiral — distributes points evenly across H5 area (~90km)
// At our projection scale, H5 spans ~120px. Space cells ~16px apart.
const SPACING = 14
const angle = cellIndex * 2.399963 // golden angle radians
const radius = SPACING * Math.sqrt(cellIndex)
return {
x: cx_px + Math.cos(angle) * radius,
y: cy_px + Math.sin(angle) * radius,
}
}
const H7_RADIUS = 8
export default function Map({ state }) { export default function Map({ state }) {
const chapter = state?.chapter ?? 1 const chapter = state?.chapter ?? 1
const [cellData, setCellData] = useState({}) const [cellData, setCellData] = useState({})
// Fetch H7 cells for all revealed waypoints.
// A waypoint is revealed when chapter >= waypoint.chapter.
// Ostia (chapter 1) is revealed from the start.
// Cells are fetched once per waypoint and cached in state.
useEffect(() => { useEffect(() => {
WAYPOINT_IDS.forEach((id) => { WAYPOINT_IDS.forEach((id) => {
const wp = WAYPOINTS[id] const wp = WAYPOINTS[id]
if (chapter < wp.chapter) return // not yet revealed if (chapter < wp.chapter) return
if (cellData[id]) return // already loaded if (cellData[id]) return
fetchMapCells(H5_IDS[id], EPOCH).then(data => { fetchMapCells(H5_IDS[id], EPOCH).then(data => {
if (data?.cells) { if (data?.cells) {
setCellData(prev => ({ ...prev, [id]: data.cells })) setCellData(prev => ({ ...prev, [id]: data.cells }))
@@ -94,24 +68,24 @@ export default function Map({ state }) {
style={{ width: '100%', maxWidth: W, display: 'block', margin: '0 auto' }} style={{ width: '100%', maxWidth: W, display: 'block', margin: '0 auto' }}
aria-label="Mediterranean trade map" aria-label="Mediterranean trade map"
> >
{/* Sea — permanent darkness beyond fog of war */} {/* Sea — permanent darkness */}
<rect width={W} height={H} fill="#0d1a26" /> <rect width={W} height={H} fill="#0d1a26" />
{/* Fog of war — land H7 cells, revealed by chapter */} {/* Fog of war — land H7 cells at real geographic positions */}
{WAYPOINT_IDS.map((id) => { {WAYPOINT_IDS.map((id) => {
const wp = WAYPOINTS[id] const wp = WAYPOINTS[id]
if (chapter < wp.chapter) return null if (chapter < wp.chapter) return null
const cells = cellData[id] const cells = cellData[id]
if (!cells) return null if (!cells) return null
return cells.map((cell, cellIdx) => { return cells.map((cell) => {
if (!cell.is_land) return null if (!cell.is_land) return null
const pos = h7ApproxPos(id, cellIdx) // lat/lon now come directly from the API — real H7 centroids
if (!pos) return null const { x, y } = project(cell.lat, cell.lon)
return ( return (
<circle <circle
key={`${id}-${cell.h7}`} key={`${id}-${cell.h7}`}
cx={pos.x} cx={x}
cy={pos.y} cy={y}
r={H7_RADIUS} r={H7_RADIUS}
fill="#2d3b2a" fill="#2d3b2a"
stroke="#3a4a30" stroke="#3a4a30"
@@ -126,7 +100,6 @@ export default function Map({ state }) {
{ROUTE_ORDER.map((routeId) => { {ROUTE_ORDER.map((routeId) => {
const route = ROUTES.find((r) => r.id === routeId) const route = ROUTES.find((r) => r.id === routeId)
if (!route) return null if (!route) return null
// Route visible when both endpoints are revealed
const fromWp = WAYPOINTS[route.from] const fromWp = WAYPOINTS[route.from]
const toWp = WAYPOINTS[route.to] const toWp = WAYPOINTS[route.to]
if (chapter < fromWp.chapter || chapter < toWp.chapter) return null if (chapter < fromWp.chapter || chapter < toWp.chapter) return null