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
33 changes: 26 additions & 7 deletions apps/geolibre-desktop/src/lib/qgis-project-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -405,13 +405,23 @@ function parseLayerGroups(document: Document): {
id,
name,
collapsed: element.getAttribute("expanded") === "0",
visible: element.getAttribute("checked") !== "Qt::Unchecked",
visible: qgisGroupVisible(element, root),
opacity: 1,
};
});
return { groups, ids };
}

function qgisGroupVisible(element: Element, root: Element): boolean {
let current: Element | null = element;
while (current) {
if (current.getAttribute("checked") === "Qt::Unchecked") return false;
if (current === root) break;
current = current.parentElement?.closest("layer-tree-group") ?? null;
}
return true;
}

function groupDisplayName(element: Element, root: Element): string {
const names: string[] = [];
let current: Element | null = element;
Expand Down Expand Up @@ -456,25 +466,34 @@ function layerOrder(document: Document, mapLayers: Element[]): string[] {

function qgisSourcePath(dataSource: string, projectPath: string): string {
let source = dataSource.split("|", 1)[0]?.trim() ?? "";
let parsedFileUrl = false;
source = source.replace(/^['"]|['"]$/g, "");
source = source.replace(/^\/vsicurl(?:_streaming)?\//i, "");
if (/^\/?vsizip\//i.test(source)) return "";
if (source.startsWith("file://")) {
if (/^file:\/\//i.test(source)) {
try {
source = decodeURIComponent(new URL(source).pathname).replace(/^\/([A-Za-z]:)/, "$1");
const url = new URL(source);
const path = decodeURIComponent(url.pathname).replace(/^\/([A-Za-z]:)/, "$1");
parsedFileUrl = true;
source =
url.hostname && url.hostname.toLowerCase() !== "localhost"
? `//${url.hostname}${path}`
: path;
} catch {
source = source.slice("file://".length);
const path = source.replace(/^file:\/\//i, "");
source = path.startsWith("/") || /^[A-Za-z]:[/\\]/.test(path) ? path : `//${path}`;
}
Comment thread
giswqs marked this conversation as resolved.
}
source = source.replace(/^file:(?:\/\/)?/i, "");
source = source.replace(/[?#].*$/, "");
source = source.replace(/^['"]|['"]$/g, "");
if (!parsedFileUrl && !isHttpSource(source)) source = source.replace(/[?#].*$/, "");
if (!source || isAbsolutePath(source) || /^[a-z]+:\/\//i.test(source)) return source;
const directory = /[/\\]/.test(projectPath) ? projectPath.replace(/[/\\][^/\\]*$/, "") : "";
return normalizeJoinedPath(directory, source);
}

function sourceExtension(source: string): string {
return source.split(/[?#]/, 1)[0]?.split(".").pop()?.toLowerCase() ?? "";
const path = isHttpSource(source) ? source.split(/[?#]/, 1)[0] : source;
return path?.split(".").pop()?.toLowerCase() ?? "";
}

function normalizeJoinedPath(directory: string, relative: string): string {
Expand Down
84 changes: 83 additions & 1 deletion tests/qgis-project-import.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import { applyGroupEffects } from "@geolibre/core";
import { strToU8, zipSync } from "fflate";
import { DOMParser } from "linkedom";
import {
Expand Down Expand Up @@ -236,7 +237,68 @@ describe("QGIS project import", () => {
);
});

it("normalizes Windows file URLs, query strings, and bare project names", () => {
it("preserves remote URL credentials when materializing GeoJSON", async () => {
const remote = importQgisProject(
projectXml({
dataSources: [
{
id: "roads",
name: "Remote",
source:
'"/vsicurl/https://example.com/roads.geojson?token=secret&version=2"|layername=roads',
},
],
}),
"/work/example.qgs",
);
const requested: string[] = [];
await materializeQgisRemoteLayers(remote, async (input) => {
requested.push(String(input));
return Response.json({ type: "FeatureCollection", features: [] });
});

assert.deepEqual(requested, ["https://example.com/roads.geojson?token=secret&version=2"]);
});

it("rejects UNC file URLs", () => {
const unc = importQgisProject(
projectXml({
dataSources: [
{
id: "cities",
name: "Network file URL",
source: "file://server/share/cities.gpkg",
},
],
}),
"C:\\projects\\example.qgs",
);
assert.deepEqual(unc.project.layers, []);
assert.deepEqual(
unc.warnings.map((warning) => [warning.layerName, warning.reason]),
[["Network file URL", "network-path"]],
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

it("inherits visibility from unchecked parent groups", () => {
const xml = projectXml().replace(
'name="Transport" checked="Qt::Checked"',
'name="Transport" checked="Qt::Unchecked"',
);
const result = importQgisProject(xml, "/work/example.qgs");

assert.deepEqual(
result.project.layerGroups?.map((group) => [group.name, group.visible]),
[
["Transport", false],
["Transport / Places", false],
],
);
const rendered = applyGroupEffects(result.project.layers, result.project.layerGroups ?? []);
assert.equal(rendered.find((layer) => layer.name === "Cities")?.visible, false);
});

it("normalizes Windows file URLs, query strings, encoded delimiters, and bare names", () => {
const windows = importQgisProject(
projectXml({
dataSources: [
Expand All @@ -251,6 +313,26 @@ describe("QGIS project import", () => {
);
assert.equal(windows.project.layers[0].sourcePath, "C:/data/points.csv");

const encoded = importQgisProject(
projectXml({
dataSources: [
{ id: "roads", name: "Encoded delimiter", source: "file:///tmp/a%23b.geojson" },
],
}),
"/work/example.qgs",
);
assert.equal(encoded.project.layers[0].sourcePath, "/tmp/a#b.geojson");

const malformed = importQgisProject(
projectXml({
dataSources: [
{ id: "roads", name: "Malformed escape", source: "file://C:/data%zz/roads.geojson" },
],
}),
"C:\\projects\\example.qgs",
);
assert.equal(malformed.project.layers[0].sourcePath, "C:/data%zz/roads.geojson");

const browser = importQgisProject(
projectXml({
dataSources: [{ id: "roads", name: "Roads", source: "data/roads.geojson" }],
Expand Down
Loading