diff --git a/frontend/search/components/config/defaultColumnGroups.ts b/frontend/search/components/config/defaultColumnGroups.ts index f7b001b..c4b563c 100644 --- a/frontend/search/components/config/defaultColumnGroups.ts +++ b/frontend/search/components/config/defaultColumnGroups.ts @@ -2,6 +2,7 @@ const groups = [ { name: 'contactStatus', header: 'Contact Status' }, { name: 'offerStatus', header: 'Offer Status' }, { name: 'locationCoarse', header: 'Location' }, + { name: 'distance', group: 'locationCoarse', header: 'Distance' }, { name: 'time', header: 'Time' }, { name: 'features', header: 'Limitations / Features' }, { name: 'animals', group: 'features', header: 'Animals' }, diff --git a/frontend/search/components/config/defaultColumnRawDefinition.ts b/frontend/search/components/config/defaultColumnRawDefinition.ts index 02ecdbe..28dee3d 100644 --- a/frontend/search/components/config/defaultColumnRawDefinition.ts +++ b/frontend/search/components/config/defaultColumnRawDefinition.ts @@ -32,6 +32,13 @@ const columnsRaw: ColumnRaw[] = [ "editable": true, "defaultWidth": 400 }, + { + "name": "place_distance", + "group": "distance", + "header": "km", + "type": "number", + "defaultWidth": 105 + }, { "name": "place_country", "group": "locationCoarse", diff --git a/frontend/search/components/ngo/HostOfferLookupWrapper.tsx b/frontend/search/components/ngo/HostOfferLookupWrapper.tsx index ade3e8a..831b5cb 100644 --- a/frontend/search/components/ngo/HostOfferLookupWrapper.tsx +++ b/frontend/search/components/ngo/HostOfferLookupWrapper.tsx @@ -6,9 +6,14 @@ import { useTranslation } from 'react-i18next' import { Login, useAuthStore } from '../Login' import { useLeafletStore } from './LeafletStore' import { filterUndefOrNull } from '../util/notEmpty' +import { haversine_distance } from '../util/distance' type HostOfferLookupWrapperProps = Partial +function floor(v: number|undefined) { + return v && Math.floor(v) +} + const HostOfferLookupWrapper = (props: HostOfferLookupWrapperProps) => { const { t } = useTranslation() const auth = useAuthStore() @@ -20,17 +25,20 @@ const HostOfferLookupWrapper = (props: HostOfferLookupWrapperProps) => { const {data: data_ro} = queryResult_ro const {data: data_rw} = queryResult_rw - const leafletStore = useLeafletStore() + const {setMarkers, center} = useLeafletStore() useEffect(() => { const markers = data_ro?.get_offers?.map(row => (row.id && row.place_lon && row.place_lat - && ({id: row.id, + && ({id: row.id, lat: row.place_lat, lng: row.place_lon, radius: 1000, // TODO content: 'TODO'}) || undefined)) - leafletStore.setMarkers(filterUndefOrNull(markers)) + setMarkers(filterUndefOrNull(markers)) }, [data_ro]) + const data_ro_withDistance = data_ro?.get_offers?.map(r => ({...r, + place_distance: floor(haversine_distance(center?.lat, center?.lng, r.place_lat, r.place_lon))})) + return <> { style={{flex: '1 1', height: '100%'}}> diff --git a/frontend/search/components/ngo/LeafletMap.tsx b/frontend/search/components/ngo/LeafletMap.tsx index 8ee9e93..3ac6608 100644 --- a/frontend/search/components/ngo/LeafletMap.tsx +++ b/frontend/search/components/ngo/LeafletMap.tsx @@ -20,10 +20,15 @@ import { useLeafletStore } from './LeafletStore' type LeafletMapProps = {onBoundsChange?: (bounds: L.LatLngBounds) => void} const BoundsChangeListener = ({onBoundsChange}: {onBoundsChange?: (bounds: L.LatLngBounds) => void}) => { + const leafletStore = useLeafletStore() + const map = useMap() const updateBounds = useCallback( () => { + leafletStore.setCenter(map.getCenter()) + + // onBoundsChange is unused at the moment onBoundsChange && onBoundsChange( map.getBounds() ) diff --git a/frontend/search/components/ngo/LeafletStore.tsx b/frontend/search/components/ngo/LeafletStore.tsx index 46a2b26..ac53dc5 100644 --- a/frontend/search/components/ngo/LeafletStore.tsx +++ b/frontend/search/components/ngo/LeafletStore.tsx @@ -1,19 +1,24 @@ import create from 'zustand' +import * as L from 'leaflet' -export interface Marker - {id: string, +export interface Marker { + id: string, lat: number, lng: number, radius: number, // in meters content: string // TODO react-child? - } +} export interface LeafletState { - markers: Marker[] - setMarkers: (markers: Marker[]) => void + center: L.LatLng|null // at the moment not used for setting the maps center, but only for distance calculation onBoundsChange + setCenter: (center: L.LatLng) => void + markers: Marker[] + setMarkers: (markers: Marker[]) => void } export const useLeafletStore = create(set => ({ + center: null, + setCenter: center => set( _orig => ({center}) ), markers: [], - setMarkers: (markers: Marker[]) => set( _orig => ({markers}) ) + setMarkers: markers => set( _orig => ({markers}) ) })) diff --git a/frontend/search/components/util/distance.ts b/frontend/search/components/util/distance.ts new file mode 100644 index 0000000..64d8958 --- /dev/null +++ b/frontend/search/components/util/distance.ts @@ -0,0 +1,14 @@ +export type lon=number|null|undefined +export type lat=number|null|undefined + +/** optimization by https://stackoverflow.com/questions/5260423/torad-javascript-function-throwing-error/21623256#21623256 **/ +export function haversine_distance(lat1:lat, lon1: lon, lat2:lat, lon2:lon) { + if(lat1 && lon1 && lat2 && lon2) { + var R = 6371 // Radius of the earth in km + var dLat = (lat2 - lat1) * Math.PI / 180 // deg2rad below + var dLon = (lon2 - lon1) * Math.PI / 180 + var a = 0.5 - Math.cos(dLat)/2 + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * (1 - Math.cos(dLon))/2 + return R * 2 * Math.asin(Math.sqrt(a)) + } +}