diff --git a/src/screens/Map.jsx b/src/screens/Map.jsx index c69fb1b..5057551 100644 --- a/src/screens/Map.jsx +++ b/src/screens/Map.jsx @@ -1,7 +1,13 @@ +import { useState, useEffect } from 'react' import { WAYPOINTS, ROUTES } from '../constants.js' +import { fetchMapCells } from '../api.js' + +// Bounding box — Mediterranean basin +const BBOX = { minLng: 5, maxLng: 38, minLat: 28, maxLat: 48 } +const W = 800 +const H = 460 // H3 res-5 cell centre coordinates — hardcoded, h3-js not in browser bundle -// Cross-checked against constants.js H3 IDs const CENTRES = { ostia: { lat: 41.73, lng: 12.23 }, capua: { lat: 41.09, lng: 14.21 }, @@ -10,10 +16,21 @@ const CENTRES = { alexandria: { lat: 31.20, lng: 29.92 }, } -// Bounding box clipped to relevant Mediterranean — no Atlantic needed -const BBOX = { minLng: 5, maxLng: 38, minLat: 28, maxLat: 48 } -const W = 800 -const H = 460 +// 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 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' function project(lat, lng) { const x = ((lng - BBOX.minLng) / (BBOX.maxLng - BBOX.minLng)) * W @@ -21,56 +38,47 @@ function project(lat, lng) { return { x, y } } -function makePath(pts) { - return pts.map(([lat, lng], i) => { - const { x, y } = project(lat, lng) - return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}` - }).join(' ') + ' Z' +// 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, + } } -// Europe + Asia Minor mainland polygon -// Traces: French Riviera → Italian peninsula → Adriatic → Greece → Thrace → -// top edge east → Turkey south coast → top edge west → close -const EUROPE = [ - [48.0,5.0], - [43.4,5.0],[43.5,5.3],[43.8,7.4], - [43.0,9.4],[41.9,8.7],[41.2,9.3],[39.8,9.6],[39.0,8.9],[38.4,9.4], - [37.9,11.7],[37.5,15.0],[38.1,15.7], - [40.1,18.4],[40.9,15.6],[41.7,13.7],[43.5,13.5],[44.4,12.3], - [45.5,13.8],[45.8,13.6],[45.2,14.0],[44.5,14.5],[43.5,16.9], - [42.4,18.5],[41.3,19.4], - [40.5,23.0],[40.0,23.5],[38.2,23.6],[37.5,22.4],[36.7,21.9], - [37.5,21.1],[38.5,22.0],[39.4,22.5],[40.0,22.6],[40.6,22.9], - [41.0,23.5],[41.5,24.5],[41.7,26.4],[41.5,28.0],[41.2,29.0], - [48.0,29.0],[48.0,38.0], - [36.8,36.6],[36.5,36.2],[36.8,35.1],[36.5,34.6],[36.2,33.3], - [36.2,32.0],[36.5,31.0],[37.0,30.0],[37.3,28.9],[37.5,27.5], - [37.0,27.2],[38.3,26.9],[38.6,26.8],[39.1,26.4],[39.6,26.2], - [40.3,26.4],[40.8,26.0], - [48.0,5.0], -] - -// North Africa mainland polygon -// Traces: Morocco → Algeria → Tunisia (with Cape Bon) → Libya → Egypt → -// Levant coast → east clip edge → south clip → close -const AFRICA = [ - [28.0,5.0], - [35.8,5.0],[35.2,5.7],[36.9,6.8],[37.2,8.5],[37.1,9.2], - [36.8,10.3],[37.1,10.8],[37.1,11.0], - [36.8,11.1],[35.5,11.1],[33.9,11.0],[33.6,11.6], - [33.0,12.0],[32.8,13.3],[32.1,15.0], - [30.8,18.5],[30.5,20.0],[30.9,20.1], - [31.2,25.0],[31.5,25.2],[31.5,28.0],[31.5,32.4], - [31.0,32.5],[31.2,33.8],[31.5,34.8],[32.0,34.7],[33.1,35.2],[35.5,36.2], - [35.0,38.0],[28.0,38.0], - [28.0,5.0], -] - -const ROUTE_ORDER = ['olive', 'wine', 'grain', 'linen'] -const WAYPOINT_IDS = ['ostia', 'capua', 'brundisium', 'carthago', 'alexandria'] +const H7_RADIUS = 8 export default function Map({ state }) { const chapter = state?.chapter ?? 1 + 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(() => { + WAYPOINT_IDS.forEach((id) => { + const wp = WAYPOINTS[id] + if (chapter < wp.chapter) return // not yet revealed + if (cellData[id]) return // already loaded + fetchMapCells(H5_IDS[id], EPOCH).then(data => { + if (data?.cells) { + setCellData(prev => ({ ...prev, [id]: data.cells })) + } + }) + }) + }, [chapter]) // eslint-disable-line react-hooks/exhaustive-deps const pts = {} Object.entries(CENTRES).forEach(([id, { lat, lng }]) => { @@ -79,9 +87,6 @@ export default function Map({ state }) { const currentId = WAYPOINT_IDS[Math.min(chapter - 1, 4)] - const europePath = makePath(EUROPE) - const africaPath = makePath(AFRICA) - return (