@artsdatabanken/nbic-map-component 0.3.0 → 0.3.1

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/README.md CHANGED
@@ -167,4 +167,32 @@ export class AppComponent implements AfterViewInit {
167
167
  height: 60vh;
168
168
  width: 100%;
169
169
  }
170
+ ```
171
+
172
+ ### Place search
173
+ `map.searchPlaces(query, options?)` does a prefix search in [Geonorge stedsnavn](https://ws.geonorge.no/stedsnavn/v1/#/default/get_navn) straight from the browser and returns one `PlaceSearchResult` per spelling (Tromsø, Romsa and Tromssa are three results with the same `placeId`).
174
+
175
+ - `zoomLevel` suits the place type (a church gets closer than a fjord) and is computed for this map's view, so it can be passed straight to `setZoom`.
176
+ - `coordinates` are WGS84 `{ lat, lon }`.
177
+ - Options: `signal`, `limit` (default 10, max 500), `fuzzy`, `municipalityNumbers`, `countyNumbers`.
178
+ - Abort `signal` to cancel. The promise then rejects with an `AbortError`.
179
+
180
+ The zoom per type lives in `src/core/place-search/place-zoom.ts`.
181
+
182
+ Debounced search with RxJS:
183
+ ```ts
184
+ const places$ = term$.pipe(
185
+ debounceTime(300),
186
+ distinctUntilChanged(),
187
+ switchMap((term) => new Observable<PlaceSearchResult[]>((subscriber) => {
188
+ const controller = new AbortController();
189
+ map.searchPlaces(term, { signal: controller.signal })
190
+ .then((places) => { subscriber.next(places); subscriber.complete(); }, (err) => subscriber.error(err));
191
+ return () => controller.abort();
192
+ })),
193
+ );
194
+
195
+ // on select
196
+ map.setCenter(map.transformCoordsFrom([place.coordinates.lon, place.coordinates.lat], 'EPSG:4326', 'EPSG:25833'));
197
+ map.setZoom(place.zoomLevel);
170
198
  ```
package/dist/index.cjs CHANGED
@@ -145,6 +145,371 @@ var mapBounds = [
145
145
  }
146
146
  ];
147
147
 
148
+ // src/core/place-search/place-zoom.ts
149
+ var METERS_PER_PIXEL_AT_ZOOM_0 = 156543.03392804097;
150
+ var PLACE_ZOOM = {
151
+ point: 17,
152
+ // ~1.2 km across: buildings, single structures
153
+ small: 15,
154
+ // ~5 km: small natural features, farms' surroundings, neighbourhoods
155
+ medium: 13,
156
+ // ~19 km: lakes, mountains, valleys, towns
157
+ large: 11.5,
158
+ // ~54 km: fjords, mountain areas, municipalities
159
+ region: 9,
160
+ // ~300 km: counties, sea areas
161
+ country: 6
162
+ // ~2 400 km
163
+ };
164
+ var DEFAULT_ZOOM_CLASS = "small";
165
+ var TYPES_BY_CLASS = {
166
+ point: [
167
+ "Adressetilleggsnavn",
168
+ "Annen bygning for religionsut\xF8velse",
169
+ "Annen industri- og lagerbygning",
170
+ "Annen kulturdetalj",
171
+ "Badeplass",
172
+ "Barnehage",
173
+ "Boinstitusjon",
174
+ "Boligblokk",
175
+ "Bomstasjon",
176
+ "Borettslag",
177
+ "Bru",
178
+ "Bruk",
179
+ "Brygge",
180
+ "Busstasjon",
181
+ "Busstopp",
182
+ "Bygg for jordbruk, fiske og fangst",
183
+ "B\xE5ke",
184
+ "Campingplass",
185
+ "Dam",
186
+ "Eiendom",
187
+ "Eiendomsteig",
188
+ "Enebolig/mindre boligbygg",
189
+ "Fabrikk",
190
+ "Fengsel",
191
+ "Ferjekai",
192
+ "Fjell i dagen",
193
+ "Fjellheis",
194
+ "Fl\xF8tningsanlegg",
195
+ "Forn\xF8yelsespark",
196
+ "Forretningsbygg",
197
+ "Forsamlingshus/kulturhus",
198
+ "Forskningsstasjon",
199
+ "Foss",
200
+ "Fritidsbolig",
201
+ "Fyllplass",
202
+ "Fyrlykt",
203
+ "Fyrstasjon",
204
+ "Gammel bosettingsplass",
205
+ "Garasje/hangarbygg",
206
+ "Gard",
207
+ "Gjerde",
208
+ "Gravplass",
209
+ "Grensemerke",
210
+ "Grind",
211
+ "Grotte",
212
+ "Grustak/steinbrudd",
213
+ "Gr\xF8ft",
214
+ "Helikopterlandingsplass",
215
+ "Heller",
216
+ "Helseinstitusjon",
217
+ "Historisk bosetting",
218
+ "Holdeplass",
219
+ "Hotell",
220
+ "H\xF8l",
221
+ "Idrettsanlegg",
222
+ "Idrettshall",
223
+ "Jernbanebru",
224
+ "Jernstang",
225
+ "Jorde",
226
+ "Kai",
227
+ "Kilde",
228
+ "Kirke",
229
+ "Klopp",
230
+ "Kraftstasjon",
231
+ "Kultur-/messehall",
232
+ "Landingsplass",
233
+ "Lanterne",
234
+ "Lysb\xF8ye",
235
+ "Matrikkeladressenavn",
236
+ "Melkeplass",
237
+ "Milit\xE6rt bygg/anlegg",
238
+ "Mindre brukonstruksjon",
239
+ "Molo",
240
+ "Museum/galleri/bibliotek",
241
+ "Navnegard",
242
+ "Offersted",
243
+ "Oppdrettsanlegg",
244
+ "Overbygg",
245
+ "Overett",
246
+ "Park",
247
+ "Parkeringsplass",
248
+ "Pensjonat",
249
+ "Pytt",
250
+ "Rasteplass",
251
+ "R\xE5dhus",
252
+ "Serveringssted",
253
+ "Seter/st\xF8l",
254
+ "Setervoll",
255
+ "Severdighet",
256
+ "Sj\xF8merke med indirekte belysning",
257
+ "Sj\xF8varde",
258
+ "Skiheis",
259
+ "Skogholt",
260
+ "Skole",
261
+ "Skytebane",
262
+ "Sluse",
263
+ "Sm\xE5b\xE5thavn",
264
+ "Stake",
265
+ "Stasjon",
266
+ "Stein",
267
+ "Stryk",
268
+ "St\xF8",
269
+ "St\xF8ls-/setereiendom",
270
+ "Sva",
271
+ "Sykehus",
272
+ "Torg",
273
+ "Torvtak",
274
+ "Tuft",
275
+ "Turisthytte",
276
+ "TV-/radio- eller mobiltelefont\xE5rn",
277
+ "T\xF8mmervelte",
278
+ "Universitet/h\xF8gskole",
279
+ "Utsiktspunkt",
280
+ "Utstikker",
281
+ "Vad",
282
+ "Vaktstasjon/beredsskapsbygning",
283
+ "Vannstandsm\xE5ler",
284
+ "Vannverk",
285
+ "Varde",
286
+ "Vegbom",
287
+ "Vegkryss",
288
+ "Vegsving",
289
+ "\xD8yr"
290
+ ],
291
+ small: [
292
+ "Administrativ bydel",
293
+ "Adressenavn",
294
+ "Alpinanlegg",
295
+ "Ankringsplass",
296
+ "Annen terrengdetalj",
297
+ "Annen vanndetalj",
298
+ "Bakke",
299
+ "Bakke (Veg)",
300
+ "Bakke i sj\xF8",
301
+ "Bakketopp i sj\xF8",
302
+ "Bekk",
303
+ "Berg",
304
+ "Bergverk",
305
+ "Boligfelt",
306
+ "Botn",
307
+ "Bydel",
308
+ "B\xE5e",
309
+ "B\xE5e i sj\xF8",
310
+ "Del av innsj\xF8",
311
+ "Del av vann",
312
+ "Egg",
313
+ "Eid",
314
+ "Eid i sj\xF8",
315
+ "Elvemel",
316
+ "Elvesving",
317
+ "Eng",
318
+ "Fiskeplass i sj\xF8",
319
+ "Fjellkant",
320
+ "Fjellside",
321
+ "Fjelltopp i sj\xF8",
322
+ "Fjellvegg",
323
+ "Flyplass",
324
+ "Fonn",
325
+ "Friluftsomr\xE5de",
326
+ "Grend",
327
+ "Grunne",
328
+ "Grunne i sj\xF8",
329
+ "Grunnkrets",
330
+ "Gruppe av tjern",
331
+ "Hammar",
332
+ "Haug",
333
+ "Havn",
334
+ "Havnehage",
335
+ "Holme",
336
+ "Holme i sj\xF8",
337
+ "Holmegruppe i sj\xF8",
338
+ "Hylle",
339
+ "Hyttefelt",
340
+ "H\xF8yde",
341
+ "Industriomr\xE5de",
342
+ "Jernbanetunnel",
343
+ "Juv",
344
+ "Kabel",
345
+ "Kanal",
346
+ "Klakk i sj\xF8",
347
+ "Korallrev",
348
+ "Kraftgate (r\xF8rgate)",
349
+ "Krater",
350
+ "Li",
351
+ "Lon",
352
+ "Mo",
353
+ "Myr",
354
+ "Nes",
355
+ "Nes i sj\xF8",
356
+ "Nes ved elver",
357
+ "Oljeinstallasjon",
358
+ "Os",
359
+ "Rev i sj\xF8",
360
+ "Rygg",
361
+ "Sand",
362
+ "Senkning",
363
+ "Sj\xF8detalj",
364
+ "Skar",
365
+ "Skj\xE6r",
366
+ "Skj\xE6r i sj\xF8",
367
+ "Skog",
368
+ "Skredomr\xE5de",
369
+ "Slette",
370
+ "Sti",
371
+ "Strand",
372
+ "Strand i sj\xF8",
373
+ "Stup",
374
+ "Sund",
375
+ "Sund i sj\xF8",
376
+ "S\xF8kk",
377
+ "Taubane",
378
+ "Tettbebyggelse",
379
+ "Tettsteddel",
380
+ "Tjern",
381
+ "Topp",
382
+ "Traktorveg",
383
+ "Tunnel",
384
+ "Ur",
385
+ "Utmark",
386
+ "Vann",
387
+ "Vik",
388
+ "Vik i sj\xF8",
389
+ "V\xE5g i sj\xF8",
390
+ "V\xE5tmarksomr\xE5de",
391
+ "\xD8y",
392
+ "\xC5s"
393
+ ],
394
+ medium: [
395
+ "Allmenning",
396
+ "Annen administrativ inndeling",
397
+ "Banestrekning",
398
+ "Banke",
399
+ "By",
400
+ "Bygdelag (bygd)",
401
+ "Dal",
402
+ "Egg i sj\xF8",
403
+ "Elv",
404
+ "Farled/skipslei",
405
+ "Ferjestrekning",
406
+ "Fjell",
407
+ "Fjellovergang",
408
+ "Fjordmunning",
409
+ "Gass-/oljefelt i sj\xF8",
410
+ "Gruppe av vann",
411
+ "Halv\xF8y",
412
+ "Halv\xF8y i sj\xF8",
413
+ "Hei",
414
+ "Hylle i sj\xF8",
415
+ "Innsj\xF8",
416
+ "Isbre",
417
+ "Kraftledning",
418
+ "Landskapsomr\xE5de",
419
+ "Morenerygg i sj\xF8",
420
+ "Poststed",
421
+ "Ras i sj\xF8",
422
+ "Renne/Kl\xF8ft i sj\xF8",
423
+ "Rygg i sj\xF8",
424
+ "R\xF8rledning",
425
+ "Sadel i sj\xF8",
426
+ "Sj\xF8stykke",
427
+ "Skogomr\xE5de",
428
+ "Skolekrets",
429
+ "Skytefelt",
430
+ "Sokn",
431
+ "Statistisk tettsted",
432
+ "S\xF8kk i sj\xF8",
433
+ "Tettsted",
434
+ "Undersj\xF8isk vegg",
435
+ "Valgkrets",
436
+ "Vegstrekning",
437
+ "Verneomr\xE5de",
438
+ "Vulkan i sj\xF8",
439
+ "\xD8y i sj\xF8",
440
+ "\xD8ygruppe"
441
+ ],
442
+ large: [
443
+ "Banke i sj\xF8",
444
+ "Basseng i sj\xF8",
445
+ "Dalf\xF8re",
446
+ "Fjellkjede i sj\xF8",
447
+ "Fjellomr\xE5de",
448
+ "Fjord",
449
+ "Geologisk struktur",
450
+ "Havdyp",
451
+ "Iskuppel",
452
+ "Kommune",
453
+ "Plat\xE5 i sj\xF8",
454
+ "Reinbeitedistrikt",
455
+ "Sokkel i sj\xF8",
456
+ "Vidde",
457
+ "\xD8ygruppe i sj\xF8"
458
+ ],
459
+ region: ["Fylke", "Havomr\xE5de", "Havstr\xF8m", "Kontinentalsokkel", "Soneinndeling til havs"],
460
+ country: ["Nasjon"]
461
+ };
462
+ var CLASS_BY_TYPE = new Map(
463
+ Object.entries(TYPES_BY_CLASS).flatMap(
464
+ ([cls, types]) => types.map((t) => [t, cls])
465
+ )
466
+ );
467
+ function zoomClassForType(type) {
468
+ return CLASS_BY_TYPE.get(type.trim()) ?? DEFAULT_ZOOM_CLASS;
469
+ }
470
+ function metersPerPixelForType(type) {
471
+ return METERS_PER_PIXEL_AT_ZOOM_0 / 2 ** PLACE_ZOOM[zoomClassForType(type)];
472
+ }
473
+
474
+ // src/core/place-search/geonorge.ts
475
+ var GEONORGE_NAVN_URL = "https://ws.geonorge.no/stedsnavn/v1/navn";
476
+ var DEFAULT_LIMIT = 10;
477
+ var MAX_LIMIT = 500;
478
+ var REJECTED_CHARACTERS = /["?;%\\]/g;
479
+ async function searchGeonorgePlaces(query, options = {}) {
480
+ options.signal?.throwIfAborted();
481
+ const term = query.replace(REJECTED_CHARACTERS, "").trim();
482
+ if (!term.replace(/\*/g, "")) return [];
483
+ const url = new URL(GEONORGE_NAVN_URL);
484
+ const params = url.searchParams;
485
+ params.set("sok", term.endsWith("*") ? term : `${term}*`);
486
+ params.set("utkoordsys", "4326");
487
+ params.set("treffPerSide", String(Math.min(Math.max(Math.trunc(options.limit ?? DEFAULT_LIMIT), 1), MAX_LIMIT)));
488
+ if (options.fuzzy) params.set("fuzzy", "true");
489
+ for (const knr of options.municipalityNumbers ?? []) params.append("knr", knr);
490
+ for (const fnr of options.countyNumbers ?? []) params.append("fnr", fnr);
491
+ const response = await fetch(url, { signal: options.signal });
492
+ if (!response.ok) {
493
+ throw new Error(`Geonorge place search failed: ${response.status} ${response.statusText}`);
494
+ }
495
+ const body = await response.json();
496
+ return (body.navn ?? []).map(toPlaceHit);
497
+ }
498
+ function toPlaceHit(navn) {
499
+ const type = navn.navneobjekttype.trim();
500
+ return {
501
+ id: `${navn.stedsnummer}-${navn.spr\u00E5k}-${navn.skrivem\u00E5te}`,
502
+ placeId: String(navn.stedsnummer),
503
+ name: navn.skrivem\u00E5te,
504
+ language: navn.spr\u00E5k,
505
+ type,
506
+ municipalities: navn.kommuner?.map((k) => k.kommunenavn.trim()) ?? [],
507
+ counties: navn.fylker?.map((f) => f.fylkesnavn.trim()) ?? [],
508
+ coordinates: { lat: navn.representasjonspunkt.nord, lon: navn.representasjonspunkt.\u00F8st },
509
+ targetMetersPerPixel: metersPerPixelForType(type)
510
+ };
511
+ }
512
+
148
513
  // src/core/state/store.ts
149
514
  function createEmitter() {
150
515
  const listeners = {};
@@ -2418,6 +2783,16 @@ var SelectController = class {
2418
2783
  }
2419
2784
  };
2420
2785
 
2786
+ // src/core/ol/zoom/ground-resolution.ts
2787
+ var import_proj6 = require("ol/proj");
2788
+ var import_math = require("ol/math");
2789
+ function zoomForGroundResolution(view, lonLat, metersPerPixel) {
2790
+ const projection = view.getProjection();
2791
+ const groundMetersPerUnit = (0, import_proj6.getPointResolution)(projection, 1, (0, import_proj6.fromLonLat)(lonLat, projection), "m");
2792
+ const zoom = view.getZoomForResolution(metersPerPixel / groundMetersPerUnit) ?? view.getMinZoom();
2793
+ return (0, import_math.clamp)(zoom, view.getMinZoom(), view.getMaxZoom());
2794
+ }
2795
+
2421
2796
  // src/core/ol/OlMapEngine.ts
2422
2797
  function resolveVectorSource(layer) {
2423
2798
  const src = layer.getSource();
@@ -2843,6 +3218,9 @@ function createOlEngine(events) {
2843
3218
  },
2844
3219
  getCenterFromExtent: (extent) => {
2845
3220
  return (0, import_extent6.getCenter)(extent);
3221
+ },
3222
+ getZoomForGroundResolution: (lonLat, metersPerPixel) => {
3223
+ return zoomForGroundResolution(map.getView(), lonLat, metersPerPixel);
2846
3224
  }
2847
3225
  };
2848
3226
  }
@@ -3090,6 +3468,21 @@ var MapAPI = class {
3090
3468
  getCenterFromExtent(extent) {
3091
3469
  return this.engine.getCenterFromExtent(extent);
3092
3470
  }
3471
+ // Place search
3472
+ /**
3473
+ * Prefix search for place names in Geonorge (https://ws.geonorge.no/stedsnavn/v1/#/default/get_navn).
3474
+ * `zoomLevel` is computed for this map's view. Abort `options.signal` to cancel.
3475
+ */
3476
+ async searchPlaces(query, options) {
3477
+ const places = await searchGeonorgePlaces(query, options);
3478
+ return places.map(({ targetMetersPerPixel, ...place }) => ({
3479
+ ...place,
3480
+ zoomLevel: this.engine.getZoomForGroundResolution(
3481
+ [place.coordinates.lon, place.coordinates.lat],
3482
+ targetMetersPerPixel
3483
+ )
3484
+ }));
3485
+ }
3093
3486
  };
3094
3487
 
3095
3488
  // src/api/config.schema.ts
package/dist/index.d.cts CHANGED
@@ -314,6 +314,43 @@ interface InsertGeomOptions {
314
314
  /** CRS of the incoming coordinates. If omitted, assume map/view projection. */
315
315
  dataProjection?: string;
316
316
  }
317
+ interface PlaceSearchOptions {
318
+ /** Abort to cancel, e.g. while the user is still typing. The promise then rejects with an AbortError. */
319
+ signal?: AbortSignal;
320
+ /** Max number of results, 1–500. Default: 10 */
321
+ limit?: number;
322
+ /** Also match similar spellings, e.g. "Galdhopiggen" finds Galdhøpiggen. */
323
+ fuzzy?: boolean;
324
+ /** Only search in these municipalities (kommunenummer), e.g. ['5501']. */
325
+ municipalityNumbers?: string[];
326
+ /** Only search in these counties (fylkesnummer), e.g. ['55']. */
327
+ countyNumbers?: string[];
328
+ }
329
+ /**
330
+ * One spelling of a place name from Geonorge. A place with several spellings
331
+ * (e.g. Tromsø / Romsa / Tromssa) gives one result per spelling, sharing `placeId`.
332
+ */
333
+ interface PlaceSearchResult {
334
+ /** Unique per result. */
335
+ id: string;
336
+ /** Geonorge stedsnummer, shared by all spellings of a place. */
337
+ placeId: string;
338
+ name: string;
339
+ /** e.g. 'Norsk', 'Nordsamisk', 'Kvensk' */
340
+ language: string;
341
+ /** Geonorge navneobjekttype, e.g. 'Fjord', 'By' */
342
+ type: string;
343
+ /** As Geonorge delivers them, including Sami/Kven variants. Empty for e.g. sea areas. */
344
+ municipalities: string[];
345
+ counties: string[];
346
+ /** WGS84 */
347
+ coordinates: {
348
+ lat: number;
349
+ lon: number;
350
+ };
351
+ /** Zoom for this map's view that suits the place type. */
352
+ zoomLevel: number;
353
+ }
317
354
 
318
355
  type MapEventMap = {
319
356
  ready: void;
@@ -542,6 +579,7 @@ interface MapEngine {
542
579
  } | null;
543
580
  getTargetElement(): HTMLElement | null;
544
581
  getCenterFromExtent(extent: Extent): MapCoord;
582
+ getZoomForGroundResolution(lonLat: [number, number], metersPerPixel: number): number;
545
583
  }
546
584
 
547
585
  declare class MapAPI {
@@ -641,6 +679,11 @@ declare class MapAPI {
641
679
  } | null;
642
680
  getTargetElement(): HTMLElement | null;
643
681
  getCenterFromExtent(extent: Extent): MapCoord;
682
+ /**
683
+ * Prefix search for place names in Geonorge (https://ws.geonorge.no/stedsnavn/v1/#/default/get_navn).
684
+ * `zoomLevel` is computed for this map's view. Abort `options.signal` to cancel.
685
+ */
686
+ searchPlaces(query: string, options?: PlaceSearchOptions): Promise<PlaceSearchResult[]>;
644
687
  }
645
688
 
646
689
  declare function createMap(target: string | HTMLElement, rawConfig: unknown): MapAPI;
@@ -802,4 +845,4 @@ interface NibTokenManager {
802
845
  */
803
846
  declare function createNibToken(config?: NibTokenConfig): NibTokenManager;
804
847
 
805
- export { type LayerDef, type MapEventName, type MapEventPayload, MapEvents, type MapInit, MapAPI as NbicMapComponent, type NibToken, type NibTokenConfig, type NibTokenManager, createMap, createNibToken, index$2 as nbicMapGeojson, index as nbicMapPresets, index$1 as nbicMapUtils };
848
+ export { type LayerDef, type MapEventName, type MapEventPayload, MapEvents, type MapInit, MapAPI as NbicMapComponent, type NibToken, type NibTokenConfig, type NibTokenManager, type PlaceSearchOptions, type PlaceSearchResult, createMap, createNibToken, index$2 as nbicMapGeojson, index as nbicMapPresets, index$1 as nbicMapUtils };
package/dist/index.d.ts CHANGED
@@ -314,6 +314,43 @@ interface InsertGeomOptions {
314
314
  /** CRS of the incoming coordinates. If omitted, assume map/view projection. */
315
315
  dataProjection?: string;
316
316
  }
317
+ interface PlaceSearchOptions {
318
+ /** Abort to cancel, e.g. while the user is still typing. The promise then rejects with an AbortError. */
319
+ signal?: AbortSignal;
320
+ /** Max number of results, 1–500. Default: 10 */
321
+ limit?: number;
322
+ /** Also match similar spellings, e.g. "Galdhopiggen" finds Galdhøpiggen. */
323
+ fuzzy?: boolean;
324
+ /** Only search in these municipalities (kommunenummer), e.g. ['5501']. */
325
+ municipalityNumbers?: string[];
326
+ /** Only search in these counties (fylkesnummer), e.g. ['55']. */
327
+ countyNumbers?: string[];
328
+ }
329
+ /**
330
+ * One spelling of a place name from Geonorge. A place with several spellings
331
+ * (e.g. Tromsø / Romsa / Tromssa) gives one result per spelling, sharing `placeId`.
332
+ */
333
+ interface PlaceSearchResult {
334
+ /** Unique per result. */
335
+ id: string;
336
+ /** Geonorge stedsnummer, shared by all spellings of a place. */
337
+ placeId: string;
338
+ name: string;
339
+ /** e.g. 'Norsk', 'Nordsamisk', 'Kvensk' */
340
+ language: string;
341
+ /** Geonorge navneobjekttype, e.g. 'Fjord', 'By' */
342
+ type: string;
343
+ /** As Geonorge delivers them, including Sami/Kven variants. Empty for e.g. sea areas. */
344
+ municipalities: string[];
345
+ counties: string[];
346
+ /** WGS84 */
347
+ coordinates: {
348
+ lat: number;
349
+ lon: number;
350
+ };
351
+ /** Zoom for this map's view that suits the place type. */
352
+ zoomLevel: number;
353
+ }
317
354
 
318
355
  type MapEventMap = {
319
356
  ready: void;
@@ -542,6 +579,7 @@ interface MapEngine {
542
579
  } | null;
543
580
  getTargetElement(): HTMLElement | null;
544
581
  getCenterFromExtent(extent: Extent): MapCoord;
582
+ getZoomForGroundResolution(lonLat: [number, number], metersPerPixel: number): number;
545
583
  }
546
584
 
547
585
  declare class MapAPI {
@@ -641,6 +679,11 @@ declare class MapAPI {
641
679
  } | null;
642
680
  getTargetElement(): HTMLElement | null;
643
681
  getCenterFromExtent(extent: Extent): MapCoord;
682
+ /**
683
+ * Prefix search for place names in Geonorge (https://ws.geonorge.no/stedsnavn/v1/#/default/get_navn).
684
+ * `zoomLevel` is computed for this map's view. Abort `options.signal` to cancel.
685
+ */
686
+ searchPlaces(query: string, options?: PlaceSearchOptions): Promise<PlaceSearchResult[]>;
644
687
  }
645
688
 
646
689
  declare function createMap(target: string | HTMLElement, rawConfig: unknown): MapAPI;
@@ -802,4 +845,4 @@ interface NibTokenManager {
802
845
  */
803
846
  declare function createNibToken(config?: NibTokenConfig): NibTokenManager;
804
847
 
805
- export { type LayerDef, type MapEventName, type MapEventPayload, MapEvents, type MapInit, MapAPI as NbicMapComponent, type NibToken, type NibTokenConfig, type NibTokenManager, createMap, createNibToken, index$2 as nbicMapGeojson, index as nbicMapPresets, index$1 as nbicMapUtils };
848
+ export { type LayerDef, type MapEventName, type MapEventPayload, MapEvents, type MapInit, MapAPI as NbicMapComponent, type NibToken, type NibTokenConfig, type NibTokenManager, type PlaceSearchOptions, type PlaceSearchResult, createMap, createNibToken, index$2 as nbicMapGeojson, index as nbicMapPresets, index$1 as nbicMapUtils };
package/dist/index.js CHANGED
@@ -61,6 +61,371 @@ var mapBounds = [
61
61
  }
62
62
  ];
63
63
 
64
+ // src/core/place-search/place-zoom.ts
65
+ var METERS_PER_PIXEL_AT_ZOOM_0 = 156543.03392804097;
66
+ var PLACE_ZOOM = {
67
+ point: 17,
68
+ // ~1.2 km across: buildings, single structures
69
+ small: 15,
70
+ // ~5 km: small natural features, farms' surroundings, neighbourhoods
71
+ medium: 13,
72
+ // ~19 km: lakes, mountains, valleys, towns
73
+ large: 11.5,
74
+ // ~54 km: fjords, mountain areas, municipalities
75
+ region: 9,
76
+ // ~300 km: counties, sea areas
77
+ country: 6
78
+ // ~2 400 km
79
+ };
80
+ var DEFAULT_ZOOM_CLASS = "small";
81
+ var TYPES_BY_CLASS = {
82
+ point: [
83
+ "Adressetilleggsnavn",
84
+ "Annen bygning for religionsut\xF8velse",
85
+ "Annen industri- og lagerbygning",
86
+ "Annen kulturdetalj",
87
+ "Badeplass",
88
+ "Barnehage",
89
+ "Boinstitusjon",
90
+ "Boligblokk",
91
+ "Bomstasjon",
92
+ "Borettslag",
93
+ "Bru",
94
+ "Bruk",
95
+ "Brygge",
96
+ "Busstasjon",
97
+ "Busstopp",
98
+ "Bygg for jordbruk, fiske og fangst",
99
+ "B\xE5ke",
100
+ "Campingplass",
101
+ "Dam",
102
+ "Eiendom",
103
+ "Eiendomsteig",
104
+ "Enebolig/mindre boligbygg",
105
+ "Fabrikk",
106
+ "Fengsel",
107
+ "Ferjekai",
108
+ "Fjell i dagen",
109
+ "Fjellheis",
110
+ "Fl\xF8tningsanlegg",
111
+ "Forn\xF8yelsespark",
112
+ "Forretningsbygg",
113
+ "Forsamlingshus/kulturhus",
114
+ "Forskningsstasjon",
115
+ "Foss",
116
+ "Fritidsbolig",
117
+ "Fyllplass",
118
+ "Fyrlykt",
119
+ "Fyrstasjon",
120
+ "Gammel bosettingsplass",
121
+ "Garasje/hangarbygg",
122
+ "Gard",
123
+ "Gjerde",
124
+ "Gravplass",
125
+ "Grensemerke",
126
+ "Grind",
127
+ "Grotte",
128
+ "Grustak/steinbrudd",
129
+ "Gr\xF8ft",
130
+ "Helikopterlandingsplass",
131
+ "Heller",
132
+ "Helseinstitusjon",
133
+ "Historisk bosetting",
134
+ "Holdeplass",
135
+ "Hotell",
136
+ "H\xF8l",
137
+ "Idrettsanlegg",
138
+ "Idrettshall",
139
+ "Jernbanebru",
140
+ "Jernstang",
141
+ "Jorde",
142
+ "Kai",
143
+ "Kilde",
144
+ "Kirke",
145
+ "Klopp",
146
+ "Kraftstasjon",
147
+ "Kultur-/messehall",
148
+ "Landingsplass",
149
+ "Lanterne",
150
+ "Lysb\xF8ye",
151
+ "Matrikkeladressenavn",
152
+ "Melkeplass",
153
+ "Milit\xE6rt bygg/anlegg",
154
+ "Mindre brukonstruksjon",
155
+ "Molo",
156
+ "Museum/galleri/bibliotek",
157
+ "Navnegard",
158
+ "Offersted",
159
+ "Oppdrettsanlegg",
160
+ "Overbygg",
161
+ "Overett",
162
+ "Park",
163
+ "Parkeringsplass",
164
+ "Pensjonat",
165
+ "Pytt",
166
+ "Rasteplass",
167
+ "R\xE5dhus",
168
+ "Serveringssted",
169
+ "Seter/st\xF8l",
170
+ "Setervoll",
171
+ "Severdighet",
172
+ "Sj\xF8merke med indirekte belysning",
173
+ "Sj\xF8varde",
174
+ "Skiheis",
175
+ "Skogholt",
176
+ "Skole",
177
+ "Skytebane",
178
+ "Sluse",
179
+ "Sm\xE5b\xE5thavn",
180
+ "Stake",
181
+ "Stasjon",
182
+ "Stein",
183
+ "Stryk",
184
+ "St\xF8",
185
+ "St\xF8ls-/setereiendom",
186
+ "Sva",
187
+ "Sykehus",
188
+ "Torg",
189
+ "Torvtak",
190
+ "Tuft",
191
+ "Turisthytte",
192
+ "TV-/radio- eller mobiltelefont\xE5rn",
193
+ "T\xF8mmervelte",
194
+ "Universitet/h\xF8gskole",
195
+ "Utsiktspunkt",
196
+ "Utstikker",
197
+ "Vad",
198
+ "Vaktstasjon/beredsskapsbygning",
199
+ "Vannstandsm\xE5ler",
200
+ "Vannverk",
201
+ "Varde",
202
+ "Vegbom",
203
+ "Vegkryss",
204
+ "Vegsving",
205
+ "\xD8yr"
206
+ ],
207
+ small: [
208
+ "Administrativ bydel",
209
+ "Adressenavn",
210
+ "Alpinanlegg",
211
+ "Ankringsplass",
212
+ "Annen terrengdetalj",
213
+ "Annen vanndetalj",
214
+ "Bakke",
215
+ "Bakke (Veg)",
216
+ "Bakke i sj\xF8",
217
+ "Bakketopp i sj\xF8",
218
+ "Bekk",
219
+ "Berg",
220
+ "Bergverk",
221
+ "Boligfelt",
222
+ "Botn",
223
+ "Bydel",
224
+ "B\xE5e",
225
+ "B\xE5e i sj\xF8",
226
+ "Del av innsj\xF8",
227
+ "Del av vann",
228
+ "Egg",
229
+ "Eid",
230
+ "Eid i sj\xF8",
231
+ "Elvemel",
232
+ "Elvesving",
233
+ "Eng",
234
+ "Fiskeplass i sj\xF8",
235
+ "Fjellkant",
236
+ "Fjellside",
237
+ "Fjelltopp i sj\xF8",
238
+ "Fjellvegg",
239
+ "Flyplass",
240
+ "Fonn",
241
+ "Friluftsomr\xE5de",
242
+ "Grend",
243
+ "Grunne",
244
+ "Grunne i sj\xF8",
245
+ "Grunnkrets",
246
+ "Gruppe av tjern",
247
+ "Hammar",
248
+ "Haug",
249
+ "Havn",
250
+ "Havnehage",
251
+ "Holme",
252
+ "Holme i sj\xF8",
253
+ "Holmegruppe i sj\xF8",
254
+ "Hylle",
255
+ "Hyttefelt",
256
+ "H\xF8yde",
257
+ "Industriomr\xE5de",
258
+ "Jernbanetunnel",
259
+ "Juv",
260
+ "Kabel",
261
+ "Kanal",
262
+ "Klakk i sj\xF8",
263
+ "Korallrev",
264
+ "Kraftgate (r\xF8rgate)",
265
+ "Krater",
266
+ "Li",
267
+ "Lon",
268
+ "Mo",
269
+ "Myr",
270
+ "Nes",
271
+ "Nes i sj\xF8",
272
+ "Nes ved elver",
273
+ "Oljeinstallasjon",
274
+ "Os",
275
+ "Rev i sj\xF8",
276
+ "Rygg",
277
+ "Sand",
278
+ "Senkning",
279
+ "Sj\xF8detalj",
280
+ "Skar",
281
+ "Skj\xE6r",
282
+ "Skj\xE6r i sj\xF8",
283
+ "Skog",
284
+ "Skredomr\xE5de",
285
+ "Slette",
286
+ "Sti",
287
+ "Strand",
288
+ "Strand i sj\xF8",
289
+ "Stup",
290
+ "Sund",
291
+ "Sund i sj\xF8",
292
+ "S\xF8kk",
293
+ "Taubane",
294
+ "Tettbebyggelse",
295
+ "Tettsteddel",
296
+ "Tjern",
297
+ "Topp",
298
+ "Traktorveg",
299
+ "Tunnel",
300
+ "Ur",
301
+ "Utmark",
302
+ "Vann",
303
+ "Vik",
304
+ "Vik i sj\xF8",
305
+ "V\xE5g i sj\xF8",
306
+ "V\xE5tmarksomr\xE5de",
307
+ "\xD8y",
308
+ "\xC5s"
309
+ ],
310
+ medium: [
311
+ "Allmenning",
312
+ "Annen administrativ inndeling",
313
+ "Banestrekning",
314
+ "Banke",
315
+ "By",
316
+ "Bygdelag (bygd)",
317
+ "Dal",
318
+ "Egg i sj\xF8",
319
+ "Elv",
320
+ "Farled/skipslei",
321
+ "Ferjestrekning",
322
+ "Fjell",
323
+ "Fjellovergang",
324
+ "Fjordmunning",
325
+ "Gass-/oljefelt i sj\xF8",
326
+ "Gruppe av vann",
327
+ "Halv\xF8y",
328
+ "Halv\xF8y i sj\xF8",
329
+ "Hei",
330
+ "Hylle i sj\xF8",
331
+ "Innsj\xF8",
332
+ "Isbre",
333
+ "Kraftledning",
334
+ "Landskapsomr\xE5de",
335
+ "Morenerygg i sj\xF8",
336
+ "Poststed",
337
+ "Ras i sj\xF8",
338
+ "Renne/Kl\xF8ft i sj\xF8",
339
+ "Rygg i sj\xF8",
340
+ "R\xF8rledning",
341
+ "Sadel i sj\xF8",
342
+ "Sj\xF8stykke",
343
+ "Skogomr\xE5de",
344
+ "Skolekrets",
345
+ "Skytefelt",
346
+ "Sokn",
347
+ "Statistisk tettsted",
348
+ "S\xF8kk i sj\xF8",
349
+ "Tettsted",
350
+ "Undersj\xF8isk vegg",
351
+ "Valgkrets",
352
+ "Vegstrekning",
353
+ "Verneomr\xE5de",
354
+ "Vulkan i sj\xF8",
355
+ "\xD8y i sj\xF8",
356
+ "\xD8ygruppe"
357
+ ],
358
+ large: [
359
+ "Banke i sj\xF8",
360
+ "Basseng i sj\xF8",
361
+ "Dalf\xF8re",
362
+ "Fjellkjede i sj\xF8",
363
+ "Fjellomr\xE5de",
364
+ "Fjord",
365
+ "Geologisk struktur",
366
+ "Havdyp",
367
+ "Iskuppel",
368
+ "Kommune",
369
+ "Plat\xE5 i sj\xF8",
370
+ "Reinbeitedistrikt",
371
+ "Sokkel i sj\xF8",
372
+ "Vidde",
373
+ "\xD8ygruppe i sj\xF8"
374
+ ],
375
+ region: ["Fylke", "Havomr\xE5de", "Havstr\xF8m", "Kontinentalsokkel", "Soneinndeling til havs"],
376
+ country: ["Nasjon"]
377
+ };
378
+ var CLASS_BY_TYPE = new Map(
379
+ Object.entries(TYPES_BY_CLASS).flatMap(
380
+ ([cls, types]) => types.map((t) => [t, cls])
381
+ )
382
+ );
383
+ function zoomClassForType(type) {
384
+ return CLASS_BY_TYPE.get(type.trim()) ?? DEFAULT_ZOOM_CLASS;
385
+ }
386
+ function metersPerPixelForType(type) {
387
+ return METERS_PER_PIXEL_AT_ZOOM_0 / 2 ** PLACE_ZOOM[zoomClassForType(type)];
388
+ }
389
+
390
+ // src/core/place-search/geonorge.ts
391
+ var GEONORGE_NAVN_URL = "https://ws.geonorge.no/stedsnavn/v1/navn";
392
+ var DEFAULT_LIMIT = 10;
393
+ var MAX_LIMIT = 500;
394
+ var REJECTED_CHARACTERS = /["?;%\\]/g;
395
+ async function searchGeonorgePlaces(query, options = {}) {
396
+ options.signal?.throwIfAborted();
397
+ const term = query.replace(REJECTED_CHARACTERS, "").trim();
398
+ if (!term.replace(/\*/g, "")) return [];
399
+ const url = new URL(GEONORGE_NAVN_URL);
400
+ const params = url.searchParams;
401
+ params.set("sok", term.endsWith("*") ? term : `${term}*`);
402
+ params.set("utkoordsys", "4326");
403
+ params.set("treffPerSide", String(Math.min(Math.max(Math.trunc(options.limit ?? DEFAULT_LIMIT), 1), MAX_LIMIT)));
404
+ if (options.fuzzy) params.set("fuzzy", "true");
405
+ for (const knr of options.municipalityNumbers ?? []) params.append("knr", knr);
406
+ for (const fnr of options.countyNumbers ?? []) params.append("fnr", fnr);
407
+ const response = await fetch(url, { signal: options.signal });
408
+ if (!response.ok) {
409
+ throw new Error(`Geonorge place search failed: ${response.status} ${response.statusText}`);
410
+ }
411
+ const body = await response.json();
412
+ return (body.navn ?? []).map(toPlaceHit);
413
+ }
414
+ function toPlaceHit(navn) {
415
+ const type = navn.navneobjekttype.trim();
416
+ return {
417
+ id: `${navn.stedsnummer}-${navn.spr\u00E5k}-${navn.skrivem\u00E5te}`,
418
+ placeId: String(navn.stedsnummer),
419
+ name: navn.skrivem\u00E5te,
420
+ language: navn.spr\u00E5k,
421
+ type,
422
+ municipalities: navn.kommuner?.map((k) => k.kommunenavn.trim()) ?? [],
423
+ counties: navn.fylker?.map((f) => f.fylkesnavn.trim()) ?? [],
424
+ coordinates: { lat: navn.representasjonspunkt.nord, lon: navn.representasjonspunkt.\u00F8st },
425
+ targetMetersPerPixel: metersPerPixelForType(type)
426
+ };
427
+ }
428
+
64
429
  // src/core/state/store.ts
65
430
  function createEmitter() {
66
431
  const listeners = {};
@@ -2334,6 +2699,16 @@ var SelectController = class {
2334
2699
  }
2335
2700
  };
2336
2701
 
2702
+ // src/core/ol/zoom/ground-resolution.ts
2703
+ import { fromLonLat, getPointResolution } from "ol/proj";
2704
+ import { clamp } from "ol/math";
2705
+ function zoomForGroundResolution(view, lonLat, metersPerPixel) {
2706
+ const projection = view.getProjection();
2707
+ const groundMetersPerUnit = getPointResolution(projection, 1, fromLonLat(lonLat, projection), "m");
2708
+ const zoom = view.getZoomForResolution(metersPerPixel / groundMetersPerUnit) ?? view.getMinZoom();
2709
+ return clamp(zoom, view.getMinZoom(), view.getMaxZoom());
2710
+ }
2711
+
2337
2712
  // src/core/ol/OlMapEngine.ts
2338
2713
  function resolveVectorSource(layer) {
2339
2714
  const src = layer.getSource();
@@ -2759,6 +3134,9 @@ function createOlEngine(events) {
2759
3134
  },
2760
3135
  getCenterFromExtent: (extent) => {
2761
3136
  return getCenter3(extent);
3137
+ },
3138
+ getZoomForGroundResolution: (lonLat, metersPerPixel) => {
3139
+ return zoomForGroundResolution(map.getView(), lonLat, metersPerPixel);
2762
3140
  }
2763
3141
  };
2764
3142
  }
@@ -3006,6 +3384,21 @@ var MapAPI = class {
3006
3384
  getCenterFromExtent(extent) {
3007
3385
  return this.engine.getCenterFromExtent(extent);
3008
3386
  }
3387
+ // Place search
3388
+ /**
3389
+ * Prefix search for place names in Geonorge (https://ws.geonorge.no/stedsnavn/v1/#/default/get_navn).
3390
+ * `zoomLevel` is computed for this map's view. Abort `options.signal` to cancel.
3391
+ */
3392
+ async searchPlaces(query, options) {
3393
+ const places = await searchGeonorgePlaces(query, options);
3394
+ return places.map(({ targetMetersPerPixel, ...place }) => ({
3395
+ ...place,
3396
+ zoomLevel: this.engine.getZoomForGroundResolution(
3397
+ [place.coordinates.lon, place.coordinates.lat],
3398
+ targetMetersPerPixel
3399
+ )
3400
+ }));
3401
+ }
3009
3402
  };
3010
3403
 
3011
3404
  // src/api/config.schema.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@artsdatabanken/nbic-map-component",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Shared map component built on OpenLayers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",