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