🗺️ Fix: Remplace la carte canvas schématique par Leaflet + OSM
La carte d'observation ne montrait qu'un nuage de points sur un canvas vide, sans littoral ni repère géographique — d'où une impression de bug. Remplacée par une vraie carte Leaflet (fond OpenStreetMap réel), utilisé directement plutôt que via un wrapper Svelte : Sveaflet (publié hier, ciblé sur une prérelease de Svelte 5) et svelte-openlayers (mature mais embarque OpenLayers, disproportionné pour 7 marqueurs) ont été évalués et écartés au profit de Leaflet nu, plus mature et plus léger. Ajoute une config de preview secondaire (port 8099) dans .claude/launch.json pour vérifier le site sans interrompre un serveur de dev déjà lancé. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,12 @@
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 8080
|
||||
},
|
||||
{
|
||||
"name": "gwada-sirius-preview",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["eleventy", "--serve", "--port=8099"],
|
||||
"port": 8099
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+3
-4
@@ -11,8 +11,8 @@ Refonte complète (v2) : passage d'une SPA Vite+Svelte 4 à un site **11ty** sta
|
||||
- **i18n** : catalogues `messages/{fr,en,ht}.json`, compilés par `@inlang/paraglide-js` en `src/paraglide/` avant chaque build (`npm run messages`). `ht` retombe automatiquement sur `fr` pour les clés non traduites (contenu long, cf. portée d'origine).
|
||||
- **Îlots Svelte 5** (3 seulement, le reste est HTML/CSS pur) :
|
||||
- `PredictionsCalculator` — hydratation immédiate, améliore le tableau statique des 7 sites
|
||||
- `ObservationMap` — carte canvas schématique, hydratation différée (IntersectionObserver)
|
||||
- `SiriusGlobe` — vague planétaire du lever héliaque (13 villes), hydratation différée
|
||||
- `ObservationMap` — vraie carte Leaflet + fond OSM, marqueurs/popups pour les 7 sites, hydratation différée (IntersectionObserver). Leaflet est utilisé directement (pas de wrapper Svelte type Sveaflet/svelte-openlayers — évalués mais écartés : le premier cible une prérelease de Svelte 5 et vient d'être publié, le second embarque OpenLayers, bien plus lourd que nécessaire pour 7 marqueurs)
|
||||
- `SiriusGlobe` — vague planétaire du lever héliaque (13 villes), carte canvas schématique (continents simplifiés), hydratation différée
|
||||
- **Onglets** (Science, Dogon) : 100 % CSS (`input[type=radio]` + `label`), zéro JS.
|
||||
- **Build** : `@11ty/eleventy-plugin-vite` fait passer la sortie 11ty par Vite (bundling CSS/JS, compilation Svelte). Un plugin maison recopie les assets passthrough après le build Vite (voir commentaire dans `eleventy.config.js` — `emptyOutDir` de Vite les efface sinon).
|
||||
|
||||
@@ -26,9 +26,8 @@ Refonte complète (v2) : passage d'une SPA Vite+Svelte 4 à un site **11ty** sta
|
||||
## 🔄 Améliorations possibles
|
||||
|
||||
- Traduire le contenu long en Kreyòl (actuellement chrome/labels uniquement, contenu de fond en français)
|
||||
- Vraies données GeoJSON pour `ObservationMap` (actuellement une carte schématique par position lat/lon normalisée, pas un tracé de côte réel)
|
||||
- Service worker pour un usage hors-ligne (mentionné dans la doctrine, pas encore implémenté)
|
||||
- Mettre à jour chaque année les informations d'événements associatifs (`associations.njk`, données à vérifier auprès des organisateurs)
|
||||
- Le chunk Leaflet (~44 Ko gzip) n'est chargé qu'au scroll sur la carte (`observer/`), mais reste le plus lourd du site ; envisager des tuiles vectorielles/un fournisseur plus léger si le budget JS devient un problème
|
||||
|
||||
## 📦 Build pour production
|
||||
|
||||
|
||||
Generated
+9
@@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "gwada-sirius",
|
||||
"version": "2.0.0",
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@11ty/eleventy": "^3.1.6",
|
||||
"@11ty/eleventy-plugin-vite": "^8.0.0",
|
||||
@@ -2068,6 +2071,12 @@
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/leaflet": {
|
||||
"version": "1.9.4",
|
||||
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
|
||||
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
|
||||
@@ -19,5 +19,8 @@
|
||||
"gh-pages": "^6.1.1",
|
||||
"svelte": "^5.56.4",
|
||||
"vite": "^8.1.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"leaflet": "^1.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,117 +1,81 @@
|
||||
<script>
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import L from "leaflet";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
let { sites, locale } = $props();
|
||||
|
||||
let canvas = $state();
|
||||
let activeId = $state(null);
|
||||
let active = $derived(sites.find((s) => s.id === activeId) ?? null);
|
||||
let mapContainer = $state();
|
||||
let map;
|
||||
|
||||
let bounds = $derived.by(() => {
|
||||
const lats = sites.map((s) => s.lat);
|
||||
const lons = sites.map((s) => s.lon);
|
||||
return {
|
||||
minLat: Math.min(...lats) - 0.15,
|
||||
maxLat: Math.max(...lats) + 0.15,
|
||||
minLon: Math.min(...lons) - 0.15,
|
||||
maxLon: Math.max(...lons) + 0.15,
|
||||
};
|
||||
});
|
||||
|
||||
function project(site, width, height) {
|
||||
const x = ((site.lon - bounds.minLon) / (bounds.maxLon - bounds.minLon)) * width;
|
||||
const y = height - ((site.lat - bounds.minLat) / (bounds.maxLat - bounds.minLat)) * height;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
function draw() {
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext("2d");
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const width = canvas.clientWidth;
|
||||
const height = canvas.clientHeight;
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const styles = getComputedStyle(canvas);
|
||||
const gold = styles.getPropertyValue("--gold-surface").trim() || "#e8c766";
|
||||
const teal = styles.getPropertyValue("--caribbean-teal").trim() || "#3a9088";
|
||||
const ink = styles.getPropertyValue("--ink-soft").trim() || "#888";
|
||||
|
||||
sites.forEach((site) => {
|
||||
const { x, y } = project(site, width, height);
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, site.id === activeId ? 9 : 6, 0, Math.PI * 2);
|
||||
ctx.fillStyle = site.id === activeId ? teal : gold;
|
||||
ctx.fill();
|
||||
|
||||
ctx.font = "12px sans-serif";
|
||||
ctx.fillStyle = ink;
|
||||
ctx.textAlign = x > width * 0.7 ? "right" : "left";
|
||||
ctx.fillText(site.name, x + (x > width * 0.7 ? -12 : 12), y + 4);
|
||||
function markerIcon(active) {
|
||||
return L.divIcon({
|
||||
className: "site-marker" + (active ? " site-marker-active" : ""),
|
||||
iconSize: active ? [22, 22] : [16, 16],
|
||||
iconAnchor: active ? [11, 11] : [8, 8],
|
||||
});
|
||||
}
|
||||
|
||||
function pickSite(clientX, clientY) {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = clientX - rect.left;
|
||||
const y = clientY - rect.top;
|
||||
let closest = null;
|
||||
let closestDist = Infinity;
|
||||
sites.forEach((site) => {
|
||||
const p = project(site, rect.width, rect.height);
|
||||
const dist = Math.hypot(p.x - x, p.y - y);
|
||||
if (dist < closestDist) {
|
||||
closestDist = dist;
|
||||
closest = site;
|
||||
}
|
||||
});
|
||||
if (closest && closestDist < 40) {
|
||||
activeId = closest.id;
|
||||
draw();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
draw();
|
||||
const onResize = () => draw();
|
||||
window.addEventListener("resize", onResize);
|
||||
return () => window.removeEventListener("resize", onResize);
|
||||
map = L.map(mapContainer, {
|
||||
scrollWheelZoom: false,
|
||||
});
|
||||
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
maxZoom: 18,
|
||||
}).addTo(map);
|
||||
|
||||
const markers = sites.map((site) => {
|
||||
const marker = L.marker([site.lat, site.lon], { icon: markerIcon(false) }).addTo(map);
|
||||
const desc = site[`description_${locale}`] ?? site.description_fr;
|
||||
marker.bindPopup(
|
||||
`<strong>${site.name}</strong><br>${desc}<br><small>${site.lat}°N, ${site.lon}°O — ${site.alt} m</small>`
|
||||
);
|
||||
marker.on("popupopen", () => marker.setIcon(markerIcon(true)));
|
||||
marker.on("popupclose", () => marker.setIcon(markerIcon(false)));
|
||||
return marker;
|
||||
});
|
||||
|
||||
const group = L.featureGroup(markers);
|
||||
map.fitBounds(group.getBounds().pad(0.2));
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
map?.remove();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="observation-map">
|
||||
<canvas
|
||||
bind:this={canvas}
|
||||
class="island-canvas"
|
||||
aria-label="Carte schématique des sites d'observation, cliquez sur un point pour voir ses détails"
|
||||
tabindex="0"
|
||||
onclick={(e) => pickSite(e.clientX, e.clientY)}
|
||||
></canvas>
|
||||
|
||||
{#if active}
|
||||
<div class="card map-detail">
|
||||
<h3>{active.name}</h3>
|
||||
<p>{active[`description_${locale}`] ?? active.description_fr}</p>
|
||||
<p style="color: var(--ink-soft); font-size: var(--text-0)">{active.lat}°N, {active.lon}°O — {active.alt} m</p>
|
||||
</div>
|
||||
{/if}
|
||||
<div bind:this={mapContainer} class="leaflet-container-host" role="group" aria-label="Carte des sites d'observation de Sirius en Guadeloupe"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.observation-map {
|
||||
display: grid;
|
||||
gap: var(--space-3);
|
||||
.leaflet-container-host {
|
||||
width: 100%;
|
||||
height: 22rem;
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.island-canvas {
|
||||
cursor: pointer;
|
||||
:global(.site-marker) {
|
||||
background: var(--gold-surface);
|
||||
border: 2px solid oklch(20% 0.03 265);
|
||||
border-radius: 50%;
|
||||
box-shadow: var(--shadow-sm);
|
||||
}
|
||||
|
||||
.map-detail h3 {
|
||||
color: var(--gold);
|
||||
margin-bottom: var(--space-2);
|
||||
:global(.site-marker-active) {
|
||||
background: var(--caribbean-teal);
|
||||
}
|
||||
|
||||
:global(.leaflet-popup-content-wrapper) {
|
||||
background: var(--surface-raised);
|
||||
color: var(--ink);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
:global(.leaflet-popup-tip) {
|
||||
background: var(--surface-raised);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user