@qaecy/cue-cli 0.0.45 → 0.0.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/main.js +2269 -46
- package/package.json +1 -1
- package/readme.md +36 -0
package/main.js
CHANGED
|
@@ -372,7 +372,7 @@ var init_md5_builder_node = __esm({
|
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
// apps/desktop/cue-cli/src/main.ts
|
|
375
|
-
var
|
|
375
|
+
var import_commander2 = require("commander");
|
|
376
376
|
var import_fs8 = require("fs");
|
|
377
377
|
var import_path4 = require("path");
|
|
378
378
|
|
|
@@ -4721,6 +4721,53 @@ var prefixCC = {
|
|
|
4721
4721
|
rex: "http://purl.obolibrary.org/obo/REX_"
|
|
4722
4722
|
};
|
|
4723
4723
|
|
|
4724
|
+
// libs/js/prefixes/src/lib/compact-expand.ts
|
|
4725
|
+
var CompactExpand = class _CompactExpand {
|
|
4726
|
+
static _instance;
|
|
4727
|
+
// Opposite map is built once
|
|
4728
|
+
_nsMap;
|
|
4729
|
+
static getInstance() {
|
|
4730
|
+
if (this._instance) {
|
|
4731
|
+
return this._instance;
|
|
4732
|
+
}
|
|
4733
|
+
this._instance = new _CompactExpand();
|
|
4734
|
+
return this._instance;
|
|
4735
|
+
}
|
|
4736
|
+
compactIRI(iri) {
|
|
4737
|
+
let nsMap = this._nsMap;
|
|
4738
|
+
if (nsMap === void 0)
|
|
4739
|
+
nsMap = this._buildNSMap();
|
|
4740
|
+
let ns = iri;
|
|
4741
|
+
if (iri.includes("#"))
|
|
4742
|
+
ns = `${iri.split("#")[0]}#`;
|
|
4743
|
+
else {
|
|
4744
|
+
const parts = iri.split("/");
|
|
4745
|
+
parts.pop();
|
|
4746
|
+
ns = parts.join("/") + "/";
|
|
4747
|
+
}
|
|
4748
|
+
if (ns === void 0)
|
|
4749
|
+
return iri;
|
|
4750
|
+
const pfx = nsMap[ns];
|
|
4751
|
+
if (pfx === void 0)
|
|
4752
|
+
return iri;
|
|
4753
|
+
return iri.replace(ns, `${pfx}:`);
|
|
4754
|
+
}
|
|
4755
|
+
expandIRI(iri) {
|
|
4756
|
+
const prefix = iri.split(":")[0];
|
|
4757
|
+
const ns = prefixCC[prefix] ?? qaecyPrefixes[prefix];
|
|
4758
|
+
return iri.replace(`${prefix}:`, ns);
|
|
4759
|
+
}
|
|
4760
|
+
_buildNSMap() {
|
|
4761
|
+
const map = {};
|
|
4762
|
+
Object.keys(prefixCC).forEach((key) => map[prefixCC[key]] = key);
|
|
4763
|
+
Object.keys(qaecyPrefixes).forEach(
|
|
4764
|
+
(key) => map[qaecyPrefixes[key]] = key
|
|
4765
|
+
);
|
|
4766
|
+
this._nsMap = map;
|
|
4767
|
+
return map;
|
|
4768
|
+
}
|
|
4769
|
+
};
|
|
4770
|
+
|
|
4724
4771
|
// libs/js/sync-tools/src/lib/list-remote-files.ts
|
|
4725
4772
|
async function listRemoteFiles(spaceId, providerId, queryHandler2, verbose = false) {
|
|
4726
4773
|
const firebase = CueFirebase.getInstance();
|
|
@@ -5090,13 +5137,17 @@ var BUCKET_PERSISTENCE2 = "db_persistence_eu_west6";
|
|
|
5090
5137
|
var ENDPOINT_CONSUMPTION = "/data-views/admin/consumption";
|
|
5091
5138
|
var ENDPOINT_PROFILE_ORGANIZATIONS = "/data-views/admin/profile/organizations";
|
|
5092
5139
|
var ENDPOINT_PROFILE_API_KEYS = "/data-views/admin/profile/api-keys";
|
|
5140
|
+
var ENDPOINT_COMMANDS_PROFILE_API_KEYS = "/commands/admin/profile/api-keys";
|
|
5141
|
+
var ENDPOINT_COMMANDS_PROFILE_TERMS = "/commands/admin/profile/terms";
|
|
5093
5142
|
var ENDPOINT_ORG_MEMBERS = (orgId) => `/data-views/admin/organizations/${orgId}/members`;
|
|
5094
5143
|
var ENDPOINT_CREATE_PROJECT = "/commands/admin/project";
|
|
5095
5144
|
var ENDPOINT_SEARCH = "/assistant/search";
|
|
5096
5145
|
var ENDPOINT_FUSEKI_QUERY = "/triplestore/query";
|
|
5097
5146
|
var ENDPOINT_FUSEKI_UPDATE = "/triplestore/update";
|
|
5147
|
+
var ENDPOINT_FUSEKI_SHACL = "/triplestore/shacl";
|
|
5098
5148
|
var ENDPOINT_QLEVER_QUERY = "/qlever-server/qlever/query";
|
|
5099
5149
|
var ENDPOINT_QLEVER_UPDATE = "/qlever-server/qlever/update";
|
|
5150
|
+
var ENDPOINT_QLEVER_SHACL = "/qlever-server/qlever/shacl";
|
|
5100
5151
|
var ENDPOINT_FSS_BATCH = "/commands/file-system-structure/batch";
|
|
5101
5152
|
var MICROSOFT_PROVIDER_ID = "microsoft.com";
|
|
5102
5153
|
var SUPERADMIN_ROLE = "superadmin";
|
|
@@ -5221,6 +5272,28 @@ var CueAuth = class {
|
|
|
5221
5272
|
async signOut() {
|
|
5222
5273
|
await (0, import_auth3.signOut)(this._auth);
|
|
5223
5274
|
}
|
|
5275
|
+
/**
|
|
5276
|
+
* Register a new user by name and email.
|
|
5277
|
+
* The backend validates that the email domain belongs to an existing organisation,
|
|
5278
|
+
* creates the Firebase Auth account, assigns org membership, and dispatches a
|
|
5279
|
+
* "set your password" email to the address provided.
|
|
5280
|
+
* Returns the new user's UID and the organisation name on success.
|
|
5281
|
+
*/
|
|
5282
|
+
async signUp(name, email) {
|
|
5283
|
+
const response = await fetch(
|
|
5284
|
+
`${this._endpoints.gatewayUrl}/commands/admin/user/signup`,
|
|
5285
|
+
{
|
|
5286
|
+
method: "POST",
|
|
5287
|
+
headers: { "Content-Type": "application/json" },
|
|
5288
|
+
body: JSON.stringify({ name, email })
|
|
5289
|
+
}
|
|
5290
|
+
);
|
|
5291
|
+
if (!response.ok) {
|
|
5292
|
+
const body = await response.json().catch(() => ({}));
|
|
5293
|
+
throw new Error(body?.message ?? `Sign-up failed (${response.status})`);
|
|
5294
|
+
}
|
|
5295
|
+
return response.json();
|
|
5296
|
+
}
|
|
5224
5297
|
/** Currently signed-in user, or null if not authenticated */
|
|
5225
5298
|
get currentUser() {
|
|
5226
5299
|
return this._auth.currentUser;
|
|
@@ -5266,6 +5339,49 @@ var CueAuth = class {
|
|
|
5266
5339
|
}
|
|
5267
5340
|
};
|
|
5268
5341
|
|
|
5342
|
+
// libs/js/cue-sdk/src/lib/tables.ts
|
|
5343
|
+
var ENDPOINT_DATA_VIEWS_TABLES = "/data-views/tables";
|
|
5344
|
+
var ENDPOINT_COMMANDS_TABLES = "/commands/tables";
|
|
5345
|
+
var CueTables = class {
|
|
5346
|
+
constructor(_auth, _gatewayUrl) {
|
|
5347
|
+
this._auth = _auth;
|
|
5348
|
+
this._gatewayUrl = _gatewayUrl;
|
|
5349
|
+
}
|
|
5350
|
+
async listTables(projectId) {
|
|
5351
|
+
const response = await this._auth.authenticatedFetch(
|
|
5352
|
+
`${this._gatewayUrl}${ENDPOINT_DATA_VIEWS_TABLES}`,
|
|
5353
|
+
{
|
|
5354
|
+
headers: {
|
|
5355
|
+
"Content-Type": "application/json",
|
|
5356
|
+
"x-project-id": projectId,
|
|
5357
|
+
"cue-project-id": projectId
|
|
5358
|
+
}
|
|
5359
|
+
}
|
|
5360
|
+
);
|
|
5361
|
+
if (!response.ok) {
|
|
5362
|
+
throw new Error(`Failed to list tables: ${response.status} ${response.statusText}`);
|
|
5363
|
+
}
|
|
5364
|
+
return response.json();
|
|
5365
|
+
}
|
|
5366
|
+
async saveTables(tables, projectId) {
|
|
5367
|
+
const response = await this._auth.authenticatedFetch(
|
|
5368
|
+
`${this._gatewayUrl}${ENDPOINT_COMMANDS_TABLES}`,
|
|
5369
|
+
{
|
|
5370
|
+
method: "PUT",
|
|
5371
|
+
headers: {
|
|
5372
|
+
"Content-Type": "application/json",
|
|
5373
|
+
"x-project-id": projectId,
|
|
5374
|
+
"cue-project-id": projectId
|
|
5375
|
+
},
|
|
5376
|
+
body: JSON.stringify(tables)
|
|
5377
|
+
}
|
|
5378
|
+
);
|
|
5379
|
+
if (!response.ok) {
|
|
5380
|
+
throw new Error(`Failed to save tables: ${response.status} ${response.statusText}`);
|
|
5381
|
+
}
|
|
5382
|
+
}
|
|
5383
|
+
};
|
|
5384
|
+
|
|
5269
5385
|
// libs/js/cue-sdk/src/lib/api.ts
|
|
5270
5386
|
var CueApi = class {
|
|
5271
5387
|
constructor(_auth, _gatewayUrl, projects, sync) {
|
|
@@ -5273,6 +5389,14 @@ var CueApi = class {
|
|
|
5273
5389
|
this._gatewayUrl = _gatewayUrl;
|
|
5274
5390
|
this.projects = projects;
|
|
5275
5391
|
this.sync = sync;
|
|
5392
|
+
this.tables = new CueTables(_auth, _gatewayUrl);
|
|
5393
|
+
}
|
|
5394
|
+
tables;
|
|
5395
|
+
/** Active language used for language-sensitive SPARQL queries across all project classes. */
|
|
5396
|
+
language = "en";
|
|
5397
|
+
/** Updates the active language. All project classes (`CueProjectSchema`, `CueProjectDocuments`, `CueProjectEntities`) read this at query time. */
|
|
5398
|
+
setLanguage(lang) {
|
|
5399
|
+
this.language = lang;
|
|
5276
5400
|
}
|
|
5277
5401
|
/**
|
|
5278
5402
|
* Returns standard authentication headers for the current user.
|
|
@@ -5316,9 +5440,12 @@ var CueApi = class {
|
|
|
5316
5440
|
* The user must be authenticated before calling this.
|
|
5317
5441
|
*/
|
|
5318
5442
|
async sparql(query3, projectId, graphType) {
|
|
5319
|
-
|
|
5320
|
-
|
|
5321
|
-
|
|
5443
|
+
if (!graphType) {
|
|
5444
|
+
const project = await this.projects.getProject(projectId);
|
|
5445
|
+
graphType = project?.projectSettings?.graph?.type ?? "qlever";
|
|
5446
|
+
}
|
|
5447
|
+
const endpoint = graphType === "fuseki" ? ENDPOINT_FUSEKI_QUERY : ENDPOINT_QLEVER_QUERY;
|
|
5448
|
+
console.log(`Executing SPARQL query against ${endpoint} for project ${projectId} with graph type ${graphType}`);
|
|
5322
5449
|
const urlencoded = new URLSearchParams();
|
|
5323
5450
|
urlencoded.append("query", query3);
|
|
5324
5451
|
const response = await this._auth.authenticatedFetch(
|
|
@@ -5341,6 +5468,57 @@ var CueApi = class {
|
|
|
5341
5468
|
}
|
|
5342
5469
|
return response.json();
|
|
5343
5470
|
}
|
|
5471
|
+
/**
|
|
5472
|
+
* Validate a SHACL shape against the project's triplestore.
|
|
5473
|
+
*
|
|
5474
|
+
* @param shape - SHACL shapes graph in Turtle syntax.
|
|
5475
|
+
* @param projectId - Project to validate against.
|
|
5476
|
+
* @param options - `format`: `'json-ld'` (default, structured result) or `'turtle'` (raw string).
|
|
5477
|
+
* `verbose`: include server-side timing logs.
|
|
5478
|
+
*
|
|
5479
|
+
* Returns a {@link ShaclValidationReport} for JSON-LD, or a Turtle string.
|
|
5480
|
+
*/
|
|
5481
|
+
async shacl(shape, projectId, options) {
|
|
5482
|
+
const fmt = options?.format ?? "json-ld";
|
|
5483
|
+
const accept = fmt === "turtle" ? "text/turtle" : "application/ld+json";
|
|
5484
|
+
if (options?.graphType === "fuseki") {
|
|
5485
|
+
const response2 = await this._auth.authenticatedFetch(
|
|
5486
|
+
`${this._gatewayUrl}${ENDPOINT_FUSEKI_SHACL}`,
|
|
5487
|
+
{
|
|
5488
|
+
method: "POST",
|
|
5489
|
+
headers: {
|
|
5490
|
+
"Content-Type": "text/turtle",
|
|
5491
|
+
"Accept": accept,
|
|
5492
|
+
"x-project-id": projectId,
|
|
5493
|
+
"cue-project-id": projectId
|
|
5494
|
+
},
|
|
5495
|
+
body: shape
|
|
5496
|
+
}
|
|
5497
|
+
);
|
|
5498
|
+
if (!response2.ok) {
|
|
5499
|
+
const msg = await response2.text().catch(() => "");
|
|
5500
|
+
throw new Error(`SHACL validation failed: ${response2.status} ${response2.statusText}${msg ? " \u2014 " + msg.slice(0, 200) : ""}`);
|
|
5501
|
+
}
|
|
5502
|
+
return fmt === "turtle" ? response2.text() : response2.json();
|
|
5503
|
+
}
|
|
5504
|
+
const url = `${this._gatewayUrl}${ENDPOINT_QLEVER_SHACL}${options?.verbose ? "?verbose=true" : ""}`;
|
|
5505
|
+
const body = new URLSearchParams({ shape });
|
|
5506
|
+
const response = await this._auth.authenticatedFetch(url, {
|
|
5507
|
+
method: "POST",
|
|
5508
|
+
headers: {
|
|
5509
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
5510
|
+
"Accept": accept,
|
|
5511
|
+
"x-project-id": projectId,
|
|
5512
|
+
"cue-project-id": projectId
|
|
5513
|
+
},
|
|
5514
|
+
body
|
|
5515
|
+
});
|
|
5516
|
+
if (!response.ok) {
|
|
5517
|
+
const msg = await response.text().catch(() => "");
|
|
5518
|
+
throw new Error(`SHACL validation failed: ${response.status} ${response.statusText}${msg ? " \u2014 " + msg.slice(0, 200) : ""}`);
|
|
5519
|
+
}
|
|
5520
|
+
return fmt === "turtle" ? response.text() : response.json();
|
|
5521
|
+
}
|
|
5344
5522
|
async getConsumption(projectId) {
|
|
5345
5523
|
const response = await this._auth.authenticatedFetch(
|
|
5346
5524
|
`${this._gatewayUrl}${ENDPOINT_CONSUMPTION}`,
|
|
@@ -5361,6 +5539,1730 @@ var CueApi = class {
|
|
|
5361
5539
|
}
|
|
5362
5540
|
};
|
|
5363
5541
|
|
|
5542
|
+
// libs/js/cue-gis/src/lib/models.ts
|
|
5543
|
+
var FEATURE_CATEGORIES = [
|
|
5544
|
+
"building",
|
|
5545
|
+
"cadastre",
|
|
5546
|
+
"zone",
|
|
5547
|
+
"address",
|
|
5548
|
+
"poi",
|
|
5549
|
+
"greenspace",
|
|
5550
|
+
"paved",
|
|
5551
|
+
"railway",
|
|
5552
|
+
"natural",
|
|
5553
|
+
"manmade"
|
|
5554
|
+
];
|
|
5555
|
+
var NOMINATIM_FEATURE_CATEGORIES = [
|
|
5556
|
+
"address",
|
|
5557
|
+
"poi",
|
|
5558
|
+
"railway",
|
|
5559
|
+
"natural",
|
|
5560
|
+
"manmade"
|
|
5561
|
+
];
|
|
5562
|
+
var FEATURE_CATEGORY_DESCRIPTORS = {
|
|
5563
|
+
address: {
|
|
5564
|
+
category: "address",
|
|
5565
|
+
label: "Address",
|
|
5566
|
+
description: "Official addresses, streets, and addressable access points.",
|
|
5567
|
+
preferredColor: "#1d4ed8"
|
|
5568
|
+
},
|
|
5569
|
+
poi: {
|
|
5570
|
+
category: "poi",
|
|
5571
|
+
label: "Points of interest",
|
|
5572
|
+
description: "Places and amenities relevant to everyday operations and access.",
|
|
5573
|
+
preferredColor: "#059669"
|
|
5574
|
+
},
|
|
5575
|
+
railway: {
|
|
5576
|
+
category: "railway",
|
|
5577
|
+
label: "Railway",
|
|
5578
|
+
description: "Rail infrastructure, stations, and rail-adjacent transport context.",
|
|
5579
|
+
preferredColor: "#dc2626"
|
|
5580
|
+
},
|
|
5581
|
+
natural: {
|
|
5582
|
+
category: "natural",
|
|
5583
|
+
label: "Natural features",
|
|
5584
|
+
description: "Natural and hydrological features such as watercourses, forests, and land cover.",
|
|
5585
|
+
preferredColor: "#0ea5e9"
|
|
5586
|
+
},
|
|
5587
|
+
manmade: {
|
|
5588
|
+
category: "manmade",
|
|
5589
|
+
label: "Built features",
|
|
5590
|
+
description: "Structures and man-made elements not covered by more specific categories.",
|
|
5591
|
+
preferredColor: "#ca8a04"
|
|
5592
|
+
},
|
|
5593
|
+
cadastre: {
|
|
5594
|
+
category: "cadastre",
|
|
5595
|
+
label: "Cadastre",
|
|
5596
|
+
description: "Land parcels, legal boundaries, official surveying, and public-law land restrictions.",
|
|
5597
|
+
preferredColor: "#7c3aed"
|
|
5598
|
+
},
|
|
5599
|
+
building: {
|
|
5600
|
+
category: "building",
|
|
5601
|
+
label: "Buildings",
|
|
5602
|
+
description: "Building footprints and structures from official registry or cadastral data.",
|
|
5603
|
+
preferredColor: "#f97316"
|
|
5604
|
+
},
|
|
5605
|
+
greenspace: {
|
|
5606
|
+
category: "greenspace",
|
|
5607
|
+
label: "Green spaces",
|
|
5608
|
+
description: "Parks, gardens, playgrounds, sports facilities, and other vegetated open areas.",
|
|
5609
|
+
preferredColor: "#16a34a"
|
|
5610
|
+
},
|
|
5611
|
+
paved: {
|
|
5612
|
+
category: "paved",
|
|
5613
|
+
label: "Paved surfaces",
|
|
5614
|
+
description: "Courtyards, sidewalks, parking areas, and other sealed ground surfaces.",
|
|
5615
|
+
preferredColor: "#78716c"
|
|
5616
|
+
},
|
|
5617
|
+
zone: {
|
|
5618
|
+
category: "zone",
|
|
5619
|
+
label: "Planning zones",
|
|
5620
|
+
description: "Land-use zones, local plans, and public-law planning constraints (Nutzungsplanung / Lokalplan).",
|
|
5621
|
+
preferredColor: "#db2777"
|
|
5622
|
+
}
|
|
5623
|
+
};
|
|
5624
|
+
function featureCategoryDescriptor(category) {
|
|
5625
|
+
return FEATURE_CATEGORY_DESCRIPTORS[category];
|
|
5626
|
+
}
|
|
5627
|
+
var ZONE_PLAN_TYPE_COLORS = {
|
|
5628
|
+
"land-use-plan": "#f59e0b",
|
|
5629
|
+
// amber – primary zoning
|
|
5630
|
+
"local-plan": "#ec4899",
|
|
5631
|
+
// pink – Danish lokalplan
|
|
5632
|
+
"design-plan": "#3b82f6",
|
|
5633
|
+
// blue – Gestaltungsplan
|
|
5634
|
+
"development-plan": "#0891b2",
|
|
5635
|
+
// cyan – Bebauungsplan
|
|
5636
|
+
"special-use-plan": "#f97316",
|
|
5637
|
+
// orange – Sondernutzungsplan
|
|
5638
|
+
"open-space-zone": "#22c55e",
|
|
5639
|
+
// green – Freihaltezone
|
|
5640
|
+
"overlay-regulation": "#a855f7",
|
|
5641
|
+
// purple – overlaying regulations
|
|
5642
|
+
"unzoned": "#94a3b8",
|
|
5643
|
+
// slate-400 – nicht zonierte Fläche
|
|
5644
|
+
"municipal-plan-framework": "#6366f1",
|
|
5645
|
+
// indigo – Kommuneplanramme
|
|
5646
|
+
"neighbourhood-conservation-plan": "#14b8a6",
|
|
5647
|
+
// teal – Quartiererhaltungszonenplan
|
|
5648
|
+
"noise-sensitivity-plan": "#eab308",
|
|
5649
|
+
// yellow – Lärmempfindlichkeitsstufenplan
|
|
5650
|
+
"water-protection-plan": "#06b6d4",
|
|
5651
|
+
// sky – Gewässerschutzzonenplan
|
|
5652
|
+
"nature-landscape-plan": "#16a34a"
|
|
5653
|
+
// dark-green – Natur- und Landschaftsschutz
|
|
5654
|
+
};
|
|
5655
|
+
function bboxIntersects(a5, b) {
|
|
5656
|
+
return a5[0] < b[2] && a5[2] > b[0] && a5[1] < b[3] && a5[3] > b[1];
|
|
5657
|
+
}
|
|
5658
|
+
|
|
5659
|
+
// libs/js/cue-gis/src/lib/nominatim-adapter.ts
|
|
5660
|
+
var NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org";
|
|
5661
|
+
var DEFAULT_USER_AGENT = "cue-gis/0.0.1";
|
|
5662
|
+
var LAYER_PROBE_QUERY = {
|
|
5663
|
+
address: "street",
|
|
5664
|
+
poi: "shop",
|
|
5665
|
+
railway: "station",
|
|
5666
|
+
natural: "park",
|
|
5667
|
+
manmade: "building",
|
|
5668
|
+
cadastre: "parcel",
|
|
5669
|
+
building: "building",
|
|
5670
|
+
greenspace: "park",
|
|
5671
|
+
paved: "road",
|
|
5672
|
+
zone: "boundary"
|
|
5673
|
+
};
|
|
5674
|
+
var NOMINATIM_SOURCE_ID = "nominatim";
|
|
5675
|
+
function toLayerId(category) {
|
|
5676
|
+
return `${NOMINATIM_SOURCE_ID}:${category}`;
|
|
5677
|
+
}
|
|
5678
|
+
function toLayerDescriptor(category) {
|
|
5679
|
+
const descriptor = featureCategoryDescriptor(category);
|
|
5680
|
+
return {
|
|
5681
|
+
id: toLayerId(category),
|
|
5682
|
+
sourceId: NOMINATIM_SOURCE_ID,
|
|
5683
|
+
sourceLayerId: category,
|
|
5684
|
+
category,
|
|
5685
|
+
preferredColor: descriptor.preferredColor,
|
|
5686
|
+
tier: "raw",
|
|
5687
|
+
label: descriptor.label,
|
|
5688
|
+
description: `Nominatim ${category} layer`,
|
|
5689
|
+
labelKey: `gis.layer.${NOMINATIM_SOURCE_ID}.${category}.label`,
|
|
5690
|
+
descriptionKey: `gis.layer.${NOMINATIM_SOURCE_ID}.${category}.description`
|
|
5691
|
+
};
|
|
5692
|
+
}
|
|
5693
|
+
var NominatimAdapter = class {
|
|
5694
|
+
baseUrl;
|
|
5695
|
+
userAgent;
|
|
5696
|
+
email;
|
|
5697
|
+
limit;
|
|
5698
|
+
constructor(options = {}) {
|
|
5699
|
+
this.baseUrl = options.baseUrl ?? NOMINATIM_BASE_URL;
|
|
5700
|
+
this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
|
|
5701
|
+
this.email = options.email;
|
|
5702
|
+
this.limit = Math.min(options.limit ?? 40, 40);
|
|
5703
|
+
}
|
|
5704
|
+
async listFeatureCategoryDescriptors(bbox) {
|
|
5705
|
+
const checks = await Promise.allSettled(
|
|
5706
|
+
NOMINATIM_FEATURE_CATEGORIES.map(async (category) => {
|
|
5707
|
+
const results = await this.search(bbox, category);
|
|
5708
|
+
return { category, hasResults: results.length > 0 };
|
|
5709
|
+
})
|
|
5710
|
+
);
|
|
5711
|
+
return checks.filter(
|
|
5712
|
+
(r) => r.status === "fulfilled" && r.value.hasResults
|
|
5713
|
+
).map((r) => featureCategoryDescriptor(r.value.category));
|
|
5714
|
+
}
|
|
5715
|
+
async listAvailableLayers(bbox) {
|
|
5716
|
+
const categories = await this.listFeatureCategoryDescriptors(bbox);
|
|
5717
|
+
return categories.map((category) => toLayerDescriptor(category.category));
|
|
5718
|
+
}
|
|
5719
|
+
async getFeaturesForLayer(bbox, layerId) {
|
|
5720
|
+
const category = FEATURE_CATEGORIES.find((candidate) => toLayerId(candidate) === layerId);
|
|
5721
|
+
if (!category) {
|
|
5722
|
+
return [];
|
|
5723
|
+
}
|
|
5724
|
+
return this.search(bbox, category);
|
|
5725
|
+
}
|
|
5726
|
+
async listFeatureCategories(bbox) {
|
|
5727
|
+
const descriptors = await this.listFeatureCategoryDescriptors(bbox);
|
|
5728
|
+
return descriptors.map((descriptor) => descriptor.category);
|
|
5729
|
+
}
|
|
5730
|
+
async getFeaturesOfCategory(bbox, category) {
|
|
5731
|
+
return this.getFeaturesForLayer(bbox, toLayerId(category));
|
|
5732
|
+
}
|
|
5733
|
+
/**
|
|
5734
|
+
* Low-level search against the Nominatim API. Uses a per-layer probe query
|
|
5735
|
+
* so bounded viewbox searches return meaningful results.
|
|
5736
|
+
*/
|
|
5737
|
+
async search(bbox, layer) {
|
|
5738
|
+
const [west, south, east, north] = bbox;
|
|
5739
|
+
const q = layer ? LAYER_PROBE_QUERY[layer] : "place";
|
|
5740
|
+
const params = new URLSearchParams({
|
|
5741
|
+
q,
|
|
5742
|
+
format: "jsonv2",
|
|
5743
|
+
viewbox: `${west},${north},${east},${south}`,
|
|
5744
|
+
bounded: "1",
|
|
5745
|
+
limit: String(this.limit),
|
|
5746
|
+
addressdetails: "1",
|
|
5747
|
+
extratags: "1",
|
|
5748
|
+
dedupe: "1"
|
|
5749
|
+
});
|
|
5750
|
+
if (layer) {
|
|
5751
|
+
params.set("layer", layer);
|
|
5752
|
+
}
|
|
5753
|
+
if (this.email) {
|
|
5754
|
+
params.set("email", this.email);
|
|
5755
|
+
}
|
|
5756
|
+
const url = `${this.baseUrl}/search?${params.toString()}`;
|
|
5757
|
+
const response = await fetch(url, {
|
|
5758
|
+
headers: {
|
|
5759
|
+
"User-Agent": this.userAgent,
|
|
5760
|
+
Accept: "application/json"
|
|
5761
|
+
}
|
|
5762
|
+
});
|
|
5763
|
+
if (!response.ok) {
|
|
5764
|
+
throw new Error(
|
|
5765
|
+
`Nominatim request failed: ${response.status} ${response.statusText}`
|
|
5766
|
+
);
|
|
5767
|
+
}
|
|
5768
|
+
const results = await response.json();
|
|
5769
|
+
return results.map((r) => this.toGisFeature(r, layer));
|
|
5770
|
+
}
|
|
5771
|
+
toGisFeature(r, category) {
|
|
5772
|
+
const [s, n, w, e] = r.boundingbox;
|
|
5773
|
+
const resolvedCategory = category ?? r.category;
|
|
5774
|
+
const descriptor = featureCategoryDescriptor(resolvedCategory);
|
|
5775
|
+
return {
|
|
5776
|
+
id: `${r.osm_type}/${r.osm_id}`,
|
|
5777
|
+
sourceId: NOMINATIM_SOURCE_ID,
|
|
5778
|
+
sourceFeatureId: `${r.osm_type}/${r.osm_id}`,
|
|
5779
|
+
layerId: toLayerId(resolvedCategory),
|
|
5780
|
+
name: r.name ?? r.display_name.split(",")[0].trim(),
|
|
5781
|
+
category: resolvedCategory,
|
|
5782
|
+
preferredColor: descriptor.preferredColor,
|
|
5783
|
+
type: r.type,
|
|
5784
|
+
lat: parseFloat(r.lat),
|
|
5785
|
+
lon: parseFloat(r.lon),
|
|
5786
|
+
displayName: r.display_name,
|
|
5787
|
+
bbox: [parseFloat(w), parseFloat(s), parseFloat(e), parseFloat(n)],
|
|
5788
|
+
tier: "raw",
|
|
5789
|
+
originalData: r
|
|
5790
|
+
};
|
|
5791
|
+
}
|
|
5792
|
+
};
|
|
5793
|
+
|
|
5794
|
+
// libs/js/cue-gis/src/lib/use-classifier.ts
|
|
5795
|
+
var DK_BBR_BUILDING = {
|
|
5796
|
+
// 100s – Residential
|
|
5797
|
+
"110": "residential",
|
|
5798
|
+
"120": "residential",
|
|
5799
|
+
"121": "residential",
|
|
5800
|
+
"122": "residential",
|
|
5801
|
+
"130": "residential",
|
|
5802
|
+
"131": "residential",
|
|
5803
|
+
"132": "residential",
|
|
5804
|
+
"140": "residential",
|
|
5805
|
+
"150": "residential",
|
|
5806
|
+
"160": "residential",
|
|
5807
|
+
"190": "residential",
|
|
5808
|
+
// 200s – Recreational / holiday
|
|
5809
|
+
"210": "recreational",
|
|
5810
|
+
"212": "recreational",
|
|
5811
|
+
"220": "recreational",
|
|
5812
|
+
"221": "recreational",
|
|
5813
|
+
"230": "recreational",
|
|
5814
|
+
"290": "residential",
|
|
5815
|
+
// 300s – Agricultural
|
|
5816
|
+
"310": "agricultural",
|
|
5817
|
+
"311": "agricultural",
|
|
5818
|
+
"312": "agricultural",
|
|
5819
|
+
"320": "agricultural",
|
|
5820
|
+
"321": "agricultural",
|
|
5821
|
+
"322": "agricultural",
|
|
5822
|
+
"330": "agricultural",
|
|
5823
|
+
"390": "agricultural",
|
|
5824
|
+
// 400s – Commercial
|
|
5825
|
+
"410": "commercial",
|
|
5826
|
+
"411": "commercial",
|
|
5827
|
+
"420": "commercial",
|
|
5828
|
+
"421": "commercial",
|
|
5829
|
+
"422": "commercial",
|
|
5830
|
+
"430": "commercial",
|
|
5831
|
+
"431": "commercial",
|
|
5832
|
+
"432": "commercial",
|
|
5833
|
+
"440": "commercial",
|
|
5834
|
+
"490": "commercial",
|
|
5835
|
+
// 500s – Industrial / infrastructure
|
|
5836
|
+
"510": "industrial",
|
|
5837
|
+
"511": "industrial",
|
|
5838
|
+
"519": "industrial",
|
|
5839
|
+
"520": "infrastructure",
|
|
5840
|
+
"521": "infrastructure",
|
|
5841
|
+
"522": "infrastructure",
|
|
5842
|
+
"529": "infrastructure",
|
|
5843
|
+
"530": "infrastructure",
|
|
5844
|
+
"531": "infrastructure",
|
|
5845
|
+
"532": "infrastructure",
|
|
5846
|
+
"533": "infrastructure",
|
|
5847
|
+
"534": "infrastructure",
|
|
5848
|
+
"535": "infrastructure",
|
|
5849
|
+
"540": "infrastructure",
|
|
5850
|
+
"585": "infrastructure",
|
|
5851
|
+
"590": "industrial",
|
|
5852
|
+
// 600s – Public / institutional
|
|
5853
|
+
"610": "public",
|
|
5854
|
+
"620": "public",
|
|
5855
|
+
"621": "public",
|
|
5856
|
+
"622": "public",
|
|
5857
|
+
"630": "public",
|
|
5858
|
+
"640": "public",
|
|
5859
|
+
"650": "recreational",
|
|
5860
|
+
"660": "public",
|
|
5861
|
+
"670": "public",
|
|
5862
|
+
"680": "public",
|
|
5863
|
+
"690": "public",
|
|
5864
|
+
// 900s – Accessory / utility
|
|
5865
|
+
"910": "other",
|
|
5866
|
+
"920": "other",
|
|
5867
|
+
"930": "other",
|
|
5868
|
+
"940": "other",
|
|
5869
|
+
"950": "infrastructure",
|
|
5870
|
+
// Teknikbygning (utility / technical building)
|
|
5871
|
+
"960": "agricultural",
|
|
5872
|
+
"970": "agricultural",
|
|
5873
|
+
"990": "other"
|
|
5874
|
+
};
|
|
5875
|
+
var CH_GWR_BUILDING = {
|
|
5876
|
+
"1010": "residential",
|
|
5877
|
+
"1020": "mixed",
|
|
5878
|
+
"1030": "residential",
|
|
5879
|
+
"1040": "other",
|
|
5880
|
+
"1060": "recreational",
|
|
5881
|
+
"1080": "residential",
|
|
5882
|
+
"1110": "commercial",
|
|
5883
|
+
"1120": "commercial",
|
|
5884
|
+
"1130": "commercial",
|
|
5885
|
+
"1140": "industrial",
|
|
5886
|
+
"1150": "infrastructure",
|
|
5887
|
+
"1160": "infrastructure",
|
|
5888
|
+
"1210": "public",
|
|
5889
|
+
"1220": "public",
|
|
5890
|
+
"1230": "public",
|
|
5891
|
+
"1240": "recreational",
|
|
5892
|
+
"1241": "public",
|
|
5893
|
+
"1242": "recreational",
|
|
5894
|
+
"1251": "public",
|
|
5895
|
+
"1261": "agricultural",
|
|
5896
|
+
"1262": "agricultural",
|
|
5897
|
+
"1263": "agricultural",
|
|
5898
|
+
"1275": "other",
|
|
5899
|
+
"1276": "other",
|
|
5900
|
+
"1277": "infrastructure",
|
|
5901
|
+
"1278": "infrastructure",
|
|
5902
|
+
"1281": "infrastructure",
|
|
5903
|
+
"1282": "infrastructure",
|
|
5904
|
+
"9999": "other"
|
|
5905
|
+
};
|
|
5906
|
+
var CH_GWR_GKLAS = {
|
|
5907
|
+
// Residential
|
|
5908
|
+
"1110": "residential",
|
|
5909
|
+
// Einfamilienhaus
|
|
5910
|
+
"1121": "recreational",
|
|
5911
|
+
// Ferienhaus / Sommerwohnsitz
|
|
5912
|
+
"1122": "residential",
|
|
5913
|
+
// Mehrfamilienhaus
|
|
5914
|
+
"1130": "mixed",
|
|
5915
|
+
// Wohn- und Geschäftshaus
|
|
5916
|
+
"1140": "mixed",
|
|
5917
|
+
// Wohn- und Landwirtschaftsgebäude
|
|
5918
|
+
// Hospitality
|
|
5919
|
+
"1211": "commercial",
|
|
5920
|
+
// Hotel, Motel
|
|
5921
|
+
"1212": "commercial",
|
|
5922
|
+
// Gaststätten, Restaurant
|
|
5923
|
+
"1213": "mixed",
|
|
5924
|
+
// Pension, Heim (care home / pension)
|
|
5925
|
+
// Office / commercial
|
|
5926
|
+
"1220": "commercial",
|
|
5927
|
+
// Büro- und Verwaltungsgebäude
|
|
5928
|
+
"1230": "commercial",
|
|
5929
|
+
// Handelsbau (retail)
|
|
5930
|
+
"1231": "commercial",
|
|
5931
|
+
// Grosshandel / Grossmarkt
|
|
5932
|
+
// Infrastructure / transport
|
|
5933
|
+
"1241": "infrastructure",
|
|
5934
|
+
// Bahnhof, Flughafen
|
|
5935
|
+
// Leisure
|
|
5936
|
+
"1242": "recreational",
|
|
5937
|
+
// Freizeitgebäude
|
|
5938
|
+
"1264": "recreational",
|
|
5939
|
+
// Sportgebäude
|
|
5940
|
+
// Industrial
|
|
5941
|
+
"1251": "industrial",
|
|
5942
|
+
// Fabrik, Werkstatt
|
|
5943
|
+
"1252": "industrial",
|
|
5944
|
+
// Lagerhalle, Silo
|
|
5945
|
+
// Public / institutional
|
|
5946
|
+
"1261": "public",
|
|
5947
|
+
// Schulen, Bildungsstätten
|
|
5948
|
+
"1262": "public",
|
|
5949
|
+
// Spitäler, Kliniken
|
|
5950
|
+
"1263": "public",
|
|
5951
|
+
// Universität, Forschungsstätte
|
|
5952
|
+
"1265": "public",
|
|
5953
|
+
// Polizei, Militär, Gefängnis
|
|
5954
|
+
"1272": "public",
|
|
5955
|
+
// Religiöse Gebäude und Kultbauten
|
|
5956
|
+
"1273": "public",
|
|
5957
|
+
// Öffentliche Verwaltungsgebäude
|
|
5958
|
+
// Agricultural
|
|
5959
|
+
"1271": "agricultural",
|
|
5960
|
+
// Landwirtschaftsgebäude
|
|
5961
|
+
// Other
|
|
5962
|
+
"1274": "other",
|
|
5963
|
+
// Anderes Gebäude
|
|
5964
|
+
"1281": "infrastructure",
|
|
5965
|
+
// Empfangsgebäude Bahn/Bus
|
|
5966
|
+
"1282": "infrastructure"
|
|
5967
|
+
// Parkhaus, Garage
|
|
5968
|
+
};
|
|
5969
|
+
var KEYWORD_RULES = [
|
|
5970
|
+
// "Gebäude" is the Swiss AV BoFlaeche land-cover label for a building footprint.
|
|
5971
|
+
// It does not encode the actual use type, so map it to 'other' as a safe fallback.
|
|
5972
|
+
[/^geb[äa]ude$/i, "other"],
|
|
5973
|
+
[/wohn|resident|housing|bolig|enfamil|etagebolig|rækkehus|lejlighed/i, "residential"],
|
|
5974
|
+
[/büro|office|handel|commercial|butik|forretning|kontor|supermarked|lager|hotel|restaurant/i, "commercial"],
|
|
5975
|
+
[/industri|fabrik|industrial|gewerbe|produktion|fabrik|fremstill/i, "industrial"],
|
|
5976
|
+
[/landwirtschaft|agrar|agricultural|landbrug|skov|dyrk|stald|lade|drivhus/i, "agricultural"],
|
|
5977
|
+
[/schule|school|hospital|gesundheit|sundhed|kirche|church|offentlig|uddannelse|kultur|social|plejehjem/i, "public"],
|
|
5978
|
+
[/freizeit|recreation|sport|sommerhus|ferienhaus|fritid|kolonihave/i, "recreational"],
|
|
5979
|
+
[/infrastruktur|verkehr|transport|energy|energie|forsyning|teknik|teknisk/i, "infrastructure"],
|
|
5980
|
+
[/gemischt|mixed|blandet/i, "mixed"]
|
|
5981
|
+
];
|
|
5982
|
+
function classifyByText(text) {
|
|
5983
|
+
for (const [pattern, use] of KEYWORD_RULES) {
|
|
5984
|
+
if (pattern.test(text))
|
|
5985
|
+
return use;
|
|
5986
|
+
}
|
|
5987
|
+
return void 0;
|
|
5988
|
+
}
|
|
5989
|
+
function classifyBuildingUse(code, sourceId) {
|
|
5990
|
+
if (code == null)
|
|
5991
|
+
return void 0;
|
|
5992
|
+
const key = String(code).trim();
|
|
5993
|
+
if (sourceId === "swiss-gwr") {
|
|
5994
|
+
return CH_GWR_GKLAS[key] ?? classifyByText(key);
|
|
5995
|
+
}
|
|
5996
|
+
if (sourceId === "danish-matrikel") {
|
|
5997
|
+
return DK_BBR_BUILDING[key] ?? classifyByText(key);
|
|
5998
|
+
}
|
|
5999
|
+
if (sourceId === "zurich-wfs" || sourceId === "swiss-av-wfs") {
|
|
6000
|
+
return CH_GWR_BUILDING[key] ?? classifyByText(key);
|
|
6001
|
+
}
|
|
6002
|
+
return CH_GWR_BUILDING[key] ?? DK_BBR_BUILDING[key] ?? classifyByText(key);
|
|
6003
|
+
}
|
|
6004
|
+
function classifyPlotUse(use, _sourceId) {
|
|
6005
|
+
if (use == null)
|
|
6006
|
+
return void 0;
|
|
6007
|
+
return classifyByText(String(use).trim());
|
|
6008
|
+
}
|
|
6009
|
+
function normKey(raw) {
|
|
6010
|
+
return raw.trim().toLowerCase().replace(/[\s_\-]/g, "");
|
|
6011
|
+
}
|
|
6012
|
+
var ZONE_LEGAL_STATUS_MAP = {
|
|
6013
|
+
// Swiss ÖREB rechtsstatus
|
|
6014
|
+
"inkraft": "in-force",
|
|
6015
|
+
"laufendeanderung": "amendment-pending",
|
|
6016
|
+
"laufende\xE4nderung": "amendment-pending",
|
|
6017
|
+
"aenderungohnevorvirkung": "amendment-pending",
|
|
6018
|
+
// guard against common typo
|
|
6019
|
+
"aenderungohnevorwirkung": "amendment-pending",
|
|
6020
|
+
"\xE4nderungohnevorwirkung": "amendment-pending",
|
|
6021
|
+
"aenderungmitvorwirkung": "amendment-in-effect",
|
|
6022
|
+
"\xE4nderungmitvorwirkung": "amendment-in-effect",
|
|
6023
|
+
"\xF6ffentlicheauflage": "proposed",
|
|
6024
|
+
"offentlicheauflage": "proposed",
|
|
6025
|
+
"aufgehoben": "repealed",
|
|
6026
|
+
// Danish plandata.dk status
|
|
6027
|
+
"vedtaget": "in-force",
|
|
6028
|
+
"forslag": "proposed",
|
|
6029
|
+
"aflyst": "repealed"
|
|
6030
|
+
};
|
|
6031
|
+
function classifyZoneLegalStatus(raw) {
|
|
6032
|
+
if (!raw)
|
|
6033
|
+
return void 0;
|
|
6034
|
+
return ZONE_LEGAL_STATUS_MAP[normKey(raw)];
|
|
6035
|
+
}
|
|
6036
|
+
var ZONE_PLAN_TYPE_EXACT = {
|
|
6037
|
+
// Swiss Nutzungsplanung (typ_gde_bezeichnung and common variants)
|
|
6038
|
+
"grundnutzungszonenplan": "land-use-plan",
|
|
6039
|
+
"nutzungszonenplan": "land-use-plan",
|
|
6040
|
+
"zonenplan": "land-use-plan",
|
|
6041
|
+
"gestaltungsplan": "design-plan",
|
|
6042
|
+
"bebauungsplan": "development-plan",
|
|
6043
|
+
"sondernutzungsplan": "special-use-plan",
|
|
6044
|
+
"quartiererhaltungszonenplan": "neighbourhood-conservation-plan",
|
|
6045
|
+
"l\xE4rmempfindlichkeitsstufenplan": "noise-sensitivity-plan",
|
|
6046
|
+
"laermempfindlichkeitsstufenplan": "noise-sensitivity-plan",
|
|
6047
|
+
"gew\xE4sserschutzzonenplan": "water-protection-plan",
|
|
6048
|
+
"gewasserschutzzonenplan": "water-protection-plan",
|
|
6049
|
+
"grundwasserschutzzonenplan": "water-protection-plan",
|
|
6050
|
+
"gew\xE4sserschutzplan": "water-protection-plan",
|
|
6051
|
+
"naturundlandschaftsschutzzonenplan": "nature-landscape-plan",
|
|
6052
|
+
"natur-undlandschaftsschutzzonenplan": "nature-landscape-plan",
|
|
6053
|
+
"landschaftsschutzzonenplan": "nature-landscape-plan",
|
|
6054
|
+
// Freihaltezone — open space / clearance zone within the Grundnutzungszonenplan
|
|
6055
|
+
"freihaltezone": "open-space-zone",
|
|
6056
|
+
// Simple Grundnutzung zone type designations (municipality labels from ogd-0156)
|
|
6057
|
+
"kernzone": "land-use-plan",
|
|
6058
|
+
"wald": "land-use-plan",
|
|
6059
|
+
"gew\xE4sser": "land-use-plan",
|
|
6060
|
+
"gewasser": "land-use-plan",
|
|
6061
|
+
"reservezone": "land-use-plan",
|
|
6062
|
+
"bahnareal": "land-use-plan",
|
|
6063
|
+
"erholungszone": "land-use-plan",
|
|
6064
|
+
// Road / transport area designations
|
|
6065
|
+
"strassen(weitere)": "land-use-plan",
|
|
6066
|
+
"strassen": "land-use-plan",
|
|
6067
|
+
// Unzoned areas
|
|
6068
|
+
"nichtzoniertfl\xE4che": "unzoned",
|
|
6069
|
+
"nichtzoniertefl\xE4che": "unzoned",
|
|
6070
|
+
"nichtzoniertflache": "unzoned",
|
|
6071
|
+
"nichtzonierteflache": "unzoned",
|
|
6072
|
+
"nichtzoniertgem\xE4ssbzo2016": "unzoned",
|
|
6073
|
+
"nichtzoniertgemassbzo2016": "unzoned",
|
|
6074
|
+
// Overlay building/development regulations (Überlagernde Festlegungen)
|
|
6075
|
+
"hochh\xE4user": "overlay-regulation",
|
|
6076
|
+
"hochhauser": "overlay-regulation",
|
|
6077
|
+
"areal\xFCberbauungenzul\xE4ssig": "overlay-regulation",
|
|
6078
|
+
"arealuberbauungenzulassig": "overlay-regulation",
|
|
6079
|
+
"erh\xF6hteausnutzung": "overlay-regulation",
|
|
6080
|
+
"erhohteausnutzung": "overlay-regulation",
|
|
6081
|
+
"erdgeschossnutzung": "overlay-regulation",
|
|
6082
|
+
"freifl\xE4chenzifferverlegungsgebiet": "overlay-regulation",
|
|
6083
|
+
"freiflachenzifferverlegungsgebiet": "overlay-regulation",
|
|
6084
|
+
"baumschutz": "overlay-regulation",
|
|
6085
|
+
// Special building regulations (Sonderbauvorschriften)
|
|
6086
|
+
"sonderbauvorschriften": "overlay-regulation",
|
|
6087
|
+
// Danish plandata.dk
|
|
6088
|
+
"lokalplan": "local-plan",
|
|
6089
|
+
"kommuneplanramme": "municipal-plan-framework"
|
|
6090
|
+
};
|
|
6091
|
+
var ZONE_PLAN_TYPE_KEYWORDS = [
|
|
6092
|
+
[/quartiererhalt/i, "neighbourhood-conservation-plan"],
|
|
6093
|
+
[/lärmempfindlich|laermempfindlich/i, "noise-sensitivity-plan"],
|
|
6094
|
+
[/gewässerschutz|grundwasserschutz/i, "water-protection-plan"],
|
|
6095
|
+
[/natur.*landschaft|landschaft.*schutz/i, "nature-landscape-plan"],
|
|
6096
|
+
[/gestaltungsplan/i, "design-plan"],
|
|
6097
|
+
[/bebauungsplan/i, "development-plan"],
|
|
6098
|
+
[/sondernutzung/i, "special-use-plan"],
|
|
6099
|
+
// Freihaltezone / open-space clearance zones (before general zonenplan catch-all)
|
|
6100
|
+
[/freihalt/i, "open-space-zone"],
|
|
6101
|
+
// Zone für öffentliche Bauten — public-facilities land-use zone
|
|
6102
|
+
[/öffentliche.*bauten|zone.*öffentlich/i, "land-use-plan"],
|
|
6103
|
+
// Overlaying building/development regulations
|
|
6104
|
+
[/hochhaus/i, "overlay-regulation"],
|
|
6105
|
+
[/arealüberbauung|arealuberbauung/i, "overlay-regulation"],
|
|
6106
|
+
[/erhöhte.*ausnutzung|ausnutzung/i, "overlay-regulation"],
|
|
6107
|
+
[/erdgeschossnutzung/i, "overlay-regulation"],
|
|
6108
|
+
[/freiflächenzifferverlegung|freiflachenzifferverlegung/i, "overlay-regulation"],
|
|
6109
|
+
[/erhaltenswert/i, "overlay-regulation"],
|
|
6110
|
+
[/m[\u00e4a]ssig.*st[\u00f6o]rend|st[\u00f6o]rendes.*gewerbe/i, "overlay-regulation"],
|
|
6111
|
+
// Grundnutzung zone type designations (municipality labels, ogd-0156)
|
|
6112
|
+
[/wohnzone/i, "land-use-plan"],
|
|
6113
|
+
[/kernzone|zentrumszone/i, "land-use-plan"],
|
|
6114
|
+
[/erholungszone|erholungs/i, "land-use-plan"],
|
|
6115
|
+
[/stra[sß]en/i, "land-use-plan"],
|
|
6116
|
+
[/landwirtschaft/i, "land-use-plan"],
|
|
6117
|
+
[/\bwald\b/i, "land-use-plan"],
|
|
6118
|
+
[/\bgew[\u00e4a]sser\b/i, "land-use-plan"],
|
|
6119
|
+
// Unzoned areas
|
|
6120
|
+
[/nicht\s*zoniert/i, "unzoned"],
|
|
6121
|
+
// Tree / vegetation protection overlay
|
|
6122
|
+
[/baumschutz/i, "overlay-regulation"],
|
|
6123
|
+
// Special building regulations not under standard building ordinance
|
|
6124
|
+
[/sonderbauvorschrift/i, "overlay-regulation"],
|
|
6125
|
+
[/kommuneplan/i, "municipal-plan-framework"],
|
|
6126
|
+
[/lokalplan/i, "local-plan"],
|
|
6127
|
+
[/zonenplan|nutzungsplan|nutzungszone/i, "land-use-plan"],
|
|
6128
|
+
// Catch-all: any remaining Xzone / Xareal designation from the Grundnutzungszonenplan
|
|
6129
|
+
[/zone$|areal$/i, "land-use-plan"]
|
|
6130
|
+
];
|
|
6131
|
+
function classifyZonePlanType(raw) {
|
|
6132
|
+
if (!raw)
|
|
6133
|
+
return void 0;
|
|
6134
|
+
const key = normKey(raw);
|
|
6135
|
+
if (key in ZONE_PLAN_TYPE_EXACT)
|
|
6136
|
+
return ZONE_PLAN_TYPE_EXACT[key];
|
|
6137
|
+
for (const [pattern, planType] of ZONE_PLAN_TYPE_KEYWORDS) {
|
|
6138
|
+
if (pattern.test(raw))
|
|
6139
|
+
return planType;
|
|
6140
|
+
}
|
|
6141
|
+
console.warn(
|
|
6142
|
+
`[cue-gis] Unknown zone plan type \u2014 add "${raw}" to classifyZonePlanType: no ZonePlanType mapping found.`
|
|
6143
|
+
);
|
|
6144
|
+
return void 0;
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
// libs/js/cue-gis/src/lib/zurich-maps-adapter.ts
|
|
6148
|
+
var WFS_BASE_URL = "https://maps.zh.ch/wfs/OGDZHWFS";
|
|
6149
|
+
var ZURICH_SOURCE_ID = "zurich-wfs";
|
|
6150
|
+
var ZURICH_CANTON_BBOX = [8.35, 47.15, 8.95, 47.7];
|
|
6151
|
+
var SWITZERLAND_BBOX = [5.9, 45.7, 10.55, 47.85];
|
|
6152
|
+
function entryTypeName(entry) {
|
|
6153
|
+
return typeof entry === "string" ? entry : entry.typeName;
|
|
6154
|
+
}
|
|
6155
|
+
function entryCqlFilter(entry) {
|
|
6156
|
+
return typeof entry === "string" ? void 0 : entry.cqlFilter;
|
|
6157
|
+
}
|
|
6158
|
+
function firstCoordinate(geometry) {
|
|
6159
|
+
if (!geometry)
|
|
6160
|
+
return [0, 0];
|
|
6161
|
+
const coords = geometry.coordinates;
|
|
6162
|
+
switch (geometry.type) {
|
|
6163
|
+
case "Point":
|
|
6164
|
+
return coords;
|
|
6165
|
+
case "MultiPoint":
|
|
6166
|
+
case "LineString":
|
|
6167
|
+
return coords[0];
|
|
6168
|
+
case "MultiLineString":
|
|
6169
|
+
case "Polygon":
|
|
6170
|
+
return coords[0][0];
|
|
6171
|
+
case "MultiPolygon":
|
|
6172
|
+
return coords[0][0][0];
|
|
6173
|
+
default:
|
|
6174
|
+
return [0, 0];
|
|
6175
|
+
}
|
|
6176
|
+
}
|
|
6177
|
+
function pickName(props, fallback) {
|
|
6178
|
+
if (!props)
|
|
6179
|
+
return fallback;
|
|
6180
|
+
for (const key of ["plannavn", "bezeichnung", "name", "strassenname", "objektname", "title", "label"]) {
|
|
6181
|
+
if (typeof props[key] === "string" && props[key])
|
|
6182
|
+
return props[key];
|
|
6183
|
+
}
|
|
6184
|
+
return fallback;
|
|
6185
|
+
}
|
|
6186
|
+
function toLayerId2(sourceId, typeName, cqlFilter) {
|
|
6187
|
+
if (!cqlFilter)
|
|
6188
|
+
return `${sourceId}:${typeName}`;
|
|
6189
|
+
return `${sourceId}:${typeName}[${cqlFilter}]`;
|
|
6190
|
+
}
|
|
6191
|
+
var PRIORITY_CATEGORIES = /* @__PURE__ */ new Set(["building", "cadastre", "greenspace", "paved", "zone"]);
|
|
6192
|
+
function toLayerDescriptor2(sourceId, category, typeName, cqlFilter) {
|
|
6193
|
+
const shortName = typeName.replace(/^ms:/, "");
|
|
6194
|
+
const descriptor = featureCategoryDescriptor(category);
|
|
6195
|
+
const tier = PRIORITY_CATEGORIES.has(category) ? "priority" : "raw";
|
|
6196
|
+
return {
|
|
6197
|
+
id: toLayerId2(sourceId, typeName, cqlFilter),
|
|
6198
|
+
sourceId,
|
|
6199
|
+
sourceLayerId: typeName,
|
|
6200
|
+
category,
|
|
6201
|
+
preferredColor: descriptor.preferredColor,
|
|
6202
|
+
tier,
|
|
6203
|
+
label: shortName,
|
|
6204
|
+
description: `Z\xFCrich WFS layer ${shortName}`,
|
|
6205
|
+
labelKey: `gis.layer.${sourceId}.${shortName}.label`,
|
|
6206
|
+
descriptionKey: `gis.layer.${sourceId}.${shortName}.description`
|
|
6207
|
+
};
|
|
6208
|
+
}
|
|
6209
|
+
function toNormalisedBuildingProperties(props, sourceId) {
|
|
6210
|
+
if (!props)
|
|
6211
|
+
return { featureType: "building" };
|
|
6212
|
+
const rawArea = props["grundflaeche"] ?? props["gbf"] ?? props["gebaeudegrundrissflaeche"] ?? props["flaeche"] ?? void 0;
|
|
6213
|
+
const rawFloors = props["vollgeschosse"] ?? props["geschossanzahl"] ?? props["anzahl_geschosse"] ?? void 0;
|
|
6214
|
+
const buildingUse = props["gebaeudefunktion"] ?? props["gfkode"] ?? props["art"] ?? props["objektart"] ?? void 0;
|
|
6215
|
+
const rawYear = props["baujahr"] ?? props["bauperiode"] ?? void 0;
|
|
6216
|
+
const registryId = String(props["egid"] ?? props["gwr_egid"] ?? props["egris_egid"] ?? "").trim() || void 0;
|
|
6217
|
+
return {
|
|
6218
|
+
featureType: "building",
|
|
6219
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0,
|
|
6220
|
+
buildingUse: buildingUse ? String(buildingUse) : void 0,
|
|
6221
|
+
buildingUseGeneric: classifyBuildingUse(buildingUse, sourceId),
|
|
6222
|
+
floors: typeof rawFloors === "number" ? rawFloors : void 0,
|
|
6223
|
+
yearBuilt: typeof rawYear === "number" ? rawYear : void 0,
|
|
6224
|
+
registryId
|
|
6225
|
+
};
|
|
6226
|
+
}
|
|
6227
|
+
function toNormalisedPlotProperties(props, sourceId) {
|
|
6228
|
+
if (!props)
|
|
6229
|
+
return { featureType: "plot" };
|
|
6230
|
+
const registryId = props["egris_egrid"] ?? props["egrid"] ?? void 0;
|
|
6231
|
+
const nummer = props["nummer"] ?? void 0;
|
|
6232
|
+
const nbident = props["nbident"] ?? void 0;
|
|
6233
|
+
const plotId = nummer && nbident ? `${nummer}, ${nbident}` : nummer ?? void 0;
|
|
6234
|
+
const rawArea = props["flaechenmass"] ?? props["flaeche"] ?? void 0;
|
|
6235
|
+
const plotUse = props["art"] ?? props["nutzungsart"] ?? void 0;
|
|
6236
|
+
return {
|
|
6237
|
+
featureType: "plot",
|
|
6238
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0,
|
|
6239
|
+
plotUse: plotUse ? String(plotUse) : void 0,
|
|
6240
|
+
plotUseGeneric: classifyPlotUse(plotUse, sourceId),
|
|
6241
|
+
plotId,
|
|
6242
|
+
registryId
|
|
6243
|
+
};
|
|
6244
|
+
}
|
|
6245
|
+
function toNormalisedGreenspaceProperties(props) {
|
|
6246
|
+
if (!props)
|
|
6247
|
+
return { featureType: "greenspace" };
|
|
6248
|
+
const rawArea = props["flaeche"] ?? void 0;
|
|
6249
|
+
const surfaceType = props["art"] ?? void 0;
|
|
6250
|
+
return {
|
|
6251
|
+
featureType: "greenspace",
|
|
6252
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0,
|
|
6253
|
+
surfaceType: surfaceType ? String(surfaceType) : void 0
|
|
6254
|
+
};
|
|
6255
|
+
}
|
|
6256
|
+
function toNormalisedPavedProperties(props) {
|
|
6257
|
+
if (!props)
|
|
6258
|
+
return { featureType: "paved" };
|
|
6259
|
+
const rawArea = props["flaeche"] ?? void 0;
|
|
6260
|
+
const surfaceType = props["art"] ?? void 0;
|
|
6261
|
+
return {
|
|
6262
|
+
featureType: "paved",
|
|
6263
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0,
|
|
6264
|
+
surfaceType: surfaceType ? String(surfaceType) : void 0
|
|
6265
|
+
};
|
|
6266
|
+
}
|
|
6267
|
+
function toNormalisedZoneProperties(props) {
|
|
6268
|
+
if (!props)
|
|
6269
|
+
return { featureType: "zone" };
|
|
6270
|
+
const zoneType = props["plannavn"] ?? props["bezeichnung"] ?? props["typ_bezeichnung"] ?? props["art"] ?? void 0;
|
|
6271
|
+
const planId = props["planid"] ?? props["plannummer"] ?? void 0;
|
|
6272
|
+
const zoneCode = props["artcode"] ?? props["typ_code"] ?? props["abkuerzung"] ?? planId ?? void 0;
|
|
6273
|
+
const rawLegalStatus = props["rechtsstatus"] ?? props["status"] ?? void 0;
|
|
6274
|
+
const legalStatus = classifyZoneLegalStatus(rawLegalStatus);
|
|
6275
|
+
const rawPlanType = props["typ_gde_bezeichnung"] ?? void 0;
|
|
6276
|
+
const planType = classifyZonePlanType(rawPlanType);
|
|
6277
|
+
const planDocumentLink = props["dagsordenpunkt_url"] ?? props["dokument_url"] ?? props["link"] ?? void 0;
|
|
6278
|
+
const publicationDate = props["auflagedatum"] ?? void 0;
|
|
6279
|
+
const fixingDate = props["festsetzungsdatum"] ?? void 0;
|
|
6280
|
+
const approvalDate = props["genehmigungsdatum"] ?? void 0;
|
|
6281
|
+
const effectiveFrom = props["inkraftsetzungsdatum"] ?? void 0;
|
|
6282
|
+
const rawArea = props["flaeche"] ?? props["flaeche_m2"] ?? void 0;
|
|
6283
|
+
return {
|
|
6284
|
+
featureType: "zone",
|
|
6285
|
+
zoneType: zoneType ? String(zoneType) : void 0,
|
|
6286
|
+
zoneCode: zoneCode ? String(zoneCode) : void 0,
|
|
6287
|
+
legalStatus,
|
|
6288
|
+
planType,
|
|
6289
|
+
planId: planId ? String(planId) : void 0,
|
|
6290
|
+
planDocumentLink: planDocumentLink ? String(planDocumentLink) : void 0,
|
|
6291
|
+
publicationDate: publicationDate ?? void 0,
|
|
6292
|
+
fixingDate: fixingDate ?? void 0,
|
|
6293
|
+
approvalDate: approvalDate ?? void 0,
|
|
6294
|
+
effectiveFrom: effectiveFrom ?? void 0,
|
|
6295
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0
|
|
6296
|
+
};
|
|
6297
|
+
}
|
|
6298
|
+
function _makeCqlPostFilter(cqlFilter) {
|
|
6299
|
+
const eqMatch = cqlFilter.match(/^(\w+)\s*=\s*'([^']+)'$/);
|
|
6300
|
+
if (eqMatch) {
|
|
6301
|
+
const [, field, value] = eqMatch;
|
|
6302
|
+
return (props) => String(props?.[field] ?? "") === value;
|
|
6303
|
+
}
|
|
6304
|
+
const inMatch = cqlFilter.match(/^(\w+)\s+IN\s*\(([^)]+)\)$/i);
|
|
6305
|
+
if (inMatch) {
|
|
6306
|
+
const [, field, valuesStr] = inMatch;
|
|
6307
|
+
const allowed = new Set(
|
|
6308
|
+
valuesStr.split(",").map((v) => v.trim().replace(/^'|'$/g, ""))
|
|
6309
|
+
);
|
|
6310
|
+
return (props) => allowed.has(String(props?.[field] ?? ""));
|
|
6311
|
+
}
|
|
6312
|
+
return () => true;
|
|
6313
|
+
}
|
|
6314
|
+
var ZurichMapsAdapter = class {
|
|
6315
|
+
categoryMap;
|
|
6316
|
+
baseUrl;
|
|
6317
|
+
sourceId;
|
|
6318
|
+
outputFormat;
|
|
6319
|
+
constructor(options) {
|
|
6320
|
+
this.categoryMap = options.categoryMap;
|
|
6321
|
+
this.baseUrl = options.baseUrl ?? WFS_BASE_URL;
|
|
6322
|
+
this.sourceId = options.sourceId ?? ZURICH_SOURCE_ID;
|
|
6323
|
+
this.outputFormat = options.outputFormat ?? "application/json; subtype=geojson";
|
|
6324
|
+
}
|
|
6325
|
+
async listFeatureCategoryDescriptors(bbox) {
|
|
6326
|
+
const layers = await this.listAvailableLayers(bbox);
|
|
6327
|
+
return [...new Map(layers.map((layer) => [layer.category, featureCategoryDescriptor(layer.category)])).values()];
|
|
6328
|
+
}
|
|
6329
|
+
async listAvailableLayers(bbox) {
|
|
6330
|
+
const entries = Object.entries(this.categoryMap);
|
|
6331
|
+
const results = await Promise.allSettled(
|
|
6332
|
+
entries.flatMap(
|
|
6333
|
+
([category, layerEntries]) => layerEntries.map(async (entry) => {
|
|
6334
|
+
const typeName = entryTypeName(entry);
|
|
6335
|
+
const cqlFilter = entryCqlFilter(entry);
|
|
6336
|
+
const fc = await this._fetchFeatures(bbox, typeName, 1, cqlFilter);
|
|
6337
|
+
return { category, typeName, cqlFilter, hasResults: fc.features.length > 0 };
|
|
6338
|
+
})
|
|
6339
|
+
)
|
|
6340
|
+
);
|
|
6341
|
+
return results.filter(
|
|
6342
|
+
(r) => r.status === "fulfilled" && r.value.hasResults
|
|
6343
|
+
).map((r) => toLayerDescriptor2(this.sourceId, r.value.category, r.value.typeName, r.value.cqlFilter));
|
|
6344
|
+
}
|
|
6345
|
+
async getFeaturesForLayer(bbox, layerId) {
|
|
6346
|
+
const found = this._findLayerById(layerId);
|
|
6347
|
+
if (!found)
|
|
6348
|
+
return [];
|
|
6349
|
+
return this._fetchAndConvert(bbox, found.descriptor, found.cqlFilter);
|
|
6350
|
+
}
|
|
6351
|
+
async _fetchAndConvert(bbox, layer, cqlFilter) {
|
|
6352
|
+
const fc = await this._fetchFeatures(bbox, layer.sourceLayerId, void 0, cqlFilter);
|
|
6353
|
+
const postFilter = cqlFilter ? _makeCqlPostFilter(cqlFilter) : void 0;
|
|
6354
|
+
const features = postFilter ? fc.features.filter((f) => postFilter(f.properties)) : fc.features;
|
|
6355
|
+
return features.map((feature, index) => this.toGisFeature(feature, layer, index));
|
|
6356
|
+
}
|
|
6357
|
+
/**
|
|
6358
|
+
* Returns the categories that have at least one configured typename AND at
|
|
6359
|
+
* least one feature in the given bbox (light `count=1` probe per typename).
|
|
6360
|
+
*/
|
|
6361
|
+
async listFeatureCategories(bbox) {
|
|
6362
|
+
const layers = await this.listAvailableLayers(bbox);
|
|
6363
|
+
return [...new Set(layers.map((layer) => layer.category))];
|
|
6364
|
+
}
|
|
6365
|
+
async getFeaturesOfCategory(bbox, category) {
|
|
6366
|
+
const entries = this.categoryMap[category] ?? [];
|
|
6367
|
+
const batches = await Promise.all(
|
|
6368
|
+
entries.map((entry) => {
|
|
6369
|
+
const typeName = entryTypeName(entry);
|
|
6370
|
+
const cqlFilter = entryCqlFilter(entry);
|
|
6371
|
+
const layer = toLayerDescriptor2(this.sourceId, category, typeName);
|
|
6372
|
+
return this._fetchAndConvert(bbox, layer, cqlFilter);
|
|
6373
|
+
})
|
|
6374
|
+
);
|
|
6375
|
+
return batches.flat();
|
|
6376
|
+
}
|
|
6377
|
+
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
6378
|
+
_findLayerById(layerId) {
|
|
6379
|
+
const entries = Object.entries(this.categoryMap);
|
|
6380
|
+
for (const [category, layerEntries] of entries) {
|
|
6381
|
+
for (const entry of layerEntries) {
|
|
6382
|
+
const typeName = entryTypeName(entry);
|
|
6383
|
+
const cqlFilter = entryCqlFilter(entry);
|
|
6384
|
+
const layer = toLayerDescriptor2(this.sourceId, category, typeName, cqlFilter);
|
|
6385
|
+
if (layer.id === layerId) {
|
|
6386
|
+
return { descriptor: layer, cqlFilter };
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
}
|
|
6390
|
+
return void 0;
|
|
6391
|
+
}
|
|
6392
|
+
async _fetchFeatures(bbox, typeName, count, cqlFilter) {
|
|
6393
|
+
const [west, south, east, north] = bbox;
|
|
6394
|
+
const bboxParam = `${south},${west},${north},${east},urn:ogc:def:crs:EPSG::4326`;
|
|
6395
|
+
const params = new URLSearchParams({
|
|
6396
|
+
service: "WFS",
|
|
6397
|
+
version: "2.0.0",
|
|
6398
|
+
request: "GetFeature",
|
|
6399
|
+
typename: typeName,
|
|
6400
|
+
bbox: bboxParam,
|
|
6401
|
+
srsName: "EPSG:4326",
|
|
6402
|
+
outputFormat: this.outputFormat
|
|
6403
|
+
});
|
|
6404
|
+
if (count !== void 0) {
|
|
6405
|
+
params.set("count", String(count));
|
|
6406
|
+
}
|
|
6407
|
+
if (cqlFilter !== void 0) {
|
|
6408
|
+
params.set("CQL_FILTER", cqlFilter);
|
|
6409
|
+
}
|
|
6410
|
+
params.set("outputFormat", this.outputFormat);
|
|
6411
|
+
const url = `${this.baseUrl}?${params.toString()}`;
|
|
6412
|
+
const response = await fetch(url);
|
|
6413
|
+
if (!response.ok) {
|
|
6414
|
+
throw new Error(
|
|
6415
|
+
`WFS request failed for ${typeName} (${this.sourceId}): ${response.status} ${response.statusText}`
|
|
6416
|
+
);
|
|
6417
|
+
}
|
|
6418
|
+
return response.json();
|
|
6419
|
+
}
|
|
6420
|
+
toGisFeature(feature, layer, index) {
|
|
6421
|
+
const [lon, lat] = firstCoordinate(feature.geometry);
|
|
6422
|
+
const name = pickName(feature.properties, `${layer.sourceLayerId}[${index}]`);
|
|
6423
|
+
const featureBBox = feature.bbox ? [feature.bbox[0], feature.bbox[1], feature.bbox[2], feature.bbox[3]] : void 0;
|
|
6424
|
+
const tier = layer.tier;
|
|
6425
|
+
const properties = tier === "priority" ? this._extractNormalisedProperties(feature, layer) : void 0;
|
|
6426
|
+
return {
|
|
6427
|
+
id: feature.id ?? `${layer.sourceLayerId}/${index}`,
|
|
6428
|
+
preferredColor: layer.preferredColor,
|
|
6429
|
+
sourceId: this.sourceId,
|
|
6430
|
+
sourceFeatureId: feature.id ?? `${layer.sourceLayerId}/${index}`,
|
|
6431
|
+
layerId: layer.id,
|
|
6432
|
+
name,
|
|
6433
|
+
category: layer.category,
|
|
6434
|
+
type: layer.sourceLayerId.replace(/^ms:/, ""),
|
|
6435
|
+
lat,
|
|
6436
|
+
lon,
|
|
6437
|
+
displayName: name,
|
|
6438
|
+
bbox: featureBBox,
|
|
6439
|
+
geometry: feature.geometry ?? void 0,
|
|
6440
|
+
tier,
|
|
6441
|
+
properties,
|
|
6442
|
+
originalData: feature.properties ?? {}
|
|
6443
|
+
};
|
|
6444
|
+
}
|
|
6445
|
+
_extractNormalisedProperties(feature, layer) {
|
|
6446
|
+
if (layer.category === "building") {
|
|
6447
|
+
return toNormalisedBuildingProperties(feature.properties, this.sourceId);
|
|
6448
|
+
}
|
|
6449
|
+
if (layer.category === "greenspace") {
|
|
6450
|
+
return toNormalisedGreenspaceProperties(feature.properties);
|
|
6451
|
+
}
|
|
6452
|
+
if (layer.category === "paved") {
|
|
6453
|
+
return toNormalisedPavedProperties(feature.properties);
|
|
6454
|
+
}
|
|
6455
|
+
if (layer.category === "zone") {
|
|
6456
|
+
return toNormalisedZoneProperties(feature.properties);
|
|
6457
|
+
}
|
|
6458
|
+
return toNormalisedPlotProperties(feature.properties, this.sourceId);
|
|
6459
|
+
}
|
|
6460
|
+
};
|
|
6461
|
+
|
|
6462
|
+
// libs/js/cue-gis/src/lib/danish-cadastre-adapter.ts
|
|
6463
|
+
var DENMARK_BBOX = [8, 54.5, 15.2, 57.8];
|
|
6464
|
+
|
|
6465
|
+
// libs/js/cue-gis/src/lib/cue-sdk-gis-adapter.ts
|
|
6466
|
+
var DEFAULT_DANISH_SDK_CATEGORY_MAP = {
|
|
6467
|
+
building: [
|
|
6468
|
+
{ source: "danish-matrikel", typename: "bbr_v001:bygning_current" }
|
|
6469
|
+
],
|
|
6470
|
+
cadastre: [
|
|
6471
|
+
{ source: "danish-matrikel", typename: "mat_v001:lodflade_current" },
|
|
6472
|
+
{ source: "danish-matrikel", typename: "mat_v001:samletfastejendom_current" }
|
|
6473
|
+
]
|
|
6474
|
+
};
|
|
6475
|
+
function firstCoordinate2(geometry) {
|
|
6476
|
+
if (!geometry)
|
|
6477
|
+
return [0, 0];
|
|
6478
|
+
const coords = geometry.coordinates;
|
|
6479
|
+
switch (geometry.type) {
|
|
6480
|
+
case "Point":
|
|
6481
|
+
return coords;
|
|
6482
|
+
case "MultiPoint":
|
|
6483
|
+
case "LineString":
|
|
6484
|
+
return coords[0];
|
|
6485
|
+
case "MultiLineString":
|
|
6486
|
+
case "Polygon":
|
|
6487
|
+
return coords[0][0];
|
|
6488
|
+
case "MultiPolygon":
|
|
6489
|
+
return coords[0][0][0];
|
|
6490
|
+
default:
|
|
6491
|
+
return [0, 0];
|
|
6492
|
+
}
|
|
6493
|
+
}
|
|
6494
|
+
function pickName2(props, fallback) {
|
|
6495
|
+
if (!props)
|
|
6496
|
+
return fallback;
|
|
6497
|
+
for (const key of [
|
|
6498
|
+
"mat:matrikelnummer",
|
|
6499
|
+
"matrikelnummer",
|
|
6500
|
+
"mat:ejerlavsnavn",
|
|
6501
|
+
"ejerlavsnavn",
|
|
6502
|
+
"name",
|
|
6503
|
+
"bezeichnung",
|
|
6504
|
+
"stednavntekst",
|
|
6505
|
+
"title",
|
|
6506
|
+
"label"
|
|
6507
|
+
]) {
|
|
6508
|
+
if (typeof props[key] === "string" && props[key])
|
|
6509
|
+
return props[key];
|
|
6510
|
+
}
|
|
6511
|
+
return fallback;
|
|
6512
|
+
}
|
|
6513
|
+
function toLayerId3(sourceId, source, typename) {
|
|
6514
|
+
return `${sourceId}:${source}:${typename}`;
|
|
6515
|
+
}
|
|
6516
|
+
var PRIORITY_CATEGORIES2 = /* @__PURE__ */ new Set(["building", "cadastre", "zone"]);
|
|
6517
|
+
function toLayerDescriptor3(sourceId, category, entry) {
|
|
6518
|
+
const shortName = entry.typename.replace(/^[^:]+:/, "");
|
|
6519
|
+
const descriptor = featureCategoryDescriptor(category);
|
|
6520
|
+
const tier = PRIORITY_CATEGORIES2.has(category) ? "priority" : "raw";
|
|
6521
|
+
return {
|
|
6522
|
+
id: toLayerId3(sourceId, entry.source, entry.typename),
|
|
6523
|
+
sourceId,
|
|
6524
|
+
sourceLayerId: entry.typename,
|
|
6525
|
+
category,
|
|
6526
|
+
preferredColor: descriptor.preferredColor,
|
|
6527
|
+
tier,
|
|
6528
|
+
label: entry.label ?? shortName,
|
|
6529
|
+
description: `${entry.source} / ${entry.typename}`,
|
|
6530
|
+
labelKey: `gis.layer.${sourceId}.${entry.source}.${shortName}.label`,
|
|
6531
|
+
descriptionKey: `gis.layer.${sourceId}.${entry.source}.${shortName}.description`
|
|
6532
|
+
};
|
|
6533
|
+
}
|
|
6534
|
+
function _extractNormalizedProperties(props, category) {
|
|
6535
|
+
if (category === "building") {
|
|
6536
|
+
const rawArea2 = props?.["byg041BebyggetAreal"] ?? props?.["byg038SamletBygningsareal"];
|
|
6537
|
+
const buildingUse = props?.["byg021BygningensAnvendelse"];
|
|
6538
|
+
const registryId = props?.["id_lokalId"];
|
|
6539
|
+
const rawYear = props?.["byg026Opf\xF8relses\xE5r"];
|
|
6540
|
+
return {
|
|
6541
|
+
featureType: "building",
|
|
6542
|
+
areaM2: typeof rawArea2 === "number" ? rawArea2 : void 0,
|
|
6543
|
+
buildingUse: buildingUse ?? void 0,
|
|
6544
|
+
yearBuilt: typeof rawYear === "number" ? rawYear : void 0,
|
|
6545
|
+
registryId: registryId ?? void 0
|
|
6546
|
+
};
|
|
6547
|
+
}
|
|
6548
|
+
if (category === "zone") {
|
|
6549
|
+
const zoneType = props?.["plannavn"] ?? props?.["bezeichnung"] ?? props?.["typ_bezeichnung"] ?? void 0;
|
|
6550
|
+
const planId = props?.["planid"] ?? props?.["plannummer"] ?? void 0;
|
|
6551
|
+
const zoneCode = props?.["artcode"] ?? props?.["typ_code"] ?? planId ?? void 0;
|
|
6552
|
+
const rawLegalStatus = props?.["rechtsstatus"] ?? props?.["status"] ?? void 0;
|
|
6553
|
+
const planDocumentLink = props?.["dagsordenpunkt_url"] ?? props?.["dokument_url"] ?? void 0;
|
|
6554
|
+
const rawArea2 = props?.["flaeche"] ?? props?.["flaeche_m2"] ?? void 0;
|
|
6555
|
+
return {
|
|
6556
|
+
featureType: "zone",
|
|
6557
|
+
zoneType: zoneType ?? void 0,
|
|
6558
|
+
zoneCode: zoneCode ?? void 0,
|
|
6559
|
+
legalStatus: classifyZoneLegalStatus(rawLegalStatus),
|
|
6560
|
+
planType: classifyZonePlanType(props?.["typ_gde_bezeichnung"]),
|
|
6561
|
+
planId: planId ?? void 0,
|
|
6562
|
+
planDocumentLink: planDocumentLink ?? void 0,
|
|
6563
|
+
publicationDate: props?.["auflagedatum"] ?? void 0,
|
|
6564
|
+
fixingDate: props?.["festsetzungsdatum"] ?? void 0,
|
|
6565
|
+
approvalDate: props?.["genehmigungsdatum"] ?? void 0,
|
|
6566
|
+
effectiveFrom: props?.["inkraftsetzungsdatum"] ?? void 0,
|
|
6567
|
+
areaM2: typeof rawArea2 === "number" ? rawArea2 : void 0
|
|
6568
|
+
};
|
|
6569
|
+
}
|
|
6570
|
+
const parcelNr = props?.["mat:matrikelnummer"] ?? props?.["matrikelnummer"];
|
|
6571
|
+
const districtName = props?.["mat:ejerlavsnavn"] ?? props?.["ejerlavsnavn"];
|
|
6572
|
+
const rawArea = props?.["mat:registreretareal"] ?? props?.["registreretareal"];
|
|
6573
|
+
const bfeNr = props?.["mat:bfenummer"] ?? props?.["bfenummer"];
|
|
6574
|
+
return {
|
|
6575
|
+
featureType: "plot",
|
|
6576
|
+
areaM2: typeof rawArea === "number" ? rawArea : void 0,
|
|
6577
|
+
plotId: parcelNr && districtName ? `${parcelNr}, ${districtName}` : parcelNr ?? void 0,
|
|
6578
|
+
registryId: bfeNr ?? void 0
|
|
6579
|
+
};
|
|
6580
|
+
}
|
|
6581
|
+
var CueSdkGisAdapter = class {
|
|
6582
|
+
categoryMap;
|
|
6583
|
+
dataViewsBaseUrl;
|
|
6584
|
+
getHeaders;
|
|
6585
|
+
sourceId;
|
|
6586
|
+
constructor(options) {
|
|
6587
|
+
this.dataViewsBaseUrl = options.dataViewsBaseUrl.replace(/\/$/, "");
|
|
6588
|
+
this.getHeaders = options.getHeaders;
|
|
6589
|
+
this.categoryMap = options.categoryMap ?? DEFAULT_DANISH_SDK_CATEGORY_MAP;
|
|
6590
|
+
this.sourceId = options.sourceId ?? "cue-sdk-gis";
|
|
6591
|
+
}
|
|
6592
|
+
async listFeatureCategoryDescriptors(bbox) {
|
|
6593
|
+
const layers = await this.listAvailableLayers(bbox);
|
|
6594
|
+
return [
|
|
6595
|
+
...new Map(
|
|
6596
|
+
layers.map((l) => [l.category, featureCategoryDescriptor(l.category)])
|
|
6597
|
+
).values()
|
|
6598
|
+
];
|
|
6599
|
+
}
|
|
6600
|
+
async listAvailableLayers(bbox) {
|
|
6601
|
+
const entries = Object.entries(this.categoryMap);
|
|
6602
|
+
const results = await Promise.allSettled(
|
|
6603
|
+
entries.flatMap(
|
|
6604
|
+
([category, layerEntries]) => layerEntries.map(async (entry) => {
|
|
6605
|
+
const fc = await this._fetchFeatures(bbox, entry, 1);
|
|
6606
|
+
return { category, entry, hasResults: fc.features.length > 0 };
|
|
6607
|
+
})
|
|
6608
|
+
)
|
|
6609
|
+
);
|
|
6610
|
+
return results.filter(
|
|
6611
|
+
(r) => r.status === "fulfilled" && r.value.hasResults
|
|
6612
|
+
).map((r) => toLayerDescriptor3(this.sourceId, r.value.category, r.value.entry));
|
|
6613
|
+
}
|
|
6614
|
+
async getFeaturesForLayer(bbox, layerId) {
|
|
6615
|
+
const result = this._findLayerById(layerId);
|
|
6616
|
+
if (!result)
|
|
6617
|
+
return [];
|
|
6618
|
+
const fc = await this._fetchFeatures(bbox, result.entry);
|
|
6619
|
+
return fc.features.map((f, i) => this._toGisFeature(f, result.descriptor, i));
|
|
6620
|
+
}
|
|
6621
|
+
async listFeatureCategories(bbox) {
|
|
6622
|
+
const layers = await this.listAvailableLayers(bbox);
|
|
6623
|
+
return [...new Set(layers.map((l) => l.category))];
|
|
6624
|
+
}
|
|
6625
|
+
async getFeaturesOfCategory(bbox, category) {
|
|
6626
|
+
const layerEntries = this.categoryMap[category] ?? [];
|
|
6627
|
+
const batches = await Promise.all(
|
|
6628
|
+
layerEntries.map((entry) => {
|
|
6629
|
+
const descriptor = toLayerDescriptor3(this.sourceId, category, entry);
|
|
6630
|
+
return this.getFeaturesForLayer(bbox, descriptor.id);
|
|
6631
|
+
})
|
|
6632
|
+
);
|
|
6633
|
+
return batches.flat();
|
|
6634
|
+
}
|
|
6635
|
+
// ─── Private helpers ────────────────────────────────────────────────────────
|
|
6636
|
+
_findLayerById(layerId) {
|
|
6637
|
+
for (const [category, entries] of Object.entries(this.categoryMap)) {
|
|
6638
|
+
for (const entry of entries) {
|
|
6639
|
+
const descriptor = toLayerDescriptor3(this.sourceId, category, entry);
|
|
6640
|
+
if (descriptor.id === layerId)
|
|
6641
|
+
return { entry, descriptor };
|
|
6642
|
+
}
|
|
6643
|
+
}
|
|
6644
|
+
return void 0;
|
|
6645
|
+
}
|
|
6646
|
+
async _fetchFeatures(bbox, entry, count) {
|
|
6647
|
+
const [west, south, east, north] = bbox;
|
|
6648
|
+
const headers = await this.getHeaders();
|
|
6649
|
+
const params = new URLSearchParams({
|
|
6650
|
+
source: entry.source,
|
|
6651
|
+
typename: entry.typename,
|
|
6652
|
+
bbox: `${west},${south},${east},${north}`
|
|
6653
|
+
});
|
|
6654
|
+
if (count !== void 0) {
|
|
6655
|
+
params.set("count", String(count));
|
|
6656
|
+
}
|
|
6657
|
+
const url = `${this.dataViewsBaseUrl}/gis/features?${params.toString()}`;
|
|
6658
|
+
const response = await fetch(url, { headers });
|
|
6659
|
+
if (!response.ok) {
|
|
6660
|
+
throw new Error(
|
|
6661
|
+
`CueSdkGisAdapter: request failed for ${entry.source}/${entry.typename}: ${response.status} ${response.statusText}`
|
|
6662
|
+
);
|
|
6663
|
+
}
|
|
6664
|
+
return response.json();
|
|
6665
|
+
}
|
|
6666
|
+
_toGisFeature(feature, layer, index) {
|
|
6667
|
+
const [lon, lat] = firstCoordinate2(feature.geometry);
|
|
6668
|
+
const featureId = feature.id ?? `${layer.sourceLayerId}/${index}`;
|
|
6669
|
+
const name = pickName2(feature.properties, featureId);
|
|
6670
|
+
const featureBBox = feature.bbox ? [feature.bbox[0], feature.bbox[1], feature.bbox[2], feature.bbox[3]] : void 0;
|
|
6671
|
+
const tier = layer.tier;
|
|
6672
|
+
const properties = tier === "priority" ? _extractNormalizedProperties(feature.properties, layer.category) : void 0;
|
|
6673
|
+
return {
|
|
6674
|
+
id: featureId,
|
|
6675
|
+
sourceId: this.sourceId,
|
|
6676
|
+
sourceFeatureId: featureId,
|
|
6677
|
+
layerId: layer.id,
|
|
6678
|
+
name,
|
|
6679
|
+
category: layer.category,
|
|
6680
|
+
preferredColor: layer.preferredColor,
|
|
6681
|
+
type: layer.sourceLayerId.replace(/^[^:]+:/, ""),
|
|
6682
|
+
lat,
|
|
6683
|
+
lon,
|
|
6684
|
+
displayName: name,
|
|
6685
|
+
bbox: featureBBox,
|
|
6686
|
+
geometry: feature.geometry ?? void 0,
|
|
6687
|
+
tier,
|
|
6688
|
+
properties,
|
|
6689
|
+
originalData: feature.properties ?? {}
|
|
6690
|
+
};
|
|
6691
|
+
}
|
|
6692
|
+
};
|
|
6693
|
+
|
|
6694
|
+
// libs/js/cue-gis/src/lib/global-config.ts
|
|
6695
|
+
var DEFAULT_ZURICH_CATEGORY_MAP = {
|
|
6696
|
+
address: [
|
|
6697
|
+
"ms:ogd-0406_arv_basis_avzh_hausnummer_pos_p",
|
|
6698
|
+
"ms:ogd-0571_afv_gv_strat_strassennetz_l"
|
|
6699
|
+
],
|
|
6700
|
+
poi: [
|
|
6701
|
+
"ms:ogd-0053_giszhpub_ogd_veloparkieranlagen_p",
|
|
6702
|
+
"ms:ogd-0406_arv_basis_avzh_gebaeudeeingang_p"
|
|
6703
|
+
],
|
|
6704
|
+
railway: [
|
|
6705
|
+
"ms:ogd-0529_afv_gv_seilbahn_skilift_l",
|
|
6706
|
+
"ms:ogd-0529_afv_gv_seilbahn_skilift_p"
|
|
6707
|
+
],
|
|
6708
|
+
natural: [
|
|
6709
|
+
"ms:ogd-0045_giszhpub_wb_fliessgewaesser_l",
|
|
6710
|
+
"ms:ogd-0045_giszhpub_wb_stehgewaesser_f",
|
|
6711
|
+
"ms:ogd-0111_giszhpub_wald_waldareal_f"
|
|
6712
|
+
],
|
|
6713
|
+
manmade: [
|
|
6714
|
+
"ms:ogd-0248_giszhpub_en_gebaeude_volumen_ha_f",
|
|
6715
|
+
// Transport / infrastructure surface types from the cantonal land-cover layer.
|
|
6716
|
+
{ typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('Bahngebiet', 'Strasse_Weg', 'Flugplatz', 'Bruecke')" }
|
|
6717
|
+
],
|
|
6718
|
+
building: [
|
|
6719
|
+
// Individual building footprint polygons from the official cantonal survey (AV ZH).
|
|
6720
|
+
// BoFlaeche covers all land-cover types; the CQL filter restricts to buildings.
|
|
6721
|
+
{ typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art = 'Geb\xE4ude'" }
|
|
6722
|
+
],
|
|
6723
|
+
greenspace: [
|
|
6724
|
+
// Parks, gardens, playgrounds, sports facilities and other vegetated open areas.
|
|
6725
|
+
{ typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('Gartenanlage', 'Parkanlage', 'Spielplatz', 'Sportanlage', 'Friedhof', 'Gehoelz', 'Feuchtgebiet')" }
|
|
6726
|
+
],
|
|
6727
|
+
paved: [
|
|
6728
|
+
// Sealed / paved ground surfaces (courtyards, sidewalks, parking, etc.).
|
|
6729
|
+
{ typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('befBoden', 'befestigte Fl\xE4che', 'Hof', 'Trottoir', 'Parkplatz')" }
|
|
6730
|
+
],
|
|
6731
|
+
cadastre: [
|
|
6732
|
+
// Land parcels (Liegenschaften) from the official cantonal survey (AV ZH)
|
|
6733
|
+
"ms:ogd-0404_arv_basis_avzh_liegenschaften_f",
|
|
6734
|
+
// Independent and permanent rights (SelbstRecht / SDR)
|
|
6735
|
+
"ms:ogd-0404_arv_basis_avzh_selbstrecht_f"
|
|
6736
|
+
],
|
|
6737
|
+
zone: [
|
|
6738
|
+
// ÖREB Nutzungsplanung (Grundnutzung) – primary land-use zoning polygons
|
|
6739
|
+
"ms:ogd-0156_arv_basis_np_gn_zonenflaeche_f",
|
|
6740
|
+
// ÖREB Überlagernde Festlegungen (Flächen) – overlaying planning constraints
|
|
6741
|
+
"ms:ogd-0155_arv_basis_np_ul_flaeche_f"
|
|
6742
|
+
]
|
|
6743
|
+
};
|
|
6744
|
+
var DEFAULT_LOCAL_SERVICE_REGIONS = [
|
|
6745
|
+
{
|
|
6746
|
+
name: "Canton of Z\xFCrich",
|
|
6747
|
+
coverageBBox: ZURICH_CANTON_BBOX,
|
|
6748
|
+
supportedCategories: ["address", "poi", "railway", "natural", "manmade", "building", "cadastre", "greenspace", "paved", "zone"],
|
|
6749
|
+
priority: 10,
|
|
6750
|
+
adapter: new ZurichMapsAdapter({
|
|
6751
|
+
categoryMap: DEFAULT_ZURICH_CATEGORY_MAP
|
|
6752
|
+
})
|
|
6753
|
+
},
|
|
6754
|
+
{
|
|
6755
|
+
name: "Switzerland (national AV cadastre)",
|
|
6756
|
+
coverageBBox: SWITZERLAND_BBOX,
|
|
6757
|
+
supportedCategories: ["cadastre"],
|
|
6758
|
+
priority: 5,
|
|
6759
|
+
// Lower priority than canton-specific adapters. For ZH, the canton adapter (priority 10) wins.
|
|
6760
|
+
adapter: new ZurichMapsAdapter({
|
|
6761
|
+
sourceId: "swiss-av-wfs",
|
|
6762
|
+
baseUrl: "https://wfs.geodienste.ch/av_0/deu",
|
|
6763
|
+
categoryMap: {
|
|
6764
|
+
cadastre: [
|
|
6765
|
+
"ms:RESF",
|
|
6766
|
+
// Rechtsgültige Liegenschaften (valid parcels, all cantons)
|
|
6767
|
+
"ms:DPRSF"
|
|
6768
|
+
// Selbständige und dauernde Rechte / SDR (all cantons)
|
|
6769
|
+
]
|
|
6770
|
+
}
|
|
6771
|
+
})
|
|
6772
|
+
},
|
|
6773
|
+
{
|
|
6774
|
+
/**
|
|
6775
|
+
* Denmark planning zones (lokalplan + kommuneplanrammer) from plandata.dk.
|
|
6776
|
+
*
|
|
6777
|
+
* This is a fully open WFS service (no API key required) provided by
|
|
6778
|
+
* Plandata, the Danish national planning data register.
|
|
6779
|
+
*
|
|
6780
|
+
* - `pdk:lokalplan_vedtaget` – adopted local plans (lokalplaner)
|
|
6781
|
+
* - `pdk:kommuneplanramme_vedtaget` – adopted municipal plan frameworks
|
|
6782
|
+
*/
|
|
6783
|
+
name: "Denmark (Plandata.dk \u2013 planning zones)",
|
|
6784
|
+
coverageBBox: DENMARK_BBOX,
|
|
6785
|
+
supportedCategories: ["zone"],
|
|
6786
|
+
priority: 10,
|
|
6787
|
+
adapter: new ZurichMapsAdapter({
|
|
6788
|
+
sourceId: "danish-plandata",
|
|
6789
|
+
baseUrl: "https://wfs.plandata.dk/geoserver/pdk/wfs",
|
|
6790
|
+
outputFormat: "application/json",
|
|
6791
|
+
categoryMap: {
|
|
6792
|
+
zone: [
|
|
6793
|
+
"pdk:lokalplan_vedtaget",
|
|
6794
|
+
// Adopted local plans
|
|
6795
|
+
"pdk:kommuneplanramme_vedtaget"
|
|
6796
|
+
// Adopted municipal plan frameworks
|
|
6797
|
+
]
|
|
6798
|
+
}
|
|
6799
|
+
})
|
|
6800
|
+
}
|
|
6801
|
+
];
|
|
6802
|
+
|
|
6803
|
+
// libs/js/cue-gis/src/lib/gis-gateway.ts
|
|
6804
|
+
function sortByCanonicalOrder(descriptors) {
|
|
6805
|
+
return [...descriptors].sort(
|
|
6806
|
+
(a5, b) => FEATURE_CATEGORIES.indexOf(a5.category) - FEATURE_CATEGORIES.indexOf(b.category)
|
|
6807
|
+
);
|
|
6808
|
+
}
|
|
6809
|
+
var GisGateway = class {
|
|
6810
|
+
nominatim;
|
|
6811
|
+
regions;
|
|
6812
|
+
/**
|
|
6813
|
+
* Effective zone plan type colour map — {@link ZONE_PLAN_TYPE_COLORS} merged
|
|
6814
|
+
* with any overrides supplied via {@link GisGatewayOptions.zonePlanTypeColors}.
|
|
6815
|
+
*/
|
|
6816
|
+
zonePlanTypeColors;
|
|
6817
|
+
constructor(options = {}) {
|
|
6818
|
+
const {
|
|
6819
|
+
regions,
|
|
6820
|
+
useDefaultRegions = true,
|
|
6821
|
+
zonePlanTypeColors,
|
|
6822
|
+
...nominatimOptions
|
|
6823
|
+
} = options;
|
|
6824
|
+
this.nominatim = new NominatimAdapter(nominatimOptions);
|
|
6825
|
+
const defaultRegions = useDefaultRegions ? DEFAULT_LOCAL_SERVICE_REGIONS : [];
|
|
6826
|
+
this.regions = regions ?? defaultRegions;
|
|
6827
|
+
this.zonePlanTypeColors = { ...ZONE_PLAN_TYPE_COLORS, ...zonePlanTypeColors };
|
|
6828
|
+
}
|
|
6829
|
+
/**
|
|
6830
|
+
* Return the available feature categories together with UI metadata such as
|
|
6831
|
+
* preferred color.
|
|
6832
|
+
*
|
|
6833
|
+
* When the matching regions declare their {@link LocalServiceRegion.supportedCategories}
|
|
6834
|
+
* the result is built from those declarations without network probing.
|
|
6835
|
+
* Otherwise the winning adapter's own probing is used.
|
|
6836
|
+
*
|
|
6837
|
+
* @param bbox [west, south, east, north] in WGS-84 decimal degrees
|
|
6838
|
+
*/
|
|
6839
|
+
async listFeatureCategoryDescriptors(bbox) {
|
|
6840
|
+
const declaredRegions = this.regions.filter((r) => bboxIntersects(r.coverageBBox, bbox) && r.supportedCategories).sort((a5, b) => (b.priority ?? 0) - (a5.priority ?? 0));
|
|
6841
|
+
if (declaredRegions.length > 0) {
|
|
6842
|
+
const seen = /* @__PURE__ */ new Set();
|
|
6843
|
+
const descriptors = [];
|
|
6844
|
+
for (const region of declaredRegions) {
|
|
6845
|
+
for (const cat of region.supportedCategories ?? []) {
|
|
6846
|
+
if (!seen.has(cat)) {
|
|
6847
|
+
seen.add(cat);
|
|
6848
|
+
descriptors.push(featureCategoryDescriptor(cat));
|
|
6849
|
+
}
|
|
6850
|
+
}
|
|
6851
|
+
}
|
|
6852
|
+
for (const cat of NOMINATIM_FEATURE_CATEGORIES) {
|
|
6853
|
+
if (!seen.has(cat)) {
|
|
6854
|
+
descriptors.push(featureCategoryDescriptor(cat));
|
|
6855
|
+
}
|
|
6856
|
+
}
|
|
6857
|
+
return sortByCanonicalOrder(descriptors);
|
|
6858
|
+
}
|
|
6859
|
+
const probed = await this._adapterFor(bbox).listFeatureCategoryDescriptors(bbox);
|
|
6860
|
+
return sortByCanonicalOrder(probed);
|
|
6861
|
+
}
|
|
6862
|
+
/**
|
|
6863
|
+
* Return the feature categories that have at least one result within the
|
|
6864
|
+
* given bounding box.
|
|
6865
|
+
*
|
|
6866
|
+
* @param bbox [west, south, east, north] in WGS-84 decimal degrees
|
|
6867
|
+
*/
|
|
6868
|
+
async listFeatureCategories(bbox) {
|
|
6869
|
+
const descriptors = await this.listFeatureCategoryDescriptors(bbox);
|
|
6870
|
+
return descriptors.map((d) => d.category);
|
|
6871
|
+
}
|
|
6872
|
+
/**
|
|
6873
|
+
* Return all available layers for the given bbox using a provider-independent
|
|
6874
|
+
* layer descriptor shape.
|
|
6875
|
+
*/
|
|
6876
|
+
async listAvailableLayers(bbox) {
|
|
6877
|
+
return this._adapterFor(bbox).listAvailableLayers(bbox);
|
|
6878
|
+
}
|
|
6879
|
+
/**
|
|
6880
|
+
* Stream GIS features of the given category from **all** services whose
|
|
6881
|
+
* coverage intersects the query bbox (plus the Nominatim global fallback).
|
|
6882
|
+
*
|
|
6883
|
+
* Processing steps:
|
|
6884
|
+
* 1. Log all configured services and their coverage bboxes.
|
|
6885
|
+
* 2. Identify services whose `coverageBBox` intersects `bbox`.
|
|
6886
|
+
* 3. Log the matched services.
|
|
6887
|
+
* 4. Fan out requests to all matched services in parallel; yield features as
|
|
6888
|
+
* each source resolves (streaming).
|
|
6889
|
+
* 5. All features are already normalised to {@link GisFeature} by each adapter.
|
|
6890
|
+
* 6. Quality-based deduplication: when two sources return a feature for the
|
|
6891
|
+
* same real-world object, only the higher-quality geometry is emitted.
|
|
6892
|
+
* A polygon/multipolygon beats a linestring, which beats a point. If a
|
|
6893
|
+
* better feature arrives after a lower-quality one was already yielded,
|
|
6894
|
+
* the better feature is yielded again (same {@link GisFeature.id}) so the
|
|
6895
|
+
* consumer can replace its copy.
|
|
6896
|
+
*
|
|
6897
|
+
* @param bbox [west, south, east, north] in WGS-84 decimal degrees
|
|
6898
|
+
* @param category One of the categories returned by {@link listFeatureCategories}
|
|
6899
|
+
*/
|
|
6900
|
+
async *streamFeaturesOfCategory(bbox, category) {
|
|
6901
|
+
console.log("[GisGateway] Available services:");
|
|
6902
|
+
for (const region of this.regions) {
|
|
6903
|
+
const [w, s, e, n] = region.coverageBBox;
|
|
6904
|
+
console.log(
|
|
6905
|
+
` \u2022 ${region.name} bbox=[${w}, ${s}, ${e}, ${n}] priority=${region.priority ?? 0}`
|
|
6906
|
+
);
|
|
6907
|
+
}
|
|
6908
|
+
console.log(" \u2022 Nominatim (OSM) bbox=[global] priority=-1");
|
|
6909
|
+
const matchedRegions = this.regions.filter(
|
|
6910
|
+
(r) => bboxIntersects(r.coverageBBox, bbox) && (!r.supportedCategories || r.supportedCategories.includes(category))
|
|
6911
|
+
);
|
|
6912
|
+
const sources = [
|
|
6913
|
+
...matchedRegions.map((r) => ({
|
|
6914
|
+
name: r.name,
|
|
6915
|
+
adapter: r.adapter,
|
|
6916
|
+
priority: r.priority ?? 0
|
|
6917
|
+
})),
|
|
6918
|
+
{ name: "Nominatim (OSM)", adapter: this.nominatim, priority: -1 }
|
|
6919
|
+
];
|
|
6920
|
+
sources.sort((a5, b) => b.priority - a5.priority);
|
|
6921
|
+
console.log(`[GisGateway] Matched services for category "${category}":`);
|
|
6922
|
+
for (const s of sources) {
|
|
6923
|
+
console.log(` \u2022 ${s.name} (priority=${s.priority})`);
|
|
6924
|
+
}
|
|
6925
|
+
const qualityMap = /* @__PURE__ */ new Map();
|
|
6926
|
+
const remaining = new Map(
|
|
6927
|
+
sources.map(
|
|
6928
|
+
(s, i) => [
|
|
6929
|
+
i,
|
|
6930
|
+
s.adapter.getFeaturesOfCategory(bbox, category).then((features) => ({ features, idx: i })).catch((err) => {
|
|
6931
|
+
console.warn(`[GisGateway] Source "${s.name}" failed:`, err);
|
|
6932
|
+
return { features: [], idx: i };
|
|
6933
|
+
})
|
|
6934
|
+
]
|
|
6935
|
+
)
|
|
6936
|
+
);
|
|
6937
|
+
while (remaining.size > 0) {
|
|
6938
|
+
const { features, idx } = await Promise.race(remaining.values());
|
|
6939
|
+
remaining.delete(idx);
|
|
6940
|
+
for (const feature of features) {
|
|
6941
|
+
const key = _dedupKey(feature);
|
|
6942
|
+
const score = _geometryQuality(feature);
|
|
6943
|
+
const best = qualityMap.get(key);
|
|
6944
|
+
if (best === void 0 || score > best) {
|
|
6945
|
+
qualityMap.set(key, score);
|
|
6946
|
+
yield feature;
|
|
6947
|
+
}
|
|
6948
|
+
}
|
|
6949
|
+
}
|
|
6950
|
+
}
|
|
6951
|
+
/**
|
|
6952
|
+
* Return all GIS features of the given category within the bounding box.
|
|
6953
|
+
*
|
|
6954
|
+
* Fans out to all matching services in parallel, then applies:
|
|
6955
|
+
* 1. Spatial merging – Nominatim point features that fall within ~100 m of a
|
|
6956
|
+
* local-service polygon are merged into that polygon (enriching its
|
|
6957
|
+
* `originalData`) rather than returned as a separate feature.
|
|
6958
|
+
* 2. Quality-based deduplication – when two unmerged features represent the
|
|
6959
|
+
* same object, the one with richer geometry is kept.
|
|
6960
|
+
*
|
|
6961
|
+
* @param bbox [west, south, east, north] in WGS-84 decimal degrees
|
|
6962
|
+
* @param category One of the categories returned by {@link listFeatureCategories}
|
|
6963
|
+
*/
|
|
6964
|
+
async getFeaturesOfCategory(bbox, category) {
|
|
6965
|
+
const matchedRegions = this.regions.filter(
|
|
6966
|
+
(r) => bboxIntersects(r.coverageBBox, bbox) && (!r.supportedCategories || r.supportedCategories.includes(category))
|
|
6967
|
+
);
|
|
6968
|
+
const sources = [
|
|
6969
|
+
...matchedRegions.map((r) => ({ name: r.name, adapter: r.adapter, isNominatim: false })),
|
|
6970
|
+
{ name: "Nominatim (OSM)", adapter: this.nominatim, isNominatim: true }
|
|
6971
|
+
];
|
|
6972
|
+
const batches = await Promise.allSettled(
|
|
6973
|
+
sources.map(
|
|
6974
|
+
(s) => s.adapter.getFeaturesOfCategory(bbox, category).then((features) => ({
|
|
6975
|
+
features,
|
|
6976
|
+
isNominatim: s.isNominatim
|
|
6977
|
+
})).catch((err) => {
|
|
6978
|
+
console.warn(`[GisGateway] Source "${s.name}" failed:`, err);
|
|
6979
|
+
return { features: [], isNominatim: s.isNominatim };
|
|
6980
|
+
})
|
|
6981
|
+
)
|
|
6982
|
+
);
|
|
6983
|
+
const localFeatures = [];
|
|
6984
|
+
const nominatimFeatures = [];
|
|
6985
|
+
for (const result of batches) {
|
|
6986
|
+
if (result.status !== "fulfilled")
|
|
6987
|
+
continue;
|
|
6988
|
+
if (result.value.isNominatim) {
|
|
6989
|
+
nominatimFeatures.push(...result.value.features);
|
|
6990
|
+
} else {
|
|
6991
|
+
localFeatures.push(...result.value.features);
|
|
6992
|
+
}
|
|
6993
|
+
}
|
|
6994
|
+
const merged = _mergeNominatimIntoPolygons(localFeatures, nominatimFeatures);
|
|
6995
|
+
const qualityMap = /* @__PURE__ */ new Map();
|
|
6996
|
+
const deduplicated = [];
|
|
6997
|
+
for (const feature of merged) {
|
|
6998
|
+
const key = _dedupKey(feature);
|
|
6999
|
+
const score = _geometryQuality(feature);
|
|
7000
|
+
const best = qualityMap.get(key);
|
|
7001
|
+
if (best === void 0 || score > best) {
|
|
7002
|
+
qualityMap.set(key, score);
|
|
7003
|
+
deduplicated.push(feature);
|
|
7004
|
+
}
|
|
7005
|
+
}
|
|
7006
|
+
return deduplicated;
|
|
7007
|
+
}
|
|
7008
|
+
/** Fetch features for a specific layer descriptor id. */
|
|
7009
|
+
async getFeaturesForLayer(bbox, layerId) {
|
|
7010
|
+
return this._adapterFor(bbox).getFeaturesForLayer(bbox, layerId);
|
|
7011
|
+
}
|
|
7012
|
+
/** Resolve the best adapter for a query bbox (legacy single-adapter routing). */
|
|
7013
|
+
_adapterFor(bbox) {
|
|
7014
|
+
const region = this.regions.filter((r) => bboxIntersects(r.coverageBBox, bbox)).sort((a5, b) => (b.priority ?? 0) - (a5.priority ?? 0))[0];
|
|
7015
|
+
return region?.adapter ?? this.nominatim;
|
|
7016
|
+
}
|
|
7017
|
+
};
|
|
7018
|
+
function _geometryQuality(feature) {
|
|
7019
|
+
const gt = feature.geometry?.type;
|
|
7020
|
+
if (gt === "Polygon" || gt === "MultiPolygon")
|
|
7021
|
+
return 3;
|
|
7022
|
+
if (gt === "LineString" || gt === "MultiLineString")
|
|
7023
|
+
return 2;
|
|
7024
|
+
return 1;
|
|
7025
|
+
}
|
|
7026
|
+
function _dedupKey(feature) {
|
|
7027
|
+
const lat = Math.round(feature.lat * 1e3) / 1e3;
|
|
7028
|
+
const lon = Math.round(feature.lon * 1e3) / 1e3;
|
|
7029
|
+
const name = (feature.name ?? feature.id ?? "").toLowerCase().trim();
|
|
7030
|
+
return `${name}|${lat}|${lon}`;
|
|
7031
|
+
}
|
|
7032
|
+
var MERGE_THRESHOLD = 1e-3;
|
|
7033
|
+
function _latLonDist(a5, b) {
|
|
7034
|
+
const dlat = a5.lat - b.lat;
|
|
7035
|
+
const dlon = a5.lon - b.lon;
|
|
7036
|
+
return Math.sqrt(dlat * dlat + dlon * dlon);
|
|
7037
|
+
}
|
|
7038
|
+
function _mergeNominatimIntoPolygons(localFeatures, nominatimFeatures) {
|
|
7039
|
+
if (nominatimFeatures.length === 0)
|
|
7040
|
+
return localFeatures;
|
|
7041
|
+
const polygons = localFeatures.filter((f) => _geometryQuality(f) === 3);
|
|
7042
|
+
const nonPolygons = localFeatures.filter((f) => _geometryQuality(f) !== 3);
|
|
7043
|
+
const enriched = polygons.map((f) => ({ feature: { ...f, originalData: { ...f.originalData } }, matched: false }));
|
|
7044
|
+
const unmatched = [];
|
|
7045
|
+
for (const nom of nominatimFeatures) {
|
|
7046
|
+
let bestIdx = -1;
|
|
7047
|
+
let bestDist = MERGE_THRESHOLD;
|
|
7048
|
+
for (let i = 0; i < enriched.length; i++) {
|
|
7049
|
+
const dist = _latLonDist(nom, enriched[i].feature);
|
|
7050
|
+
if (dist < bestDist) {
|
|
7051
|
+
bestDist = dist;
|
|
7052
|
+
bestIdx = i;
|
|
7053
|
+
}
|
|
7054
|
+
}
|
|
7055
|
+
if (bestIdx >= 0) {
|
|
7056
|
+
enriched[bestIdx].feature.originalData = {
|
|
7057
|
+
...nom.originalData,
|
|
7058
|
+
...enriched[bestIdx].feature.originalData
|
|
7059
|
+
};
|
|
7060
|
+
enriched[bestIdx].matched = true;
|
|
7061
|
+
} else {
|
|
7062
|
+
unmatched.push(nom);
|
|
7063
|
+
}
|
|
7064
|
+
}
|
|
7065
|
+
return [
|
|
7066
|
+
...enriched.map((e) => e.feature),
|
|
7067
|
+
...nonPolygons,
|
|
7068
|
+
...unmatched
|
|
7069
|
+
];
|
|
7070
|
+
}
|
|
7071
|
+
|
|
7072
|
+
// libs/js/cue-sdk/src/lib/gis.ts
|
|
7073
|
+
var DATA_VIEWS_PATH = "/data-views";
|
|
7074
|
+
var DEBOUNCE_MS = 1500;
|
|
7075
|
+
var CueGis = class {
|
|
7076
|
+
// undefined = not yet built
|
|
7077
|
+
/** @internal — construct via `cue.gis`, not directly. */
|
|
7078
|
+
constructor(_getAuthHeaders, _gatewayUrl) {
|
|
7079
|
+
this._getAuthHeaders = _getAuthHeaders;
|
|
7080
|
+
this._gatewayUrl = _gatewayUrl;
|
|
7081
|
+
}
|
|
7082
|
+
_bbox = null;
|
|
7083
|
+
_projectId = null;
|
|
7084
|
+
_selectedCategories = /* @__PURE__ */ new Set();
|
|
7085
|
+
_queryToken = 0;
|
|
7086
|
+
_categoryTokens = /* @__PURE__ */ new Map();
|
|
7087
|
+
_debounceTimer = null;
|
|
7088
|
+
_availableCategories = [];
|
|
7089
|
+
_features = /* @__PURE__ */ new Map();
|
|
7090
|
+
_catListeners = /* @__PURE__ */ new Set();
|
|
7091
|
+
_featListeners = /* @__PURE__ */ new Set();
|
|
7092
|
+
_loadListeners = /* @__PURE__ */ new Set();
|
|
7093
|
+
_gatewayCache = null;
|
|
7094
|
+
_gatewayProjectId = void 0;
|
|
7095
|
+
// ── Setters ───────────────────────────────────────────────────────────────
|
|
7096
|
+
/**
|
|
7097
|
+
* Update the current map viewport.
|
|
7098
|
+
* Triggers a debounced query for available categories and reloads selected ones.
|
|
7099
|
+
*/
|
|
7100
|
+
setBbox(bbox) {
|
|
7101
|
+
this._bbox = bbox;
|
|
7102
|
+
this._scheduleDebouncedQuery();
|
|
7103
|
+
}
|
|
7104
|
+
/**
|
|
7105
|
+
* Set or clear the active project.
|
|
7106
|
+
* Rebuilds the authenticated adapter so subsequent requests carry the new project header.
|
|
7107
|
+
*/
|
|
7108
|
+
setProjectId(projectId) {
|
|
7109
|
+
if (projectId === this._projectId)
|
|
7110
|
+
return;
|
|
7111
|
+
this._projectId = projectId;
|
|
7112
|
+
this._gatewayCache = null;
|
|
7113
|
+
this._scheduleDebouncedQuery();
|
|
7114
|
+
}
|
|
7115
|
+
/**
|
|
7116
|
+
* Replace the full set of selected categories.
|
|
7117
|
+
* Newly added categories begin loading immediately; removed categories are cleared.
|
|
7118
|
+
*/
|
|
7119
|
+
setSelectedCategories(categories) {
|
|
7120
|
+
const prev = this._selectedCategories;
|
|
7121
|
+
this._selectedCategories = new Set(categories);
|
|
7122
|
+
let featuresChanged = false;
|
|
7123
|
+
for (const cat of prev) {
|
|
7124
|
+
if (!categories.has(cat)) {
|
|
7125
|
+
this._features.delete(cat);
|
|
7126
|
+
featuresChanged = true;
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
if (featuresChanged)
|
|
7130
|
+
this._emitFeatures();
|
|
7131
|
+
const bbox = this._bbox;
|
|
7132
|
+
if (bbox) {
|
|
7133
|
+
for (const cat of categories) {
|
|
7134
|
+
if (!prev.has(cat))
|
|
7135
|
+
this._loadCategory(cat, bbox, this._queryToken);
|
|
7136
|
+
}
|
|
7137
|
+
}
|
|
7138
|
+
}
|
|
7139
|
+
// ── Subscriptions ─────────────────────────────────────────────────────────
|
|
7140
|
+
/**
|
|
7141
|
+
* Subscribe to available-category updates.
|
|
7142
|
+
* Replays the current value immediately, then fires on every bbox change.
|
|
7143
|
+
* @returns An unsubscribe function.
|
|
7144
|
+
*/
|
|
7145
|
+
onAvailableCategories(cb) {
|
|
7146
|
+
cb([...this._availableCategories]);
|
|
7147
|
+
this._catListeners.add(cb);
|
|
7148
|
+
return () => this._catListeners.delete(cb);
|
|
7149
|
+
}
|
|
7150
|
+
/**
|
|
7151
|
+
* Subscribe to the feature map (category → GisFeature[]).
|
|
7152
|
+
* Replays the current value immediately, then fires whenever features change.
|
|
7153
|
+
* @returns An unsubscribe function.
|
|
7154
|
+
*/
|
|
7155
|
+
onFeaturesChange(cb) {
|
|
7156
|
+
cb(new Map(this._features));
|
|
7157
|
+
this._featListeners.add(cb);
|
|
7158
|
+
return () => this._featListeners.delete(cb);
|
|
7159
|
+
}
|
|
7160
|
+
/**
|
|
7161
|
+
* Subscribe to the global loading state.
|
|
7162
|
+
* Fires `true` while the category list is being queried, `false` when done.
|
|
7163
|
+
* @returns An unsubscribe function.
|
|
7164
|
+
*/
|
|
7165
|
+
onLoadingChange(cb) {
|
|
7166
|
+
this._loadListeners.add(cb);
|
|
7167
|
+
return () => this._loadListeners.delete(cb);
|
|
7168
|
+
}
|
|
7169
|
+
/** Cancel all pending requests and clear all listeners. */
|
|
7170
|
+
destroy() {
|
|
7171
|
+
if (this._debounceTimer !== null)
|
|
7172
|
+
clearTimeout(this._debounceTimer);
|
|
7173
|
+
this._queryToken++;
|
|
7174
|
+
this._catListeners.clear();
|
|
7175
|
+
this._featListeners.clear();
|
|
7176
|
+
this._loadListeners.clear();
|
|
7177
|
+
}
|
|
7178
|
+
// ── Private ───────────────────────────────────────────────────────────────
|
|
7179
|
+
_scheduleDebouncedQuery() {
|
|
7180
|
+
if (this._debounceTimer !== null)
|
|
7181
|
+
clearTimeout(this._debounceTimer);
|
|
7182
|
+
this._debounceTimer = setTimeout(() => this._queryLayers(), DEBOUNCE_MS);
|
|
7183
|
+
}
|
|
7184
|
+
_getGateway() {
|
|
7185
|
+
if (this._gatewayCache !== null && this._gatewayProjectId === this._projectId) {
|
|
7186
|
+
return this._gatewayCache;
|
|
7187
|
+
}
|
|
7188
|
+
const projectId = this._projectId;
|
|
7189
|
+
const regions = [...DEFAULT_LOCAL_SERVICE_REGIONS];
|
|
7190
|
+
if (projectId) {
|
|
7191
|
+
regions.unshift({
|
|
7192
|
+
name: "Cue SDK (authenticated)",
|
|
7193
|
+
coverageBBox: DENMARK_BBOX,
|
|
7194
|
+
supportedCategories: Object.keys(DEFAULT_DANISH_SDK_CATEGORY_MAP),
|
|
7195
|
+
priority: 10,
|
|
7196
|
+
adapter: new CueSdkGisAdapter({
|
|
7197
|
+
dataViewsBaseUrl: `${this._gatewayUrl}${DATA_VIEWS_PATH}`,
|
|
7198
|
+
getHeaders: async () => ({
|
|
7199
|
+
...await this._getAuthHeaders(),
|
|
7200
|
+
"x-project-id": projectId
|
|
7201
|
+
})
|
|
7202
|
+
})
|
|
7203
|
+
});
|
|
7204
|
+
}
|
|
7205
|
+
this._gatewayCache = new GisGateway({ regions });
|
|
7206
|
+
this._gatewayProjectId = projectId;
|
|
7207
|
+
return this._gatewayCache;
|
|
7208
|
+
}
|
|
7209
|
+
async _queryLayers() {
|
|
7210
|
+
const bbox = this._bbox;
|
|
7211
|
+
if (!bbox)
|
|
7212
|
+
return;
|
|
7213
|
+
this._queryToken++;
|
|
7214
|
+
const token = this._queryToken;
|
|
7215
|
+
this._emitLoading(true);
|
|
7216
|
+
try {
|
|
7217
|
+
const descriptors = await this._getGateway().listFeatureCategoryDescriptors(bbox);
|
|
7218
|
+
if (token !== this._queryToken)
|
|
7219
|
+
return;
|
|
7220
|
+
this._availableCategories = descriptors;
|
|
7221
|
+
this._catListeners.forEach((cb) => cb([...descriptors]));
|
|
7222
|
+
const available = new Set(descriptors.map((d) => d.category));
|
|
7223
|
+
let changed = false;
|
|
7224
|
+
for (const cat of [...this._selectedCategories]) {
|
|
7225
|
+
if (!available.has(cat)) {
|
|
7226
|
+
this._selectedCategories.delete(cat);
|
|
7227
|
+
this._features.delete(cat);
|
|
7228
|
+
changed = true;
|
|
7229
|
+
}
|
|
7230
|
+
}
|
|
7231
|
+
if (changed)
|
|
7232
|
+
this._emitFeatures();
|
|
7233
|
+
for (const cat of this._selectedCategories) {
|
|
7234
|
+
this._loadCategory(cat, bbox, token);
|
|
7235
|
+
}
|
|
7236
|
+
} catch (err) {
|
|
7237
|
+
console.error("[CueGis] Failed to list categories:", err);
|
|
7238
|
+
} finally {
|
|
7239
|
+
if (token === this._queryToken)
|
|
7240
|
+
this._emitLoading(false);
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
async _loadCategory(category, bbox, ownerToken) {
|
|
7244
|
+
const catToken = (this._categoryTokens.get(category) ?? 0) + 1;
|
|
7245
|
+
this._categoryTokens.set(category, catToken);
|
|
7246
|
+
try {
|
|
7247
|
+
const features = await this._getGateway().getFeaturesOfCategory(bbox, category);
|
|
7248
|
+
const stale = this._categoryTokens.get(category) !== catToken || ownerToken !== this._queryToken || !this._selectedCategories.has(category);
|
|
7249
|
+
if (stale)
|
|
7250
|
+
return;
|
|
7251
|
+
this._features.set(category, features);
|
|
7252
|
+
this._emitFeatures();
|
|
7253
|
+
} catch (err) {
|
|
7254
|
+
console.error(`[CueGis] Failed to load "${category}":`, err);
|
|
7255
|
+
}
|
|
7256
|
+
}
|
|
7257
|
+
_emitFeatures() {
|
|
7258
|
+
const snapshot = new Map(this._features);
|
|
7259
|
+
this._featListeners.forEach((cb) => cb(snapshot));
|
|
7260
|
+
}
|
|
7261
|
+
_emitLoading(v) {
|
|
7262
|
+
this._loadListeners.forEach((cb) => cb(v));
|
|
7263
|
+
}
|
|
7264
|
+
};
|
|
7265
|
+
|
|
5364
7266
|
// libs/js/cue-sdk/src/lib/project.ts
|
|
5365
7267
|
var import_firestore3 = require("firebase/firestore");
|
|
5366
7268
|
var import_functions2 = require("firebase/functions");
|
|
@@ -5549,7 +7451,7 @@ var CueProfile = class {
|
|
|
5549
7451
|
}
|
|
5550
7452
|
/** Creates a new API key for the current user. */
|
|
5551
7453
|
async createAPIKey(expiration) {
|
|
5552
|
-
return this._fetch(
|
|
7454
|
+
return this._fetch(ENDPOINT_COMMANDS_PROFILE_API_KEYS, {
|
|
5553
7455
|
method: "POST",
|
|
5554
7456
|
headers: { "Content-Type": "application/json" },
|
|
5555
7457
|
body: JSON.stringify({ expiration })
|
|
@@ -5558,7 +7460,7 @@ var CueProfile = class {
|
|
|
5558
7460
|
/** Revokes the current user's API key. */
|
|
5559
7461
|
async revokeAPIKey() {
|
|
5560
7462
|
const response = await this._auth.authenticatedFetch(
|
|
5561
|
-
this._url(
|
|
7463
|
+
this._url(ENDPOINT_COMMANDS_PROFILE_API_KEYS),
|
|
5562
7464
|
{ method: "DELETE" }
|
|
5563
7465
|
);
|
|
5564
7466
|
if (!response.ok) {
|
|
@@ -5595,10 +7497,26 @@ var CueProfile = class {
|
|
|
5595
7497
|
const res = await fn({ uids });
|
|
5596
7498
|
return res.data;
|
|
5597
7499
|
}
|
|
5598
|
-
/** Record that the current user has accepted the terms of service. */
|
|
5599
|
-
async acceptTerms() {
|
|
5600
|
-
|
|
5601
|
-
|
|
7500
|
+
/** Record that the current user has accepted the terms of service. Sets a `terms` custom claim on the token. */
|
|
7501
|
+
async acceptTerms(version) {
|
|
7502
|
+
await this._fetch(ENDPOINT_COMMANDS_PROFILE_TERMS, {
|
|
7503
|
+
method: "POST",
|
|
7504
|
+
headers: { "Content-Type": "application/json" },
|
|
7505
|
+
body: JSON.stringify({ version })
|
|
7506
|
+
});
|
|
7507
|
+
}
|
|
7508
|
+
/**
|
|
7509
|
+
* Returns the terms version the current user has accepted (e.g. `"v1"`),
|
|
7510
|
+
* or `null` if they have not accepted any version yet.
|
|
7511
|
+
* Reads from the cached ID token — call after `acceptTerms()` with a
|
|
7512
|
+
* force-refreshed token to see the updated value.
|
|
7513
|
+
*/
|
|
7514
|
+
async latestTermsAccepted() {
|
|
7515
|
+
const user = this._auth.currentUser;
|
|
7516
|
+
if (!user)
|
|
7517
|
+
return null;
|
|
7518
|
+
const result = await (0, import_auth4.getIdTokenResult)(user);
|
|
7519
|
+
return result.claims["terms"] ?? null;
|
|
5602
7520
|
}
|
|
5603
7521
|
};
|
|
5604
7522
|
|
|
@@ -5785,19 +7703,20 @@ var CueProjectSchema = class {
|
|
|
5785
7703
|
this._projectId = _projectId;
|
|
5786
7704
|
this._queryCache = _queryCache;
|
|
5787
7705
|
this._graphType = _graphType;
|
|
5788
|
-
this.
|
|
7706
|
+
this._currentLang = language;
|
|
7707
|
+
this._api.setLanguage(language);
|
|
5789
7708
|
this._contentCategories = new CueSignal([]);
|
|
5790
7709
|
this._entityCategories = new CueSignal([]);
|
|
5791
7710
|
this._relationships = new CueSignal([]);
|
|
5792
7711
|
this.availableContentCategories = this._contentCategories.asReadonly();
|
|
5793
7712
|
this.availableEntityCategories = this._entityCategories.asReadonly();
|
|
5794
7713
|
this.availableEntityRelationships = this._relationships.asReadonly();
|
|
5795
|
-
this._load(language).catch(
|
|
7714
|
+
this.ready = this._load(language).catch(
|
|
5796
7715
|
(err) => console.error("[CueProjectSchema] Initial load failed:", err)
|
|
5797
7716
|
);
|
|
5798
7717
|
}
|
|
5799
7718
|
_cache = /* @__PURE__ */ new Map();
|
|
5800
|
-
|
|
7719
|
+
_currentLang;
|
|
5801
7720
|
_contentCategories;
|
|
5802
7721
|
_entityCategories;
|
|
5803
7722
|
_relationships;
|
|
@@ -5807,9 +7726,14 @@ var CueProjectSchema = class {
|
|
|
5807
7726
|
availableEntityCategories;
|
|
5808
7727
|
/** Currently active entity relationship types for the selected language. */
|
|
5809
7728
|
availableEntityRelationships;
|
|
7729
|
+
/**
|
|
7730
|
+
* Resolves when the initial schema load for the constructor language has
|
|
7731
|
+
* completed (or failed). Await this before reading signal values imperatively.
|
|
7732
|
+
*/
|
|
7733
|
+
ready;
|
|
5810
7734
|
/** Returns the currently active language. */
|
|
5811
7735
|
get language() {
|
|
5812
|
-
return this.
|
|
7736
|
+
return this._api.language;
|
|
5813
7737
|
}
|
|
5814
7738
|
/**
|
|
5815
7739
|
* Switch the active language. If the data for this language has already been
|
|
@@ -5817,9 +7741,10 @@ var CueProjectSchema = class {
|
|
|
5817
7741
|
* is triggered.
|
|
5818
7742
|
*/
|
|
5819
7743
|
setLanguage(lang) {
|
|
5820
|
-
if (this.
|
|
7744
|
+
if (this._currentLang === lang)
|
|
5821
7745
|
return;
|
|
5822
|
-
this.
|
|
7746
|
+
this._currentLang = lang;
|
|
7747
|
+
this._api.setLanguage(lang);
|
|
5823
7748
|
this._load(lang).catch(
|
|
5824
7749
|
(err) => console.error("[CueProjectSchema] Language switch failed:", err)
|
|
5825
7750
|
);
|
|
@@ -5829,7 +7754,7 @@ var CueProjectSchema = class {
|
|
|
5829
7754
|
* Useful when the triplestore data has changed.
|
|
5830
7755
|
*/
|
|
5831
7756
|
async refresh() {
|
|
5832
|
-
const lang = this.
|
|
7757
|
+
const lang = this._api.language;
|
|
5833
7758
|
this._cache.delete(lang);
|
|
5834
7759
|
await this._load(lang);
|
|
5835
7760
|
}
|
|
@@ -5855,7 +7780,7 @@ var CueProjectSchema = class {
|
|
|
5855
7780
|
},
|
|
5856
7781
|
(snapshot) => {
|
|
5857
7782
|
this._cache.set(lang, snapshot);
|
|
5858
|
-
if (this.
|
|
7783
|
+
if (this._currentLang === lang)
|
|
5859
7784
|
this._apply(snapshot);
|
|
5860
7785
|
},
|
|
5861
7786
|
this._queryCache
|
|
@@ -5944,7 +7869,9 @@ var CueProjectEntities = class {
|
|
|
5944
7869
|
baseURL;
|
|
5945
7870
|
// ── Internal writable slices ───────────────────────────────────────────────
|
|
5946
7871
|
_entityDetails = new CueSignal({});
|
|
5947
|
-
_entityDocuments = new CueSignal(
|
|
7872
|
+
_entityDocuments = new CueSignal(
|
|
7873
|
+
{}
|
|
7874
|
+
);
|
|
5948
7875
|
_entityRelationships = new CueSignal({});
|
|
5949
7876
|
_entityOSMMap = new CueSignal({});
|
|
5950
7877
|
_osmWKTMap = new CueSignal({});
|
|
@@ -5986,7 +7913,10 @@ var CueProjectEntities = class {
|
|
|
5986
7913
|
this._entityGraph.set(void 0);
|
|
5987
7914
|
this._fetchingOSMIds.clear();
|
|
5988
7915
|
this._fetchEntityGraph().catch(
|
|
5989
|
-
(err) => console.error(
|
|
7916
|
+
(err) => console.error(
|
|
7917
|
+
"[CueProjectEntities] Entity graph fetch failed after reset:",
|
|
7918
|
+
err
|
|
7919
|
+
)
|
|
5990
7920
|
);
|
|
5991
7921
|
}
|
|
5992
7922
|
// ── Public imperative API ──────────────────────────────────────────────────
|
|
@@ -5997,7 +7927,9 @@ var CueProjectEntities = class {
|
|
|
5997
7927
|
* Data is merged into `entityInfoMap` once the SPARQL response arrives.
|
|
5998
7928
|
*/
|
|
5999
7929
|
requestEntityData(uuids, includeMentionCount = false) {
|
|
6000
|
-
const newUUIDs = uuids.filter(
|
|
7930
|
+
const newUUIDs = uuids.filter(
|
|
7931
|
+
(id) => this._entityDetails.get()[id] === void 0
|
|
7932
|
+
);
|
|
6001
7933
|
if (newUUIDs.length === 0)
|
|
6002
7934
|
return;
|
|
6003
7935
|
const values = newUUIDs.map((id) => `r:${id}`).join(" ");
|
|
@@ -6015,7 +7947,9 @@ WHERE {
|
|
|
6015
7947
|
GROUP BY ?id ?mentionCount`;
|
|
6016
7948
|
this._api.sparql(q, this._projectId, this._graphType).then((data) => {
|
|
6017
7949
|
const result = data;
|
|
6018
|
-
const updates = {
|
|
7950
|
+
const updates = {
|
|
7951
|
+
...this._entityDetails.get()
|
|
7952
|
+
};
|
|
6019
7953
|
result.results.bindings.forEach((b) => {
|
|
6020
7954
|
if (!b["id"])
|
|
6021
7955
|
return;
|
|
@@ -6039,7 +7973,9 @@ GROUP BY ?id ?mentionCount`;
|
|
|
6039
7973
|
* query and merged into `entityInfoMap` reactively once it arrives.
|
|
6040
7974
|
*/
|
|
6041
7975
|
async requestEntityLocations(uuids) {
|
|
6042
|
-
const newUUIDs = uuids.filter(
|
|
7976
|
+
const newUUIDs = uuids.filter(
|
|
7977
|
+
(id) => this._entityOSMMap.get()[id] === void 0
|
|
7978
|
+
);
|
|
6043
7979
|
if (newUUIDs.length === 0)
|
|
6044
7980
|
return;
|
|
6045
7981
|
const osmInit = { ...this._entityOSMMap.get() };
|
|
@@ -6075,7 +8011,11 @@ WHERE {
|
|
|
6075
8011
|
}
|
|
6076
8012
|
BIND(REPLACE(STR(?iri), "^.*/([^/]*)$", "$1") AS ?id)
|
|
6077
8013
|
}`;
|
|
6078
|
-
const data = await this._api.sparql(
|
|
8014
|
+
const data = await this._api.sparql(
|
|
8015
|
+
q,
|
|
8016
|
+
this._projectId,
|
|
8017
|
+
this._graphType
|
|
8018
|
+
);
|
|
6079
8019
|
const update = { ...this._entityOSMMap.get() };
|
|
6080
8020
|
data.results.bindings.forEach((b) => {
|
|
6081
8021
|
if (!b["id"] || !b["osm"])
|
|
@@ -6114,7 +8054,10 @@ WHERE {
|
|
|
6114
8054
|
this._fetchIncomingRelationships(iri)
|
|
6115
8055
|
]);
|
|
6116
8056
|
const result = { outgoing, incoming };
|
|
6117
|
-
this._entityRelationships.set({
|
|
8057
|
+
this._entityRelationships.set({
|
|
8058
|
+
...this._entityRelationships.get(),
|
|
8059
|
+
[uuid]: result
|
|
8060
|
+
});
|
|
6118
8061
|
return result;
|
|
6119
8062
|
}
|
|
6120
8063
|
/**
|
|
@@ -6138,11 +8081,111 @@ WHERE {
|
|
|
6138
8081
|
qcy:about ?iri .
|
|
6139
8082
|
BIND(REPLACE(STR(?doc), "^.*/([^/]*)$", "$1") AS ?id)
|
|
6140
8083
|
}`;
|
|
6141
|
-
const data = await this._api.sparql(
|
|
8084
|
+
const data = await this._api.sparql(
|
|
8085
|
+
q,
|
|
8086
|
+
this._projectId,
|
|
8087
|
+
this._graphType
|
|
8088
|
+
);
|
|
6142
8089
|
const ids = data.results.bindings.filter((b) => b["id"] !== void 0).map((b) => b["id"].value);
|
|
6143
8090
|
this._entityDocuments.set({ ...this._entityDocuments.get(), [uuid]: ids });
|
|
6144
8091
|
return ids;
|
|
6145
8092
|
}
|
|
8093
|
+
/**
|
|
8094
|
+
* Fetches all `qcy:EntityCategory` IRIs and their preferred labels for this
|
|
8095
|
+
* project. Uses `api.language` (default `'en'`);
|
|
8096
|
+
* falls back to an untagged label when no match is found.
|
|
8097
|
+
*/
|
|
8098
|
+
async contentCategoriesInProject(orderByOccurences = true) {
|
|
8099
|
+
const language = this._api.language;
|
|
8100
|
+
const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
|
|
8101
|
+
PREFIX skos: <${prefixCC["skos"]}>
|
|
8102
|
+
SELECT ?iri (SAMPLE(?l) AS ?label) ?count
|
|
8103
|
+
WHERE {
|
|
8104
|
+
?iri a qcy:EntityCategory .
|
|
8105
|
+
OPTIONAL { ?iri skos:prefLabel ?lang_label FILTER(LANG(?lang_label) = "${language}") }
|
|
8106
|
+
OPTIONAL { ?iri skos:prefLabel ?no_lang_label }
|
|
8107
|
+
BIND(COALESCE(?lang_label, ?no_lang_label) AS ?l)
|
|
8108
|
+
${orderByOccurences ? "{ SELECT (COUNT(?x) AS ?count) ?iri where { ?x a qcy:CanonicalEntity ; qcy:hasEntityCategory ?iri } GROUP BY ?iri }" : ""}
|
|
8109
|
+
}
|
|
8110
|
+
GROUP BY ?iri ?count
|
|
8111
|
+
ORDER BY ${orderByOccurences ? "DESC(?count)" : "ASC(?label)"}`;
|
|
8112
|
+
const data = await this._api.sparql(
|
|
8113
|
+
q,
|
|
8114
|
+
this._projectId,
|
|
8115
|
+
this._graphType
|
|
8116
|
+
);
|
|
8117
|
+
return data.results.bindings.filter((b) => b["iri"] !== void 0).map((b) => {
|
|
8118
|
+
const count = b["count"] ? parseInt(b["count"].value, 10) : 0;
|
|
8119
|
+
const label = b["label"]?.value ?? b["iri"].value.split("#").at(-1);
|
|
8120
|
+
const labelWithCount = count > 0 ? `${label} (${count})` : label ?? "";
|
|
8121
|
+
return {
|
|
8122
|
+
iri: b["iri"].value,
|
|
8123
|
+
label: labelWithCount
|
|
8124
|
+
};
|
|
8125
|
+
});
|
|
8126
|
+
}
|
|
8127
|
+
async buildSummaryGraph(format) {
|
|
8128
|
+
const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
|
|
8129
|
+
SELECT
|
|
8130
|
+
?sourceCat
|
|
8131
|
+
?predicate
|
|
8132
|
+
?targetCat
|
|
8133
|
+
(COUNT(*) AS ?weight)
|
|
8134
|
+
WHERE {
|
|
8135
|
+
?s a qcy:CanonicalEntity ;
|
|
8136
|
+
qcy:hasEntityCategory ?sourceCat .
|
|
8137
|
+
?o a qcy:CanonicalEntity ;
|
|
8138
|
+
qcy:hasEntityCategory ?targetCat .
|
|
8139
|
+
?s ?predicate ?o .
|
|
8140
|
+
FILTER(isIRI(?s) && isIRI(?o))
|
|
8141
|
+
FILTER(?predicate != qcy:relatedEntity)
|
|
8142
|
+
}
|
|
8143
|
+
GROUP BY ?sourceCat ?predicate ?targetCat
|
|
8144
|
+
ORDER BY DESC(?weight)`;
|
|
8145
|
+
const data = await this._api.sparql(
|
|
8146
|
+
q,
|
|
8147
|
+
this._projectId,
|
|
8148
|
+
this._graphType
|
|
8149
|
+
);
|
|
8150
|
+
const bindings = data.results.bindings.filter(
|
|
8151
|
+
(b) => b["sourceCat"] && b["predicate"] && b["targetCat"]
|
|
8152
|
+
);
|
|
8153
|
+
if (format === "graph") {
|
|
8154
|
+
const irisSet = /* @__PURE__ */ new Set();
|
|
8155
|
+
const relations = [];
|
|
8156
|
+
for (const b of bindings) {
|
|
8157
|
+
const sourceID = b["sourceCat"].value;
|
|
8158
|
+
const predicate = b["predicate"].value;
|
|
8159
|
+
const targetID = b["targetCat"].value;
|
|
8160
|
+
const weight = parseInt(b["weight"].value, 10);
|
|
8161
|
+
irisSet.add(sourceID);
|
|
8162
|
+
irisSet.add(targetID);
|
|
8163
|
+
relations.push({ sourceID, predicate, targetID, weight });
|
|
8164
|
+
}
|
|
8165
|
+
return {
|
|
8166
|
+
entities: Array.from(irisSet).map((iri) => ({ iri })),
|
|
8167
|
+
relations
|
|
8168
|
+
};
|
|
8169
|
+
}
|
|
8170
|
+
if (format === "md") {
|
|
8171
|
+
const ce = CompactExpand.getInstance();
|
|
8172
|
+
const rows = bindings.map((b) => ({
|
|
8173
|
+
src: ce.compactIRI(b["sourceCat"].value),
|
|
8174
|
+
pred: ce.compactIRI(b["predicate"].value),
|
|
8175
|
+
tgt: ce.compactIRI(b["targetCat"].value),
|
|
8176
|
+
weight: parseInt(b["weight"].value, 10)
|
|
8177
|
+
}));
|
|
8178
|
+
if (rows.length === 0)
|
|
8179
|
+
return "(no results)";
|
|
8180
|
+
const maxSrc = Math.max(...rows.map((r) => r.src.length));
|
|
8181
|
+
const maxPred = Math.max(...rows.map((r) => r.pred.length));
|
|
8182
|
+
const maxTgt = Math.max(...rows.map((r) => r.tgt.length));
|
|
8183
|
+
return rows.map(
|
|
8184
|
+
(r) => `${r.src.padEnd(maxSrc)} -> ${r.pred.padEnd(maxPred)} -> ${r.tgt.padEnd(maxTgt)} (${r.weight})`
|
|
8185
|
+
).join("\n");
|
|
8186
|
+
}
|
|
8187
|
+
return data;
|
|
8188
|
+
}
|
|
6146
8189
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
6147
8190
|
_computeEntityInfoMap() {
|
|
6148
8191
|
const details = this._entityDetails.get();
|
|
@@ -6216,7 +8259,11 @@ WHERE {
|
|
|
6216
8259
|
FILTER(?rel != qcy:relatedEntity)
|
|
6217
8260
|
}
|
|
6218
8261
|
GROUP BY ?rel ?related`;
|
|
6219
|
-
const data = await this._api.sparql(
|
|
8262
|
+
const data = await this._api.sparql(
|
|
8263
|
+
q,
|
|
8264
|
+
this._projectId,
|
|
8265
|
+
this._graphType
|
|
8266
|
+
);
|
|
6220
8267
|
const result = [];
|
|
6221
8268
|
const detailsUpdate = { ...this._entityDetails.get() };
|
|
6222
8269
|
data.results.bindings.forEach((b) => {
|
|
@@ -6250,7 +8297,11 @@ WHERE {
|
|
|
6250
8297
|
FILTER(?rel != qcy:relatedEntity)
|
|
6251
8298
|
}
|
|
6252
8299
|
GROUP BY ?rel ?relating`;
|
|
6253
|
-
const data = await this._api.sparql(
|
|
8300
|
+
const data = await this._api.sparql(
|
|
8301
|
+
q,
|
|
8302
|
+
this._projectId,
|
|
8303
|
+
this._graphType
|
|
8304
|
+
);
|
|
6254
8305
|
const result = [];
|
|
6255
8306
|
const detailsUpdate = { ...this._entityDetails.get() };
|
|
6256
8307
|
data.results.bindings.forEach((b) => {
|
|
@@ -6285,7 +8336,11 @@ GROUP BY ?e1Cat ?e2Cat`;
|
|
|
6285
8336
|
await staleWhileRevalidate(
|
|
6286
8337
|
q,
|
|
6287
8338
|
async () => {
|
|
6288
|
-
const data = await this._api.sparql(
|
|
8339
|
+
const data = await this._api.sparql(
|
|
8340
|
+
q,
|
|
8341
|
+
this._projectId,
|
|
8342
|
+
this._graphType
|
|
8343
|
+
);
|
|
6289
8344
|
const entities = [];
|
|
6290
8345
|
const relations = [];
|
|
6291
8346
|
data.results.bindings.forEach((b) => {
|
|
@@ -6342,7 +8397,11 @@ SELECT * WHERE {
|
|
|
6342
8397
|
?s geo:hasGeometry/geo:asWKT ?wkt
|
|
6343
8398
|
}
|
|
6344
8399
|
}`;
|
|
6345
|
-
const data = await this._api.sparql(
|
|
8400
|
+
const data = await this._api.sparql(
|
|
8401
|
+
q,
|
|
8402
|
+
this._projectId,
|
|
8403
|
+
this._graphType
|
|
8404
|
+
);
|
|
6346
8405
|
const update = { ...this._osmWKTMap.get() };
|
|
6347
8406
|
data.results.bindings.forEach((b) => {
|
|
6348
8407
|
if (!b["s"] || !b["wkt"])
|
|
@@ -6362,13 +8421,14 @@ var CueProjectDocuments = class {
|
|
|
6362
8421
|
this._queryCache = _queryCache;
|
|
6363
8422
|
this._graphType = _graphType;
|
|
6364
8423
|
this.baseURL = `${rdfBase}${_projectId}/`;
|
|
6365
|
-
this.
|
|
8424
|
+
this._currentLang = language ?? this._api.language;
|
|
6366
8425
|
this.documentInfoMap = this._documentInfoMap.asReadonly();
|
|
6367
8426
|
this.projectDocumentsData = this._projectDocumentsData.asReadonly();
|
|
6368
8427
|
}
|
|
6369
8428
|
/** Full RDF base URL for this project, e.g. `https://cue.qaecy.com/r/{pid}/` */
|
|
6370
8429
|
baseURL;
|
|
6371
|
-
|
|
8430
|
+
/** Tracks the language for which `_documentInfoMap` is currently populated. */
|
|
8431
|
+
_currentLang;
|
|
6372
8432
|
_documentInfoMap = new CueSignal({});
|
|
6373
8433
|
_projectDocumentsData = new CueSignal({
|
|
6374
8434
|
duplicateCount: 0,
|
|
@@ -6398,9 +8458,10 @@ var CueProjectDocuments = class {
|
|
|
6398
8458
|
* `requestDocumentData()` call.
|
|
6399
8459
|
*/
|
|
6400
8460
|
setLanguage(lang) {
|
|
6401
|
-
if (this.
|
|
8461
|
+
if (this._currentLang === lang)
|
|
6402
8462
|
return;
|
|
6403
|
-
this.
|
|
8463
|
+
this._currentLang = lang;
|
|
8464
|
+
this._api.setLanguage(lang);
|
|
6404
8465
|
this._documentInfoMap.set({});
|
|
6405
8466
|
}
|
|
6406
8467
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
@@ -6442,7 +8503,7 @@ var CueProjectDocuments = class {
|
|
|
6442
8503
|
if (newUUIDs.length === 0)
|
|
6443
8504
|
return;
|
|
6444
8505
|
const values = newUUIDs.map((id) => `r:${id}`).join(" ");
|
|
6445
|
-
const lang = this.
|
|
8506
|
+
const lang = this._api.language;
|
|
6446
8507
|
const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
|
|
6447
8508
|
PREFIX r: <${this.baseURL}>
|
|
6448
8509
|
SELECT ?id ?contentIRI ?suffix ?size ?subject ?summary
|
|
@@ -6495,6 +8556,21 @@ GROUP BY ?id ?contentIRI ?suffix ?size ?subject ?summary`;
|
|
|
6495
8556
|
(err) => console.error("[CueProjectDocuments] requestDocumentData failed:", err)
|
|
6496
8557
|
);
|
|
6497
8558
|
}
|
|
8559
|
+
/**
|
|
8560
|
+
* Returns a single arbitrary file path from the project's triplestore.
|
|
8561
|
+
* Useful for pre-filling path-based query inputs with a realistic example.
|
|
8562
|
+
*/
|
|
8563
|
+
async randomFilePath() {
|
|
8564
|
+
const q = `PREFIX qcy: <${qaecyPrefixes["qcy"]}>
|
|
8565
|
+
SELECT ?path
|
|
8566
|
+
WHERE {
|
|
8567
|
+
?fl a qcy:FileLocation ;
|
|
8568
|
+
qcy:filePath ?path .
|
|
8569
|
+
}
|
|
8570
|
+
LIMIT 1`;
|
|
8571
|
+
const data = await this._api.sparql(q, this._projectId, this._graphType);
|
|
8572
|
+
return data.results.bindings[0]?.["path"]?.value ?? null;
|
|
8573
|
+
}
|
|
6498
8574
|
// ── Private helpers ────────────────────────────────────────────────────────
|
|
6499
8575
|
async _fetchDocumentsBySuffix() {
|
|
6500
8576
|
return this._runDocumentsBySuffixQuery(this._buildDocumentsBySuffixQuery());
|
|
@@ -6597,6 +8673,7 @@ var CueProjectView = class {
|
|
|
6597
8673
|
this.availableContentCategories = this.schema.availableContentCategories;
|
|
6598
8674
|
this.availableEntityCategories = this.schema.availableEntityCategories;
|
|
6599
8675
|
this.availableEntityRelationships = this.schema.availableEntityRelationships;
|
|
8676
|
+
this.schemaReady = this.schema.ready;
|
|
6600
8677
|
this.entityInfoMap = this.entities.entityInfoMap;
|
|
6601
8678
|
this.entityGraph = this.entities.entityGraph;
|
|
6602
8679
|
this.documentInfoMap = this.documents.documentInfoMap;
|
|
@@ -6617,6 +8694,11 @@ var CueProjectView = class {
|
|
|
6617
8694
|
availableEntityCategories;
|
|
6618
8695
|
/** Available entity relationship types. Auto-fetched on init. */
|
|
6619
8696
|
availableEntityRelationships;
|
|
8697
|
+
/**
|
|
8698
|
+
* Resolves when the initial schema load has completed. Await before reading
|
|
8699
|
+
* schema signal values imperatively.
|
|
8700
|
+
*/
|
|
8701
|
+
schemaReady;
|
|
6620
8702
|
/** Merged per-entity detail map. Populated lazily via `requestEntityData()` etc. */
|
|
6621
8703
|
entityInfoMap;
|
|
6622
8704
|
/** Project-level entity co-occurrence graph. Fetched once on init. */
|
|
@@ -7724,7 +9806,10 @@ function uploadedFileMetadata(originalName, projectId, userId, md5, providerId,
|
|
|
7724
9806
|
|
|
7725
9807
|
// libs/js/cue-sdk/src/lib/sync.ts
|
|
7726
9808
|
async function _fs2() {
|
|
7727
|
-
return import(
|
|
9809
|
+
return import(
|
|
9810
|
+
/* webpackIgnore: true */
|
|
9811
|
+
"fs/promises"
|
|
9812
|
+
);
|
|
7728
9813
|
}
|
|
7729
9814
|
async function _readNodeFile(fullPath) {
|
|
7730
9815
|
if (typeof window !== "undefined") {
|
|
@@ -7759,11 +9844,17 @@ async function _initWasm() {
|
|
|
7759
9844
|
_scanFn = mod.scan;
|
|
7760
9845
|
} else {
|
|
7761
9846
|
const { readFile } = await _fs2();
|
|
7762
|
-
const { join: join4 } = await import(
|
|
7763
|
-
|
|
9847
|
+
const { join: join4 } = await import(
|
|
9848
|
+
/* webpackIgnore: true */
|
|
9849
|
+
"path"
|
|
9850
|
+
);
|
|
9851
|
+
const { pathToFileURL } = await import(
|
|
9852
|
+
/* webpackIgnore: true */
|
|
9853
|
+
"url"
|
|
9854
|
+
);
|
|
7764
9855
|
const wasmDir = join4(__dirname, "assets", "wasm");
|
|
7765
9856
|
const wasmBinary = await readFile(join4(wasmDir, "dir_scanner_wasm_bg.wasm"));
|
|
7766
|
-
const glueUrl = pathToFileURL(join4(wasmDir, "dir_scanner_wasm.
|
|
9857
|
+
const glueUrl = pathToFileURL(join4(wasmDir, "dir_scanner_wasm.js")).href;
|
|
7767
9858
|
const mod = await import(
|
|
7768
9859
|
/* @vite-ignore */
|
|
7769
9860
|
glueUrl
|
|
@@ -7772,12 +9863,18 @@ async function _initWasm() {
|
|
|
7772
9863
|
_scanFn = mod.scan;
|
|
7773
9864
|
}
|
|
7774
9865
|
}
|
|
7775
|
-
var DEFAULT_GRAPH_TYPE = "
|
|
9866
|
+
var DEFAULT_GRAPH_TYPE = "qlever";
|
|
7776
9867
|
var FSS_BATCH_CHUNK_SIZE = 1e3;
|
|
7777
9868
|
var PENDING_LS_PREFIX = "cue:pending:";
|
|
7778
9869
|
async function _pendingFilePath(spaceId) {
|
|
7779
|
-
const { tmpdir } = await import(
|
|
7780
|
-
|
|
9870
|
+
const { tmpdir } = await import(
|
|
9871
|
+
/* webpackIgnore: true */
|
|
9872
|
+
"os"
|
|
9873
|
+
);
|
|
9874
|
+
const { join: join4 } = await import(
|
|
9875
|
+
/* webpackIgnore: true */
|
|
9876
|
+
"path"
|
|
9877
|
+
);
|
|
7781
9878
|
return join4(tmpdir(), `cue-sync-pending-${spaceId}.json`);
|
|
7782
9879
|
}
|
|
7783
9880
|
async function _loadPending(spaceId) {
|
|
@@ -8412,6 +10509,27 @@ var Cue = class _Cue {
|
|
|
8412
10509
|
_app;
|
|
8413
10510
|
_endpoints;
|
|
8414
10511
|
_isEmulator;
|
|
10512
|
+
_gis = null;
|
|
10513
|
+
/**
|
|
10514
|
+
* Reactive GIS service. Lazily constructed on first access.
|
|
10515
|
+
*
|
|
10516
|
+
* @example
|
|
10517
|
+
* ```ts
|
|
10518
|
+
* cue.gis.setProjectId('my-project');
|
|
10519
|
+
* cue.gis.onAvailableCategories(cats => ...);
|
|
10520
|
+
* cue.gis.setBbox([west, south, east, north]);
|
|
10521
|
+
* cue.gis.setSelectedCategories(new Set(['cadastre', 'building']));
|
|
10522
|
+
* ```
|
|
10523
|
+
*/
|
|
10524
|
+
get gis() {
|
|
10525
|
+
if (!this._gis) {
|
|
10526
|
+
this._gis = new CueGis(
|
|
10527
|
+
() => this.api.getAuthHeaders(),
|
|
10528
|
+
this._endpoints.gatewayUrl
|
|
10529
|
+
);
|
|
10530
|
+
}
|
|
10531
|
+
return this._gis;
|
|
10532
|
+
}
|
|
8415
10533
|
constructor(config = {}) {
|
|
8416
10534
|
const usingDefaults = !config.apiKey && !config.appId && !config.measurementId;
|
|
8417
10535
|
if (usingDefaults) {
|
|
@@ -9398,10 +11516,17 @@ async function syncHandler(options) {
|
|
|
9398
11516
|
process.exit(0);
|
|
9399
11517
|
} catch (err) {
|
|
9400
11518
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11519
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
11520
|
+
const causeCode = cause instanceof Error ? cause.code : void 0;
|
|
11521
|
+
const causeMsg = cause instanceof Error ? cause.message : void 0;
|
|
11522
|
+
const isTimeout = causeCode === "UND_ERR_HEADERS_TIMEOUT" || causeCode === "UND_ERR_BODY_TIMEOUT" || causeCode === "UND_ERR_CONNECT_TIMEOUT" || causeMsg?.includes("Timeout") || msg.includes("fetch failed");
|
|
9401
11523
|
if (msg.includes("GRAPH_UNAVAILABLE")) {
|
|
9402
11524
|
console.error("The knowledge graph for this project has no database configured. Contact your administrator or check the project setup.");
|
|
9403
|
-
} else if (msg.includes("GRAPH_TIMEOUT")) {
|
|
9404
|
-
console.error("Could not reach the
|
|
11525
|
+
} else if (msg.includes("GRAPH_TIMEOUT") || isTimeout) {
|
|
11526
|
+
console.error("Could not reach the QAECY API \u2014 the request timed out or the server could not be reached.");
|
|
11527
|
+
console.error("Check your internet connection and try again. If the problem persists, the service may be temporarily unavailable.");
|
|
11528
|
+
if (verbose && causeCode)
|
|
11529
|
+
console.error(` Cause: ${causeMsg ?? causeCode}`);
|
|
9405
11530
|
} else if (msg.includes("METADATA_SYNC_FAILED")) {
|
|
9406
11531
|
console.error("Metadata sync failed. Try again.");
|
|
9407
11532
|
} else if (msg.includes("LEDGER_WRITE_FAILED") || msg.includes("File structure batch POST failed")) {
|
|
@@ -9409,7 +11534,13 @@ async function syncHandler(options) {
|
|
|
9409
11534
|
"Failed communication with the knowledge graph. The files are up to date but metadata is not. Run the tool again to sync metadata. This is a swift job."
|
|
9410
11535
|
);
|
|
9411
11536
|
} else {
|
|
9412
|
-
console.error("
|
|
11537
|
+
console.error("An unexpected error occurred during sync.");
|
|
11538
|
+
if (verbose) {
|
|
11539
|
+
console.error("Details:", err);
|
|
11540
|
+
} else {
|
|
11541
|
+
console.error(` ${msg}`);
|
|
11542
|
+
console.error("Re-run with --verbose for more details.");
|
|
11543
|
+
}
|
|
9413
11544
|
}
|
|
9414
11545
|
process.exit(1);
|
|
9415
11546
|
}
|
|
@@ -9872,6 +12003,97 @@ Project created \u2705`);
|
|
|
9872
12003
|
}
|
|
9873
12004
|
}
|
|
9874
12005
|
|
|
12006
|
+
// apps/desktop/cue-cli/src/cue-cli-app-builder-tools.ts
|
|
12007
|
+
var import_commander = require("commander");
|
|
12008
|
+
async function buildCueClient(options) {
|
|
12009
|
+
const key = options.key ?? process.env["CUE_API_KEY"];
|
|
12010
|
+
if (!key) {
|
|
12011
|
+
console.error("API key is required. Provide it via --key or CUE_API_KEY env variable.");
|
|
12012
|
+
process.exit(1);
|
|
12013
|
+
}
|
|
12014
|
+
const cue = new CueNode({
|
|
12015
|
+
apiKey: FIREBASE_CONFIG().apiKey,
|
|
12016
|
+
appId: FIREBASE_CONFIG().appId,
|
|
12017
|
+
measurementId: FIREBASE_CONFIG().measurementId,
|
|
12018
|
+
environment: options.emulators ? "emulator" : "production",
|
|
12019
|
+
...options.emulators ? { endpoints: getEmulatorEndpoints() } : {}
|
|
12020
|
+
});
|
|
12021
|
+
await cue.auth.signInWithApiKey(key);
|
|
12022
|
+
return cue;
|
|
12023
|
+
}
|
|
12024
|
+
async function listProjectsHandler(options) {
|
|
12025
|
+
try {
|
|
12026
|
+
const cue = await buildCueClient(options);
|
|
12027
|
+
if (options.verbose)
|
|
12028
|
+
console.error("Fetching projects...");
|
|
12029
|
+
const projects = await cue.api.projects.listProjects();
|
|
12030
|
+
const output = projects.map((p) => ({
|
|
12031
|
+
id: p.id,
|
|
12032
|
+
name: p.name,
|
|
12033
|
+
organizationID: p.organizationID,
|
|
12034
|
+
isPublic: p.isPublic,
|
|
12035
|
+
graphType: p.projectSettings?.graph?.type ?? "qlever",
|
|
12036
|
+
lastSync: p.lastSync
|
|
12037
|
+
}));
|
|
12038
|
+
console.log(JSON.stringify(output, null, 2));
|
|
12039
|
+
} catch (err) {
|
|
12040
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
12041
|
+
process.exit(1);
|
|
12042
|
+
}
|
|
12043
|
+
}
|
|
12044
|
+
async function entitySummaryGraphHandler(options) {
|
|
12045
|
+
try {
|
|
12046
|
+
const cue = await buildCueClient(options);
|
|
12047
|
+
if (options.verbose)
|
|
12048
|
+
console.error(`Fetching entity summary graph for project ${options.space}...`);
|
|
12049
|
+
const entities = new CueProjectEntities(cue.api, options.space);
|
|
12050
|
+
if (options.format === "graph") {
|
|
12051
|
+
const graph = await entities.buildSummaryGraph("graph");
|
|
12052
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
12053
|
+
} else if (options.format === "md") {
|
|
12054
|
+
const md = await entities.buildSummaryGraph("md");
|
|
12055
|
+
console.log(md);
|
|
12056
|
+
} else {
|
|
12057
|
+
const raw = await entities.buildSummaryGraph();
|
|
12058
|
+
console.log(JSON.stringify(raw, null, 2));
|
|
12059
|
+
}
|
|
12060
|
+
} catch (err) {
|
|
12061
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
12062
|
+
process.exit(1);
|
|
12063
|
+
}
|
|
12064
|
+
}
|
|
12065
|
+
async function sparqlHandler(options) {
|
|
12066
|
+
try {
|
|
12067
|
+
let sparqlQuery = options.query;
|
|
12068
|
+
if (!sparqlQuery) {
|
|
12069
|
+
if (process.stdin.isTTY) {
|
|
12070
|
+
console.error("Provide a SPARQL query via --query or pipe it via stdin.");
|
|
12071
|
+
process.exit(1);
|
|
12072
|
+
}
|
|
12073
|
+
const chunks = [];
|
|
12074
|
+
for await (const chunk of process.stdin)
|
|
12075
|
+
chunks.push(chunk);
|
|
12076
|
+
sparqlQuery = Buffer.concat(chunks).toString("utf8").trim();
|
|
12077
|
+
}
|
|
12078
|
+
if (!sparqlQuery) {
|
|
12079
|
+
console.error("SPARQL query is empty.");
|
|
12080
|
+
process.exit(1);
|
|
12081
|
+
}
|
|
12082
|
+
const cue = await buildCueClient(options);
|
|
12083
|
+
if (options.verbose)
|
|
12084
|
+
console.error(`Executing SPARQL query against project ${options.space}...`);
|
|
12085
|
+
const result = await cue.api.sparql(sparqlQuery, options.space);
|
|
12086
|
+
console.log(JSON.stringify(result, null, 2));
|
|
12087
|
+
} catch (err) {
|
|
12088
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
12089
|
+
process.exit(1);
|
|
12090
|
+
}
|
|
12091
|
+
}
|
|
12092
|
+
var appBuilderToolsCommand = new import_commander.Command("app-builder-tools").description("Tools for agent-assisted Cue app development. Outputs JSON for machine consumption.");
|
|
12093
|
+
appBuilderToolsCommand.command("list-projects").description("List all projects accessible to the authenticated user").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(listProjectsHandler);
|
|
12094
|
+
appBuilderToolsCommand.command("entity-summary-graph").description("Fetch the entity category relationship summary graph for a project").requiredOption("-s, --space <id>", "Project ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-f, --format <format>", "Output format: md (aligned text), graph (JSON nodes+edges), json (raw SPARQL)", "md").option("-v, --verbose", "Enable verbose output", false).action(entitySummaryGraphHandler);
|
|
12095
|
+
appBuilderToolsCommand.command("sparql").description("Execute a SPARQL SELECT query against a project triplestore. Query via --query or stdin.").requiredOption("-s, --space <id>", "Project ID (required)").option("-q, --query <sparql>", "SPARQL query string (or pipe query via stdin)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Use emulators", false).option("-v, --verbose", "Enable verbose output", false).action(sparqlHandler);
|
|
12096
|
+
|
|
9875
12097
|
// apps/desktop/cue-cli/src/main.ts
|
|
9876
12098
|
var packageJson;
|
|
9877
12099
|
try {
|
|
@@ -9884,7 +12106,7 @@ try {
|
|
|
9884
12106
|
console.warn("Could not find package.json, using fallback version");
|
|
9885
12107
|
}
|
|
9886
12108
|
}
|
|
9887
|
-
var program = new
|
|
12109
|
+
var program = new import_commander2.Command();
|
|
9888
12110
|
program.name("cue-cli").description("Cue Command Line Interface").version(packageJson.version);
|
|
9889
12111
|
program.command("sync").description("Sync files to Cue").option("-s, --space <id>", "Specify the space ID (omit to pick interactively)").requiredOption("-p, --path <id>", "Specify the folder path (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("--provider <provider ID>", "Specify the provider ID (eg. sharepoint, drive, dropbox) or leave empty for default provider", "").option("-v, --verbose", "Enable verbose output", false).option("-e, --emulators", "Uses emulators for sync", false).option("-z, --zip", 'Include zipped content (will be unzipped to path "<zip_path>_unzipped". Max uncompressed size: 500 MB, max recursion depth: 3)', false).option("--legacy", "Write RDF as BLOBs to the processed bucket instead of patching the graph directly", false).option("--metadata-only", "Push filesystem-structure metadata for all local files without checking credits or remote state", false).action(syncHandler);
|
|
9890
12112
|
program.command("dump").description("Dump Cue Knowledge Graph data to file\n Examples:\n $ cue-cli dump -s <space_id> -l -v\n $ cue-cli dump -s <space_id> -j -v").requiredOption("-s, --space <id>", "Specify the space ID (required)").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-v, --verbose", "Enable verbose output", false).option("-e, --emulators", "Uses emulators for sync", false).option("-q, --query", "Uses a construct query to get the dump rather than using the /data endpoint", false).option("-j, --jelly", "Downloads a Jelly file rather than the standard Gzipped NQuads format", false).option("-l, --load", "Loads the dumped file into a local triplestore (requires emulators)", false).action(dumpHandler);
|
|
@@ -9894,4 +12116,5 @@ program.command("repair-ttl").description("Repair TTL files in the specified spa
|
|
|
9894
12116
|
program.command("util-rdf-compare").description("Compare two Turtle (.ttl) files and report semantic differences").requiredOption("--file1 <path>", "Path to the first TTL file").requiredOption("--file2 <path>", "Path to the second TTL file").option("-v, --verbose", "Enable verbose output", false).action(utilRdfCompareHandler);
|
|
9895
12117
|
program.command("util-remove-rdf-star").description("Remove RDF-star (quoted) triples from an NQuads file. Supports .nq and .nq.gz input/output.").requiredOption("-i, --input <path>", "Input file path (.nq or .nq.gz)").requiredOption("-o, --output <path>", "Output file path (.nq or .nq.gz)").option("-v, --verbose", "Enable verbose output", false).action(utilRemoveRdfStarHandler);
|
|
9896
12118
|
program.command("create-project").description("Interactively create a new project under an organisation").option("-k, --key <api-key>", "Specify the API key (or set CUE_API_KEY env variable)").option("-e, --emulators", "Uses emulators", false).option("-v, --verbose", "Enable verbose output", false).action(createProjectHandler);
|
|
12119
|
+
program.addCommand(appBuilderToolsCommand);
|
|
9897
12120
|
program.parse(process.argv);
|