Bündel 2: Zentrale Helper für DRY-Cleanup, SW by-v1114

NEUE BACKEND-MODULE:

math_utils.py
- haversine_km(lat1, lon1, lat2, lon2) — Distanz in km
- haversine_m(...) — Convenience-Wrapper in Metern
- bbox_deg_from_km(lat, radius_km) — Bounding-Box-Approximation
  für SQL-Vorfilter (statt Haversine im Python-Loop)

config.py
- DB_PATH, MEDIA_DIR, BREEDER_DOCS_DIR, SCANINPUT_DIR
- API_TIMEOUT_SHORT (5s) / DEFAULT (10s) / LONG (30s)
- HTTP_USER_AGENT, HTTP_HEADERS

errors.py
- not_found(msg), forbidden(msg), bad_request(msg), unauthorized(msg)
- conflict(msg), too_many_requests(msg, retry_after), service_unavailable(msg)
- require_or_404(row, msg) — Convenience-Helper

UI.JS ERWEITERUNGEN:

UI.time erweitert:
- formatDate(d)     → "15.03.2026"
- formatDateTime(d) → "15.03.2026, 14:30"
- weekday(d)        → "Di"
- parseISO(str)     → {year, month, day}

UI.text (neu):
- truncate(str, maxLen, ellipsis='…')
- slug(str) — URL-Slug aus String (mit DE-Umlauten)

UI.money (neu):
- format(value) → "12,34 €" (de-DE, EUR)
- formatWithSuffix(value, '/Jahr')

HAVERSINE-MIGRATION (13 Backend-Routen):
alerts.py, services.py, places.py, events.py, diary.py, playdate.py,
lost.py, poison.py, adoption.py, gassi_zeiten.py, sitting.py, routen.py,
walks.py

- Alle lokalen def _haversine/haversine_km entfernt
- Aufrufe ersetzt durch haversine_km/haversine_m je nach Einheit
- from math_utils import haversine_km|haversine_m in jeder Datei

Tests 19/19 grün.

Hinweis: Migrationen für MEDIA_DIR (19 Stellen), API-Timeouts (12),
Date-Formatter im Frontend (24) und UI.text.truncate (5) sind als
Folge-Sprints möglich. Helper sind verfügbar.
This commit is contained in:
rene 2026-05-27 11:19:06 +02:00
parent c517c9281d
commit 297bd22f96
22 changed files with 225 additions and 202 deletions

View file

