Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/iceberg/components/IcebergMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ watch(map, (map) => {
toZoomLevel: 7,
}),
pluginReverseGeocoder({
type: 'wps',
url: 'https://geodienste.hamburg.de/HH_WPS',
coordinateSources: [
{
Expand Down
12 changes: 11 additions & 1 deletion examples/snowbox/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@
colorScheme,
startCenter: [565874, 5934140],
layers: [
// TODO: Add internalization to snowbox

Check warning on line 95 in examples/snowbox/index.js

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: Add internalization to snowbox'
{
id: basemapId,
visibility: true,
Expand Down Expand Up @@ -275,6 +275,7 @@
map,
pluginAddressSearch({
searchMethods: [
/*
{
queryParameters: {
searchStreets: true,
Expand All @@ -283,6 +284,12 @@
type: 'mpapi',
url: 'https://geodienste.hamburg.de/HH_WFS_GAGES?service=WFS&request=GetFeature&version=2.0.0',
},
*/
{
type: 'nominatim',
url: 'https://polar.dataport.de/nominatim/search',
queryParameters: {},
},
],
minLength: 3,
waitMs: 300,
Expand Down Expand Up @@ -311,7 +318,10 @@
addPlugin(
map,
pluginReverseGeocoder({
url: 'https://geodienste.hamburg.de/HH_WPS',
// type: 'wps',
// url: 'https://geodienste.hamburg.de/HH_WPS',
type: 'nominatim',
url: 'https://polar.dataport.de/nominatim/reverse',
coordinateSources: [
{
plugin: 'pins',
Expand Down Expand Up @@ -342,7 +352,7 @@
},
],
menus: [
// TODO: Delete the mock plugins including the components once the correct plugins have been implemented

Check warning on line 355 in examples/snowbox/index.js

View workflow job for this annotation

GitHub Actions / Linting

Unexpected 'todo' comment: 'TODO: Delete the mock plugins including...'
[
{
plugin: pluginFullscreen({}),
Expand Down
62 changes: 62 additions & 0 deletions src/lib/getFeatures/nominatim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { Point } from 'geojson'

import { transform as transformCoordinates } from 'ol/proj'

import type { PolarGeoJsonFeatureCollection } from '@/core'

import type { NominatimParameters } from './types'

export default async function (
signal: AbortSignal,
url: string,
inputValue: string,
queryParameters: NominatimParameters
): Promise<PolarGeoJsonFeatureCollection> {
const { epsg, maxFeatures, ...native } = queryParameters

const fetchUrl = new URL(url)
Object.entries({
...Object.fromEntries(
Object.entries(native).map(([key, value]) => [
key,
Array.isArray(value) ? value.join(',') : value,
])
),
...(maxFeatures ? { limit: maxFeatures } : {}),
q: inputValue,
format: 'geojson',
polygon_geojson: 0, // TODO: Setting this to 1 is currently not supported well by addressSearch plugin. The plugin expects a point geometry.
polygon_threshold: 5,
}).forEach(([key, value]) => {
fetchUrl.searchParams.set(key, value.toString())
})

return await fetch(fetchUrl, {
signal,
})
.then((response) => response.json())
.then((featureCollection) => ({
...featureCollection,
features: featureCollection.features.map((feature) => ({
...feature,
title: feature.properties.display_name,
geometry: {
...feature.geometry,
coordinates:
epsg === 'EPSG:4326'
? feature.geometry.coordinates
: feature.geometry.type === 'Polygon'
? feature.geometry.coordinates.map((ring) =>
ring.map((coord) =>
transformCoordinates(coord, 'EPSG:4326', epsg)
)
)
: transformCoordinates(
(feature.geometry as Point).coordinates,
'EPSG:4326',
epsg
),
},
})),
}))
}
82 changes: 82 additions & 0 deletions src/lib/getFeatures/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,85 @@ export interface WfsParameters extends QueryParameters {
*/
useRightHandWildcard?: boolean
}

export interface NominatimParameters extends QueryParameters {
/**
* Include address details in result?
*/
addressdetails?: boolean

/**
* Name and/or type of POI.
*/
amenity?: string

/**
* Whether to consider the viewbox as exact instead of fuzzy filter.
*/
bounded?: boolean

/**
* City.
*/
city?: string

/**
* Country.
*/
country?: string

/**
* Country code (ISO 3166-1alpha2).
*/
countrycodes?: string[]

/**
* County.
*/
county?: string

/**
* Include entrances to buildings in results?
*/
entrances?: boolean

/**
* Add extra tags to result?
*/
extratags?: boolean

/**
* Which feature types to include.
*/
featureType?: 'country' | 'state' | 'city' | 'settlement'

/**
* Which type(s) of results to return.
*/
layer?: ('address' | 'poi' | 'railway' | 'natural' | 'manmade')[]

/**
* Include more names in alternate languages?
*/
namedetails?: boolean

/**
* Postal code.
*/
postalcode?: string

/**
* State.
*/
state?: string

/**
* House number and street name.
*/
street?: string

/**
* Viewbox to prefer for searching the given term.
*/
viewbox?: [number, number, number, number]
}
4 changes: 2 additions & 2 deletions src/plugins/addressSearch/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export interface AddressSearchPluginOptions extends PluginOptions {

/** Possible search methods by type. */
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
export type SearchType = 'bkg' | 'wfs' | 'mpapi' | string
export type SearchType = 'bkg' | 'wfs' | 'mpapi' | 'nominatim' | string

export type SearchDisplayMode = 'mixed' | 'categorized'

Expand Down Expand Up @@ -177,7 +177,7 @@ export interface SearchMethodConfiguration {

/**
* The object further describes details for the search request.
* Its contents vary by service type, see {@link BKGParameters}, {@link MpapiParameters} or {@link WfsParameters}.
* Its contents vary by service type, see {@link BKGParameters}, {@link MpapiParameters}, {@link WfsParameters} or {@link NominatimParameters}.
*/
queryParameters?: QueryParameters
}
Expand Down
3 changes: 2 additions & 1 deletion src/plugins/addressSearch/utils/methodContainer.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import bkg from '@/lib/getFeatures/bkg'
import mpapi from '@/lib/getFeatures/mpapi'
import nominatim from '@/lib/getFeatures/nominatim'
import { getWfsFeatures } from '@/lib/getFeatures/wfs'

import type { SearchMethodFunction } from '../types'

export function getMethodContainer() {
const methods = { bkg, mpapi, wfs: getWfsFeatures }
const methods = { bkg, mpapi, nominatim, wfs: getWfsFeatures }

return {
registerSearchMethods: (
Expand Down
26 changes: 17 additions & 9 deletions src/plugins/reverseGeocoder/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ import {
type ReverseGeocoderFeature,
type ReverseGeocoderPluginOptions,
} from './types'
import { reverseGeocode as reverseGeocodeUtil } from './utils/reverseGeocode'
import { reverseGeocodeNominatim } from './utils/reverseGeocodeNominatim'
import { reverseGeocodeWps } from './utils/reverseGeocodeWps'

/* eslint-disable tsdoc/syntax */
/**
Expand Down Expand Up @@ -83,9 +84,14 @@ export const useReverseGeocoderStore = defineStore(
async function reverseGeocode(coordinate: [number, number]) {
const finish = indicateLoading()
try {
const reverseGeocodeUtil = {
wps: reverseGeocodeWps,
nominatim: reverseGeocodeNominatim,
}[configuration.value.type]
const feature = await reverseGeocodeUtil(
configuration.value.url,
coordinate
coordinate,
coreStore.configuration.epsg
)
if (configuration.value.addressTarget) {
passFeatureToTarget(configuration.value.addressTarget, feature)
Expand Down Expand Up @@ -129,20 +135,20 @@ if (import.meta.vitest) {
const { createPinia, setActivePinia } = await import('pinia')
const { reactive } = await import('vue')
const useCoreStoreFile = await import('@/core/stores')
const reverseGeocodeUtilFile = await import('./utils/reverseGeocode')
const reverseGeocodeUtilFile = await import('./utils/reverseGeocodeWps')
const indicateLoadingFile = await import('@/lib/indicateLoading')

/* eslint-disable no-empty-pattern */
const test = _test.extend<{
reverseGeocodeUtil: Mock<typeof reverseGeocodeUtil>
reverseGeocodeUtil: Mock<typeof reverseGeocodeWps>
indicateLoading: Mock<typeof indicateLoading>
coreStore: Reactive<Record<string, unknown>>
store: ReturnType<typeof useReverseGeocoderStore>
}>({
reverseGeocodeUtil: [
async ({}, use) => {
const reverseGeocodeUtil = vi
.spyOn(reverseGeocodeUtilFile, 'reverseGeocode')
.spyOn(reverseGeocodeUtilFile, 'reverseGeocodeWps')
.mockResolvedValue(null as unknown as ReverseGeocoderFeature)
await use(reverseGeocodeUtil)
},
Expand All @@ -162,7 +168,9 @@ if (import.meta.vitest) {
const fit = vi.fn()
const coreStore = reactive({
configuration: {
epsg: 'EPSG:25832',
[PluginId]: {
type: 'wps',
url: 'https://wps.example',
coordinateSources: [{ key: 'coordinateSource' }],
addressTarget: { key: 'addressTarget' },
Expand Down Expand Up @@ -200,9 +208,10 @@ if (import.meta.vitest) {
}) => {
coreStore.coordinateSource = [1, 2]
await new Promise((resolve) => setTimeout(resolve))
expect(reverseGeocodeUtil).toHaveBeenCalledWith(
expect(reverseGeocodeUtil).toHaveBeenCalledExactlyOnceWith(
'https://wps.example',
[1, 2]
[1, 2],
'EPSG:25832'
)
})

Expand All @@ -216,8 +225,7 @@ if (import.meta.vitest) {
feature as unknown as ReverseGeocoderFeature
)
await store.reverseGeocode([3, 4])
expect(coreStore.addressTarget).toHaveBeenCalledOnce()
expect(coreStore.addressTarget).toHaveBeenCalledWith(feature)
expect(coreStore.addressTarget).toHaveBeenCalledExactlyOnceWith(feature)
})

test('zooms to input coordinate', async ({
Expand Down
5 changes: 5 additions & 0 deletions src/plugins/reverseGeocoder/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const PluginId = 'reverseGeocoder'
* Plugin options for reverse geocoder plugin.
*/
export interface ReverseGeocoderPluginOptions extends PluginOptions {
/**
* Type of reverse geocoding service.
*/
type: 'wps' | 'nominatim'

/**
* URL of a WPS service to use for reverse geocoding.
*/
Expand Down
50 changes: 50 additions & 0 deletions src/plugins/reverseGeocoder/utils/reverseGeocodeNominatim.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { transform as transformCoordinate } from 'ol/proj'

import type { ReverseGeocoderFeature } from '../types'

export async function reverseGeocodeNominatim(
url: string,
coordinate: [number, number],
epsg: string
): Promise<ReverseGeocoderFeature> {
const searchCoordinate = transformCoordinate(
coordinate,
epsg,
'EPSG:4326'
) as [number, number]

const fetchUrl = new URL(url)
fetchUrl.searchParams.set('lat', searchCoordinate[1].toString())
fetchUrl.searchParams.set('lon', searchCoordinate[0].toString())
fetchUrl.searchParams.set('format', 'jsonv2')

const result = await fetch(fetchUrl).then((response) => response.json())

const resultObject: ReverseGeocoderFeature = {
type: 'reverse_geocoded',
title: [
[result.address.road, result.address.house_number]
.filter((x) => x)
.join(' '),
result.address.town || result.address.city || result.address.village,
]
.filter((x) => x)
.join(', '),
properties: result.properties,
geometry: {
// as clicked by user - usually want to keep this since user is pointing at something
coordinates: coordinate,
type: 'Point',
},
addressGeometry: {
// as returned by reverse geocoder
coordinates: transformCoordinate(
[result.lon, result.lat],
'EPSG:4326',
epsg
),
type: 'Point',
},
}
return resultObject
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const parser = new Parser({
tagNameProcessors: [processors.stripPrefix],
})

export async function reverseGeocode(
export async function reverseGeocodeWps(
url: string,
coordinate: [number, number]
): Promise<ReverseGeocoderFeature> {
Expand Down Expand Up @@ -136,7 +136,7 @@ if (import.meta.vitest) {
text: () => Promise.resolve(testResponse),
} as Response)

const feature = await reverseGeocode(testUrl, testCoordinates)
const feature = await reverseGeocodeWps(testUrl, testCoordinates)

expect(fetchMock).toHaveBeenCalledOnce()
expect(fetchMock).toHaveBeenCalledWith(testUrl, {
Expand Down
Loading