Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 1 addition & 1 deletion apps/geolibre-desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@
"maplibre-gl-swipe": "^0.11.1",
"maplibre-gl-time-slider": "^1.8.4",
"maplibre-gl-usgs-lidar": "^0.11.1",
"maplibre-gl-vector": "^0.10.7",
"maplibre-gl-vector": "^0.10.8",
"openai": "^7.3.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.8",
Expand Down
74 changes: 64 additions & 10 deletions apps/geolibre-desktop/src/components/layout/DesktopShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
import { buildKmlModelLayer } from "../../lib/kml-model-layer";
import { isPhotoDropFileName, type GeotaggedPhotoResult } from "../../lib/geotagged-photos";
import type { LargeVectorDataset } from "../../lib/duckdb-vector-guard";
import { detectNonGeographicCoordinates } from "@geolibre/core";
import { PANEL_RESIZE_END_EVENT, PANEL_RESIZE_START_EVENT } from "../../lib/panel-resize";
import i18n from "../../i18n";
import {
Expand Down Expand Up @@ -806,6 +807,11 @@
const [mapReadyGeneration, setMapReadyGeneration] = useState(0);
const [dropMessage, setDropMessage] = useState<string | null>(null);
const [dropError, setDropError] = useState<string | null>(null);
// Kept out of `dropError` because the drop handler sets its own success
// message after `addImportedVectorLayers` returns, which would clobber this
// one. A mislabelled-CRS layer loads *successfully* and still renders
// nowhere, so both messages are true at once and need separate slots.
const [crsWarning, setCrsWarning] = useState<string | null>(null);
const [diagnosticsOpen, setDiagnosticsOpen] = useState(false);
const diagnostics = useDiagnosticsSnapshot();
const externalPluginsReady = useExternalPluginsReady(mapControllerRef);
Expand Down Expand Up @@ -1251,6 +1257,9 @@
const addImportedVectorLayers = useCallback(
(importedLayers: ImportedVectorLayer[]) => {
let lastLayerId: string | null = null;
// Layers whose coordinates cannot be WGS84; surfaced together after the
// loop so a multi-file drop reports once rather than per file.
const nonGeographic: string[] = [];
// Frame ids for each time-animated overlay sequence (keyed by the loader's
// group marker), so they can be gathered into one layer group afterward.
const frameGroups = new Map<string, string[]>();
Expand Down Expand Up @@ -1301,13 +1310,30 @@
}
// `||` (not `??`) so an empty-string name falls back to the path, and
// matches the name shown in the drop confirmation toast.
lastLayerId = addGeoJsonLayer(
layer.name || layerNameFromPath(layer.path),
layer.data,
layer.path,
);
const layerName = layer.name || layerNameFromPath(layer.path);
// A file that declares WGS84 but holds projected coordinates loads
// cleanly, lists in the Layers panel, and renders nowhere — the map
// simply never moves. Warn rather than fail: the data is readable and
// only the user knows its true CRS.
const offRange = detectNonGeographicCoordinates(layer.data);
if (offRange) {
nonGeographic.push(layerName);
console.warn(
`[GeoLibre] "${layerName}" declares geographic coordinates but its values are out of range ` +
`(max |x| ${Math.round(offRange.maxAbsX).toLocaleString()}, max |y| ${Math.round(offRange.maxAbsY).toLocaleString()} ` +
`over ${offRange.sampled.toLocaleString()} sampled coordinates). The file's CRS is almost certainly ` +
`mislabelled — reproject it, or correct its .prj/crs, and load it again.`,
);
}
lastLayerId = addGeoJsonLayer(layerName, layer.data, layer.path);
}

setCrsWarning(
nonGeographic.length > 0
? t("addData.nonGeographicCoordinates", { names: nonGeographic.join(", ") })
: null,
);
Comment thread
giswqs marked this conversation as resolved.

// Gather each time-animated overlay's frames into one collapsible group so
// the sequence reads as a single timeline entry, not N stacked layers.
const sequences = [...frameGroups.values()].filter((ids) => ids.length > 1);
Expand Down Expand Up @@ -1363,6 +1389,10 @@
useEffect(() => {
setKmlFileImportHandler(async (imports) => {
setDropError(null);
// Matches the drop handlers: the catch below sets `dropError` without
// reaching `addImportedVectorLayers`, so without this a previous file's
// banner would sit beside the new error.
setCrsWarning(null);
try {
const paths = imports
.map(({ sourcePath }) => sourcePath)
Expand All @@ -1372,20 +1402,20 @@
// pyramid, which a path-less browser File cannot support.
const layers =
paths.length === imports.length
? await loadDroppedVectorPaths(paths, {
onLargeDataset: confirmLargeVectorDataset,
})
? await loadDroppedVectorPaths(paths, { onLargeDataset: confirmLargeVectorDataset })
: await loadDroppedVectorFiles(
imports.map(({ file }) => file),
{ onLargeDataset: confirmLargeVectorDataset },
{
onLargeDataset: confirmLargeVectorDataset,
},
);
Comment thread
giswqs marked this conversation as resolved.
addImportedVectorLayers(layers);
} catch (error) {
setDropError(error instanceof Error ? error.message : t("kml.importFailed"));
}
});
return () => setKmlFileImportHandler(null);
}, [addImportedVectorLayers, confirmLargeVectorDataset, t]);
}, [addImportedVectorLayers, t]);
Comment thread
giswqs marked this conversation as resolved.

