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);
});
})();