const tabsContainer = document.getElementById("tabs"); const flagsContainer = document.getElementById("flags"); const searchInput = document.getElementById("search"); const noResultEl = document.getElementById("no-result"); const mapTitleEl = document.getElementById("map-title"); const mapCoordEl = document.getElementById("map-coord"); let regionGroups = {}; let activeRegion = "Asia"; let currentFilter = ""; let map = null; let marker = null; let polygonsByCode = {}; let overlayLayer = null; let popupPinned = false; function formatCoord(lat, lng) { return `座標:${lat.toFixed(4)}, ${lng.toFixed(4)}`; } function normalizeLatLng(value) { if (!Array.isArray(value)) return null; const flattened = value.flat ? value.flat(Infinity) : flattenArray(value); const coords = flattened.filter(num => typeof num === "number" && Number.isFinite(num)); if (coords.length < 2) return null; const pickPair = (a, b) => { let lat = a; let lng = b; if (Math.abs(lat) > 90 && Math.abs(lng) <= 90) { [lat, lng] = [lng, lat]; } if (Math.abs(lat) <= 90 && Math.abs(lng) <= 180) { return [lat, lng]; } return null; }; let pair = pickPair(coords[0], coords[1]); if (pair) return pair; for (let i = 2; i < coords.length - 1; i++) { pair = pickPair(coords[i], coords[i + 1]); if (pair) return pair; } return null; } function flattenArray(arr) { const out = []; arr.forEach(item => { if (Array.isArray(item)) { out.push(...flattenArray(item)); } else { out.push(item); } }); return out; } function buildPopupContent(item) { const codeLabel = `${item.code.toUpperCase()} ${item.nameZh}`; const flagUrl = `flags/${item.code}.svg`; return ` `; } function attachMarkerInteractions(markerInstance) { markerInstance.on("mouseover", () => { if (!popupPinned) { markerInstance.openPopup(); } }); markerInstance.on("mouseout", () => { if (!popupPinned) { markerInstance.closePopup(); } }); markerInstance.on("click", () => { popupPinned = !popupPinned; if (popupPinned) { markerInstance.openPopup(); } else { markerInstance.closePopup(); } }); } function initMap(defaultLatLng) { const center = defaultLatLng || [23.6978, 120.9605]; // 台灣大致中心 map = L.map("map").setView(center, 4); L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 18, attribution: "© OpenStreetMap contributors" }).addTo(map); // 初始 marker marker = L.marker(center).addTo(map); attachMarkerInteractions(marker); marker.bindPopup("請點選上方任一國旗卡片"); mapTitleEl.textContent = "地圖:尚未選擇國家"; mapCoordEl.textContent = ""; } function focusCountryOnMap(item) { if (!map) return; const coords = normalizeLatLng(item.latlng); if (!coords) return; const [lat, lng] = coords; item.latlng = coords; // 先清掉舊的 overlay(polygon / circle) if (overlayLayer) { map.removeLayer(overlayLayer); overlayLayer = null; } const feat = polygonsByCode[item.code]; if (feat) { // 有 polygon 的正常畫輪廓 overlayLayer = L.geoJSON(feat, { style: { color: "#ff6600", weight: 2, fillOpacity: 0.12 } }).addTo(map); const bounds = overlayLayer.getBounds(); if (bounds.isValid()) { map.fitBounds(bounds, { padding: [12, 12] }); } else { map.setView([lat, lng], 5); } } else if (item.code === "tw") { // dataset 仍缺台灣 polygon 時,使用大圓圈當 fallback overlayLayer = L.circle([lat, lng], { radius: 250000, // 250km 左右,把台灣圈起來 color: "#ff6600", weight: 2, fillOpacity: 0.12 }).addTo(map); map.fitBounds(overlayLayer.getBounds(), { padding: [12, 12] }); } else { // 其他沒有 polygon 的國家,就只用中心點 map.setView([lat, lng], 5); } // Marker 移動到該國家位置並顯示國旗 popup if (marker) { marker.setLatLng([lat, lng]); } else { marker = L.marker([lat, lng]).addTo(map); attachMarkerInteractions(marker); } marker.bindPopup(buildPopupContent(item)); popupPinned = false; marker.closePopup(); mapTitleEl.textContent = `地圖:${item.code.toUpperCase()} - ${item.nameZh} / ${item.nameEn}`; mapCoordEl.textContent = formatCoord(lat, lng); } async function loadCountryMeta() { const map = {}; try { const url = "https://restcountries.com/v3.1/alpha?codes=" + countryCodes.map(c => c.toUpperCase()).join(",") + "&fields=cca2,name,region,subregion,latlng"; const resp = await fetch(url); if (!resp.ok) { console.warn("無法從 restcountries 取得資料,status =", resp.status); return map; } const data = await resp.json(); data.forEach(item => { if (!item.cca2) return; const code = item.cca2.toLowerCase(); const nameEn = item.name?.common || code.toUpperCase(); const nameZh = CUSTOM_ZH[code] || nameEn; const latlng = normalizeLatLng(item.latlng); map[code] = { nameEn, nameZh, region: item.region || "Other", subregion: item.subregion || "", latlng }; }); } catch (e) { console.error("取得國家資訊失敗:", e); } return map; } // 讀取世界國家 GeoJSON,依 ISO3166-1 Alpha-2 建索引 async function loadTaiwanPolygon() { try { const resp = await fetch("https://raw.githubusercontent.com/g0v/twgeojson/master/json/twCounty2010.geo.json"); if (!resp.ok) { console.warn("無法取得台灣 polygon 資料,status =", resp.status); return null; } const gj = await resp.json(); if (Array.isArray(gj.features) && gj.features.length > 0) { const polygons = []; gj.features.forEach(feature => { const geom = feature.geometry; if (!geom || !geom.coordinates) return; if (geom.type === "Polygon") { polygons.push(geom.coordinates); } else if (geom.type === "MultiPolygon") { geom.coordinates.forEach(poly => polygons.push(poly)); } }); if (polygons.length === 0) return null; return { type: "Feature", properties: { ISO_A2: "TW" }, geometry: { type: "MultiPolygon", coordinates: polygons } }; } } catch (e) { console.error("取得台灣 polygon 失敗:", e); } return null; } async function loadCountryPolygons() { const result = {}; try { const url = "https://raw.githubusercontent.com/datasets/geo-countries/master/data/countries.geojson"; const resp = await fetch(url); if (!resp.ok) { console.warn("無法取得國家 polygon 資料,status =", resp.status); } else { const gj = await resp.json(); if (Array.isArray(gj.features)) { gj.features.forEach(feat => { const props = feat.properties || {}; const iso2 = props["ISO_A2"] || props["ISO3166-1-Alpha-2"] || props.iso2; if (iso2 && iso2 !== "-99") { result[iso2.toLowerCase()] = feat; } }); } } } catch (e) { console.error("取得國家 polygon 失敗:", e); } if (!result.tw) { const twFeat = await loadTaiwanPolygon(); if (twFeat) { result.tw = twFeat; } } return result; } function buildRegionGroups(metaMap) { const groups = {}; REGION_ORDER.forEach(r => (groups[r] = [])); countryCodes.forEach(code => { const meta = metaMap[code] || { nameEn: code.toUpperCase(), nameZh: CUSTOM_ZH[code] || code.toUpperCase(), region: "Other", subregion: "", latlng: null }; const region = REGION_ORDER.includes(meta.region) ? meta.region : "Other"; groups[region].push({ code, nameEn: meta.nameEn, nameZh: meta.nameZh, region, subregion: meta.subregion, latlng: normalizeLatLng(meta.latlng) }); }); Object.values(groups).forEach(list => { list.sort((a, b) => a.nameEn.localeCompare(b.nameEn, "en")); }); return groups; } function renderTabs() { tabsContainer.innerHTML = ""; const regions = REGION_ORDER.filter(r => regionGroups[r] && regionGroups[r].length > 0); regions.forEach(region => { const btn = document.createElement("button"); btn.className = "tab" + (region === activeRegion ? " active" : ""); btn.textContent = REGION_LABELS[region] || region; btn.dataset.region = region; btn.addEventListener("click", () => { if (activeRegion === region) return; activeRegion = region; document.querySelectorAll(".tab").forEach(t => t.classList.remove("active")); btn.classList.add("active"); renderFlagsForRegion(region); }); tabsContainer.appendChild(btn); }); } function getFilteredList(region) { const list = regionGroups[region] || []; const q = currentFilter.trim().toLowerCase(); if (!q) return list; return list.filter(item => { const codeMatch = item.code.toLowerCase().includes(q); const enMatch = item.nameEn.toLowerCase().includes(q); const zhMatch = (item.nameZh || "").toLowerCase().includes(q); return codeMatch || enMatch || zhMatch; }); } function renderFlagsForRegion(region) { const list = getFilteredList(region); flagsContainer.innerHTML = ""; if (list.length === 0) { noResultEl.style.display = "block"; return; } else { noResultEl.style.display = "none"; } list.forEach(item => { const card = document.createElement("div"); card.className = "flag-card"; const img = document.createElement("img"); img.src = "flags/" + item.code + ".svg"; img.alt = item.nameEn + " (" + item.code.toUpperCase() + ")"; img.loading = "lazy"; img.onerror = () => { img.style.opacity = "0.3"; const err = document.createElement("div"); err.className = "error"; err.textContent = "找不到圖片檔:" + item.code + ".svg"; card.appendChild(err); }; const meta = document.createElement("div"); meta.className = "meta"; const line1 = document.createElement("div"); const codeSpan = document.createElement("span"); codeSpan.className = "code"; codeSpan.textContent = item.code.toUpperCase(); const nameZhSpan = document.createElement("span"); nameZhSpan.className = "name-zh"; nameZhSpan.textContent = " " + item.nameZh; line1.appendChild(codeSpan); line1.appendChild(nameZhSpan); const nameEnSpan = document.createElement("span"); nameEnSpan.className = "name-en"; nameEnSpan.textContent = item.nameEn; meta.appendChild(line1); meta.appendChild(nameEnSpan); if (item.subregion) { const sub = document.createElement("span"); sub.className = "subregion"; sub.textContent = item.subregion; meta.appendChild(sub); } card.appendChild(img); card.appendChild(meta); // 點國旗 → 地圖移到該國家 + 顯示邊界 card.addEventListener("click", () => { focusCountryOnMap(item); }); flagsContainer.appendChild(card); }); } (async function init() { // 同時抓國家基本資料 + polygon const [metaMap, polyMap] = await Promise.all([ loadCountryMeta(), loadCountryPolygons() ]); regionGroups = buildRegionGroups(metaMap); polygonsByCode = polyMap; // Leaflet 地圖初始化(優先用台灣) const twMeta = metaMap["tw"]; const defaultLatLng = normalizeLatLng(twMeta?.latlng) || [23.6978, 120.9605]; initMap(defaultLatLng); if (!regionGroups[activeRegion] || regionGroups[activeRegion].length === 0) { const firstRegion = REGION_ORDER.find(r => regionGroups[r] && regionGroups[r].length > 0); if (firstRegion) activeRegion = firstRegion; } renderTabs(); renderFlagsForRegion(activeRegion); searchInput.addEventListener("input", () => { currentFilter = searchInput.value || ""; renderFlagsForRegion(activeRegion); }); })();