feat: oninput handler copies building # to trailer number
- Manual asset form: manBuildingNumber → manAddress (address/trailer field) - Location form: locFormBldgNum → locFormTrailer
This commit is contained in:
@@ -998,6 +998,72 @@ def get_asset(asset_id: int):
|
||||
return result
|
||||
|
||||
|
||||
# ─── Navigation endpoint ────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/assets/{asset_id}/navigation")
|
||||
def get_navigation(asset_id: int, lat: float = Query(...), lng: float = Query(...)):
|
||||
"""Return distance, bearing, and coordinates for navigation from GPS to asset."""
|
||||
import math
|
||||
|
||||
conn = get_db()
|
||||
row = conn.execute(
|
||||
"SELECT id, name, latitude, longitude, machine_id, address FROM assets WHERE id = ?",
|
||||
(asset_id,),
|
||||
).fetchone()
|
||||
conn.close()
|
||||
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Asset not found")
|
||||
|
||||
if row["latitude"] is None or row["longitude"] is None:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Asset has no GPS coordinates set",
|
||||
)
|
||||
|
||||
dest_lat, dest_lng = row["latitude"], row["longitude"]
|
||||
|
||||
# Haversine distance
|
||||
R = 6371000 # Earth radius in meters
|
||||
phi1 = math.radians(lat)
|
||||
phi2 = math.radians(dest_lat)
|
||||
dphi = math.radians(dest_lat - lat)
|
||||
dlambda = math.radians(dest_lng - lng)
|
||||
|
||||
a_val = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2
|
||||
c_val = 2 * math.atan2(math.sqrt(a_val), math.sqrt(1 - a_val))
|
||||
distance_m = R * c_val
|
||||
|
||||
# Bearing
|
||||
y = math.sin(dlambda) * math.cos(phi2)
|
||||
x = math.cos(phi1) * math.sin(phi2) - math.sin(phi1) * math.cos(phi2) * math.cos(dlambda)
|
||||
bearing = (math.degrees(math.atan2(y, x)) + 360) % 360
|
||||
|
||||
# Cardinal direction
|
||||
dirs = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]
|
||||
idx = round(bearing / 45) % 8
|
||||
cardinal = dirs[idx]
|
||||
|
||||
return {
|
||||
"asset_id": asset_id,
|
||||
"asset_name": row["name"],
|
||||
"asset_lat": dest_lat,
|
||||
"asset_lng": dest_lng,
|
||||
"asset_address": row["address"] or None,
|
||||
"origin_lat": lat,
|
||||
"origin_lng": lng,
|
||||
"distance_meters": round(distance_m, 1),
|
||||
"distance_km": round(distance_m / 1000, 2),
|
||||
"bearing": round(bearing, 1),
|
||||
"cardinal": cardinal,
|
||||
"google_maps_url": (
|
||||
f"https://www.google.com/maps/dir/?api=1"
|
||||
f"&origin={lat},{lng}"
|
||||
f"&destination={dest_lat},{dest_lng}"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
# ─── Task 6: PUT / DELETE /api/assets/{id} ─────────────────────────────────
|
||||
|
||||
|
||||
@@ -2595,6 +2661,64 @@ async def ocr_sticker(file: UploadFile = File(...)):
|
||||
}
|
||||
|
||||
|
||||
# ─── Reverse Geocode (Nominatim) ────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/geocode")
|
||||
def geocode(lat: float = Query(...), lng: float = Query(...)):
|
||||
"""Reverse geocode GPS coordinates to address using OpenStreetMap Nominatim."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
try:
|
||||
url = (
|
||||
f"https://nominatim.openstreetmap.org/reverse"
|
||||
f"?format=json&lat={lat}&lon={lng}&zoom=18&addressdetails=1"
|
||||
)
|
||||
# Set a proper User-Agent as required by Nominatim ToS
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "CanteenAssetTracker/2.0"})
|
||||
with urllib.request.urlopen(req, timeout=5) as resp:
|
||||
data = _json.loads(resp.read())
|
||||
except (urllib.error.URLError, _json.JSONDecodeError, OSError) as e:
|
||||
raise HTTPException(status_code=502, detail=f"Geocoding failed: {str(e)}")
|
||||
|
||||
addr = data.get("address", {})
|
||||
display_name = data.get("display_name", "")
|
||||
|
||||
# Map Nominatim address parts to our schema
|
||||
building = addr.get("building", "") or addr.get("house_number", "")
|
||||
building_name = addr.get("building_name", "") or addr.get("amenity", "")
|
||||
street = addr.get("road", "") or addr.get("pedestrian", "") or addr.get("path", "")
|
||||
house_number = addr.get("house_number", "")
|
||||
city = addr.get("city", "") or addr.get("town", "") or addr.get("village", "")
|
||||
state = addr.get("state", "")
|
||||
postcode = addr.get("postcode", "")
|
||||
|
||||
# Build a formatted address string
|
||||
parts = []
|
||||
if house_number and street:
|
||||
parts.append(f"{house_number} {street}")
|
||||
elif street:
|
||||
parts.append(street)
|
||||
if city:
|
||||
parts.append(city)
|
||||
if state:
|
||||
parts.append(state)
|
||||
if postcode:
|
||||
parts.append(postcode)
|
||||
formatted = ", ".join(parts) if parts else display_name
|
||||
|
||||
return {
|
||||
"display_name": display_name,
|
||||
"formatted": formatted,
|
||||
"building_number": house_number or building,
|
||||
"building_name": building_name,
|
||||
"street": street,
|
||||
"city": city,
|
||||
"state": state,
|
||||
"postcode": postcode,
|
||||
"address": formatted, # mapped to our asset.address field
|
||||
}
|
||||
|
||||
|
||||
# ─── Static Files (mounted last to not shadow routes) ──────────────────────
|
||||
|
||||
app.mount("/uploads", StaticFiles(directory=str(UPLOADS_DIR)), name="uploads")
|
||||
|
||||
Reference in New Issue
Block a user