Khi xây dựng Bản đồ Xung đột Toàn cầu trong LacQuant, chúng tôi phải đối mặt với một câu hỏi không đơn giản: làm sao để một bản đồ Opensource thể hiện đúng lập trường chủ quyền của Việt Nam đối với Hoàng Sa và Trường Sa?
Bản đồ Xung đột LacQuant — layer Hoàng Sa & Trường Sa
Câu trả lời không nằm ở việc chọn đúng một thư viện hay một file GeoJSON. Nó nằm ở hàng loạt quyết định kỹ thuật nhỏ, mỗi quyết định đều có lý do rõ ràng.
Layer Hoàng Sa & Trường Sa — Bản đồ Xung đột LacQuant
Một trong 14 layer của Conflict Map, hiển thị 12 thực thể địa lý thuộc chủ quyền Việt Nam tại Biển Đông
12
Thực thể địa lý được đánh dấu
2
Quần đảo: Hoàng Sa & Trường Sa
3
Thực thể TQ chiếm đóng trái phép
Vấn đề với OSM tile chuẩn
OpenStreetMap tile (tile.openstreetmap.org) render tên theo ngôn ngữ của khu vực địa lý. Biển Đông và các đảo tranh chấp sẽ hiện tên Trung Quốc: 南海, 西沙群岛, 南沙群岛.
Đây không phải lỗi của OSM — cộng đồng OSM không có đồng thuận về cách gán tên ở các vùng tranh chấp, và render engine của họ chọn tên theo nguyên tắc "ngôn ngữ địa phương". Với Biển Đông, điều đó có nghĩa là tên Trung Quốc.
Ngoài ra, OSM tile chuẩn có nhiều label, màu sắc và chi tiết đường xá — phù hợp cho navigation nhưng tạo nền quá bận cho bản đồ phân tích địa chính trị. Các layer conflict, tension, military spots sẽ bị "chìm" vào nền.
Tại sao chọn CartoDB thay vì OSM thuần túy?
CartoDB (basemaps.cartocdn.com) vẫn dùng dữ liệu OpenStreetMap nhưng render lại thành tile tối giản với label tiếng Anh — trung lập hơn và phù hợp làm nền bản đồ phân tích. Không cần API key, hỗ trợ 4 subdomain (a/b/c/d) để browser tải song song, và có sẵn dark mode tile.
Conflict map — CartoDB dark tile với layer Hoàng Sa & Trường Sa
| Tiêu chí | CartoDB | OSM thuần túy |
|---|---|---|
| Label ngôn ngữ | Tiếng Anh (trung lập) | Theo khu vực địa lý |
| Nền thị giác | Tối giản, phù hợp overlay | Bận, nhiều chi tiết |
| Dark mode | dark_all tile sẵn có | Cần CSS filter |
| API key | Không cần | Không cần |
| Dữ liệu nguồn | OSM data, CartoDB render | OSM data, OSM render |
Attribution vẫn bắt buộc
CartoDB dùng dữ liệu OSM nên bạn vẫn phải giữ attribution cho cả OpenStreetMap contributors và CartoDB trong tile layer config.
Tại sao không dùng GeoJSON để vẽ ranh giới đảo?
Câu hỏi hợp lý tiếp theo: tại sao không vẽ polygon ranh giới từng đảo bằng GeoJSON?
Không có dữ liệu GeoJSON chất lượng cao và trung lập
Hoàng Sa và Trường Sa gồm hàng chục thực thể — đảo san hô, bãi đá, cồn cát, nhiều thực thể chỉ rộng vài trăm mét vuông và ngập khi thủy triều lên. Natural Earth thiếu nhiều thực thể nhỏ và tên thường thiên về tên Trung Quốc. OSM có dữ liệu nhưng không nhất quán và chưa có đồng thuận về metadata chủ quyền.
Zoom range làm polygon vô nghĩa
Bản đồ được giới hạn maxZoom: 6 — đây là công cụ phân tích vĩ mô, không phải bản đồ điều hướng. Ở zoom 4–6, một đảo rộng 1km² chỉ hiển thị khoảng 1–2 pixel. Polygon chi tiết ở zoom đó sẽ render thành một dot nhỏ không đọc được.
Dot marker + popup là lựa chọn phù hợp
Mục tiêu là annotation địa chính trị, không phải vẽ bản đồ địa lý. Dot marker truyền đạt thông tin rõ ràng hơn ở zoom tổng quan, đơn giản hơn để triển khai và bảo trì.
Giải pháp: Layer annotation tự xây dựng
Thay vì phụ thuộc vào dữ liệu có sẵn, chúng tôi xây dựng một layer riêng (vnIslandsLayer) với data model kiểm soát hoàn toàn:
type VNIsland = {
lat: number;
lng: number;
name_vi: string; // Tên tiếng Việt chính thức — luôn đứng trước
name_en: string; // Tên quốc tế — đứng sau
occupied: boolean; // true = đang bị Trung Quốc chiếm đóng trái phép
claimants: string;
note_vi: string;
note_en: string;
};
type VNIsland = {
lat: number;
lng: number;
name_vi: string; // Tên tiếng Việt chính thức — luôn đứng trước
name_en: string; // Tên quốc tế — đứng sau
occupied: boolean; // true = đang bị Trung Quốc chiếm đóng trái phép
claimants: string;
note_vi: string;
note_en: string;
};
Conflict map — layer Hoàng Sa & Trường Sa ở zoom khu vực
12 thực thể được đánh dấu, chia thành hai nhóm rõ ràng — Hoàng Sa (3 điểm, toàn bộ bị chiếm từ 1974) và Trường Sa (9 điểm: 6 Việt Nam kiểm soát + 3 Trung Quốc chiếm đóng: Đá Chữ Thập, Đá Vành Khăn, Đá Xu Bi).
Nguyên tắc trình bày: trung tính thị giác, rõ ràng nội dung
Tại sao chọn màu xám cho marker?
Layer dùng màu xám (#666666 light / #999999 dark) — không phải để né tránh lập trường, mà để không gây nhiễu với các layer khác (conflict đỏ, tension vàng, stable xanh lá). Tone xám trùng với label đất liền trên CartoDB tile, khiến layer đảo trông như một phần tự nhiên của base map.
Lập trường chủ quyền được thể hiện qua popup và badge, không qua màu sắc marker. Badge luôn song ngữ Việt-Anh:
const statusBadge = island.occupied
? `<span style="background:rgba(239,68,68,0.15);color:#ef4444">
Trung Quốc chiếm đóng trái phép · Vietnam sovereignty disputed
</span>`
: `<span style="background:rgba(16,185,129,0.12);color:#10b981">
Việt Nam kiểm soát · Vietnam-administered
</span>`;
const statusBadge = island.occupied
? `<span style="background:rgba(239,68,68,0.15);color:#ef4444">
Trung Quốc chiếm đóng trái phép · Vietnam sovereignty disputed
</span>`
: `<span style="background:rgba(16,185,129,0.12);color:#10b981">
Việt Nam kiểm soát · Vietnam-administered
</span>`;
Các chi tiết kỹ thuật đáng chú ý
Zoom-aware visibility
< 4
Layer ẩn
Zoom toàn cầu, Biển Đông quá nhỏ
≥ 4
Layer hiện
Biển Đông chiếm ~1/3 màn hình
map.on("zoomend", () => {
const show = map.getZoom() >= 4;
for (const m of vnAllMarkers) {
const el = m.getElement();
if (el) el.style.display = show ? "" : "none";
}
});
map.on("zoomend", () => {
const show = map.getZoom() >= 4;
for (const m of vnAllMarkers) {
const el = m.getElement();
if (el) el.style.display = show ? "" : "none";
}
});
Dark mode tự động
Khi data-theme="dark" trên <html>, bản đồ tự động switch sang dark_all tile qua MutationObserver:
const observer = new MutationObserver(() => {
tile.setUrl(isDarkMode() ? TILE_DARK : TILE_LIGHT);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
const observer = new MutationObserver(() => {
tile.setUrl(isDarkMode() ? TILE_DARK : TILE_LIGHT);
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
L.divIcon thay vì L.circleMarker
Mỗi đảo được vẽ bằng L.divIcon (HTML div) thay vì L.circleMarker. Lý do: dễ style bằng CSS custom properties, không bị ảnh hưởng bởi Webpack icon path issue trong Next.js, và nhẹ hơn SVG.
Translation override cho tên địa danh
const TRANSLATION_OVERRIDES = {
"paracel islands": { vi: "Quần đảo Hoàng Sa", ja: "西沙諸島" },
"spratly islands": { vi: "Quần đảo Trường Sa", ja: "南沙諸島" },
};
const TRANSLATION_OVERRIDES = {
"paracel islands": { vi: "Quần đảo Hoàng Sa", ja: "西沙諸島" },
"spratly islands": { vi: "Quần đảo Trường Sa", ja: "南沙諸島" },
};
Override này đảm bảo dù content API trả về tên tiếng Anh, các thuật ngữ địa lý quan trọng sẽ luôn được dịch đúng — không phụ thuộc vào MyMemory API.
Bài học chính
Đừng tin tưởng dữ liệu có sẵn về những vùng tranh chấp. Nếu bạn cần truyền đạt lập trường chủ quyền rõ ràng, hãy kiểm soát dữ liệu của bạn — tên gọi, mô tả, trạng thái — và kiểm soát hoàn toàn cách nó được render.
Vấn đề "chủ quyền trên bản đồ Opensource" không có một giải pháp duy nhất. Nhưng có một nguyên tắc chỉ đường: về mặt kỹ thuật, điều đó không khó — về mặt lập trường, nó cần rõ ràng ngay từ đầu.