Add Map screen — SVG Mediterranean with waypoints and routes
This commit is contained in:
127
src/screens/Map.jsx
Normal file
127
src/screens/Map.jsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { WAYPOINTS, ROUTES } from '../constants.js'
|
||||||
|
|
||||||
|
// 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 },
|
||||||
|
brundisium: { lat: 40.63, lng: 17.94 },
|
||||||
|
carthago: { lat: 36.86, lng: 10.32 },
|
||||||
|
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
|
||||||
|
|
||||||
|
function project(lat, lng) {
|
||||||
|
const x = ((lng - BBOX.minLng) / (BBOX.maxLng - BBOX.minLng)) * W
|
||||||
|
const y = ((BBOX.maxLat - lat) / (BBOX.maxLat - BBOX.minLat)) * H
|
||||||
|
return { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rough Mediterranean land outline — placeholder, will be replaced
|
||||||
|
const LAND = [
|
||||||
|
[43.4,5.3],[41.4,2.2],[40.4,0.7],[39.4,0.2],[38.0,0.1],[37.4,0.8],
|
||||||
|
[36.8,2.3],[36.2,5.4],[36.9,10.2],[37.1,11.1],[33.0,12.0],[32.9,14.0],
|
||||||
|
[32.1,15.0],[30.8,19.8],[30.9,25.2],[31.2,25.3],[31.5,28.0],[31.5,32.3],
|
||||||
|
[31.2,34.2],[33.0,35.1],[35.5,36.2],[36.5,36.2],[36.8,36.6],
|
||||||
|
[36.5,36.0],[35.4,34.6],[35.5,35.9],[36.5,36.2],[36.8,36.6],
|
||||||
|
[37.0,36.2],[37.5,36.8],[36.8,28.0],[37.0,27.2],[37.5,27.0],
|
||||||
|
[38.6,26.8],[39.1,26.4],[39.5,26.1],[40.2,26.3],[40.8,26.0],
|
||||||
|
[41.7,26.4],[41.5,28.0],[40.6,22.9],[40.0,22.6],[39.4,22.5],
|
||||||
|
[38.5,22.0],[37.5,21.1],[36.7,21.9],[37.5,22.4],[38.2,23.6],
|
||||||
|
[40.0,23.5],[40.5,23.0],[41.3,19.4],[42.4,18.5],[43.5,16.9],
|
||||||
|
[44.5,14.5],[45.8,13.6],[45.5,13.8],[44.4,12.3],[43.5,13.5],
|
||||||
|
[41.7,13.7],[40.9,15.6],[40.1,18.4],[38.1,15.7],[37.5,15.0],
|
||||||
|
[38.2,13.0],[37.1,11.8],[38.4,9.4],[39.0,8.9],[39.8,9.6],
|
||||||
|
[41.2,9.3],[41.9,8.7],[43.0,9.4],[43.8,7.4],[43.5,5.3],
|
||||||
|
]
|
||||||
|
|
||||||
|
const ROUTE_ORDER = ['olive', 'wine', 'grain', 'linen']
|
||||||
|
const WAYPOINT_IDS = ['ostia', 'capua', 'brundisium', 'carthago', 'alexandria']
|
||||||
|
|
||||||
|
export default function Map({ state }) {
|
||||||
|
const chapter = state?.chapter ?? 1
|
||||||
|
|
||||||
|
const pts = {}
|
||||||
|
Object.entries(CENTRES).forEach(([id, { lat, lng }]) => {
|
||||||
|
pts[id] = project(lat, lng)
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentId = WAYPOINT_IDS[Math.min(chapter - 1, 4)]
|
||||||
|
|
||||||
|
const landPath = LAND.map((c, i) => {
|
||||||
|
const { x, y } = project(c[0], c[1])
|
||||||
|
return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`
|
||||||
|
}).join(' ') + ' Z'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="map-screen">
|
||||||
|
<svg
|
||||||
|
viewBox={`0 0 ${W} ${H}`}
|
||||||
|
style={{ width: '100%', maxWidth: W, display: 'block', margin: '0 auto' }}
|
||||||
|
aria-label="Mediterranean trade map"
|
||||||
|
>
|
||||||
|
{/* Sea */}
|
||||||
|
<rect width={W} height={H} fill="#1a2a3a" />
|
||||||
|
|
||||||
|
{/* Land */}
|
||||||
|
<path d={landPath} fill="#2d3b2a" stroke="#4a5a3a" strokeWidth="1" />
|
||||||
|
|
||||||
|
{/* Routes */}
|
||||||
|
{ROUTE_ORDER.map((routeId) => {
|
||||||
|
const route = ROUTES.find((r) => r.id === routeId)
|
||||||
|
if (!route) return null
|
||||||
|
const a = pts[route.from]
|
||||||
|
const b = pts[route.to]
|
||||||
|
const unlocked = chapter > route.chapter
|
||||||
|
return (
|
||||||
|
<line
|
||||||
|
key={routeId}
|
||||||
|
x1={a.x} y1={a.y}
|
||||||
|
x2={b.x} y2={b.y}
|
||||||
|
stroke={unlocked ? '#c8a96e' : '#4a5a6a'}
|
||||||
|
strokeWidth={unlocked ? 1.5 : 1}
|
||||||
|
strokeDasharray={unlocked ? undefined : '4 4'}
|
||||||
|
opacity={unlocked ? 0.8 : 0.35}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* Waypoints */}
|
||||||
|
{WAYPOINT_IDS.map((id) => {
|
||||||
|
const wp = WAYPOINTS[id]
|
||||||
|
const { x, y } = pts[id]
|
||||||
|
const isCurrent = id === currentId
|
||||||
|
const isReached = WAYPOINT_IDS.indexOf(id) < chapter - 1
|
||||||
|
return (
|
||||||
|
<g key={id}>
|
||||||
|
{isCurrent && (
|
||||||
|
<circle cx={x} cy={y} r={11} fill="none" stroke="#e8c87e" strokeWidth="1" opacity="0.35" />
|
||||||
|
)}
|
||||||
|
<circle
|
||||||
|
cx={x} cy={y}
|
||||||
|
r={isCurrent ? 5 : 4}
|
||||||
|
fill={isCurrent ? '#e8c87e' : isReached ? '#7aab6a' : '#3a4a5a'}
|
||||||
|
stroke={isCurrent ? '#c8a050' : isReached ? '#5a8a4a' : '#2a3a4a'}
|
||||||
|
strokeWidth="1"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={x}
|
||||||
|
y={y - 11}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontSize="10"
|
||||||
|
fill={isCurrent ? '#e8c87e' : isReached ? '#7aab6a' : '#5a7a8a'}
|
||||||
|
fontFamily="Georgia, serif"
|
||||||
|
>
|
||||||
|
{wp.latin}
|
||||||
|
</text>
|
||||||
|
</g>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user