Initial commit: OpenMaps project
This commit is contained in:
46
.dockerignore
Normal file
46
.dockerignore
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
env/
|
||||||
|
venv/
|
||||||
|
.venv/
|
||||||
|
ENV/
|
||||||
|
env.bak/
|
||||||
|
venv.bak/
|
||||||
|
|
||||||
|
# Flask
|
||||||
|
instance/
|
||||||
|
.webassets-cache
|
||||||
|
.well-known/cache
|
||||||
|
|
||||||
|
# Git
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.gitattributes
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
Dockerfile
|
||||||
|
.dockerignore
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.yaml
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
tests/
|
||||||
|
pytest.ini
|
||||||
54
.gitignore
vendored
Normal file
54
.gitignore
vendored
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Docker
|
||||||
|
.docker/
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
logs/
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
.tox/
|
||||||
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
FROM python:3.12-slim
|
||||||
|
|
||||||
|
# Set environment variables
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PORT=5000
|
||||||
|
|
||||||
|
# Set working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Install system dependencies (if any needed)
|
||||||
|
# RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
# && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy requirements first for better layer caching
|
||||||
|
COPY requirements.txt .
|
||||||
|
|
||||||
|
# Install Python dependencies
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
# Copy application code
|
||||||
|
COPY app.py .
|
||||||
|
COPY templates/ templates/
|
||||||
|
|
||||||
|
# Create a non-root user to run the app
|
||||||
|
RUN useradd -m appuser
|
||||||
|
USER appuser
|
||||||
|
|
||||||
|
# Expose the port
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
# Run the app with gunicorn
|
||||||
|
CMD ["sh", "-c", "gunicorn --bind 0.0.0.0:${PORT} --workers 2 --threads 4 app:app"]
|
||||||
219
app.py
Normal file
219
app.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
from flask import Flask, render_template, request, jsonify
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
app = Flask(__name__)
|
||||||
|
|
||||||
|
NOMINATIM_SEARCH_URL = "https://nominatim.openstreetmap.org/search"
|
||||||
|
NOMINATIM_REVERSE_URL = "https://nominatim.openstreetmap.org/reverse"
|
||||||
|
|
||||||
|
# Nominatim asks clients to identify themselves.
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "LibreMapViewer/1.0 (local Flask application)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Very simple rate limiting so we don't hammer Nominatim.
|
||||||
|
last_request_time = 0
|
||||||
|
MIN_REQUEST_INTERVAL = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def nominatim_request(url, params):
|
||||||
|
"""
|
||||||
|
Make a request to Nominatim while respecting a small
|
||||||
|
delay between requests.
|
||||||
|
"""
|
||||||
|
global last_request_time
|
||||||
|
|
||||||
|
elapsed = time.time() - last_request_time
|
||||||
|
|
||||||
|
if elapsed < MIN_REQUEST_INTERVAL:
|
||||||
|
time.sleep(MIN_REQUEST_INTERVAL - elapsed)
|
||||||
|
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
params=params,
|
||||||
|
headers=HEADERS,
|
||||||
|
timeout=15
|
||||||
|
)
|
||||||
|
|
||||||
|
last_request_time = time.time()
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def index():
|
||||||
|
return render_template("index.html")
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/search")
|
||||||
|
def search():
|
||||||
|
query = request.args.get("q", "").strip()
|
||||||
|
|
||||||
|
if not query:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Please enter a location"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"q": query,
|
||||||
|
"format": "jsonv2",
|
||||||
|
"limit": 8,
|
||||||
|
"addressdetails": 1,
|
||||||
|
"extratags": 1,
|
||||||
|
"namedetails": 1,
|
||||||
|
"dedupe": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
results = nominatim_request(
|
||||||
|
NOMINATIM_SEARCH_URL,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Search failed: {e}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
normalized = []
|
||||||
|
|
||||||
|
for item in results:
|
||||||
|
|
||||||
|
normalized.append({
|
||||||
|
"place_id": item.get("place_id"),
|
||||||
|
|
||||||
|
"osm_type": item.get("osm_type"),
|
||||||
|
"osm_id": item.get("osm_id"),
|
||||||
|
|
||||||
|
"display_name":
|
||||||
|
item.get("display_name", ""),
|
||||||
|
|
||||||
|
"lat":
|
||||||
|
float(item["lat"]),
|
||||||
|
|
||||||
|
"lon":
|
||||||
|
float(item["lon"]),
|
||||||
|
|
||||||
|
"type":
|
||||||
|
item.get("type"),
|
||||||
|
|
||||||
|
"category":
|
||||||
|
item.get("category"),
|
||||||
|
|
||||||
|
"address":
|
||||||
|
item.get("address", {}),
|
||||||
|
|
||||||
|
"extratags":
|
||||||
|
item.get("extratags", {}),
|
||||||
|
|
||||||
|
"namedetails":
|
||||||
|
item.get("namedetails", {}),
|
||||||
|
|
||||||
|
"importance":
|
||||||
|
item.get("importance", 0),
|
||||||
|
|
||||||
|
"boundingbox":
|
||||||
|
item.get("boundingbox"),
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify(normalized)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/geocode")
|
||||||
|
def geocode():
|
||||||
|
|
||||||
|
lat = request.args.get("lat")
|
||||||
|
lon = request.args.get("lon")
|
||||||
|
|
||||||
|
if not lat or not lon:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Missing latitude or longitude"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
try:
|
||||||
|
lat = float(lat)
|
||||||
|
lon = float(lon)
|
||||||
|
except ValueError:
|
||||||
|
return jsonify({
|
||||||
|
"error": "Invalid latitude or longitude"
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"lat": lat,
|
||||||
|
"lon": lon,
|
||||||
|
|
||||||
|
"format": "jsonv2",
|
||||||
|
|
||||||
|
"zoom": 18,
|
||||||
|
|
||||||
|
"addressdetails": 1,
|
||||||
|
|
||||||
|
"extratags": 1,
|
||||||
|
|
||||||
|
"namedetails": 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
result = nominatim_request(
|
||||||
|
NOMINATIM_REVERSE_URL,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"error": f"Reverse geocoding failed: {e}"
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"display_name": None
|
||||||
|
})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
|
||||||
|
"place_id":
|
||||||
|
result.get("place_id"),
|
||||||
|
|
||||||
|
"osm_type":
|
||||||
|
result.get("osm_type"),
|
||||||
|
|
||||||
|
"osm_id":
|
||||||
|
result.get("osm_id"),
|
||||||
|
|
||||||
|
"display_name":
|
||||||
|
result.get("display_name", ""),
|
||||||
|
|
||||||
|
"lat":
|
||||||
|
float(result.get("lat", lat)),
|
||||||
|
|
||||||
|
"lon":
|
||||||
|
float(result.get("lon", lon)),
|
||||||
|
|
||||||
|
"type":
|
||||||
|
result.get("type"),
|
||||||
|
|
||||||
|
"category":
|
||||||
|
result.get("category"),
|
||||||
|
|
||||||
|
"address":
|
||||||
|
result.get("address", {}),
|
||||||
|
|
||||||
|
"extratags":
|
||||||
|
result.get("extratags", {}),
|
||||||
|
|
||||||
|
"namedetails":
|
||||||
|
result.get("namedetails", {}),
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run(
|
||||||
|
port=5000,
|
||||||
|
debug=True
|
||||||
|
)
|
||||||
9
docker-compose.yml
Normal file
9
docker-compose.yml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
services:
|
||||||
|
openmaps:
|
||||||
|
build: .
|
||||||
|
container_name: openmaps
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
environment:
|
||||||
|
- PORT=5000
|
||||||
|
restart: unless-stopped
|
||||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
Flask==3.0.3
|
||||||
|
requests==2.32.3
|
||||||
|
gunicorn==22.0.0
|
||||||
2375
templates/index.html
Normal file
2375
templates/index.html
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user