Fix coordinate parsing to handle DMS format and prevent stack traces
- Add parse_coordinate() function to handle both decimal degrees and DMS format - Replace direct float() conversion with robust coordinate parsing - Add proper validation for latitude/longitude ranges - Create user-friendly error page instead of showing stack traces - Support formats like "56°5'58.56"N" and "3°22'33.71"W" - Update both /detail and / routes with improved error handling Fixes #29: Don't return stack trace when lat/lon are not integers
This commit is contained in:
parent
72afb4c286
commit
7235df4cad
81
lookup.py
81
lookup.py
|
@ -3,6 +3,7 @@
|
|||
|
||||
import inspect
|
||||
import random
|
||||
import re
|
||||
import socket
|
||||
import sys
|
||||
import traceback
|
||||
|
@ -234,6 +235,53 @@ def handle_database_error(error: Exception) -> tuple[str, int]:
|
|||
return render_template("database_error.html"), 500
|
||||
|
||||
|
||||
def parse_coordinate(coord_str: str) -> float:
|
||||
"""Parse coordinate string in various formats to decimal degrees."""
|
||||
coord_str = coord_str.strip()
|
||||
|
||||
# Try decimal degrees first
|
||||
try:
|
||||
return float(coord_str)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Parse DMS format (e.g., "56°5'58.56"N" or "3°22'33.71"W")
|
||||
dms_pattern = r"""
|
||||
(?P<degrees>\d+)°
|
||||
(?P<minutes>\d+)'
|
||||
(?P<seconds>[\d.]+)"?
|
||||
(?P<direction>[NSEW])?
|
||||
"""
|
||||
|
||||
match = re.match(dms_pattern, coord_str, re.VERBOSE)
|
||||
if match:
|
||||
degrees = int(match.group("degrees"))
|
||||
minutes = int(match.group("minutes"))
|
||||
seconds = float(match.group("seconds"))
|
||||
direction = match.group("direction")
|
||||
|
||||
# Convert to decimal degrees
|
||||
decimal = degrees + minutes / 60 + seconds / 3600
|
||||
|
||||
# Apply direction
|
||||
if direction in ["S", "W"]:
|
||||
decimal = -decimal
|
||||
|
||||
return decimal
|
||||
|
||||
# If all parsing attempts fail
|
||||
raise ValueError(f"Could not parse coordinate: {coord_str}")
|
||||
|
||||
|
||||
def validate_coordinates(lat: float, lon: float) -> str | None:
|
||||
"""Validate latitude and longitude ranges. Returns error message if invalid."""
|
||||
if lat < -90 or lat > 90:
|
||||
return "Latitude must be between -90 and 90 degrees"
|
||||
if lon < -180 or lon > 180:
|
||||
return "Longitude must be between -180 and 180 degrees"
|
||||
return None
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index() -> str | Response:
|
||||
"""Index page."""
|
||||
|
@ -251,13 +299,20 @@ def index() -> str | Response:
|
|||
|
||||
lat, lon = float(lat_str), float(lon_str)
|
||||
|
||||
if lat < -90 or lat > 90 or lon < -180 or lon > 180:
|
||||
try:
|
||||
lat = parse_coordinate(lat_str)
|
||||
lon = parse_coordinate(lon_str)
|
||||
except ValueError:
|
||||
return jsonify(
|
||||
coords={"lat": lat, "lon": lon},
|
||||
error="lat must be between -90 and 90, "
|
||||
+ "and lon must be between -180 and 180",
|
||||
coords={"lat": lat_str, "lon": lon_str},
|
||||
error="Invalid coordinate format. "
|
||||
+ "Please use decimal degrees (e.g., 56.099600) "
|
||||
+ "or DMS format (e.g., 56°5'58.56\"N)",
|
||||
)
|
||||
|
||||
if error_msg := validate_coordinates(lat, lon):
|
||||
return jsonify(coords={"lat": lat, "lon": lon}, error=error_msg)
|
||||
|
||||
result = lat_lon_to_wikidata(lat, lon)["result"]
|
||||
result.pop("element", None)
|
||||
result.pop("geojson", None)
|
||||
|
@ -352,12 +407,22 @@ def build_detail_page(lat: float, lon: float) -> str:
|
|||
def detail_page() -> Response | str:
|
||||
"""Detail page."""
|
||||
database.session.execute(text("SELECT 1"))
|
||||
try:
|
||||
lat_str, lon_str = request.args["lat"], request.args["lon"]
|
||||
lat, lon = float(lat_str), float(lon_str)
|
||||
except TypeError:
|
||||
|
||||
lat_str = request.args.get("lat")
|
||||
lon_str = request.args.get("lon")
|
||||
|
||||
if not lat_str or not lon_str:
|
||||
return redirect(url_for("index"))
|
||||
|
||||
try:
|
||||
lat = parse_coordinate(lat_str)
|
||||
lon = parse_coordinate(lon_str)
|
||||
except ValueError as e:
|
||||
error = f"Invalid coordinate format: {str(e)}"
|
||||
return render_template(
|
||||
"coordinate_error.html", lat_str=lat_str, lon_str=lon_str, error=error
|
||||
)
|
||||
|
||||
return build_detail_page(lat, lon)
|
||||
|
||||
|
||||
|
|
40
templates/coordinate_error.html
Normal file
40
templates/coordinate_error.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Geocode to Commons: coordinate error{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="m-3">
|
||||
<h1>Geocode coordinates to Commons Category</h1>
|
||||
|
||||
<div class="alert alert-danger" role="alert">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="coordinates">
|
||||
<strong>Received coordinates:</strong><br>
|
||||
Latitude: {{ lat_str }}<br>
|
||||
Longitude: {{ lon_str }}
|
||||
</div>
|
||||
|
||||
<div class="help-section">
|
||||
<h2 class="help-title">Supported coordinate formats:</h2>
|
||||
|
||||
<div class="examples">
|
||||
<div class="example">Decimal degrees: 56.099600, -3.376031</div>
|
||||
<div class="example">DMS format: 56°5'58.56"N, 3°22'33.71"W</div>
|
||||
<div class="example">Mixed format: 56.099600, 3°22'33.71"W</div>
|
||||
</div>
|
||||
|
||||
|
||||
<p>
|
||||
<div>Latitude must be between -90 and 90 degrees.</div>
|
||||
<div>Longitude must be between -180 and 180 degrees.</div>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="{{ url_for('index') }}" class="back-link">← Back to Home</a>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue