diff --git a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx index 1bb98f89..d14d22bd 100644 --- a/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx +++ b/lib/src/Components/Map/Subcomponents/ItemPopupComponents/HeaderView.tsx @@ -10,7 +10,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ import EllipsisVerticalIcon from '@heroicons/react/16/solid/EllipsisVerticalIcon' -import { MapPinIcon, ShareIcon, UserPlusIcon } from '@heroicons/react/24/outline' +import { MapPinIcon, ShareIcon } from '@heroicons/react/24/outline' import PencilIcon from '@heroicons/react/24/solid/PencilIcon' import TrashIcon from '@heroicons/react/24/solid/TrashIcon' import { useState } from 'react' @@ -20,7 +20,9 @@ import { useNavigate } from 'react-router-dom' import TargetDotSVG from '#assets/targetDot.svg' import { useAppState } from '#components/AppShell/hooks/useAppState' +import { useGeoDistance } from '#components/Map/hooks/useGeoDistance' import { useHasUserPermission } from '#components/Map/hooks/usePermissions' +import { useGetItemTags } from '#components/Map/hooks/useTags' import DialogModal from '#components/Templates/DialogModal' import type { Item } from '#types/Item' @@ -56,8 +58,9 @@ export function HeaderView({ const hasUserPermission = useHasUserPermission() const navigate = useNavigate() const appState = useAppState() - + const getItemTags = useGetItemTags() const [imageLoaded, setImageLoaded] = useState(false) + const { distance } = useGeoDistance(item?.position ?? undefined) const avatar = (item?.image && appState.assetsApi.url + item.image + '?width=160&heigth=160') || @@ -69,6 +72,11 @@ export function HeaderView({ const params = new URLSearchParams(window.location.search) + const formatDistance = (dist: number | null): string | null => { + if (!dist) return null + return dist < 10 ? `${dist.toFixed(1)} km` : `${Math.round(dist)} km` + } + const openDeleteModal = async (event: React.MouseEvent) => { setModalOpen(true) event.stopPropagation() @@ -196,13 +204,28 @@ export function HeaderView({ {big && (
-
+
+ {' '} + {formatDistance(distance) && ( + + {formatDistance(distance)} + + )} +
-
- +
+
diff --git a/lib/src/Components/Map/hooks/useGeoDistance.tsx b/lib/src/Components/Map/hooks/useGeoDistance.tsx new file mode 100644 index 00000000..4004f129 --- /dev/null +++ b/lib/src/Components/Map/hooks/useGeoDistance.tsx @@ -0,0 +1,53 @@ +import { useState, useEffect } from 'react' + +import { useMyProfile } from './useMyProfile' + +import type { Point } from 'geojson' + +const getDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => { + const R = 6371 // Earth's radius in km + const dLat = ((lat2 - lat1) * Math.PI) / 180 + const dLon = ((lon2 - lon1) * Math.PI) / 180 + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos((lat1 * Math.PI) / 180) * + Math.cos((lat2 * Math.PI) / 180) * + Math.sin(dLon / 2) * + Math.sin(dLon / 2) + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)) + return R * c // Distance in km +} + +export const useGeoDistance = (targetPoint?: Point) => { + const [distance, setDistance] = useState(null) + const [error, setError] = useState(null) + const { myProfile, isMyProfileLoaded } = useMyProfile() + + useEffect(() => { + setError(null) + setDistance(null) + + if (!isMyProfileLoaded) { + return + } + + if (!myProfile?.position || !targetPoint) { + setError('Missing location data') + return + } + + try { + const userGeoJson = myProfile.position + const [userLon, userLat] = userGeoJson.coordinates + const [targetLon, targetLat] = targetPoint.coordinates + + const dist = getDistance(userLat, userLon, targetLat, targetLon) + setDistance(dist) + } catch (err) { + setError('Calculation error') + throw err + } + }, [myProfile, isMyProfileLoaded, targetPoint]) + + return { distance, error, userLocation: myProfile?.position as Point | undefined } +}