@qaecy/cue-cli 0.0.45 → 0.0.46

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.
Files changed (2) hide show
  1. package/main.js +1819 -16
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -5090,6 +5090,8 @@ var BUCKET_PERSISTENCE2 = "db_persistence_eu_west6";
5090
5090
  var ENDPOINT_CONSUMPTION = "/data-views/admin/consumption";
5091
5091
  var ENDPOINT_PROFILE_ORGANIZATIONS = "/data-views/admin/profile/organizations";
5092
5092
  var ENDPOINT_PROFILE_API_KEYS = "/data-views/admin/profile/api-keys";
5093
+ var ENDPOINT_COMMANDS_PROFILE_API_KEYS = "/commands/admin/profile/api-keys";
5094
+ var ENDPOINT_COMMANDS_PROFILE_TERMS = "/commands/admin/profile/terms";
5093
5095
  var ENDPOINT_ORG_MEMBERS = (orgId) => `/data-views/admin/organizations/${orgId}/members`;
5094
5096
  var ENDPOINT_CREATE_PROJECT = "/commands/admin/project";
5095
5097
  var ENDPOINT_SEARCH = "/assistant/search";
@@ -5221,6 +5223,28 @@ var CueAuth = class {
5221
5223
  async signOut() {
5222
5224
  await (0, import_auth3.signOut)(this._auth);
5223
5225
  }
5226
+ /**
5227
+ * Register a new user by name and email.
5228
+ * The backend validates that the email domain belongs to an existing organisation,
5229
+ * creates the Firebase Auth account, assigns org membership, and dispatches a
5230
+ * "set your password" email to the address provided.
5231
+ * Returns the new user's UID and the organisation name on success.
5232
+ */
5233
+ async signUp(name, email) {
5234
+ const response = await fetch(
5235
+ `${this._endpoints.gatewayUrl}/commands/admin/user/signup`,
5236
+ {
5237
+ method: "POST",
5238
+ headers: { "Content-Type": "application/json" },
5239
+ body: JSON.stringify({ name, email })
5240
+ }
5241
+ );
5242
+ if (!response.ok) {
5243
+ const body = await response.json().catch(() => ({}));
5244
+ throw new Error(body?.message ?? `Sign-up failed (${response.status})`);
5245
+ }
5246
+ return response.json();
5247
+ }
5224
5248
  /** Currently signed-in user, or null if not authenticated */
5225
5249
  get currentUser() {
5226
5250
  return this._auth.currentUser;
@@ -5316,9 +5340,12 @@ var CueApi = class {
5316
5340
  * The user must be authenticated before calling this.
5317
5341
  */
5318
5342
  async sparql(query3, projectId, graphType) {
5319
- console.log(graphType);
5320
- const endpoint = graphType === "qlever" ? ENDPOINT_QLEVER_QUERY : ENDPOINT_FUSEKI_QUERY;
5321
- console.log(`Executing SPARQL query against ${endpoint} for project ${projectId} with graph type ${graphType ?? "fuseki"}`);
5343
+ if (!graphType) {
5344
+ const project = await this.projects.getProject(projectId);
5345
+ graphType = project?.projectSettings?.graph?.type ?? "qlever";
5346
+ }
5347
+ const endpoint = graphType === "fuseki" ? ENDPOINT_FUSEKI_QUERY : ENDPOINT_QLEVER_QUERY;
5348
+ console.log(`Executing SPARQL query against ${endpoint} for project ${projectId} with graph type ${graphType}`);
5322
5349
  const urlencoded = new URLSearchParams();
5323
5350
  urlencoded.append("query", query3);
5324
5351
  const response = await this._auth.authenticatedFetch(
@@ -5361,6 +5388,1730 @@ var CueApi = class {
5361
5388
  }
5362
5389
  };
5363
5390
 
5391
+ // libs/js/cue-gis/src/lib/models.ts
5392
+ var FEATURE_CATEGORIES = [
5393
+ "building",
5394
+ "cadastre",
5395
+ "zone",
5396
+ "address",
5397
+ "poi",
5398
+ "greenspace",
5399
+ "paved",
5400
+ "railway",
5401
+ "natural",
5402
+ "manmade"
5403
+ ];
5404
+ var NOMINATIM_FEATURE_CATEGORIES = [
5405
+ "address",
5406
+ "poi",
5407
+ "railway",
5408
+ "natural",
5409
+ "manmade"
5410
+ ];
5411
+ var FEATURE_CATEGORY_DESCRIPTORS = {
5412
+ address: {
5413
+ category: "address",
5414
+ label: "Address",
5415
+ description: "Official addresses, streets, and addressable access points.",
5416
+ preferredColor: "#1d4ed8"
5417
+ },
5418
+ poi: {
5419
+ category: "poi",
5420
+ label: "Points of interest",
5421
+ description: "Places and amenities relevant to everyday operations and access.",
5422
+ preferredColor: "#059669"
5423
+ },
5424
+ railway: {
5425
+ category: "railway",
5426
+ label: "Railway",
5427
+ description: "Rail infrastructure, stations, and rail-adjacent transport context.",
5428
+ preferredColor: "#dc2626"
5429
+ },
5430
+ natural: {
5431
+ category: "natural",
5432
+ label: "Natural features",
5433
+ description: "Natural and hydrological features such as watercourses, forests, and land cover.",
5434
+ preferredColor: "#0ea5e9"
5435
+ },
5436
+ manmade: {
5437
+ category: "manmade",
5438
+ label: "Built features",
5439
+ description: "Structures and man-made elements not covered by more specific categories.",
5440
+ preferredColor: "#ca8a04"
5441
+ },
5442
+ cadastre: {
5443
+ category: "cadastre",
5444
+ label: "Cadastre",
5445
+ description: "Land parcels, legal boundaries, official surveying, and public-law land restrictions.",
5446
+ preferredColor: "#7c3aed"
5447
+ },
5448
+ building: {
5449
+ category: "building",
5450
+ label: "Buildings",
5451
+ description: "Building footprints and structures from official registry or cadastral data.",
5452
+ preferredColor: "#f97316"
5453
+ },
5454
+ greenspace: {
5455
+ category: "greenspace",
5456
+ label: "Green spaces",
5457
+ description: "Parks, gardens, playgrounds, sports facilities, and other vegetated open areas.",
5458
+ preferredColor: "#16a34a"
5459
+ },
5460
+ paved: {
5461
+ category: "paved",
5462
+ label: "Paved surfaces",
5463
+ description: "Courtyards, sidewalks, parking areas, and other sealed ground surfaces.",
5464
+ preferredColor: "#78716c"
5465
+ },
5466
+ zone: {
5467
+ category: "zone",
5468
+ label: "Planning zones",
5469
+ description: "Land-use zones, local plans, and public-law planning constraints (Nutzungsplanung / Lokalplan).",
5470
+ preferredColor: "#db2777"
5471
+ }
5472
+ };
5473
+ function featureCategoryDescriptor(category) {
5474
+ return FEATURE_CATEGORY_DESCRIPTORS[category];
5475
+ }
5476
+ var ZONE_PLAN_TYPE_COLORS = {
5477
+ "land-use-plan": "#f59e0b",
5478
+ // amber – primary zoning
5479
+ "local-plan": "#ec4899",
5480
+ // pink – Danish lokalplan
5481
+ "design-plan": "#3b82f6",
5482
+ // blue – Gestaltungsplan
5483
+ "development-plan": "#0891b2",
5484
+ // cyan – Bebauungsplan
5485
+ "special-use-plan": "#f97316",
5486
+ // orange – Sondernutzungsplan
5487
+ "open-space-zone": "#22c55e",
5488
+ // green – Freihaltezone
5489
+ "overlay-regulation": "#a855f7",
5490
+ // purple – overlaying regulations
5491
+ "unzoned": "#94a3b8",
5492
+ // slate-400 – nicht zonierte Fläche
5493
+ "municipal-plan-framework": "#6366f1",
5494
+ // indigo – Kommuneplanramme
5495
+ "neighbourhood-conservation-plan": "#14b8a6",
5496
+ // teal – Quartiererhaltungszonenplan
5497
+ "noise-sensitivity-plan": "#eab308",
5498
+ // yellow – Lärmempfindlichkeitsstufenplan
5499
+ "water-protection-plan": "#06b6d4",
5500
+ // sky – Gewässerschutzzonenplan
5501
+ "nature-landscape-plan": "#16a34a"
5502
+ // dark-green – Natur- und Landschaftsschutz
5503
+ };
5504
+ function bboxIntersects(a5, b) {
5505
+ return a5[0] < b[2] && a5[2] > b[0] && a5[1] < b[3] && a5[3] > b[1];
5506
+ }
5507
+
5508
+ // libs/js/cue-gis/src/lib/nominatim-adapter.ts
5509
+ var NOMINATIM_BASE_URL = "https://nominatim.openstreetmap.org";
5510
+ var DEFAULT_USER_AGENT = "cue-gis/0.0.1";
5511
+ var LAYER_PROBE_QUERY = {
5512
+ address: "street",
5513
+ poi: "shop",
5514
+ railway: "station",
5515
+ natural: "park",
5516
+ manmade: "building",
5517
+ cadastre: "parcel",
5518
+ building: "building",
5519
+ greenspace: "park",
5520
+ paved: "road",
5521
+ zone: "boundary"
5522
+ };
5523
+ var NOMINATIM_SOURCE_ID = "nominatim";
5524
+ function toLayerId(category) {
5525
+ return `${NOMINATIM_SOURCE_ID}:${category}`;
5526
+ }
5527
+ function toLayerDescriptor(category) {
5528
+ const descriptor = featureCategoryDescriptor(category);
5529
+ return {
5530
+ id: toLayerId(category),
5531
+ sourceId: NOMINATIM_SOURCE_ID,
5532
+ sourceLayerId: category,
5533
+ category,
5534
+ preferredColor: descriptor.preferredColor,
5535
+ tier: "raw",
5536
+ label: descriptor.label,
5537
+ description: `Nominatim ${category} layer`,
5538
+ labelKey: `gis.layer.${NOMINATIM_SOURCE_ID}.${category}.label`,
5539
+ descriptionKey: `gis.layer.${NOMINATIM_SOURCE_ID}.${category}.description`
5540
+ };
5541
+ }
5542
+ var NominatimAdapter = class {
5543
+ baseUrl;
5544
+ userAgent;
5545
+ email;
5546
+ limit;
5547
+ constructor(options = {}) {
5548
+ this.baseUrl = options.baseUrl ?? NOMINATIM_BASE_URL;
5549
+ this.userAgent = options.userAgent ?? DEFAULT_USER_AGENT;
5550
+ this.email = options.email;
5551
+ this.limit = Math.min(options.limit ?? 40, 40);
5552
+ }
5553
+ async listFeatureCategoryDescriptors(bbox) {
5554
+ const checks = await Promise.allSettled(
5555
+ NOMINATIM_FEATURE_CATEGORIES.map(async (category) => {
5556
+ const results = await this.search(bbox, category);
5557
+ return { category, hasResults: results.length > 0 };
5558
+ })
5559
+ );
5560
+ return checks.filter(
5561
+ (r) => r.status === "fulfilled" && r.value.hasResults
5562
+ ).map((r) => featureCategoryDescriptor(r.value.category));
5563
+ }
5564
+ async listAvailableLayers(bbox) {
5565
+ const categories = await this.listFeatureCategoryDescriptors(bbox);
5566
+ return categories.map((category) => toLayerDescriptor(category.category));
5567
+ }
5568
+ async getFeaturesForLayer(bbox, layerId) {
5569
+ const category = FEATURE_CATEGORIES.find((candidate) => toLayerId(candidate) === layerId);
5570
+ if (!category) {
5571
+ return [];
5572
+ }
5573
+ return this.search(bbox, category);
5574
+ }
5575
+ async listFeatureCategories(bbox) {
5576
+ const descriptors = await this.listFeatureCategoryDescriptors(bbox);
5577
+ return descriptors.map((descriptor) => descriptor.category);
5578
+ }
5579
+ async getFeaturesOfCategory(bbox, category) {
5580
+ return this.getFeaturesForLayer(bbox, toLayerId(category));
5581
+ }
5582
+ /**
5583
+ * Low-level search against the Nominatim API. Uses a per-layer probe query
5584
+ * so bounded viewbox searches return meaningful results.
5585
+ */
5586
+ async search(bbox, layer) {
5587
+ const [west, south, east, north] = bbox;
5588
+ const q = layer ? LAYER_PROBE_QUERY[layer] : "place";
5589
+ const params = new URLSearchParams({
5590
+ q,
5591
+ format: "jsonv2",
5592
+ viewbox: `${west},${north},${east},${south}`,
5593
+ bounded: "1",
5594
+ limit: String(this.limit),
5595
+ addressdetails: "1",
5596
+ extratags: "1",
5597
+ dedupe: "1"
5598
+ });
5599
+ if (layer) {
5600
+ params.set("layer", layer);
5601
+ }
5602
+ if (this.email) {
5603
+ params.set("email", this.email);
5604
+ }
5605
+ const url = `${this.baseUrl}/search?${params.toString()}`;
5606
+ const response = await fetch(url, {
5607
+ headers: {
5608
+ "User-Agent": this.userAgent,
5609
+ Accept: "application/json"
5610
+ }
5611
+ });
5612
+ if (!response.ok) {
5613
+ throw new Error(
5614
+ `Nominatim request failed: ${response.status} ${response.statusText}`
5615
+ );
5616
+ }
5617
+ const results = await response.json();
5618
+ return results.map((r) => this.toGisFeature(r, layer));
5619
+ }
5620
+ toGisFeature(r, category) {
5621
+ const [s, n, w, e] = r.boundingbox;
5622
+ const resolvedCategory = category ?? r.category;
5623
+ const descriptor = featureCategoryDescriptor(resolvedCategory);
5624
+ return {
5625
+ id: `${r.osm_type}/${r.osm_id}`,
5626
+ sourceId: NOMINATIM_SOURCE_ID,
5627
+ sourceFeatureId: `${r.osm_type}/${r.osm_id}`,
5628
+ layerId: toLayerId(resolvedCategory),
5629
+ name: r.name ?? r.display_name.split(",")[0].trim(),
5630
+ category: resolvedCategory,
5631
+ preferredColor: descriptor.preferredColor,
5632
+ type: r.type,
5633
+ lat: parseFloat(r.lat),
5634
+ lon: parseFloat(r.lon),
5635
+ displayName: r.display_name,
5636
+ bbox: [parseFloat(w), parseFloat(s), parseFloat(e), parseFloat(n)],
5637
+ tier: "raw",
5638
+ originalData: r
5639
+ };
5640
+ }
5641
+ };
5642
+
5643
+ // libs/js/cue-gis/src/lib/use-classifier.ts
5644
+ var DK_BBR_BUILDING = {
5645
+ // 100s – Residential
5646
+ "110": "residential",
5647
+ "120": "residential",
5648
+ "121": "residential",
5649
+ "122": "residential",
5650
+ "130": "residential",
5651
+ "131": "residential",
5652
+ "132": "residential",
5653
+ "140": "residential",
5654
+ "150": "residential",
5655
+ "160": "residential",
5656
+ "190": "residential",
5657
+ // 200s – Recreational / holiday
5658
+ "210": "recreational",
5659
+ "212": "recreational",
5660
+ "220": "recreational",
5661
+ "221": "recreational",
5662
+ "230": "recreational",
5663
+ "290": "residential",
5664
+ // 300s – Agricultural
5665
+ "310": "agricultural",
5666
+ "311": "agricultural",
5667
+ "312": "agricultural",
5668
+ "320": "agricultural",
5669
+ "321": "agricultural",
5670
+ "322": "agricultural",
5671
+ "330": "agricultural",
5672
+ "390": "agricultural",
5673
+ // 400s – Commercial
5674
+ "410": "commercial",
5675
+ "411": "commercial",
5676
+ "420": "commercial",
5677
+ "421": "commercial",
5678
+ "422": "commercial",
5679
+ "430": "commercial",
5680
+ "431": "commercial",
5681
+ "432": "commercial",
5682
+ "440": "commercial",
5683
+ "490": "commercial",
5684
+ // 500s – Industrial / infrastructure
5685
+ "510": "industrial",
5686
+ "511": "industrial",
5687
+ "519": "industrial",
5688
+ "520": "infrastructure",
5689
+ "521": "infrastructure",
5690
+ "522": "infrastructure",
5691
+ "529": "infrastructure",
5692
+ "530": "infrastructure",
5693
+ "531": "infrastructure",
5694
+ "532": "infrastructure",
5695
+ "533": "infrastructure",
5696
+ "534": "infrastructure",
5697
+ "535": "infrastructure",
5698
+ "540": "infrastructure",
5699
+ "585": "infrastructure",
5700
+ "590": "industrial",
5701
+ // 600s – Public / institutional
5702
+ "610": "public",
5703
+ "620": "public",
5704
+ "621": "public",
5705
+ "622": "public",
5706
+ "630": "public",
5707
+ "640": "public",
5708
+ "650": "recreational",
5709
+ "660": "public",
5710
+ "670": "public",
5711
+ "680": "public",
5712
+ "690": "public",
5713
+ // 900s – Accessory / utility
5714
+ "910": "other",
5715
+ "920": "other",
5716
+ "930": "other",
5717
+ "940": "other",
5718
+ "950": "infrastructure",
5719
+ // Teknikbygning (utility / technical building)
5720
+ "960": "agricultural",
5721
+ "970": "agricultural",
5722
+ "990": "other"
5723
+ };
5724
+ var CH_GWR_BUILDING = {
5725
+ "1010": "residential",
5726
+ "1020": "mixed",
5727
+ "1030": "residential",
5728
+ "1040": "other",
5729
+ "1060": "recreational",
5730
+ "1080": "residential",
5731
+ "1110": "commercial",
5732
+ "1120": "commercial",
5733
+ "1130": "commercial",
5734
+ "1140": "industrial",
5735
+ "1150": "infrastructure",
5736
+ "1160": "infrastructure",
5737
+ "1210": "public",
5738
+ "1220": "public",
5739
+ "1230": "public",
5740
+ "1240": "recreational",
5741
+ "1241": "public",
5742
+ "1242": "recreational",
5743
+ "1251": "public",
5744
+ "1261": "agricultural",
5745
+ "1262": "agricultural",
5746
+ "1263": "agricultural",
5747
+ "1275": "other",
5748
+ "1276": "other",
5749
+ "1277": "infrastructure",
5750
+ "1278": "infrastructure",
5751
+ "1281": "infrastructure",
5752
+ "1282": "infrastructure",
5753
+ "9999": "other"
5754
+ };
5755
+ var CH_GWR_GKLAS = {
5756
+ // Residential
5757
+ "1110": "residential",
5758
+ // Einfamilienhaus
5759
+ "1121": "recreational",
5760
+ // Ferienhaus / Sommerwohnsitz
5761
+ "1122": "residential",
5762
+ // Mehrfamilienhaus
5763
+ "1130": "mixed",
5764
+ // Wohn- und Geschäftshaus
5765
+ "1140": "mixed",
5766
+ // Wohn- und Landwirtschaftsgebäude
5767
+ // Hospitality
5768
+ "1211": "commercial",
5769
+ // Hotel, Motel
5770
+ "1212": "commercial",
5771
+ // Gaststätten, Restaurant
5772
+ "1213": "mixed",
5773
+ // Pension, Heim (care home / pension)
5774
+ // Office / commercial
5775
+ "1220": "commercial",
5776
+ // Büro- und Verwaltungsgebäude
5777
+ "1230": "commercial",
5778
+ // Handelsbau (retail)
5779
+ "1231": "commercial",
5780
+ // Grosshandel / Grossmarkt
5781
+ // Infrastructure / transport
5782
+ "1241": "infrastructure",
5783
+ // Bahnhof, Flughafen
5784
+ // Leisure
5785
+ "1242": "recreational",
5786
+ // Freizeitgebäude
5787
+ "1264": "recreational",
5788
+ // Sportgebäude
5789
+ // Industrial
5790
+ "1251": "industrial",
5791
+ // Fabrik, Werkstatt
5792
+ "1252": "industrial",
5793
+ // Lagerhalle, Silo
5794
+ // Public / institutional
5795
+ "1261": "public",
5796
+ // Schulen, Bildungsstätten
5797
+ "1262": "public",
5798
+ // Spitäler, Kliniken
5799
+ "1263": "public",
5800
+ // Universität, Forschungsstätte
5801
+ "1265": "public",
5802
+ // Polizei, Militär, Gefängnis
5803
+ "1272": "public",
5804
+ // Religiöse Gebäude und Kultbauten
5805
+ "1273": "public",
5806
+ // Öffentliche Verwaltungsgebäude
5807
+ // Agricultural
5808
+ "1271": "agricultural",
5809
+ // Landwirtschaftsgebäude
5810
+ // Other
5811
+ "1274": "other",
5812
+ // Anderes Gebäude
5813
+ "1281": "infrastructure",
5814
+ // Empfangsgebäude Bahn/Bus
5815
+ "1282": "infrastructure"
5816
+ // Parkhaus, Garage
5817
+ };
5818
+ var KEYWORD_RULES = [
5819
+ // "Gebäude" is the Swiss AV BoFlaeche land-cover label for a building footprint.
5820
+ // It does not encode the actual use type, so map it to 'other' as a safe fallback.
5821
+ [/^geb[äa]ude$/i, "other"],
5822
+ [/wohn|resident|housing|bolig|enfamil|etagebolig|rækkehus|lejlighed/i, "residential"],
5823
+ [/büro|office|handel|commercial|butik|forretning|kontor|supermarked|lager|hotel|restaurant/i, "commercial"],
5824
+ [/industri|fabrik|industrial|gewerbe|produktion|fabrik|fremstill/i, "industrial"],
5825
+ [/landwirtschaft|agrar|agricultural|landbrug|skov|dyrk|stald|lade|drivhus/i, "agricultural"],
5826
+ [/schule|school|hospital|gesundheit|sundhed|kirche|church|offentlig|uddannelse|kultur|social|plejehjem/i, "public"],
5827
+ [/freizeit|recreation|sport|sommerhus|ferienhaus|fritid|kolonihave/i, "recreational"],
5828
+ [/infrastruktur|verkehr|transport|energy|energie|forsyning|teknik|teknisk/i, "infrastructure"],
5829
+ [/gemischt|mixed|blandet/i, "mixed"]
5830
+ ];
5831
+ function classifyByText(text) {
5832
+ for (const [pattern, use] of KEYWORD_RULES) {
5833
+ if (pattern.test(text))
5834
+ return use;
5835
+ }
5836
+ return void 0;
5837
+ }
5838
+ function classifyBuildingUse(code, sourceId) {
5839
+ if (code == null)
5840
+ return void 0;
5841
+ const key = String(code).trim();
5842
+ if (sourceId === "swiss-gwr") {
5843
+ return CH_GWR_GKLAS[key] ?? classifyByText(key);
5844
+ }
5845
+ if (sourceId === "danish-matrikel") {
5846
+ return DK_BBR_BUILDING[key] ?? classifyByText(key);
5847
+ }
5848
+ if (sourceId === "zurich-wfs" || sourceId === "swiss-av-wfs") {
5849
+ return CH_GWR_BUILDING[key] ?? classifyByText(key);
5850
+ }
5851
+ return CH_GWR_BUILDING[key] ?? DK_BBR_BUILDING[key] ?? classifyByText(key);
5852
+ }
5853
+ function classifyPlotUse(use, _sourceId) {
5854
+ if (use == null)
5855
+ return void 0;
5856
+ return classifyByText(String(use).trim());
5857
+ }
5858
+ function normKey(raw) {
5859
+ return raw.trim().toLowerCase().replace(/[\s_\-]/g, "");
5860
+ }
5861
+ var ZONE_LEGAL_STATUS_MAP = {
5862
+ // Swiss ÖREB rechtsstatus
5863
+ "inkraft": "in-force",
5864
+ "laufendeanderung": "amendment-pending",
5865
+ "laufende\xE4nderung": "amendment-pending",
5866
+ "aenderungohnevorvirkung": "amendment-pending",
5867
+ // guard against common typo
5868
+ "aenderungohnevorwirkung": "amendment-pending",
5869
+ "\xE4nderungohnevorwirkung": "amendment-pending",
5870
+ "aenderungmitvorwirkung": "amendment-in-effect",
5871
+ "\xE4nderungmitvorwirkung": "amendment-in-effect",
5872
+ "\xF6ffentlicheauflage": "proposed",
5873
+ "offentlicheauflage": "proposed",
5874
+ "aufgehoben": "repealed",
5875
+ // Danish plandata.dk status
5876
+ "vedtaget": "in-force",
5877
+ "forslag": "proposed",
5878
+ "aflyst": "repealed"
5879
+ };
5880
+ function classifyZoneLegalStatus(raw) {
5881
+ if (!raw)
5882
+ return void 0;
5883
+ return ZONE_LEGAL_STATUS_MAP[normKey(raw)];
5884
+ }
5885
+ var ZONE_PLAN_TYPE_EXACT = {
5886
+ // Swiss Nutzungsplanung (typ_gde_bezeichnung and common variants)
5887
+ "grundnutzungszonenplan": "land-use-plan",
5888
+ "nutzungszonenplan": "land-use-plan",
5889
+ "zonenplan": "land-use-plan",
5890
+ "gestaltungsplan": "design-plan",
5891
+ "bebauungsplan": "development-plan",
5892
+ "sondernutzungsplan": "special-use-plan",
5893
+ "quartiererhaltungszonenplan": "neighbourhood-conservation-plan",
5894
+ "l\xE4rmempfindlichkeitsstufenplan": "noise-sensitivity-plan",
5895
+ "laermempfindlichkeitsstufenplan": "noise-sensitivity-plan",
5896
+ "gew\xE4sserschutzzonenplan": "water-protection-plan",
5897
+ "gewasserschutzzonenplan": "water-protection-plan",
5898
+ "grundwasserschutzzonenplan": "water-protection-plan",
5899
+ "gew\xE4sserschutzplan": "water-protection-plan",
5900
+ "naturundlandschaftsschutzzonenplan": "nature-landscape-plan",
5901
+ "natur-undlandschaftsschutzzonenplan": "nature-landscape-plan",
5902
+ "landschaftsschutzzonenplan": "nature-landscape-plan",
5903
+ // Freihaltezone — open space / clearance zone within the Grundnutzungszonenplan
5904
+ "freihaltezone": "open-space-zone",
5905
+ // Simple Grundnutzung zone type designations (municipality labels from ogd-0156)
5906
+ "kernzone": "land-use-plan",
5907
+ "wald": "land-use-plan",
5908
+ "gew\xE4sser": "land-use-plan",
5909
+ "gewasser": "land-use-plan",
5910
+ "reservezone": "land-use-plan",
5911
+ "bahnareal": "land-use-plan",
5912
+ "erholungszone": "land-use-plan",
5913
+ // Road / transport area designations
5914
+ "strassen(weitere)": "land-use-plan",
5915
+ "strassen": "land-use-plan",
5916
+ // Unzoned areas
5917
+ "nichtzoniertfl\xE4che": "unzoned",
5918
+ "nichtzoniertefl\xE4che": "unzoned",
5919
+ "nichtzoniertflache": "unzoned",
5920
+ "nichtzonierteflache": "unzoned",
5921
+ "nichtzoniertgem\xE4ssbzo2016": "unzoned",
5922
+ "nichtzoniertgemassbzo2016": "unzoned",
5923
+ // Overlay building/development regulations (Überlagernde Festlegungen)
5924
+ "hochh\xE4user": "overlay-regulation",
5925
+ "hochhauser": "overlay-regulation",
5926
+ "areal\xFCberbauungenzul\xE4ssig": "overlay-regulation",
5927
+ "arealuberbauungenzulassig": "overlay-regulation",
5928
+ "erh\xF6hteausnutzung": "overlay-regulation",
5929
+ "erhohteausnutzung": "overlay-regulation",
5930
+ "erdgeschossnutzung": "overlay-regulation",
5931
+ "freifl\xE4chenzifferverlegungsgebiet": "overlay-regulation",
5932
+ "freiflachenzifferverlegungsgebiet": "overlay-regulation",
5933
+ "baumschutz": "overlay-regulation",
5934
+ // Special building regulations (Sonderbauvorschriften)
5935
+ "sonderbauvorschriften": "overlay-regulation",
5936
+ // Danish plandata.dk
5937
+ "lokalplan": "local-plan",
5938
+ "kommuneplanramme": "municipal-plan-framework"
5939
+ };
5940
+ var ZONE_PLAN_TYPE_KEYWORDS = [
5941
+ [/quartiererhalt/i, "neighbourhood-conservation-plan"],
5942
+ [/lärmempfindlich|laermempfindlich/i, "noise-sensitivity-plan"],
5943
+ [/gewässerschutz|grundwasserschutz/i, "water-protection-plan"],
5944
+ [/natur.*landschaft|landschaft.*schutz/i, "nature-landscape-plan"],
5945
+ [/gestaltungsplan/i, "design-plan"],
5946
+ [/bebauungsplan/i, "development-plan"],
5947
+ [/sondernutzung/i, "special-use-plan"],
5948
+ // Freihaltezone / open-space clearance zones (before general zonenplan catch-all)
5949
+ [/freihalt/i, "open-space-zone"],
5950
+ // Zone für öffentliche Bauten — public-facilities land-use zone
5951
+ [/öffentliche.*bauten|zone.*öffentlich/i, "land-use-plan"],
5952
+ // Overlaying building/development regulations
5953
+ [/hochhaus/i, "overlay-regulation"],
5954
+ [/arealüberbauung|arealuberbauung/i, "overlay-regulation"],
5955
+ [/erhöhte.*ausnutzung|ausnutzung/i, "overlay-regulation"],
5956
+ [/erdgeschossnutzung/i, "overlay-regulation"],
5957
+ [/freiflächenzifferverlegung|freiflachenzifferverlegung/i, "overlay-regulation"],
5958
+ [/erhaltenswert/i, "overlay-regulation"],
5959
+ [/m[\u00e4a]ssig.*st[\u00f6o]rend|st[\u00f6o]rendes.*gewerbe/i, "overlay-regulation"],
5960
+ // Grundnutzung zone type designations (municipality labels, ogd-0156)
5961
+ [/wohnzone/i, "land-use-plan"],
5962
+ [/kernzone|zentrumszone/i, "land-use-plan"],
5963
+ [/erholungszone|erholungs/i, "land-use-plan"],
5964
+ [/stra[sß]en/i, "land-use-plan"],
5965
+ [/landwirtschaft/i, "land-use-plan"],
5966
+ [/\bwald\b/i, "land-use-plan"],
5967
+ [/\bgew[\u00e4a]sser\b/i, "land-use-plan"],
5968
+ // Unzoned areas
5969
+ [/nicht\s*zoniert/i, "unzoned"],
5970
+ // Tree / vegetation protection overlay
5971
+ [/baumschutz/i, "overlay-regulation"],
5972
+ // Special building regulations not under standard building ordinance
5973
+ [/sonderbauvorschrift/i, "overlay-regulation"],
5974
+ [/kommuneplan/i, "municipal-plan-framework"],
5975
+ [/lokalplan/i, "local-plan"],
5976
+ [/zonenplan|nutzungsplan|nutzungszone/i, "land-use-plan"],
5977
+ // Catch-all: any remaining Xzone / Xareal designation from the Grundnutzungszonenplan
5978
+ [/zone$|areal$/i, "land-use-plan"]
5979
+ ];
5980
+ function classifyZonePlanType(raw) {
5981
+ if (!raw)
5982
+ return void 0;
5983
+ const key = normKey(raw);
5984
+ if (key in ZONE_PLAN_TYPE_EXACT)
5985
+ return ZONE_PLAN_TYPE_EXACT[key];
5986
+ for (const [pattern, planType] of ZONE_PLAN_TYPE_KEYWORDS) {
5987
+ if (pattern.test(raw))
5988
+ return planType;
5989
+ }
5990
+ console.warn(
5991
+ `[cue-gis] Unknown zone plan type \u2014 add "${raw}" to classifyZonePlanType: no ZonePlanType mapping found.`
5992
+ );
5993
+ return void 0;
5994
+ }
5995
+
5996
+ // libs/js/cue-gis/src/lib/zurich-maps-adapter.ts
5997
+ var WFS_BASE_URL = "https://maps.zh.ch/wfs/OGDZHWFS";
5998
+ var ZURICH_SOURCE_ID = "zurich-wfs";
5999
+ var ZURICH_CANTON_BBOX = [8.35, 47.15, 8.95, 47.7];
6000
+ var SWITZERLAND_BBOX = [5.9, 45.7, 10.55, 47.85];
6001
+ function entryTypeName(entry) {
6002
+ return typeof entry === "string" ? entry : entry.typeName;
6003
+ }
6004
+ function entryCqlFilter(entry) {
6005
+ return typeof entry === "string" ? void 0 : entry.cqlFilter;
6006
+ }
6007
+ function firstCoordinate(geometry) {
6008
+ if (!geometry)
6009
+ return [0, 0];
6010
+ const coords = geometry.coordinates;
6011
+ switch (geometry.type) {
6012
+ case "Point":
6013
+ return coords;
6014
+ case "MultiPoint":
6015
+ case "LineString":
6016
+ return coords[0];
6017
+ case "MultiLineString":
6018
+ case "Polygon":
6019
+ return coords[0][0];
6020
+ case "MultiPolygon":
6021
+ return coords[0][0][0];
6022
+ default:
6023
+ return [0, 0];
6024
+ }
6025
+ }
6026
+ function pickName(props, fallback) {
6027
+ if (!props)
6028
+ return fallback;
6029
+ for (const key of ["plannavn", "bezeichnung", "name", "strassenname", "objektname", "title", "label"]) {
6030
+ if (typeof props[key] === "string" && props[key])
6031
+ return props[key];
6032
+ }
6033
+ return fallback;
6034
+ }
6035
+ function toLayerId2(sourceId, typeName, cqlFilter) {
6036
+ if (!cqlFilter)
6037
+ return `${sourceId}:${typeName}`;
6038
+ return `${sourceId}:${typeName}[${cqlFilter}]`;
6039
+ }
6040
+ var PRIORITY_CATEGORIES = /* @__PURE__ */ new Set(["building", "cadastre", "greenspace", "paved", "zone"]);
6041
+ function toLayerDescriptor2(sourceId, category, typeName, cqlFilter) {
6042
+ const shortName = typeName.replace(/^ms:/, "");
6043
+ const descriptor = featureCategoryDescriptor(category);
6044
+ const tier = PRIORITY_CATEGORIES.has(category) ? "priority" : "raw";
6045
+ return {
6046
+ id: toLayerId2(sourceId, typeName, cqlFilter),
6047
+ sourceId,
6048
+ sourceLayerId: typeName,
6049
+ category,
6050
+ preferredColor: descriptor.preferredColor,
6051
+ tier,
6052
+ label: shortName,
6053
+ description: `Z\xFCrich WFS layer ${shortName}`,
6054
+ labelKey: `gis.layer.${sourceId}.${shortName}.label`,
6055
+ descriptionKey: `gis.layer.${sourceId}.${shortName}.description`
6056
+ };
6057
+ }
6058
+ function toNormalisedBuildingProperties(props, sourceId) {
6059
+ if (!props)
6060
+ return { featureType: "building" };
6061
+ const rawArea = props["grundflaeche"] ?? props["gbf"] ?? props["gebaeudegrundrissflaeche"] ?? props["flaeche"] ?? void 0;
6062
+ const rawFloors = props["vollgeschosse"] ?? props["geschossanzahl"] ?? props["anzahl_geschosse"] ?? void 0;
6063
+ const buildingUse = props["gebaeudefunktion"] ?? props["gfkode"] ?? props["art"] ?? props["objektart"] ?? void 0;
6064
+ const rawYear = props["baujahr"] ?? props["bauperiode"] ?? void 0;
6065
+ const registryId = String(props["egid"] ?? props["gwr_egid"] ?? props["egris_egid"] ?? "").trim() || void 0;
6066
+ return {
6067
+ featureType: "building",
6068
+ areaM2: typeof rawArea === "number" ? rawArea : void 0,
6069
+ buildingUse: buildingUse ? String(buildingUse) : void 0,
6070
+ buildingUseGeneric: classifyBuildingUse(buildingUse, sourceId),
6071
+ floors: typeof rawFloors === "number" ? rawFloors : void 0,
6072
+ yearBuilt: typeof rawYear === "number" ? rawYear : void 0,
6073
+ registryId
6074
+ };
6075
+ }
6076
+ function toNormalisedPlotProperties(props, sourceId) {
6077
+ if (!props)
6078
+ return { featureType: "plot" };
6079
+ const registryId = props["egris_egrid"] ?? props["egrid"] ?? void 0;
6080
+ const nummer = props["nummer"] ?? void 0;
6081
+ const nbident = props["nbident"] ?? void 0;
6082
+ const plotId = nummer && nbident ? `${nummer}, ${nbident}` : nummer ?? void 0;
6083
+ const rawArea = props["flaechenmass"] ?? props["flaeche"] ?? void 0;
6084
+ const plotUse = props["art"] ?? props["nutzungsart"] ?? void 0;
6085
+ return {
6086
+ featureType: "plot",
6087
+ areaM2: typeof rawArea === "number" ? rawArea : void 0,
6088
+ plotUse: plotUse ? String(plotUse) : void 0,
6089
+ plotUseGeneric: classifyPlotUse(plotUse, sourceId),
6090
+ plotId,
6091
+ registryId
6092
+ };
6093
+ }
6094
+ function toNormalisedGreenspaceProperties(props) {
6095
+ if (!props)
6096
+ return { featureType: "greenspace" };
6097
+ const rawArea = props["flaeche"] ?? void 0;
6098
+ const surfaceType = props["art"] ?? void 0;
6099
+ return {
6100
+ featureType: "greenspace",
6101
+ areaM2: typeof rawArea === "number" ? rawArea : void 0,
6102
+ surfaceType: surfaceType ? String(surfaceType) : void 0
6103
+ };
6104
+ }
6105
+ function toNormalisedPavedProperties(props) {
6106
+ if (!props)
6107
+ return { featureType: "paved" };
6108
+ const rawArea = props["flaeche"] ?? void 0;
6109
+ const surfaceType = props["art"] ?? void 0;
6110
+ return {
6111
+ featureType: "paved",
6112
+ areaM2: typeof rawArea === "number" ? rawArea : void 0,
6113
+ surfaceType: surfaceType ? String(surfaceType) : void 0
6114
+ };
6115
+ }
6116
+ function toNormalisedZoneProperties(props) {
6117
+ if (!props)
6118
+ return { featureType: "zone" };
6119
+ const zoneType = props["plannavn"] ?? props["bezeichnung"] ?? props["typ_bezeichnung"] ?? props["art"] ?? void 0;
6120
+ const planId = props["planid"] ?? props["plannummer"] ?? void 0;
6121
+ const zoneCode = props["artcode"] ?? props["typ_code"] ?? props["abkuerzung"] ?? planId ?? void 0;
6122
+ const rawLegalStatus = props["rechtsstatus"] ?? props["status"] ?? void 0;
6123
+ const legalStatus = classifyZoneLegalStatus(rawLegalStatus);
6124
+ const rawPlanType = props["typ_gde_bezeichnung"] ?? void 0;
6125
+ const planType = classifyZonePlanType(rawPlanType);
6126
+ const planDocumentLink = props["dagsordenpunkt_url"] ?? props["dokument_url"] ?? props["link"] ?? void 0;
6127
+ const publicationDate = props["auflagedatum"] ?? void 0;
6128
+ const fixingDate = props["festsetzungsdatum"] ?? void 0;
6129
+ const approvalDate = props["genehmigungsdatum"] ?? void 0;
6130
+ const effectiveFrom = props["inkraftsetzungsdatum"] ?? void 0;
6131
+ const rawArea = props["flaeche"] ?? props["flaeche_m2"] ?? void 0;
6132
+ return {
6133
+ featureType: "zone",
6134
+ zoneType: zoneType ? String(zoneType) : void 0,
6135
+ zoneCode: zoneCode ? String(zoneCode) : void 0,
6136
+ legalStatus,
6137
+ planType,
6138
+ planId: planId ? String(planId) : void 0,
6139
+ planDocumentLink: planDocumentLink ? String(planDocumentLink) : void 0,
6140
+ publicationDate: publicationDate ?? void 0,
6141
+ fixingDate: fixingDate ?? void 0,
6142
+ approvalDate: approvalDate ?? void 0,
6143
+ effectiveFrom: effectiveFrom ?? void 0,
6144
+ areaM2: typeof rawArea === "number" ? rawArea : void 0
6145
+ };
6146
+ }
6147
+ function _makeCqlPostFilter(cqlFilter) {
6148
+ const eqMatch = cqlFilter.match(/^(\w+)\s*=\s*'([^']+)'$/);
6149
+ if (eqMatch) {
6150
+ const [, field, value] = eqMatch;
6151
+ return (props) => String(props?.[field] ?? "") === value;
6152
+ }
6153
+ const inMatch = cqlFilter.match(/^(\w+)\s+IN\s*\(([^)]+)\)$/i);
6154
+ if (inMatch) {
6155
+ const [, field, valuesStr] = inMatch;
6156
+ const allowed = new Set(
6157
+ valuesStr.split(",").map((v) => v.trim().replace(/^'|'$/g, ""))
6158
+ );
6159
+ return (props) => allowed.has(String(props?.[field] ?? ""));
6160
+ }
6161
+ return () => true;
6162
+ }
6163
+ var ZurichMapsAdapter = class {
6164
+ categoryMap;
6165
+ baseUrl;
6166
+ sourceId;
6167
+ outputFormat;
6168
+ constructor(options) {
6169
+ this.categoryMap = options.categoryMap;
6170
+ this.baseUrl = options.baseUrl ?? WFS_BASE_URL;
6171
+ this.sourceId = options.sourceId ?? ZURICH_SOURCE_ID;
6172
+ this.outputFormat = options.outputFormat ?? "application/json; subtype=geojson";
6173
+ }
6174
+ async listFeatureCategoryDescriptors(bbox) {
6175
+ const layers = await this.listAvailableLayers(bbox);
6176
+ return [...new Map(layers.map((layer) => [layer.category, featureCategoryDescriptor(layer.category)])).values()];
6177
+ }
6178
+ async listAvailableLayers(bbox) {
6179
+ const entries = Object.entries(this.categoryMap);
6180
+ const results = await Promise.allSettled(
6181
+ entries.flatMap(
6182
+ ([category, layerEntries]) => layerEntries.map(async (entry) => {
6183
+ const typeName = entryTypeName(entry);
6184
+ const cqlFilter = entryCqlFilter(entry);
6185
+ const fc = await this._fetchFeatures(bbox, typeName, 1, cqlFilter);
6186
+ return { category, typeName, cqlFilter, hasResults: fc.features.length > 0 };
6187
+ })
6188
+ )
6189
+ );
6190
+ return results.filter(
6191
+ (r) => r.status === "fulfilled" && r.value.hasResults
6192
+ ).map((r) => toLayerDescriptor2(this.sourceId, r.value.category, r.value.typeName, r.value.cqlFilter));
6193
+ }
6194
+ async getFeaturesForLayer(bbox, layerId) {
6195
+ const found = this._findLayerById(layerId);
6196
+ if (!found)
6197
+ return [];
6198
+ return this._fetchAndConvert(bbox, found.descriptor, found.cqlFilter);
6199
+ }
6200
+ async _fetchAndConvert(bbox, layer, cqlFilter) {
6201
+ const fc = await this._fetchFeatures(bbox, layer.sourceLayerId, void 0, cqlFilter);
6202
+ const postFilter = cqlFilter ? _makeCqlPostFilter(cqlFilter) : void 0;
6203
+ const features = postFilter ? fc.features.filter((f) => postFilter(f.properties)) : fc.features;
6204
+ return features.map((feature, index) => this.toGisFeature(feature, layer, index));
6205
+ }
6206
+ /**
6207
+ * Returns the categories that have at least one configured typename AND at
6208
+ * least one feature in the given bbox (light `count=1` probe per typename).
6209
+ */
6210
+ async listFeatureCategories(bbox) {
6211
+ const layers = await this.listAvailableLayers(bbox);
6212
+ return [...new Set(layers.map((layer) => layer.category))];
6213
+ }
6214
+ async getFeaturesOfCategory(bbox, category) {
6215
+ const entries = this.categoryMap[category] ?? [];
6216
+ const batches = await Promise.all(
6217
+ entries.map((entry) => {
6218
+ const typeName = entryTypeName(entry);
6219
+ const cqlFilter = entryCqlFilter(entry);
6220
+ const layer = toLayerDescriptor2(this.sourceId, category, typeName);
6221
+ return this._fetchAndConvert(bbox, layer, cqlFilter);
6222
+ })
6223
+ );
6224
+ return batches.flat();
6225
+ }
6226
+ // ─── Private helpers ────────────────────────────────────────────────────────
6227
+ _findLayerById(layerId) {
6228
+ const entries = Object.entries(this.categoryMap);
6229
+ for (const [category, layerEntries] of entries) {
6230
+ for (const entry of layerEntries) {
6231
+ const typeName = entryTypeName(entry);
6232
+ const cqlFilter = entryCqlFilter(entry);
6233
+ const layer = toLayerDescriptor2(this.sourceId, category, typeName, cqlFilter);
6234
+ if (layer.id === layerId) {
6235
+ return { descriptor: layer, cqlFilter };
6236
+ }
6237
+ }
6238
+ }
6239
+ return void 0;
6240
+ }
6241
+ async _fetchFeatures(bbox, typeName, count, cqlFilter) {
6242
+ const [west, south, east, north] = bbox;
6243
+ const bboxParam = `${south},${west},${north},${east},urn:ogc:def:crs:EPSG::4326`;
6244
+ const params = new URLSearchParams({
6245
+ service: "WFS",
6246
+ version: "2.0.0",
6247
+ request: "GetFeature",
6248
+ typename: typeName,
6249
+ bbox: bboxParam,
6250
+ srsName: "EPSG:4326",
6251
+ outputFormat: this.outputFormat
6252
+ });
6253
+ if (count !== void 0) {
6254
+ params.set("count", String(count));
6255
+ }
6256
+ if (cqlFilter !== void 0) {
6257
+ params.set("CQL_FILTER", cqlFilter);
6258
+ }
6259
+ params.set("outputFormat", this.outputFormat);
6260
+ const url = `${this.baseUrl}?${params.toString()}`;
6261
+ const response = await fetch(url);
6262
+ if (!response.ok) {
6263
+ throw new Error(
6264
+ `WFS request failed for ${typeName} (${this.sourceId}): ${response.status} ${response.statusText}`
6265
+ );
6266
+ }
6267
+ return response.json();
6268
+ }
6269
+ toGisFeature(feature, layer, index) {
6270
+ const [lon, lat] = firstCoordinate(feature.geometry);
6271
+ const name = pickName(feature.properties, `${layer.sourceLayerId}[${index}]`);
6272
+ const featureBBox = feature.bbox ? [feature.bbox[0], feature.bbox[1], feature.bbox[2], feature.bbox[3]] : void 0;
6273
+ const tier = layer.tier;
6274
+ const properties = tier === "priority" ? this._extractNormalisedProperties(feature, layer) : void 0;
6275
+ return {
6276
+ id: feature.id ?? `${layer.sourceLayerId}/${index}`,
6277
+ preferredColor: layer.preferredColor,
6278
+ sourceId: this.sourceId,
6279
+ sourceFeatureId: feature.id ?? `${layer.sourceLayerId}/${index}`,
6280
+ layerId: layer.id,
6281
+ name,
6282
+ category: layer.category,
6283
+ type: layer.sourceLayerId.replace(/^ms:/, ""),
6284
+ lat,
6285
+ lon,
6286
+ displayName: name,
6287
+ bbox: featureBBox,
6288
+ geometry: feature.geometry ?? void 0,
6289
+ tier,
6290
+ properties,
6291
+ originalData: feature.properties ?? {}
6292
+ };
6293
+ }
6294
+ _extractNormalisedProperties(feature, layer) {
6295
+ if (layer.category === "building") {
6296
+ return toNormalisedBuildingProperties(feature.properties, this.sourceId);
6297
+ }
6298
+ if (layer.category === "greenspace") {
6299
+ return toNormalisedGreenspaceProperties(feature.properties);
6300
+ }
6301
+ if (layer.category === "paved") {
6302
+ return toNormalisedPavedProperties(feature.properties);
6303
+ }
6304
+ if (layer.category === "zone") {
6305
+ return toNormalisedZoneProperties(feature.properties);
6306
+ }
6307
+ return toNormalisedPlotProperties(feature.properties, this.sourceId);
6308
+ }
6309
+ };
6310
+
6311
+ // libs/js/cue-gis/src/lib/danish-cadastre-adapter.ts
6312
+ var DENMARK_BBOX = [8, 54.5, 15.2, 57.8];
6313
+
6314
+ // libs/js/cue-gis/src/lib/cue-sdk-gis-adapter.ts
6315
+ var DEFAULT_DANISH_SDK_CATEGORY_MAP = {
6316
+ building: [
6317
+ { source: "danish-matrikel", typename: "bbr_v001:bygning_current" }
6318
+ ],
6319
+ cadastre: [
6320
+ { source: "danish-matrikel", typename: "mat_v001:lodflade_current" },
6321
+ { source: "danish-matrikel", typename: "mat_v001:samletfastejendom_current" }
6322
+ ]
6323
+ };
6324
+ function firstCoordinate2(geometry) {
6325
+ if (!geometry)
6326
+ return [0, 0];
6327
+ const coords = geometry.coordinates;
6328
+ switch (geometry.type) {
6329
+ case "Point":
6330
+ return coords;
6331
+ case "MultiPoint":
6332
+ case "LineString":
6333
+ return coords[0];
6334
+ case "MultiLineString":
6335
+ case "Polygon":
6336
+ return coords[0][0];
6337
+ case "MultiPolygon":
6338
+ return coords[0][0][0];
6339
+ default:
6340
+ return [0, 0];
6341
+ }
6342
+ }
6343
+ function pickName2(props, fallback) {
6344
+ if (!props)
6345
+ return fallback;
6346
+ for (const key of [
6347
+ "mat:matrikelnummer",
6348
+ "matrikelnummer",
6349
+ "mat:ejerlavsnavn",
6350
+ "ejerlavsnavn",
6351
+ "name",
6352
+ "bezeichnung",
6353
+ "stednavntekst",
6354
+ "title",
6355
+ "label"
6356
+ ]) {
6357
+ if (typeof props[key] === "string" && props[key])
6358
+ return props[key];
6359
+ }
6360
+ return fallback;
6361
+ }
6362
+ function toLayerId3(sourceId, source, typename) {
6363
+ return `${sourceId}:${source}:${typename}`;
6364
+ }
6365
+ var PRIORITY_CATEGORIES2 = /* @__PURE__ */ new Set(["building", "cadastre", "zone"]);
6366
+ function toLayerDescriptor3(sourceId, category, entry) {
6367
+ const shortName = entry.typename.replace(/^[^:]+:/, "");
6368
+ const descriptor = featureCategoryDescriptor(category);
6369
+ const tier = PRIORITY_CATEGORIES2.has(category) ? "priority" : "raw";
6370
+ return {
6371
+ id: toLayerId3(sourceId, entry.source, entry.typename),
6372
+ sourceId,
6373
+ sourceLayerId: entry.typename,
6374
+ category,
6375
+ preferredColor: descriptor.preferredColor,
6376
+ tier,
6377
+ label: entry.label ?? shortName,
6378
+ description: `${entry.source} / ${entry.typename}`,
6379
+ labelKey: `gis.layer.${sourceId}.${entry.source}.${shortName}.label`,
6380
+ descriptionKey: `gis.layer.${sourceId}.${entry.source}.${shortName}.description`
6381
+ };
6382
+ }
6383
+ function _extractNormalizedProperties(props, category) {
6384
+ if (category === "building") {
6385
+ const rawArea2 = props?.["byg041BebyggetAreal"] ?? props?.["byg038SamletBygningsareal"];
6386
+ const buildingUse = props?.["byg021BygningensAnvendelse"];
6387
+ const registryId = props?.["id_lokalId"];
6388
+ const rawYear = props?.["byg026Opf\xF8relses\xE5r"];
6389
+ return {
6390
+ featureType: "building",
6391
+ areaM2: typeof rawArea2 === "number" ? rawArea2 : void 0,
6392
+ buildingUse: buildingUse ?? void 0,
6393
+ yearBuilt: typeof rawYear === "number" ? rawYear : void 0,
6394
+ registryId: registryId ?? void 0
6395
+ };
6396
+ }
6397
+ if (category === "zone") {
6398
+ const zoneType = props?.["plannavn"] ?? props?.["bezeichnung"] ?? props?.["typ_bezeichnung"] ?? void 0;
6399
+ const planId = props?.["planid"] ?? props?.["plannummer"] ?? void 0;
6400
+ const zoneCode = props?.["artcode"] ?? props?.["typ_code"] ?? planId ?? void 0;
6401
+ const rawLegalStatus = props?.["rechtsstatus"] ?? props?.["status"] ?? void 0;
6402
+ const planDocumentLink = props?.["dagsordenpunkt_url"] ?? props?.["dokument_url"] ?? void 0;
6403
+ const rawArea2 = props?.["flaeche"] ?? props?.["flaeche_m2"] ?? void 0;
6404
+ return {
6405
+ featureType: "zone",
6406
+ zoneType: zoneType ?? void 0,
6407
+ zoneCode: zoneCode ?? void 0,
6408
+ legalStatus: classifyZoneLegalStatus(rawLegalStatus),
6409
+ planType: classifyZonePlanType(props?.["typ_gde_bezeichnung"]),
6410
+ planId: planId ?? void 0,
6411
+ planDocumentLink: planDocumentLink ?? void 0,
6412
+ publicationDate: props?.["auflagedatum"] ?? void 0,
6413
+ fixingDate: props?.["festsetzungsdatum"] ?? void 0,
6414
+ approvalDate: props?.["genehmigungsdatum"] ?? void 0,
6415
+ effectiveFrom: props?.["inkraftsetzungsdatum"] ?? void 0,
6416
+ areaM2: typeof rawArea2 === "number" ? rawArea2 : void 0
6417
+ };
6418
+ }
6419
+ const parcelNr = props?.["mat:matrikelnummer"] ?? props?.["matrikelnummer"];
6420
+ const districtName = props?.["mat:ejerlavsnavn"] ?? props?.["ejerlavsnavn"];
6421
+ const rawArea = props?.["mat:registreretareal"] ?? props?.["registreretareal"];
6422
+ const bfeNr = props?.["mat:bfenummer"] ?? props?.["bfenummer"];
6423
+ return {
6424
+ featureType: "plot",
6425
+ areaM2: typeof rawArea === "number" ? rawArea : void 0,
6426
+ plotId: parcelNr && districtName ? `${parcelNr}, ${districtName}` : parcelNr ?? void 0,
6427
+ registryId: bfeNr ?? void 0
6428
+ };
6429
+ }
6430
+ var CueSdkGisAdapter = class {
6431
+ categoryMap;
6432
+ dataViewsBaseUrl;
6433
+ getHeaders;
6434
+ sourceId;
6435
+ constructor(options) {
6436
+ this.dataViewsBaseUrl = options.dataViewsBaseUrl.replace(/\/$/, "");
6437
+ this.getHeaders = options.getHeaders;
6438
+ this.categoryMap = options.categoryMap ?? DEFAULT_DANISH_SDK_CATEGORY_MAP;
6439
+ this.sourceId = options.sourceId ?? "cue-sdk-gis";
6440
+ }
6441
+ async listFeatureCategoryDescriptors(bbox) {
6442
+ const layers = await this.listAvailableLayers(bbox);
6443
+ return [
6444
+ ...new Map(
6445
+ layers.map((l) => [l.category, featureCategoryDescriptor(l.category)])
6446
+ ).values()
6447
+ ];
6448
+ }
6449
+ async listAvailableLayers(bbox) {
6450
+ const entries = Object.entries(this.categoryMap);
6451
+ const results = await Promise.allSettled(
6452
+ entries.flatMap(
6453
+ ([category, layerEntries]) => layerEntries.map(async (entry) => {
6454
+ const fc = await this._fetchFeatures(bbox, entry, 1);
6455
+ return { category, entry, hasResults: fc.features.length > 0 };
6456
+ })
6457
+ )
6458
+ );
6459
+ return results.filter(
6460
+ (r) => r.status === "fulfilled" && r.value.hasResults
6461
+ ).map((r) => toLayerDescriptor3(this.sourceId, r.value.category, r.value.entry));
6462
+ }
6463
+ async getFeaturesForLayer(bbox, layerId) {
6464
+ const result = this._findLayerById(layerId);
6465
+ if (!result)
6466
+ return [];
6467
+ const fc = await this._fetchFeatures(bbox, result.entry);
6468
+ return fc.features.map((f, i) => this._toGisFeature(f, result.descriptor, i));
6469
+ }
6470
+ async listFeatureCategories(bbox) {
6471
+ const layers = await this.listAvailableLayers(bbox);
6472
+ return [...new Set(layers.map((l) => l.category))];
6473
+ }
6474
+ async getFeaturesOfCategory(bbox, category) {
6475
+ const layerEntries = this.categoryMap[category] ?? [];
6476
+ const batches = await Promise.all(
6477
+ layerEntries.map((entry) => {
6478
+ const descriptor = toLayerDescriptor3(this.sourceId, category, entry);
6479
+ return this.getFeaturesForLayer(bbox, descriptor.id);
6480
+ })
6481
+ );
6482
+ return batches.flat();
6483
+ }
6484
+ // ─── Private helpers ────────────────────────────────────────────────────────
6485
+ _findLayerById(layerId) {
6486
+ for (const [category, entries] of Object.entries(this.categoryMap)) {
6487
+ for (const entry of entries) {
6488
+ const descriptor = toLayerDescriptor3(this.sourceId, category, entry);
6489
+ if (descriptor.id === layerId)
6490
+ return { entry, descriptor };
6491
+ }
6492
+ }
6493
+ return void 0;
6494
+ }
6495
+ async _fetchFeatures(bbox, entry, count) {
6496
+ const [west, south, east, north] = bbox;
6497
+ const headers = await this.getHeaders();
6498
+ const params = new URLSearchParams({
6499
+ source: entry.source,
6500
+ typename: entry.typename,
6501
+ bbox: `${west},${south},${east},${north}`
6502
+ });
6503
+ if (count !== void 0) {
6504
+ params.set("count", String(count));
6505
+ }
6506
+ const url = `${this.dataViewsBaseUrl}/gis/features?${params.toString()}`;
6507
+ const response = await fetch(url, { headers });
6508
+ if (!response.ok) {
6509
+ throw new Error(
6510
+ `CueSdkGisAdapter: request failed for ${entry.source}/${entry.typename}: ${response.status} ${response.statusText}`
6511
+ );
6512
+ }
6513
+ return response.json();
6514
+ }
6515
+ _toGisFeature(feature, layer, index) {
6516
+ const [lon, lat] = firstCoordinate2(feature.geometry);
6517
+ const featureId = feature.id ?? `${layer.sourceLayerId}/${index}`;
6518
+ const name = pickName2(feature.properties, featureId);
6519
+ const featureBBox = feature.bbox ? [feature.bbox[0], feature.bbox[1], feature.bbox[2], feature.bbox[3]] : void 0;
6520
+ const tier = layer.tier;
6521
+ const properties = tier === "priority" ? _extractNormalizedProperties(feature.properties, layer.category) : void 0;
6522
+ return {
6523
+ id: featureId,
6524
+ sourceId: this.sourceId,
6525
+ sourceFeatureId: featureId,
6526
+ layerId: layer.id,
6527
+ name,
6528
+ category: layer.category,
6529
+ preferredColor: layer.preferredColor,
6530
+ type: layer.sourceLayerId.replace(/^[^:]+:/, ""),
6531
+ lat,
6532
+ lon,
6533
+ displayName: name,
6534
+ bbox: featureBBox,
6535
+ geometry: feature.geometry ?? void 0,
6536
+ tier,
6537
+ properties,
6538
+ originalData: feature.properties ?? {}
6539
+ };
6540
+ }
6541
+ };
6542
+
6543
+ // libs/js/cue-gis/src/lib/global-config.ts
6544
+ var DEFAULT_ZURICH_CATEGORY_MAP = {
6545
+ address: [
6546
+ "ms:ogd-0406_arv_basis_avzh_hausnummer_pos_p",
6547
+ "ms:ogd-0571_afv_gv_strat_strassennetz_l"
6548
+ ],
6549
+ poi: [
6550
+ "ms:ogd-0053_giszhpub_ogd_veloparkieranlagen_p",
6551
+ "ms:ogd-0406_arv_basis_avzh_gebaeudeeingang_p"
6552
+ ],
6553
+ railway: [
6554
+ "ms:ogd-0529_afv_gv_seilbahn_skilift_l",
6555
+ "ms:ogd-0529_afv_gv_seilbahn_skilift_p"
6556
+ ],
6557
+ natural: [
6558
+ "ms:ogd-0045_giszhpub_wb_fliessgewaesser_l",
6559
+ "ms:ogd-0045_giszhpub_wb_stehgewaesser_f",
6560
+ "ms:ogd-0111_giszhpub_wald_waldareal_f"
6561
+ ],
6562
+ manmade: [
6563
+ "ms:ogd-0248_giszhpub_en_gebaeude_volumen_ha_f",
6564
+ // Transport / infrastructure surface types from the cantonal land-cover layer.
6565
+ { typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('Bahngebiet', 'Strasse_Weg', 'Flugplatz', 'Bruecke')" }
6566
+ ],
6567
+ building: [
6568
+ // Individual building footprint polygons from the official cantonal survey (AV ZH).
6569
+ // BoFlaeche covers all land-cover types; the CQL filter restricts to buildings.
6570
+ { typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art = 'Geb\xE4ude'" }
6571
+ ],
6572
+ greenspace: [
6573
+ // Parks, gardens, playgrounds, sports facilities and other vegetated open areas.
6574
+ { typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('Gartenanlage', 'Parkanlage', 'Spielplatz', 'Sportanlage', 'Friedhof', 'Gehoelz', 'Feuchtgebiet')" }
6575
+ ],
6576
+ paved: [
6577
+ // Sealed / paved ground surfaces (courtyards, sidewalks, parking, etc.).
6578
+ { typeName: "ms:ogd-0401_arv_basis_avzh_bodenbedeckung_f", cqlFilter: "art IN ('befBoden', 'befestigte Fl\xE4che', 'Hof', 'Trottoir', 'Parkplatz')" }
6579
+ ],
6580
+ cadastre: [
6581
+ // Land parcels (Liegenschaften) from the official cantonal survey (AV ZH)
6582
+ "ms:ogd-0404_arv_basis_avzh_liegenschaften_f",
6583
+ // Independent and permanent rights (SelbstRecht / SDR)
6584
+ "ms:ogd-0404_arv_basis_avzh_selbstrecht_f"
6585
+ ],
6586
+ zone: [
6587
+ // ÖREB Nutzungsplanung (Grundnutzung) – primary land-use zoning polygons
6588
+ "ms:ogd-0156_arv_basis_np_gn_zonenflaeche_f",
6589
+ // ÖREB Überlagernde Festlegungen (Flächen) – overlaying planning constraints
6590
+ "ms:ogd-0155_arv_basis_np_ul_flaeche_f"
6591
+ ]
6592
+ };
6593
+ var DEFAULT_LOCAL_SERVICE_REGIONS = [
6594
+ {
6595
+ name: "Canton of Z\xFCrich",
6596
+ coverageBBox: ZURICH_CANTON_BBOX,
6597
+ supportedCategories: ["address", "poi", "railway", "natural", "manmade", "building", "cadastre", "greenspace", "paved", "zone"],
6598
+ priority: 10,
6599
+ adapter: new ZurichMapsAdapter({
6600
+ categoryMap: DEFAULT_ZURICH_CATEGORY_MAP
6601
+ })
6602
+ },
6603
+ {
6604
+ name: "Switzerland (national AV cadastre)",
6605
+ coverageBBox: SWITZERLAND_BBOX,
6606
+ supportedCategories: ["cadastre"],
6607
+ priority: 5,
6608
+ // Lower priority than canton-specific adapters. For ZH, the canton adapter (priority 10) wins.
6609
+ adapter: new ZurichMapsAdapter({
6610
+ sourceId: "swiss-av-wfs",
6611
+ baseUrl: "https://wfs.geodienste.ch/av_0/deu",
6612
+ categoryMap: {
6613
+ cadastre: [
6614
+ "ms:RESF",
6615
+ // Rechtsgültige Liegenschaften (valid parcels, all cantons)
6616
+ "ms:DPRSF"
6617
+ // Selbständige und dauernde Rechte / SDR (all cantons)
6618
+ ]
6619
+ }
6620
+ })
6621
+ },
6622
+ {
6623
+ /**
6624
+ * Denmark planning zones (lokalplan + kommuneplanrammer) from plandata.dk.
6625
+ *
6626
+ * This is a fully open WFS service (no API key required) provided by
6627
+ * Plandata, the Danish national planning data register.
6628
+ *
6629
+ * - `pdk:lokalplan_vedtaget` – adopted local plans (lokalplaner)
6630
+ * - `pdk:kommuneplanramme_vedtaget` – adopted municipal plan frameworks
6631
+ */
6632
+ name: "Denmark (Plandata.dk \u2013 planning zones)",
6633
+ coverageBBox: DENMARK_BBOX,
6634
+ supportedCategories: ["zone"],
6635
+ priority: 10,
6636
+ adapter: new ZurichMapsAdapter({
6637
+ sourceId: "danish-plandata",
6638
+ baseUrl: "https://wfs.plandata.dk/geoserver/pdk/wfs",
6639
+ outputFormat: "application/json",
6640
+ categoryMap: {
6641
+ zone: [
6642
+ "pdk:lokalplan_vedtaget",
6643
+ // Adopted local plans
6644
+ "pdk:kommuneplanramme_vedtaget"
6645
+ // Adopted municipal plan frameworks
6646
+ ]
6647
+ }
6648
+ })
6649
+ }
6650
+ ];
6651
+
6652
+ // libs/js/cue-gis/src/lib/gis-gateway.ts
6653
+ function sortByCanonicalOrder(descriptors) {
6654
+ return [...descriptors].sort(
6655
+ (a5, b) => FEATURE_CATEGORIES.indexOf(a5.category) - FEATURE_CATEGORIES.indexOf(b.category)
6656
+ );
6657
+ }
6658
+ var GisGateway = class {
6659
+ nominatim;
6660
+ regions;
6661
+ /**
6662
+ * Effective zone plan type colour map — {@link ZONE_PLAN_TYPE_COLORS} merged
6663
+ * with any overrides supplied via {@link GisGatewayOptions.zonePlanTypeColors}.
6664
+ */
6665
+ zonePlanTypeColors;
6666
+ constructor(options = {}) {
6667
+ const {
6668
+ regions,
6669
+ useDefaultRegions = true,
6670
+ zonePlanTypeColors,
6671
+ ...nominatimOptions
6672
+ } = options;
6673
+ this.nominatim = new NominatimAdapter(nominatimOptions);
6674
+ const defaultRegions = useDefaultRegions ? DEFAULT_LOCAL_SERVICE_REGIONS : [];
6675
+ this.regions = regions ?? defaultRegions;
6676
+ this.zonePlanTypeColors = { ...ZONE_PLAN_TYPE_COLORS, ...zonePlanTypeColors };
6677
+ }
6678
+ /**
6679
+ * Return the available feature categories together with UI metadata such as
6680
+ * preferred color.
6681
+ *
6682
+ * When the matching regions declare their {@link LocalServiceRegion.supportedCategories}
6683
+ * the result is built from those declarations without network probing.
6684
+ * Otherwise the winning adapter's own probing is used.
6685
+ *
6686
+ * @param bbox [west, south, east, north] in WGS-84 decimal degrees
6687
+ */
6688
+ async listFeatureCategoryDescriptors(bbox) {
6689
+ const declaredRegions = this.regions.filter((r) => bboxIntersects(r.coverageBBox, bbox) && r.supportedCategories).sort((a5, b) => (b.priority ?? 0) - (a5.priority ?? 0));
6690
+ if (declaredRegions.length > 0) {
6691
+ const seen = /* @__PURE__ */ new Set();
6692
+ const descriptors = [];
6693
+ for (const region of declaredRegions) {
6694
+ for (const cat of region.supportedCategories ?? []) {
6695
+ if (!seen.has(cat)) {
6696
+ seen.add(cat);
6697
+ descriptors.push(featureCategoryDescriptor(cat));
6698
+ }
6699
+ }
6700
+ }
6701
+ for (const cat of NOMINATIM_FEATURE_CATEGORIES) {
6702
+ if (!seen.has(cat)) {
6703
+ descriptors.push(featureCategoryDescriptor(cat));
6704
+ }
6705
+ }
6706
+ return sortByCanonicalOrder(descriptors);
6707
+ }
6708
+ const probed = await this._adapterFor(bbox).listFeatureCategoryDescriptors(bbox);
6709
+ return sortByCanonicalOrder(probed);
6710
+ }
6711
+ /**
6712
+ * Return the feature categories that have at least one result within the
6713
+ * given bounding box.
6714
+ *
6715
+ * @param bbox [west, south, east, north] in WGS-84 decimal degrees
6716
+ */
6717
+ async listFeatureCategories(bbox) {
6718
+ const descriptors = await this.listFeatureCategoryDescriptors(bbox);
6719
+ return descriptors.map((d) => d.category);
6720
+ }
6721
+ /**
6722
+ * Return all available layers for the given bbox using a provider-independent
6723
+ * layer descriptor shape.
6724
+ */
6725
+ async listAvailableLayers(bbox) {
6726
+ return this._adapterFor(bbox).listAvailableLayers(bbox);
6727
+ }
6728
+ /**
6729
+ * Stream GIS features of the given category from **all** services whose
6730
+ * coverage intersects the query bbox (plus the Nominatim global fallback).
6731
+ *
6732
+ * Processing steps:
6733
+ * 1. Log all configured services and their coverage bboxes.
6734
+ * 2. Identify services whose `coverageBBox` intersects `bbox`.
6735
+ * 3. Log the matched services.
6736
+ * 4. Fan out requests to all matched services in parallel; yield features as
6737
+ * each source resolves (streaming).
6738
+ * 5. All features are already normalised to {@link GisFeature} by each adapter.
6739
+ * 6. Quality-based deduplication: when two sources return a feature for the
6740
+ * same real-world object, only the higher-quality geometry is emitted.
6741
+ * A polygon/multipolygon beats a linestring, which beats a point. If a
6742
+ * better feature arrives after a lower-quality one was already yielded,
6743
+ * the better feature is yielded again (same {@link GisFeature.id}) so the
6744
+ * consumer can replace its copy.
6745
+ *
6746
+ * @param bbox [west, south, east, north] in WGS-84 decimal degrees
6747
+ * @param category One of the categories returned by {@link listFeatureCategories}
6748
+ */
6749
+ async *streamFeaturesOfCategory(bbox, category) {
6750
+ console.log("[GisGateway] Available services:");
6751
+ for (const region of this.regions) {
6752
+ const [w, s, e, n] = region.coverageBBox;
6753
+ console.log(
6754
+ ` \u2022 ${region.name} bbox=[${w}, ${s}, ${e}, ${n}] priority=${region.priority ?? 0}`
6755
+ );
6756
+ }
6757
+ console.log(" \u2022 Nominatim (OSM) bbox=[global] priority=-1");
6758
+ const matchedRegions = this.regions.filter(
6759
+ (r) => bboxIntersects(r.coverageBBox, bbox) && (!r.supportedCategories || r.supportedCategories.includes(category))
6760
+ );
6761
+ const sources = [
6762
+ ...matchedRegions.map((r) => ({
6763
+ name: r.name,
6764
+ adapter: r.adapter,
6765
+ priority: r.priority ?? 0
6766
+ })),
6767
+ { name: "Nominatim (OSM)", adapter: this.nominatim, priority: -1 }
6768
+ ];
6769
+ sources.sort((a5, b) => b.priority - a5.priority);
6770
+ console.log(`[GisGateway] Matched services for category "${category}":`);
6771
+ for (const s of sources) {
6772
+ console.log(` \u2022 ${s.name} (priority=${s.priority})`);
6773
+ }
6774
+ const qualityMap = /* @__PURE__ */ new Map();
6775
+ const remaining = new Map(
6776
+ sources.map(
6777
+ (s, i) => [
6778
+ i,
6779
+ s.adapter.getFeaturesOfCategory(bbox, category).then((features) => ({ features, idx: i })).catch((err) => {
6780
+ console.warn(`[GisGateway] Source "${s.name}" failed:`, err);
6781
+ return { features: [], idx: i };
6782
+ })
6783
+ ]
6784
+ )
6785
+ );
6786
+ while (remaining.size > 0) {
6787
+ const { features, idx } = await Promise.race(remaining.values());
6788
+ remaining.delete(idx);
6789
+ for (const feature of features) {
6790
+ const key = _dedupKey(feature);
6791
+ const score = _geometryQuality(feature);
6792
+ const best = qualityMap.get(key);
6793
+ if (best === void 0 || score > best) {
6794
+ qualityMap.set(key, score);
6795
+ yield feature;
6796
+ }
6797
+ }
6798
+ }
6799
+ }
6800
+ /**
6801
+ * Return all GIS features of the given category within the bounding box.
6802
+ *
6803
+ * Fans out to all matching services in parallel, then applies:
6804
+ * 1. Spatial merging – Nominatim point features that fall within ~100 m of a
6805
+ * local-service polygon are merged into that polygon (enriching its
6806
+ * `originalData`) rather than returned as a separate feature.
6807
+ * 2. Quality-based deduplication – when two unmerged features represent the
6808
+ * same object, the one with richer geometry is kept.
6809
+ *
6810
+ * @param bbox [west, south, east, north] in WGS-84 decimal degrees
6811
+ * @param category One of the categories returned by {@link listFeatureCategories}
6812
+ */
6813
+ async getFeaturesOfCategory(bbox, category) {
6814
+ const matchedRegions = this.regions.filter(
6815
+ (r) => bboxIntersects(r.coverageBBox, bbox) && (!r.supportedCategories || r.supportedCategories.includes(category))
6816
+ );
6817
+ const sources = [
6818
+ ...matchedRegions.map((r) => ({ name: r.name, adapter: r.adapter, isNominatim: false })),
6819
+ { name: "Nominatim (OSM)", adapter: this.nominatim, isNominatim: true }
6820
+ ];
6821
+ const batches = await Promise.allSettled(
6822
+ sources.map(
6823
+ (s) => s.adapter.getFeaturesOfCategory(bbox, category).then((features) => ({
6824
+ features,
6825
+ isNominatim: s.isNominatim
6826
+ })).catch((err) => {
6827
+ console.warn(`[GisGateway] Source "${s.name}" failed:`, err);
6828
+ return { features: [], isNominatim: s.isNominatim };
6829
+ })
6830
+ )
6831
+ );
6832
+ const localFeatures = [];
6833
+ const nominatimFeatures = [];
6834
+ for (const result of batches) {
6835
+ if (result.status !== "fulfilled")
6836
+ continue;
6837
+ if (result.value.isNominatim) {
6838
+ nominatimFeatures.push(...result.value.features);
6839
+ } else {
6840
+ localFeatures.push(...result.value.features);
6841
+ }
6842
+ }
6843
+ const merged = _mergeNominatimIntoPolygons(localFeatures, nominatimFeatures);
6844
+ const qualityMap = /* @__PURE__ */ new Map();
6845
+ const deduplicated = [];
6846
+ for (const feature of merged) {
6847
+ const key = _dedupKey(feature);
6848
+ const score = _geometryQuality(feature);
6849
+ const best = qualityMap.get(key);
6850
+ if (best === void 0 || score > best) {
6851
+ qualityMap.set(key, score);
6852
+ deduplicated.push(feature);
6853
+ }
6854
+ }
6855
+ return deduplicated;
6856
+ }
6857
+ /** Fetch features for a specific layer descriptor id. */
6858
+ async getFeaturesForLayer(bbox, layerId) {
6859
+ return this._adapterFor(bbox).getFeaturesForLayer(bbox, layerId);
6860
+ }
6861
+ /** Resolve the best adapter for a query bbox (legacy single-adapter routing). */
6862
+ _adapterFor(bbox) {
6863
+ const region = this.regions.filter((r) => bboxIntersects(r.coverageBBox, bbox)).sort((a5, b) => (b.priority ?? 0) - (a5.priority ?? 0))[0];
6864
+ return region?.adapter ?? this.nominatim;
6865
+ }
6866
+ };
6867
+ function _geometryQuality(feature) {
6868
+ const gt = feature.geometry?.type;
6869
+ if (gt === "Polygon" || gt === "MultiPolygon")
6870
+ return 3;
6871
+ if (gt === "LineString" || gt === "MultiLineString")
6872
+ return 2;
6873
+ return 1;
6874
+ }
6875
+ function _dedupKey(feature) {
6876
+ const lat = Math.round(feature.lat * 1e3) / 1e3;
6877
+ const lon = Math.round(feature.lon * 1e3) / 1e3;
6878
+ const name = (feature.name ?? feature.id ?? "").toLowerCase().trim();
6879
+ return `${name}|${lat}|${lon}`;
6880
+ }
6881
+ var MERGE_THRESHOLD = 1e-3;
6882
+ function _latLonDist(a5, b) {
6883
+ const dlat = a5.lat - b.lat;
6884
+ const dlon = a5.lon - b.lon;
6885
+ return Math.sqrt(dlat * dlat + dlon * dlon);
6886
+ }
6887
+ function _mergeNominatimIntoPolygons(localFeatures, nominatimFeatures) {
6888
+ if (nominatimFeatures.length === 0)
6889
+ return localFeatures;
6890
+ const polygons = localFeatures.filter((f) => _geometryQuality(f) === 3);
6891
+ const nonPolygons = localFeatures.filter((f) => _geometryQuality(f) !== 3);
6892
+ const enriched = polygons.map((f) => ({ feature: { ...f, originalData: { ...f.originalData } }, matched: false }));
6893
+ const unmatched = [];
6894
+ for (const nom of nominatimFeatures) {
6895
+ let bestIdx = -1;
6896
+ let bestDist = MERGE_THRESHOLD;
6897
+ for (let i = 0; i < enriched.length; i++) {
6898
+ const dist = _latLonDist(nom, enriched[i].feature);
6899
+ if (dist < bestDist) {
6900
+ bestDist = dist;
6901
+ bestIdx = i;
6902
+ }
6903
+ }
6904
+ if (bestIdx >= 0) {
6905
+ enriched[bestIdx].feature.originalData = {
6906
+ ...nom.originalData,
6907
+ ...enriched[bestIdx].feature.originalData
6908
+ };
6909
+ enriched[bestIdx].matched = true;
6910
+ } else {
6911
+ unmatched.push(nom);
6912
+ }
6913
+ }
6914
+ return [
6915
+ ...enriched.map((e) => e.feature),
6916
+ ...nonPolygons,
6917
+ ...unmatched
6918
+ ];
6919
+ }
6920
+
6921
+ // libs/js/cue-sdk/src/lib/gis.ts
6922
+ var DATA_VIEWS_PATH = "/data-views";
6923
+ var DEBOUNCE_MS = 1500;
6924
+ var CueGis = class {
6925
+ // undefined = not yet built
6926
+ /** @internal — construct via `cue.gis`, not directly. */
6927
+ constructor(_getAuthHeaders, _gatewayUrl) {
6928
+ this._getAuthHeaders = _getAuthHeaders;
6929
+ this._gatewayUrl = _gatewayUrl;
6930
+ }
6931
+ _bbox = null;
6932
+ _projectId = null;
6933
+ _selectedCategories = /* @__PURE__ */ new Set();
6934
+ _queryToken = 0;
6935
+ _categoryTokens = /* @__PURE__ */ new Map();
6936
+ _debounceTimer = null;
6937
+ _availableCategories = [];
6938
+ _features = /* @__PURE__ */ new Map();
6939
+ _catListeners = /* @__PURE__ */ new Set();
6940
+ _featListeners = /* @__PURE__ */ new Set();
6941
+ _loadListeners = /* @__PURE__ */ new Set();
6942
+ _gatewayCache = null;
6943
+ _gatewayProjectId = void 0;
6944
+ // ── Setters ───────────────────────────────────────────────────────────────
6945
+ /**
6946
+ * Update the current map viewport.
6947
+ * Triggers a debounced query for available categories and reloads selected ones.
6948
+ */
6949
+ setBbox(bbox) {
6950
+ this._bbox = bbox;
6951
+ this._scheduleDebouncedQuery();
6952
+ }
6953
+ /**
6954
+ * Set or clear the active project.
6955
+ * Rebuilds the authenticated adapter so subsequent requests carry the new project header.
6956
+ */
6957
+ setProjectId(projectId) {
6958
+ if (projectId === this._projectId)
6959
+ return;
6960
+ this._projectId = projectId;
6961
+ this._gatewayCache = null;
6962
+ this._scheduleDebouncedQuery();
6963
+ }
6964
+ /**
6965
+ * Replace the full set of selected categories.
6966
+ * Newly added categories begin loading immediately; removed categories are cleared.
6967
+ */
6968
+ setSelectedCategories(categories) {
6969
+ const prev = this._selectedCategories;
6970
+ this._selectedCategories = new Set(categories);
6971
+ let featuresChanged = false;
6972
+ for (const cat of prev) {
6973
+ if (!categories.has(cat)) {
6974
+ this._features.delete(cat);
6975
+ featuresChanged = true;
6976
+ }
6977
+ }
6978
+ if (featuresChanged)
6979
+ this._emitFeatures();
6980
+ const bbox = this._bbox;
6981
+ if (bbox) {
6982
+ for (const cat of categories) {
6983
+ if (!prev.has(cat))
6984
+ this._loadCategory(cat, bbox, this._queryToken);
6985
+ }
6986
+ }
6987
+ }
6988
+ // ── Subscriptions ─────────────────────────────────────────────────────────
6989
+ /**
6990
+ * Subscribe to available-category updates.
6991
+ * Replays the current value immediately, then fires on every bbox change.
6992
+ * @returns An unsubscribe function.
6993
+ */
6994
+ onAvailableCategories(cb) {
6995
+ cb([...this._availableCategories]);
6996
+ this._catListeners.add(cb);
6997
+ return () => this._catListeners.delete(cb);
6998
+ }
6999
+ /**
7000
+ * Subscribe to the feature map (category → GisFeature[]).
7001
+ * Replays the current value immediately, then fires whenever features change.
7002
+ * @returns An unsubscribe function.
7003
+ */
7004
+ onFeaturesChange(cb) {
7005
+ cb(new Map(this._features));
7006
+ this._featListeners.add(cb);
7007
+ return () => this._featListeners.delete(cb);
7008
+ }
7009
+ /**
7010
+ * Subscribe to the global loading state.
7011
+ * Fires `true` while the category list is being queried, `false` when done.
7012
+ * @returns An unsubscribe function.
7013
+ */
7014
+ onLoadingChange(cb) {
7015
+ this._loadListeners.add(cb);
7016
+ return () => this._loadListeners.delete(cb);
7017
+ }
7018
+ /** Cancel all pending requests and clear all listeners. */
7019
+ destroy() {
7020
+ if (this._debounceTimer !== null)
7021
+ clearTimeout(this._debounceTimer);
7022
+ this._queryToken++;
7023
+ this._catListeners.clear();
7024
+ this._featListeners.clear();
7025
+ this._loadListeners.clear();
7026
+ }
7027
+ // ── Private ───────────────────────────────────────────────────────────────
7028
+ _scheduleDebouncedQuery() {
7029
+ if (this._debounceTimer !== null)
7030
+ clearTimeout(this._debounceTimer);
7031
+ this._debounceTimer = setTimeout(() => this._queryLayers(), DEBOUNCE_MS);
7032
+ }
7033
+ _getGateway() {
7034
+ if (this._gatewayCache !== null && this._gatewayProjectId === this._projectId) {
7035
+ return this._gatewayCache;
7036
+ }
7037
+ const projectId = this._projectId;
7038
+ const regions = [...DEFAULT_LOCAL_SERVICE_REGIONS];
7039
+ if (projectId) {
7040
+ regions.unshift({
7041
+ name: "Cue SDK (authenticated)",
7042
+ coverageBBox: DENMARK_BBOX,
7043
+ supportedCategories: Object.keys(DEFAULT_DANISH_SDK_CATEGORY_MAP),
7044
+ priority: 10,
7045
+ adapter: new CueSdkGisAdapter({
7046
+ dataViewsBaseUrl: `${this._gatewayUrl}${DATA_VIEWS_PATH}`,
7047
+ getHeaders: async () => ({
7048
+ ...await this._getAuthHeaders(),
7049
+ "x-project-id": projectId
7050
+ })
7051
+ })
7052
+ });
7053
+ }
7054
+ this._gatewayCache = new GisGateway({ regions });
7055
+ this._gatewayProjectId = projectId;
7056
+ return this._gatewayCache;
7057
+ }
7058
+ async _queryLayers() {
7059
+ const bbox = this._bbox;
7060
+ if (!bbox)
7061
+ return;
7062
+ this._queryToken++;
7063
+ const token = this._queryToken;
7064
+ this._emitLoading(true);
7065
+ try {
7066
+ const descriptors = await this._getGateway().listFeatureCategoryDescriptors(bbox);
7067
+ if (token !== this._queryToken)
7068
+ return;
7069
+ this._availableCategories = descriptors;
7070
+ this._catListeners.forEach((cb) => cb([...descriptors]));
7071
+ const available = new Set(descriptors.map((d) => d.category));
7072
+ let changed = false;
7073
+ for (const cat of [...this._selectedCategories]) {
7074
+ if (!available.has(cat)) {
7075
+ this._selectedCategories.delete(cat);
7076
+ this._features.delete(cat);
7077
+ changed = true;
7078
+ }
7079
+ }
7080
+ if (changed)
7081
+ this._emitFeatures();
7082
+ for (const cat of this._selectedCategories) {
7083
+ this._loadCategory(cat, bbox, token);
7084
+ }
7085
+ } catch (err) {
7086
+ console.error("[CueGis] Failed to list categories:", err);
7087
+ } finally {
7088
+ if (token === this._queryToken)
7089
+ this._emitLoading(false);
7090
+ }
7091
+ }
7092
+ async _loadCategory(category, bbox, ownerToken) {
7093
+ const catToken = (this._categoryTokens.get(category) ?? 0) + 1;
7094
+ this._categoryTokens.set(category, catToken);
7095
+ try {
7096
+ const features = await this._getGateway().getFeaturesOfCategory(bbox, category);
7097
+ const stale = this._categoryTokens.get(category) !== catToken || ownerToken !== this._queryToken || !this._selectedCategories.has(category);
7098
+ if (stale)
7099
+ return;
7100
+ this._features.set(category, features);
7101
+ this._emitFeatures();
7102
+ } catch (err) {
7103
+ console.error(`[CueGis] Failed to load "${category}":`, err);
7104
+ }
7105
+ }
7106
+ _emitFeatures() {
7107
+ const snapshot = new Map(this._features);
7108
+ this._featListeners.forEach((cb) => cb(snapshot));
7109
+ }
7110
+ _emitLoading(v) {
7111
+ this._loadListeners.forEach((cb) => cb(v));
7112
+ }
7113
+ };
7114
+
5364
7115
  // libs/js/cue-sdk/src/lib/project.ts
5365
7116
  var import_firestore3 = require("firebase/firestore");
5366
7117
  var import_functions2 = require("firebase/functions");
@@ -5549,7 +7300,7 @@ var CueProfile = class {
5549
7300
  }
5550
7301
  /** Creates a new API key for the current user. */
5551
7302
  async createAPIKey(expiration) {
5552
- return this._fetch(ENDPOINT_PROFILE_API_KEYS, {
7303
+ return this._fetch(ENDPOINT_COMMANDS_PROFILE_API_KEYS, {
5553
7304
  method: "POST",
5554
7305
  headers: { "Content-Type": "application/json" },
5555
7306
  body: JSON.stringify({ expiration })
@@ -5558,7 +7309,7 @@ var CueProfile = class {
5558
7309
  /** Revokes the current user's API key. */
5559
7310
  async revokeAPIKey() {
5560
7311
  const response = await this._auth.authenticatedFetch(
5561
- this._url(ENDPOINT_PROFILE_API_KEYS),
7312
+ this._url(ENDPOINT_COMMANDS_PROFILE_API_KEYS),
5562
7313
  { method: "DELETE" }
5563
7314
  );
5564
7315
  if (!response.ok) {
@@ -5595,10 +7346,26 @@ var CueProfile = class {
5595
7346
  const res = await fn({ uids });
5596
7347
  return res.data;
5597
7348
  }
5598
- /** Record that the current user has accepted the terms of service. */
5599
- async acceptTerms() {
5600
- const fn = (0, import_functions3.httpsCallable)(this._functions, "acceptTerms");
5601
- await fn({});
7349
+ /** Record that the current user has accepted the terms of service. Sets a `terms` custom claim on the token. */
7350
+ async acceptTerms(version) {
7351
+ await this._fetch(ENDPOINT_COMMANDS_PROFILE_TERMS, {
7352
+ method: "POST",
7353
+ headers: { "Content-Type": "application/json" },
7354
+ body: JSON.stringify({ version })
7355
+ });
7356
+ }
7357
+ /**
7358
+ * Returns the terms version the current user has accepted (e.g. `"v1"`),
7359
+ * or `null` if they have not accepted any version yet.
7360
+ * Reads from the cached ID token — call after `acceptTerms()` with a
7361
+ * force-refreshed token to see the updated value.
7362
+ */
7363
+ async latestTermsAccepted() {
7364
+ const user = this._auth.currentUser;
7365
+ if (!user)
7366
+ return null;
7367
+ const result = await (0, import_auth4.getIdTokenResult)(user);
7368
+ return result.claims["terms"] ?? null;
5602
7369
  }
5603
7370
  };
5604
7371
 
@@ -7724,7 +9491,10 @@ function uploadedFileMetadata(originalName, projectId, userId, md5, providerId,
7724
9491
 
7725
9492
  // libs/js/cue-sdk/src/lib/sync.ts
7726
9493
  async function _fs2() {
7727
- return import("fs/promises");
9494
+ return import(
9495
+ /* webpackIgnore: true */
9496
+ "fs/promises"
9497
+ );
7728
9498
  }
7729
9499
  async function _readNodeFile(fullPath) {
7730
9500
  if (typeof window !== "undefined") {
@@ -7759,11 +9529,17 @@ async function _initWasm() {
7759
9529
  _scanFn = mod.scan;
7760
9530
  } else {
7761
9531
  const { readFile } = await _fs2();
7762
- const { join: join4 } = await import("path");
7763
- const { pathToFileURL } = await import("url");
9532
+ const { join: join4 } = await import(
9533
+ /* webpackIgnore: true */
9534
+ "path"
9535
+ );
9536
+ const { pathToFileURL } = await import(
9537
+ /* webpackIgnore: true */
9538
+ "url"
9539
+ );
7764
9540
  const wasmDir = join4(__dirname, "assets", "wasm");
7765
9541
  const wasmBinary = await readFile(join4(wasmDir, "dir_scanner_wasm_bg.wasm"));
7766
- const glueUrl = pathToFileURL(join4(wasmDir, "dir_scanner_wasm.mjs")).href;
9542
+ const glueUrl = pathToFileURL(join4(wasmDir, "dir_scanner_wasm.js")).href;
7767
9543
  const mod = await import(
7768
9544
  /* @vite-ignore */
7769
9545
  glueUrl
@@ -7772,12 +9548,18 @@ async function _initWasm() {
7772
9548
  _scanFn = mod.scan;
7773
9549
  }
7774
9550
  }
7775
- var DEFAULT_GRAPH_TYPE = "fuseki";
9551
+ var DEFAULT_GRAPH_TYPE = "qlever";
7776
9552
  var FSS_BATCH_CHUNK_SIZE = 1e3;
7777
9553
  var PENDING_LS_PREFIX = "cue:pending:";
7778
9554
  async function _pendingFilePath(spaceId) {
7779
- const { tmpdir } = await import("os");
7780
- const { join: join4 } = await import("path");
9555
+ const { tmpdir } = await import(
9556
+ /* webpackIgnore: true */
9557
+ "os"
9558
+ );
9559
+ const { join: join4 } = await import(
9560
+ /* webpackIgnore: true */
9561
+ "path"
9562
+ );
7781
9563
  return join4(tmpdir(), `cue-sync-pending-${spaceId}.json`);
7782
9564
  }
7783
9565
  async function _loadPending(spaceId) {
@@ -8412,6 +10194,27 @@ var Cue = class _Cue {
8412
10194
  _app;
8413
10195
  _endpoints;
8414
10196
  _isEmulator;
10197
+ _gis = null;
10198
+ /**
10199
+ * Reactive GIS service. Lazily constructed on first access.
10200
+ *
10201
+ * @example
10202
+ * ```ts
10203
+ * cue.gis.setProjectId('my-project');
10204
+ * cue.gis.onAvailableCategories(cats => ...);
10205
+ * cue.gis.setBbox([west, south, east, north]);
10206
+ * cue.gis.setSelectedCategories(new Set(['cadastre', 'building']));
10207
+ * ```
10208
+ */
10209
+ get gis() {
10210
+ if (!this._gis) {
10211
+ this._gis = new CueGis(
10212
+ () => this.api.getAuthHeaders(),
10213
+ this._endpoints.gatewayUrl
10214
+ );
10215
+ }
10216
+ return this._gis;
10217
+ }
8415
10218
  constructor(config = {}) {
8416
10219
  const usingDefaults = !config.apiKey && !config.appId && !config.measurementId;
8417
10220
  if (usingDefaults) {