From 1e722252e23905b4a9c27af68d3fddb86f24e750 Mon Sep 17 00:00:00 2001 From: sHa Date: Thu, 14 Aug 2025 02:05:49 +0300 Subject: [PATCH] Enhance CountryMap component with zoom and recenter functionality, and add map controls for improved user interaction --- src/components/CardFull.svelte | 7 +- src/components/CountryMap.svelte | 163 +++++++++++++++++++++++++++++-- 2 files changed, 159 insertions(+), 11 deletions(-) diff --git a/src/components/CardFull.svelte b/src/components/CardFull.svelte index e9943ed..691e0cf 100644 --- a/src/components/CardFull.svelte +++ b/src/components/CardFull.svelte @@ -165,7 +165,11 @@
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]} - + {/if}
@@ -510,5 +514,4 @@ .logo-details span { color: var(--color-text); } - diff --git a/src/components/CountryMap.svelte b/src/components/CountryMap.svelte index be93f5f..e65886e 100644 --- a/src/components/CountryMap.svelte +++ b/src/components/CountryMap.svelte @@ -4,8 +4,40 @@ export let countryCodes = []; export let countryNames = []; export let mapPath = "/data/world.svg"; + export let countryScale = false; + export let scalePadding = 30; let wrapperRef; + // --- Zoom and recenter logic --- + let lastViewBox = null; + function getSvgEl() { + return wrapperRef ? wrapperRef.querySelector("svg") : null; + } + function zoom(factor) { + const svgEl = getSvgEl(); + if (!svgEl) return; + const vb = svgEl.getAttribute("viewBox"); + if (!vb) return; + let [x, y, w, h] = vb.split(" ").map(Number); + const cx = x + w / 2; + const cy = y + h / 2; + w /= factor; + h /= factor; + x = cx - w / 2; + y = cy - h / 2; + svgEl.setAttribute("viewBox", `${x} ${y} ${w} ${h}`); + lastViewBox = `${x} ${y} ${w} ${h}`; + } + function zoomIn() { + zoom(1.2); + } + function zoomOut() { + zoom(0.8); + } + function recenter() { + highlightCountries(); + } + // Highlight countries after SVG loads, using MutationObserver for reliability let observer; function highlightCountries() { @@ -13,15 +45,17 @@ const svgEl = wrapperRef.querySelector("svg"); if (!svgEl) return; // Highlight by country code (id) + let highlighted = []; countryCodes.forEach((code) => { const countryPath = svgEl.querySelector(`#${code}`); if (countryPath) { countryPath.setAttribute("fill", "#4f8cff"); countryPath.setAttribute("stroke", "#222"); countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)"; + highlighted.push(countryPath); } }); - // Highlight by country name (name attribute) + // Highlight by country name (name attribute or class) const names = Array.isArray(countryNames) ? countryNames : countryNames @@ -37,8 +71,89 @@ countryPath.setAttribute("fill", "#4f8cff"); countryPath.setAttribute("stroke", "#222"); countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)"; + highlighted.push(countryPath); }); }); + // Smart scale/center if enabled and at least one country is highlighted + if (countryScale && highlighted.length > 0) { + // Compute bounding box of all highlighted paths + let minX = Infinity, + minY = Infinity, + maxX = -Infinity, + maxY = -Infinity; + highlighted.forEach((path) => { + let bbox; + try { + bbox = path.getBBox(); + } catch (e) { + return; + } + if (bbox.x < minX) minX = bbox.x; + if (bbox.y < minY) minY = bbox.y; + if (bbox.x + bbox.width > maxX) maxX = bbox.x + bbox.width; + if (bbox.y + bbox.height > maxY) maxY = bbox.y + bbox.height; + }); + // Center and scale the SVG viewBox to the highlighted country + if ( + isFinite(minX) && + isFinite(minY) && + isFinite(maxX) && + isFinite(maxY) + ) { + minX -= scalePadding; + minY -= scalePadding; + maxX += scalePadding; + maxY += scalePadding; + const width = maxX - minX; + const height = maxY - minY; + svgEl.setAttribute( + "viewBox", + `${minX} ${minY} ${width} ${height}`, + ); + } + } + } + // --- Map drag/move by mouse --- + let isDragging = false; + let dragStart = { x: 0, y: 0 }; + let viewBoxStart = null; + + function onMouseDown(e) { + const svgEl = wrapperRef.querySelector("svg"); + if (!svgEl) return; + isDragging = true; + dragStart = { x: e.clientX, y: e.clientY }; + const vb = svgEl.getAttribute("viewBox"); + if (vb) { + const [x, y, w, h] = vb.split(" ").map(Number); + viewBoxStart = { x, y, w, h }; + } + window.addEventListener("mousemove", onMouseMove); + window.addEventListener("mouseup", onMouseUp); + } + + function onMouseMove(e) { + if (!isDragging || !viewBoxStart) return; + const svgEl = wrapperRef.querySelector("svg"); + if (!svgEl) return; + const dx = e.clientX - dragStart.x; + const dy = e.clientY - dragStart.y; + // Scale drag to SVG units + const scaleX = viewBoxStart.w / wrapperRef.offsetWidth; + const scaleY = viewBoxStart.h / wrapperRef.offsetHeight; + const newX = viewBoxStart.x - dx * scaleX; + const newY = viewBoxStart.y - dy * scaleY; + svgEl.setAttribute( + "viewBox", + `${newX} ${newY} ${viewBoxStart.w} ${viewBoxStart.h}`, + ); + } + + function onMouseUp() { + isDragging = false; + viewBoxStart = null; + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("mouseup", onMouseUp); } function observeSvg() { @@ -65,8 +180,15 @@
-
+
+ {#if countryScale} +
+ + + +
+ {/if}
@@ -77,12 +199,35 @@ padding: 1rem; box-shadow: 0 2px 8px 2px rgba(0, 0, 0, 0.08); } - .svg-wrapper { - width: 100%; - height: 180px; - margin: 0 auto; - background: var(--color-bg); - border-radius: 8px; - box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04); + + .map-controls-on-map { + position: absolute; + right: 0.5em; + bottom: 0.5em; + display: flex; + flex-direction: column; + gap: 0.5em; + z-index: 10; + } + .zoom-btn-on-map { + background: var(--color-accent, #222); + color: var(--color-bg, #4f8cff); + border: 1px solid var(--color-border, #222); + border-radius: 50%; + width: 20px; + height: 20px; + font-size: 1em; + cursor: pointer; + box-shadow: 0 2px 8px 2px rgba(0,0,0,0.08); + transition: background 0.2s, color 0.2s; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + padding: 0; + } + .zoom-btn-on-map:hover { + background: var(--color-bg, #4f8cff); + color: var(--color-text, #fff); }