Skip to content

Commit e108145

Browse files
committed
add coastal gazetteer search draft
1 parent 80e37a4 commit e108145

File tree

3 files changed

+258
-4
lines changed

3 files changed

+258
-4
lines changed

packages/clients/textLocator/src/addPlugins.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import Scale from '@polar/plugin-scale'
1111
import Toast from '@polar/plugin-toast'
1212
import Zoom from '@polar/plugin-zoom'
1313

14+
import { searchCoastalGazetteer } from './search/coastalGazetteer'
1415
import { idRegister } from './services'
1516

1617
// this is fine for list-like setup functions
@@ -71,11 +72,9 @@ export const addPlugins = (core) => {
7172
searchMethods: [{ type: 'coastalGazetteer' }],
7273
addLoading: 'plugin/loadingIndicator/addLoadingKey',
7374
removeLoading: 'plugin/loadingIndicator/removeLoadingKey',
74-
customSearchMethods: {
75-
/* TODO */
76-
},
75+
customSearchMethods: { coastalGazetteer: searchCoastalGazetteer },
7776
customSelectResult: {
78-
/* TODO */
77+
/* categoryDenkmalsucheAutocomplete: selectResult */
7978
},
8079
}),
8180
Draw({
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// TODO write search by geometry
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
// such names exist on the service
2+
/* eslint-disable @typescript-eslint/naming-convention */
3+
import { CoreState } from '@polar/lib-custom-types'
4+
import { Feature, FeatureCollection } from 'geojson'
5+
import { Map } from 'ol'
6+
import { GeoJSON, WKT } from 'ol/format'
7+
import { Store } from 'vuex'
8+
import levenshtein from 'js-levenshtein'
9+
10+
const ignoreIds = {
11+
global: ['EuroNat-33'],
12+
geometries: [
13+
'EuroNat-33',
14+
'SH-WATTENMEER-DM-1',
15+
'SH-WATTENMEER-1',
16+
'Ak2006-51529',
17+
'Landsg-2016-110',
18+
],
19+
}
20+
const wellKnownText = new WKT()
21+
const geoJson = new GeoJSON()
22+
23+
interface RequestPayload {
24+
keyword: string
25+
searchType: 'like' | 'exact' | 'id'
26+
lang: '-' | string
27+
sdate: string
28+
edate: string
29+
type: '-' | string
30+
page?: string // numerical
31+
geom?: string
32+
}
33+
34+
interface ResponseName {
35+
Start: string // YYYY-MM-DD
36+
Ende: string // YYYY-MM-DD
37+
GeomID: string
38+
ObjectID: string
39+
Name: string
40+
Quellen: object[] // not used
41+
Rezent: boolean
42+
Sprache: string
43+
Typ: string
44+
}
45+
46+
interface ResponseGeom {
47+
Start: string // YYYY-MM-DD
48+
Ende: string // YYYY-MM-DD
49+
GeomID: string
50+
ObjectID: string
51+
Quellen: object[] // not used
52+
Typ: string
53+
'Typ Beschreibung': string
54+
WKT: string // WKT geometry
55+
}
56+
57+
interface ResponseResult {
58+
id: string
59+
names: ResponseName[]
60+
geoms: ResponseGeom[]
61+
}
62+
63+
interface ResponsePayload {
64+
count: string // numerical
65+
currentpage: string // numerical
66+
pages: string // numerical
67+
keyword: string
68+
querystring: string
69+
results: ResponseResult[]
70+
time: number
71+
}
72+
73+
interface CoastalGazetteerParameters {
74+
epsg: `EPSG:${string}`
75+
map: Map
76+
}
77+
78+
// arbitrary sort based on input – prefer 1. startsWith 2. closer string
79+
const sorter =
80+
(searchPhrase: string, sortKey: string) =>
81+
(a: ResponseName | Feature, b: ResponseName | Feature) => {
82+
const aStartsWith = a[sortKey].startsWith(searchPhrase)
83+
const bStartsWith = b[sortKey].startsWith(searchPhrase)
84+
85+
return aStartsWith && !bStartsWith
86+
? -1
87+
: !aStartsWith && bStartsWith
88+
? 1
89+
: levenshtein(a[sortKey], searchPhrase) -
90+
levenshtein(b[sortKey], searchPhrase)
91+
}
92+
93+
const searchRequestDefaultPayload: Partial<RequestPayload> = {
94+
searchType: 'like',
95+
lang: '-',
96+
sdate: '0001-01-01',
97+
edate: new Date().toJSON().slice(0, 10),
98+
type: '-',
99+
}
100+
101+
const getEmptyFeatureCollection = (): FeatureCollection => ({
102+
type: 'FeatureCollection',
103+
features: [],
104+
})
105+
106+
const getEmptyResponsePayload = (): ResponsePayload => ({
107+
count: '',
108+
currentpage: '',
109+
pages: '',
110+
keyword: '',
111+
querystring: '',
112+
results: [],
113+
time: NaN,
114+
})
115+
116+
const mergeResponses = (
117+
initialResponse: ResponsePayload,
118+
responses: ResponsePayload[]
119+
) => ({
120+
...initialResponse,
121+
currentpage: 'merged',
122+
results: [
123+
initialResponse.results,
124+
...responses.map(({ results }) => results),
125+
].flat(1),
126+
time: NaN, // not used, setting NaN to indicate it's not the actual time
127+
})
128+
129+
async function getAllPages(
130+
this: Store<CoreState>,
131+
signal: AbortSignal,
132+
url: string,
133+
inputValue: string,
134+
page: string | undefined
135+
): Promise<ResponsePayload> {
136+
const response = await fetch(
137+
`${url}?${new URLSearchParams({
138+
...searchRequestDefaultPayload,
139+
keyword: `*${inputValue}*`,
140+
...(typeof page !== 'undefined' ? { page } : {}),
141+
}).toString()}`,
142+
{
143+
method: 'GET',
144+
signal,
145+
}
146+
)
147+
148+
if (!response.ok) {
149+
this.dispatch('plugin/toast/addToast', {
150+
type: 'error',
151+
text: 'textLocator.error.searchCoastalGazetteer', // TODO use page || '1'
152+
})
153+
return getEmptyResponsePayload()
154+
}
155+
const responsePayload: ResponsePayload = await response.json()
156+
const pages = parseInt(responsePayload.pages, 10)
157+
const initialRequestMerge = typeof page === 'undefined' && pages > 1
158+
159+
if (!initialRequestMerge) {
160+
return responsePayload
161+
}
162+
163+
return mergeResponses(
164+
responsePayload,
165+
await Promise.all(
166+
Array.from(Array(pages - 1)).map((_, index) =>
167+
getAllPages.call(this, signal, url, inputValue, `${index + 2}`)
168+
)
169+
)
170+
)
171+
}
172+
173+
const featurify =
174+
(epsg: `EPSG:${string}`, searchPhrase: string) =>
175+
(feature: ResponseResult): Feature => ({
176+
type: 'Feature',
177+
geometry: geoJson.writeGeometryObject(
178+
wellKnownText.readGeometry(
179+
feature.geoms.find(
180+
(geom) => !ignoreIds.geometries.includes(geom.GeomID)
181+
)?.WKT,
182+
{
183+
dataProjection: 'EPSG:4326',
184+
featureProjection: epsg,
185+
}
186+
)
187+
),
188+
id: feature.id,
189+
properties: {
190+
names: feature.names,
191+
geometries: feature.geoms.filter(
192+
(geom) => !ignoreIds.geometries.includes(geom.GeomID)
193+
),
194+
},
195+
// @ts-expect-error | used in POLAR for text display
196+
title:
197+
feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || 'Ohne Namen', // TODO i18n
198+
})
199+
200+
const featureCollectionify = (
201+
fullResponse: ResponsePayload,
202+
epsg: `EPSG:${string}`,
203+
searchPhrase: string
204+
): FeatureCollection => {
205+
const featureCollection = getEmptyFeatureCollection()
206+
featureCollection.features.push(
207+
...fullResponse.results.reduce((accumulator, feature) => {
208+
if (ignoreIds.global.includes(feature.id)) {
209+
return accumulator
210+
}
211+
try {
212+
// TODO this shouldn't be try-catch, it's detectable
213+
const featurified = featurify(epsg, searchPhrase)(feature)
214+
accumulator.push(featurified)
215+
return accumulator
216+
} catch (e) {
217+
return accumulator
218+
}
219+
}, [] as Feature[])
220+
)
221+
featureCollection.features = featureCollection.features.sort(
222+
sorter(searchPhrase, 'title')
223+
)
224+
return featureCollection
225+
}
226+
227+
export async function searchCoastalGazetteer(
228+
this: Store<CoreState>,
229+
signal: AbortSignal,
230+
url: string,
231+
inputValue: string,
232+
queryParameters: CoastalGazetteerParameters
233+
): Promise<FeatureCollection> | never {
234+
let fullResponse: ResponsePayload
235+
try {
236+
fullResponse = await getAllPages.call(
237+
this,
238+
signal,
239+
url,
240+
inputValue,
241+
undefined
242+
)
243+
} catch (e) {
244+
console.error(e)
245+
this.dispatch('plugin/toast/addToast', {
246+
type: 'error',
247+
text: 'textLocator.error.searchCoastalGazetteer',
248+
})
249+
return getEmptyFeatureCollection()
250+
}
251+
return Promise.resolve(
252+
featureCollectionify(fullResponse, queryParameters.epsg, inputValue)
253+
)
254+
}

0 commit comments

Comments
 (0)