+
+ );
+}
diff --git a/apps/geolibre-desktop/src/components/layout/add-data/types.ts b/apps/geolibre-desktop/src/components/layout/add-data/types.ts
index c2e985d085..a7d809ab91 100644
--- a/apps/geolibre-desktop/src/components/layout/add-data/types.ts
+++ b/apps/geolibre-desktop/src/components/layout/add-data/types.ts
@@ -24,7 +24,8 @@ export type AddDataKind =
| "deckgl-viz"
| "video"
| "cesium-ion"
- | "czml";
+ | "czml"
+ | "kml";
/** A data source loadable either from a remote URL or a local file. */
export type FeedMode = "url" | "file";
diff --git a/apps/geolibre-desktop/src/components/layout/toolbar/AddDataMenu.tsx b/apps/geolibre-desktop/src/components/layout/toolbar/AddDataMenu.tsx
index a2e98c442e..37b96a126d 100644
--- a/apps/geolibre-desktop/src/components/layout/toolbar/AddDataMenu.tsx
+++ b/apps/geolibre-desktop/src/components/layout/toolbar/AddDataMenu.tsx
@@ -104,6 +104,7 @@ export function AddDataMenu({
"cesium-ion": { onSelect: () => onSetAddDataKind("cesium-ion"), disabled: !cesiumPrimary },
// CZML dynamic 3D scenes load through Cesium only (issue #2290).
czml: { onSelect: () => onSetAddDataKind("czml"), disabled: !cesiumPrimary },
+ kml: { onSelect: () => onSetAddDataKind("kml"), disabled: !cesiumPrimary },
// The glTF model opens the same deck.gl scenegraph builder, so it is
// gated the way "deckgl-viz" is.
"gltf-model": { onSelect: onAddGltfModel, disabled: !capabilities.customLayers },
diff --git a/apps/geolibre-desktop/src/components/panels/LayerPanel.tsx b/apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
index 74af0ecc5d..538ebef7cf 100644
--- a/apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
+++ b/apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
@@ -3551,9 +3551,16 @@ export function LayerPanel({
{layerTypeLabel(layer, t)}
- {isPlaceholderLayer(layer) && (
-
{placeholderMessage(layer)}
- )}
+ {/* Placeholder detection checks MapLibre source ids, which the
+ globe's own layers never create. Suppress it only for the
+ kinds Cesium actually draws — a kind it cannot draw (e.g.
+ duckdb-query) keeps its message while the globe is primary. */}
+ {(!cesiumPrimary || !isCesiumSupportedLayerType(layer)) &&
+ isPlaceholderLayer(layer) && (
+
);
diff --git a/apps/geolibre-desktop/src/i18n/locales/ar.json b/apps/geolibre-desktop/src/i18n/locales/ar.json
index 37e2a50549..a2208a708c 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ar.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ar.json
@@ -355,6 +355,10 @@
"polyline": {
"label": "إضافة طبقة خط متعدد مشفر",
"description": "استيراد سلاسل الخطوط المتعددة المشفرة (Google وOSRM وValhalla) من نص أو ملفات كخطوط متجهة."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "حمّل KML أو KMZ مع الأنماط والتراكبات وروابط الشبكة الأصلية على الكرة الأرضية."
}
},
"shared": {
@@ -420,6 +424,11 @@
"importedWithDropped_many": "تم استيراد {{count}} خدمةً (تم تخطي {{dropped}} — المكتبة ممتلئة).",
"importedWithDropped_other": "تم استيراد {{count}} خدمة (تم تخطي {{dropped}} — المكتبة ممتلئة)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "عنوان URL لملف KML / KMZ",
+ "errorSource": "أدخل رابط KML/KMZ أو اختر ملفًا."
+ },
"xyz": {
"defaultName": "طبقة XYZ",
"sampleLabel": "مرئيات USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/de.json b/apps/geolibre-desktop/src/i18n/locales/de.json
index 35cef7e084..dd83ae3bd5 100644
--- a/apps/geolibre-desktop/src/i18n/locales/de.json
+++ b/apps/geolibre-desktop/src/i18n/locales/de.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Kodierte Polyline-Ebene hinzufügen",
"description": "Kodierte Polyline-Zeichenfolgen (Google, OSRM, Valhalla) aus Text oder Dateien als Vektorlinien importieren."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "KML oder KMZ mit nativen Stilen, Overlays und Netzwerklinks auf dem Globus laden."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} Dienst importiert ({{dropped}} übersprungen — Bibliothek voll).",
"importedWithDropped_other": "{{count}} Dienste importiert ({{dropped}} übersprungen — Bibliothek voll)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML-/KMZ-URL",
+ "errorSource": "Geben Sie eine KML/KMZ-URL ein oder wählen Sie eine Datei."
+ },
"xyz": {
"defaultName": "XYZ-Ebene",
"sampleLabel": "USGS-Luftbilder",
diff --git a/apps/geolibre-desktop/src/i18n/locales/en.json b/apps/geolibre-desktop/src/i18n/locales/en.json
index 7092209e81..9c0fc0dc3f 100644
--- a/apps/geolibre-desktop/src/i18n/locales/en.json
+++ b/apps/geolibre-desktop/src/i18n/locales/en.json
@@ -355,6 +355,10 @@
"czml": {
"label": "Add CZML Dynamic 3D Layer",
"description": "Load a dynamic Cesium Language (CZML) document describing orbits, trajectories, vehicle paths, and time-varying 3D scenes on the globe."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Load KML or KMZ with native globe styling, overlays, and network links."
}
},
"shared": {
@@ -438,6 +442,11 @@
"errorInvalidCzml": "The file does not contain a valid CZML document (expected JSON array or packet object).",
"readError": "Failed to read CZML data."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL",
+ "errorSource": "Enter a KML/KMZ URL or choose a file."
+ },
"xyz": {
"defaultName": "XYZ Layer",
"sampleLabel": "USGS imagery",
diff --git a/apps/geolibre-desktop/src/i18n/locales/es.json b/apps/geolibre-desktop/src/i18n/locales/es.json
index 4f2790b875..5166240455 100644
--- a/apps/geolibre-desktop/src/i18n/locales/es.json
+++ b/apps/geolibre-desktop/src/i18n/locales/es.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Añadir capa de polilínea codificada",
"description": "Importar cadenas de polilínea codificada (Google, OSRM, Valhalla) desde texto o archivos como líneas vectoriales."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Carga KML o KMZ con estilos nativos, superposiciones y enlaces de red en el globo."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "Se importó {{count}} servicio ({{dropped}} omitido: biblioteca llena).",
"importedWithDropped_other": "Se importaron {{count}} servicios ({{dropped}} omitidos: biblioteca llena)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL de KML / KMZ",
+ "errorSource": "Introduce una URL KML/KMZ o elige un archivo."
+ },
"xyz": {
"defaultName": "Capa XYZ",
"sampleLabel": "Imágenes de USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/fa.json b/apps/geolibre-desktop/src/i18n/locales/fa.json
index a9ab6982c3..a42a16f5d0 100644
--- a/apps/geolibre-desktop/src/i18n/locales/fa.json
+++ b/apps/geolibre-desktop/src/i18n/locales/fa.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "افزودن لایهٔ چندخطی کدگذاریشده",
"description": "وارد کردن رشتههای چندخطی کدگذاریشده (Google، OSRM، Valhalla) از متن یا فایلها بهصورت خطوط برداری."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "KML یا KMZ را با سبکهای بومی، همپوشانیها و پیوندهای شبکه روی کره بارگذاری کنید."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} سرویس وارد شد ({{dropped}} مورد رد شد — کتابخانه پر است).",
"importedWithDropped_other": "{{count}} سرویس وارد شد ({{dropped}} مورد رد شد — کتابخانه پر است)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "نشانی KML / KMZ",
+ "errorSource": "نشانی KML/KMZ را وارد کنید یا یک فایل انتخاب کنید."
+ },
"xyz": {
"defaultName": "لایهٔ XYZ",
"sampleLabel": "تصاویر USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/fr.json b/apps/geolibre-desktop/src/i18n/locales/fr.json
index 9d835bca45..9baf145fc7 100644
--- a/apps/geolibre-desktop/src/i18n/locales/fr.json
+++ b/apps/geolibre-desktop/src/i18n/locales/fr.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Ajouter une couche de polyligne encodée",
"description": "Importer des chaînes de polylignes encodées (Google, OSRM, Valhalla) depuis du texte ou des fichiers sous forme de lignes vectorielles."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Chargez un KML ou KMZ avec ses styles natifs, superpositions et liens réseau sur le globe."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} service importé ({{dropped}} ignoré — bibliothèque pleine).",
"importedWithDropped_other": "{{count}} services importés ({{dropped}} ignorés — bibliothèque pleine)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Saisissez une URL KML/KMZ ou choisissez un fichier."
+ },
"xyz": {
"defaultName": "Couche XYZ",
"sampleLabel": "Imagerie USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/hi.json b/apps/geolibre-desktop/src/i18n/locales/hi.json
index e08a3d4fb6..5c40d71254 100644
--- a/apps/geolibre-desktop/src/i18n/locales/hi.json
+++ b/apps/geolibre-desktop/src/i18n/locales/hi.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "एन्कोडेड पॉलीलाइन लेयर जोड़ें",
"description": "एन्कोडेड पॉलीलाइन स्ट्रिंग (Google, OSRM, Valhalla) को टेक्स्ट या फ़ाइलों से वेक्टर लाइनों के रूप में आयात करें।"
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "ग्लोब पर मूल शैलियों, ओवरले और नेटवर्क लिंक के साथ KML या KMZ लोड करें।"
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} सेवा आयात की गई ({{dropped}} छोड़ी गई — लाइब्रेरी भर गई)।",
"importedWithDropped_other": "{{count}} सेवाएँ आयात की गईं ({{dropped}} छोड़ी गईं — लाइब्रेरी भर गई)।"
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL",
+ "errorSource": "KML/KMZ URL दर्ज करें या फ़ाइल चुनें।"
+ },
"xyz": {
"defaultName": "XYZ लेयर",
"sampleLabel": "USGS इमेजरी",
diff --git a/apps/geolibre-desktop/src/i18n/locales/id.json b/apps/geolibre-desktop/src/i18n/locales/id.json
index 95f991e41f..8a99201c26 100644
--- a/apps/geolibre-desktop/src/i18n/locales/id.json
+++ b/apps/geolibre-desktop/src/i18n/locales/id.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "Tambahkan Layer Polyline Terenkode",
"description": "Impor string polyline terenkode (Google, OSRM, Valhalla) dari teks atau file sebagai garis vektor."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Muat KML atau KMZ dengan gaya asli, overlay, dan tautan jaringan pada globe."
}
},
"shared": {
@@ -400,6 +404,11 @@
"imported_other": "Mengimpor {{count}} layanan.",
"importedWithDropped_other": "Mengimpor {{count}} layanan ({{dropped}} dilewati — pustaka penuh)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Masukkan URL KML/KMZ atau pilih berkas."
+ },
"xyz": {
"defaultName": "Layer XYZ",
"sampleLabel": "Citra USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/it.json b/apps/geolibre-desktop/src/i18n/locales/it.json
index ca3c0d4199..024ae1c723 100644
--- a/apps/geolibre-desktop/src/i18n/locales/it.json
+++ b/apps/geolibre-desktop/src/i18n/locales/it.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Aggiungi layer polilinea codificata",
"description": "Importa stringhe di polilinea codificata (Google, OSRM, Valhalla) da testo o file come linee vettoriali."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Carica KML o KMZ con stili nativi, sovrapposizioni e collegamenti di rete sul globo."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "Importato {{count}} servizio ({{dropped}} ignorato — libreria piena).",
"importedWithDropped_other": "Importati {{count}} servizi ({{dropped}} ignorati — libreria piena)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Inserisci un URL KML/KMZ o scegli un file."
+ },
"xyz": {
"defaultName": "Layer XYZ",
"sampleLabel": "Immagini USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ja.json b/apps/geolibre-desktop/src/i18n/locales/ja.json
index b1a0dbf0f0..13dd9de1a7 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ja.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ja.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "エンコードポリラインレイヤーを追加",
"description": "エンコードされたポリライン文字列(Google、OSRM、Valhalla)をテキストまたはファイルからベクターラインとしてインポートします。"
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "元のスタイル、オーバーレイ、ネットワークリンクを保持して KML または KMZ を地球儀に読み込みます。"
}
},
"shared": {
@@ -400,6 +404,11 @@
"imported_other": "{{count}}件のサービスをインポートしました。",
"importedWithDropped_other": "{{count}}件のサービスをインポートしました({{dropped}}件はライブラリが上限に達したためスキップされました)。"
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ の URL",
+ "errorSource": "KML/KMZ の URL を入力するか、ファイルを選択してください。"
+ },
"xyz": {
"defaultName": "XYZレイヤー",
"sampleLabel": "USGS 衛星画像",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ka.json b/apps/geolibre-desktop/src/i18n/locales/ka.json
index af58821709..a021ab3540 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ka.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ka.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "დაშიფრული პოლიხაზის შრის დამატება",
"description": "დაშიფრული პოლიხაზის სტრიქონების (Google, OSRM, Valhalla) იმპორტი ტექსტიდან ან ფაილებიდან ვექტორული ხაზების სახით."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "ჩატვირთეთ KML ან KMZ გლობუსზე ორიგინალური სტილებით, გადაფარვებითა და ქსელური ბმულებით."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "დაიმპორტდა {{count}} სერვისი ({{dropped}} გამოტოვდა — ბიბლიოთეკა სავსეა).",
"importedWithDropped_other": "დაიმპორტდა {{count}} სერვისი ({{dropped}} გამოტოვდა — ბიბლიოთეკა სავსეა)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL",
+ "errorSource": "შეიყვანეთ KML/KMZ URL ან აირჩიეთ ფაილი."
+ },
"xyz": {
"defaultName": "XYZ შრე",
"sampleLabel": "USGS-ის სურათები",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ko.json b/apps/geolibre-desktop/src/i18n/locales/ko.json
index 94a708d429..b0522c0cf8 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ko.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ko.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "인코딩된 폴리라인 레이어 추가",
"description": "인코딩된 폴리라인 문자열(Google, OSRM, Valhalla)을 텍스트 또는 파일에서 벡터 라인으로 가져옵니다."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "원본 스타일, 오버레이 및 네트워크 링크를 유지하여 KML 또는 KMZ를 지구본에 불러옵니다."
}
},
"shared": {
@@ -400,6 +404,11 @@
"imported_other": "서비스 {{count}}개를 가져왔습니다.",
"importedWithDropped_other": "서비스 {{count}}개를 가져왔습니다({{dropped}}개는 라이브러리가 가득 차서 건너뜀)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL",
+ "errorSource": "KML/KMZ URL을 입력하거나 파일을 선택하세요."
+ },
"xyz": {
"defaultName": "XYZ 레이어",
"sampleLabel": "USGS 영상",
diff --git a/apps/geolibre-desktop/src/i18n/locales/nl.json b/apps/geolibre-desktop/src/i18n/locales/nl.json
index aeb6db582e..2f5b4a27ee 100644
--- a/apps/geolibre-desktop/src/i18n/locales/nl.json
+++ b/apps/geolibre-desktop/src/i18n/locales/nl.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Gecodeerde polylijnlaag toevoegen",
"description": "Gecodeerde polylijn-tekenreeksen (Google, OSRM, Valhalla) uit tekst of bestanden importeren als vectorlijnen."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Laad KML of KMZ met oorspronkelijke stijlen, overlays en netwerkkoppelingen op de globe."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} service geïmporteerd ({{dropped}} overgeslagen — bibliotheek vol).",
"importedWithDropped_other": "{{count}} services geïmporteerd ({{dropped}} overgeslagen — bibliotheek vol)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML-/KMZ-URL",
+ "errorSource": "Voer een KML/KMZ-URL in of kies een bestand."
+ },
"xyz": {
"defaultName": "XYZ-laag",
"sampleLabel": "USGS-luchtfoto's",
diff --git a/apps/geolibre-desktop/src/i18n/locales/pt.json b/apps/geolibre-desktop/src/i18n/locales/pt.json
index f280ec5303..1e2bab1174 100644
--- a/apps/geolibre-desktop/src/i18n/locales/pt.json
+++ b/apps/geolibre-desktop/src/i18n/locales/pt.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Adicionar camada de polilinha codificada",
"description": "Importar strings de polilinha codificada (Google, OSRM, Valhalla) de texto ou arquivos como linhas vetoriais."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Carregue KML ou KMZ com estilos nativos, sobreposições e links de rede no globo."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} serviço importado ({{dropped}} ignorado — biblioteca cheia).",
"importedWithDropped_other": "{{count}} serviços importados ({{dropped}} ignorados — biblioteca cheia)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Insira uma URL KML/KMZ ou escolha um arquivo."
+ },
"xyz": {
"defaultName": "Camada XYZ",
"sampleLabel": "Imagens USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/ru.json b/apps/geolibre-desktop/src/i18n/locales/ru.json
index cd96cecf9d..a92a81866b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/ru.json
+++ b/apps/geolibre-desktop/src/i18n/locales/ru.json
@@ -351,6 +351,10 @@
"polyline": {
"label": "Добавить слой закодированной полилинии",
"description": "Импорт закодированных строк полилиний (Google, OSRM, Valhalla) из текста или файлов в виде векторных линий."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Загрузите KML или KMZ на глобус с исходными стилями, наложениями и сетевыми ссылками."
}
},
"shared": {
@@ -412,6 +416,11 @@
"importedWithDropped_many": "Импортировано {{count}} сервисов ({{dropped}} пропущено — библиотека заполнена).",
"importedWithDropped_other": "Импортировано {{count}} сервиса ({{dropped}} пропущено — библиотека заполнена)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Введите URL KML/KMZ или выберите файл."
+ },
"xyz": {
"defaultName": "Слой XYZ",
"sampleLabel": "Снимки USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/th.json b/apps/geolibre-desktop/src/i18n/locales/th.json
index 5eeba7597b..08acc2901b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/th.json
+++ b/apps/geolibre-desktop/src/i18n/locales/th.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "เพิ่มเลเยอร์โพลีไลน์ที่เข้ารหัส",
"description": "นำเข้าสตริงโพลีไลน์ที่เข้ารหัส (Google, OSRM, Valhalla) จากข้อความหรือไฟล์เป็นเส้นเวกเตอร์"
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "โหลด KML หรือ KMZ บนลูกโลกโดยคงรูปแบบต้นฉบับ ภาพซ้อนทับ และลิงก์เครือข่าย"
}
},
"shared": {
@@ -400,6 +404,11 @@
"imported_other": "นำเข้าบริการแล้ว {{count}} รายการ",
"importedWithDropped_other": "นำเข้าบริการแล้ว {{count}} รายการ (ข้าม {{dropped}} รายการ — คลังเต็ม)"
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL ของ KML / KMZ",
+ "errorSource": "ป้อน URL ของ KML/KMZ หรือเลือกไฟล์"
+ },
"xyz": {
"defaultName": "เลเยอร์ XYZ",
"sampleLabel": "ภาพถ่าย USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/tr.json b/apps/geolibre-desktop/src/i18n/locales/tr.json
index edd11302ba..ca94c40778 100644
--- a/apps/geolibre-desktop/src/i18n/locales/tr.json
+++ b/apps/geolibre-desktop/src/i18n/locales/tr.json
@@ -347,6 +347,10 @@
"polyline": {
"label": "Kodlanmış Çoklu Çizgi Katmanı Ekle",
"description": "Kodlanmış çoklu çizgi dizelerini (Google, OSRM, Valhalla) metin veya dosyalardan vektör çizgiler olarak içe aktarın."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "KML veya KMZ dosyalarını özgün stiller, kaplamalar ve ağ bağlantılarıyla küreye yükleyin."
}
},
"shared": {
@@ -404,6 +408,11 @@
"importedWithDropped_one": "{{count}} servis içe aktarıldı ({{dropped}} tanesi atlandı — kitaplık dolu).",
"importedWithDropped_other": "{{count}} servis içe aktarıldı ({{dropped}} tanesi atlandı — kitaplık dolu)."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL'si",
+ "errorSource": "Bir KML/KMZ URL’si girin veya dosya seçin."
+ },
"xyz": {
"defaultName": "XYZ Katmanı",
"sampleLabel": "USGS uydu görüntüsü",
diff --git a/apps/geolibre-desktop/src/i18n/locales/vi.json b/apps/geolibre-desktop/src/i18n/locales/vi.json
index f999d785f8..9c2586d7f9 100644
--- a/apps/geolibre-desktop/src/i18n/locales/vi.json
+++ b/apps/geolibre-desktop/src/i18n/locales/vi.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "Thêm lớp polyline mã hóa",
"description": "Nhập các chuỗi polyline mã hóa (Google, OSRM, Valhalla) từ văn bản hoặc tệp dưới dạng đường vector."
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "Tải KML hoặc KMZ lên địa cầu với kiểu gốc, lớp phủ và liên kết mạng."
}
},
"shared": {
@@ -400,6 +404,11 @@
"errorRead": "Không thể đọc tệp đó dưới dạng thư viện dịch vụ.",
"imported_other": "Đã nhập dịch vụ {{count}}."
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "URL KML / KMZ",
+ "errorSource": "Nhập URL KML/KMZ hoặc chọn một tệp."
+ },
"xyz": {
"defaultName": "Lớp XYZ",
"sampleLabel": "Hình ảnh USGS",
diff --git a/apps/geolibre-desktop/src/i18n/locales/zh.json b/apps/geolibre-desktop/src/i18n/locales/zh.json
index c35b373b03..f0b868f37b 100644
--- a/apps/geolibre-desktop/src/i18n/locales/zh.json
+++ b/apps/geolibre-desktop/src/i18n/locales/zh.json
@@ -345,6 +345,10 @@
"polyline": {
"label": "添加编码折线图层",
"description": "将编码折线字符串(Google、OSRM、Valhalla)从文本或文件导入为矢量线。"
+ },
+ "kml": {
+ "label": "KML / KMZ",
+ "description": "在地球仪上加载 KML 或 KMZ,保留原生样式、叠加层和网络链接。"
}
},
"shared": {
@@ -400,6 +404,11 @@
"imported_other": "已导入 {{count}} 个服务。",
"importedWithDropped_other": "已导入 {{count}} 个服务({{dropped}} 个因库已满而跳过)。"
},
+ "kml": {
+ "defaultName": "KML / KMZ",
+ "url": "KML / KMZ URL",
+ "errorSource": "输入 KML/KMZ URL 或选择文件。"
+ },
"xyz": {
"defaultName": "XYZ 图层",
"sampleLabel": "USGS 影像",
diff --git a/apps/geolibre-desktop/src/lib/ui-profile.ts b/apps/geolibre-desktop/src/lib/ui-profile.ts
index 55e75efbe8..aed1a1fb49 100644
--- a/apps/geolibre-desktop/src/lib/ui-profile.ts
+++ b/apps/geolibre-desktop/src/lib/ui-profile.ts
@@ -160,6 +160,7 @@ export const DATA_SOURCE_CATALOG: readonly DataSourceCatalogEntry[] = [
labelKey: "toolbar.layerType.czml",
tier: "advanced",
},
+ { id: "kml", section: "threeD", labelKey: "addData.kind.kml.label", tier: "advanced" },
{
id: "gltf-model",
section: "threeD",
diff --git a/apps/geolibre-desktop/tsconfig.json b/apps/geolibre-desktop/tsconfig.json
index a4d45813ff..5ac0d3b88b 100644
--- a/apps/geolibre-desktop/tsconfig.json
+++ b/apps/geolibre-desktop/tsconfig.json
@@ -2,6 +2,7 @@
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"composite": true,
+ "types": ["node", "react", "react-dom", "vite/client"],
"paths": {
"@/*": ["./src/*"]
}
diff --git a/apps/geolibre-desktop/vite.config.ts b/apps/geolibre-desktop/vite.config.ts
index 4ba09c0bfe..3b73397384 100644
--- a/apps/geolibre-desktop/vite.config.ts
+++ b/apps/geolibre-desktop/vite.config.ts
@@ -1209,8 +1209,9 @@ export default defineConfig({
// cog-tiler-wasm's mask-aware LERC decoder (lerc-decoder.js) reaches
// these through dynamic import() the first time a LERC COG opens; they
// are geotiff's own codec packages, listed here for the same
- // discover-and-reload reason as above. (`lerc` itself is excluded below:
- // it locates its .wasm via import.meta.url.)
+ // discover-and-reload reason as above. The raster loader supplies LERC's
+ // WASM URL explicitly, so its ESM decoder can also be pre-bundled.
+ "lerc",
"pako",
"zstddec",
// Cesium (the 3D-globe view). Pre-bundle it up front so esbuild applies
@@ -1266,11 +1267,6 @@ export default defineConfig({
// breaks that asset reference so the tiler stops rendering. Serve it
// as-is. (Its plain-JS deps are pre-bundled via optimizeDeps.include.)
"cog-tiler-wasm",
- // lerc 4.x (cog-tiler-wasm's mask-aware LERC decoder) fetches
- // lerc-wasm.wasm via `new URL(..., import.meta.url)`; pre-bundled, that
- // resolves against the .vite/deps chunk and the request falls through to
- // index.html ("expected magic word 00 61 73 6d, found 3c 21 64 6f").
- "lerc",
// h5wasm (local NetCDF/HDF5 reader) loads its libhdf5 .wasm via
// `new URL(..., import.meta.url)`; esbuild pre-bundling mangles that
// asset reference, so serve it as-is. Only reached through the lazy
diff --git a/docs/architecture.md b/docs/architecture.md
index af050ad37f..8fa82495fa 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -52,7 +52,7 @@ The 2D MapLibre map can be joined by — or replaced with — a 3D globe rendere
- **Lazy loading.** The whole Cesium engine (~4.8 MB) is `import()`-ed only when a globe first mounts, kept in its own build chunk (`manualChunks`) and off the 2D boot path. A Vite plugin (`vite-plugins/copy-cesium-assets.ts`) stages Cesium's runtime Workers/Assets/Widgets into `public/cesium/` and the canvas sets `window.CESIUM_BASE_URL` so the engine finds them.
- **Drawing.** Both engines implement `drawExtent` with a shared pointer lifecycle and geographic extents. Cesium uses a draggable entity for manual placement and ground-clamped rectangles for extraction previews. Escape, pointer cancellation, focus loss, and renderer teardown restore navigation. The raster-subset panel uses these operations on both renderers; crossing extents retain west > east so the extractor can explain that they must be split. The basemap extraction panel shares the drawing helper, while its menu remains gated on vector-style support.
- **Camera sync.** `packages/map/src/cesium-camera.ts` converts between MapLibre's Web-Mercator `MapViewState` (zoom, nadir-referenced pitch, bearing) and Cesium's camera (metric range, horizon-referenced pitch, heading), matched by **ground resolution** (metres per pixel) so the on-screen scale stays in step even when the panes differ in height. `CesiumCanvas` seeds its camera from the shared `mapView`, applies store changes to the globe, and writes the globe's own moves back — bidirectional, like the 2D panes — with a tolerance check that suppresses the apply→`moveEnd` echo so there is no jitter loop.
-- **Layer sync.** `CesiumLayerSync` (`packages/map/src/cesium-layer-sync.ts`) reconciles the store's `GeoLibreLayer[]` onto the globe the way `MapController.syncLayers` does for MapLibre, reusing the same per-pane visibility overrides and group effects as `SecondaryMapCanvas`. It renders the kinds where Cesium is the natural fit — GeoJSON (a draped `GeoJsonDataSource` whose entities are styled per feature by `createFeatureStyleResolver` (`packages/map/src/cesium-feature-style.ts`, issue #2278): it evaluates the same MapLibre expressions `@geolibre/core` builds for the 2D map — single, categorized, graduated, rule-based with per-rule symbol overrides, and expression modes, simplestyle properties, proportional sizing, metre-unit strokes — with the style-spec engine, so polygon fills and outlines, line widths and arrow decorations, circle radii, classified marker sprites, fill patterns, and the layer's zoom range (a shared `DistanceDisplayCondition`) match the 2D map; points draw as circles unless the layer renders markers; a point-only layer with `pointRenderer: "cluster"` clusters through the data source's `EntityCluster` with bubbles and abbreviated counts styled like the 2D cluster layers and switched off past `clusterMaxZoom`, and a point-only layer above 50 000 features bypasses entities for one `PointPrimitiveCollection` whose primitives carry the feature reference on `id` so picking, highlighting, and filters still work (`packages/map/src/cesium-points.ts`, issue #2282; the heatmap renderer has no globe form and draws as plain circles); with field or expression labels, halos, and label zoom limits), XYZ/raster/WMTS, WMS, ArcGIS MapServer (`ArcGisMapServerImageryProvider`), and georeferenced image overlays (`SingleTileImageryProvider`) (as `ImageryLayer`s), and 3D Tiles (a `Cesium3DTileset` primitive that consumes the layer's tileset URL, request headers, and altitude offset directly, and whose features are classified by `cesium-tileset-style.ts` (issue #2290): the layer's colour expression and composed feature filter are translated from MapLibre expressions into the 3D Tiles styling language and applied as a `Cesium3DTileStyle`, so a tileset categorizes, graduates, or follows a rule tree the way an extruded vector layer does, and the layer opacity reaches it as a white multiply that fades textured tiles without tinting them — an untranslatable colour or filter leaves the tileset drawing its own colours and showing every feature rather than guessing; the attribute names the Style panel lists come from the first rendered tile, published onto `metadata.fields` because a tileset carries its schema in the tiles rather than in the layer record; an ArcGIS I3S scene layer goes through Cesium's own `I3SDataProvider` instead of loaders.gl, a Gaussian-splat layer whose asset is a 3D Tiles tileset renders through the same branch because Cesium draws `KHR_gaussian_splatting` tiles natively, and a point cloud in 3D Tiles form gets `pointCloudShading` eye-dome lighting), plus COPC point clouds decoded in the browser with the `copc` package into a bounded `PointPrimitiveCollection` preview reprojected through the archive's WKT (`packages/map/src/cesium-point-cloud.ts`, issue #2285) — with live visibility/opacity, rebuild-on-source-change, and removal. Tile sources that reach the 2D map through a MapLibre custom protocol — COG tiles from the WASM tiler, raster PMTiles, local MBTiles, the desktop's native XYZ/WMS fetcher, KML super-overlays — render through `ProtocolImageryProvider` (`packages/map/src/cesium-protocol-imagery.ts`, issue #2283), a Cesium `ImageryProvider` that expands the `{z}/{x}/{y}` template the way MapLibre does, hands each tile URL to the handler in `maplibregl.config.REGISTERED_PROTOCOLS` (process-wide, so it works with no MapLibre map mounted), and decodes the bytes into an `ImageBitmap` flipped the way Cesium expects; an unregistered scheme is reported as a layer error rather than drawn blank. COG layers open the same `cog-tiler-wasm` source the raster control uses and render from the persisted `metadata.rasterState` (`cesium-cog-imagery.ts`), so a COG looks the same on both renderers; a COG restored from a project with only a desktop file path stays 2D-only until the raster control has reopened it. The raster symbology (brightness, contrast, saturation, hue) maps onto `ImageryLayer`'s own adjustments for every imagery kind. Tile-backed vector layers (`vector-tiles`, vector PMTiles, vector MBTiles) are draped (`cesium-drape.ts`, issue #2284): the globe runs one hidden, single-tile MapLibre map that receives the same store layers through the headless `createLayerSync` path, renders one Web Mercator tile at a time (`jumpTo` the tile centre, wait for `idle`, read the canvas back), and feeds Cesium through a `ProtocolImageryProvider`, so the Style Spec is never re-implemented and every style, filter, order, and visibility edit rebuilds the drape's imagery layer. Draped content is flat, its labels are placed per tile, and their glyphs come from a remote font host (so a local archive viewed offline drapes without text); picking on it, `arcgis` VectorTileServer layers (painted by their own control), Zarr, EPT and plain LAS/LAZ point clouds, raw `.ply`/`.splat` splat files, and deck.gl viz are still skipped on the globe and render in the 2D panes; the exported `isCesiumSupportedLayerType` predicate lets the pane's layer menu — and the Layers panel, when the globe is the primary renderer — tag those "2D only".
+- **Layer sync.** `CesiumLayerSync` (`packages/map/src/cesium-layer-sync.ts`) reconciles the store's `GeoLibreLayer[]` onto the globe the way `MapController.syncLayers` does for MapLibre, reusing the same per-pane visibility overrides and group effects as `SecondaryMapCanvas`. It renders the kinds where Cesium is the natural fit — GeoJSON (a draped `GeoJsonDataSource` whose entities are styled per feature by `createFeatureStyleResolver` (`packages/map/src/cesium-feature-style.ts`, issue #2278): it evaluates the same MapLibre expressions `@geolibre/core` builds for the 2D map — single, categorized, graduated, rule-based with per-rule symbol overrides, and expression modes, simplestyle properties, proportional sizing, metre-unit strokes — with the style-spec engine, so polygon fills and outlines, line widths and arrow decorations, circle radii, classified marker sprites, fill patterns, and the layer's zoom range (a shared `DistanceDisplayCondition`) match the 2D map; points draw as circles unless the layer renders markers; a point-only layer with `pointRenderer: "cluster"` clusters through the data source's `EntityCluster` with bubbles and abbreviated counts styled like the 2D cluster layers and switched off past `clusterMaxZoom`, and a point-only layer above 50 000 features bypasses entities for one `PointPrimitiveCollection` whose primitives carry the feature reference on `id` so picking, highlighting, and filters still work (`packages/map/src/cesium-points.ts`, issue #2282; the heatmap renderer has no globe form and draws as plain circles); with field or expression labels, halos, and label zoom limits), XYZ/raster/WMTS, WMS, ArcGIS MapServer (`ArcGisMapServerImageryProvider`), and georeferenced image overlays (`SingleTileImageryProvider`) (as `ImageryLayer`s), and 3D Tiles (a `Cesium3DTileset` primitive that consumes the layer's tileset URL, request headers, and altitude offset directly, and whose features are classified by `cesium-tileset-style.ts` (issue #2290): the layer's colour expression and composed feature filter are translated from MapLibre expressions into the 3D Tiles styling language and applied as a `Cesium3DTileStyle`, so a tileset categorizes, graduates, or follows a rule tree the way an extruded vector layer does, and the layer opacity reaches it as a white multiply that fades textured tiles without tinting them — an untranslatable colour or filter leaves the tileset drawing its own colours and showing every feature rather than guessing; the attribute names the Style panel lists come from the first rendered tile, published onto `metadata.fields` because a tileset carries its schema in the tiles rather than in the layer record; an ArcGIS I3S scene layer goes through Cesium's own `I3SDataProvider` instead of loaders.gl, a Gaussian-splat layer whose asset is a 3D Tiles tileset renders through the same branch because Cesium draws `KHR_gaussian_splatting` tiles natively, and a point cloud in 3D Tiles form gets `pointCloudShading` eye-dome lighting), plus COPC point clouds decoded in the browser with the `copc` package into a bounded `PointPrimitiveCollection` preview reprojected through the archive's WKT (`packages/map/src/cesium-point-cloud.ts`, issue #2285) — with live visibility/opacity, rebuild-on-source-change, and removal. Tile sources that reach the 2D map through a MapLibre custom protocol — COG tiles from the WASM tiler, raster PMTiles, local MBTiles, the desktop's native XYZ/WMS fetcher, KML super-overlays — render through `ProtocolImageryProvider` (`packages/map/src/cesium-protocol-imagery.ts`, issue #2283), a Cesium `ImageryProvider` that expands the `{z}/{x}/{y}` template the way MapLibre does, hands each tile URL to the handler in `maplibregl.config.REGISTERED_PROTOCOLS` (process-wide, so it works with no MapLibre map mounted), and decodes the bytes into an `ImageBitmap` flipped the way Cesium expects; an unregistered scheme is reported as a layer error rather than drawn blank. COG layers open the same `cog-tiler-wasm` source the raster control uses and render from the persisted `metadata.rasterState` (`cesium-cog-imagery.ts`), so a COG looks the same on both renderers; a COG restored from a project with only a desktop file path stays 2D-only until the raster control has reopened it. The raster symbology (brightness, contrast, saturation, hue) maps onto `ImageryLayer`'s own adjustments for every imagery kind. Tile-backed vector layers (`vector-tiles`, vector PMTiles, vector MBTiles) are draped (`cesium-drape.ts`, issue #2284): the globe runs one hidden, single-tile MapLibre map that receives the same store layers through the headless `createLayerSync` path, renders one Web Mercator tile at a time (`jumpTo` the tile centre, wait for `idle`, read the canvas back), and feeds Cesium through a `ProtocolImageryProvider`, so the Style Spec is never re-implemented and every style, filter, order, and visibility edit rebuilds the drape's imagery layer. Draped content is flat, its labels are placed per tile, and their glyphs come from a remote font host (so a local archive viewed offline drapes without text); picking on it, `arcgis` VectorTileServer layers (painted by their own control), Zarr, EPT and plain LAS/LAZ point clouds, raw `.ply`/`.splat` splat files, and deck.gl viz are still skipped on the globe and render in the 2D panes; the exported `isCesiumSupportedLayerType` predicate lets the pane's layer menu — and the Layers panel, when the globe is the primary renderer — tag those "2D only". Native KML/KMZ documents use `KmlDataSource`, preserving document styling, network links, and overlays; visibility and opacity are applied without replacing the original color properties. Local KMZ archives persist as data URLs. The Elevation Profile plugin draws native entities and samples the active terrain provider, rejecting results when that provider changes.
- **Globe interaction parity.** The primary globe publishes ground cursor coordinates and optional terrain elevation to the shared status bar, clearing them on pointer exit and teardown. Projected scene modes report a Mercator projection. Home, scene-mode, and fullscreen widgets participate in shared control positioning without remounting their DOM. In the Controls menu they answer to the existing entries: **Compass** governs the Home (reset view) button and **Globe** governs the scene-mode picker, as the globe's counterparts of the 2D reset-bearing and projection controls, and the toolbar replays those choices onto a freshly mounted globe.
- **Environment plugins.** The Sun simulation, Atmospheric Effects, Flight Simulator, Clouds, and Precipitation plugins declare both engines and branch on `app.getCesiumScene()` (issue #2287), a typed handle to the primary globe's namespace, widget, scene, camera, and clock that `CesiumEngine.getCesiumScene()` exposes as the globe's counterpart to `getMap()`. On the globe the Sun drives a native `SunLight`, `globe.enableLighting`, and the scene clock (the night-side depth maps onto `globe.vertexShadowDarkness`); Effects toggles the sky box and atmosphere and re-tints `SkyAtmosphere` by hue/saturation/brightness shifts from the halo settings; the Flight Simulator shares its keyboard, physics, and HUD with the 2D map behind a camera adapter that places `camera.setView` each frame with `screenSpaceCameraController.enableInputs` suspended; the weather overlays are store tile layers and render through `CesiumLayerSync` unchanged. The Spinning Globe control needs no branch: it drives the store through the control host's MapLibre facade. `DesktopShell` re-attaches these plugins after either engine mounts, so a renderer swap rebinds them the way a MapLibre re-init does.
- **Persistence.** A pane's `viewKind` is part of `SecondaryMapView` and round-trips through the `.geolibre.json` project format (`normalizeSecondaryMapViews`), so a project saved with a globe pane reopens as the globe, token or not. The workspace's own choice persists as the top-level `primaryRenderer` (`normalizePrimaryRenderer`), written only when it is not the default — so a MapLibre project is byte-identical to one saved before the setting existed, and a 3D-first project reopens directly on the globe.
diff --git a/docs/cesium-parity-testing.md b/docs/cesium-parity-testing.md
index c2af6f4cbe..5c19a925c5 100644
--- a/docs/cesium-parity-testing.md
+++ b/docs/cesium-parity-testing.md
@@ -26,6 +26,28 @@ here; #2291 describes it as a possible follow-up.
## Remaining work and ordering
+September 11 audit: the open Cesium trackers are #2259, #2261, and #2262.
+The tileset styling requirement of #2290 landed in #2337 and native CZML in
+#2350. Although #2350 closed #2290, several requirements remain.
+Clipping polygons, terrain sampling in the measurement tools,
+Ion terrain assets, and Google Photorealistic 3D Tiles remain outstanding.
+The layer-format gaps listed below also remain, including ArcGIS vector tiles,
+drape picking, Zarr, raw point clouds and splats, and deck.gl visualizations.
+
+The control host now forwards camera and geographic pointer events and reports
+the actual canvas container dimensions. It rejects source mutations as well as
+style-layer mutations. This fixes the facade contract but does not make controls
+that paint through MapLibre compatible; their engine declarations remain gated.
+
+The Vite audit also found that excluding `lerc` from dependency optimization
+externalized Cesium's LERC 2 import to the top-level LERC 4 package, preventing
+the globe from opening. Both versions now remain in their respective dependency
+graphs. The globe's COG loader supplies LERC 4's WASM URL explicitly, as the
+2D raster loader already does. A real single-band Athens LERC DEM in
+EPSG:2100 renders with a terrain colormap in both themes. The Layers panel
+no longer applies its MapLibre-source placeholder warning to native Cesium
+layers.
+
| Issues | Next work | Required evidence |
| --- | --- | --- |
| #2276 | Implemented manual placement and shared extent drawing | Real pin drag and Done, rectangle drawing and Escape in both themes, renderer swaps, and Esri World Imagery extraction to a 22×12 EPSG:4326 GeoTIFF with nonconstant pixels; unit coverage includes antimeridian extents, pointer ownership, sky release, blur, and cancellation |
@@ -41,7 +63,7 @@ here; #2291 describes it as a possible follow-up.
| #2287 (implemented) | Native environment plugins | Sun clock and lighting on/off, atmosphere/sky box on/off and restore, spin start/stop, cloud imagery add/remove, and flight take-over/teardown verified in the real app in both themes; unit tests cover each Cesium branch against the real Cesium maths with a faked widget |
| #2288, #2262 | Enforce declared support in activation, URL dispatch, project restore, delayed controls, and command palette | Tests cover unsupported callbacks, renderer round trips, saved settings, and compatible-control remounting. The wider control facade and native plugin implementations remain separate work. |
| #2289 | Python/MCP/embed renderer authoring | Project round trips, renderer events, pane kinds, invalid inputs, and docs examples |
-| #2290 (Ion assets implemented) | Cesium-native authoring features | Ion assets: Cesium OSM Buildings (asset 96188) and Bing Aerial (asset 2) added from the Add Data dialog on the globe in both themes, the same project reopened on the 2D map showing the "3D only" badge, a missing token surfacing as a layer error; unit tests cover the layer builder, the asset-id parser, the globe's tileset/imagery routing through `IonResource`/`IonImageryProvider`, rebuild on asset change, and the Python/MCP builders. Tileset styling, clipping polygons, KML/CZML, and terrain sampling remain separate follow-ups. |
+| #2290 (Ion assets implemented) | Cesium-native authoring features | Ion assets: Cesium OSM Buildings (asset 96188) and Bing Aerial (asset 2) added from the Add Data dialog on the globe in both themes, the same project reopened on the 2D map showing the "3D only" badge, a missing token surfacing as a layer error; unit tests cover the layer builder, the asset-id parser, the globe's tileset/imagery routing through `IonResource`/`IonImageryProvider`, rebuild on asset change, and the Python/MCP builders. Tileset styling and CZML have merged. Native KML/KMZ and elevation profiles are implemented: a real San Francisco landmarks KMZ retains its billboard styles and labels in both themes; a drawn 3.43 km profile samples World Terrain from -27 m to 74 m. Tests cover document loading, cancellation, opacity, cleanup, Python serialization, and terrain-provider replacement. Clipping polygons, Terrain Measure, Ion terrain assets, and Google Photorealistic 3D Tiles remain follow-ups. |
| #2261, #2259 | Update umbrella completion only after child requirements are verified | Accurate supported-layer predicates and an explicit record of remaining gaps |
## Test gates
diff --git a/docs/mcp.md b/docs/mcp.md
index 30c5bc37e6..7c4a6a94fb 100644
--- a/docs/mcp.md
+++ b/docs/mcp.md
@@ -105,6 +105,7 @@ Give it a directory meant for maps, not your home directory.
| `add_3d_tiles_layer` | OGC 3D Tiles tilesets, by URL or Cesium Ion asset id. |
| `add_cesium_ion_layer` | Cesium Ion assets (tileset or imagery) by id; rendered by the 3D globe only. |
| `add_czml_layer` | A CZML (Cesium Language) dynamic 3D scene, by URL or inline packets; rendered by the 3D globe only. |
+| `add_cesium_kml_layer` | Native globe KML/KMZ from a URL, inline XML, or KMZ data URL, preserving document styles and overlays. |
### Editing
diff --git a/docs/plugin-api.md b/docs/plugin-api.md
index a9deb67247..9efffd961d 100644
--- a/docs/plugin-api.md
+++ b/docs/plugin-api.md
@@ -1164,7 +1164,11 @@ over its canvas — so the scoped CSS in `index.css` keeps applying — and hand
`Map`. The facade answers:
- `getContainer`, `getCanvas`, `isStyleLoaded`, and the `Evented` methods
- (`on` / `off` / `once` / `fire`).
+ (`on` / `off` / `once` / `fire`). `getContainer` returns the sized canvas
+ parent so controls can anchor their panels. Camera `movestart`, `move`, and
+ `moveend`, canvas `resize`, and geographic mouse events reach subscriptions;
+ pointer events over space are omitted because they have no ground location.
+ The host removes these subscriptions when the globe is destroyed.
- `getCenter`, `getZoom`, `getBearing`, `getPitch` from the store's map view,
and `jumpTo` / `flyTo` / `easeTo` by writing it back.
- `project`, `unproject`, and `getBounds` from the live scene: a coordinate is
@@ -1176,9 +1180,9 @@ over its canvas — so the scoped CSS in `index.css` keeps applying — and hand
shapes MapLibre's globe projection answers with, so a control keeps running
instead of throwing mid-render.
- `setStyle(url)`, routed to the project basemap.
-- `addSource` / `getSource` / `removeSource`, kept in a map on the facade.
-Everything that paints through the Mapbox Style Spec — `addLayer`,
+Everything that paints through the Mapbox Style Spec, including `addSource`,
+`removeSource`, `addLayer`, `removeLayer`,
`setPaintProperty`, `setLayoutProperty`, `getStyle` — **throws**. That is the
honest boundary: a control that draws its own map layers has no globe
representation, and a silent no-op would leave it reporting success while
diff --git a/docs/python.md b/docs/python.md
index 990d36aec4..ad0a11c5b1 100644
--- a/docs/python.md
+++ b/docs/python.md
@@ -297,6 +297,7 @@ m.on_layer_change(lambda e: print("layers", e["layerIds"]))
| `add_3d_tiles(url=None, name=, ion_asset_id=, altitude_offset=, request_headers=, **style)` | Add a 3D Tiles `tileset.json` URL, or a Cesium Ion tileset by asset id (3D globe only). |
| `add_cesium_ion(asset_id, name=, kind="3d-tiles", altitude_offset=, **style)` | Add a Cesium Ion asset by id: a 3D Tiles tileset or (`kind="imagery"`) an imagery layer. Renders on the 3D globe, with the app's Ion token. |
| `add_czml(url=None, name=, data=, source_path=, **style)` | Add a CZML (Cesium Language) dynamic 3D scene by URL or inline packets: orbits, vehicle tracks, moving models. Renders on the 3D globe, which follows the document's clock. |
+| `add_cesium_kml(url=None, name=, data=, source_path=, **style)` | Native KML/KMZ on the globe with document styles and overlays. Supply a URL, inline XML, or a KMZ data URL; use `add_kml` for vector conversion. |
| `add_video(urls, coordinates, name=, **style)` | Add a georeferenced video (four `[lng, lat]` corners). |
| `add_basemap(basemap)` | Set the background basemap. |
| `split_map(left_layers=None, right_layers=None, orientation=, position=, control_position=)` | Add a swipe (split-map) comparison slider between two layer sets. |
diff --git a/packages/core/src/cesium-ion.ts b/packages/core/src/cesium-ion.ts
index ac5605b360..e7eb24b549 100644
--- a/packages/core/src/cesium-ion.ts
+++ b/packages/core/src/cesium-ion.ts
@@ -1,4 +1,5 @@
import { isCzmlLayer } from "./czml";
+import { isCesiumKmlLayer } from "./cesium-kml";
import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from "./types";
// Cesium Ion assets (issue #2290): 3D Tiles and imagery referenced by Ion
@@ -58,7 +59,7 @@ export function cesiumIonAssetKind(layer: Pick): CesiumIo
* `isCesiumSupportedLayerType`, for the Layers panel to badge on the 2D map.
*/
export function isCesiumOnlyLayer(layer: Pick): boolean {
- return isCesiumIonLayer(layer) || isCzmlLayer(layer);
+ return isCesiumIonLayer(layer) || isCzmlLayer(layer) || isCesiumKmlLayer(layer);
}
function newLayerId(): string {
diff --git a/packages/core/src/cesium-kml.ts b/packages/core/src/cesium-kml.ts
new file mode 100644
index 0000000000..97d48c03cc
--- /dev/null
+++ b/packages/core/src/cesium-kml.ts
@@ -0,0 +1,48 @@
+import { DEFAULT_LAYER_STYLE, type GeoLibreLayer } from "./types";
+
+/** Native KML/KMZ documents retain their own styling on the globe. */
+export const CESIUM_KML_SOURCE_KIND = "cesium-kml";
+
+export function isCesiumKmlLayer(layer: Pick): boolean {
+ return layer.metadata?.sourceKind === CESIUM_KML_SOURCE_KIND;
+}
+
+export interface CesiumKmlLayerOptions {
+ id?: string;
+ name: string;
+ /** HTTP(S) KML or KMZ URL. Relative resources resolve against this URL. */
+ url?: string;
+ /** Inline XML, or a KMZ data URL so local archives survive project saves. */
+ data?: string;
+ sourcePath?: string;
+}
+
+export function cesiumKmlSource(layer: Pick): string | null {
+ if (!isCesiumKmlLayer(layer)) return null;
+ for (const value of [layer.source.kmlData, layer.source.url]) {
+ if (typeof value === "string" && value.trim()) return value.trim();
+ }
+ return null;
+}
+
+export function createCesiumKmlLayer(options: CesiumKmlLayerOptions): GeoLibreLayer {
+ const data = options.data?.trim();
+ const url = options.url?.trim();
+ if (!data && !url) throw new Error("Provide a KML/KMZ document or URL.");
+ const id = options.id ?? crypto.randomUUID();
+ return {
+ id,
+ name: options.name,
+ type: "3d-tiles",
+ ...(options.sourcePath ? { sourcePath: options.sourcePath } : {}),
+ source: { type: "3d-tiles", sourceId: id, ...(data ? { kmlData: data } : { url }) },
+ visible: true,
+ opacity: 1,
+ style: { ...DEFAULT_LAYER_STYLE },
+ metadata: {
+ sourceKind: CESIUM_KML_SOURCE_KIND,
+ externalNativeLayer: true,
+ identifiable: false,
+ },
+ };
+}
diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts
index 5a725daaf5..b3a9958f44 100644
--- a/packages/core/src/index.ts
+++ b/packages/core/src/index.ts
@@ -191,3 +191,10 @@ export {
readStoredAuthorName,
setStoredAuthorName,
} from "./editor-identity";
+export {
+ CESIUM_KML_SOURCE_KIND,
+ isCesiumKmlLayer,
+ cesiumKmlSource,
+ createCesiumKmlLayer,
+ type CesiumKmlLayerOptions,
+} from "./cesium-kml";
diff --git a/packages/map/src/cesium-control-host.ts b/packages/map/src/cesium-control-host.ts
index bec73e301b..c977d61964 100644
--- a/packages/map/src/cesium-control-host.ts
+++ b/packages/map/src/cesium-control-host.ts
@@ -17,8 +17,8 @@ type CesiumNs = typeof import("@cesium/engine");
const OFF_SCREEN_PX = -1e6;
class CesiumMapFacade extends maplibregl.Evented {
- private sources = new Map();
- private layers = new Map();
+ private cleanups: Array<() => void> = [];
+ private disposed = false;
constructor(
private host: CesiumControlHost,
@@ -26,10 +26,73 @@ class CesiumMapFacade extends maplibregl.Evented {
private Cesium: CesiumNs | null,
) {
super();
+ for (const [event, name] of [
+ [viewer.camera.moveStart, "movestart"],
+ [viewer.camera.changed, "move"],
+ [viewer.camera.moveEnd, "moveend"],
+ ] as const) {
+ if (event) this.cleanups.push(event.addEventListener(() => this.fire(name)));
+ }
+ if (typeof ResizeObserver !== "undefined") {
+ const observer = new ResizeObserver(() => this.fire("resize"));
+ observer.observe(viewer.canvas);
+ this.cleanups.push(() => observer.disconnect());
+ }
+ for (const name of [
+ "click",
+ "dblclick",
+ "mousemove",
+ "mousedown",
+ "mouseup",
+ "contextmenu",
+ ] as const) {
+ const listener = (originalEvent: MouseEvent) => {
+ const C = this.Cesium;
+ const scene = this.scene();
+ if (!C || !scene) return;
+ // `pickGlobeHit` costs a terrain ray intersection, and `mousemove` fires
+ // on every pointer frame. The engine's own cursor readout already picks
+ // on move, so skip the work entirely when no control is listening here.
+ if (!this.listens(name)) return;
+ const rect = viewer.canvas.getBoundingClientRect();
+ const point = new maplibregl.Point(
+ originalEvent.clientX - rect.left,
+ originalEvent.clientY - rect.top,
+ );
+ const hit = pickGlobeHit(C, viewer, point);
+ // A click on space has no geographic location. Do not fabricate the
+ // view centre for a control that may place a marker or start a query.
+ if (!hit) return;
+ // `pickGlobeHit` falls back to a WGS84 ellipsoid pick when the scene has
+ // no globe, so the conversion cannot assume one is configured either.
+ const position = (scene.globe?.ellipsoid ?? C.Ellipsoid.WGS84).cartesianToCartographic(
+ hit.position,
+ );
+ const lngLat = new maplibregl.LngLat(
+ C.Math.toDegrees(position.longitude),
+ C.Math.toDegrees(position.latitude),
+ );
+ this.fire(
+ new maplibregl.Event(name, {
+ point,
+ lngLat,
+ originalEvent,
+ preventDefault: () => originalEvent.preventDefault(),
+ }),
+ );
+ };
+ viewer.canvas.addEventListener(name, listener);
+ this.cleanups.push(() => viewer.canvas.removeEventListener(name, listener));
+ }
}
getContainer() {
- return this.host.getContainer();
+ return this.viewer.canvas.parentElement ?? this.host.getContainer();
+ }
+
+ dispose() {
+ this.disposed = true;
+ for (const cleanup of this.cleanups.splice(0)) cleanup();
}
getCanvas() {
@@ -51,7 +114,7 @@ class CesiumMapFacade extends maplibregl.Evented {
// controls wait for it before finishing a basemap swap (they would hang
// otherwise); it does not promise the pixels have changed.
setTimeout(() => {
- this.fire(new maplibregl.Event("style.load"));
+ if (!this.disposed) this.fire(new maplibregl.Event("style.load"));
}, 0);
} else {
throw new Error("CesiumControlHost: setStyle with an object is not supported.");
@@ -85,17 +148,15 @@ class CesiumMapFacade extends maplibregl.Evented {
}
addSource(id: string, source: any) {
- this.sources.set(id, source);
- return this;
+ throw new Error("CesiumControlHost: addSource is not supported on the globe.");
}
getSource(id: string) {
- return this.sources.get(id);
+ return undefined;
}
removeSource(id: string) {
- this.sources.delete(id);
- return this;
+ throw new Error("CesiumControlHost: removeSource is not supported on the globe.");
}
addLayer(layer: any, beforeId?: string) {
@@ -103,8 +164,7 @@ class CesiumMapFacade extends maplibregl.Evented {
}
removeLayer(id: string) {
- this.layers.delete(id);
- return this;
+ throw new Error("CesiumControlHost: removeLayer is not supported on the globe.");
}
/**
@@ -257,6 +317,7 @@ export class CesiumControlHost {
}
destroy() {
+ this.facade.dispose();
for (const control of Array.from(this.controls.keys())) {
this.removeControl(control);
}
diff --git a/packages/map/src/cesium-document-opacity.ts b/packages/map/src/cesium-document-opacity.ts
new file mode 100644
index 0000000000..bb9c85587e
--- /dev/null
+++ b/packages/map/src/cesium-document-opacity.ts
@@ -0,0 +1,56 @@
+import type { Color, DataSource, Entity, Property } from "@cesium/engine";
+
+type CesiumNs = typeof import("@cesium/engine");
+
+/** Fade native document graphics without replacing their time-dependent colors. */
+export function bindDocumentOpacity(
+ C: CesiumNs,
+ source: DataSource,
+ opacity: () => number,
+): () => void {
+ const wrapped = new WeakSet