Skip to content

Commit

Permalink
Merge pull request #172 from Dataport/feature/text-locator-literature…
Browse files Browse the repository at this point in the history
…-search

TextLocator/implement literature search
  • Loading branch information
warm-coolguy authored Oct 25, 2024
2 parents 3c3a96a + c5f42d9 commit 7afd7e5
Show file tree
Hide file tree
Showing 25 changed files with 425 additions and 148 deletions.
5 changes: 5 additions & 0 deletions packages/clients/textLocator/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## unpublished

- Breaking: Adapt client to new backend API. Previous versions are no longer runnable due the backend API change.
- Feature: Implement document search, that is, the AddressSearch bar now also offers results for documents and clicking on one will retrieve a list of toponyms from the backend and resolve them against the gazetteer. Previously, we had a "get all documents regarding place" functionality. Now, a "get all places regarding document" feature is implemented. Additionally to the AddressSearch, this can be triggered on documents found in the previously implemented way; the "get all places regarding document" functionality is available from within the GeoSearch result display.

## 1.0.0-alpha.0

Initial alpha release.
35 changes: 30 additions & 5 deletions packages/clients/textLocator/src/addPlugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ import {
selectResult,
} from './utils/coastalGazetteer/searchToponym'
import { idRegister } from './services'
import { searchLiterature } from './utils/textLocatorBackend/findLiterature/searchLiterature'
import { selectLiterature } from './utils/textLocatorBackend/findLiterature/selectLiterature'

export const ids = {
groupId: 'groupTL',
categoryIdToponym: 'categoryToponym',
categoryIdLiterature: 'categoryLiterature',
typeGazetteer: 'coastalGazetteer',
typeLiterature: 'literature',
}

