diff --git a/README.md b/README.md index 9cc0e5836..3fcadf105 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ The control comes with a number of default providers: - [LocationIQ] - [OpenCage] - [OpenStreetMap] +- [PdokNl] - [Pelias] - [Mapbox](https://docs.mapbox.com/help/tutorials/local-search-geocoding-api/) - [GeoApiFR](https://geo.api.gouv.fr/adresse) @@ -352,6 +353,7 @@ refresh when you change source files. [locationiq]: https://leaflet-geosearch.meijer.works/providers/locationiq [opencage]: https://leaflet-geosearch.meijer.works/providers/opencage [openstreetmap]: https://leaflet-geosearch.meijer.works/providers/openstreetmap +[pdoknl]: https://leaflet-geosearch.meijer.works/providers/pdoknl [pelias]: https://leaflet-geosearch.meijer.works/providers/pelias ## Contributors ✨ diff --git a/docs/lib/providers.ts b/docs/lib/providers.ts index 61285d9e3..36c4ec44f 100644 --- a/docs/lib/providers.ts +++ b/docs/lib/providers.ts @@ -11,6 +11,7 @@ import { LocationIQProvider, OpenCageProvider, OpenStreetMapProvider, + PdokNlProvider, PeliasProvider, } from 'leaflet-geosearch'; @@ -54,5 +55,7 @@ export default { OpenStreetMap: new OpenStreetMapProvider(), + PdokNl: new PdokNlProvider(), + Pelias: new PeliasProvider(), }; diff --git a/docs/providers/pdoknl.mdx b/docs/providers/pdoknl.mdx new file mode 100644 index 000000000..3eb92e63e --- /dev/null +++ b/docs/providers/pdoknl.mdx @@ -0,0 +1,51 @@ +--- +name: PdokNl +menu: Providers +route: /providers/pdoknl +--- + +import Playground from '../components/Playground'; +import Map from '../components/Map'; + +# PdokNl Provider + +For more options and configurations, see the [PDOK Locatieserver][1]. + + + + + +```js +import { PdokNlProvider } from 'leaflet-geosearch'; + +const provider = new PdokNlProvider(); + +// add to leaflet +import { GeoSearchControl } from 'leaflet-geosearch'; + +map.addControl( + new GeoSearchControl({ + provider, + style: 'bar', + }), +); +``` + +## Optional parameters + +PDOK Locatieserver supports a number of [optional parameters][2]. As the api requires those parameters to be added to the url, they can be added to the `params` key of the provider. + +All options defined next to the `params` key, would have been added to the request body. + +```js +const provider = new PdokNlProvider({ + params: { + fq: 'gemeentenaam:Leiden', // limit search results to municipality + lat: 0, // Latitude in degree + lon: 0, // Longitude in degree + }, +}); +``` + +[1]: https://www.pdok.nl/pdok-locatieserver +[2]: https://api.pdok.nl/bzk/locatieserver/search/v3_1/ui/#/Locatieserver/suggest diff --git a/src/index.ts b/src/index.ts index 20e177bfc..23832934f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,6 @@ export { default as MapBoxProvider } from './providers/mapBoxProvider'; export { default as OpenCageProvider } from './providers/openCageProvider'; export { default as OpenStreetMapProvider } from './providers/openStreetMapProvider'; export { default as PeliasProvider } from './providers/peliasProvider'; +export { default as PdokNlProvider } from './providers/pdokNlProvider'; export { default as JsonProvider } from './providers/provider'; diff --git a/src/providers/__tests__/pdokNlProvider.spec.js b/src/providers/__tests__/pdokNlProvider.spec.js new file mode 100644 index 000000000..9f6981bed --- /dev/null +++ b/src/providers/__tests__/pdokNlProvider.spec.js @@ -0,0 +1,25 @@ +import Provider from '../pdokNlProvider'; +import fixtures from './pdokNlResponse.json'; + +describe('PdokNlProvider', () => { + beforeAll(() => { + fetch.mockResponse(async () => ({ body: JSON.stringify(fixtures) })); + }); + + test('Can fetch results', async () => { + const provider = new Provider(); + + const results = await provider.search({ query: '3e Binnenvestgracht' }); + const result = results[0]; + + const position = fixtures.response.docs[0].centroide_ll + .replace(/POINT\(|\)/g, '') + .trim() + .split(' ') + .map(Number); + + expect(result.label).toBeTruthy(); + expect(result.x).toEqual(position[0]); + expect(result.y).toEqual(position[1]); + }); +}); diff --git a/src/providers/__tests__/pdokNlResponse.json b/src/providers/__tests__/pdokNlResponse.json new file mode 100644 index 000000000..793806267 --- /dev/null +++ b/src/providers/__tests__/pdokNlResponse.json @@ -0,0 +1,64 @@ +{ + "response": { + "numFound": 49, + "start": 0, + "maxScore": 10.934887, + "numFoundExact": true, + "docs": [ + { + "weergavenaam": "3e Binnenvestgracht, 2312NR Leiden", + "id": "pcd-f14950155195f418398b6eda76d66adb", + "centroide_ll": "POINT(4.49093267 52.16412132)" + }, + { + "weergavenaam": "3e Binnenvestgracht 23L-1, 2312NR Leiden", + "id": "adr-37dba6d7e92c0ccca8fc738005057bd9", + "centroide_ll": "POINT(4.49157312 52.16420333)" + }, + { + "weergavenaam": "3e Binnenvestgracht 23L-2, 2312NR Leiden", + "id": "adr-fd7be59d3e4ea77dafba1e8fd69017e6", + "centroide_ll": "POINT(4.49157 52.16425474)" + }, + { + "weergavenaam": "3e Binnenvestgracht 23T-1, 2312NR Leiden", + "id": "adr-763525b75ad60b4e13fdf3d00b195912", + "centroide_ll": "POINT(4.49202289 52.164203)" + }, + { + "weergavenaam": "3e Binnenvestgracht 23A, 2312NR Leiden", + "id": "adr-6d98d9468e29425d0ad81dcfdbe00f7d", + "centroide_ll": "POINT(4.49098397 52.16416908)" + } + ] + }, + "highlighting": { + "pcd-f14950155195f418398b6eda76d66adb": { + "suggest": ["3e Binnenvestgracht, 2312 NR Leiden"] + }, + "adr-37dba6d7e92c0ccca8fc738005057bd9": { + "suggest": [ + "3e Binnenvestgracht 23L-1, 2312NR Leiden" + ] + }, + "adr-fd7be59d3e4ea77dafba1e8fd69017e6": { + "suggest": [ + "3e Binnenvestgracht 23L-2, 2312NR Leiden" + ] + }, + "adr-763525b75ad60b4e13fdf3d00b195912": { + "suggest": [ + "3e Binnenvestgracht 23T-1, 2312NR Leiden" + ] + }, + "adr-6d98d9468e29425d0ad81dcfdbe00f7d": { + "suggest": [ + "3e Binnenvestgracht 23A, 2312NR Leiden" + ] + } + }, + "spellcheck": { + "suggestions": [], + "collations": [] + } +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 470f52f68..524ef1953 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -12,6 +12,7 @@ export { default as LocationIQProvider } from './locationIQProvider'; export { default as MapBoxProvider } from './mapBoxProvider'; export { default as OpenCageProvider } from './openCageProvider'; export { default as OpenStreetMapProvider } from './openStreetMapProvider'; +export { default as PdokNlProvider } from './pdokNlProvider'; export { default as PeliasProvider } from './peliasProvider'; export { default as Provider } from './provider'; diff --git a/src/providers/pdokNlProvider.ts b/src/providers/pdokNlProvider.ts new file mode 100644 index 000000000..9dde1651d --- /dev/null +++ b/src/providers/pdokNlProvider.ts @@ -0,0 +1,143 @@ +import AbstractProvider, { + EndpointArgument, + ParseArgument, + ProviderOptions, + SearchResult, + RequestType, + SearchArgument, +} from './provider'; + +interface Doc { + weergavenaam: string; + id: string; + centroide_ll: string; +} + +export interface RequestResult { + response: { + numFound: number; + start: number; + maxScore: number; + numFoundExact: boolean; + docs: Doc[]; + }; + highlighting: { + [key: string]: { + suggest: string[]; + }; + }; + spellcheck: { + suggestions: [ + string, + { + numFound: number; + startOffset: number; + endOffset: number; + suggestion: string[]; + }, + ]; + collations: [ + 'collation', + { + collationQuery: string; + hits: number; + misspellingsAndCorrections: string[]; + }, + ]; + }; +} + +export interface RawResult extends Doc { + highlight: string; +} + +export type PdokNlProviderOptions = ProviderOptions; + +export default class PdokNlProvider extends AbstractProvider< + RequestResult, + RawResult +> { + searchUrl: string; + reverseUrl: string; + + constructor(options: PdokNlProviderOptions = {}) { + super({ + ...options, + params: { + bq: 'type:gemeente^0.5 type:woonplaats^0.5 type:weg^1.0 type:postcode^1.5 type:adres^2.0', + fl: 'id,weergavenaam,centroide_ll', + rows: 5, + ...options.params, + }, + }); + + const base = 'https://api.pdok.nl/bzk/locatieserver/search/v3_1'; + this.searchUrl = `${base}/suggest`; + this.reverseUrl = `${base}/reverse`; + } + + endpoint({ query, type }: EndpointArgument) { + const params = typeof query === 'string' ? { q: query } : query; + + switch (type) { + case RequestType.REVERSE: + return this.getUrl(this.reverseUrl, params); + + default: + return this.getUrl(this.searchUrl, params); + } + } + + parse({ data }: ParseArgument) { + return data.response.docs.map((doc) => { + const position = doc.centroide_ll + .replace(/POINT\(|\)/g, '') + .trim() + .split(' ') + .map(Number); + + let highlight = doc.weergavenaam; + if (data.highlighting[doc.id].suggest) { + highlight = data.highlighting[doc.id].suggest[0]; + } + + return { + x: position[0], + y: position[1], + label: doc.weergavenaam, + bounds: null, + raw: { + ...doc, + highlight, + }, + }; + }); + } + + async search(options: SearchArgument): Promise[]> { + const url = this.endpoint({ + query: options.query, + }); + + const json = await fetch(url) + .then((response) => response.json()) + .then((json: RequestResult) => { + // If there are no results found but there are spellcheck corrections, we search again using the corrected query. + if (json.response.numFound < 1) { + if (json.spellcheck.collations.length >= 2) { + const url = this.endpoint({ + query: json.spellcheck.collations[1].collationQuery, + }); + + return fetch(url).then( + (response): Promise => response.json(), + ); + } + } + + return json; + }); + + return this.parse({ data: json }); + } +}