{{ if eq $lang "de" }}Interaktive Weltkarte: Kinderschutzgesetze{{ else if eq $lang "fr" }}Carte interactive : lois de protection de l'enfance{{ else }}Interactive Map: Child Protection Laws{{ end }}
{{ if eq $lang "de" }}Klicken Sie auf ein Land für Details{{ else if eq $lang "fr" }}Cliquez sur un pays pour plus de détails{{ else }}Click a country for details{{ end }}
-
+
@@ -68,187 +66,262 @@
var byIsoNum = {};
countries.forEach(function(c) { byIsoNum[c.isoNum] = c; });
- // Map dimensions (Equirectangular projection)
var W = 960, H = 480;
- var svg = document.getElementById('ifk-map-svg');
+ var canvas = document.getElementById('ifk-map-canvas');
var container = document.getElementById('ifk-map-container');
var popup = document.getElementById('ifk-map-popup');
var popupContent = document.getElementById('ifk-popup-content');
+ var dpr = window.devicePixelRatio || 1;
// ViewBox state
var vb = { x: 0, y: 0, w: W, h: H };
var VB_INIT = { x: 0, y: 0, w: W, h: H };
- var MIN_W = W / 16; // max zoom in
+ var MIN_W = W / 16;
- function setViewBox() {
- svg.setAttribute('viewBox', vb.x + ' ' + vb.y + ' ' + vb.w + ' ' + vb.h);
+ // Sizing: match exact device pixels to prevent GPU scaling
+ function sizeCanvas() {
+ var rect = container.getBoundingClientRect();
+ canvas.width = Math.round(rect.width * dpr);
+ canvas.height = Math.round(rect.height * dpr);
+ canvas.style.width = rect.width + 'px';
+ canvas.style.height = rect.height + 'px';
}
- // Equirectangular projection
function projectCoord(lon, lat) {
return [(lon + 180) * (W / 360), (90 - lat) * (H / 180)];
}
- function projectRing(ring) {
- var d = '';
- for (var i = 0; i < ring.length; i++) {
- var p = projectCoord(ring[i][0], ring[i][1]);
- d += (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ',' + p[1].toFixed(1);
- }
- return d + 'Z';
+ // Pre-computed paths for hit testing
+ var countryPaths = [];
+ var geoData = null;
+
+ function drawMap() {
+ var ctx = canvas.getContext('2d');
+ var cw = canvas.width, ch = canvas.height;
+ // Transform from viewBox to canvas
+ var sx = cw / vb.w, sy = ch / vb.h;
+
+ ctx.clearRect(0, 0, cw, ch);
+
+ // Ocean background
+ ctx.fillStyle = '#c8d3df';
+ ctx.fillRect(0, 0, cw, ch);
+
+ if (!geoData) return;
+
+ // Draw land base fill
+ ctx.fillStyle = '#ffffff';
+ drawGeometry(ctx, geoData.land, sx, sy, true);
+
+ // Draw country fills
+ geoData.features.forEach(function(f) {
+ var id = String(f.id).padStart(3, '0');
+ var c = byIsoNum[id];
+ if (c) {
+ ctx.globalAlpha = 0.65;
+ ctx.fillStyle = STATUS_COLORS[c.status];
+ } else {
+ ctx.globalAlpha = 1;
+ ctx.fillStyle = '#ffffff';
+ }
+ drawGeometry(ctx, f.geometry, sx, sy, true);
+ });
+
+ ctx.globalAlpha = 1;
+
+ // Draw borders
+ ctx.strokeStyle = '#94a3b8';
+ ctx.lineWidth = 0.5 * sx / (cw / vb.w);
+ drawGeometry(ctx, geoData.borders, sx, sy, false);
}
- function projectGeometry(geom) {
- var d = '';
+ function drawGeometry(ctx, geom, sx, sy, fill) {
+ ctx.beginPath();
+ var coords;
if (geom.type === 'Polygon') {
- geom.coordinates.forEach(function(ring) { d += projectRing(ring); });
+ geom.coordinates.forEach(function(ring) { traceRing(ctx, ring, sx, sy); });
} else if (geom.type === 'MultiPolygon') {
geom.coordinates.forEach(function(poly) {
- poly.forEach(function(ring) { d += projectRing(ring); });
+ poly.forEach(function(ring) { traceRing(ctx, ring, sx, sy); });
});
+ } else if (geom.type === 'MultiLineString') {
+ geom.coordinates.forEach(function(line) { traceLine(ctx, line, sx, sy); });
+ } else if (geom.type === 'LineString') {
+ traceLine(ctx, geom.coordinates, sx, sy);
}
- return d;
+ if (fill) ctx.fill('evenodd');
+ else ctx.stroke();
}
- // Build SVG
+ function traceRing(ctx, ring, sx, sy) {
+ for (var i = 0; i < ring.length; i++) {
+ var p = projectCoord(ring[i][0], ring[i][1]);
+ var x = (p[0] - vb.x) * sx;
+ var y = (p[1] - vb.y) * sy;
+ if (i === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ }
+ ctx.closePath();
+ }
+
+ function traceLine(ctx, line, sx, sy) {
+ for (var i = 0; i < line.length; i++) {
+ var p = projectCoord(line[i][0], line[i][1]);
+ var x = (p[0] - vb.x) * sx;
+ var y = (p[1] - vb.y) * sy;
+ if (i === 0) ctx.moveTo(x, y);
+ else ctx.lineTo(x, y);
+ }
+ }
+
+ // Load data
fetch('{{ "data/countries-50m.json" | relURL }}')
.then(function(r) { return r.json(); })
.then(function(topo) {
var geo = topojson.feature(topo, topo.objects.countries);
var borders = topojson.mesh(topo, topo.objects.countries, function(a, b) { return a !== b; });
-
- // Land base fill (covers gaps at poles between country polygons)
var land = topojson.feature(topo, topo.objects.land);
- var landPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- landPath.setAttribute('d', projectGeometry(land.features ? land.features[0].geometry : land.geometry));
- landPath.setAttribute('fill', '#ffffff');
- landPath.setAttribute('stroke', 'none');
- landPath.setAttribute('pointer-events', 'none');
- svg.appendChild(landPath);
+ var landGeom = land.features ? land.features[0].geometry : land.geometry;
- // Country fills (ocean is CSS background on container)
+ geoData = { features: geo.features, borders: borders, land: landGeom };
+
+ // Pre-build hit-test data
geo.features.forEach(function(f) {
var id = String(f.id).padStart(3, '0');
var c = byIsoNum[id];
- var path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- var d = projectGeometry(f.geometry);
- if (!d) return;
- path.setAttribute('d', d);
- path.setAttribute('fill', c ? STATUS_COLORS[c.status] : '#ffffff');
- path.setAttribute('fill-opacity', c ? '0.65' : '1');
- path.setAttribute('class', 'country' + (c ? ' has-data' : ''));
- path.setAttribute('data-iso', id);
- svg.appendChild(path);
+ if (c) countryPaths.push({ id: id, geometry: f.geometry, data: c });
});
- // Borders (shared edges only)
- var bordersD = projectGeometry(borders);
- if (bordersD) {
- var borderPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
- borderPath.setAttribute('d', bordersD);
- borderPath.setAttribute('fill', 'none');
- borderPath.setAttribute('stroke', '#94a3b8');
- borderPath.setAttribute('stroke-width', '0.3');
- borderPath.setAttribute('pointer-events', 'none');
- svg.appendChild(borderPath);
- }
-
- setViewBox();
-
- // Click handler
- svg.addEventListener('click', function(e) {
- var target = e.target;
- if (!target.classList.contains('has-data')) { popup.style.display = 'none'; return; }
- var id = target.getAttribute('data-iso');
- var c = byIsoNum[id];
- if (!c) return;
-
- var loc = c[LANG] || c.en;
- var labels = STATUS_LABELS[LANG] || STATUS_LABELS.en;
- var ageLabel = AGE_LABELS[LANG] || AGE_LABELS.en;
- var html = '' + c.flag + ' ' + loc.name + ' ' +
- '' + loc.law + ' ' +
- '' + labels[c.status] + ' (' + c.year + ')' +
- '