// this is fine for list-like setup functions
// eslint-disable-next-line max-lines-per-function
Expand All @@ -30,14 +40,29 @@ export const addPlugins = (core) => {
layoutTag: NineLayoutTag.TOP_LEFT,
addLoading: 'plugin/loadingIndicator/addLoadingKey',
removeLoading: 'plugin/loadingIndicator/removeLoadingKey',
// @ts-expect-error | Local parameter requirements diverge from type
customSearchMethods: {
// @ts-expect-error | Local parameter requirements diverge from type
coastalGazetteer: searchCoastalGazetteerByToponym,
[ids.typeGazetteer]: searchCoastalGazetteerByToponym,
[ids.typeLiterature]: searchLiterature,
},
customSelectResult: {
// it's defined like that
// eslint-disable-next-line @typescript-eslint/naming-convention
'': selectResult,
[ids.categoryIdToponym]: selectResult,
[ids.categoryIdLiterature]: selectLiterature,
},
groupProperties: {
[ids.groupId]: {
label: `textLocator.addressSearch.${ids.groupId}`,
resultDisplayMode: 'categorized',
limitResults: 3,
},
},
categoryProperties: {
[ids.categoryIdToponym]: {
label: 'textLocator.addressSearch.toponym',
},
[ids.categoryIdLiterature]: {
label: 'textLocator.addressSearch.literature',
},
},
afterResultComponent: ResultInfo,
}),
Expand Down
44 changes: 40 additions & 4 deletions packages/clients/textLocator/src/components/ResultInfo.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
<v-icon small> fa-info-circle </v-icon>
</span>
</template>
<v-simple-table dense>
<p v-if="isLiterature" class="result-info-title-display">
{{ featureAsLiterature.title }}
</p>
<v-simple-table v-else dense>
<template #default>
<thead>
<tr>
Expand All @@ -30,8 +33,14 @@
</thead>
<tbody>
<tr
v-for="{ ObjectID, Name, Sprache, Typ, Start, Ende } in feature
.properties.names"
v-for="{
ObjectID,
Name,
Sprache,
Typ,
Start,
Ende,
} in featureAsGeometry.properties.names"
:key="ObjectID"
>
<td>{{ Name }}</td>
Expand All @@ -48,13 +57,15 @@
<script lang="ts">
import Vue, { PropType } from 'vue'
import { GeometrySearchState } from '../plugins/GeometrySearch/types'
import { LiteratureFeature } from '../types'
export default Vue.extend({
name: 'ResultInfo',
props: {
feature: {
type: Object as PropType<
GeometrySearchState['featureCollection']['features'][number]
| GeometrySearchState['featureCollection']['features'][number]
| LiteratureFeature
>,
required: true,
},
Expand All @@ -73,9 +84,34 @@ export default Vue.extend({
default: -1,
},
},
computed: {
isLiterature() {
return Boolean(this.feature.properties.location_hits)
},
featureAsGeometry() {
return this
.feature as GeometrySearchState['featureCollection']['features'][number]
},
featureAsLiterature() {
return this.feature as LiteratureFeature
},
},
})
</script>

<style scoped>
.v-icon {
margin-left: 0.5em;
}
.result-info-title-display {
background: #f5f5f5;
color: #333;
max-width: 30ch;
padding: 0.5em 1em;
}
</style>

<style>
/* suppress table wrap; table looks fine as an element in itself */
.v-tooltip__content:has(.v-data-table) {
Expand Down
12 changes: 4 additions & 8 deletions packages/clients/textLocator/src/locales.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ const locales: LanguageOption[] = [
type: 'de',
resources: {
textLocator: {
// TODO temporary key, should be removed when no longer needed
notImplemented: 'Diese Funktion ist noch nicht implementiert.',
layers: {
[openStreetMap]: 'OpenStreetMap',
[openSeaMap]: 'OpenSeaMap',
Expand All @@ -34,6 +32,9 @@ const locales: LanguageOption[] = [
language: 'Sprache',
timeFrame: 'Zeitraum',
},
groupTL: 'Literatur- und Ortssuche',
toponym: 'Ortssuche',
literature: 'Literatursuche',
},
attributions: {
[openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © <a href='https://www.openstreetmap.org/copyright' target='_blank'>OpenStreetMap</a> contributors`,
Expand All @@ -57,15 +58,10 @@ const locales: LanguageOption[] = [
'Es gibt viele Ergebnisse zu der letzten Anfrage. Der Ladevorgang kann einen Moment länger dauern.',
},
error: {
searchCoastalGazetteer:
search:
'Die Suche ist mit einem unbekannten Fehler fehlgeschlagen. Bitte versuchen Sie es später erneut.',
},
},
plugins: {
addressSearch: {
defaultGroup: 'Ortssuche',
},
},
},
},
// NOTE: English translation not yet required and may be incomplete
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
</template>
{{ item.name }}
<template v-if="item.type === 'toponym' && item.feature">
&nbsp;
<ResultInfo
:tab-index="0"
:feature="item.feature"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,7 @@
import { PolarActionContext, PolarStore } from '@polar/lib-custom-types'
import { PolarActionContext } from '@polar/lib-custom-types'
import { GeometrySearchGetters, GeometrySearchState } from '../../types'
import {
TitleLocationFrequency,
searchLiteratureByToponym,
} from '../../../../utils/literatureByToponym'

export function setupWatchers(
this: PolarStore<GeometrySearchState, GeometrySearchGetters>,
{
dispatch,
rootGetters,
}: PolarActionContext<GeometrySearchState, GeometrySearchGetters>
) {
// load titleLocationFrequency on each featureCollection update
this.watch(
() => rootGetters['plugin/geometrySearch/featureCollection'],
() => dispatch('updateFrequencies')
)
}
import { searchLiteratureByToponym } from '../../../../utils/textLocatorBackend/literatureByToponym'
import { TitleLocationFrequency } from '../../../../types'

const requestLiteraturePerFeature = (
featureCollection: GeometrySearchState['featureCollection'],
Expand All @@ -28,35 +12,33 @@ const requestLiteraturePerFeature = (
.filter((names) => names.length)
.map((names) => searchLiteratureByToponym(textLocatorBackendUrl, names))

const aggregatePerFeatureId =
(featureCollection: GeometrySearchState['featureCollection']) =>
(
featureFrequency: TitleLocationFrequency,
index: number
): TitleLocationFrequency =>
Object.fromEntries(
Object.entries(featureFrequency).map(([literatureTitle, frequency]) => [
literatureTitle,
{
[featureCollection.features[index].id as string]: Object.values(
frequency
).reduce((accumulator, current) => accumulator + current, 0),
},
])
const aggregateFeatureHitsByLocationOfLiterature = (
featureCollection: GeometrySearchState['featureCollection'],
titleLocationFrequencies: TitleLocationFrequency[]
): TitleLocationFrequency =>
titleLocationFrequencies.reduce((accumulator, current, index) => {
Object.entries(current).forEach(
([literatureId, { title, location_frequency: locationFrequency }]) => {
accumulator[literatureId] = {
title,
location_frequency: {
...(accumulator[literatureId]?.location_frequency || {}),
...Object.fromEntries(
Object.entries(locationFrequency).map((entry) => [
featureCollection.features[index].id as string,
entry[1] +
(locationFrequency[
featureCollection.features[index].id as string
] || 0),
])
),
},
}
}
)

const flattenFrequencies = (
accumulator: TitleLocationFrequency,
current: TitleLocationFrequency
) => {
Object.entries(current).forEach(([title, findings]) => {
accumulator[title] = {
...(accumulator[title] || {}),
...findings,
}
})
return accumulator
}
return accumulator
}, {} as TitleLocationFrequency)

export async function updateFrequencies({
commit,
Expand All @@ -78,7 +60,9 @@ export async function updateFrequencies({
dispatch('changeActiveData', null)
return
}
const titleLocationFrequency = (

const titleLocationFrequency = aggregateFeatureHitsByLocationOfLiterature(
featureCollection,
await Promise.all(
requestLiteraturePerFeature(
featureCollection,
Expand All @@ -87,8 +71,6 @@ export async function updateFrequencies({
)
)
)
.map(aggregatePerFeatureId(featureCollection))
.reduce(flattenFrequencies, {})

commit('setTitleLocationFrequency', titleLocationFrequency)
if (Object.keys(titleLocationFrequency).length) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@ import { getEmptyFeatureCollection } from '../../../utils/coastalGazetteer/respo
import { makeTreeView } from '../utils/makeTreeView'
import { updateVectorLayer, vectorLayer } from '../utils/vectorDisplay'
import { geoJson } from '../../../utils/coastalGazetteer/common'
import { selectLiterature } from '../../../utils/textLocatorBackend/findLiterature/selectLiterature'
import { searchToponymByLiterature } from '../../../utils/textLocatorBackend/toponymByLiterature'
import { setupTooltip } from './actions/setupTooltip'
import { setupDrawReaction } from './actions/setupDrawReaction'
import { setupWatchers, updateFrequencies } from './actions/watchers'
import { updateFrequencies } from './actions/updateFrequencies'

let counter = 0
const searchLoadingKey = 'geometrySearchLoadingKey'
Expand All @@ -41,14 +43,11 @@ export const makeStoreModule = () => {
dispatch('setupDrawReaction')
dispatch('setupTooltip')
map.addLayer(vectorLayer)
// register watchers after store is ready (else immediate-like firing on not-really change)
setTimeout(() => dispatch('setupWatchers'), 0)
},
setupTooltip,
setupDrawReaction,
setupWatchers,
updateFrequencies,
searchGeometry({ rootGetters, commit }, feature: Feature) {
searchGeometry({ rootGetters, commit, dispatch }, feature: Feature) {
const loadingKey = getSearchLoadingKey()
commit('plugin/loadingIndicator/addLoadingKey', loadingKey, {
root: true,
Expand All @@ -61,7 +60,10 @@ export const makeStoreModule = () => {
rootGetters.configuration.geometrySearch.url,
rootGetters.configuration.epsg
)
.then((result) => commit('setFeatureCollection', result))
.then((result) => {
commit('setFeatureCollection', result)
dispatch('updateFrequencies')
})
.finally(() =>
commit('plugin/loadingIndicator/removeLoadingKey', loadingKey, {
root: true,
Expand All @@ -85,16 +87,23 @@ export const makeStoreModule = () => {
fullSearchOnToponym({ dispatch }, item: TreeViewItem) {
dispatch('searchGeometry', geoJson.readFeature(item.feature))
},
fullSearchLiterature({ dispatch }) {
dispatch(
'plugin/toast/addToast',
{
type: 'info',
text: 'common:textLocator.notImplemented',
timeout: 5000,
},
{ root: true }
async fullSearchLiterature(actionContext, item: TreeViewItem) {
const titleLocationFrequency = await searchToponymByLiterature(
// @ts-expect-error | added in polar-client.ts locally
actionContext.rootGetters.configuration.textLocatorBackendUrl,
item.id
)
selectLiterature.call(this, actionContext, {
categoryId: 0, // dummy to fit API
feature: {
type: 'Feature',
// fake geom to fit APIs; ignored by custom selectLiterature
geometry: { type: 'Point', coordinates: [0, 0] },
properties: titleLocationFrequency[item.id].location_frequency,
id: item.id,
title: item.name,
},
})
},
},
mutations: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
GeoJsonProperties,
Geometry,
} from 'geojson'
import { TitleLocationFrequency } from '../../utils/literatureByToponym'
import { TitleLocationFrequency } from '../../types'
import { ResponseGeom, ResponseName } from '../../utils/coastalGazetteer/types'

export type TextLocatorCategories = 'text' | 'toponym'
Expand Down
Loading

0 comments on commit 7afd7e5

Please sign in to comment.