From 7235df4cad74ea2e1cb7badd0d7166202c1ff695 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Tue, 15 Jul 2025 09:05:31 +0000 Subject: [PATCH] Fix coordinate parsing to handle DMS format and prevent stack traces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- lookup.py | 81 +++++++++++++++++++++++++++++---- templates/coordinate_error.html | 40 ++++++++++++++++ 2 files changed, 113 insertions(+), 8 deletions(-) create mode 100644 templates/coordinate_error.html diff --git a/lookup.py b/lookup.py index f58196a..fa2ff45 100755 --- a/lookup.py +++ b/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\d+)° + (?P\d+)' + (?P[\d.]+)"? + (?P[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) diff --git a/templates/coordinate_error.html b/templates/coordinate_error.html new file mode 100644 index 0000000..ce851a9 --- /dev/null +++ b/templates/coordinate_error.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %}Geocode to Commons: coordinate error{% endblock %} + + +{% block content %} +
+

Geocode coordinates to Commons Category

+ + + + +
+ Received coordinates:
+ Latitude: {{ lat_str }}
+ Longitude: {{ lon_str }} +
+ +
+

Supported coordinate formats:

+ +
+
Decimal degrees: 56.099600, -3.376031
+
DMS format: 56°5'58.56"N, 3°22'33.71"W
+
Mixed format: 56.099600, 3°22'33.71"W
+
+ + +

+

Latitude must be between -90 and 90 degrees.
+
Longitude must be between -180 and 180 degrees.
+

+
+ +← Back to Home + +
+{% endblock %}