Files
42_104/caddy/www/qiji-luopan/app.js
2026-06-23 19:33:50 +08:00

444 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 `
<div class="popup-flag">
<img src="${flagUrl}" alt="${codeLabel}" />
<div class="popup-code">${codeLabel}</div>
<div class="popup-name">${item.nameEn}</div>
</div>
`;
}
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: "&copy; 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;
// 先清掉舊的 overlaypolygon / 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);
});
})();