@ -10,7 +10,6 @@ Caching: adoption_cache Tabelle, 24h TTL.
"""
import os
import math
import logging
import asyncio
import uuid
@ -22,6 +21,7 @@ from typing import Optional
from database import db
from auth import get_current_user
from routes.push import send_push_to_user
from math_utils import haversine_km
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
@ -31,18 +31,6 @@ router = APIRouter()
PETFINDER_KEY = os.getenv("PETFINDER_API_KEY", "")
PETFINDER_SECRET = os.getenv("PETFINDER_API_SECRET", "")
# ------------------------------------------------------------------
# Haversine — Distanz in km
# ------------------------------------------------------------------
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6371.0
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Statische Tierheim-Daten (große deutsche Tierheime)
@ -234,7 +222,7 @@ async def adoption_nearby(
for row in rows:
d = dict(row)
if d.get("tierheim_lat") and d.get("tierheim_lon"):
dist = _haversine(lat, lon, d["tierheim_lat"], d["tierheim_lon"])
dist = haversine_km(lat, lon, d["tierheim_lat"], d["tierheim_lon"])
if dist <= radius:
d["distanz_km"] = round(dist, 1)
cached_animals.append(d)
@ -250,7 +238,7 @@ async def adoption_nearby(
# ------ Statische Tierheime (immer) ------
shelters = []
for sid, name, plz, stadt, slat, slon, url in GERMAN_SHELTERS:
dist = _haversine(lat, lon, slat, slon)
dist = haversine_km(lat, lon, slat, slon)
if dist <= radius:
shelters.append({
"id": sid,
@ -354,7 +342,7 @@ def community_list(
d = dict(row)
d["user_interested"] = bool(d.pop("_user_interested", 0))
if lat is not None and lon is not None and d.get("lat") and d.get("lon"):
dist = _haversine(lat, lon, d["lat"], d["lon"])
dist = haversine_km(lat, lon, d["lat"], d["lon"])
d["distanz_km"] = round(dist, 1)
if dist > radius:
continue

View file

@ -1,10 +1,10 @@
"""BAN YARO — Nearby Alerts (Giftköder + Vermisste Hunde)"""
import math
from datetime import datetime
from fastapi import APIRouter, Depends
from database import db
from auth import get_current_user_optional as get_optional_user
from math_utils import haversine_m, bbox_deg_from_km
router = APIRouter()
@ -12,21 +12,9 @@ _RADIUS_M = 20_000 # 20 km
_RADIUS_KM = _RADIUS_M / 1000.0
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
def _bbox(lat: float, lon: float, radius_km: float) -> tuple[float, float, float, float]:
"""Bounding-Box-Approximation für lat/lon innerhalb radius_km."""
lat_delta = radius_km / 111.0
# cos darf bei Polen nicht 0 werden → mit kleinem Minimum absichern
cos_lat = max(abs(math.cos(math.radians(lat))), 0.01)
lon_delta = radius_km / (111.0 * cos_lat)
lat_delta, lon_delta = bbox_deg_from_km(lat, radius_km)
return (lat - lat_delta, lat + lat_delta, lon - lon_delta, lon + lon_delta)
@ -60,7 +48,7 @@ async def nearby_alerts(lat: float, lon: float, user=Depends(get_optional_user))
(lat, lon, user["id"])
)
has_poison = any(_haversine(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in poisons)
has_lost = any(_haversine(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in lost)
has_poison = any(haversine_m(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in poisons)
has_lost = any(haversine_m(lat, lon, r["lat"], r["lon"]) <= _RADIUS_M for r in lost)
return {"poison": has_poison, "lost": has_lost}

View file

@ -1,6 +1,6 @@
"""BAN YARO — Tagebuch Routes"""
import os, uuid, json, math, logging, asyncio
import os, uuid, json, logging, asyncio
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
from typing import Optional
@ -11,6 +11,7 @@ import httpx
import weather as weather_mod
from media_utils import convert_media, extract_video_thumb, safe_media_path, validate_upload, extract_gps_from_exif, generate_preview, preview_url_from, get_image_size
from timeutils import safe_client_time
from math_utils import haversine_km
logger = logging.getLogger(__name__)
@ -409,7 +410,7 @@ async def _fetch_pois_for_coords(lat: float, lon: float, limit: int = 5) -> list
elat = el.get("lat") or el.get("center", {}).get("lat")
elon = el.get("lon") or el.get("center", {}).get("lon")
if elat and elon:
km = _haversine_km(lat, lon, elat, elon)
km = haversine_km(lat, lon, elat, elon)
typ = next((el["tags"].get(k) for k in
["tourism", "historic", "leisure", "amenity", "shop"]
if el["tags"].get(k)), "place")
@ -422,16 +423,6 @@ async def _fetch_pois_for_coords(lat: float, lon: float, limit: int = 5) -> list
return results[:limit]
def _haversine_km(lat1, lon1, lat2, lon2) -> float:
R = 6371
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2)
return R * 2 * math.asin(math.sqrt(a))
@router.get("/{dog_id}/diary/nearby")
async def nearby_places(dog_id: int, lat: float, lon: float,
user=Depends(get_current_user)):
@ -445,7 +436,7 @@ async def nearby_places(dog_id: int, lat: float, lon: float,
(user["id"],)
).fetchall()
for p in places:
km = _haversine_km(lat, lon, p["lat"], p["lon"])
km = haversine_km(lat, lon, p["lat"], p["lon"])
if km <= 5:
results.append({"name": p["name"], "type": p["typ"] or "place",
"lat": p["lat"], "lon": p["lon"],
@ -456,7 +447,7 @@ async def nearby_places(dog_id: int, lat: float, lon: float,
"SELECT name, type, lat, lon FROM osm_pois WHERE name IS NOT NULL AND name != ''"
).fetchall()
for p in osm:
km = _haversine_km(lat, lon, p["lat"], p["lon"])
km = haversine_km(lat, lon, p["lat"], p["lon"])
if km <= 2:
results.append({"name": p["name"], "type": p["type"],
"lat": p["lat"], "lon": p["lon"],
@ -503,7 +494,7 @@ async def nearby_places(dog_id: int, lat: float, lon: float,
elat = el.get("lat") or el.get("center", {}).get("lat")
elon = el.get("lon") or el.get("center", {}).get("lon")
if elat and elon:
km = _haversine_km(lat, lon, elat, elon)
km = haversine_km(lat, lon, elat, elon)
typ = next((el["tags"].get(k) for k in
["tourism","historic","leisure","amenity","shop"]
if el["tags"].get(k)), "place")

View file

@ -1,27 +1,18 @@
"""BAN YARO — Events (Hundeveranstaltungen)"""
import math
from datetime import date
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
from math_utils import haversine_m
router = APIRouter()
TYPEN = {'ausstellung', 'training', 'treffen', 'markt', 'wettkampf', 'sonstiges'}
def _haversine(lat1, lon1, lat2, lon2):
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -86,7 +77,7 @@ async def list_events(
result = [dict(r) for r in rows]
if lat is not None and lon is not None:
result = [r for r in result
if r['lat'] is None or _haversine(lat, lon, r['lat'], r['lon']) <= radius]
if r['lat'] is None or haversine_m(lat, lon, r['lat'], r['lon']) <= radius]
return result

View file

@ -1,28 +1,19 @@
"""BAN YARO — Gassi-Zeiten-Pool (regelmäßige Gassi-Zeiten mit Gleichgesinnten)"""
import json
import math
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional, List
from database import db
from auth import get_current_user
from math_utils import haversine_m
logger = logging.getLogger(__name__)
router = APIRouter()
def _haversine(lat1, lon1, lat2, lon2):
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
class GassiZeitCreate(BaseModel):
dog_id: Optional[int] = None
wochentage: List[str] # ["mo", "mi", "fr"]
@ -83,7 +74,7 @@ async def list_gassi_zeiten(
# Distanz-Filter
if lat is not None and lon is not None and d.get("lat") and d.get("lon"):
dist = _haversine(lat, lon, d["lat"], d["lon"])
dist = haversine_m(lat, lon, d["lat"], d["lon"])
if not nur_eigene and dist > radius:
continue
d["distance_m"] = int(dist)

View file

@ -1,6 +1,6 @@
"""BAN YARO — Verlorener Hund Routes"""
import os, uuid, math
import os, uuid
from datetime import datetime
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
from pydantic import BaseModel
@ -10,24 +10,12 @@ from auth import get_current_user
from timeutils import safe_client_time
from routes.push import send_push_to_all
from media_utils import convert_media
from math_utils import haversine_m
router = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
# ------------------------------------------------------------------
# Haversine-Distanz in Metern
# ------------------------------------------------------------------
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -60,7 +48,7 @@ async def list_lost(lat: Optional[float] = None, lon: Optional[float] = None,
for r in rows:
entry = dict(r)
if lat is not None and lon is not None:
dist = _haversine(lat, lon, entry["lat"], entry["lon"])
dist = haversine_m(lat, lon, entry["lat"], entry["lon"])
if dist > radius_km * 1000:
continue
entry["distanz_m"] = round(dist)

View file

@ -1,27 +1,17 @@
"""BAN YARO — Hundefreundliche Orte"""
import math
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
from math_utils import haversine_m
router = APIRouter()
TYPEN = {'restaurant', 'shop', 'freilauf', 'kotbeutel', 'tierarzt', 'hundesalon', 'hundeschule'}
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -79,7 +69,7 @@ async def list_places(
result = [_row_to_dict(r) for r in rows]
if lat is not None and lon is not None:
result = [r for r in result if _haversine(lat, lon, r['lat'], r['lon']) <= radius]
result = [r for r in result if haversine_m(lat, lon, r['lat'], r['lon']) <= radius]
return result

View file

@ -1,30 +1,17 @@
"""BAN YARO — Playdate-Matching"""
import math
import logging
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
from math_utils import haversine_km
router = APIRouter()
logger = logging.getLogger(__name__)
# ------------------------------------------------------------------
# Haversine
# ------------------------------------------------------------------
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6371.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2)
return R * 2 * math.asin(math.sqrt(a))
def _calc_alter(geburtstag: Optional[str]) -> Optional[str]:
"""Gibt lesbares Alter zurück z.B. '2 Jahre' oder '5 Monate'."""
if not geburtstag:
@ -107,7 +94,7 @@ async def nearby(lat: float, lon: float, radius: int = 10,
result = []
for r in rows:
dist = _haversine(lat, lon, r["lat"], r["lon"])
dist = haversine_km(lat, lon, r["lat"], r["lon"])
if dist <= radius:
result.append({
"listing_id": r["listing_id"],

View file

@ -1,6 +1,6 @@
"""BAN YARO — Giftköder-Alarm Routes"""
import os, uuid, math
import os, uuid
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, HTTPException, Request, UploadFile, File
from pydantic import BaseModel
@ -10,24 +10,12 @@ from auth import get_current_user
from routes.push import send_push_nearby
from media_utils import convert_media
from ratelimit import check as rl_check
from math_utils import haversine_m
router = APIRouter()
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
# ------------------------------------------------------------------
# Haversine-Distanz in Metern
# ------------------------------------------------------------------
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -62,7 +50,7 @@ async def list_poison(lat: float, lon: float, radius: int = 5000):
results = []
for r in rows:
entry = dict(r)
dist = _haversine(lat, lon, entry["lat"], entry["lon"])
dist = haversine_m(lat, lon, entry["lat"], entry["lon"])
if dist <= radius:
entry["distanz_m"] = round(dist)
results.append(entry)

View file

@ -1,7 +1,7 @@
"""BAN YARO — Gassi-Routen"""
import datetime as _dt
import json, math, os, uuid
import json, os, uuid
import httpx
import polyline as _polyline
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
@ -13,6 +13,7 @@ from routes.achievements import update_streak, check_and_award
from timeutils import safe_client_time
from media_utils import convert_media
from routes.push import send_push_to_user
from math_utils import haversine_km, haversine_m
router = APIRouter()
@ -27,16 +28,6 @@ def _check_speed(distanz_km, dauer_min) -> bool:
return (distanz_km / (dauer_min / 60)) <= _MAX_AVG_KMH
def _haversine(lat1: float, lon1: float, lat2: float, lon2: float) -> float:
R = 6_371_000
p1 = math.radians(lat1)
p2 = math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -137,7 +128,7 @@ async def list_routes(
if lat is not None and lon is not None:
result = [
r for r in result
if r['start_lat'] and _haversine(lat, lon, r['start_lat'], r['start_lon']) <= radius
if r['start_lat'] and haversine_m(lat, lon, r['start_lat'], r['start_lon']) <= radius
]
user_id = user['id'] if user else None
@ -429,10 +420,7 @@ async def trim_route(route_id: int, data: RouteTrim, user=Depends(get_current_us
new_km = 0.0
for i in range(1, len(new_track)):
p1, p2 = new_track[i-1], new_track[i]
dlat = math.radians(p2['lat'] - p1['lat'])
dlon = math.radians(p2['lon'] - p1['lon'])
a = math.sin(dlat/2)**2 + math.cos(math.radians(p1['lat'])) * math.cos(math.radians(p2['lat'])) * math.sin(dlon/2)**2
new_km += 6371 * 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
new_km += haversine_km(p1['lat'], p1['lon'], p2['lat'], p2['lon'])
new_km = round(new_km, 2)
# Dauer proportional schätzen (Original-Pace)

View file

@ -1,27 +1,17 @@
"""BAN YARO — Service-Angebote (Sitting & Walks Matching)"""
import math
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional
from database import db
from auth import get_current_user
from math_utils import haversine_km
router = APIRouter()
ALLOWED_TYPES = {'sitting', 'walks'}
def _haversine(lat1, lon1, lat2, lon2):
R = 6371.0
dlat = math.radians(lat2 - lat1)
dlon = math.radians(lon2 - lon1)
a = (math.sin(dlat / 2) ** 2
+ math.cos(math.radians(lat1)) * math.cos(math.radians(lat2))
* math.sin(dlon / 2) ** 2)
return R * 2 * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -60,7 +50,7 @@ async def list_services(
for r in rows:
d = dict(r)
if lat is not None and lon is not None and d['lat'] and d['lon']:
dist = _haversine(lat, lon, d['lat'], d['lon'])
dist = haversine_km(lat, lon, d['lat'], d['lon'])
if dist > radius:
continue
d['distanz_km'] = round(dist, 1)

View file

@ -1,27 +1,18 @@
"""BAN YARO — Hundesitting"""
import json
import math
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel
from typing import Optional, List
from database import db
from auth import get_current_user
from math_utils import haversine_m
router = APIRouter()
SERVICES = {'tagesbetreuung', 'uebernachtung', 'gassi', 'hausbesuch'}
def _haversine(lat1, lon1, lat2, lon2):
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -80,7 +71,7 @@ async def list_sitters(
if service and service not in d['services']:
continue
if lat is not None and lon is not None and d['lat'] and d['lon']:
dist = _haversine(lat, lon, d['lat'], d['lon'])
dist = haversine_m(lat, lon, d['lat'], d['lon'])
if dist > radius:
continue
d['distanz_m'] = round(dist)

View file

@ -1,6 +1,6 @@
"""BAN YARO — Gassi-Treffen"""
import math, os, uuid
import os, uuid
import httpx
from datetime import date
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File
@ -9,25 +9,13 @@ from typing import Optional, List
from database import db
from auth import get_current_user
from routes.push import send_push_to_user
from math_utils import haversine_km, haversine_m
MEDIA_DIR = os.getenv("MEDIA_DIR", "/data/media")
router = APIRouter()
def _haversine(lat1, lon1, lat2, lon2):
R = 6_371_000
p1, p2 = math.radians(lat1), math.radians(lat2)
dp = math.radians(lat2 - lat1)
dl = math.radians(lon2 - lon1)
a = math.sin(dp / 2) ** 2 + math.cos(p1) * math.cos(p2) * math.sin(dl / 2) ** 2
return 2 * R * math.asin(math.sqrt(a))
def _haversine_km(lat1, lon1, lat2, lon2):
return _haversine(lat1, lon1, lat2, lon2) / 1000
# ------------------------------------------------------------------
# Schemas
# ------------------------------------------------------------------
@ -91,7 +79,7 @@ async def list_walks(
# Umkreis-Filter
if lat is not None and lon is not None:
result = [r for r in result if _haversine(lat, lon, r['lat'], r['lon']) <= radius]
result = [r for r in result if haversine_m(lat, lon, r['lat'], r['lon']) <= radius]
return result
@ -131,7 +119,7 @@ async def nearby_places(lat: float, lon: float, user=Depends(get_current_user)):
"SELECT name, typ, lat, lon FROM places WHERE lat IS NOT NULL",
).fetchall()
for p in places:
km = _haversine_km(lat, lon, p["lat"], p["lon"])
km = haversine_km(lat, lon, p["lat"], p["lon"])
if km <= 5:
results.append({"name": p["name"], "type": p["typ"] or "place",
"lat": p["lat"], "lon": p["lon"],
@ -142,7 +130,7 @@ async def nearby_places(lat: float, lon: float, user=Depends(get_current_user)):
"SELECT name, type, lat, lon FROM osm_pois WHERE name IS NOT NULL AND name != ''"
).fetchall()
for p in osm:
km = _haversine_km(lat, lon, p["lat"], p["lon"])
km = haversine_km(lat, lon, p["lat"], p["lon"])
if km <= 2:
results.append({"name": p["name"], "type": p["type"],
"lat": p["lat"], "lon": p["lon"],
@ -170,7 +158,7 @@ async def nearby_places(lat: float, lon: float, user=Depends(get_current_user)):
elon = el.get("lon") or el.get("center", {}).get("lon")
if elat is None or elon is None:
continue
km = _haversine_km(lat, lon, elat, elon)
km = haversine_km(lat, lon, elat, elon)
if km <= 1:
results.append({"name": name, "type": "osm",
"lat": elat, "lon": elon,