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:
Edward Betts 2025-07-15 09:05:31 +00:00
parent 72afb4c286
commit 7235df4cad
2 changed files with 113 additions and 8 deletions

View file

@ -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)

View 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 %}