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 inspect
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
@ -234,6 +235,53 @@ def handle_database_error(error: Exception) -> tuple[str, int]:
|
||||||
return render_template("database_error.html"), 500
|
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("/")
|
@app.route("/")
|
||||||
def index() -> str | Response:
|
def index() -> str | Response:
|
||||||
"""Index page."""
|
"""Index page."""
|
||||||
|
@ -251,13 +299,20 @@ def index() -> str | Response:
|
||||||
|
|
||||||
lat, lon = float(lat_str), float(lon_str)
|
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(
|
return jsonify(
|
||||||
coords={"lat": lat, "lon": lon},
|
coords={"lat": lat_str, "lon": lon_str},
|
||||||
error="lat must be between -90 and 90, "
|
error="Invalid coordinate format. "
|
||||||
+ "and lon must be between -180 and 180",
|
+ "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 = lat_lon_to_wikidata(lat, lon)["result"]
|
||||||
result.pop("element", None)
|
result.pop("element", None)
|
||||||
result.pop("geojson", None)
|
result.pop("geojson", None)
|
||||||
|
@ -352,12 +407,22 @@ def build_detail_page(lat: float, lon: float) -> str:
|
||||||
def detail_page() -> Response | str:
|
def detail_page() -> Response | str:
|
||||||
"""Detail page."""
|
"""Detail page."""
|
||||||
database.session.execute(text("SELECT 1"))
|
database.session.execute(text("SELECT 1"))
|
||||||
try:
|
|
||||||
lat_str, lon_str = request.args["lat"], request.args["lon"]
|
lat_str = request.args.get("lat")
|
||||||
lat, lon = float(lat_str), float(lon_str)
|
lon_str = request.args.get("lon")
|
||||||
except TypeError:
|
|
||||||
|
if not lat_str or not lon_str:
|
||||||
return redirect(url_for("index"))
|
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)
|
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