Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/geolibre-desktop/src/components/layout/AddDataDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ArcGISSource } from "./add-data/sources/ArcGISSource";
import { CadSource } from "./add-data/sources/CadSource";
import { CesiumIonSource } from "./add-data/sources/CesiumIonSource";
import { CzmlSource } from "./add-data/sources/CzmlSource";
import { KmlSource } from "./add-data/sources/KmlSource";
import { DeckVizSource } from "./add-data/sources/DeckVizSource";
import { DelimitedTextSource } from "./add-data/sources/DelimitedTextSource";
import { GdbSource } from "./add-data/sources/GdbSource";
Expand Down Expand Up @@ -87,6 +88,8 @@ function renderSource(
return <CesiumIonSource />;
case "czml":
return <CzmlSource initialUrl={initialUrl} />;
case "kml":
return <KmlSource initialUrl={initialUrl} />;
case "wms":
return <WmsSource initialUrl={initialUrl} initialLayers={initialLayer} />;
case "csw":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export type KindI18nKey =
| "deckglViz"
| "video"
| "cesiumIon"
| "czml";
| "czml"
| "kml";

/**
* Maps each Add Data kind to its `addData.kind.<key>` i18n segment. The dialog
Expand Down Expand Up @@ -63,6 +64,7 @@ export const KIND_I18N_KEY: Record<AddDataKind, KindI18nKey> = {
video: "video",
"cesium-ion": "cesiumIon",
czml: "czml",
kml: "kml",
};

export const DEFAULT_XYZ_URL =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { createCesiumKmlLayer } from "@geolibre/core";
import { Button, Input, Label } from "@geolibre/ui";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { openLocalDataFileWithFallback } from "../../../../lib/tauri-io";
import { errorMessage, layerNameFromPath } from "../helpers";
import { AddDataSourceForm, useAddDataSource } from "../shared";

/** Preserve the original KML document or KMZ archive in the project. */
export function KmlSource({ initialUrl }: { initialUrl?: string }) {
const { t } = useTranslation();
const [defaultName] = useState(() => t("addData.kml.defaultName"));
const source = useAddDataSource(defaultName);
const [url, setUrl] = useState(initialUrl ?? "");
const [file, setFile] = useState<{ path: string; data: string } | null>(null);
const chooseFile = async () => {
source.setError(null);
try {
const picked = await openLocalDataFileWithFallback({
filters: [{ name: "KML / KMZ", extensions: ["kml", "kmz"] }],
accept: ".kml,.kmz",
readText: true,
binaryExtensions: ["kmz"],
});
if (!picked) return;
let data = picked.text ?? "";
if (picked.data) {
data = await new Promise<string>((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result));
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(
new Blob([picked.data!], { type: "application/vnd.google-earth.kmz" }),
);
});
}
if (!data.trim()) throw new Error(t("addData.kml.errorSource"));
setFile({ path: picked.path, data });
setUrl("");
source.setLayerName((current) =>
current.trim() && current !== defaultName
? current
: layerNameFromPath(picked.path, defaultName),
);
} catch (error) {
source.setError(errorMessage(error, t("addData.shared.addError")));
}
};
const submit = source.runSubmit(() => {
if (!file && !url.trim()) throw new Error(t("addData.kml.errorSource"));
source.addAndClose(
createCesiumKmlLayer({
name: source.layerName.trim() || defaultName,
url: url.trim(),
data: file?.data,
sourcePath: file?.path,
}),
);
});
return (
<AddDataSourceForm
layerName={source.layerName}
onLayerNameChange={source.setLayerName}
beforeLayerId={source.beforeLayerId}
onBeforeLayerIdChange={source.setBeforeLayerId}
onSubmit={submit}
error={source.error}
submitDisabled={source.isSubmitting}
>
<div className="space-y-3">
<div className="space-y-1.5">
<Label htmlFor="kml-url">{t("addData.kml.url")}</Label>
<Input
id="kml-url"
value={url}
placeholder="https://example.com/map.kmz"
onChange={(event) => {
setUrl(event.target.value);
setFile(null);
}}
/>
</div>
Comment on lines +65 to +82

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A locally-picked .kmz is read fully into memory and base64-encoded into a data URL with no size ceiling. KMZ archives commonly bundle textures/models/overlay imagery and can be tens of MB; base64 inflates that by ~4/3 again before it lands in layer.source.kmlData, which is written verbatim into the saved .geolibre.json.

This is the same failure mode embedLocalGltf (apps/geolibre-desktop/src/lib/local-gltf.ts) explicitly guards against with MAX_LOCAL_GLTF_BYTES ("a future rewrite... hang the tab and produce an unusable project file"). Worth adding an analogous size check here before the FileReader/readAsDataURL conversion.

Confidence: medium — real-world impact depends on how large a KMZ users typically pick, but the guard already exists once elsewhere in this codebase for exactly this reason.

<div className="flex items-center gap-2">
<Button type="button" variant="outline" onClick={chooseFile}>
{t("addData.common.chooseFile")}
</Button>
<span className="truncate text-xs text-muted-foreground">
{file?.path ?? t("addData.common.noFileSelected")}
</span>
</div>
</div>
</AddDataSourceForm>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
13 changes: 10 additions & 3 deletions apps/geolibre-desktop/src/components/panels/LayerPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3551,9 +3551,16 @@ export function LayerPanel({
{layerTypeLabel(layer, t)}
</span>
</div>
{isPlaceholderLayer(layer) && (
<p className="mt-1 text-[10px] text-amber-600">{placeholderMessage(layer)}</p>
)}
{/* 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) && (
<p className="mt-1 text-[10px] text-amber-600">
{placeholderMessage(layer)}
</p>
)}
{refreshStatus && (
<p
title={layer.connection?.lastError ?? layer.connection?.lastSyncedAt ?? ""}
Expand Down
15 changes: 11 additions & 4 deletions apps/geolibre-desktop/src/components/panels/StylePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
collectDiagramData,
geojsonHasZCoordinates,
isCzmlLayer,
isCesiumKmlLayer,
isStyleLibraryTargetLayer,
parseJsonExpression,
pluginOwnsPaint,
Expand Down Expand Up @@ -1781,8 +1782,8 @@ export function StylePanel({
// `CesiumLayerSync` only toggles its visibility: no `Cesium3DTileStyle` is
// compiled for it and no feature filter reaches its entities, so the tileset
// symbology and quick-filter controls would be silent no-ops (#2290).
const isCzmlScene = isCzmlLayer(layer);
const hasTilesetSymbology = isThreeDTilesLayer && !isCzmlScene;
const isNativeDocumentScene = isCzmlLayer(layer) || isCesiumKmlLayer(layer);
const hasTilesetSymbology = isThreeDTilesLayer && !isNativeDocumentScene;
// An external plugin's MapLibre custom (WebGL) layer draws its own pixels and
// has no MapLibre paint properties, so every paint editor below would be inert
// for it (#1445). The plugin declares that with `paintMode: "plugin"`; the
Expand Down Expand Up @@ -1828,7 +1829,7 @@ export function StylePanel({
// `type` (a deck GeoJSON layer is still `"geojson"`), so testing the type
// first would let it through even though a custom layer accepts no filter.
!hasExternalDeckLayer(layer) &&
!isCzmlScene &&
!isNativeDocumentScene &&
(layer.type === "geojson" ||
layer.type === "vector-tiles" ||
layer.type === "mbtiles" ||
Expand Down Expand Up @@ -5027,7 +5028,13 @@ export function StylePanel({
</ScrollArea>
<Separator />
<p className="p-2 text-[10px] text-muted-foreground">
{t("style.selectedLayerType", { type: isCzmlScene ? "czml" : layer.type })}
{t("style.selectedLayerType", {
type: isCesiumKmlLayer(layer)
? "KML / KMZ"
: isNativeDocumentScene
? "czml"
: layer.type,
})}
</p>
</aside>
);
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,10 @@
"polyline": {
"label": "إضافة طبقة خط متعدد مشفر",
"description": "استيراد سلاسل الخطوط المتعددة المشفرة (Google وOSRM وValhalla) من نص أو ملفات كخطوط متجهة."
},
"kml": {
"label": "KML / KMZ",
"description": "حمّل KML أو KMZ مع الأنماط والتراكبات وروابط الشبكة الأصلية على الكرة الأرضية."
}
},
"shared": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/fa.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@
"polyline": {
"label": "افزودن لایهٔ چندخطی کدگذاری‌شده",
"description": "وارد کردن رشته‌های چندخطی کدگذاری‌شده (Google، OSRM، Valhalla) از متن یا فایل‌ها به‌صورت خطوط برداری."
},
"kml": {
"label": "KML / KMZ",
"description": "KML یا KMZ را با سبک‌های بومی، هم‌پوشانی‌ها و پیوندهای شبکه روی کره بارگذاری کنید."
}
},
"shared": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@
"polyline": {
"label": "एन्कोडेड पॉलीलाइन लेयर जोड़ें",
"description": "एन्कोडेड पॉलीलाइन स्ट्रिंग (Google, OSRM, Valhalla) को टेक्स्ट या फ़ाइलों से वेक्टर लाइनों के रूप में आयात करें।"
},
"kml": {
"label": "KML / KMZ",
"description": "ग्लोब पर मूल शैलियों, ओवरले और नेटवर्क लिंक के साथ KML या KMZ लोड करें।"
}
},
"shared": {
Expand Down Expand Up @@ -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 इमेजरी",
Expand Down
9 changes: 9 additions & 0 deletions apps/geolibre-desktop/src/i18n/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading