serverside geocoding

This commit is contained in:
Anton Tranelis 2025-10-16 16:06:55 +02:00
parent 719e1e16b5
commit ca4d481acf
8 changed files with 333 additions and 33 deletions

View File

@ -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"
}
]

View File

@ -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",

View File

@ -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
}
}

View File

@ -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 (
<div className='tw:text-sm tw:flex tw:items-center tw:text-gray-500 tw:w-full'>
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
<span title={address} className={truncate ? 'tw:truncate' : ''}>
{address}
{showDistance && distance && distance >= 0.1
? ` (${formatDistance(distance) ?? ''})`
: ''}
</span>
</div>
)
}
if (mode === 'custom' && subtitle) {
return (
<div className={`tw:text-sm tw:opacity-50 tw:items-center ${truncate ? 'tw:truncate' : ''}`}>
{subtitle}
</div>
)
}
return null
}

View File

@ -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<HTMLDivElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
const [fontSize, setFontSize] = useState<string>('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}
</div>
{subtitleMode === 'address' && address && (
<div className='tw:text-sm tw:flex tw:items-center tw:text-gray-500 tw:w-full'>
<MapPinIcon className='tw:w-4 tw:mr-1 tw:flex-shrink-0' />
<span title={address} className='tw:truncate'>
{address}
{distance && distance >= 0.1 ? ` (${formatDistance(distance) ?? ''})` : ''}
</span>
</div>
)}
{subtitleMode === 'custom' && subtitle && (
<div
className={`tw:text-sm tw:opacity-50 tw:items-center ${truncateSubname ? 'tw:truncate' : ''}`}
>
{subtitle}
</div>
)}
<ItemSubtitle item={item} mode={subtitleMode} truncate={truncateSubname} />
</div>
)
}

View File

@ -12,6 +12,8 @@ import { QRModal } from './QRModal'
import type { HeaderViewProps } from './types'
export { ItemSubtitle } from './ItemSubtitle'
export function HeaderView({
item,
api,

View File

@ -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'}
/>
<div
className={`tw:grow tw:mr-4 ${item.layer?.itemType.subtitle_mode === 'custom' ? 'tw:pt-1' : 'tw:flex tw:items-center'}`}
>
<div className='tw:grow tw:mr-4 tw:pt-1'>
<TextInput
placeholder='Name'
defaultValue={item.name ? item.name : ''}
@ -68,6 +67,11 @@ export const FormHeader = ({ item, state, setState }: Props) => {
inputStyle='tw:input-sm'
/>
)}
{item.layer?.itemType.subtitle_mode === 'address' && (
<div className='tw:px-4 tw:mt-2'>
<ItemSubtitle item={item} />
</div>
)}
</div>
</div>
</div>

View File

@ -62,6 +62,7 @@ export interface Item {
openCollectiveSlug?: string
secrets?: ItemSecret[]
extended?: JSON
address?: string
// {
// coordinates: [number, number]