const addDroppedPhotos = useCallback(
(result: GeotaggedPhotoResult | null): number => {
Expand Down Expand Up @@ -1539,6 +1569,9 @@

setIsDraggingFiles(false);
setDropError(null);
// Matches the browser drop handler: a warning about a previous file
// must not linger over an unrelated drop.
setCrsWarning(null);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
setDropMessage("Importing data...");

try {
Expand Down Expand Up @@ -1661,7 +1694,7 @@
disposed = true;
unlisten?.();
};
}, [

Check warning on line 1697 in apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

View workflow job for this annotation

GitHub Actions / Build and test

React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array
clearDropMessageLater,
finishDrop,
addDroppedRasters,
Expand Down Expand Up @@ -1708,6 +1741,10 @@
dragDepthRef.current = 0;
setIsDraggingFiles(false);
setDropError(null);
// Not auto-dismissed on the status timeout (it has its own Close button),
// so it is cleared here instead: a warning about a previous file must not
// linger over an unrelated drop.
setCrsWarning(null);
setDropMessage("Importing data...");

try {
Expand Down Expand Up @@ -1805,7 +1842,7 @@
clearDropMessageLater();
}
},
[

Check warning on line 1845 in apps/geolibre-desktop/src/components/layout/DesktopShell.tsx

View workflow job for this annotation

GitHub Actions / Build and test

React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array
clearDropMessageLater,
finishDrop,
addDroppedRasters,
Expand Down Expand Up @@ -2642,6 +2679,23 @@
{projectUrlLoadState.error}
</div>
) : null}
{crsWarning ? (
<div
data-testid="crs-warning"
role="status"
aria-live="polite"
className="absolute bottom-24 left-1/2 z-50 max-w-[min(90vw,36rem)] -translate-x-1/2 rounded-md border border-destructive/40 bg-background px-3 py-2 text-center text-sm text-destructive shadow-lg"
>
{crsWarning}
<button
type="button"
onClick={() => setCrsWarning(null)}
className="ms-2 underline underline-offset-2"
>
{t("common.close")}
</button>
</div>
) : null}
{dropMessage || dropError ? (
<div
data-testid="drop-status"
Expand Down
25 changes: 24 additions & 1 deletion apps/geolibre-desktop/src/hooks/useProjectHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,30 @@ export function useProjectHistory(mapControllerRef: RefObject<MapController | nu
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
timerRef.current = window.setTimeout(() => {
timerRef.current = null;
const content = serializeProject(buildProjectSnapshot(mapControllerRef));
// `serializeProject` runs synchronously, so its failure cannot be caught
// by the promise chain below. A project embedding a large vector layer
// serializes to more than V8's 536,870,888-byte string cap and throws
// `RangeError: Invalid string length`, which previously escaped as an
// unhandled error on every autosave tick. Autosave is best-effort — a
// project too large to snapshot must degrade to "no crash recovery",
// never to a crash.
// Built and serialized in separate steps so the two failures are not
// conflated: a snapshot that cannot be constructed is a genuine error,
// while one too large to stringify is an expected limit.
let snapshot: ReturnType<typeof buildProjectSnapshot>;
try {
snapshot = buildProjectSnapshot(mapControllerRef);
} catch (error) {
console.error("Could not autosave the project.", error);
return;
}
let content: string;
try {
content = serializeProject(snapshot);
} catch (error) {
console.warn("Project autosave skipped: the project is too large to serialize.", error);
return;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
giswqs marked this conversation as resolved.
void addProjectSnapshot(content, currentProjectKey()).catch((error) =>
console.error("Could not autosave the project.", error),
);
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,8 @@
"errorMaxFeatures": "أدخل قيمة للحد الأقصى للمعالم تساوي 1 أو أكثر.",
"errorBbox": "أدخل الصندوق المحيط بالصيغة «غرب,جنوب,شرق,شمال».",
"errorNoFeatures": "لم تُرجع هذه المجموعة أي معالم."
}
},
"nonGeographicCoordinates": "الإحداثيات في {{names}} ليست خط طول/عرض، لذا لن يُرسم أي شيء على الخريطة. نظام الإحداثيات مذكور بشكل خاطئ — أعد إسقاط البيانات (أو صحّح ملف .prj) ثم حمّلها مرة أخرى."
},
"offline": {
"title": "تنزيل منطقة دون اتصال",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Geben Sie für die maximale Objektanzahl einen Wert von 1 oder mehr ein.",
"errorBbox": "Geben Sie den Begrenzungsrahmen als „West,Süd,Ost,Nord“ an.",
"errorNoFeatures": "Diese Collection hat keine Objekte zurückgegeben."
}
},
"nonGeographicCoordinates": "Die Koordinaten in {{names}} sind keine geografische Länge/Breite, daher wird nichts auf der Karte gezeichnet. Das CRS ist falsch angegeben — projizieren Sie die Daten um (oder korrigieren Sie die .prj) und laden Sie sie erneut."
},
"offline": {
"title": "Offline-Bereich herunterladen",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,8 @@
"lineWidth": "Line width",
"extrusion": "3D extrusion",
"modelUrlPlaceholder": "https://example.com/model.glb"
}
},
"nonGeographicCoordinates": "Coordinates in {{names}} are not longitude/latitude, so nothing will be drawn on the map. The CRS is mislabelled — reproject the data (or fix its .prj) and load it again."
},
"offline": {
"title": "Download Offline Area",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Introduzca un máximo de entidades de 1 o más.",
"errorBbox": "Introduzca el cuadro delimitador como «oeste,sur,este,norte».",
"errorNoFeatures": "Esta colección no devolvió ninguna entidad."
}
},
"nonGeographicCoordinates": "Las coordenadas de {{names}} no son de longitud/latitud, por lo que no se dibujará nada en el mapa. El CRS está mal declarado: reproyecte los datos (o corrija su .prj) y vuelva a cargarlos."
},
"offline": {
"title": "Descargar área sin conexión",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Saisissez un nombre maximal d'entités égal ou supérieur à 1.",
"errorBbox": "Saisissez la boîte englobante sous la forme « ouest,sud,est,nord ».",
"errorNoFeatures": "Cette collection n'a renvoyé aucune entité."
}
},
"nonGeographicCoordinates": "Les coordonnées de {{names}} ne sont pas des longitudes/latitudes : rien ne sera dessiné sur la carte. Le SCR est mal déclaré — reprojetez les données (ou corrigez le .prj) puis rechargez-les."
},
"offline": {
"title": "Télécharger une zone hors ligne",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/hi.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "अधिकतम फ़ीचर के लिए 1 या उससे अधिक मान दर्ज करें।",
"errorBbox": "बाउंडिंग बॉक्स को «पश्चिम,दक्षिण,पूर्व,उत्तर» के रूप में दर्ज करें।",
"errorNoFeatures": "इस कलेक्शन ने कोई फ़ीचर नहीं लौटाया।"
}
},
"nonGeographicCoordinates": "{{names}} के निर्देशांक देशांतर/अक्षांश नहीं हैं, इसलिए मानचित्र पर कुछ भी नहीं बनेगा। CRS ग़लत दर्ज है — डेटा को पुनःप्रक्षेपित करें (या .prj ठीक करें) और दोबारा लोड करें।"
},
"offline": {
"title": "ऑफ़लाइन क्षेत्र डाउनलोड करें",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/id.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@
"errorMaxFeatures": "Masukkan nilai jumlah fitur maksimum 1 atau lebih.",
"errorBbox": "Masukkan kotak pembatas sebagai \"barat,selatan,timur,utara\".",
"errorNoFeatures": "Koleksi ini tidak mengembalikan fitur apa pun."
}
},
"nonGeographicCoordinates": "Koordinat pada {{names}} bukan bujur/lintang, sehingga tidak ada yang digambar di peta. CRS salah — proyeksikan ulang data (atau perbaiki .prj-nya) lalu muat kembali."
},
"offline": {
"title": "Unduh Area Offline",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Inserisci un numero massimo di elementi pari a 1 o superiore.",
"errorBbox": "Inserisci il rettangolo di delimitazione come «ovest,sud,est,nord».",
"errorNoFeatures": "Questa collection non ha restituito alcun elemento."
}
},
"nonGeographicCoordinates": "Le coordinate in {{names}} non sono longitudine/latitudine, quindi non verrà disegnato nulla sulla mappa. Il CRS è errato: riproietta i dati (o correggi il .prj) e ricaricali."
},
"offline": {
"title": "Scarica area offline",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@
"errorMaxFeatures": "最大フィーチャ数には1以上の値を入力してください。",
"errorBbox": "バウンディングボックスを「西,南,東,北」の形式で入力してください。",
"errorNoFeatures": "このコレクションはフィーチャを返しませんでした。"
}
},
"nonGeographicCoordinates": "{{names}} の座標は経緯度ではないため、地図には何も描画されません。CRS が誤って記録されています — データを再投影する(または .prj を修正する)と読み込み直してください。"
},
"offline": {
"title": "オフラインエリアをダウンロード",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/ka.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"lineWidth": "ხაზის სიგანე",
"extrusion": "3D ამოწევა",
"modelUrlPlaceholder": "https://example.com/model.glb"
}
},
"nonGeographicCoordinates": "{{names}}-ის კოორდინატები არ არის გრძედი/განედი, ამიტომ რუკაზე არაფერი დაიხატება. CRS არასწორადაა მითითებული — გადააპროექტეთ მონაცემები (ან შეასწორეთ .prj) და ხელახლა ჩატვირთეთ."
},
"offline": {
"title": "ოფლაინ ტერიტორიის ჩამოტვირთვა",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@
"errorMaxFeatures": "최대 피처 수를 1 이상으로 입력하세요.",
"errorBbox": "경계 상자를 “서,남,동,북” 형식으로 입력하세요.",
"errorNoFeatures": "이 컬렉션은 피처를 반환하지 않았습니다."
}
},
"nonGeographicCoordinates": "{{names}}의 좌표가 경위도가 아니므로 지도에 아무것도 그려지지 않습니다. CRS가 잘못 지정되어 있습니다 — 데이터를 재투영하거나 .prj를 수정한 뒤 다시 불러오세요."
},
"offline": {
"title": "오프라인 지역 다운로드",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Voer voor het maximale aantal features een waarde van 1 of hoger in.",
"errorBbox": "Voer het begrenzingskader in als ‘west,zuid,oost,noord’.",
"errorNoFeatures": "Deze collectie leverde geen features op."
}
},
"nonGeographicCoordinates": "De coördinaten in {{names}} zijn geen lengte-/breedtegraad, dus er wordt niets op de kaart getekend. Het CRS klopt niet — herprojecteer de gegevens (of corrigeer de .prj) en laad ze opnieuw."
},
"offline": {
"title": "Offline gebied downloaden",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/pt.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Informe um valor de máximo de feições igual ou maior que 1.",
"errorBbox": "Informe a caixa delimitadora como “oeste,sul,leste,norte”.",
"errorNoFeatures": "Esta coleção não retornou nenhuma feição."
}
},
"nonGeographicCoordinates": "As coordenadas em {{names}} não são de longitude/latitude, portanto nada será desenhado no mapa. O CRS está incorreto — reprojete os dados (ou corrija o .prj) e carregue novamente."
},
"offline": {
"title": "Baixar área offline",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -783,7 +783,8 @@
"errorMaxFeatures": "Введите максимальное количество объектов, равное 1 или больше.",
"errorBbox": "Введите ограничивающий прямоугольник в виде «запад,юг,восток,север».",
"errorNoFeatures": "Эта коллекция не вернула ни одного объекта."
}
},
"nonGeographicCoordinates": "Координаты в {{names}} не являются долготой/широтой, поэтому на карте ничего не отобразится. СК указана неверно — перепроецируйте данные (или исправьте .prj) и загрузите снова."
},
"offline": {
"title": "Скачать офлайн-область",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/th.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@
"lineWidth": "ความหนาเส้น",
"extrusion": "การยกตัวแบบ 3 มิติ",
"modelUrlPlaceholder": "https://example.com/model.glb"
}
},
"nonGeographicCoordinates": "พิกัดใน {{names}} ไม่ใช่ลองจิจูด/ละติจูด จึงจะไม่มีสิ่งใดถูกวาดบนแผนที่ ระบบพิกัดระบุไว้ผิด — โปรดฉายพิกัดข้อมูลใหม่ (หรือแก้ไฟล์ .prj) แล้วโหลดอีกครั้ง"
},
"offline": {
"title": "ดาวน์โหลดพื้นที่แบบออฟไลน์",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/tr.json
Original file line number Diff line number Diff line change
Expand Up @@ -743,7 +743,8 @@
"errorMaxFeatures": "Maksimum öğe değeri olarak 1 veya daha büyük bir sayı girin.",
"errorBbox": "Sınırlayıcı kutuyu «batı,güney,doğu,kuzey» biçiminde girin.",
"errorNoFeatures": "Bu koleksiyon hiç öğe döndürmedi."
}
},
"nonGeographicCoordinates": "{{names}} içindeki koordinatlar boylam/enlem değil, bu yüzden haritaya hiçbir şey çizilmeyecek. CRS bilgisi yanlış — verileri yeniden projelendirin (veya .prj dosyasını düzeltin) ve tekrar yükleyin."
},
"offline": {
"title": "Çevrimdışı Alan İndir",
Expand Down
3 changes: 2 additions & 1 deletion apps/geolibre-desktop/src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -723,7 +723,8 @@
"errorMaxFeatures": "请输入不小于 1 的最大要素数。",
"errorBbox": "请按“西,南,东,北”的格式输入边界框。",
"errorNoFeatures": "此集合未返回任何要素。"
}
},
"nonGeographicCoordinates": "{{names}} 中的坐标不是经纬度,因此地图上不会绘制任何内容。坐标参考系标注有误 — 请重新投影数据(或修正 .prj)后再次加载。"
},
"offline": {
"title": "下载离线区域",
Expand Down
Loading
Loading