From e95a5409908ddf39d798fc2d0fd1386bf882d2b8 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 23 Feb 2024 10:45:06 +0100 Subject: [PATCH 001/173] add client textLocator basic configuration/installation --- package.json | 3 + packages/clients/textLocator/CHANGELOG.md | 5 + packages/clients/textLocator/LICENSE | 287 ++++++++++++++++++ packages/clients/textLocator/README.md | 11 + packages/clients/textLocator/package.json | 37 +++ .../clients/textLocator/src/addPlugins.ts | 115 +++++++ packages/clients/textLocator/src/index.html | 37 +++ packages/clients/textLocator/src/locales.ts | 80 +++++ packages/clients/textLocator/src/mapConfig.ts | 103 +++++++ packages/clients/textLocator/src/palettes.ts | 54 ++++ .../clients/textLocator/src/polar-client.ts | 32 ++ packages/clients/textLocator/src/services.ts | 91 ++++++ packages/clients/textLocator/src/types.ts | 1 + packages/clients/textLocator/tsconfig.json | 3 + packages/clients/textLocator/vite.config.js | 5 + 15 files changed, 864 insertions(+) create mode 100644 packages/clients/textLocator/CHANGELOG.md create mode 100644 packages/clients/textLocator/LICENSE create mode 100644 packages/clients/textLocator/README.md create mode 100644 packages/clients/textLocator/package.json create mode 100644 packages/clients/textLocator/src/addPlugins.ts create mode 100644 packages/clients/textLocator/src/index.html create mode 100644 packages/clients/textLocator/src/locales.ts create mode 100644 packages/clients/textLocator/src/mapConfig.ts create mode 100644 packages/clients/textLocator/src/palettes.ts create mode 100644 packages/clients/textLocator/src/polar-client.ts create mode 100644 packages/clients/textLocator/src/services.ts create mode 100644 packages/clients/textLocator/src/types.ts create mode 100644 packages/clients/textLocator/tsconfig.json create mode 100644 packages/clients/textLocator/vite.config.js diff --git a/package.json b/package.json index 2c3be44cf..bb0f020b9 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,9 @@ "snowbox": "cd packages/clients/snowbox/ && vite --host", "snowbox:build": "lerna run build --scope @polar/client-snowbox --stream", "snowbox:build:serve": "http-server packages/clients/snowbox -o /dist/index.html", + "textLocator:build": "lerna run build --scope @polar/client-text-locator --stream", + "textLocator:build:serve": "http-server ./packages/clients/textLocator -o /dist/index.html", + "textLocator:dev": "cd packages/clients/textLocator/ && vite --host", "pages:build": "rimraf ./pages/docs && npm run generic:build && bash ./scripts/buildPages.sh", "pages:build:serve": "http-server pages -o index.html", "clean": "lerna clean && rimraf --glob packages/**/{.cache,dist,docs} && rimraf --glob {.cache,dist} && node ./scripts/clean", diff --git a/packages/clients/textLocator/CHANGELOG.md b/packages/clients/textLocator/CHANGELOG.md new file mode 100644 index 000000000..1237beb10 --- /dev/null +++ b/packages/clients/textLocator/CHANGELOG.md @@ -0,0 +1,5 @@ +# CHANGELOG + +## unpublished + +Initial release. diff --git a/packages/clients/textLocator/LICENSE b/packages/clients/textLocator/LICENSE new file mode 100644 index 000000000..c29ce2f83 --- /dev/null +++ b/packages/clients/textLocator/LICENSE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. \ No newline at end of file diff --git a/packages/clients/textLocator/README.md b/packages/clients/textLocator/README.md new file mode 100644 index 000000000..f0430d402 --- /dev/null +++ b/packages/clients/textLocator/README.md @@ -0,0 +1,11 @@ +# POLAR client TextLocator + +## Content + +The TextLocator client offers usage of the TextLocator backend that allows searching for papers in a geospatial fashion; that is, selecting a region will produce a list of papers found regarding that region. + +Please see the CHANGELOG.md for all changes after the initial release. + +## Usage + +The product is a hostable HTML page. diff --git a/packages/clients/textLocator/package.json b/packages/clients/textLocator/package.json new file mode 100644 index 000000000..ffb44a19d --- /dev/null +++ b/packages/clients/textLocator/package.json @@ -0,0 +1,37 @@ +{ + "name": "@polar/client-text-locator", + "version": "0.1.0", + "description": "Client TextLocator", + "license": "EUPL-1.2", + "type": "module", + "author": "Dataport AöR ", + "repository": { + "type": "git", + "url": "https://github.com/Dataport/polar.git", + "directory": "packages/clients/textLocator" + }, + "files": [ + "dist/**/**.*", + "CHANGELOG.md" + ], + "scripts": { + "postversion": "npm run build", + "build": "rimraf dist && vite build" + }, + "devDependencies": { + "@polar/core": "^1.4.1", + "@polar/lib-custom-types": "^1.4.1", + "@polar/plugin-address-search": "^1.2.1", + "@polar/plugin-attributions": "^1.2.1", + "@polar/plugin-draw": "1.1.0", + "@polar/plugin-gfi": "^1.2.2", + "@polar/plugin-icon-menu": "^1.2.0", + "@polar/plugin-layer-chooser": "^1.2.0", + "@polar/plugin-legend": "^1.1.0", + "@polar/plugin-loading-indicator": "^1.1.0", + "@polar/plugin-scale": "^1.1.0", + "@polar/plugin-toast": "^1.1.0", + "@polar/plugin-zoom": "^1.2.0", + "js-levenshtein": "^1.1.6" + } +} diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts new file mode 100644 index 000000000..662b53fdb --- /dev/null +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -0,0 +1,115 @@ +import { setLayout, NineLayout, NineLayoutTag } from '@polar/core' +import AddressSearch from '@polar/plugin-address-search' +import Attributions from '@polar/plugin-attributions' +import Draw from '@polar/plugin-draw' +import Gfi from '@polar/plugin-gfi' +import IconMenu from '@polar/plugin-icon-menu' +import LayerChooser from '@polar/plugin-layer-chooser' +import Legend from '@polar/plugin-legend' +import LoadingIndicator from '@polar/plugin-loading-indicator' +import Scale from '@polar/plugin-scale' +import Toast from '@polar/plugin-toast' +import Zoom from '@polar/plugin-zoom' + +import { idRegister } from './services' + +// this is fine for list-like setup functions +// eslint-disable-next-line max-lines-per-function +export const addPlugins = (core) => { + setLayout(NineLayout) + + core.addPlugins([ + IconMenu({ + displayComponent: true, + // TODO fix, it's broken ... + initiallyOpen: 'attributions', + menus: [ + { + plugin: LayerChooser({}), + icon: 'fa-layer-group', + id: 'layerChooser', + }, + { + plugin: Gfi({ + renderType: 'iconMenu', + coordinateSources: [], + layers: {}, + }), + icon: 'fa-location-pin', + id: 'gfi', + }, + { + plugin: Zoom({ renderType: 'iconMenu' }), + id: 'zoom', + }, + { + plugin: Attributions({ + renderType: 'iconMenu', + listenToChanges: [ + 'plugin/zoom/zoomLevel', + 'plugin/layerChooser/activeBackgroundId', + 'plugin/layerChooser/activeMaskIds', + ], + layerAttributions: idRegister.map((id) => ({ + id, + title: `textLocator.attributions.${id}`, + })), + staticAttributions: ['textLocator.attributions.static'], + }), + icon: 'fa-regular fa-copyright', + id: 'attributions', + }, + ], + layoutTag: NineLayoutTag.TOP_RIGHT, + }), + AddressSearch({ + displayComponent: true, + layoutTag: NineLayoutTag.TOP_LEFT, + minLength: 3, + waitMs: 500, + // @ts-expect-error | URL configured in a different way (simple API) + searchMethods: [{ type: 'coastalGazetteer' }], + addLoading: 'plugin/loadingIndicator/addLoadingKey', + removeLoading: 'plugin/loadingIndicator/removeLoadingKey', + customSearchMethods: { + /* TODO */ + }, + customSelectResult: { + /* TODO */ + }, + }), + Draw({ + displayComponent: false, + selectableDrawModes: ['Point', 'LineString', 'Circle', 'Polygon'], + style: { + fill: { + color: 'rgba(255, 255, 255, 0.5)', + }, + stroke: { + color: '#e51313', + width: 2, + }, + circle: { + radius: 7, + fillColor: '#e51313', + }, + }, + }), + Legend({ + displayComponent: true, + layoutTag: NineLayoutTag.BOTTOM_RIGHT, + }), + LoadingIndicator({ + displayComponent: true, + layoutTag: NineLayoutTag.MIDDLE_MIDDLE, + }), + Scale({ + displayComponent: true, + layoutTag: NineLayoutTag.BOTTOM_RIGHT, + }), + Toast({ + displayComponent: true, + layoutTag: NineLayoutTag.BOTTOM_MIDDLE, + }), + ]) +} diff --git a/packages/clients/textLocator/src/index.html b/packages/clients/textLocator/src/index.html new file mode 100644 index 000000000..fa622602f --- /dev/null +++ b/packages/clients/textLocator/src/index.html @@ -0,0 +1,37 @@ + + + + + + + TextLocator + + + +
+
+
+ + + diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts new file mode 100644 index 000000000..f4be2d8a3 --- /dev/null +++ b/packages/clients/textLocator/src/locales.ts @@ -0,0 +1,80 @@ +import { LanguageOption } from '@polar/lib-custom-types' +import { + openStreetMap, + openSeaMap, + mdiSeaNames, + wmtsTopplusOpenWeb, + wmtsTopplusOpenWebGrey, + wmtsTopplusOpenLight, + wmtsTopplusOpenLightGrey, +} from './services' + +// Gefundene Orte +// Gefundene Texte + +const locales: LanguageOption[] = [ + { + type: 'de', + resources: { + textLocator: { + layers: { + [openStreetMap]: 'OpenStreetMap', + [openSeaMap]: 'OpenSeaMap', + [mdiSeaNames]: 'Namensdienst Küste', + [wmtsTopplusOpenWeb]: 'TopPlusOpen (Web)', + [wmtsTopplusOpenWebGrey]: 'TopPlusOpen (Web, Grau)', + [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', + [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grau)', + }, + attributions: { + [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, + [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, + [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, + [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, + static: + '
Impressum', + }, + error: { + searchCoastalGazetteer: 'Die Suche ist fehlgeschlagen. TODO', + }, + }, + plugins: { + addressSearch: { + defaultGroup: 'Ortssuche', + }, + }, + }, + }, + { + type: 'en', + resources: { + textLocator: { + layers: { + [openStreetMap]: 'OpenStreetMap', + [openSeaMap]: 'OpenSeaMap', + [mdiSeaNames]: 'Coastal name service', + [wmtsTopplusOpenWeb]: 'TopPlusOpen (Web)', + [wmtsTopplusOpenWebGrey]: 'TopPlusOpen (Web, Grey)', + [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', + [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grey)', + }, + }, + attributions: { + [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, + [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, + [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, + [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, + static: + '
Legal notice (Impressum)', + }, + }, + }, +] + +export default locales diff --git a/packages/clients/textLocator/src/mapConfig.ts b/packages/clients/textLocator/src/mapConfig.ts new file mode 100644 index 000000000..579145a96 --- /dev/null +++ b/packages/clients/textLocator/src/mapConfig.ts @@ -0,0 +1,103 @@ +// number-only keys needed in layers object +/* eslint-disable @typescript-eslint/naming-convention */ +import { MapConfig } from '@polar/lib-custom-types' +import locales from './locales' +import { + openStreetMap, + openSeaMap, + mdiSeaNames, + wmtsTopplusOpenWeb, + wmtsTopplusOpenWebGrey, + wmtsTopplusOpenLight, + wmtsTopplusOpenLightGrey, +} from './services' +import { theme } from './palettes' + +let zoomLevel = 0 + +// NOTE: Only configure core properties here; plugin details in `addPlugins.ts` +export const mapConfiguration: Partial = { + startResolution: 132.291595229, + startCenter: [475496.3346486868, 5997512.151535563], + extent: [ + 288427.40898665343, 5888233.576754751, 880090.4063210202, 6188713.349959846, + ], + epsg: 'EPSG:25832', + locales, + vuetify: { theme }, + layers: [ + { + id: wmtsTopplusOpenLight, + type: 'background', + name: `textLocator.layers.${wmtsTopplusOpenLight}`, + visibility: true, + }, + { + id: wmtsTopplusOpenLightGrey, + type: 'background', + name: `textLocator.layers.${wmtsTopplusOpenLightGrey}`, + }, + { + id: wmtsTopplusOpenWeb, + type: 'background', + name: `textLocator.layers.${wmtsTopplusOpenWeb}`, + }, + { + id: wmtsTopplusOpenWebGrey, + type: 'background', + name: `textLocator.layers.${wmtsTopplusOpenWebGrey}`, + }, + { + id: openStreetMap, + type: 'background', + name: `textLocator.layers.${openStreetMap}`, + }, + { + id: openSeaMap, + type: 'mask', + name: `textLocator.layers.${openSeaMap}`, + visibility: true, + }, + { + id: mdiSeaNames, + type: 'mask', + name: `textLocator.layers.${mdiSeaNames}`, + visibility: true, + }, + ], + options: [ + { resolution: 2116.66552366, scale: 10000000, zoomLevel: zoomLevel++ }, + { resolution: 1058.33276183, scale: 5000000, zoomLevel: zoomLevel++ }, + { resolution: 529.166380916, scale: 2500000, zoomLevel: zoomLevel++ }, + { resolution: 264.583190458, scale: 1000000, zoomLevel: zoomLevel++ }, + { resolution: 132.291595229, scale: 500000, zoomLevel: zoomLevel++ }, + { resolution: 66.14579761460263, scale: 250000, zoomLevel: zoomLevel++ }, + { resolution: 26.458319045841044, scale: 100000, zoomLevel: zoomLevel++ }, + { resolution: 15.874991427504629, scale: 60000, zoomLevel: zoomLevel++ }, + { resolution: 10.583327618336419, scale: 40000, zoomLevel: zoomLevel++ }, + { resolution: 5.2916638091682096, scale: 20000, zoomLevel: zoomLevel++ }, + { resolution: 2.6458319045841048, scale: 10000, zoomLevel: zoomLevel++ }, + { resolution: 1.3229159522920524, scale: 5000, zoomLevel: zoomLevel++ }, + { resolution: 0.6614579761460262, scale: 2500, zoomLevel: zoomLevel++ }, + { resolution: 0.2645831904584105, scale: 1000, zoomLevel: zoomLevel++ }, + { resolution: 0.1322915952292052, scale: 500, zoomLevel: zoomLevel++ }, + ], + namedProjections: [ + [ + 'EPSG:31467', + '+proj=tmerc +lat_0=0 +lon_0=9 +k=1 +x_0=3500000 +y_0=0 +ellps=bessel +nadgrids=BETA2007.gsb +units=m +no_defs +type=crs', + ], + [ + 'EPSG:25832', + '+proj=utm +zone=32 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs', + ], + [ + 'EPSG:3857', + '+proj=merc +a=6378137 +b=6378137 +lat_ts=0.0 +lon_0=0.0 +x_0=0.0 +y_0=0 +k=1.0 +units=m +nadgrids=@null +wktext +no_defs', + ], + [ + 'EPSG:4326', + '+title=WGS 84 (long/lat) +proj=longlat +ellps=WGS84 +datum=WGS84 +no_defs', + ], + ], +} diff --git a/packages/clients/textLocator/src/palettes.ts b/packages/clients/textLocator/src/palettes.ts new file mode 100644 index 000000000..c34c72be5 --- /dev/null +++ b/packages/clients/textLocator/src/palettes.ts @@ -0,0 +1,54 @@ +export const dataportPalette = { + black: '#000000', + white: '#FFFFFF', + persianPlum: '#7D212B', + auburn: '#9E292B', + charcoal: '#3A424B', + cadet: '#54616E', + coolGrey: '#8A9199', + lightGray: '#CCD0D4', + antiFlashWhite: '#EEEFF1', + ghostWhite: '#F8F9F9', + slateGray: '#697896', + glitter: '#E0E8F8', + carolinaBlue: '#81BED7', + lightCyan: '#D9F3FF', + viridian: '#39756F', + antiFlashWhiteMint: '#E8F5F1', + gold: '#FFD500', +} + +export const pastelPalette = [ + '#66c2a5', + '#fc8d62', + '#8da0cb', + '#e78ac3', + '#a6d854', + '#ffd92f', + '#e5c494', + '#b3b3b3', +] + +export const heatPalette = [ + '#fff5f0', + '#fee2d5', + '#fcc3ac', + '#fca082', + '#fb7c5c', + '#f6553d', + '#e32f27', + '#c3161b', + '#9e0d14', + '#67000d', +] + +export const theme = { + themes: { + light: { + primary: dataportPalette.persianPlum, + primaryContrast: dataportPalette.white, + secondary: dataportPalette.cadet, + secondaryContrast: dataportPalette.white, + }, + }, +} diff --git a/packages/clients/textLocator/src/polar-client.ts b/packages/clients/textLocator/src/polar-client.ts new file mode 100644 index 000000000..4105c9314 --- /dev/null +++ b/packages/clients/textLocator/src/polar-client.ts @@ -0,0 +1,32 @@ +import client from '@polar/core' +import packageInfo from '../package.json' +import { addPlugins } from './addPlugins' +import { services as layerConf } from './services' +import { mapConfiguration } from './mapConfig' + +// eslint-disable-next-line no-console +console.log(`TextLocator map client running in version ${packageInfo.version}.`) +const containerId = 'polarstern' +addPlugins(client) + +interface TextLocatorParameters { + urls: { + backend: string + gazetteerClient: string + gazetteerWfs: string + } +} + +export async function initializeClient({ urls }: TextLocatorParameters) { + client.rawLayerList.initializeLayerList(layerConf) + mapConfiguration.layerConf = layerConf + mapConfiguration.addressSearch = { + // @ts-expect-error | rest configured in addPlugins.ts (simple API) + searchMethods: [{ url: urls.gazetteerClient }], + } + + return await client.createMap({ + containerId, + mapConfiguration, + }) +} diff --git a/packages/clients/textLocator/src/services.ts b/packages/clients/textLocator/src/services.ts new file mode 100644 index 000000000..b3b110f44 --- /dev/null +++ b/packages/clients/textLocator/src/services.ts @@ -0,0 +1,91 @@ +export const openStreetMap = 'openStreetMap' +export const openSeaMap = 'openSeaMap' +export const mdiSeaNames = 'mdiSeaNames' +export const wmtsTopplusOpenWeb = 'wmtsTopplusOpenWeb' +export const wmtsTopplusOpenWebGrey = 'wmtsTopplusOpenWebGrey' +export const wmtsTopplusOpenLight = 'wmtsTopplusOpenLight' +export const wmtsTopplusOpenLightGrey = 'wmtsTopplusOpenLightGrey' + +export const idRegister = [ + openStreetMap, + openSeaMap, + mdiSeaNames, + wmtsTopplusOpenWeb, + wmtsTopplusOpenWebGrey, + wmtsTopplusOpenLight, + wmtsTopplusOpenLightGrey, +] + +const layerNames = { + [wmtsTopplusOpenWeb]: 'web', + [wmtsTopplusOpenWebGrey]: 'web_grau', + [wmtsTopplusOpenLight]: 'web_light', + [wmtsTopplusOpenLightGrey]: 'web_light_grau', +} + +export const services = [ + ...[ + wmtsTopplusOpenLight, + wmtsTopplusOpenLightGrey, + wmtsTopplusOpenWeb, + wmtsTopplusOpenWebGrey, + ].map((id) => ({ + id, + capabilitiesUrl: + 'https://sgx.geodatenzentrum.de/wmts_topplus_open/1.0.0/WMTSCapabilities.xml', + urls: 'https://sgx.geodatenzentrum.de/wmts_topplus_open', + optionsFromCapabilities: true, + tileMatrixSet: 'EU_EPSG_25832_TOPPLUS', + typ: 'WMTS', + layers: layerNames[id], + legendURL: `https://sg.geodatenzentrum.de/wms_topplus_open?styles=&layer=${layerNames[id]}&service=WMS&format=image/png&sld_version=1.1.0&request=GetLegendGraphic&version=1.1.1`, + })), + { + id: openStreetMap, + urls: [ + 'https://a.tile.openstreetmap.org/{TileMatrix}/{TileCol}/{TileRow}.png', + 'https://b.tile.openstreetmap.org/{TileMatrix}/{TileCol}/{TileRow}.png', + 'https://c.tile.openstreetmap.org/{TileMatrix}/{TileCol}/{TileRow}.png', + ], + typ: 'WMTS', + format: 'image/png', + coordinateSystem: 'EPSG:3857', + origin: [-20037508.3428, 20037508.3428], + transparent: false, + tileSize: '256', + minScale: '1', + maxScale: '2500000', + tileMatrixSet: 'google3857', + requestEncoding: 'REST', + resLength: '20', + }, + { + id: openSeaMap, + urls: [ + 'https://tiles.openseamap.org/seamark/{TileMatrix}/{TileCol}/{TileRow}.png', + ], + typ: 'WMTS', + format: 'image/png', + coordinateSystem: 'EPSG:3857', + origin: [-20037508.3428, 20037508.3428], + transparent: true, + tileSize: '256', + minScale: '1', + maxScale: '2500000', + tileMatrixSet: 'google3857', + requestEncoding: 'REST', + resLength: '20', + }, + { + id: mdiSeaNames, + url: `https://mdi-de-dienste.org/geoserver_gaz/nokis/ows`, + typ: 'WMS', + layers: 'name_service', + legendURL: 'ignore', + format: 'image/png', + version: '1.3.0', + transparent: true, + singleTile: true, + STYLES: 'Seeseitig', + }, +] diff --git a/packages/clients/textLocator/src/types.ts b/packages/clients/textLocator/src/types.ts new file mode 100644 index 000000000..429751982 --- /dev/null +++ b/packages/clients/textLocator/src/types.ts @@ -0,0 +1 @@ +// TODO if this file stays empty, delete it diff --git a/packages/clients/textLocator/tsconfig.json b/packages/clients/textLocator/tsconfig.json new file mode 100644 index 000000000..618c6c3e9 --- /dev/null +++ b/packages/clients/textLocator/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../../tsconfig.json" +} diff --git a/packages/clients/textLocator/vite.config.js b/packages/clients/textLocator/vite.config.js new file mode 100644 index 000000000..84e1d7ed1 --- /dev/null +++ b/packages/clients/textLocator/vite.config.js @@ -0,0 +1,5 @@ +import { getClientConfig } from '../../../viteConfigs' + +export default getClientConfig({ + base: '', +}) From 80e37a4ca341414edec202150c723bff09795a23 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 23 Feb 2024 10:46:44 +0100 Subject: [PATCH 002/173] add draft store --- .../clients/textLocator/src/store/module.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/clients/textLocator/src/store/module.ts diff --git a/packages/clients/textLocator/src/store/module.ts b/packages/clients/textLocator/src/store/module.ts new file mode 100644 index 000000000..20b649176 --- /dev/null +++ b/packages/clients/textLocator/src/store/module.ts @@ -0,0 +1,20 @@ +// some names are defined by the environment +/* eslint-disable camelcase */ + +import { PolarModule } from '@polar/lib-custom-types' + +// TODO remove +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface TextLocatorGetters {} + +/* TextLocator VueX Store Module for system-specific contents. */ +export const textLocatorModule: PolarModule< + Record, + TextLocatorGetters +> = { + namespaced: true, + state: {}, + actions: {}, + mutations: {}, + getters: {}, +} From e108145ccdd12be302377f20c2f607242d0d1ae7 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 23 Feb 2024 10:51:18 +0100 Subject: [PATCH 003/173] add coastal gazetteer search draft --- .../clients/textLocator/src/addPlugins.ts | 7 +- .../textLocator/src/search/byGeometry.ts | 1 + .../src/search/coastalGazetteer.ts | 254 ++++++++++++++++++ 3 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 packages/clients/textLocator/src/search/byGeometry.ts create mode 100644 packages/clients/textLocator/src/search/coastalGazetteer.ts diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index 662b53fdb..a0e4fa7af 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -11,6 +11,7 @@ import Scale from '@polar/plugin-scale' import Toast from '@polar/plugin-toast' import Zoom from '@polar/plugin-zoom' +import { searchCoastalGazetteer } from './search/coastalGazetteer' import { idRegister } from './services' // this is fine for list-like setup functions @@ -71,11 +72,9 @@ export const addPlugins = (core) => { searchMethods: [{ type: 'coastalGazetteer' }], addLoading: 'plugin/loadingIndicator/addLoadingKey', removeLoading: 'plugin/loadingIndicator/removeLoadingKey', - customSearchMethods: { - /* TODO */ - }, + customSearchMethods: { coastalGazetteer: searchCoastalGazetteer }, customSelectResult: { - /* TODO */ + /* categoryDenkmalsucheAutocomplete: selectResult */ }, }), Draw({ diff --git a/packages/clients/textLocator/src/search/byGeometry.ts b/packages/clients/textLocator/src/search/byGeometry.ts new file mode 100644 index 000000000..a75baaf24 --- /dev/null +++ b/packages/clients/textLocator/src/search/byGeometry.ts @@ -0,0 +1 @@ +// TODO write search by geometry diff --git a/packages/clients/textLocator/src/search/coastalGazetteer.ts b/packages/clients/textLocator/src/search/coastalGazetteer.ts new file mode 100644 index 000000000..6f7f89de6 --- /dev/null +++ b/packages/clients/textLocator/src/search/coastalGazetteer.ts @@ -0,0 +1,254 @@ +// such names exist on the service +/* eslint-disable @typescript-eslint/naming-convention */ +import { CoreState } from '@polar/lib-custom-types' +import { Feature, FeatureCollection } from 'geojson' +import { Map } from 'ol' +import { GeoJSON, WKT } from 'ol/format' +import { Store } from 'vuex' +import levenshtein from 'js-levenshtein' + +const ignoreIds = { + global: ['EuroNat-33'], + geometries: [ + 'EuroNat-33', + 'SH-WATTENMEER-DM-1', + 'SH-WATTENMEER-1', + 'Ak2006-51529', + 'Landsg-2016-110', + ], +} +const wellKnownText = new WKT() +const geoJson = new GeoJSON() + +interface RequestPayload { + keyword: string + searchType: 'like' | 'exact' | 'id' + lang: '-' | string + sdate: string + edate: string + type: '-' | string + page?: string // numerical + geom?: string +} + +interface ResponseName { + Start: string // YYYY-MM-DD + Ende: string // YYYY-MM-DD + GeomID: string + ObjectID: string + Name: string + Quellen: object[] // not used + Rezent: boolean + Sprache: string + Typ: string +} + +interface ResponseGeom { + Start: string // YYYY-MM-DD + Ende: string // YYYY-MM-DD + GeomID: string + ObjectID: string + Quellen: object[] // not used + Typ: string + 'Typ Beschreibung': string + WKT: string // WKT geometry +} + +interface ResponseResult { + id: string + names: ResponseName[] + geoms: ResponseGeom[] +} + +interface ResponsePayload { + count: string // numerical + currentpage: string // numerical + pages: string // numerical + keyword: string + querystring: string + results: ResponseResult[] + time: number +} + +interface CoastalGazetteerParameters { + epsg: `EPSG:${string}` + map: Map +} + +// arbitrary sort based on input – prefer 1. startsWith 2. closer string +const sorter = + (searchPhrase: string, sortKey: string) => + (a: ResponseName | Feature, b: ResponseName | Feature) => { + const aStartsWith = a[sortKey].startsWith(searchPhrase) + const bStartsWith = b[sortKey].startsWith(searchPhrase) + + return aStartsWith && !bStartsWith + ? -1 + : !aStartsWith && bStartsWith + ? 1 + : levenshtein(a[sortKey], searchPhrase) - + levenshtein(b[sortKey], searchPhrase) + } + +const searchRequestDefaultPayload: Partial = { + searchType: 'like', + lang: '-', + sdate: '0001-01-01', + edate: new Date().toJSON().slice(0, 10), + type: '-', +} + +const getEmptyFeatureCollection = (): FeatureCollection => ({ + type: 'FeatureCollection', + features: [], +}) + +const getEmptyResponsePayload = (): ResponsePayload => ({ + count: '', + currentpage: '', + pages: '', + keyword: '', + querystring: '', + results: [], + time: NaN, +}) + +const mergeResponses = ( + initialResponse: ResponsePayload, + responses: ResponsePayload[] +) => ({ + ...initialResponse, + currentpage: 'merged', + results: [ + initialResponse.results, + ...responses.map(({ results }) => results), + ].flat(1), + time: NaN, // not used, setting NaN to indicate it's not the actual time +}) + +async function getAllPages( + this: Store, + signal: AbortSignal, + url: string, + inputValue: string, + page: string | undefined +): Promise { + const response = await fetch( + `${url}?${new URLSearchParams({ + ...searchRequestDefaultPayload, + keyword: `*${inputValue}*`, + ...(typeof page !== 'undefined' ? { page } : {}), + }).toString()}`, + { + method: 'GET', + signal, + } + ) + + if (!response.ok) { + this.dispatch('plugin/toast/addToast', { + type: 'error', + text: 'textLocator.error.searchCoastalGazetteer', // TODO use page || '1' + }) + return getEmptyResponsePayload() + } + const responsePayload: ResponsePayload = await response.json() + const pages = parseInt(responsePayload.pages, 10) + const initialRequestMerge = typeof page === 'undefined' && pages > 1 + + if (!initialRequestMerge) { + return responsePayload + } + + return mergeResponses( + responsePayload, + await Promise.all( + Array.from(Array(pages - 1)).map((_, index) => + getAllPages.call(this, signal, url, inputValue, `${index + 2}`) + ) + ) + ) +} + +const featurify = + (epsg: `EPSG:${string}`, searchPhrase: string) => + (feature: ResponseResult): Feature => ({ + type: 'Feature', + geometry: geoJson.writeGeometryObject( + wellKnownText.readGeometry( + feature.geoms.find( + (geom) => !ignoreIds.geometries.includes(geom.GeomID) + )?.WKT, + { + dataProjection: 'EPSG:4326', + featureProjection: epsg, + } + ) + ), + id: feature.id, + properties: { + names: feature.names, + geometries: feature.geoms.filter( + (geom) => !ignoreIds.geometries.includes(geom.GeomID) + ), + }, + // @ts-expect-error | used in POLAR for text display + title: + feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || 'Ohne Namen', // TODO i18n + }) + +const featureCollectionify = ( + fullResponse: ResponsePayload, + epsg: `EPSG:${string}`, + searchPhrase: string +): FeatureCollection => { + const featureCollection = getEmptyFeatureCollection() + featureCollection.features.push( + ...fullResponse.results.reduce((accumulator, feature) => { + if (ignoreIds.global.includes(feature.id)) { + return accumulator + } + try { + // TODO this shouldn't be try-catch, it's detectable + const featurified = featurify(epsg, searchPhrase)(feature) + accumulator.push(featurified) + return accumulator + } catch (e) { + return accumulator + } + }, [] as Feature[]) + ) + featureCollection.features = featureCollection.features.sort( + sorter(searchPhrase, 'title') + ) + return featureCollection +} + +export async function searchCoastalGazetteer( + this: Store, + signal: AbortSignal, + url: string, + inputValue: string, + queryParameters: CoastalGazetteerParameters +): Promise | never { + let fullResponse: ResponsePayload + try { + fullResponse = await getAllPages.call( + this, + signal, + url, + inputValue, + undefined + ) + } catch (e) { + console.error(e) + this.dispatch('plugin/toast/addToast', { + type: 'error', + text: 'textLocator.error.searchCoastalGazetteer', + }) + return getEmptyFeatureCollection() + } + return Promise.resolve( + featureCollectionify(fullResponse, queryParameters.epsg, inputValue) + ) +} From 7928be25266dc3eb8bad959ff23f3dab8afca087 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 23 Feb 2024 10:52:06 +0100 Subject: [PATCH 004/173] add client header --- .../clients/textLocator/src/addPlugins.ts | 5 +++ .../textLocator/src/plugins/Header/Header.vue | 42 +++++++++++++++++++ .../textLocator/src/plugins/Header/index.ts | 12 ++++++ .../src/plugins/Header/language.ts | 31 ++++++++++++++ 4 files changed, 90 insertions(+) create mode 100644 packages/clients/textLocator/src/plugins/Header/Header.vue create mode 100644 packages/clients/textLocator/src/plugins/Header/index.ts create mode 100644 packages/clients/textLocator/src/plugins/Header/language.ts diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index a0e4fa7af..6d385e4c4 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -11,6 +11,7 @@ import Scale from '@polar/plugin-scale' import Toast from '@polar/plugin-toast' import Zoom from '@polar/plugin-zoom' +import Header from './plugins/Header' import { searchCoastalGazetteer } from './search/coastalGazetteer' import { idRegister } from './services' @@ -20,6 +21,10 @@ export const addPlugins = (core) => { setLayout(NineLayout) core.addPlugins([ + Header({ + displayComponent: true, + layoutTag: NineLayoutTag.TOP_LEFT, + }), IconMenu({ displayComponent: true, // TODO fix, it's broken ... diff --git a/packages/clients/textLocator/src/plugins/Header/Header.vue b/packages/clients/textLocator/src/plugins/Header/Header.vue new file mode 100644 index 000000000..f574e3486 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/Header/Header.vue @@ -0,0 +1,42 @@ + + + + + + + diff --git a/packages/clients/textLocator/src/plugins/Header/index.ts b/packages/clients/textLocator/src/plugins/Header/index.ts new file mode 100644 index 000000000..142182c3d --- /dev/null +++ b/packages/clients/textLocator/src/plugins/Header/index.ts @@ -0,0 +1,12 @@ +import Vue from 'vue' +import { PluginOptions } from '@polar/lib-custom-types' +import Header from './Header.vue' +import language from './language' + +export default (options: PluginOptions) => (instance: Vue) => + instance.$store.dispatch('addComponent', { + name: 'header', + plugin: Header, + language, + options, + }) diff --git a/packages/clients/textLocator/src/plugins/Header/language.ts b/packages/clients/textLocator/src/plugins/Header/language.ts new file mode 100644 index 000000000..825ad2121 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/Header/language.ts @@ -0,0 +1,31 @@ +import { LanguageOption } from '@polar/lib-custom-types' + +const lang: LanguageOption[] = [ + { + type: 'de', + resources: { + plugins: { + textLocator: { + header: { + // remove lang="en" in component if this becomes german + text: 'TextLocator', + }, + }, + }, + }, + }, + { + type: 'en', + resources: { + plugins: { + textLocator: { + header: { + text: 'TextLocator', + }, + }, + }, + }, + }, +] + +export default lang From af88f80b3bb281ad966d1ec89046a8d2cbddbdce Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Mon, 26 Feb 2024 10:13:18 +0100 Subject: [PATCH 005/173] improve logging/dev access --- packages/clients/textLocator/src/index.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/clients/textLocator/src/index.html b/packages/clients/textLocator/src/index.html index fa622602f..96bd85040 100644 --- a/packages/clients/textLocator/src/index.html +++ b/packages/clients/textLocator/src/index.html @@ -27,11 +27,11 @@ initializeClient({ urls: { - backend: 'https://textlocator.ai.dataport.de/api/', - gazetteerClient: 'https://mdi-de-dienste.org/GazetteerClient/search', - gazetteerWfs: 'https://mdi-de-dienste.org/geoserver_gazClient/nokis/ows' + textLocatorBackend: 'https://textlocator.ai.dataport.de/api/', + gazetteerClient: 'https://mdi-de-dienste.org/GazetteerClient/search' } - }) + }).then(console.info.bind(null, 'Map client instance:')) + .catch(console.error.bind(null, 'Map client setup failed:')) From 5af2eeb7999c8f93054a694f7c50bda45d5b90b4 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Mon, 26 Feb 2024 10:14:08 +0100 Subject: [PATCH 006/173] reorder contents, remove gfi (not needed) --- packages/clients/textLocator/TODO.md | 28 +++ packages/clients/textLocator/heap/map.js | 144 +++++++++++ .../textLocator/heap/requestHandling.js | 233 ++++++++++++++++++ packages/clients/textLocator/package.json | 1 - .../clients/textLocator/src/addPlugins.ts | 52 ++-- .../components/GeometrySearch.vue | 17 ++ .../GeometrySearch/components/index.ts | 1 + .../src/plugins/GeometrySearch/index.ts | 18 ++ .../src/plugins/GeometrySearch/language.ts | 32 +++ .../src/plugins/GeometrySearch/store/index.ts | 33 +++ .../src/plugins/GeometrySearch/types.ts | 3 + .../textLocator/src/search/byGeometry.ts | 1 - packages/clients/textLocator/src/types.ts | 1 - .../utils/coastalGazetteer/geometrySearch.ts | 3 + .../utils/coastalGazetteer/makeRequestUrl.ts | 47 ++++ .../coastalGazetteer/toponymSearch.ts} | 35 +-- .../src/utils/literatureByToponym.ts | 0 17 files changed, 590 insertions(+), 59 deletions(-) create mode 100644 packages/clients/textLocator/TODO.md create mode 100644 packages/clients/textLocator/heap/map.js create mode 100644 packages/clients/textLocator/heap/requestHandling.js create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/components/index.ts create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/index.ts create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/language.ts create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts create mode 100644 packages/clients/textLocator/src/plugins/GeometrySearch/types.ts delete mode 100644 packages/clients/textLocator/src/search/byGeometry.ts delete mode 100644 packages/clients/textLocator/src/types.ts create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts rename packages/clients/textLocator/src/{search/coastalGazetteer.ts => utils/coastalGazetteer/toponymSearch.ts} (90%) create mode 100644 packages/clients/textLocator/src/utils/literatureByToponym.ts diff --git a/packages/clients/textLocator/TODO.md b/packages/clients/textLocator/TODO.md new file mode 100644 index 000000000..b91ff8f9e --- /dev/null +++ b/packages/clients/textLocator/TODO.md @@ -0,0 +1,28 @@ +# TODO + +## MUSS + +* Liste ermittelter Orte +* Liste ermittelter Texte mit Ortsbezug +* Ortsbezüge eines Textes graphisch darstellen +* Suchgeometrien: Rechtecke, Punkte +* Suchgeometrien: Ortspolygone (Adresssuche?) +* Kontextsuche: Filtern ermittelter Texte (???) +* Relevanzbewertung von Ortsbezügen (UI nur Count?) + +## SOLL + +* Häufigkeit von Funden +* Dataport-Geo-Design +* Ortsbezüge von Texten als Ortsgebirge (?) +* Zugriff auf weitere Geodaten (???) +* Schreibweisen/Sprachen von Orten berücksichtigen +* Historische Ortspolygone beachten +* Suchgeometrien: Kreise, Vielecke +* Ortssuche + +## KANN + +* 3D-Darstellung Ortsgebirge +* Höhenlinien Ortsgebirge +* Farbliche Markieren Ortsgebirge diff --git a/packages/clients/textLocator/heap/map.js b/packages/clients/textLocator/heap/map.js new file mode 100644 index 000000000..458d6c59d --- /dev/null +++ b/packages/clients/textLocator/heap/map.js @@ -0,0 +1,144 @@ +import { Feature, Map, View } from 'ol' +import TileLayer from 'ol/layer/Tile' +import TileWMS from 'ol/source/TileWMS.js' +import OSM, { ATTRIBUTION } from 'ol/source/OSM' +import { getTransform } from 'ol/proj.js' +import WKT from 'ol/format/WKT.js' +import Polygon from 'ol/geom/Polygon.js' +import VectorSource from 'ol/source/Vector' +import VectorLayer from 'ol/layer/Vector' +import Overlay from 'ol/Overlay.js' +import { Fill, Stroke, Style } from 'ol/style.js' +import { Control, defaults as defaultControls } from 'ol/control.js' + +const WKTfmt = new WKT() + +const resultFeatureStyle = [ + new Style({ + stroke: new Stroke({ + color: 'blue', + width: 3, + }), + fill: new Fill({ + color: 'rgba(0, 0, 255, 0.1)', + }), + }), +] +const resultVectorLayer = new VectorLayer({ + source: new VectorSource({ + features: resultFeatures, + style: resultFeatureStyle, + }), +}) +// used to display the colored Geometries when clicking a location +const geometriesVectorLayer = new VectorLayer({ + source: new VectorSource({ + features: [], + style: resultFeatureStyle, + }), +}) +geometriesVectorLayer.setOpacity(0.7) + +// for coloring all geometries to all locations found in a title +const titleGeometriesVectorLayer = new VectorLayer({ + source: new VectorSource({ + features: [], + style: resultFeatureStyle, + }), +}) +titleGeometriesVectorLayer.setOpacity(0.8) + +const popupOverlay = new Overlay({ + element: popupContainer, + autoPan: { + animation: { + duration: 250, + }, + }, +}) + +popupCloser.onclick = function () { + popupOverlay.setPosition(undefined) + popupCloser.blur() + return false + +class ResetLocationGeometries extends Control { + geometriesVectorLayer.getSource().clear() + +class ResetTextGeometries extends Control { + titleGeometriesVectorLayer.getSource().clear() + + overlays: [popupOverlay] + + showPopup(text, coordinate) { + popupOverlay.setPosition(coordinate) + this.popupContent.innerHTML = text + + displayUserSelectedPoly(geometry) { + const displayPoly = geometry.clone() + displayPoly.applyTransform(getTransform('EPSG:4326', 'EPSG:3857')) + this.resultVectorLayer.getSource().getFeatures()[0].setGeometry(displayPoly) + + removeUserSelectedPoly() { + this.resultVectorLayer + .getSource() + .getFeatures()[0] + .setGeometry(new Polygon([])) + + displayGeometriesOnMap(geometriesList) { + this.resultVectorLayer.getSource().clear() + this.resultVectorLayer.setOpacity(0.45) + geometriesList.forEach((wktGeom) => { + const feature = WKTfmt.readFeature(wktGeom, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857', + }) + this.resultVectorLayer.getSource().addFeature(feature) + + colorGeometries(locationName, nameGeometryDict) { + this.geometriesVectorLayer.getSource().clear() + const geometriesList = nameGeometryDict[locationName] + if (geometriesList && Array.isArray(geometriesList)) { + let geometryCounter = 0 + geometriesList.forEach((wktGeom) => { + const style = new Style({ + fill: new Fill({ + color: colorPalette[geometryCounter], + }), + }) + const feature = WKTfmt.readFeature(wktGeom, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857', + }) + feature.setStyle(style) + this.geometriesVectorLayer.getSource().addFeature(feature) + geometryCounter++ + }) + } else { + console.log('No Geometries for this location') + + colorGeometriesMultipleLocations( + article_title, + titleLocationFreqDict, + nameGeometryDict + ) { + this.titleGeometriesVectorLayer.getSource().clear() + const locationsFreqs = titleLocationFreqDict[article_title] + for (const locationName in locationsFreqs) { + const geometriesList = nameGeometryDict[locationName] + if (geometriesList && Array.isArray(geometriesList)) { + geometriesList.forEach((wktGeom) => { + const style = new Style({ + fill: new Fill({ + color: heatColorPalette[locationsFreqs[locationName]], + }), + }) + const feature = WKTfmt.readFeature(wktGeom, { + dataProjection: 'EPSG:4326', + featureProjection: 'EPSG:3857', + }) + feature.setStyle(style) + this.titleGeometriesVectorLayer.getSource().addFeature(feature) + }) + } else { + console.log('No Geometries for this location') diff --git a/packages/clients/textLocator/heap/requestHandling.js b/packages/clients/textLocator/heap/requestHandling.js new file mode 100644 index 000000000..8d0c4b373 --- /dev/null +++ b/packages/clients/textLocator/heap/requestHandling.js @@ -0,0 +1,233 @@ +import WKT from 'ol/format/WKT.js' + +const ignoreIdsName = ['EuroNat-33'] +const ignoreIdsGeometries = [ + 'EuroNat-33', + 'SH-WATTENMEER-DM-1', + 'SH-WATTENMEER-1', + 'Ak2006-51529', + 'Landsg-2016-110', +] +const nameGeometryDict = {} +let titleLocationFreqDict = {} + +const WKTfmt = new WKT() + +// endpoint configuration - load endpoints from a json file and fall back to default config if that json is not available +const defaultConfig = { + backend_endpoint: 'http://localhost:8000', +} + +let config_data = null +async function loadConfig() { + if (config_data != null) { + return config_data + } + try { + const response = await fetch('./textloc-config.json') + if (response.ok) { + const config = await response.json() + config_data = config + return config + } + console.error( + 'config request failed:', + response.status, + response.statusText + ) + return defaultConfig + } catch (error) { + console.error('config request error:', error) + return defaultConfig + } +} + +class requestHandler { + constructor() {} +} + +/** + * Extract all location names and geometries from the gazetteer response json while skipping names that have an ignoreId + * example for data value: [{"id":"shnwattdt075","names":[{"ObjectID":"shnwatthist1912003","GeomID":"shnwattdt075","Start":"1868-01-01","Ende":"1875-12-31","Name":"Bielshöven","Rezent":false,"Sprache":"Hochdeutsch","Typ":"früherer Name, hist. Name","Quellen":[{"Autor":"F.A. Meyer","Datum":"1875","Titel":"Elbemündung ( Katalognummer 81)","Media":"A.W. Lang, Historisches Seekartenwerk der Deutschen Bucht ( Neumünster 1969-1981)","Ort":"Hamburg"},{"Autor":"F.A. Meyer","Datum":"1868","Titel":"Einsegelung in die Elbe ( Katalognummer 75)","Media":"A.W. Lang, Historisches Seekartenwerk der Deutschen Bucht ( Neumünster 1969-1981)","Ort":"Hamburg"}]}, + * @param {dictionary} data + * @param {list of string} ignoreIds + * @returns + */ +function extractNamesAndGeometries(data) { + const namesList = [] + const allGeometriesList = [] + data.forEach((entry) => { + if (!ignoreIdsName.includes(entry.id)) { + if (entry.names && Array.isArray(entry.names)) { + entry.names.forEach((nameObj) => { + if (nameObj.Name) { + namesList.push(nameObj.Name) + } + if (!ignoreIdsGeometries.includes(entry.id)) { + const geometriesList = [] + if (entry.geoms && Array.isArray(entry.geoms)) { + entry.geoms.forEach((geomObj) => { + if (geomObj.WKT) { + allGeometriesList.push(geomObj.WKT) + geometriesList.push(geomObj.WKT) + } + }) + } + nameGeometryDict[nameObj.Name] = geometriesList + } + }) + } + } + }) + return [namesList, allGeometriesList, nameGeometryDict] +} + +/** + * send a request to our own backend to search for articles that contain the location names returned by the kuesten gazetteer. + * Each location has their own BM25 search, such that the frequencies can be determined for each location + * @param {*} locationNamesArray + * @returns {dictionary} - dictionary that contains the found article titles as keys and another dict with location : frequency pairs + * as values + */ +async function locationLookupIndividually(locationNamesArray) { + const config = await loadConfig() + const url = config.backendEndpoint + 'lookup/locations_individually' + + const locationsArray = { location_names: locationNamesArray } + const requestData = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(locationsArray), + } + + try { + const response = await fetch(url, requestData) + if (response.ok) { + const data = await response.json() + return data + } + console.error('Request failed:', response.status, response.statusText) + return null + } catch (error) { + console.error('Request error:', error) + return null + } +} + +/** + * send a request to our own backend to search for articles that contain the location names returned by the kuesten gazetteer. + * All locations are run through the BM25 algorithm bundled together + * @param {string} name - comma seperated location names + * @returns + */ +async function locationLookup(name) { + const config = await loadConfig() + const url = config.backendEndpoint + 'lookup/location_name' + + const requestData = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ name }), + } + + try { + const response = await fetch(url, requestData) + if (response.ok) { + const data = await response.json() + return data + } + console.error('Request failed:', response.status, response.statusText) + return null + } catch (error) { + console.error('Request error:', error) + return null + } +} + +/** + * Main function to run when user clicks on the map. Requests locations from the Gazetteer and searches locations in articles via + * our backend + * @param {*} geometry + * @param {*} popup_position + * @param {*} mapClass + * @param {*} htmlCreator + * @returns + */ +export const searchAction = async ( + geometry, + popup_position, + mapClass, + htmlCreator +) => { + mapClass.showPopup('Suche nach Ortsnamen...', popup_position) + mapClass.displayUserSelectedPoly(geometry) + let resultJSON = await requestResultJSON(geometry, 1) + console.log('fetched result') + console.log(resultJSON) + if (resultJSON == undefined) { + mapClass.showPopup( + 'Die Suche nach Ortsnamen ist fehlgeschlagen.', + popup_position + ) + return + } + var [locationNamesArray, geometriesList, nameGeometryDict] = + extractNamesAndGeometries(resultJSON.results) + console.log(locationNamesArray) + console.log(geometriesList) + console.log(nameGeometryDict) + if (resultJSON.pages > 1) { + for (let page = 2; page <= resultJSON.pages; page++) { + mapClass.showPopup( + 'Suche nach Ortsnamen... ' + page + ' / ' + resultJSON.pages, + popup_position + ) + resultJSON = await requestResultJSON(geometry, page) + var [locationNamesPages, geometriesPages, nameGeometryDict] = + extractNamesAndGeometries(resultJSON.results) + locationNamesArray = locationNamesArray.concat(locationNamesPages) + geometriesList = geometriesList.concat(geometriesPages) + } + } + + htmlCreator.populateSidebarLocations( + locationNamesArray, + mapClass, + nameGeometryDict + ) + mapClass.popupCloser.onclick() + const locationNames = locationNamesArray.join(',') + console.log('location names: ' + locationNames) + mapClass.displayGeometriesOnMap(geometriesList) + + mapClass.showPopup('Suche nach passenden Texten...', popup_position) + const lookupResults = await locationLookupIndividually(locationNamesArray) + if (lookupResults == null) { + mapClass.showPopup( + 'Die Suche nach passenden Texten ist fehlgeschlagen.', + popup_position + ) + return + } + + titleLocationFreqDict = lookupResults.title_location_freq + + let result_text = + "
Suchergebnisse
Suchbegriffe: " + + locationNames + + '
' + for (const article_title in titleLocationFreqDict) { + console.log(article_title) + result_text += '
' + article_title + '
\n' + } + mapClass.popupCloser.onclick() + htmlCreator.populateSidebarTexts( + titleLocationFreqDict, + nameGeometryDict, + mapClass + ) +} diff --git a/packages/clients/textLocator/package.json b/packages/clients/textLocator/package.json index ffb44a19d..690c32b35 100644 --- a/packages/clients/textLocator/package.json +++ b/packages/clients/textLocator/package.json @@ -24,7 +24,6 @@ "@polar/plugin-address-search": "^1.2.1", "@polar/plugin-attributions": "^1.2.1", "@polar/plugin-draw": "1.1.0", - "@polar/plugin-gfi": "^1.2.2", "@polar/plugin-icon-menu": "^1.2.0", "@polar/plugin-layer-chooser": "^1.2.0", "@polar/plugin-legend": "^1.1.0", diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index 6d385e4c4..16d7ab946 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -2,7 +2,6 @@ import { setLayout, NineLayout, NineLayoutTag } from '@polar/core' import AddressSearch from '@polar/plugin-address-search' import Attributions from '@polar/plugin-attributions' import Draw from '@polar/plugin-draw' -import Gfi from '@polar/plugin-gfi' import IconMenu from '@polar/plugin-icon-menu' import LayerChooser from '@polar/plugin-layer-chooser' import Legend from '@polar/plugin-legend' @@ -12,7 +11,8 @@ import Toast from '@polar/plugin-toast' import Zoom from '@polar/plugin-zoom' import Header from './plugins/Header' -import { searchCoastalGazetteer } from './search/coastalGazetteer' +import GeometrySearch from './plugins/GeometrySearch' +import { searchCoastalGazetteer } from './utils/coastalGazetteer/toponymSearch' import { idRegister } from './services' // this is fine for list-like setup functions @@ -28,43 +28,22 @@ export const addPlugins = (core) => { IconMenu({ displayComponent: true, // TODO fix, it's broken ... - initiallyOpen: 'attributions', + initiallyOpen: 'geometrySearch', menus: [ + { + plugin: GeometrySearch({}), + icon: 'fa-solid fa-magnifying-glass-location', + id: 'geometrySearch', + }, { plugin: LayerChooser({}), icon: 'fa-layer-group', id: 'layerChooser', }, - { - plugin: Gfi({ - renderType: 'iconMenu', - coordinateSources: [], - layers: {}, - }), - icon: 'fa-location-pin', - id: 'gfi', - }, { plugin: Zoom({ renderType: 'iconMenu' }), id: 'zoom', }, - { - plugin: Attributions({ - renderType: 'iconMenu', - listenToChanges: [ - 'plugin/zoom/zoomLevel', - 'plugin/layerChooser/activeBackgroundId', - 'plugin/layerChooser/activeMaskIds', - ], - layerAttributions: idRegister.map((id) => ({ - id, - title: `textLocator.attributions.${id}`, - })), - staticAttributions: ['textLocator.attributions.static'], - }), - icon: 'fa-regular fa-copyright', - id: 'attributions', - }, ], layoutTag: NineLayoutTag.TOP_RIGHT, }), @@ -99,6 +78,21 @@ export const addPlugins = (core) => { }, }, }), + Attributions({ + displayComponent: true, + layoutTag: NineLayoutTag.BOTTOM_RIGHT, + initiallyOpen: true, + listenToChanges: [ + 'plugin/zoom/zoomLevel', + 'plugin/layerChooser/activeBackgroundId', + 'plugin/layerChooser/activeMaskIds', + ], + layerAttributions: idRegister.map((id) => ({ + id, + title: `textLocator.attributions.${id}`, + })), + staticAttributions: ['textLocator.attributions.static'], + }), Legend({ displayComponent: true, layoutTag: NineLayoutTag.BOTTOM_RIGHT, diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue new file mode 100644 index 000000000..be8a89e6b --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue @@ -0,0 +1,17 @@ + + + + + diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/components/index.ts new file mode 100644 index 000000000..bf61d94d6 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/index.ts @@ -0,0 +1 @@ +export { default as GeometrySearch } from './GeometrySearch.vue' diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts new file mode 100644 index 000000000..8e7b6a305 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts @@ -0,0 +1,18 @@ +import Vue from 'vue' +import { PluginOptions } from '@polar/lib-custom-types' +import { GeometrySearch } from './components' +import language from './language' +import { makeStoreModule } from './store' + +interface GeometrySearchConfiguration extends PluginOptions { + thing: string +} + +export default (options: GeometrySearchConfiguration) => (instance: Vue) => + instance.$store.dispatch('addComponent', { + name: 'geometrySearch', + plugin: GeometrySearch, + language, + storeModule: makeStoreModule(), + options, + }) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts new file mode 100644 index 000000000..7481ff33a --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts @@ -0,0 +1,32 @@ +import { LanguageOption } from '@polar/lib-custom-types' + +const language: LanguageOption[] = [ + { + type: 'de', + resources: { + plugins: { + iconMenu: { + hints: { + geometrySearch: 'Geometriesuche', + }, + }, + geometrySearch: {}, + }, + }, + }, + { + type: 'en', + resources: { + plugins: { + iconMenu: { + hints: { + geometrySearch: 'Geometry search', + }, + }, + geometrySearch: {}, + }, + }, + }, +] + +export default language diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts new file mode 100644 index 000000000..5971690c9 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts @@ -0,0 +1,33 @@ +import { + generateSimpleGetters, + generateSimpleMutations, +} from '@repositoryname/vuex-generators' +import { PolarModule } from '@polar/lib-custom-types' +import { GeometrySearchState, GeometrySearchGetters } from '../types' + +const getInitialState = (): GeometrySearchState => ({}) + +// OK for module creation +// eslint-disable-next-line max-lines-per-function +export const makeStoreModule = () => { + const storeModule: PolarModule = { + namespaced: true, + state: getInitialState(), + actions: { + setupModule({ + commit, + dispatch, + getters: { listenToChanges, renderType }, + rootGetters, + }): void {}, + }, + mutations: { + ...generateSimpleMutations(getInitialState()), + }, + getters: { + ...generateSimpleGetters(getInitialState()), + }, + } + + return storeModule +} diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts new file mode 100644 index 000000000..3a13108d5 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts @@ -0,0 +1,3 @@ +export interface GeometrySearchState {} + +export type GeometrySearchGetters = GeometrySearchState diff --git a/packages/clients/textLocator/src/search/byGeometry.ts b/packages/clients/textLocator/src/search/byGeometry.ts deleted file mode 100644 index a75baaf24..000000000 --- a/packages/clients/textLocator/src/search/byGeometry.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO write search by geometry diff --git a/packages/clients/textLocator/src/types.ts b/packages/clients/textLocator/src/types.ts deleted file mode 100644 index 429751982..000000000 --- a/packages/clients/textLocator/src/types.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO if this file stays empty, delete it diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts new file mode 100644 index 000000000..dbc2454ce --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts @@ -0,0 +1,3 @@ +// https://mdi-de-dienste.org/GazetteerClient/search?geom=POINT(8.511120645998783 54.10846498210793)&keyword=&searchType=like&lang=-&sdate=0001-01-01&edate=2030-08-16&type=-&page=1 + +// https://mdi-de-dienste.org/GazetteerClient/search?geom=POLYGON%28%288.621975250313447+54.161985993052355%2C8.621975250313447+54.14485476891343%2C8.65430784323856+54.14485476891343%2C8.65430784323856+54.161985993052355%2C8.621975250313447+54.161985993052355%29%29&keyword=&searchType=like&lang=-&sdate=0001-01-01&edate=2030-08-16&type=-&page=1 diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts new file mode 100644 index 000000000..153ecbd38 --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts @@ -0,0 +1,47 @@ +import { Geometry } from 'geojson' +import { GeoJSON, WKT } from 'ol/format' + +const geoJson = new GeoJSON() +const wellKnownText = new WKT() + +interface RequestPayload { + keyword: string + searchType: 'like' | 'exact' | 'id' + lang: '-' | string + sdate: string + edate: string + type: '-' | string + page?: string // numerical + geom?: string +} + +const searchRequestDefaultPayload: Partial = { + searchType: 'like', + lang: '-', + sdate: '0001-01-01', + edate: new Date().toJSON().slice(0, 10), + type: '-', +} + +export const makeRequestUrl = ( + url: string, + inputValue: string, + page: string | undefined, + geometry: Geometry | undefined, + epsg: string +): string => + `${url}?${new URLSearchParams({ + ...searchRequestDefaultPayload, + keyword: inputValue ? `*${inputValue}*` : '', + ...(typeof page !== 'undefined' ? { page } : {}), + ...(typeof geometry !== 'undefined' + ? { + geom: wellKnownText.writeGeometry( + geoJson.readGeometry(geometry, { + dataProjection: epsg, + featureProjection: 'EPSG:4326', + }) + ), + } + : {}), + }).toString()}` diff --git a/packages/clients/textLocator/src/search/coastalGazetteer.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts similarity index 90% rename from packages/clients/textLocator/src/search/coastalGazetteer.ts rename to packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts index 6f7f89de6..65c3d0bd4 100644 --- a/packages/clients/textLocator/src/search/coastalGazetteer.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts @@ -6,6 +6,7 @@ import { Map } from 'ol' import { GeoJSON, WKT } from 'ol/format' import { Store } from 'vuex' import levenshtein from 'js-levenshtein' +import { makeRequestUrl } from './makeRequestUrl' const ignoreIds = { global: ['EuroNat-33'], @@ -20,17 +21,6 @@ const ignoreIds = { const wellKnownText = new WKT() const geoJson = new GeoJSON() -interface RequestPayload { - keyword: string - searchType: 'like' | 'exact' | 'id' - lang: '-' | string - sdate: string - edate: string - type: '-' | string - page?: string // numerical - geom?: string -} - interface ResponseName { Start: string // YYYY-MM-DD Ende: string // YYYY-MM-DD @@ -90,14 +80,6 @@ const sorter = levenshtein(b[sortKey], searchPhrase) } -const searchRequestDefaultPayload: Partial = { - searchType: 'like', - lang: '-', - sdate: '0001-01-01', - edate: new Date().toJSON().slice(0, 10), - type: '-', -} - const getEmptyFeatureCollection = (): FeatureCollection => ({ type: 'FeatureCollection', features: [], @@ -131,14 +113,11 @@ async function getAllPages( signal: AbortSignal, url: string, inputValue: string, - page: string | undefined + page: string | undefined, + epsg: `EPSG:${string}` ): Promise { const response = await fetch( - `${url}?${new URLSearchParams({ - ...searchRequestDefaultPayload, - keyword: `*${inputValue}*`, - ...(typeof page !== 'undefined' ? { page } : {}), - }).toString()}`, + makeRequestUrl(url, inputValue, page, undefined, epsg), { method: 'GET', signal, @@ -164,7 +143,7 @@ async function getAllPages( responsePayload, await Promise.all( Array.from(Array(pages - 1)).map((_, index) => - getAllPages.call(this, signal, url, inputValue, `${index + 2}`) + getAllPages.call(this, signal, url, inputValue, `${index + 2}`, epsg) ) ) ) @@ -175,6 +154,7 @@ const featurify = (feature: ResponseResult): Feature => ({ type: 'Feature', geometry: geoJson.writeGeometryObject( + // NOTE currently only the first geometry is used wellKnownText.readGeometry( feature.geoms.find( (geom) => !ignoreIds.geometries.includes(geom.GeomID) @@ -238,7 +218,8 @@ export async function searchCoastalGazetteer( signal, url, inputValue, - undefined + undefined, + queryParameters.epsg ) } catch (e) { console.error(e) diff --git a/packages/clients/textLocator/src/utils/literatureByToponym.ts b/packages/clients/textLocator/src/utils/literatureByToponym.ts new file mode 100644 index 000000000..e69de29bb From 29384e627da7428bba6f23ee0616ebdc3bc015f8 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Mon, 26 Feb 2024 11:48:24 +0100 Subject: [PATCH 007/173] delete unused code fragments --- packages/clients/textLocator/heap/map.js | 144 ----------- .../textLocator/heap/requestHandling.js | 233 ------------------ 2 files changed, 377 deletions(-) delete mode 100644 packages/clients/textLocator/heap/map.js delete mode 100644 packages/clients/textLocator/heap/requestHandling.js diff --git a/packages/clients/textLocator/heap/map.js b/packages/clients/textLocator/heap/map.js deleted file mode 100644 index 458d6c59d..000000000 --- a/packages/clients/textLocator/heap/map.js +++ /dev/null @@ -1,144 +0,0 @@ -import { Feature, Map, View } from 'ol' -import TileLayer from 'ol/layer/Tile' -import TileWMS from 'ol/source/TileWMS.js' -import OSM, { ATTRIBUTION } from 'ol/source/OSM' -import { getTransform } from 'ol/proj.js' -import WKT from 'ol/format/WKT.js' -import Polygon from 'ol/geom/Polygon.js' -import VectorSource from 'ol/source/Vector' -import VectorLayer from 'ol/layer/Vector' -import Overlay from 'ol/Overlay.js' -import { Fill, Stroke, Style } from 'ol/style.js' -import { Control, defaults as defaultControls } from 'ol/control.js' - -const WKTfmt = new WKT() - -const resultFeatureStyle = [ - new Style({ - stroke: new Stroke({ - color: 'blue', - width: 3, - }), - fill: new Fill({ - color: 'rgba(0, 0, 255, 0.1)', - }), - }), -] -const resultVectorLayer = new VectorLayer({ - source: new VectorSource({ - features: resultFeatures, - style: resultFeatureStyle, - }), -}) -// used to display the colored Geometries when clicking a location -const geometriesVectorLayer = new VectorLayer({ - source: new VectorSource({ - features: [], - style: resultFeatureStyle, - }), -}) -geometriesVectorLayer.setOpacity(0.7) - -// for coloring all geometries to all locations found in a title -const titleGeometriesVectorLayer = new VectorLayer({ - source: new VectorSource({ - features: [], - style: resultFeatureStyle, - }), -}) -titleGeometriesVectorLayer.setOpacity(0.8) - -const popupOverlay = new Overlay({ - element: popupContainer, - autoPan: { - animation: { - duration: 250, - }, - }, -}) - -popupCloser.onclick = function () { - popupOverlay.setPosition(undefined) - popupCloser.blur() - return false - -class ResetLocationGeometries extends Control { - geometriesVectorLayer.getSource().clear() - -class ResetTextGeometries extends Control { - titleGeometriesVectorLayer.getSource().clear() - - overlays: [popupOverlay] - - showPopup(text, coordinate) { - popupOverlay.setPosition(coordinate) - this.popupContent.innerHTML = text - - displayUserSelectedPoly(geometry) { - const displayPoly = geometry.clone() - displayPoly.applyTransform(getTransform('EPSG:4326', 'EPSG:3857')) - this.resultVectorLayer.getSource().getFeatures()[0].setGeometry(displayPoly) - - removeUserSelectedPoly() { - this.resultVectorLayer - .getSource() - .getFeatures()[0] - .setGeometry(new Polygon([])) - - displayGeometriesOnMap(geometriesList) { - this.resultVectorLayer.getSource().clear() - this.resultVectorLayer.setOpacity(0.45) - geometriesList.forEach((wktGeom) => { - const feature = WKTfmt.readFeature(wktGeom, { - dataProjection: 'EPSG:4326', - featureProjection: 'EPSG:3857', - }) - this.resultVectorLayer.getSource().addFeature(feature) - - colorGeometries(locationName, nameGeometryDict) { - this.geometriesVectorLayer.getSource().clear() - const geometriesList = nameGeometryDict[locationName] - if (geometriesList && Array.isArray(geometriesList)) { - let geometryCounter = 0 - geometriesList.forEach((wktGeom) => { - const style = new Style({ - fill: new Fill({ - color: colorPalette[geometryCounter], - }), - }) - const feature = WKTfmt.readFeature(wktGeom, { - dataProjection: 'EPSG:4326', - featureProjection: 'EPSG:3857', - }) - feature.setStyle(style) - this.geometriesVectorLayer.getSource().addFeature(feature) - geometryCounter++ - }) - } else { - console.log('No Geometries for this location') - - colorGeometriesMultipleLocations( - article_title, - titleLocationFreqDict, - nameGeometryDict - ) { - this.titleGeometriesVectorLayer.getSource().clear() - const locationsFreqs = titleLocationFreqDict[article_title] - for (const locationName in locationsFreqs) { - const geometriesList = nameGeometryDict[locationName] - if (geometriesList && Array.isArray(geometriesList)) { - geometriesList.forEach((wktGeom) => { - const style = new Style({ - fill: new Fill({ - color: heatColorPalette[locationsFreqs[locationName]], - }), - }) - const feature = WKTfmt.readFeature(wktGeom, { - dataProjection: 'EPSG:4326', - featureProjection: 'EPSG:3857', - }) - feature.setStyle(style) - this.titleGeometriesVectorLayer.getSource().addFeature(feature) - }) - } else { - console.log('No Geometries for this location') diff --git a/packages/clients/textLocator/heap/requestHandling.js b/packages/clients/textLocator/heap/requestHandling.js deleted file mode 100644 index 8d0c4b373..000000000 --- a/packages/clients/textLocator/heap/requestHandling.js +++ /dev/null @@ -1,233 +0,0 @@ -import WKT from 'ol/format/WKT.js' - -const ignoreIdsName = ['EuroNat-33'] -const ignoreIdsGeometries = [ - 'EuroNat-33', - 'SH-WATTENMEER-DM-1', - 'SH-WATTENMEER-1', - 'Ak2006-51529', - 'Landsg-2016-110', -] -const nameGeometryDict = {} -let titleLocationFreqDict = {} - -const WKTfmt = new WKT() - -// endpoint configuration - load endpoints from a json file and fall back to default config if that json is not available -const defaultConfig = { - backend_endpoint: 'http://localhost:8000', -} - -let config_data = null -async function loadConfig() { - if (config_data != null) { - return config_data - } - try { - const response = await fetch('./textloc-config.json') - if (response.ok) { - const config = await response.json() - config_data = config - return config - } - console.error( - 'config request failed:', - response.status, - response.statusText - ) - return defaultConfig - } catch (error) { - console.error('config request error:', error) - return defaultConfig - } -} - -class requestHandler { - constructor() {} -} - -/** - * Extract all location names and geometries from the gazetteer response json while skipping names that have an ignoreId - * example for data value: [{"id":"shnwattdt075","names":[{"ObjectID":"shnwatthist1912003","GeomID":"shnwattdt075","Start":"1868-01-01","Ende":"1875-12-31","Name":"Bielshöven","Rezent":false,"Sprache":"Hochdeutsch","Typ":"früherer Name, hist. Name","Quellen":[{"Autor":"F.A. Meyer","Datum":"1875","Titel":"Elbemündung ( Katalognummer 81)","Media":"A.W. Lang, Historisches Seekartenwerk der Deutschen Bucht ( Neumünster 1969-1981)","Ort":"Hamburg"},{"Autor":"F.A. Meyer","Datum":"1868","Titel":"Einsegelung in die Elbe ( Katalognummer 75)","Media":"A.W. Lang, Historisches Seekartenwerk der Deutschen Bucht ( Neumünster 1969-1981)","Ort":"Hamburg"}]}, - * @param {dictionary} data - * @param {list of string} ignoreIds - * @returns - */ -function extractNamesAndGeometries(data) { - const namesList = [] - const allGeometriesList = [] - data.forEach((entry) => { - if (!ignoreIdsName.includes(entry.id)) { - if (entry.names && Array.isArray(entry.names)) { - entry.names.forEach((nameObj) => { - if (nameObj.Name) { - namesList.push(nameObj.Name) - } - if (!ignoreIdsGeometries.includes(entry.id)) { - const geometriesList = [] - if (entry.geoms && Array.isArray(entry.geoms)) { - entry.geoms.forEach((geomObj) => { - if (geomObj.WKT) { - allGeometriesList.push(geomObj.WKT) - geometriesList.push(geomObj.WKT) - } - }) - } - nameGeometryDict[nameObj.Name] = geometriesList - } - }) - } - } - }) - return [namesList, allGeometriesList, nameGeometryDict] -} - -/** - * send a request to our own backend to search for articles that contain the location names returned by the kuesten gazetteer. - * Each location has their own BM25 search, such that the frequencies can be determined for each location - * @param {*} locationNamesArray - * @returns {dictionary} - dictionary that contains the found article titles as keys and another dict with location : frequency pairs - * as values - */ -async function locationLookupIndividually(locationNamesArray) { - const config = await loadConfig() - const url = config.backendEndpoint + 'lookup/locations_individually' - - const locationsArray = { location_names: locationNamesArray } - const requestData = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(locationsArray), - } - - try { - const response = await fetch(url, requestData) - if (response.ok) { - const data = await response.json() - return data - } - console.error('Request failed:', response.status, response.statusText) - return null - } catch (error) { - console.error('Request error:', error) - return null - } -} - -/** - * send a request to our own backend to search for articles that contain the location names returned by the kuesten gazetteer. - * All locations are run through the BM25 algorithm bundled together - * @param {string} name - comma seperated location names - * @returns - */ -async function locationLookup(name) { - const config = await loadConfig() - const url = config.backendEndpoint + 'lookup/location_name' - - const requestData = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ name }), - } - - try { - const response = await fetch(url, requestData) - if (response.ok) { - const data = await response.json() - return data - } - console.error('Request failed:', response.status, response.statusText) - return null - } catch (error) { - console.error('Request error:', error) - return null - } -} - -/** - * Main function to run when user clicks on the map. Requests locations from the Gazetteer and searches locations in articles via - * our backend - * @param {*} geometry - * @param {*} popup_position - * @param {*} mapClass - * @param {*} htmlCreator - * @returns - */ -export const searchAction = async ( - geometry, - popup_position, - mapClass, - htmlCreator -) => { - mapClass.showPopup('Suche nach Ortsnamen...', popup_position) - mapClass.displayUserSelectedPoly(geometry) - let resultJSON = await requestResultJSON(geometry, 1) - console.log('fetched result') - console.log(resultJSON) - if (resultJSON == undefined) { - mapClass.showPopup( - 'Die Suche nach Ortsnamen ist fehlgeschlagen.', - popup_position - ) - return - } - var [locationNamesArray, geometriesList, nameGeometryDict] = - extractNamesAndGeometries(resultJSON.results) - console.log(locationNamesArray) - console.log(geometriesList) - console.log(nameGeometryDict) - if (resultJSON.pages > 1) { - for (let page = 2; page <= resultJSON.pages; page++) { - mapClass.showPopup( - 'Suche nach Ortsnamen... ' + page + ' / ' + resultJSON.pages, - popup_position - ) - resultJSON = await requestResultJSON(geometry, page) - var [locationNamesPages, geometriesPages, nameGeometryDict] = - extractNamesAndGeometries(resultJSON.results) - locationNamesArray = locationNamesArray.concat(locationNamesPages) - geometriesList = geometriesList.concat(geometriesPages) - } - } - - htmlCreator.populateSidebarLocations( - locationNamesArray, - mapClass, - nameGeometryDict - ) - mapClass.popupCloser.onclick() - const locationNames = locationNamesArray.join(',') - console.log('location names: ' + locationNames) - mapClass.displayGeometriesOnMap(geometriesList) - - mapClass.showPopup('Suche nach passenden Texten...', popup_position) - const lookupResults = await locationLookupIndividually(locationNamesArray) - if (lookupResults == null) { - mapClass.showPopup( - 'Die Suche nach passenden Texten ist fehlgeschlagen.', - popup_position - ) - return - } - - titleLocationFreqDict = lookupResults.title_location_freq - - let result_text = - "
Suchergebnisse
Suchbegriffe: " + - locationNames + - '
' - for (const article_title in titleLocationFreqDict) { - console.log(article_title) - result_text += '
' + article_title + '
\n' - } - mapClass.popupCloser.onclick() - htmlCreator.populateSidebarTexts( - titleLocationFreqDict, - nameGeometryDict, - mapClass - ) -} From 8cb34930b24a62b49486b4dc45e35929acd76a6b Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Mon, 26 Feb 2024 12:53:52 +0100 Subject: [PATCH 008/173] update overall code structure & generify search --- .../clients/textLocator/src/addPlugins.ts | 7 +- packages/clients/textLocator/src/locales.ts | 38 +++-- .../src/utils/coastalGazetteer/common.ts | 14 ++ .../src/utils/coastalGazetteer/getAllPages.ts | 74 ++++++++++ .../utils/coastalGazetteer/makeRequestUrl.ts | 28 +--- .../{geometrySearch.ts => searchGeometry.ts} | 0 .../{toponymSearch.ts => searchToponym.ts} | 136 ++---------------- .../src/utils/coastalGazetteer/types.ts | 61 ++++++++ .../clients/textLocator/src/utils/common.ts | 1 + 9 files changed, 199 insertions(+), 160 deletions(-) create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/common.ts create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/getAllPages.ts rename packages/clients/textLocator/src/utils/coastalGazetteer/{geometrySearch.ts => searchGeometry.ts} (100%) rename packages/clients/textLocator/src/utils/coastalGazetteer/{toponymSearch.ts => searchToponym.ts} (52%) create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/types.ts create mode 100644 packages/clients/textLocator/src/utils/common.ts diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index 16d7ab946..396730eca 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -12,7 +12,7 @@ import Zoom from '@polar/plugin-zoom' import Header from './plugins/Header' import GeometrySearch from './plugins/GeometrySearch' -import { searchCoastalGazetteer } from './utils/coastalGazetteer/toponymSearch' +import { searchCoastalGazetteerByToponym } from './utils/coastalGazetteer/searchToponym' import { idRegister } from './services' // this is fine for list-like setup functions @@ -56,7 +56,10 @@ export const addPlugins = (core) => { searchMethods: [{ type: 'coastalGazetteer' }], addLoading: 'plugin/loadingIndicator/addLoadingKey', removeLoading: 'plugin/loadingIndicator/removeLoadingKey', - customSearchMethods: { coastalGazetteer: searchCoastalGazetteer }, + customSearchMethods: { + // @ts-expect-error | Local parameter requirements diverge from type + coastalGazetteer: searchCoastalGazetteerByToponym, + }, customSelectResult: { /* categoryDenkmalsucheAutocomplete: selectResult */ }, diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index f4be2d8a3..b568e0a06 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -26,6 +26,9 @@ const locales: LanguageOption[] = [ [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grau)', }, + addressSearch: { + unnamed: 'Unbenannt', + }, attributions: { [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, @@ -38,7 +41,8 @@ const locales: LanguageOption[] = [ '
Impressum', }, error: { - searchCoastalGazetteer: 'Die Suche ist fehlgeschlagen. TODO', + searchCoastalGazetteer: + 'Die Suche ist mit einem unbekannten Fehler fehlgeschlagen. Bitte versuchen Sie es später erneut.', }, }, plugins: { @@ -61,17 +65,29 @@ const locales: LanguageOption[] = [ [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grey)', }, + addressSearch: { + unnamed: 'Unnamed', + }, + attributions: { + [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, + [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, + [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, + [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, + static: + '
Legal notice (Impressum)', + }, + error: { + searchCoastalGazetteer: + 'The search failed with an unknown error. Please try again later.', + }, }, - attributions: { - [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, - [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, - [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, - [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, - static: - '
Legal notice (Impressum)', + plugins: { + addressSearch: { + defaultGroup: 'Location search', + }, }, }, }, diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/common.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/common.ts new file mode 100644 index 000000000..5b7aaa35c --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/common.ts @@ -0,0 +1,14 @@ +import { WKT, GeoJSON } from 'ol/format' + +export const ignoreIds = { + global: ['EuroNat-33'], + geometries: [ + 'EuroNat-33', + 'SH-WATTENMEER-DM-1', + 'SH-WATTENMEER-1', + 'Ak2006-51529', + 'Landsg-2016-110', + ], +} +export const wellKnownText = new WKT() +export const geoJson = new GeoJSON() diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/getAllPages.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/getAllPages.ts new file mode 100644 index 000000000..63f23f373 --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/getAllPages.ts @@ -0,0 +1,74 @@ +import { CoreState } from '@polar/lib-custom-types' +import { Store } from 'vuex' +import { MakeRequestUrlParameters, ResponsePayload } from './types' +import { makeRequestUrl } from './makeRequestUrl' + +const getEmptyResponsePayload = (): ResponsePayload => ({ + count: '', + currentpage: '', + pages: '', + keyword: '', + querystring: '', + results: [], + time: NaN, +}) + +const mergeResponses = ( + initialResponse: ResponsePayload, + responses: ResponsePayload[] +) => ({ + ...initialResponse, + currentpage: 'merged', + results: [ + initialResponse.results, + ...responses.map(({ results }) => results), + ].flat(1), + time: NaN, // not used, setting NaN to indicate it's not the actual time +}) + +export async function getAllPages( + this: Store, + signal: AbortSignal, + url: string, + params: Partial, + epsg: `EPSG:${string}` +): Promise { + const response = await fetch(makeRequestUrl(url, params, epsg), { + method: 'GET', + signal, + }) + + if (!response.ok) { + this.dispatch('plugin/toast/addToast', { + type: 'error', + text: 'textLocator.error.searchCoastalGazetteer', + }) + console.error('Gazetteer response:', response) + return getEmptyResponsePayload() + } + const responsePayload: ResponsePayload = await response.json() + const pages = parseInt(responsePayload.pages, 10) + const initialRequestMerge = typeof params.page === 'undefined' && pages > 1 + + if (!initialRequestMerge) { + return responsePayload + } + + return mergeResponses( + responsePayload, + await Promise.all( + Array.from(Array(pages - 1)).map((_, index) => + getAllPages.call( + this, + signal, + url, + { + ...params, + page: `${index + 2}`, + }, + epsg + ) + ) + ) + ) +} diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts index 153ecbd38..f0c1ac2f0 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/makeRequestUrl.ts @@ -1,19 +1,6 @@ -import { Geometry } from 'geojson' -import { GeoJSON, WKT } from 'ol/format' - -const geoJson = new GeoJSON() -const wellKnownText = new WKT() - -interface RequestPayload { - keyword: string - searchType: 'like' | 'exact' | 'id' - lang: '-' | string - sdate: string - edate: string - type: '-' | string - page?: string // numerical - geom?: string -} +import { wgs84ProjectionCode } from '../common' +import { geoJson, wellKnownText } from './common' +import { MakeRequestUrlParameters, RequestPayload } from './types' const searchRequestDefaultPayload: Partial = { searchType: 'like', @@ -25,23 +12,22 @@ const searchRequestDefaultPayload: Partial = { export const makeRequestUrl = ( url: string, - inputValue: string, - page: string | undefined, - geometry: Geometry | undefined, + { keyword, page, geometry, ...rest }: Partial, epsg: string ): string => `${url}?${new URLSearchParams({ ...searchRequestDefaultPayload, - keyword: inputValue ? `*${inputValue}*` : '', + keyword: keyword ? `*${keyword}*` : '', ...(typeof page !== 'undefined' ? { page } : {}), ...(typeof geometry !== 'undefined' ? { geom: wellKnownText.writeGeometry( geoJson.readGeometry(geometry, { dataProjection: epsg, - featureProjection: 'EPSG:4326', + featureProjection: wgs84ProjectionCode, }) ), } : {}), + ...rest, }).toString()}` diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts similarity index 100% rename from packages/clients/textLocator/src/utils/coastalGazetteer/geometrySearch.ts rename to packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts similarity index 52% rename from packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts rename to packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts index 65c3d0bd4..bd6a02d05 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/toponymSearch.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts @@ -1,64 +1,12 @@ -// such names exist on the service -/* eslint-disable @typescript-eslint/naming-convention */ -import { CoreState } from '@polar/lib-custom-types' import { Feature, FeatureCollection } from 'geojson' import { Map } from 'ol' -import { GeoJSON, WKT } from 'ol/format' import { Store } from 'vuex' import levenshtein from 'js-levenshtein' -import { makeRequestUrl } from './makeRequestUrl' - -const ignoreIds = { - global: ['EuroNat-33'], - geometries: [ - 'EuroNat-33', - 'SH-WATTENMEER-DM-1', - 'SH-WATTENMEER-1', - 'Ak2006-51529', - 'Landsg-2016-110', - ], -} -const wellKnownText = new WKT() -const geoJson = new GeoJSON() - -interface ResponseName { - Start: string // YYYY-MM-DD - Ende: string // YYYY-MM-DD - GeomID: string - ObjectID: string - Name: string - Quellen: object[] // not used - Rezent: boolean - Sprache: string - Typ: string -} - -interface ResponseGeom { - Start: string // YYYY-MM-DD - Ende: string // YYYY-MM-DD - GeomID: string - ObjectID: string - Quellen: object[] // not used - Typ: string - 'Typ Beschreibung': string - WKT: string // WKT geometry -} - -interface ResponseResult { - id: string - names: ResponseName[] - geoms: ResponseGeom[] -} - -interface ResponsePayload { - count: string // numerical - currentpage: string // numerical - pages: string // numerical - keyword: string - querystring: string - results: ResponseResult[] - time: number -} +import { CoreState } from '@polar/lib-custom-types' +import { wgs84ProjectionCode } from '../common' +import { ResponseName, ResponsePayload, ResponseResult } from './types' +import { getAllPages } from './getAllPages' +import { geoJson, ignoreIds, wellKnownText } from './common' interface CoastalGazetteerParameters { epsg: `EPSG:${string}` @@ -85,70 +33,6 @@ const getEmptyFeatureCollection = (): FeatureCollection => ({ features: [], }) -const getEmptyResponsePayload = (): ResponsePayload => ({ - count: '', - currentpage: '', - pages: '', - keyword: '', - querystring: '', - results: [], - time: NaN, -}) - -const mergeResponses = ( - initialResponse: ResponsePayload, - responses: ResponsePayload[] -) => ({ - ...initialResponse, - currentpage: 'merged', - results: [ - initialResponse.results, - ...responses.map(({ results }) => results), - ].flat(1), - time: NaN, // not used, setting NaN to indicate it's not the actual time -}) - -async function getAllPages( - this: Store, - signal: AbortSignal, - url: string, - inputValue: string, - page: string | undefined, - epsg: `EPSG:${string}` -): Promise { - const response = await fetch( - makeRequestUrl(url, inputValue, page, undefined, epsg), - { - method: 'GET', - signal, - } - ) - - if (!response.ok) { - this.dispatch('plugin/toast/addToast', { - type: 'error', - text: 'textLocator.error.searchCoastalGazetteer', // TODO use page || '1' - }) - return getEmptyResponsePayload() - } - const responsePayload: ResponsePayload = await response.json() - const pages = parseInt(responsePayload.pages, 10) - const initialRequestMerge = typeof page === 'undefined' && pages > 1 - - if (!initialRequestMerge) { - return responsePayload - } - - return mergeResponses( - responsePayload, - await Promise.all( - Array.from(Array(pages - 1)).map((_, index) => - getAllPages.call(this, signal, url, inputValue, `${index + 2}`, epsg) - ) - ) - ) -} - const featurify = (epsg: `EPSG:${string}`, searchPhrase: string) => (feature: ResponseResult): Feature => ({ @@ -160,7 +44,7 @@ const featurify = (geom) => !ignoreIds.geometries.includes(geom.GeomID) )?.WKT, { - dataProjection: 'EPSG:4326', + dataProjection: wgs84ProjectionCode, featureProjection: epsg, } ) @@ -174,7 +58,8 @@ const featurify = }, // @ts-expect-error | used in POLAR for text display title: - feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || 'Ohne Namen', // TODO i18n + feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || + 'textLocator.addressSearch.unnamed', }) const featureCollectionify = ( @@ -204,7 +89,7 @@ const featureCollectionify = ( return featureCollection } -export async function searchCoastalGazetteer( +export async function searchCoastalGazetteerByToponym( this: Store, signal: AbortSignal, url: string, @@ -217,8 +102,7 @@ export async function searchCoastalGazetteer( this, signal, url, - inputValue, - undefined, + { keyword: inputValue }, queryParameters.epsg ) } catch (e) { diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/types.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/types.ts new file mode 100644 index 000000000..87c9563cb --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/types.ts @@ -0,0 +1,61 @@ +// such names exist on the service +/* eslint-disable @typescript-eslint/naming-convention */ +import { Geometry } from 'geojson' + +// // // REQUEST // // // + +export interface RequestPayload { + keyword: string + searchType: 'like' | 'exact' | 'id' + lang: '-' | string + sdate: string + edate: string + type: '-' | string + page?: string // numerical + geom?: string +} + +export interface MakeRequestUrlParameters extends RequestPayload { + geometry?: Geometry +} + +// // // RESPONSE // // // + +export interface ResponseName { + Start: string // YYYY-MM-DD + Ende: string // YYYY-MM-DD + GeomID: string + ObjectID: string + Name: string + Quellen: object[] // not used + Rezent: boolean + Sprache: string + Typ: string +} + +export interface ResponseGeom { + Start: string // YYYY-MM-DD + Ende: string // YYYY-MM-DD + GeomID: string + ObjectID: string + Quellen: object[] // not used + Typ: string + 'Typ Beschreibung': string + WKT: string // WKT geometry +} + +export interface ResponseResult { + id: string + names: ResponseName[] + geoms: ResponseGeom[] +} + +export interface ResponsePayload { + count: string // numerical + currentpage: string // numerical + pages: string // numerical + keyword: string + querystring: string + results: ResponseResult[] + time: number +} diff --git a/packages/clients/textLocator/src/utils/common.ts b/packages/clients/textLocator/src/utils/common.ts new file mode 100644 index 000000000..c88bfcc9f --- /dev/null +++ b/packages/clients/textLocator/src/utils/common.ts @@ -0,0 +1 @@ +export const wgs84ProjectionCode = 'EPSG:4326' From 6eaf0f3f81c660596e6796f495dba35c961e35a3 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 28 Feb 2024 06:46:07 +0100 Subject: [PATCH 009/173] implement rough GeometrySearch draft --- packages/clients/textLocator/package.json | 3 +- .../clients/textLocator/src/addPlugins.ts | 43 +++-- packages/clients/textLocator/src/index.html | 2 +- .../components/GeometrySearch.vue | 115 ++++++++++- .../src/plugins/GeometrySearch/index.ts | 2 +- .../src/plugins/GeometrySearch/language.ts | 40 +++- .../src/plugins/GeometrySearch/store/index.ts | 181 +++++++++++++++++- .../src/plugins/GeometrySearch/types.ts | 13 +- .../clients/textLocator/src/polar-client.ts | 9 +- .../clients/textLocator/src/store/module.ts | 20 -- .../coastalGazetteer/responseInterpreter.ts | 84 ++++++++ .../utils/coastalGazetteer/searchGeometry.ts | 52 ++++- .../utils/coastalGazetteer/searchToponym.ts | 125 ++++-------- .../src/utils/literatureByToponym.ts | 33 ++++ 14 files changed, 574 insertions(+), 148 deletions(-) delete mode 100644 packages/clients/textLocator/src/store/module.ts create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts diff --git a/packages/clients/textLocator/package.json b/packages/clients/textLocator/package.json index 690c32b35..21e611455 100644 --- a/packages/clients/textLocator/package.json +++ b/packages/clients/textLocator/package.json @@ -31,6 +31,7 @@ "@polar/plugin-scale": "^1.1.0", "@polar/plugin-toast": "^1.1.0", "@polar/plugin-zoom": "^1.2.0", - "js-levenshtein": "^1.1.6" + "js-levenshtein": "^1.1.6", + "lodash.debounce": "^4.0.8" } } diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index 396730eca..bb74e9cbd 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -12,7 +12,10 @@ import Zoom from '@polar/plugin-zoom' import Header from './plugins/Header' import GeometrySearch from './plugins/GeometrySearch' -import { searchCoastalGazetteerByToponym } from './utils/coastalGazetteer/searchToponym' +import { + searchCoastalGazetteerByToponym, + selectResult, +} from './utils/coastalGazetteer/searchToponym' import { idRegister } from './services' // this is fine for list-like setup functions @@ -25,6 +28,23 @@ export const addPlugins = (core) => { displayComponent: true, layoutTag: NineLayoutTag.TOP_LEFT, }), + Draw({ + displayComponent: false, + selectableDrawModes: ['Point', 'LineString', 'Circle', 'Polygon'], + style: { + fill: { + color: 'rgba(255, 255, 255, 0.5)', + }, + stroke: { + color: '#e51313', + width: 2, + }, + circle: { + radius: 7, + fillColor: '#e51313', + }, + }, + }), IconMenu({ displayComponent: true, // TODO fix, it's broken ... @@ -61,24 +81,9 @@ export const addPlugins = (core) => { coastalGazetteer: searchCoastalGazetteerByToponym, }, customSelectResult: { - /* categoryDenkmalsucheAutocomplete: selectResult */ - }, - }), - Draw({ - displayComponent: false, - selectableDrawModes: ['Point', 'LineString', 'Circle', 'Polygon'], - style: { - fill: { - color: 'rgba(255, 255, 255, 0.5)', - }, - stroke: { - color: '#e51313', - width: 2, - }, - circle: { - radius: 7, - fillColor: '#e51313', - }, + // it's defined like that + // eslint-disable-next-line @typescript-eslint/naming-convention + '': selectResult, }, }), Attributions({ diff --git a/packages/clients/textLocator/src/index.html b/packages/clients/textLocator/src/index.html index 96bd85040..193c35b7c 100644 --- a/packages/clients/textLocator/src/index.html +++ b/packages/clients/textLocator/src/index.html @@ -27,7 +27,7 @@ initializeClient({ urls: { - textLocatorBackend: 'https://textlocator.ai.dataport.de/api/', + textLocatorBackend: 'https://textlocator.ai.dataport.de/api', gazetteerClient: 'https://mdi-de-dienste.org/GazetteerClient/search' } }).then(console.info.bind(null, 'Map client instance:')) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue index be8a89e6b..3649c0b40 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue @@ -1,17 +1,124 @@ - + diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts index 8e7b6a305..9dca6a62e 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/index.ts @@ -5,7 +5,7 @@ import language from './language' import { makeStoreModule } from './store' interface GeometrySearchConfiguration extends PluginOptions { - thing: string + url: string } export default (options: GeometrySearchConfiguration) => (instance: Vue) => diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts index 7481ff33a..ecbf8b349 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts @@ -1,3 +1,5 @@ +// other names required +/* eslint-disable @typescript-eslint/naming-convention */ import { LanguageOption } from '@polar/lib-custom-types' const language: LanguageOption[] = [ @@ -10,7 +12,24 @@ const language: LanguageOption[] = [ geometrySearch: 'Geometriesuche', }, }, - geometrySearch: {}, + geometrySearch: { + draw: { + title: 'Zeichenmodus', + description: { + Point: + 'Klicken Sie in die Karte, um Ortsnamen und Literatur zu einer Punktkoordinate abzufragen. $t(plugins.geometrySearch.draw.description.Common)', + Polygon: + 'Klicken Sie wiederholt in die Karte, um eine Fläche zu zeichnen, zu der Ortsnamen und Literatur abgefragt werden. Doppelklick beendet eine Zeichnung. $t(plugins.geometrySearch.draw.description.Common)', + Common: 'Neue Zeichnungen verwerfen vorangehende Ergebnisse.', + }, + }, + results: { + title: 'Funde', + byLocation: 'Ort', + byText: 'Text', + none: 'Keine Suchergebnisse', + }, + }, }, }, }, @@ -23,7 +42,24 @@ const language: LanguageOption[] = [ geometrySearch: 'Geometry search', }, }, - geometrySearch: {}, + geometrySearch: { + draw: { + title: 'Draw mode', + description: { + Point: + 'Click somewhere in the map to request location names and literature to a point coordinate. $t(plugins.geometrySearch.draw.description.Common)', + Polygon: + 'Click repeatedly in the map to draw an area to request location names and literature to. Double click finished an area. $t(plugins.geometrySearch.draw.description.Common)', + Common: 'New drawings discard previous results.', + }, + }, + results: { + title: 'Findings', + byLocation: 'Place', + byText: 'Text', + none: 'No search results', + }, + }, }, }, }, diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts index 5971690c9..817536b0c 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts @@ -2,30 +2,195 @@ import { generateSimpleGetters, generateSimpleMutations, } from '@repositoryname/vuex-generators' -import { PolarModule } from '@polar/lib-custom-types' +import { PolarActionHandler, PolarModule } from '@polar/lib-custom-types' +import debounce from 'lodash.debounce' +import { Feature } from 'ol' +import VectorSource, { VectorSourceEvent } from 'ol/source/Vector' import { GeometrySearchState, GeometrySearchGetters } from '../types' +import { searchGeometry } from '../../../utils/coastalGazetteer/searchGeometry' +import { getEmptyFeatureCollection } from '../../../utils/coastalGazetteer/responseInterpreter' +import { searchLiteratureByToponym } from '../../../utils/literatureByToponym' -const getInitialState = (): GeometrySearchState => ({}) +interface TreeViewItem { + id: string + name: string + count: number + children?: TreeViewItem[] +} + +let counter = 0 +const searchLoadingKey = 'geometrySearchLoadingKey' +const getSearchLoadingKey = () => `${searchLoadingKey}-${++counter}` + +const getInitialState = (): GeometrySearchState => ({ + featureCollection: getEmptyFeatureCollection(), + titleLocationFrequency: {}, + draw: 'Point', + byCategory: 'text', +}) // OK for module creation // eslint-disable-next-line max-lines-per-function export const makeStoreModule = () => { + let debouncedSearchGeometry: PolarActionHandler< + GeometrySearchState, + GeometrySearchGetters + > + const storeModule: PolarModule = { namespaced: true, state: getInitialState(), actions: { - setupModule({ - commit, - dispatch, - getters: { listenToChanges, renderType }, - rootGetters, - }): void {}, + setupModule({ dispatch }): void { + // initially, point drawing is active + dispatch('setDrawMode', 'Point') + dispatch('setupDrawReaction') + dispatch('setupWatchers') + }, + setupDrawReaction({ rootGetters }): void { + // features added multiple times; avoid overly requesting + debouncedSearchGeometry = debounce( + (feature) => + this.dispatch('plugin/geometrySearch/searchGeometry', feature), + 20 + ).bind(this) + + // only keep a single feature in the draw tool + const drawSource = rootGetters['plugin/draw/drawSource'] as VectorSource + let lastFeature: Feature | undefined + drawSource.on(['addfeature'], function (event) { + const nextFeature = (event as VectorSourceEvent).feature + if (nextFeature && lastFeature !== nextFeature) { + lastFeature = nextFeature + drawSource.clear() + drawSource.addFeature(nextFeature) + // TODO confusing, figure out + // @ts-expect-error | The function is bound, the error seems not to apply + debouncedSearchGeometry(nextFeature) + } + }) + }, + setupWatchers({ commit, dispatch, rootGetters }): void { + // load titleLocationFrequency on each featureCollection update + this.watch( + () => rootGetters['plugin/geometrySearch/featureCollection'], + async () => { + const names: string[] = rootGetters[ + 'plugin/geometrySearch/featureCollection' + ].features + .map((feature) => + feature.properties?.names?.map((name) => name.Name) + ) + .flat(1) + .filter((x) => x) + const titleLocationFrequency = await searchLiteratureByToponym( + // @ts-expect-error | local addition + rootGetters.configuration.textLocatorBackendUrl, + names + ) + commit('setTitleLocationFrequency', titleLocationFrequency) + if (Object.keys(titleLocationFrequency).length) { + dispatch('plugin/iconMenu/openMenuById', 'geometrySearch', { + root: true, + }) + } + }, + { immediate: false } + ) + }, + searchGeometry({ rootGetters, commit }, feature): void { + const loadingKey = getSearchLoadingKey() + commit('plugin/loadingIndicator/addLoadingKey', loadingKey, { + root: true, + }) + searchGeometry + .call( + // TODO figure out + // @ts-expect-error | the part somehow is the whole + this, + feature, + // @ts-expect-error | added in polar-client.ts locally + rootGetters.configuration.geometrySearch.url, + rootGetters.configuration.epsg + ) + .then((result) => commit('setFeatureCollection', result)) + .finally(() => + commit('plugin/loadingIndicator/removeLoadingKey', loadingKey, { + root: true, + }) + ) + }, + setDrawMode({ commit, dispatch }, drawMode): void { + dispatch('plugin/draw/setMode', 'draw', { root: true }) + dispatch('plugin/draw/setDrawMode', drawMode, { root: true }) + commit('plugin/geometrySearch/setDraw', drawMode, { root: true }) + }, }, mutations: { ...generateSimpleMutations(getInitialState()), }, getters: { ...generateSimpleGetters(getInitialState()), + // TODO + // eslint-disable-next-line max-lines-per-function + treeViewItems({ + titleLocationFrequency, + byCategory, + }: GeometrySearchState): TreeViewItem[] { + const byTextTreeViewItems: TreeViewItem[] = Object.entries( + titleLocationFrequency + ).map(([title, locations]) => ({ + id: title, + name: title, + count: Object.values(locations).reduce((acc, curr) => acc + curr), + children: Object.entries(locations).map(([location, count]) => ({ + id: location, + name: location, + count, + })), + })) + + if (byCategory === 'text') { + return byTextTreeViewItems + } + + /* +interface TreeViewItem { + id: string + name: string + children?: TreeViewItem[] +} + */ + + const locationNames = [ + ...new Set( + byTextTreeViewItems + .map((item) => + (item.children || []).map((child) => child.name).flat(1) + ) + .flat(1) + ), + ] + + console.warn(locationNames) + + return locationNames.map((locationName) => ({ + id: locationName, + name: locationName, + count: byTextTreeViewItems + .map( + (item) => + item.children?.find((child) => child.name === locationName) + ?.count || 0 + ) + .reduce((acc, curr) => acc + curr), + children: byTextTreeViewItems + .filter((item) => + item.children?.find((child) => child.name === locationName) + ) + .map((item) => ({ ...item, children: undefined })), + })) + }, }, } diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts index 3a13108d5..5f41b3d5f 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts @@ -1,3 +1,14 @@ -export interface GeometrySearchState {} +import { FeatureCollection } from 'geojson' +import { TitleLocationFrequency } from '../../utils/literatureByToponym' + +export type TextLocatorDrawModes = 'Point' | 'Polygon' +export type TextLocatorCategories = 'text' | 'toponym' + +export interface GeometrySearchState { + featureCollection: FeatureCollection + titleLocationFrequency: TitleLocationFrequency + draw: TextLocatorDrawModes + byCategory: TextLocatorCategories +} export type GeometrySearchGetters = GeometrySearchState diff --git a/packages/clients/textLocator/src/polar-client.ts b/packages/clients/textLocator/src/polar-client.ts index 4105c9314..b6b1e5af8 100644 --- a/packages/clients/textLocator/src/polar-client.ts +++ b/packages/clients/textLocator/src/polar-client.ts @@ -11,9 +11,8 @@ addPlugins(client) interface TextLocatorParameters { urls: { - backend: string + textLocatorBackend: string gazetteerClient: string - gazetteerWfs: string } } @@ -24,6 +23,12 @@ export async function initializeClient({ urls }: TextLocatorParameters) { // @ts-expect-error | rest configured in addPlugins.ts (simple API) searchMethods: [{ url: urls.gazetteerClient }], } + // @ts-expect-error | local addition + mapConfiguration.geometrySearch = { + url: urls.gazetteerClient, + } + // @ts-expect-error | local addition + mapConfiguration.textLocatorBackendUrl = urls.textLocatorBackend return await client.createMap({ containerId, diff --git a/packages/clients/textLocator/src/store/module.ts b/packages/clients/textLocator/src/store/module.ts deleted file mode 100644 index 20b649176..000000000 --- a/packages/clients/textLocator/src/store/module.ts +++ /dev/null @@ -1,20 +0,0 @@ -// some names are defined by the environment -/* eslint-disable camelcase */ - -import { PolarModule } from '@polar/lib-custom-types' - -// TODO remove -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface TextLocatorGetters {} - -/* TextLocator VueX Store Module for system-specific contents. */ -export const textLocatorModule: PolarModule< - Record, - TextLocatorGetters -> = { - namespaced: true, - state: {}, - actions: {}, - mutations: {}, - getters: {}, -} diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts new file mode 100644 index 000000000..68e835122 --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts @@ -0,0 +1,84 @@ +import { Feature, FeatureCollection } from 'geojson' +import levenshtein from 'js-levenshtein' +import { wgs84ProjectionCode } from '../common' +import { ResponseName, ResponsePayload, ResponseResult } from './types' +import { geoJson, ignoreIds, wellKnownText } from './common' + +// arbitrary sort based on input – prefer 1. startsWith 2. closer string +const sorter = + (searchPhrase: string, sortKey: string) => + (a: ResponseName | Feature, b: ResponseName | Feature) => { + const aStartsWith = a[sortKey].startsWith(searchPhrase) + const bStartsWith = b[sortKey].startsWith(searchPhrase) + + return aStartsWith && !bStartsWith + ? -1 + : !aStartsWith && bStartsWith + ? 1 + : levenshtein(a[sortKey], searchPhrase) - + levenshtein(b[sortKey], searchPhrase) + } + +export const getEmptyFeatureCollection = (): FeatureCollection => ({ + type: 'FeatureCollection', + features: [], +}) + +const featurify = + (epsg: `EPSG:${string}`, searchPhrase: string | null) => + (feature: ResponseResult): Feature | null => { + const geometries = feature.geoms.filter( + (geom) => !ignoreIds.geometries.includes(geom.GeomID) + ) + return { + type: 'Feature', + geometry: geometries.length + ? geoJson.writeGeometryObject( + // NOTE arbitrarily, the first geometry is used + wellKnownText.readGeometry(geometries[0].WKT, { + dataProjection: wgs84ProjectionCode, + featureProjection: epsg, + }) + ) + : { type: 'Point', coordinates: [] }, + id: feature.id, + properties: { + names: feature.names, + geometries, + }, + // @ts-expect-error | used in POLAR for text display + title: + (searchPhrase + ? feature.names.sort(sorter(searchPhrase, 'Name')) + : feature.names)[0]?.Name || 'textLocator.addressSearch.unnamed', + } + } + +export const featureCollectionify = ( + fullResponse: ResponsePayload, + epsg: `EPSG:${string}`, + searchPhrase: string | null +): FeatureCollection => { + const featureCollection = getEmptyFeatureCollection() + featureCollection.features.push( + ...fullResponse.results.reduce((accumulator, feature) => { + if (ignoreIds.global.includes(feature.id)) { + return accumulator + } + const featurified = featurify(epsg, searchPhrase)(feature) + if (featurified === null) { + return accumulator + } + accumulator.push(featurified) + return accumulator + }, [] as Feature[]) + ) + + if (searchPhrase) { + featureCollection.features = featureCollection.features.sort( + sorter(searchPhrase, 'title') + ) + } + + return featureCollection +} diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts index dbc2454ce..1e079aad0 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/searchGeometry.ts @@ -1,3 +1,51 @@ -// https://mdi-de-dienste.org/GazetteerClient/search?geom=POINT(8.511120645998783 54.10846498210793)&keyword=&searchType=like&lang=-&sdate=0001-01-01&edate=2030-08-16&type=-&page=1 +import { Feature } from 'ol' +import { Store } from 'vuex' +import { CoreState } from '@polar/lib-custom-types' +import { FeatureCollection } from 'geojson' +import { getAllPages } from './getAllPages' +import { geoJson } from './common' +import { ResponsePayload } from './types' +import { + featureCollectionify, + getEmptyFeatureCollection, +} from './responseInterpreter' -// https://mdi-de-dienste.org/GazetteerClient/search?geom=POLYGON%28%288.621975250313447+54.161985993052355%2C8.621975250313447+54.14485476891343%2C8.65430784323856+54.14485476891343%2C8.65430784323856+54.161985993052355%2C8.621975250313447+54.161985993052355%29%29&keyword=&searchType=like&lang=-&sdate=0001-01-01&edate=2030-08-16&type=-&page=1 +let abortController + +export async function searchGeometry( + this: Store, + feature: Feature, + url: string, + epsg: `EPSG:${string}` +): Promise | never { + if (abortController) { + abortController.abort() + } + const geometry = feature.getGeometry() + if (!geometry) { + return getEmptyFeatureCollection() + } + abortController = new AbortController() + const signal = abortController.signal + let fullResponse: ResponsePayload + try { + fullResponse = await getAllPages.call( + this, + signal, + url, + { geometry: geoJson.writeGeometryObject(geometry) }, + epsg + ) + } catch (e) { + if (!signal.aborted) { + console.error(e) + this.dispatch('plugin/toast/addToast', { + type: 'error', + text: 'textLocator.error.searchCoastalGazetteer', + }) + } + return getEmptyFeatureCollection() + } + abortController = null + return Promise.resolve(featureCollectionify(fullResponse, epsg, null)) +} diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts index bd6a02d05..04e11edfc 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts @@ -1,94 +1,21 @@ -import { Feature, FeatureCollection } from 'geojson' +import { FeatureCollection } from 'geojson' import { Map } from 'ol' import { Store } from 'vuex' -import levenshtein from 'js-levenshtein' -import { CoreState } from '@polar/lib-custom-types' -import { wgs84ProjectionCode } from '../common' -import { ResponseName, ResponsePayload, ResponseResult } from './types' +import { CoreState, SelectResultFunction } from '@polar/lib-custom-types' +import SearchResultSymbols from '@polar/plugin-address-search/src/utils/searchResultSymbols' +import { ResponsePayload } from './types' import { getAllPages } from './getAllPages' -import { geoJson, ignoreIds, wellKnownText } from './common' +import { + getEmptyFeatureCollection, + featureCollectionify, +} from './responseInterpreter' interface CoastalGazetteerParameters { epsg: `EPSG:${string}` map: Map } -// arbitrary sort based on input – prefer 1. startsWith 2. closer string -const sorter = - (searchPhrase: string, sortKey: string) => - (a: ResponseName | Feature, b: ResponseName | Feature) => { - const aStartsWith = a[sortKey].startsWith(searchPhrase) - const bStartsWith = b[sortKey].startsWith(searchPhrase) - - return aStartsWith && !bStartsWith - ? -1 - : !aStartsWith && bStartsWith - ? 1 - : levenshtein(a[sortKey], searchPhrase) - - levenshtein(b[sortKey], searchPhrase) - } - -const getEmptyFeatureCollection = (): FeatureCollection => ({ - type: 'FeatureCollection', - features: [], -}) - -const featurify = - (epsg: `EPSG:${string}`, searchPhrase: string) => - (feature: ResponseResult): Feature => ({ - type: 'Feature', - geometry: geoJson.writeGeometryObject( - // NOTE currently only the first geometry is used - wellKnownText.readGeometry( - feature.geoms.find( - (geom) => !ignoreIds.geometries.includes(geom.GeomID) - )?.WKT, - { - dataProjection: wgs84ProjectionCode, - featureProjection: epsg, - } - ) - ), - id: feature.id, - properties: { - names: feature.names, - geometries: feature.geoms.filter( - (geom) => !ignoreIds.geometries.includes(geom.GeomID) - ), - }, - // @ts-expect-error | used in POLAR for text display - title: - feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || - 'textLocator.addressSearch.unnamed', - }) - -const featureCollectionify = ( - fullResponse: ResponsePayload, - epsg: `EPSG:${string}`, - searchPhrase: string -): FeatureCollection => { - const featureCollection = getEmptyFeatureCollection() - featureCollection.features.push( - ...fullResponse.results.reduce((accumulator, feature) => { - if (ignoreIds.global.includes(feature.id)) { - return accumulator - } - try { - // TODO this shouldn't be try-catch, it's detectable - const featurified = featurify(epsg, searchPhrase)(feature) - accumulator.push(featurified) - return accumulator - } catch (e) { - return accumulator - } - }, [] as Feature[]) - ) - featureCollection.features = featureCollection.features.sort( - sorter(searchPhrase, 'title') - ) - return featureCollection -} - +// this method is meant to be injected into the AddressSearch plugin export async function searchCoastalGazetteerByToponym( this: Store, signal: AbortSignal, @@ -106,14 +33,38 @@ export async function searchCoastalGazetteerByToponym( queryParameters.epsg ) } catch (e) { - console.error(e) - this.dispatch('plugin/toast/addToast', { - type: 'error', - text: 'textLocator.error.searchCoastalGazetteer', - }) + if (!signal.aborted) { + console.error(e) + this.dispatch('plugin/toast/addToast', { + type: 'error', + text: 'textLocator.error.searchCoastalGazetteer', + }) + } return getEmptyFeatureCollection() } return Promise.resolve( featureCollectionify(fullResponse, queryParameters.epsg, inputValue) ) } + +export const selectResult: SelectResultFunction = ({ commit }, { feature }) => { + // default behaviour (AddressSearch selects and is out further behaviour) + commit('plugin/addressSearch/setChosenAddress', feature, { root: true }) + commit('plugin/addressSearch/setInputValue', feature.title, { root: true }) + commit( + 'plugin/addressSearch/setSearchResults', + SearchResultSymbols.NO_SEARCH, + { root: true } + ) + + // added behaviour: push as one-element feature collection to search store + commit('plugin/geometrySearch/setByCategory', 'toponym', { root: true }) + commit( + 'plugin/geometrySearch/setFeatureCollection', + { + ...getEmptyFeatureCollection(), + features: [feature], + }, + { root: true } + ) +} diff --git a/packages/clients/textLocator/src/utils/literatureByToponym.ts b/packages/clients/textLocator/src/utils/literatureByToponym.ts index e69de29bb..544492cb0 100644 --- a/packages/clients/textLocator/src/utils/literatureByToponym.ts +++ b/packages/clients/textLocator/src/utils/literatureByToponym.ts @@ -0,0 +1,33 @@ +// APIs require other names +/* eslint-disable @typescript-eslint/naming-convention */ +const urlSuffix = { + lookUpLocationsIndividually: '/lookup/locations_individually', +} + +type LiteratureName = string +type Toponym = string + +export type TitleLocationFrequency = Record< + LiteratureName, + Record +> + +export async function searchLiteratureByToponym( + url: string, + names: string[] +): Promise { + if (!names.length) { + return Promise.resolve({}) + } + const response = await fetch( + `${url}${urlSuffix.lookUpLocationsIndividually}`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: `{"location_names":${JSON.stringify(names)}}`, + } + ) + return (await response.json()).title_location_freq as TitleLocationFrequency +} From dfca21aa450e66d29761aabd0cd141bb97a78bc7 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 28 Feb 2024 11:31:45 +0100 Subject: [PATCH 010/173] refine GeometrySearch draft --- .../clients/textLocator/src/addPlugins.ts | 45 ++++--- packages/clients/textLocator/src/locales.ts | 32 +++-- packages/clients/textLocator/src/mapConfig.ts | 6 + .../components/GeometrySearch.vue | 67 +++++++--- .../src/plugins/GeometrySearch/index.ts | 17 +-- .../src/plugins/GeometrySearch/store/index.ts | 118 ++++++------------ .../src/plugins/GeometrySearch/types.ts | 9 +- packages/clients/textLocator/src/services.ts | 12 ++ .../utils/coastalGazetteer/searchToponym.ts | 1 - .../textLocator/src/utils/makeTreeView.ts | 53 ++++++++ 10 files changed, 224 insertions(+), 136 deletions(-) create mode 100644 packages/clients/textLocator/src/utils/makeTreeView.ts diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index bb74e9cbd..1f6974943 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -24,13 +24,27 @@ export const addPlugins = (core) => { setLayout(NineLayout) core.addPlugins([ - Header({ + AddressSearch({ displayComponent: true, layoutTag: NineLayoutTag.TOP_LEFT, + minLength: 3, + waitMs: 500, + // @ts-expect-error | URL configured in a different way (simple API) + searchMethods: [{ type: 'coastalGazetteer' }], + addLoading: 'plugin/loadingIndicator/addLoadingKey', + removeLoading: 'plugin/loadingIndicator/removeLoadingKey', + customSearchMethods: { + // @ts-expect-error | Local parameter requirements diverge from type + coastalGazetteer: searchCoastalGazetteerByToponym, + }, + customSelectResult: { + // it's defined like that + // eslint-disable-next-line @typescript-eslint/naming-convention + '': selectResult, + }, }), Draw({ - displayComponent: false, - selectableDrawModes: ['Point', 'LineString', 'Circle', 'Polygon'], + selectableDrawModes: ['Point', 'Polygon'], style: { fill: { color: 'rgba(255, 255, 255, 0.5)', @@ -45,10 +59,12 @@ export const addPlugins = (core) => { }, }, }), + Header({ + displayComponent: true, + layoutTag: NineLayoutTag.TOP_LEFT, + }), IconMenu({ displayComponent: true, - // TODO fix, it's broken ... - initiallyOpen: 'geometrySearch', menus: [ { plugin: GeometrySearch({}), @@ -67,25 +83,6 @@ export const addPlugins = (core) => { ], layoutTag: NineLayoutTag.TOP_RIGHT, }), - AddressSearch({ - displayComponent: true, - layoutTag: NineLayoutTag.TOP_LEFT, - minLength: 3, - waitMs: 500, - // @ts-expect-error | URL configured in a different way (simple API) - searchMethods: [{ type: 'coastalGazetteer' }], - addLoading: 'plugin/loadingIndicator/addLoadingKey', - removeLoading: 'plugin/loadingIndicator/removeLoadingKey', - customSearchMethods: { - // @ts-expect-error | Local parameter requirements diverge from type - coastalGazetteer: searchCoastalGazetteerByToponym, - }, - customSelectResult: { - // it's defined like that - // eslint-disable-next-line @typescript-eslint/naming-convention - '': selectResult, - }, - }), Attributions({ displayComponent: true, layoutTag: NineLayoutTag.BOTTOM_RIGHT, diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index b568e0a06..05a6e6901 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -7,6 +7,7 @@ import { wmtsTopplusOpenWebGrey, wmtsTopplusOpenLight, wmtsTopplusOpenLightGrey, + aerial, } from './services' // Gefundene Orte @@ -25,6 +26,7 @@ const locales: LanguageOption[] = [ [wmtsTopplusOpenWebGrey]: 'TopPlusOpen (Web, Grau)', [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grau)', + [aerial]: 'Luftbilder Sen2Europe', }, addressSearch: { unnamed: 'Unbenannt', @@ -33,13 +35,20 @@ const locales: LanguageOption[] = [ [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, - [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [aerial]: + '© Europäische Union, enthält veränderte Copernicus Sentinel-Daten ({{YEAR}})', static: '
Impressum', }, + info: { + noLiteratureFound: 'Es wurden keine Texte zu diesen Orten gefunden.', + noGeometriesFound: + 'Es wurden keine Orte zu dieser Geometrie gefunden.', + }, error: { searchCoastalGazetteer: 'Die Suche ist mit einem unbekannten Fehler fehlgeschlagen. Bitte versuchen Sie es später erneut.', @@ -64,6 +73,7 @@ const locales: LanguageOption[] = [ [wmtsTopplusOpenWebGrey]: 'TopPlusOpen (Web, Grey)', [wmtsTopplusOpenLight]: 'TopPlusOpen (Light)', [wmtsTopplusOpenLightGrey]: 'TopPlusOpen (Light, Grey)', + [aerial]: 'Luftbilder Sen2Europe', }, addressSearch: { unnamed: 'Unnamed', @@ -72,13 +82,19 @@ const locales: LanguageOption[] = [ [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, [openSeaMap]: `$t(textLocator.layers.${openSeaMap}): © OpenSeaMap`, [mdiSeaNames]: `$t(textLocator.layers.${mdiSeaNames}): © MDI DE`, - [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie `, - [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie `, + [wmtsTopplusOpenWeb]: `$t(textLocator.layers.${wmtsTopplusOpenWeb}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenWebGrey]: `$t(textLocator.layers.${wmtsTopplusOpenWebGrey}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenLight]: `$t(textLocator.layers.${wmtsTopplusOpenLight}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, + [aerial]: + '© European Union, contains modified Copernicus Sentinel data ({{YEAR}})', static: '
Legal notice (Impressum)', }, + info: { + noLiteratureFound: 'No texts were found regarding these places.', + noGeometriesFound: 'No places were found regarding this geometry.', + }, error: { searchCoastalGazetteer: 'The search failed with an unknown error. Please try again later.', diff --git a/packages/clients/textLocator/src/mapConfig.ts b/packages/clients/textLocator/src/mapConfig.ts index 579145a96..bcc14204f 100644 --- a/packages/clients/textLocator/src/mapConfig.ts +++ b/packages/clients/textLocator/src/mapConfig.ts @@ -10,6 +10,7 @@ import { wmtsTopplusOpenWebGrey, wmtsTopplusOpenLight, wmtsTopplusOpenLightGrey, + aerial, } from './services' import { theme } from './palettes' @@ -52,6 +53,11 @@ export const mapConfiguration: Partial = { type: 'background', name: `textLocator.layers.${openStreetMap}`, }, + { + id: aerial, + type: 'background', + name: `textLocator.layers.${aerial}`, + }, { id: openSeaMap, type: 'mask', diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue index 3649c0b40..689db9c9b 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue @@ -4,7 +4,7 @@ {{ $t('common:plugins.geometrySearch.draw.title') }} - + - + {{ - $t(`common:plugins.geometrySearch.draw.description.${_draw}`) + $t(`common:plugins.geometrySearch.draw.description.${_drawMode}`) }} @@ -50,9 +53,21 @@ dense hoverable activatable + color="info" :items="treeViewItems" + @update:active="changeActiveData" > - + {{ $t('common:plugins.geometrySearch.results.none') }} @@ -62,9 +77,10 @@ From b5d878e59d2ab8ace201370305cd7a712188b691 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 10 Apr 2024 07:10:53 +0200 Subject: [PATCH 022/173] add notes to clarify content state --- packages/clients/textLocator/src/locales.ts | 4 +--- packages/clients/textLocator/src/palettes.ts | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index 05a6e6901..0ef6a93f7 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -10,9 +10,6 @@ import { aerial, } from './services' -// Gefundene Orte -// Gefundene Texte - const locales: LanguageOption[] = [ { type: 'de', @@ -61,6 +58,7 @@ const locales: LanguageOption[] = [ }, }, }, + // NOTE: English translation not yet required and may be incomplete { type: 'en', resources: { diff --git a/packages/clients/textLocator/src/palettes.ts b/packages/clients/textLocator/src/palettes.ts index c34c72be5..b9ec4889c 100644 --- a/packages/clients/textLocator/src/palettes.ts +++ b/packages/clients/textLocator/src/palettes.ts @@ -18,6 +18,7 @@ export const dataportPalette = { gold: '#FFD500', } +// TODO: May be used later to distinguish multiple geometries to an entry export const pastelPalette = [ '#66c2a5', '#fc8d62', @@ -29,6 +30,7 @@ export const pastelPalette = [ '#b3b3b3', ] +// TODO: Use later for text search result heatmaps export const heatPalette = [ '#fff5f0', '#fee2d5', From 91cd66bbcd357f9fa7ff13fc5990935726c5aedf Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 10 Apr 2024 07:11:24 +0200 Subject: [PATCH 023/173] add fitting version --- packages/clients/textLocator/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clients/textLocator/package.json b/packages/clients/textLocator/package.json index d4416b7d5..b5e776fce 100644 --- a/packages/clients/textLocator/package.json +++ b/packages/clients/textLocator/package.json @@ -1,6 +1,6 @@ { "name": "@polar/client-text-locator", - "version": "0.1.0", + "version": "1.0.0-alpha.0", "description": "Client TextLocator", "license": "EUPL-1.2", "type": "module", From 45818453a118b4178fda6f69d2c3b18c660ba02c Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 10 Apr 2024 07:11:59 +0200 Subject: [PATCH 024/173] update note --- .../src/plugins/GeometrySearch/utils/makeTreeView.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/utils/makeTreeView.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/utils/makeTreeView.ts index e89724634..aa2b1e124 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/utils/makeTreeView.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/utils/makeTreeView.ts @@ -8,12 +8,12 @@ export const makeTreeView = ( const byTextTreeViewItems: TreeViewItem[] = Object.entries( titleLocationFrequency ).map(([title, locations]) => ({ - id: title, // TODO request an additional ID + id: title, // TODO additional ID requested; switch when available name: title, count: Object.values(locations).reduce((acc, curr) => acc + curr), type: 'text', children: Object.entries(locations).map(([location, count]) => ({ - id: location, // TODO request an additional ID + id: location, // TODO additional ID requested; switch when available name: location, count, type: 'toponym', From a746a96bd907f2e316e441019154e0bc59dc70d2 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Wed, 10 Apr 2024 07:12:20 +0200 Subject: [PATCH 025/173] improve typing to denote required feature properties --- .../src/plugins/GeometrySearch/types.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts index 7e0a0f825..37d02f15f 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/types.ts @@ -1,5 +1,6 @@ -import { FeatureCollection } from 'geojson' +import { FeatureCollection, GeoJsonProperties, Geometry } from 'geojson' import { TitleLocationFrequency } from '../../utils/literatureByToponym' +import { ResponseGeom, ResponseName } from '../../utils/coastalGazetteer/types' export type TextLocatorCategories = 'text' | 'toponym' @@ -11,8 +12,18 @@ export interface TreeViewItem { type: TextLocatorCategories } +export type GeometrySearchFeatureProperties = GeoJsonProperties & { + title: string + // using only implemented gazetteer's type for now – on extension, this may require a broader type + names: ResponseName[] + geometries: ResponseGeom[] +} + export interface GeometrySearchState { - featureCollection: FeatureCollection + featureCollection: FeatureCollection< + Geometry, + GeometrySearchFeatureProperties + > titleLocationFrequency: TitleLocationFrequency byCategory: TextLocatorCategories } From fe9b2d68a825f5ff535f1bde52c22f041d235982 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Thu, 11 Apr 2024 09:59:13 +0200 Subject: [PATCH 026/173] add result info per search result --- .../clients/textLocator/src/addPlugins.ts | 2 + .../textLocator/src/components/ResultInfo.vue | 70 +++++++++++++++++++ packages/clients/textLocator/src/locales.ts | 6 ++ packages/plugins/AddressSearch/CHANGELOG.md | 1 + packages/plugins/AddressSearch/README.md | 1 + .../AddressSearch/src/components/Results.vue | 10 +++ .../AddressSearch/src/store/getters.ts | 4 ++ 7 files changed, 94 insertions(+) create mode 100644 packages/clients/textLocator/src/components/ResultInfo.vue diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index 1f6974943..ddde8c84c 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -12,6 +12,7 @@ import Zoom from '@polar/plugin-zoom' import Header from './plugins/Header' import GeometrySearch from './plugins/GeometrySearch' +import ResultInfo from './components/ResultInfo.vue' import { searchCoastalGazetteerByToponym, selectResult, @@ -42,6 +43,7 @@ export const addPlugins = (core) => { // eslint-disable-next-line @typescript-eslint/naming-convention '': selectResult, }, + afterResultComponent: ResultInfo, }), Draw({ selectableDrawModes: ['Point', 'Polygon'], diff --git a/packages/clients/textLocator/src/components/ResultInfo.vue b/packages/clients/textLocator/src/components/ResultInfo.vue new file mode 100644 index 000000000..e92c9325c --- /dev/null +++ b/packages/clients/textLocator/src/components/ResultInfo.vue @@ -0,0 +1,70 @@ + + + + + diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index 0ef6a93f7..b5f078223 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -27,6 +27,12 @@ const locales: LanguageOption[] = [ }, addressSearch: { unnamed: 'Unbenannt', + resultInfo: { + name: 'Name', + type: 'Typ', + language: 'Sprache', + timeFrame: 'Zeitraum', + }, }, attributions: { [openStreetMap]: `$t(textLocator.layers.${openStreetMap}): © OpenStreetMap contributors`, diff --git a/packages/plugins/AddressSearch/CHANGELOG.md b/packages/plugins/AddressSearch/CHANGELOG.md index 56c1507f5..928ae0794 100644 --- a/packages/plugins/AddressSearch/CHANGELOG.md +++ b/packages/plugins/AddressSearch/CHANGELOG.md @@ -3,6 +3,7 @@ ## unpublished - Feature: Add title internationalization; i.e. features may now contain locale keys as titles. +- Feature: New configuration field `afterResultComponent` was added that allows to display a custom component for each search result. ## 1.2.1 diff --git a/packages/plugins/AddressSearch/README.md b/packages/plugins/AddressSearch/README.md index 3190b558e..9b15bbc22 100644 --- a/packages/plugins/AddressSearch/README.md +++ b/packages/plugins/AddressSearch/README.md @@ -32,6 +32,7 @@ In `categoryProperties` and `groupProperties`, id strings called `groupId` and ` | categoryProperties | Record? | An object defining properties for a category. The searchMethod's categoryId is used as identifier. A service without categoryId does not have a fallback category. | | focusAfterSearch | boolean? | Whether the focus should switch to the first result after a successful search. Defaults to `false`. | | groupProperties | Record? | An object defining properties for a group. The searchMethod's groupId is used as identifier. All services without groupId fall back to the key `"defaultGroup"`. | +| afterResultComponent | Vue? | If given, this component will be rendered in the last line of every single search result. It will be forwarded its search result feature as prop `feature` of type `GeoJSON.Feature`, and the focus state of the result as prop `focus` of type `boolean`. | #### addressSearch.searchMethodsObject diff --git a/packages/plugins/AddressSearch/src/components/Results.vue b/packages/plugins/AddressSearch/src/components/Results.vue index e89e3ed88..1ab83a419 100644 --- a/packages/plugins/AddressSearch/src/components/Results.vue +++ b/packages/plugins/AddressSearch/src/components/Results.vue @@ -52,11 +52,19 @@ @keydown.down.prevent.stop="(event) => focusNextElement(true, event)" @keydown.up.prevent.stop="(event) => focusNextElement(false, event)" @click="selectResult({ feature, categoryId })" + @focus="focusIndex = `${index}-${innerDex}`" + @blur="focusIndex = ''" > + ({ openCategories: [] as string[], + focusIndex: '', }), computed: { ...mapGetters(['clientHeight', 'hasWindowSize']), ...mapGetters('plugin/addressSearch', [ + 'afterResultComponent', 'featuresAvailable', 'featureListsWithCategory', 'focusAfterSearch', diff --git a/packages/plugins/AddressSearch/src/store/getters.ts b/packages/plugins/AddressSearch/src/store/getters.ts index 0cedc48cb..7f036c819 100644 --- a/packages/plugins/AddressSearch/src/store/getters.ts +++ b/packages/plugins/AddressSearch/src/store/getters.ts @@ -5,6 +5,7 @@ import { PolarGetterTree, SearchMethodConfiguration, } from '@polar/lib-custom-types' +import Vue from 'vue' import SearchResults from '../utils/searchResultSymbols' import { AddressSearchGetters, @@ -61,6 +62,9 @@ const getters: PolarGetterTree = { ...(rootGetters.configuration?.addressSearch || {}), } }, + afterResultComponent(_, { addressSearchConfiguration }): Vue | null { + return addressSearchConfiguration.afterResultComponent || null + }, minLength(_, { addressSearchConfiguration }) { return addressSearchConfiguration.minLength }, From de8d53c4673d568bf845776bab93df5407febff6 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Thu, 11 Apr 2024 12:17:21 +0200 Subject: [PATCH 027/173] add source for search service in attributions --- packages/clients/textLocator/src/addPlugins.ts | 5 ++++- packages/clients/textLocator/src/locales.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/clients/textLocator/src/addPlugins.ts b/packages/clients/textLocator/src/addPlugins.ts index ddde8c84c..fbbbf65b2 100644 --- a/packages/clients/textLocator/src/addPlugins.ts +++ b/packages/clients/textLocator/src/addPlugins.ts @@ -98,7 +98,10 @@ export const addPlugins = (core) => { id, title: `textLocator.attributions.${id}`, })), - staticAttributions: ['textLocator.attributions.static'], + staticAttributions: [ + 'textLocator.attributions.kuestengazetteer', + 'textLocator.attributions.static', + ], }), Legend({ displayComponent: true, diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index b5f078223..e1d9753e1 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -44,8 +44,10 @@ const locales: LanguageOption[] = [ [wmtsTopplusOpenLightGrey]: `$t(textLocator.layers.${wmtsTopplusOpenLightGrey}): © Bundesamt für Kartographie und Geodäsie {{YEAR}}`, [aerial]: '© Europäische Union, enthält veränderte Copernicus Sentinel-Daten ({{YEAR}})', + kuestengazetteer: + '
Zur Ortssuche wird der Küstengazetteer eingesetzt.', static: - '
Impressum', + 'Impressum', }, info: { noLiteratureFound: 'Es wurden keine Texte zu diesen Orten gefunden.', From 7dd17823e568d251b8217c79e4550b5b5590987c Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 12 Apr 2024 07:53:29 +0200 Subject: [PATCH 028/173] add mock button for full geometrysearch for a text backend method not yet implemented --- packages/clients/textLocator/src/locales.ts | 2 ++ .../components/GeometrySearch.vue | 20 +++++++++++++++++++ .../src/plugins/GeometrySearch/language.ts | 1 + .../src/plugins/GeometrySearch/store/index.ts | 11 ++++++++++ 4 files changed, 34 insertions(+) diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index e1d9753e1..4108f3ba7 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -15,6 +15,8 @@ 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', diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue index 6bc702119..d578452c0 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue @@ -122,6 +122,25 @@ {{ $t('plugins.geometrySearch.tooltip.focusSearch') }} + + + {{ $t('plugins.geometrySearch.tooltip.textSearch') }} + @@ -170,6 +189,7 @@ export default Vue.extend({ ...mapActions('plugin/geometrySearch', [ 'changeActiveData', 'fullSearchOnToponym', + 'fullSearchLiterature', ]), ...mapMutations('plugin/geometrySearch', ['setByCategory']), }, diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts index 6b2164d4d..62d73f1e1 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts @@ -30,6 +30,7 @@ const language: LanguageOption[] = [ heat: 'Auf Funde zoomen und nach Relevanz färben', }, focusSearch: 'Neue Suche nach allen Geometrien zu dieser Geometrie', + textSearch: 'Suche nach allen Geometrien zu diesem Text', }, results: { title: 'Funde', diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts index f96b6b8c0..a708b8766 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/store/index.ts @@ -93,6 +93,17 @@ export const makeStoreModule = () => { ) dispatch('searchGeometry', geoJson.readFeature(feature)) }, + fullSearchLiterature({ dispatch }) { + dispatch( + 'plugin/toast/addToast', + { + type: 'info', + text: 'common:textLocator.notImplemented', + timeout: 5000, + }, + { root: true } + ) + }, }, mutations: { ...generateSimpleMutations(getInitialState()), From cd8b28fd62073d996c284ff0a084e3ceebc9b713 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 12 Apr 2024 08:53:11 +0200 Subject: [PATCH 029/173] change geometry tooltip display to primary names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, all names of all geometries were shown as a list. Now, only primary names of an object are used, and if an object holds multiple primary names, they are listed in a single list item. There seem to be data errors where objects are without names. They have been reported. For the time being, '???' is displayed for those items, in the same fashion as it's done in the Küstengazetteer. --- .../store/actions/setupTooltip.ts | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts index 4daf891aa..d6663eb4b 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts @@ -2,6 +2,10 @@ import { Tooltip, getTooltip } from '@polar/lib-tooltip' import { Feature, Overlay } from 'ol' import { vectorLayer } from '../../utils/vectorDisplay' +const localeKeys: [string, string][] = [ + ['h2', 'plugins.geometrySearch.tooltip.title'], +] + export function setupTooltip({ rootGetters: { map } }) { let element: Tooltip['element'], unregister: Tooltip['unregister'] const overlay = new Overlay({ @@ -14,10 +18,7 @@ export function setupTooltip({ rootGetters: { map } }) { return } let hasFeatureAtPixel = false - const localeKeys: [string, string][] = [ - ['h2', 'plugins.geometrySearch.tooltip.title'], - ] - // TODO do not list every name separately, + const listEntries: string[] = [] map.forEachFeatureAtPixel( pixel, (feature) => { @@ -28,13 +29,15 @@ export function setupTooltip({ rootGetters: { map } }) { hasFeatureAtPixel = true overlay.setPosition(map.getCoordinateFromPixel(pixel)) } - localeKeys.push([ - 'ul', - feature - .get('names') - .map((name) => `
  • ${name.Name}
  • `) - .join(''), - ]) + listEntries.push( + `
  • ${ + feature + .get('names') + .filter((name) => name.Typ === 'Primärer Name') + .map((name) => `${name.Name}`) + .join(', ') || `${feature.get('names')[0]?.Name || '???'}` + }
  • ` + ) }, { layerFilter: (layer) => layer === vectorLayer, @@ -44,7 +47,9 @@ export function setupTooltip({ rootGetters: { map } }) { overlay.setPosition(undefined) } else { unregister?.() - ;({ element, unregister } = getTooltip({ localeKeys })) + ;({ element, unregister } = getTooltip({ + localeKeys: [...localeKeys, ['ul', listEntries.join('')]], + })) overlay.setElement(element) return true } From 0f56219b7232cabe9707076d37e3a408f11e6b4f Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 12 Apr 2024 09:47:03 +0200 Subject: [PATCH 030/173] add tooltips for non-selfexplanatory numbers The numbers and their implications regarding text and toponym relations are now explained in detail by tooltip texts. --- .../components/GeometrySearch.vue | 28 ++++++++++++++----- .../src/plugins/GeometrySearch/language.ts | 10 +++++++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue index d578452c0..353065a3e 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/GeometrySearch.vue @@ -56,13 +56,27 @@ :items="treeViewItems" > diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts index 62d73f1e1..8f049efb5 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/language.ts @@ -31,6 +31,16 @@ const language: LanguageOption[] = [ }, focusSearch: 'Neue Suche nach allen Geometrien zu dieser Geometrie', textSearch: 'Suche nach allen Geometrien zu diesem Text', + badge: { + textToToponym: + 'Anzahl der Ortsnennungen zu den aktuell angezeigten Geometrien in diesem Text', + toponymInText: + 'Anzahl der Funde dieses Ortes im aktuell geöffneten Text', + toponymToText: + 'Anzahl der Ortsnennungen dieses Ortes über alle aktuell betrachteten Texte', + textInToponym: + 'Anzahl der Funde des aktuellen geöffneten Ortes in diesem Text', + }, }, results: { title: 'Funde', From dd46fe22ca5a4ee8cd8c92241ea5b4e084436607 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Fri, 12 Apr 2024 11:23:38 +0200 Subject: [PATCH 031/173] change feature display to include all geometries Previously, only the first geometry of an object was used, leading to potentially misleading UI regarding what an object actually encompasses. This has been resolved by merging all geometries belonging to a feature to a single MultiPolygon that is, for now, always shown in its completeness. --- packages/clients/textLocator/src/locales.ts | 1 - .../store/actions/setupTooltip.ts | 11 +---- .../utils/coastalGazetteer/getPrimaryName.ts | 14 ++++++ .../coastalGazetteer/responseInterpreter.ts | 47 ++++++++++++++----- 4 files changed, 51 insertions(+), 22 deletions(-) create mode 100644 packages/clients/textLocator/src/utils/coastalGazetteer/getPrimaryName.ts diff --git a/packages/clients/textLocator/src/locales.ts b/packages/clients/textLocator/src/locales.ts index 4108f3ba7..484e6b8d4 100644 --- a/packages/clients/textLocator/src/locales.ts +++ b/packages/clients/textLocator/src/locales.ts @@ -28,7 +28,6 @@ const locales: LanguageOption[] = [ [aerial]: 'Luftbilder Sen2Europe', }, addressSearch: { - unnamed: 'Unbenannt', resultInfo: { name: 'Name', type: 'Typ', diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts b/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts index d6663eb4b..17e524afa 100644 --- a/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/store/actions/setupTooltip.ts @@ -1,6 +1,7 @@ import { Tooltip, getTooltip } from '@polar/lib-tooltip' import { Feature, Overlay } from 'ol' import { vectorLayer } from '../../utils/vectorDisplay' +import { getPrimaryName } from '../../../../utils/coastalGazetteer/getPrimaryName' const localeKeys: [string, string][] = [ ['h2', 'plugins.geometrySearch.tooltip.title'], @@ -29,15 +30,7 @@ export function setupTooltip({ rootGetters: { map } }) { hasFeatureAtPixel = true overlay.setPosition(map.getCoordinateFromPixel(pixel)) } - listEntries.push( - `
  • ${ - feature - .get('names') - .filter((name) => name.Typ === 'Primärer Name') - .map((name) => `${name.Name}`) - .join(', ') || `${feature.get('names')[0]?.Name || '???'}` - }
  • ` - ) + listEntries.push(`
  • ${getPrimaryName(feature.get('names'))}
  • `) }, { layerFilter: (layer) => layer === vectorLayer, diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/getPrimaryName.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/getPrimaryName.ts new file mode 100644 index 000000000..b23c60e92 --- /dev/null +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/getPrimaryName.ts @@ -0,0 +1,14 @@ +import { ResponseName } from './types' + +/** + * Finds primary names of a response and returns them comma-separated as string. + * If no primary name is included, the first-best name will be chosen. + * If no name exists, '???' is returned as a fallback (as in Küstengazetteer). + */ +export const getPrimaryName = (names: ResponseName[]): string => + names + .filter((name) => name.Typ === 'Primärer Name') + .map((name) => `${name.Name}`) + .join(', ') || + names[0]?.Name || + '???' diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts index b58986de7..3cad86f66 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/responseInterpreter.ts @@ -1,8 +1,15 @@ import { Feature, FeatureCollection } from 'geojson' import levenshtein from 'js-levenshtein' +import { MultiPolygon } from 'ol/geom' import { wgs84ProjectionCode } from '../common' -import { ResponseName, ResponsePayload, ResponseResult } from './types' +import { + ResponseGeom, + ResponseName, + ResponsePayload, + ResponseResult, +} from './types' import { geoJson, idPrefixes, wellKnownText } from './common' +import { getPrimaryName } from './getPrimaryName' // arbitrary sort based on input – prefer 1. startsWith 2. closer string const sorter = @@ -24,23 +31,39 @@ export const getEmptyFeatureCollection = (): FeatureCollection => ({ features: [], }) +// for now: merge all geometries, independent of their timeframe +const geoJsonifyAllGeometries = ( + geoms: ResponseGeom[], + epsg: `EPSG:${string}` +) => + geoJson.writeGeometryObject( + geoms.reduce( + (multiPolygon, currentGeom) => + ( + wellKnownText.readGeometry(currentGeom.WKT, { + dataProjection: wgs84ProjectionCode, + featureProjection: epsg, + }) as MultiPolygon + ) + .getPolygons() + .reduce((accumulator, currentPolygon) => { + accumulator.appendPolygon(currentPolygon) + return accumulator + }, multiPolygon), + new MultiPolygon([]) + ) + ) + const featurify = (epsg: `EPSG:${string}`, searchPhrase: string | null) => (feature: ResponseResult): Feature | null => { - const title = - (searchPhrase - ? feature.names.sort(sorter(searchPhrase, 'Name')) - : feature.names)[0]?.Name || 'textLocator.addressSearch.unnamed' + const title = searchPhrase + ? feature.names.sort(sorter(searchPhrase, 'Name'))[0]?.Name || '???' + : getPrimaryName(feature.names) return { type: 'Feature', geometry: feature.geoms.length - ? geoJson.writeGeometryObject( - // NOTE arbitrarily, the first geometry is used - wellKnownText.readGeometry(feature.geoms[0].WKT, { - dataProjection: wgs84ProjectionCode, - featureProjection: epsg, - }) - ) + ? geoJsonifyAllGeometries(feature.geoms, epsg) : { type: 'Point', coordinates: [] }, id: feature.id, properties: { From 606e59959a22b7ac47d2f085879c9fc36b4d40d9 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Tue, 16 Apr 2024 08:50:19 +0200 Subject: [PATCH 032/173] add cleaning drawing on address search selection --- .../src/utils/coastalGazetteer/searchToponym.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts index b79a1e2f4..0850b16cc 100644 --- a/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts +++ b/packages/clients/textLocator/src/utils/coastalGazetteer/searchToponym.ts @@ -3,6 +3,7 @@ import { Map } from 'ol' import { Store } from 'vuex' import { CoreState, SelectResultFunction } from '@polar/lib-custom-types' import SearchResultSymbols from '@polar/plugin-address-search/src/utils/searchResultSymbols' +import VectorSource from 'ol/source/Vector' import { ResponsePayload } from './types' import { getAllPages } from './getAllPages' import { @@ -47,7 +48,10 @@ export async function searchCoastalGazetteerByToponym( ) } -export const selectResult: SelectResultFunction = ({ commit }, { feature }) => { +export const selectResult: SelectResultFunction = ( + { commit, rootGetters }, + { feature } +) => { // default behaviour (AddressSearch selects and is out further behaviour) commit('plugin/addressSearch/setChosenAddress', feature, { root: true }) commit('plugin/addressSearch/setInputValue', feature.title, { root: true }) @@ -56,6 +60,8 @@ export const selectResult: SelectResultFunction = ({ commit }, { feature }) => { SearchResultSymbols.NO_SEARCH, { root: true } ) + const drawSource = rootGetters['plugin/draw/drawSource'] as VectorSource + drawSource.clear() // added behaviour: push as one-element feature collection to search store commit( From 13e8f05caceacd0510a02dfa4ee8c76b3c7f07b9 Mon Sep 17 00:00:00 2001 From: Dennis Sen Date: Tue, 16 Apr 2024 08:51:02 +0200 Subject: [PATCH 033/173] add tooltip direction to ResultInfo component --- .../clients/textLocator/src/components/ResultInfo.vue | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/clients/textLocator/src/components/ResultInfo.vue b/packages/clients/textLocator/src/components/ResultInfo.vue index e92c9325c..a66b8de54 100644 --- a/packages/clients/textLocator/src/components/ResultInfo.vue +++ b/packages/clients/textLocator/src/components/ResultInfo.vue @@ -1,5 +1,5 @@ {{ item.name }} + {{ item.name }} @@ -85,80 +31,38 @@ @@ -169,49 +73,42 @@ @@ -225,27 +122,6 @@ export default Vue.extend({ min-width: 400px; white-space: normal; - .text-locator-card-collapse { - padding-bottom: 0; - } - - .text-locator-btn-group-button { - text-transform: unset; - - // determined by trial & error ¯\ツ/¯ - &:hover:not(:last-child), - &:focus:not(:last-child) { - z-index: 1; - margin-left: -2px; - margin-right: -2px; - } - - &:hover:last-child, - &:focus:last-child { - margin-left: -3px; - } - } - .text-locator-result-badge { cursor: pointer; max-width: 500px; @@ -256,3 +132,9 @@ export default Vue.extend({ } } + + diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/ViewToggle.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/ViewToggle.vue new file mode 100644 index 000000000..791d3c984 --- /dev/null +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/ViewToggle.vue @@ -0,0 +1,55 @@ + + + + + From 2cb437107085505b694abbb09c6b8a75530f38f9 Mon Sep 17 00:00:00 2001 From: Pascal Roehling Date: Thu, 18 Apr 2024 18:08:53 +0200 Subject: [PATCH 038/173] Adjust documentation to link to core documentation --- packages/clients/generic/API.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clients/generic/API.md b/packages/clients/generic/API.md index a38a34c97..f5a850e9e 100644 --- a/packages/clients/generic/API.md +++ b/packages/clients/generic/API.md @@ -16,7 +16,7 @@ The method expects a single object with the following parameters. | - | - | - | | containerId | string | ID of the container the map is supposed to render itself to. | | services | object[] \| string | Either a link to a service registry or an array containing service description objects. For more details, see the startup section on [POLAR documentation](https://dataport.github.io/polar/documentation.html#configuration). | -| mapConfiguration | object | See full documentation for all possible configuration options. | +| mapConfiguration | object | See [documentation of `@polar/core`](https://dataport.github.io/polar/docs/generic/core.html) for all possible configuration options. | | enabledPlugins | string[]? | This is a client-specific field. Since the `@polar/client-generic` client contains all existing plugins, they are activated by strings. The strings match their package names: `'address-search' \| 'attributions' \| 'draw'* \| 'export' \| 'filter'* \| 'fullscreen'* \| 'geo-location'* \| 'gfi'* \| 'icon-menu' \| 'layer-chooser'* \| 'legend' \| 'loading-indicator' \| 'pins' \| 'reverse-geocoder' \| 'scale' \| 'toast' \| 'zoom'*`. The plugins marked with * are nested in the `'icon-menu'` in this pre-layouting, hence they depend upon it being active, too. | | modifyLayerConfiguration | ((layerConf: object[]) => object[])? | Defaults to identity function. This function is applied to the loaded layer configuration before usage. That is, the `services` can be modified by this to e.g. set parameters not supported by the service register, add additional layers, and so on. | From dcf43480141c06d26ff9b9bb1b1bc3cfc5f82e76 Mon Sep 17 00:00:00 2001 From: Pascal Roehling Date: Thu, 18 Apr 2024 18:12:03 +0200 Subject: [PATCH 039/173] Add a MWE for the generic client --- packages/clients/generic/API.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/clients/generic/API.md b/packages/clients/generic/API.md index f5a850e9e..94ce749d6 100644 --- a/packages/clients/generic/API.md +++ b/packages/clients/generic/API.md @@ -6,6 +6,8 @@ For all additional details, check the [full documentation](https://dataport.gith For the development test deployments and example configurations, [see here](https://dataport.github.io/polar#plugin-gallery). +For a minimum working example, [see here](https://github.com/dopenguin/polar-fossgis-2024). + ## Basic usage The NPM package `@polar/client-generic` can be installed via NPM or downloaded from the [release page](https://github.com/Dataport/polar/releases). When using `import mapClient from '@polar/client-generic'`, the object `mapClient` contains a method `createMap`. This is the main method required to get the client up and running. Should you use another import method, check the package's `dist` folder for available files. From a64c5154d2a5cc01c8bbe6d5a8072150b936aefb Mon Sep 17 00:00:00 2001 From: Pascal Roehling Date: Thu, 18 Apr 2024 18:13:06 +0200 Subject: [PATCH 040/173] Fix outdated type of pins.initial --- packages/plugins/Pins/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/Pins/README.md b/packages/plugins/Pins/README.md index d15cf0735..2be8bf3b6 100644 --- a/packages/plugins/Pins/README.md +++ b/packages/plugins/Pins/README.md @@ -17,7 +17,7 @@ The usage of `displayComponent` has no influence on the creation of Pins on the | appearOnClick | appearOnClick | Pin restrictions. See object description below. | | coordinateSource | string | The pins plugin may react to changes in other plugins. This specifies the path to such store positions. The position must, when subscribed to, return a GeoJSON feature. Please mind that, when referencing another plugin, that plugin must be in `addPlugins` before this one. | | style | style? | Display style configuration. | -| initial | number[]? | Configuration options for setting an initial pin. | +| initial | initial? | Configuration options for setting an initial pin. | | boundaryLayerId | string? | Id of a vector layer to restrict pins to. When pins are moved or created outside of the boundary, an information will be shown and the pin is reset to its previous state. The map will wait at most 10s for the layer to load; should it not happen, the geolocation feature is turned off. | | boundaryOnError | ('strict' \| 'permissive')? | If the boundary layer check does not work due to loading or configuration errors, style `'strict'` will disable the pins feature, and style `'permissive'` will act as if no boundaryLayerId was set. Defaults to `'permissive'`. | | toastAction | string? | If `boundaryLayerId` is set, and the pin is moved or created outside the boundary, this string will be used as action to send a toast information to the user. If no toast information is desired, leave this field undefined; for testing purposes, you can still find information in the console. | From 71df4379eed5cc462917fc052af09f044dca3020 Mon Sep 17 00:00:00 2001 From: Pascal Roehling Date: Fri, 19 Apr 2024 18:51:13 +0200 Subject: [PATCH 041/173] Add MWE to documentation page --- pages/documentation.html | 1 + 1 file changed, 1 insertion(+) diff --git a/pages/documentation.html b/pages/documentation.html index b1f6b61f7..9142a7607 100644 --- a/pages/documentation.html +++ b/pages/documentation.html @@ -133,6 +133,7 @@

    Usage pattern

  • Startup code. This is mostly putting the aforementioned parts together and adding configuration and subscriptions. + For a minimum working example, checkout this repository which includes the example shown in the FOSSGIS 2024 presentation.
    
     import client from '@polar/client-generic'
     
    
    From 25be13556bd5780e40ea56fc73c93bc32a4fa91f Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 18:51:33 +0200
    Subject: [PATCH 042/173] Add further description to configuration parameter
     services of generic
    
    ---
     packages/clients/generic/API.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/packages/clients/generic/API.md b/packages/clients/generic/API.md
    index 94ce749d6..b8d5eb09b 100644
    --- a/packages/clients/generic/API.md
    +++ b/packages/clients/generic/API.md
    @@ -17,7 +17,7 @@ The method expects a single object with the following parameters.
     | fieldName | type | description |
     | - | - | - |
     | containerId | string | ID of the container the map is supposed to render itself to. |
    -| services | object[] \| string | Either a link to a service registry or an array containing service description objects. For more details, see the startup section on [POLAR documentation](https://dataport.github.io/polar/documentation.html#configuration). |
    +| services | object[] \| string | Either a link to a service registry or an array containing service description objects. For more details, see the startup section on [POLAR documentation](https://dataport.github.io/polar/documentation.html#configuration). This parameter can be seen as an accesible version of `mapConfiguration.layerConf`. |
     | mapConfiguration | object | See [documentation of `@polar/core`](https://dataport.github.io/polar/docs/generic/core.html) for all possible configuration options. |
     | enabledPlugins  | string[]? | This is a client-specific field. Since the `@polar/client-generic` client contains all existing plugins, they are activated by strings. The strings match their package names: `'address-search' \| 'attributions' \| 'draw'* \| 'export' \| 'filter'* \| 'fullscreen'* \| 'geo-location'* \| 'gfi'* \| 'icon-menu' \| 'layer-chooser'* \| 'legend' \| 'loading-indicator' \| 'pins' \| 'reverse-geocoder' \| 'scale' \| 'toast' \| 'zoom'*`. The plugins marked with * are nested in the `'icon-menu'` in this pre-layouting, hence they depend upon it being active, too. |
     | modifyLayerConfiguration | ((layerConf: object[]) => object[])? | Defaults to identity function. This function is applied to the loaded layer configuration before usage. That is, the `services` can be modified by this to e.g. set parameters not supported by the service register, add additional layers, and so on. |
    
    From 40ade59244a12e0c0bce76f8c08fb03d1b53373c Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 19:19:09 +0200
    Subject: [PATCH 043/173] Add linebreak to documentation
    
    ---
     pages/documentation.html | 1 +
     1 file changed, 1 insertion(+)
    
    diff --git a/pages/documentation.html b/pages/documentation.html
    index 9142a7607..67e552319 100644
    --- a/pages/documentation.html
    +++ b/pages/documentation.html
    @@ -133,6 +133,7 @@ 

    Usage pattern

  • Startup code. This is mostly putting the aforementioned parts together and adding configuration and subscriptions. +
    For a minimum working example, checkout this repository which includes the example shown in the FOSSGIS 2024 presentation.
    
     import client from '@polar/client-generic'
    
    From 8ec43a63bfcdfdadb4887527def07b53eb648ae3 Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 19:20:06 +0200
    Subject: [PATCH 044/173] Add missing 's'
    
    ---
     packages/clients/generic/API.md | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/packages/clients/generic/API.md b/packages/clients/generic/API.md
    index b8d5eb09b..abf55fb6a 100644
    --- a/packages/clients/generic/API.md
    +++ b/packages/clients/generic/API.md
    @@ -17,7 +17,7 @@ The method expects a single object with the following parameters.
     | fieldName | type | description |
     | - | - | - |
     | containerId | string | ID of the container the map is supposed to render itself to. |
    -| services | object[] \| string | Either a link to a service registry or an array containing service description objects. For more details, see the startup section on [POLAR documentation](https://dataport.github.io/polar/documentation.html#configuration). This parameter can be seen as an accesible version of `mapConfiguration.layerConf`. |
    +| services | object[] \| string | Either a link to a service registry or an array containing service description objects. For more details, see the startup section on [POLAR documentation](https://dataport.github.io/polar/documentation.html#configuration). This parameter can be seen as an accessible version of `mapConfiguration.layerConf`. |
     | mapConfiguration | object | See [documentation of `@polar/core`](https://dataport.github.io/polar/docs/generic/core.html) for all possible configuration options. |
     | enabledPlugins  | string[]? | This is a client-specific field. Since the `@polar/client-generic` client contains all existing plugins, they are activated by strings. The strings match their package names: `'address-search' \| 'attributions' \| 'draw'* \| 'export' \| 'filter'* \| 'fullscreen'* \| 'geo-location'* \| 'gfi'* \| 'icon-menu' \| 'layer-chooser'* \| 'legend' \| 'loading-indicator' \| 'pins' \| 'reverse-geocoder' \| 'scale' \| 'toast' \| 'zoom'*`. The plugins marked with * are nested in the `'icon-menu'` in this pre-layouting, hence they depend upon it being active, too. |
     | modifyLayerConfiguration | ((layerConf: object[]) => object[])? | Defaults to identity function. This function is applied to the loaded layer configuration before usage. That is, the `services` can be modified by this to e.g. set parameters not supported by the service register, add additional layers, and so on. |
    
    From 9b3240763f052909d7bbcda8937c0f878f7e8e45 Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 20:47:31 +0200
    Subject: [PATCH 045/173] Add basic tests for the store of
     @polar/plugin-icon-menu
    
    ---
     packages/plugins/IconMenu/tests/store.spec.ts | 81 +++++++++++++++++++
     1 file changed, 81 insertions(+)
     create mode 100644 packages/plugins/IconMenu/tests/store.spec.ts
    
    diff --git a/packages/plugins/IconMenu/tests/store.spec.ts b/packages/plugins/IconMenu/tests/store.spec.ts
    new file mode 100644
    index 000000000..0c9f090e7
    --- /dev/null
    +++ b/packages/plugins/IconMenu/tests/store.spec.ts
    @@ -0,0 +1,81 @@
    +import i18next, { TFunction } from 'i18next'
    +import { makeStoreModule } from '../src/store'
    +
    +describe('plugin-icon-menu', () => {
    +  describe('store', () => {
    +    describe('actions', () => {
    +      let actionContext
    +      let commit
    +      let dispatch
    +      let storeModule
    +      beforeEach(() => {
    +        commit = jest.fn()
    +        dispatch = jest.fn()
    +        storeModule = makeStoreModule()
    +        actionContext = {
    +          commit,
    +          dispatch,
    +          getters: {
    +            menus: [
    +              { id: 'draw', hint: 'Draw hint', plugin: Symbol('draw plugin') },
    +            ],
    +          },
    +          rootGetters: {},
    +        }
    +      })
    +      describe('openMenuById', () => {
    +        it('should open the menu with the given id if it is configured', () => {
    +          const openId = 'gfi'
    +          actionContext.getters.menus.push({ id: openId })
    +          storeModule.actions.openMenuById(actionContext, openId)
    +
    +          expect(commit.mock.calls.length).toEqual(1)
    +          expect(commit.mock.calls[0][0]).toEqual('setOpen')
    +          expect(commit.mock.calls[0][1]).toEqual(1)
    +          expect(dispatch.mock.calls.length).toEqual(1)
    +          expect(dispatch.mock.calls[0][0]).toEqual('openInMoveHandle')
    +          expect(dispatch.mock.calls[0][1]).toEqual(1)
    +        })
    +        it('should do nothing if the menu with the given id is not configured', () => {
    +          storeModule.actions.openMenuById(actionContext, '')
    +
    +          expect(commit.mock.calls.length).toEqual(0)
    +          expect(dispatch.mock.calls.length).toEqual(0)
    +        })
    +      })
    +      describe('openInMoveHandle', () => {
    +        it('should add the menu with the given index to the moveHandle if the client has the same size as the window and the width of the client is small', () => {
    +          i18next.init({
    +            lng: 'cimode',
    +            debug: false,
    +          })
    +
    +          actionContext.rootGetters.hasWindowSize = true
    +          actionContext.rootGetters.hasSmallWidth = true
    +          storeModule.actions.openInMoveHandle(actionContext, 0)
    +
    +          expect(commit.mock.calls.length).toEqual(1)
    +          expect(commit.mock.calls[0][0]).toEqual('setMoveHandle')
    +          const secondParameter = commit.mock.calls[0][1]
    +          expect(typeof secondParameter).toEqual('object')
    +          expect(secondParameter.closeLabel).toEqual(
    +            'plugins.iconMenu.mobileCloseButton'
    +          )
    +          expect(typeof secondParameter.closeFunction).toEqual('function')
    +          expect(secondParameter.component).toEqual(
    +            actionContext.getters.menus[0].plugin
    +          )
    +          expect(secondParameter.plugin).toEqual('iconMenu')
    +
    +          expect(dispatch.mock.calls.length).toEqual(0)
    +        })
    +        it('should do nothing if the client either does not have the same size as the window or the width of the client is considered large', () => {
    +          storeModule.actions.openMenuById(actionContext, '')
    +
    +          expect(commit.mock.calls.length).toEqual(0)
    +          expect(dispatch.mock.calls.length).toEqual(0)
    +        })
    +      })
    +    })
    +  })
    +})
    
    From 4780d3751caa00697a0c04aab76f6041badf97ee Mon Sep 17 00:00:00 2001
    From: =?UTF-8?q?Pascal=20R=C3=B6hling?=
     <73653210+dopenguin@users.noreply.github.com>
    Date: Fri, 19 Apr 2024 20:51:08 +0200
    Subject: [PATCH 046/173] Remove unused import
    
    ---
     packages/plugins/IconMenu/tests/store.spec.ts | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/packages/plugins/IconMenu/tests/store.spec.ts b/packages/plugins/IconMenu/tests/store.spec.ts
    index 0c9f090e7..318f04d5e 100644
    --- a/packages/plugins/IconMenu/tests/store.spec.ts
    +++ b/packages/plugins/IconMenu/tests/store.spec.ts
    @@ -1,4 +1,4 @@
    -import i18next, { TFunction } from 'i18next'
    +import i18next from 'i18next'
     import { makeStoreModule } from '../src/store'
     
     describe('plugin-icon-menu', () => {
    
    From 2b395dbc815888cb555f77f068c54fbcc35a1a22 Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 21:29:05 +0200
    Subject: [PATCH 047/173] Update initiallyOpen mechanism by adding new core
     state value
    
    Also, move watcher to store.
    ---
     packages/core/CHANGELOG.md                       |  4 ++++
     .../src/utils/createMap/updateSizeOnReady.ts     |  2 ++
     packages/core/src/vuePlugins/vuex.ts             |  1 +
     .../plugins/IconMenu/src/components/IconMenu.vue | 12 ------------
     packages/plugins/IconMenu/src/store/index.ts     | 16 +++++++++++++++-
     packages/types/custom/CHANGELOG.md               |  1 +
     packages/types/custom/core.ts                    |  2 ++
     7 files changed, 25 insertions(+), 13 deletions(-)
    
    diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
    index 03a6b1329..08741e69a 100644
    --- a/packages/core/CHANGELOG.md
    +++ b/packages/core/CHANGELOG.md
    @@ -1,5 +1,9 @@
     # CHANGELOG
     
    +## unpublished
    +
    +- Feature: Add new state parameter `mapHasDimensions` to let plugins have a "hook" to react on when the map is ready.
    +
     ## 1.4.1
     
     - Feature: Additionally export `PolarCore` type.
    diff --git a/packages/core/src/utils/createMap/updateSizeOnReady.ts b/packages/core/src/utils/createMap/updateSizeOnReady.ts
    index 1d2faeaf3..94dc14aca 100644
    --- a/packages/core/src/utils/createMap/updateSizeOnReady.ts
    +++ b/packages/core/src/utils/createMap/updateSizeOnReady.ts
    @@ -20,12 +20,14 @@ export const updateSizeOnReady = (instance: Vue) => {
           console.error(
             `@polar/core: The POLAR map client could not update its size. The map is probably invisible due to having 0 width or 0 height. This might be a CSS issue – please check the wrapper's size.`
           )
    +      instance.$store.commit('setMapHasDimensions', false)
         } else {
           // OL prints warnings – add this log to reduce confusion
           // eslint-disable-next-line no-console
           console.log(
             `@polar/core: The map now has dimensions and can be rendered.`
           )
    +      instance.$store.commit('setMapHasDimensions', true)
           clearInterval(intervalId)
         }
       }, 0)
    diff --git a/packages/core/src/vuePlugins/vuex.ts b/packages/core/src/vuePlugins/vuex.ts
    index 3b94c3124..c610af0aa 100644
    --- a/packages/core/src/vuePlugins/vuex.ts
    +++ b/packages/core/src/vuePlugins/vuex.ts
    @@ -74,6 +74,7 @@ const getInitialState = (): CoreState => ({
       hasSmallDisplay: false,
       errors: [],
       language: '',
    +  mapHasDimensions: null,
     })
     
     // OK for store creation
    diff --git a/packages/plugins/IconMenu/src/components/IconMenu.vue b/packages/plugins/IconMenu/src/components/IconMenu.vue
    index 5df4ad784..b4942e564 100644
    --- a/packages/plugins/IconMenu/src/components/IconMenu.vue
    +++ b/packages/plugins/IconMenu/src/components/IconMenu.vue
    @@ -68,7 +68,6 @@ export default Vue.extend({
       computed: {
         ...mapGetters([
           'clientHeight',
    -      'clientWidth',
           'hasSmallDisplay',
           'hasSmallHeight',
           'hasSmallWidth',
    @@ -97,17 +96,6 @@ export default Vue.extend({
         },
       },
       watch: {
    -    // The map initially has a height of 0, so initially opening a menu only works after the height has changed
    -    clientHeight(newValue: number, oldValue: number) {
    -      if (
    -        oldValue === 0 &&
    -        newValue > 0 &&
    -        !this.hasSmallHeight &&
    -        !this.hasSmallWidth
    -      ) {
    -        this.openMenuById(this.initiallyOpen)
    -      }
    -    },
         // Fixes an issue if the orientation of a mobile device is changed while a plugin is open
         isHorizontal(newVal: boolean) {
           if (!newVal) {
    diff --git a/packages/plugins/IconMenu/src/store/index.ts b/packages/plugins/IconMenu/src/store/index.ts
    index addbae4d9..cf9ab2dd3 100644
    --- a/packages/plugins/IconMenu/src/store/index.ts
    +++ b/packages/plugins/IconMenu/src/store/index.ts
    @@ -18,7 +18,7 @@ export const makeStoreModule = () => {
         namespaced: true,
         state: getInitialState(),
         actions: {
    -      setupModule({ commit, rootGetters }): void {
    +      setupModule({ commit, dispatch, getters, rootGetters }): void {
             const menus = rootGetters.configuration?.iconMenu?.menus || []
             const initializedMenus = menus
               .filter(({ id }) => {
    @@ -45,6 +45,20 @@ export const makeStoreModule = () => {
               })
     
             commit('setMenus', initializedMenus)
    +
    +        // The map initially has a height of 0, so initially opening a menu only works after the height has changed
    +        this.watch(
    +          () => rootGetters.mapHasDimensions,
    +          (value) => {
    +            if (
    +              value &&
    +              !rootGetters.hasSmallHeight &&
    +              !rootGetters.hasSmallWidth
    +            ) {
    +              dispatch('openMenuById', getters.initiallyOpen)
    +            }
    +          }
    +        )
           },
           openMenuById({ commit, dispatch, getters }, openId: string) {
             const index = getters.menus.findIndex(({ id }) => id === openId)
    diff --git a/packages/types/custom/CHANGELOG.md b/packages/types/custom/CHANGELOG.md
    index bf2ba3460..8055d28a8 100644
    --- a/packages/types/custom/CHANGELOG.md
    +++ b/packages/types/custom/CHANGELOG.md
    @@ -2,6 +2,7 @@
     
     ## unpublished
     
    +- Feature: Add `mapHasDimensions` to `CoreState` and `CoreGetters`.
     - Fix: Add `string` as option for `SearchType` since arbitrary strings can be registered.
     
     ## 1.4.1
    diff --git a/packages/types/custom/core.ts b/packages/types/custom/core.ts
    index dc8cdaa25..3dfca4369 100644
    --- a/packages/types/custom/core.ts
    +++ b/packages/types/custom/core.ts
    @@ -648,6 +648,7 @@ export interface CoreState {
       hovered: number
       language: string
       map: number
    +  mapHasDimensions: boolean | null
       moveHandle: number
       moveHandleActionButton: number
       plugin: object
    @@ -665,6 +666,7 @@ export interface CoreGetters {
       hovered: Feature | null
       errors: PolarError[]
       map: Map
    +  mapHasDimensions: boolean | null
       moveHandle: MoveHandleProperties
       moveHandleActionButton: MoveHandleActionButton
       selected: Feature | null
    
    From 7804ed76b3289563f1a9fb1a7bf8bef9d9de930c Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 19 Apr 2024 21:33:55 +0200
    Subject: [PATCH 048/173] Fix tests by adding missing new state parameter
    
    ---
     packages/lib/testMountParameters/CHANGELOG.md | 4 ++++
     packages/lib/testMountParameters/index.ts     | 1 +
     2 files changed, 5 insertions(+)
    
    diff --git a/packages/lib/testMountParameters/CHANGELOG.md b/packages/lib/testMountParameters/CHANGELOG.md
    index 91d704261..ccfedc1a2 100644
    --- a/packages/lib/testMountParameters/CHANGELOG.md
    +++ b/packages/lib/testMountParameters/CHANGELOG.md
    @@ -1,5 +1,9 @@
     # CHANGELOG
     
    +## unpublished
    +
    +- Feature: Extend mock state to match current core state type.
    +
     ## 1.2.1
     
     - Fix: Test now uses a mock EPSG code instead of an empty string.
    diff --git a/packages/lib/testMountParameters/index.ts b/packages/lib/testMountParameters/index.ts
    index aa42e8b61..8e718a654 100644
    --- a/packages/lib/testMountParameters/index.ts
    +++ b/packages/lib/testMountParameters/index.ts
    @@ -61,6 +61,7 @@ export default (): MockParameters => {
           moveHandleActionButton: 0,
           plugin: {},
           language: '',
    +      mapHasDimensions: null,
           zoomLevel: 0,
           hovered: 0,
           selected: 0,
    
    From 6a9803222add77276293bfb2bf35baf76aa26082 Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Thu, 25 Apr 2024 18:02:39 +0200
    Subject: [PATCH 049/173] Remove props of AttributionsContent and move logic to
     the component
    
    ---
     .../src/components/AttributionContent.vue     | 47 +++++++++++--------
     .../src/components/Attributions.vue           | 19 +-------
     2 files changed, 30 insertions(+), 36 deletions(-)
    
    diff --git a/packages/plugins/Attributions/src/components/AttributionContent.vue b/packages/plugins/Attributions/src/components/AttributionContent.vue
    index e6c7b9f18..19a297fb6 100644
    --- a/packages/plugins/Attributions/src/components/AttributionContent.vue
    +++ b/packages/plugins/Attributions/src/components/AttributionContent.vue
    @@ -1,12 +1,6 @@
     
         
    @@ -62,10 +61,7 @@ export default Vue.extend({
       },
       data: () => ({ closeLabel: 'common:plugins.gfi.header.close' }),
       computed: {
    -    ...mapGetters(['clientWidth', 'hasSmallWidth', 'hasWindowSize']),
    -    photoHeight(): number {
    -      return this.clientWidth * 0.15
    -    },
    +    ...mapGetters(['hasSmallWidth', 'hasWindowSize']),
       },
       methods: {
         ...mapActions('plugin/gfi', ['close']),
    diff --git a/packages/plugins/Gfi/src/components/FeatureTableBody.vue b/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    index 0a5503ced..958e47172 100644
    --- a/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    +++ b/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    @@ -9,7 +9,7 @@
                 :alt="$t('common:plugins.gfi.property.imageAlt')"
                 :title="$t('common:plugins.gfi.property.linkTitle')"
                 :aria-label="$t('common:plugins.gfi.property.linkTitle')"
    -            :height="photoHeight < 200 ? 200 : photoHeight"
    +            :height="photoHeight"
                 width="auto"
               />
             
    @@ -34,6 +34,7 @@
     
     
    -
    +
    
    From 65e5ba9e1641fd47c6061cb0ea6635bbec133123 Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Thu, 25 Apr 2024 19:08:59 +0200
    Subject: [PATCH 054/173] Remove redundant component and add logic to the main
     component
    
    ---
     .../src/components/DefaultIndicator.vue       | 24 -------------------
     .../src/components/LoadingIndicator.vue       | 11 ++++++---
     2 files changed, 8 insertions(+), 27 deletions(-)
     delete mode 100644 packages/plugins/LoadingIndicator/src/components/DefaultIndicator.vue
    
    diff --git a/packages/plugins/LoadingIndicator/src/components/DefaultIndicator.vue b/packages/plugins/LoadingIndicator/src/components/DefaultIndicator.vue
    deleted file mode 100644
    index 94fd5a77f..000000000
    --- a/packages/plugins/LoadingIndicator/src/components/DefaultIndicator.vue
    +++ /dev/null
    @@ -1,24 +0,0 @@
    -
    -
    -
    -
    -
    diff --git a/packages/plugins/LoadingIndicator/src/components/LoadingIndicator.vue b/packages/plugins/LoadingIndicator/src/components/LoadingIndicator.vue
    index 03d9efd90..b423afbca 100644
    --- a/packages/plugins/LoadingIndicator/src/components/LoadingIndicator.vue
    +++ b/packages/plugins/LoadingIndicator/src/components/LoadingIndicator.vue
    @@ -1,15 +1,20 @@
     
     
     
    diff --git a/packages/plugins/Gfi/src/components/Gfi.vue b/packages/plugins/Gfi/src/components/Gfi.vue
    index bcf836938..de7d3975e 100644
    --- a/packages/plugins/Gfi/src/components/Gfi.vue
    +++ b/packages/plugins/Gfi/src/components/Gfi.vue
    @@ -45,7 +45,6 @@ export default Vue.extend({
             : {
                 currentProperties: this.currentProperties,
                 exportProperty: this.exportProperty,
    -            showSwitchButtons: this.showSwitchButtons,
               }
         },
         currentProperties(): GeoJsonProperties {
    @@ -88,10 +87,6 @@ export default Vue.extend({
         renderUi(): boolean {
           return this.windowFeatures.length > 0 || this.showList
         },
    -    /** only show switch buttons if multiple property sets are available */
    -    showSwitchButtons(): boolean {
    -      return this.windowFeatures.length > 1
    -    },
       },
       watch: {
         moveHandleProperties(
    diff --git a/packages/plugins/Gfi/src/store/getters.ts b/packages/plugins/Gfi/src/store/getters.ts
    index 87e95026d..efec454ab 100644
    --- a/packages/plugins/Gfi/src/store/getters.ts
    +++ b/packages/plugins/Gfi/src/store/getters.ts
    @@ -53,6 +53,10 @@ const getters: PolarGetterTree = {
           {} as Record
         )
       },
    +  /** only show switch buttons if multiple property sets are available */
    +  showSwitchButtons(_, { windowFeatures }) {
    +    return windowFeatures.length > 1
    +  },
       windowLayerKeys(_, { gfiConfiguration }): string[] {
         return Object.entries(gfiConfiguration?.layers || {}).reduce(
           (accumulator, [key, { window }]) => {
    diff --git a/packages/plugins/Gfi/src/types.ts b/packages/plugins/Gfi/src/types.ts
    index 5d3e8b9a2..c782de769 100644
    --- a/packages/plugins/Gfi/src/types.ts
    +++ b/packages/plugins/Gfi/src/types.ts
    @@ -58,6 +58,7 @@ export interface GfiGetters extends GfiState {
       renderMoveHandle: boolean
       renderType: RenderType
       showList: boolean
    +  showSwitchButtons: boolean
       /** subset of layerKeys, where features' properties are to be shown in UI */
       windowLayerKeys: string[]
       /**
    
    From aaa83884a92b92b927f9ee65d31aee43313b765d Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 26 Apr 2024 14:26:56 +0200
    Subject: [PATCH 057/173] Move logic of currentProperties to the store as a
     getter
    
    ---
     packages/clients/dish/src/plugins/Gfi/Content.vue  |  5 +----
     .../meldemichel/src/plugins/Gfi/Feature.vue        |  4 +---
     packages/plugins/Gfi/src/components/Feature.vue    |  8 +-------
     .../Gfi/src/components/FeatureTableBody.vue        |  7 +------
     packages/plugins/Gfi/src/components/Gfi.vue        | 14 +-------------
     packages/plugins/Gfi/src/store/getters.ts          | 14 ++++++++++++++
     packages/plugins/Gfi/src/types.ts                  |  1 +
     7 files changed, 20 insertions(+), 33 deletions(-)
    
    diff --git a/packages/clients/dish/src/plugins/Gfi/Content.vue b/packages/clients/dish/src/plugins/Gfi/Content.vue
    index c036a0517..a11d33a18 100644
    --- a/packages/clients/dish/src/plugins/Gfi/Content.vue
    +++ b/packages/clients/dish/src/plugins/Gfi/Content.vue
    @@ -102,10 +102,6 @@ export default Vue.extend({
       name: 'DishGfiContent',
       components: { ActionButton },
       props: {
    -    currentProperties: {
    -      type: Object as PropType,
    -      required: true,
    -    },
         exportProperty: {
           type: String,
           default: '',
    @@ -125,6 +121,7 @@ export default Vue.extend({
       computed: {
         ...mapGetters(['clientWidth', 'hasSmallWidth', 'hasWindowSize']),
         ...mapGetters('plugin/gfi', [
    +      'currentProperties',
           'imageLoaded',
           'visibleWindowFeatureIndex',
           'windowFeatures',
    diff --git a/packages/clients/meldemichel/src/plugins/Gfi/Feature.vue b/packages/clients/meldemichel/src/plugins/Gfi/Feature.vue
    index f4dcbcf21..ef5782368 100644
    --- a/packages/clients/meldemichel/src/plugins/Gfi/Feature.vue
    +++ b/packages/clients/meldemichel/src/plugins/Gfi/Feature.vue
    @@ -69,6 +69,7 @@ export default Vue.extend({
       computed: {
         ...mapGetters(['hasSmallWidth', 'hasWindowSize']),
         ...mapGetters('plugin/gfi', [
    +      'currentProperties',
           'imageLoaded',
           'visibleWindowFeatureIndex',
           'windowFeatures',
    @@ -77,9 +78,6 @@ export default Vue.extend({
         displayImage(): boolean {
           return this.currentProperties.pic
         },
    -    currentProperties(): GeoJsonProperties {
    -      return { ...this.windowFeatures[this.visibleWindowFeatureIndex] }
    -    },
       },
       watch: {
         currentProperties(newProps: object) {
    diff --git a/packages/plugins/Gfi/src/components/Feature.vue b/packages/plugins/Gfi/src/components/Feature.vue
    index 4a0a9fc4a..2de0e1f07 100644
    --- a/packages/plugins/Gfi/src/components/Feature.vue
    +++ b/packages/plugins/Gfi/src/components/Feature.vue
    @@ -17,9 +17,7 @@
         
           
         
         ,
    -      required: true,
    -    },
         exportProperty: {
           type: String,
           default: '',
    diff --git a/packages/plugins/Gfi/src/components/FeatureTableBody.vue b/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    index 958e47172..5a983787e 100644
    --- a/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    +++ b/packages/plugins/Gfi/src/components/FeatureTableBody.vue
    @@ -39,14 +39,9 @@ import isValidHttpUrl from '../utils/isValidHttpUrl'
     
     export default Vue.extend({
       name: 'GfiFeatureTableBody',
    -  props: {
    -    currentProperties: {
    -      type: Object as PropType,
    -      required: true,
    -    },
    -  },
       computed: {
         ...mapGetters(['clientWidth']),
    +    ...mapGetters('plugin/gfi', ['currentProperties']),
         /** Removes polarInternalLayerKey as it shouldn't be displayed to the user. */
         filteredProperties() {
           if (this.currentProperties) {
    diff --git a/packages/plugins/Gfi/src/components/Gfi.vue b/packages/plugins/Gfi/src/components/Gfi.vue
    index de7d3975e..12fff4459 100644
    --- a/packages/plugins/Gfi/src/components/Gfi.vue
    +++ b/packages/plugins/Gfi/src/components/Gfi.vue
    @@ -16,7 +16,6 @@ import compare from 'just-compare'
     import { t } from 'i18next'
     import Vue from 'vue'
     import { mapActions, mapGetters, mapMutations } from 'vuex'
    -import { GeoJsonProperties } from 'geojson'
     import { MoveHandleProperties } from '@polar/lib-custom-types'
     import Feature from './Feature.vue'
     import List from './List.vue'
    @@ -26,6 +25,7 @@ export default Vue.extend({
       computed: {
         ...mapGetters(['moveHandle']),
         ...mapGetters('plugin/gfi', [
    +      'currentProperties',
           'exportPropertyLayerKeys',
           'gfiContentComponent',
           'gfiConfiguration',
    @@ -43,21 +43,9 @@ export default Vue.extend({
           return this.showList
             ? {}
             : {
    -            currentProperties: this.currentProperties,
                 exportProperty: this.exportProperty,
               }
         },
    -    currentProperties(): GeoJsonProperties {
    -      const properties = {
    -        ...this.windowFeatures[this.visibleWindowFeatureIndex],
    -      }
    -      const exportProperty =
    -        this.exportPropertyLayerKeys[properties.polarInternalLayerKey]
    -      if (exportProperty?.length > 0) {
    -        delete properties[exportProperty]
    -      }
    -      return properties
    -    },
         exportProperty(): string {
           if (this.currentProperties) {
             const property =
    diff --git a/packages/plugins/Gfi/src/store/getters.ts b/packages/plugins/Gfi/src/store/getters.ts
    index efec454ab..cf8245f56 100644
    --- a/packages/plugins/Gfi/src/store/getters.ts
    +++ b/packages/plugins/Gfi/src/store/getters.ts
    @@ -41,6 +41,20 @@ const getters: PolarGetterTree = {
           ? gfiConfiguration.afterLoadFunction
           : null
       },
    +  currentProperties(
    +    _,
    +    { exportPropertyLayerKeys, visibleWindowFeatureIndex, windowFeatures }
    +  ) {
    +    const properties = {
    +      ...windowFeatures[visibleWindowFeatureIndex],
    +    }
    +    const exportProperty =
    +      exportPropertyLayerKeys[properties.polarInternalLayerKey]
    +    if (exportProperty?.length > 0) {
    +      delete properties[exportProperty]
    +    }
    +    return properties
    +  },
       layerKeys(_, { gfiConfiguration }) {
         return Object.keys(gfiConfiguration?.layers || {})
       },
    diff --git a/packages/plugins/Gfi/src/types.ts b/packages/plugins/Gfi/src/types.ts
    index c782de769..f3c83177c 100644
    --- a/packages/plugins/Gfi/src/types.ts
    +++ b/packages/plugins/Gfi/src/types.ts
    @@ -43,6 +43,7 @@ export interface GfiState {
     
     export interface GfiGetters extends GfiState {
       afterLoadFunction: GfiAfterLoadFunction | null
    +  currentProperties: GeoJsonProperties
       exportPropertyLayerKeys: Record
       /** subset of layerKeys, where features' geometries are to be shown on map */
       geometryLayerKeys: string[]
    
    From 04f9a07cf7b6356b8dd3fe3dda0ab25df3e5c8da Mon Sep 17 00:00:00 2001
    From: Pascal Roehling 
    Date: Fri, 26 Apr 2024 14:30:53 +0200
    Subject: [PATCH 058/173] Remove unused imports
    
    ---
     packages/plugins/Gfi/src/components/Feature.vue | 3 +--
     1 file changed, 1 insertion(+), 2 deletions(-)
    
    diff --git a/packages/plugins/Gfi/src/components/Feature.vue b/packages/plugins/Gfi/src/components/Feature.vue
    index 2de0e1f07..73037df00 100644
    --- a/packages/plugins/Gfi/src/components/Feature.vue
    +++ b/packages/plugins/Gfi/src/components/Feature.vue
    @@ -28,8 +28,7 @@
     
     
     
    diff --git a/packages/clients/dish/src/plugins/Gfi/Content.vue b/packages/clients/dish/src/plugins/Gfi/Content.vue
    index a11d33a18..3b5fc3a64 100644
    --- a/packages/clients/dish/src/plugins/Gfi/Content.vue
    +++ b/packages/clients/dish/src/plugins/Gfi/Content.vue
    @@ -1,7 +1,7 @@
     
     
     
     
    @@ -121,15 +42,6 @@ export default Vue.extend({
       pointer-events: all;
       min-width: 400px;
       white-space: normal;
    -
    -  .text-locator-result-badge {
    -    cursor: pointer;
    -    max-width: 500px;
    -    width: 500px;
    -    white-space: normal;
    -    justify-content: left;
    -    margin: 0.5em 0;
    -  }
     }
     
     
    diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/Tree.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/Tree.vue
    new file mode 100644
    index 000000000..521d6918b
    --- /dev/null
    +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/Tree.vue
    @@ -0,0 +1,99 @@
    +
    +
    +
    +
    +
    
    From 0918a8270e05cd9fca2c579b8d74afec72f130c2 Mon Sep 17 00:00:00 2001
    From: Dennis Sen 
    Date: Thu, 16 May 2024 06:08:28 +0200
    Subject: [PATCH 132/173] fix keyboard interaction (tabbing fired buttons)
    
    ---
     .../src/plugins/GeometrySearch/components/Action.vue            | 2 +-
     1 file changed, 1 insertion(+), 1 deletion(-)
    
    diff --git a/packages/clients/textLocator/src/plugins/GeometrySearch/components/Action.vue b/packages/clients/textLocator/src/plugins/GeometrySearch/components/Action.vue
    index f9c4acafd..9405c048f 100644
    --- a/packages/clients/textLocator/src/plugins/GeometrySearch/components/Action.vue
    +++ b/packages/clients/textLocator/src/plugins/GeometrySearch/components/Action.vue
    @@ -11,7 +11,7 @@
             v-bind="attrs"
             v-on="on"
             @click="action(item)"
    -        @keydown="action(item)"
    +        @keydown.enter="action(item)"
           >
             {{ icon }}
           
    
    From 6d05108e2c0929ceef6fc8b8d61cf49b4c19cb6c Mon Sep 17 00:00:00 2001
    From: Dennis Sen 
    Date: Thu, 16 May 2024 07:13:06 +0200
    Subject: [PATCH 133/173] add keyboard user support for tooltips
    
    ---
     .../clients/textLocator/src/components/ResultInfo.vue     | 8 +++++++-
     .../src/plugins/GeometrySearch/components/Tree.vue        | 8 ++++++--
     2 files changed, 13 insertions(+), 3 deletions(-)
    
    diff --git a/packages/clients/textLocator/src/components/ResultInfo.vue b/packages/clients/textLocator/src/components/ResultInfo.vue
    index 5c451f3ca..940d4e053 100644
    --- a/packages/clients/textLocator/src/components/ResultInfo.vue
    +++ b/packages/clients/textLocator/src/components/ResultInfo.vue
    @@ -6,7 +6,9 @@
         transition="none"
       >