From ca4d481acf88aba9249480c0ab5c6f103df1be17 Mon Sep 17 00:00:00 2001 From: Anton Tranelis Date: Thu, 16 Oct 2025 16:06:55 +0200 Subject: [PATCH] serverside geocoding --- .../development/collections/flows.json | 56 ++++++ .../development/collections/operations.json | 161 ++++++++++++++++++ .../snapshot/fields/items/address.json | 43 +++++ .../HeaderView/ItemSubtitle.tsx | 61 +++++++ .../HeaderView/ItemTitle.tsx | 32 +--- .../ItemPopupComponents/HeaderView/index.tsx | 2 + .../Profile/Subcomponents/FormHeader.tsx | 10 +- lib/src/types/Item.d.ts | 1 + 8 files changed, 333 insertions(+), 33 deletions(-) create mode 100644 backend/directus-config/development/snapshot/fields/items/address.json create mode 100644 lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemSubtitle.tsx diff --git a/backend/directus-config/development/collections/flows.json b/backend/directus-config/development/collections/flows.json index d72a202a..b1b46365 100644 --- a/backend/directus-config/development/collections/flows.json +++ b/backend/directus-config/development/collections/flows.json @@ -269,6 +269,48 @@ "operation": "43de95f1-d63b-4231-80c3-b399c45470f6", "_syncId": "d7e74f35-a19a-4a0b-9ae8-59af2fa0f081" }, + { + "name": "Reverse Geocoder: Trigger Populare Items", + "icon": "home_pin", + "color": null, + "description": "Set multiple items addresses at once", + "status": "active", + "trigger": "manual", + "accountability": "all", + "options": { + "collections": [ + "items" + ], + "requireConfirmation": true, + "async": true, + "location": "collection", + "confirmationDescription": "Be aware that you can run into request limits on the external reverse geocoder" + }, + "operation": "c7317e38-c026-4dc0-8b73-42340e205c33", + "_syncId": "d8b14c06-fb08-4a27-8a4b-75e6a4cd7216" + }, + { + "name": "Reverse Geocoder: On Create and Update", + "icon": "home_pin", + "color": null, + "description": "Requests the items address on update / create", + "status": "active", + "trigger": "event", + "accountability": "all", + "options": { + "type": "filter", + "scope": [ + "items.update", + "items.create" + ], + "collections": [ + "items" + ], + "return": "$last" + }, + "operation": "0d18abcf-70f9-4545-b583-e18e5ec533fa", + "_syncId": "e28759ce-3995-4943-bbfc-f1ed08d42497" + }, { "name": "Slug Generation", "icon": "bolt", @@ -288,5 +330,19 @@ }, "operation": "55857562-e0ab-49a5-a292-18f6c1cb075e", "_syncId": "f2beb617-9c21-48b2-a8ec-c04197d1b7d1" + }, + { + "name": "Reverse Geocoder: Single Item Address", + "icon": "home_pin", + "color": null, + "description": "Single item address request", + "status": "active", + "trigger": "operation", + "accountability": "all", + "options": { + "return": "$last" + }, + "operation": "fa077e33-be44-48d4-b3a4-9f61c99c002c", + "_syncId": "fbf2ab06-eab8-4dc4-8e25-101f6a4edba6" } ] diff --git a/backend/directus-config/development/collections/operations.json b/backend/directus-config/development/collections/operations.json index 8eb4b1bd..a96afe53 100644 --- a/backend/directus-config/development/collections/operations.json +++ b/backend/directus-config/development/collections/operations.json @@ -227,6 +227,48 @@ "flow": "cc80ec73-ecf5-4789-bee5-1127fb1a6ed4", "_syncId": "06716525-6d29-46e2-bb01-0764bccd74e9" }, + { + "name": "Run Script", + "key": "exec_bouoe", + "type": "exec", + "position_x": 37, + "position_y": 1, + "options": { + "code": "module.exports = async function(data) {\n\treturn data['$last'].map((item) => {\n \treturn {\n \"position\" : item.position,\n \"id\" : item.id,\n };\n });\n}" + }, + "resolve": "fdca64f4-41e2-4940-a813-0f7698f1580b", + "reject": null, + "flow": "d8b14c06-fb08-4a27-8a4b-75e6a4cd7216", + "_syncId": "d3c50715-47c3-4763-832f-caf92d29331f" + }, + { + "name": "Run Script", + "key": "exec_dsl0t", + "type": "exec", + "position_x": 56, + "position_y": 1, + "options": { + "code": "// Your function in the myScript operation\nmodule.exports = function (data) {\n const payload = {...data.$trigger.payload}\n payload.address = data.get_address_string.data.features[0].properties.city;\n return payload;\n};" + }, + "resolve": null, + "reject": null, + "flow": "e28759ce-3995-4943-bbfc-f1ed08d42497", + "_syncId": "d18a1aaf-77a6-453e-8142-5ca4f4181b05" + }, + { + "name": "Run Script", + "key": "exec_kuwlb", + "type": "exec", + "position_x": 37, + "position_y": 1, + "options": { + "code": "module.exports = function(data) {\nif (data.$last.data.features[0].properties.type === \"city\") {\n return {\n \"address\" : data.$last.data.features[0].properties.name || null\n }\n} else {\n return {\n \"address\": data.$last.data.features[0].properties.city ?? data.$last.data.features[0].properties.town ?? data.$last.data.features[0].properties.village\n }\n}\n}" + }, + "resolve": "daa7b615-11f3-4cd9-8713-494fded63757", + "reject": null, + "flow": "fbf2ab06-eab8-4dc4-8e25-101f6a4edba6", + "_syncId": "dcf26e93-3b5c-44c3-b2fb-00c45d9a1fd6" + }, { "name": "Run Script", "key": "exec_p2t3z", @@ -255,6 +297,36 @@ "flow": "7b978be2-605f-4061-b5b3-46f151b1b80a", "_syncId": "67847550-3c95-4ee4-af02-32ebb69747d6" }, + { + "name": "Get Address String", + "key": "get_address_string", + "type": "request", + "position_x": 19, + "position_y": 1, + "options": { + "method": "GET", + "url": "https://photon.komoot.io/reverse?lat={{$trigger.payload.position.coordinates[1]}}&lon={{$trigger.payload.position.coordinates[0]}}&lang=de&limit=1" + }, + "resolve": "7e6101d2-3d8c-49e2-894e-3121c9eff06b", + "reject": null, + "flow": "e28759ce-3995-4943-bbfc-f1ed08d42497", + "_syncId": "0d18abcf-70f9-4545-b583-e18e5ec533fa" + }, + { + "name": "Get Address String", + "key": "get_address_string", + "type": "request", + "position_x": 19, + "position_y": 1, + "options": { + "method": "GET", + "url": "https://photon.komoot.io/reverse?lat={{$trigger.position.coordinates[1]}}&lon={{$trigger.position.coordinates[0]}}&lang=de&limit=1" + }, + "resolve": "dcf26e93-3b5c-44c3-b2fb-00c45d9a1fd6", + "reject": null, + "flow": "fbf2ab06-eab8-4dc4-8e25-101f6a4edba6", + "_syncId": "fa077e33-be44-48d4-b3a4-9f61c99c002c" + }, { "name": "get Creator", "key": "get_creator", @@ -733,6 +805,35 @@ "flow": "a78d01a4-13b3-46a4-8938-9606bf26e329", "_syncId": "9838d2ca-3698-4d29-8429-038dfcaf7fab" }, + { + "name": "Read Data", + "key": "item_read_ceep3", + "type": "item-read", + "position_x": 19, + "position_y": 1, + "options": { + "permissions": "$trigger", + "emitEvents": false, + "collection": "items", + "query": { + "limit": -1, + "fields": [ + "id", + "position" + ], + "filter": { + "id": { + "_in": "{{$trigger.body.keys}}" + } + } + }, + "key": [] + }, + "resolve": "d3c50715-47c3-4763-832f-caf92d29331f", + "reject": null, + "flow": "d8b14c06-fb08-4a27-8a4b-75e6a4cd7216", + "_syncId": "c7317e38-c026-4dc0-8b73-42340e205c33" + }, { "name": "Read Data", "key": "item_read_evgvk", @@ -1054,6 +1155,28 @@ "flow": "7b978be2-605f-4061-b5b3-46f151b1b80a", "_syncId": "017875a5-3736-478a-9bcc-ed473117c74d" }, + { + "name": "Update Data", + "key": "item_update_96g9y", + "type": "item-update", + "position_x": 56, + "position_y": 1, + "options": { + "permissions": "$trigger", + "emitEvents": false, + "collection": "items", + "payload": { + "address": "{{$last.address}}" + }, + "key": [ + "{{$trigger.id}}" + ] + }, + "resolve": null, + "reject": null, + "flow": "fbf2ab06-eab8-4dc4-8e25-101f6a4edba6", + "_syncId": "daa7b615-11f3-4cd9-8713-494fded63757" + }, { "name": "Update Data", "key": "item_update_chszs", @@ -1125,6 +1248,28 @@ "flow": "cb772a2c-150c-4cca-bc2c-1f8498a5cd92", "_syncId": "b22755ba-4ec5-4e04-a3fe-a390a9bc75ab" }, + { + "name": "Update Data", + "key": "item_update_h2lfe", + "type": "item-update", + "position_x": 38, + "position_y": 1, + "options": { + "permissions": "$trigger", + "emitEvents": false, + "collection": "items", + "key": [ + "{{$trigger.payload.id}}" + ], + "payload": { + "address": "{{$last.data.features[0].properties.city}}" + } + }, + "resolve": "d18a1aaf-77a6-453e-8142-5ca4f4181b05", + "reject": null, + "flow": "e28759ce-3995-4943-bbfc-f1ed08d42497", + "_syncId": "7e6101d2-3d8c-49e2-894e-3121c9eff06b" + }, { "name": "Update Nomads Home", "key": "item_update_o6cn8", @@ -1599,6 +1744,22 @@ "flow": "9a1d1084-438f-471e-aac5-47e0749375e7", "_syncId": "95d762f9-4695-4168-aa65-5bd065b40742" }, + { + "name": "Trigger Flow", + "key": "trigger_uromw", + "type": "trigger", + "position_x": 55, + "position_y": 1, + "options": { + "iterationMode": "parallel", + "flow": "fbf2ab06-eab8-4dc4-8e25-101f6a4edba6", + "payload": "{{ $last }}" + }, + "resolve": null, + "reject": null, + "flow": "d8b14c06-fb08-4a27-8a4b-75e6a4cd7216", + "_syncId": "fdca64f4-41e2-4940-a813-0f7698f1580b" + }, { "name": "Updated?", "key": "updated", diff --git a/backend/directus-config/development/snapshot/fields/items/address.json b/backend/directus-config/development/snapshot/fields/items/address.json new file mode 100644 index 00000000..b4cac98b --- /dev/null +++ b/backend/directus-config/development/snapshot/fields/items/address.json @@ -0,0 +1,43 @@ +{ + "collection": "items", + "field": "address", + "type": "string", + "meta": { + "collection": "items", + "conditions": null, + "display": null, + "display_options": null, + "field": "address", + "group": null, + "hidden": false, + "interface": "input", + "note": null, + "options": null, + "readonly": false, + "required": false, + "sort": 34, + "special": null, + "translations": null, + "validation": null, + "validation_message": null, + "width": "full" + }, + "schema": { + "name": "address", + "table": "items", + "data_type": "character varying", + "default_value": null, + "max_length": 255, + "numeric_precision": null, + "numeric_scale": null, + "is_nullable": true, + "is_unique": false, + "is_indexed": false, + "is_primary_key": false, + "is_generated": false, + "generation_expression": null, + "has_auto_increment": false, + "foreign_key_table": null, + "foreign_key_column": null + } +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemSubtitle.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemSubtitle.tsx new file mode 100644 index 00000000..074099f0 --- /dev/null +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemSubtitle.tsx @@ -0,0 +1,61 @@ +import { MapPinIcon } from '@heroicons/react/24/solid' + +import { useGeoDistance } from '#components/Map/hooks/useGeoDistance' +import { useReverseGeocode } from '#components/Map/hooks/useReverseGeocode' + +import { useFormatDistance } from './hooks' + +import type { Item } from '#types/Item' + +interface ItemSubtitleProps { + item: Item + mode?: 'address' | 'custom' | 'none' + truncate?: boolean + showDistance?: boolean +} + +export function ItemSubtitle({ + item, + mode = 'address', + truncate = true, + showDistance = true, +}: ItemSubtitleProps) { + const { distance } = useGeoDistance(item.position ?? undefined) + const { formatDistance } = useFormatDistance() + + // Use item.address from backend if available, otherwise use reverse geocoding + const shouldReverseGeocode = mode === 'address' && (!item.address || item.address.trim() === '') + + const { address: geocodedAddress } = useReverseGeocode( + item.position?.coordinates as [number, number] | undefined, + shouldReverseGeocode, + 'municipality', + ) + + const address = item.address && item.address.trim() !== '' ? item.address : geocodedAddress + const subtitle = item.subname + + if (mode === 'address' && address) { + return ( +
+ + + {address} + {showDistance && distance && distance >= 0.1 + ? ` (${formatDistance(distance) ?? ''})` + : ''} + +
+ ) + } + + if (mode === 'custom' && subtitle) { + return ( +
+ {subtitle} +
+ ) + } + + return null +} diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx index 713f3373..a371e1f3 100644 --- a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemTitle.tsx @@ -1,10 +1,6 @@ -import { MapPinIcon } from '@heroicons/react/24/solid' import { useEffect, useRef, useState } from 'react' -import { useGeoDistance } from '#components/Map/hooks/useGeoDistance' -import { useReverseGeocode } from '#components/Map/hooks/useReverseGeocode' - -import { useFormatDistance } from './hooks' +import { ItemSubtitle } from './ItemSubtitle' import type { Item } from '#types/Item' @@ -23,20 +19,11 @@ export function ItemTitle({ subtitleMode = 'address', hasAvatar = false, }: ItemTitleProps) { - const { distance } = useGeoDistance(item.position ?? undefined) - const { formatDistance } = useFormatDistance() const titleRef = useRef(null) const containerRef = useRef(null) const [fontSize, setFontSize] = useState('tw:text-xl') - const { address } = useReverseGeocode( - item.position?.coordinates as [number, number] | undefined, - subtitleMode === 'address', - 'municipality', - ) - const title = item.name ?? item.layer?.item_default_name - const subtitle = item.subname useEffect(() => { if (!containerRef.current || !title) { @@ -111,22 +98,7 @@ export function ItemTitle({ > {title} - {subtitleMode === 'address' && address && ( -
- - - {address} - {distance && distance >= 0.1 ? ` (${formatDistance(distance) ?? ''})` : ''} - -
- )} - {subtitleMode === 'custom' && subtitle && ( -
- {subtitle} -
- )} + ) } diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx index 1befab1c..0ecef896 100644 --- a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView/index.tsx @@ -12,6 +12,8 @@ import { QRModal } from './QRModal' import type { HeaderViewProps } from './types' +export { ItemSubtitle } from './ItemSubtitle' + export function HeaderView({ item, api, diff --git a/lib/src/Components/Profile/Subcomponents/FormHeader.tsx b/lib/src/Components/Profile/Subcomponents/FormHeader.tsx index 29c53aa9..630c7754 100644 --- a/lib/src/Components/Profile/Subcomponents/FormHeader.tsx +++ b/lib/src/Components/Profile/Subcomponents/FormHeader.tsx @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { TextInput } from '#components/Input' +import { ItemSubtitle } from '#components/Map/Subcomponents/ItemPopupComponents/HeaderView/ItemSubtitle' import { AvatarWidget } from './AvatarWidget' import { ColorPicker } from './ColorPicker' @@ -38,9 +39,7 @@ export const FormHeader = ({ item, state, setState }: Props) => { } className={'tw:-left-6 tw:top-14 tw:-mr-6'} /> -
+
{ inputStyle='tw:input-sm' /> )} + {item.layer?.itemType.subtitle_mode === 'address' && ( +
+ +
+ )}
diff --git a/lib/src/types/Item.d.ts b/lib/src/types/Item.d.ts index 967277b7..fa6dcfed 100644 --- a/lib/src/types/Item.d.ts +++ b/lib/src/types/Item.d.ts @@ -62,6 +62,7 @@ export interface Item { openCollectiveSlug?: string secrets?: ItemSecret[] extended?: JSON + address?: string // { // coordinates: [number, number]