generated from nathanwoodburn/python-webserver-template
feat: Add map to make site easier to use
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
This commit is contained in:
8
db.py
8
db.py
@@ -24,11 +24,12 @@ def load_locations():
|
|||||||
except (json.JSONDecodeError, FileNotFoundError):
|
except (json.JSONDecodeError, FileNotFoundError):
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def save_location(name, description="", rating=None):
|
def save_location(latitude, longitude, description="", rating=None):
|
||||||
"""Save a new kite location to the database
|
"""Save a new kite location to the database
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
name (str): Name of the kite flying location
|
latitude (float): Latitude coordinate
|
||||||
|
longitude (float): Longitude coordinate
|
||||||
description (str, optional): Description of the location
|
description (str, optional): Description of the location
|
||||||
rating (int, optional): Rating from 1-5 stars
|
rating (int, optional): Rating from 1-5 stars
|
||||||
|
|
||||||
@@ -39,7 +40,8 @@ def save_location(name, description="", rating=None):
|
|||||||
|
|
||||||
# Create new location entry
|
# Create new location entry
|
||||||
new_location = {
|
new_location = {
|
||||||
"name": name,
|
"latitude": float(latitude),
|
||||||
|
"longitude": float(longitude),
|
||||||
"description": description,
|
"description": description,
|
||||||
"rating": rating,
|
"rating": rating,
|
||||||
"date_added": datetime.now().isoformat()
|
"date_added": datetime.now().isoformat()
|
||||||
|
|||||||
53
server.py
53
server.py
@@ -13,7 +13,7 @@ from flask import (
|
|||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
import dotenv
|
import dotenv
|
||||||
import db # Import our new db module
|
import db # Import our new db module
|
||||||
import re
|
import re
|
||||||
@@ -147,43 +147,57 @@ def index():
|
|||||||
# Kite Watch API Routes
|
# Kite Watch API Routes
|
||||||
@app.route("/api/locations", methods=["GET"])
|
@app.route("/api/locations", methods=["GET"])
|
||||||
def get_locations():
|
def get_locations():
|
||||||
locations = db.load_locations()
|
# Get all locations from DB
|
||||||
return jsonify(locations)
|
all_locations = db.load_locations()
|
||||||
|
|
||||||
|
# Check if we should filter by time (last 48 hours)
|
||||||
|
cutoff_time = datetime.now() - timedelta(hours=48)
|
||||||
|
|
||||||
|
# Filter locations by time
|
||||||
|
recent_locations = [
|
||||||
|
loc for loc in all_locations
|
||||||
|
if datetime.fromisoformat(loc["date_added"]) > cutoff_time
|
||||||
|
]
|
||||||
|
|
||||||
|
return jsonify(recent_locations)
|
||||||
|
|
||||||
@app.route("/api/locations", methods=["POST"])
|
@app.route("/api/locations", methods=["POST"])
|
||||||
def add_location():
|
def add_location():
|
||||||
data = request.json
|
data = request.json
|
||||||
if not data or "name" not in data:
|
if not data:
|
||||||
return jsonify({"error": "Name is required"}), 400
|
return jsonify({"error": "Invalid request data"}), 400
|
||||||
|
|
||||||
name = data.get("name")
|
latitude = data.get("latitude")
|
||||||
|
longitude = data.get("longitude")
|
||||||
description = data.get("description", "")
|
description = data.get("description", "")
|
||||||
rating = data.get("rating")
|
rating = data.get("rating")
|
||||||
|
|
||||||
# Validate required fields
|
# Validate required fields
|
||||||
if len(name.strip()) < 5:
|
if latitude is None or longitude is None:
|
||||||
return jsonify({"error": "Location name must be at least 5 characters long"}), 400
|
return jsonify({"error": "Latitude and longitude are required"}), 400
|
||||||
if len(name.strip()) > 100:
|
|
||||||
return jsonify({"error": "Location name is too long (max 100 characters)"}), 400
|
try:
|
||||||
|
latitude = float(latitude)
|
||||||
|
longitude = float(longitude)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
return jsonify({"error": "Latitude and longitude must be valid numbers"}), 400
|
||||||
|
|
||||||
|
# Validate coordinate ranges
|
||||||
|
if not (-90 <= latitude <= 90) or not (-180 <= longitude <= 180):
|
||||||
|
return jsonify({"error": "Invalid coordinate values"}), 400
|
||||||
|
|
||||||
if len(description.strip()) > 2000:
|
if len(description.strip()) > 2000:
|
||||||
return jsonify({"error": "Notes are too long (max 2000 characters)"}), 400
|
return jsonify({"error": "Notes are too long (max 2000 characters)"}), 400
|
||||||
if not name.strip():
|
|
||||||
return jsonify({"error": "Location name cannot be empty"}), 400
|
|
||||||
|
|
||||||
|
|
||||||
# Check for profanity
|
# Check for profanity
|
||||||
if contains_profanity(name):
|
|
||||||
return jsonify({"error": "Location name contains inappropriate language"}), 400
|
|
||||||
|
|
||||||
if contains_profanity(description):
|
if contains_profanity(description):
|
||||||
return jsonify({"error": "Notes contain inappropriate language"}), 400
|
return jsonify({"error": "Notes contain inappropriate language"}), 400
|
||||||
|
|
||||||
# Validate for JSON safety
|
# Validate for JSON safety
|
||||||
if not is_json_safe(name) or not is_json_safe(description):
|
if not is_json_safe(description):
|
||||||
return jsonify({"error": "Input contains invalid characters"}), 400
|
return jsonify({"error": "Input contains invalid characters"}), 400
|
||||||
|
|
||||||
# Sanitize inputs
|
# Sanitize inputs
|
||||||
name = sanitize_text_input(name)
|
|
||||||
description = sanitize_text_input(description)
|
description = sanitize_text_input(description)
|
||||||
|
|
||||||
# Validate rating
|
# Validate rating
|
||||||
@@ -197,7 +211,8 @@ def add_location():
|
|||||||
else:
|
else:
|
||||||
return jsonify({"error": "Rating is required"}), 400
|
return jsonify({"error": "Rating is required"}), 400
|
||||||
|
|
||||||
location = db.save_location(name, description, rating)
|
# Save to database
|
||||||
|
location = db.save_location(latitude, longitude, description, rating)
|
||||||
return jsonify(location), 201
|
return jsonify(location), 201
|
||||||
|
|
||||||
@app.route("/<path:path>")
|
@app.route("/<path:path>")
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ h2 {
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-group {
|
.form-group {
|
||||||
@@ -294,3 +295,142 @@ a:hover {
|
|||||||
.footer a:hover {
|
.footer a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Map styling */
|
||||||
|
#map-container {
|
||||||
|
height: 500px; /* Increased height for better resolution */
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
z-index: 1;
|
||||||
|
/* Improved rendering for high-DPI displays */
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
backface-visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix for Leaflet tiles to render at higher resolution */
|
||||||
|
.leaflet-container {
|
||||||
|
image-rendering: high-quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-retina .leaflet-tile {
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom marker styling */
|
||||||
|
.custom-marker {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.marker-pin {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #ff0000;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Coordinates input styling */
|
||||||
|
.location-controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinates-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.coordinates-group input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#get-location-btn {
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border: 1px solid #444;
|
||||||
|
padding: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
#get-location-btn:hover {
|
||||||
|
background-color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Popup styling */
|
||||||
|
.leaflet-popup-content-wrapper {
|
||||||
|
background-color: #222;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-content {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-popup-tip {
|
||||||
|
background-color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content .rating {
|
||||||
|
font-weight: bold;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 0;
|
||||||
|
color: #ffd700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup-content .date {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: #aaa;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modifications to existing styles to accommodate map */
|
||||||
|
.card {
|
||||||
|
/* ...existing code... */
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
/* ...existing code... */
|
||||||
|
|
||||||
|
.coordinates-group {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the coordinate inputs */
|
||||||
|
.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input map styling */
|
||||||
|
#input-map-container {
|
||||||
|
height: 300px;
|
||||||
|
width: 100%;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-help {
|
||||||
|
margin: 10px 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #adb5bd;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -7,6 +7,12 @@
|
|||||||
<title>Kite Watch</title>
|
<title>Kite Watch</title>
|
||||||
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
|
||||||
<link rel="stylesheet" href="/assets/css/index.css">
|
<link rel="stylesheet" href="/assets/css/index.css">
|
||||||
|
<!-- Leaflet CSS -->
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
crossorigin=""/>
|
||||||
|
<!-- Leaflet JavaScript -->
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
crossorigin=""></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -19,10 +25,17 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Add a Kite Flying Location</h2>
|
<h2>Add a Kite Flying Location</h2>
|
||||||
<div id="notification" class="notification hidden"></div>
|
<div id="notification" class="notification hidden"></div>
|
||||||
<form id="location-form">
|
<form id="location-form" novalidate>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location-name">Location Name *</label>
|
<label>Select Your Location</label>
|
||||||
<input type="text" id="location-name" required placeholder="Arboretum, Weston Park, etc.">
|
<div id="input-map-container"></div>
|
||||||
|
<div class="location-controls">
|
||||||
|
<!-- Using hidden input type instead of visually hiding regular inputs -->
|
||||||
|
<input type="hidden" id="location-lat" name="location-lat">
|
||||||
|
<input type="hidden" id="location-lng" name="location-lng">
|
||||||
|
<button type="button" id="get-location-btn">Get My Location</button>
|
||||||
|
<p class="location-help">Click on the map or use the button above to set your location</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="location-description">Notes</label>
|
<label for="location-description">Notes</label>
|
||||||
@@ -40,7 +53,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Great Places to Fly Kites</h2>
|
<h2>Great Places to Fly Kites in Canberra</h2>
|
||||||
|
<div id="map-container"></div>
|
||||||
<div id="locations-list">
|
<div id="locations-list">
|
||||||
<p class="loading">Loading locations...</p>
|
<p class="loading">Loading locations...</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,28 +68,218 @@
|
|||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Global map variable
|
||||||
|
let map;
|
||||||
|
let inputMap; // New map for input section
|
||||||
|
let markers = [];
|
||||||
|
let locationMarker; // Marker for the currently selected location
|
||||||
|
|
||||||
|
// Canberra coordinates - defined at the top level for reuse
|
||||||
|
const CANBERRA_LAT = -35.2809;
|
||||||
|
const CANBERRA_LNG = 149.1300;
|
||||||
|
const MAX_DISTANCE_KM = 50; // Maximum distance from Canberra allowed (in kilometers)
|
||||||
|
|
||||||
|
// Calculate distance between two points using the Haversine formula (in kilometers)
|
||||||
|
function calculateDistance(lat1, lon1, lat2, lon2) {
|
||||||
|
const R = 6371; // Earth's radius in kilometers
|
||||||
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||||||
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||||||
|
const a =
|
||||||
|
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
|
||||||
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||||||
|
Math.sin(dLon / 2) * Math.sin(dLon / 2);
|
||||||
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
return R * c;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a location is within the allowed distance from Canberra
|
||||||
|
function isWithinCanberraRange(lat, lng) {
|
||||||
|
const distance = calculateDistance(CANBERRA_LAT, CANBERRA_LNG, lat, lng);
|
||||||
|
return distance <= MAX_DISTANCE_KM;
|
||||||
|
}
|
||||||
|
|
||||||
// Load locations when the page loads
|
// Load locations when the page loads
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Make sure Leaflet is fully loaded before initializing the map
|
||||||
|
if (typeof L !== 'undefined') {
|
||||||
|
// Initialize map centered on Canberra
|
||||||
|
initMap();
|
||||||
|
|
||||||
|
// Load kite flying locations
|
||||||
loadLocations();
|
loadLocations();
|
||||||
|
|
||||||
|
// Automatically get user's location on page load
|
||||||
|
getUserLocation();
|
||||||
|
} else {
|
||||||
|
console.error("Leaflet library not loaded. Attempting to retry in 500ms.");
|
||||||
|
// Try again after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
if (typeof L !== 'undefined') {
|
||||||
|
initMap();
|
||||||
|
loadLocations();
|
||||||
|
getUserLocation();
|
||||||
|
} else {
|
||||||
|
showNotification("Failed to load map. Please refresh the page.", "error");
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle get location button
|
||||||
|
document.getElementById('get-location-btn').addEventListener('click', getUserLocation);
|
||||||
|
|
||||||
// Handle form submission
|
// Handle form submission
|
||||||
document.getElementById('location-form').addEventListener('submit', (e) => {
|
document.getElementById('location-form').addEventListener('submit', (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const name = document.getElementById('location-name').value;
|
const lat = parseFloat(document.getElementById('location-lat').value);
|
||||||
|
const lng = parseFloat(document.getElementById('location-lng').value);
|
||||||
const description = document.getElementById('location-description').value;
|
const description = document.getElementById('location-description').value;
|
||||||
const rating = parseInt(document.getElementById('rating').value);
|
const rating = parseInt(document.getElementById('rating').value);
|
||||||
|
|
||||||
if (!name) {
|
if (isNaN(lat) || isNaN(lng)) {
|
||||||
showNotification('Please enter a location name', 'error');
|
showNotification('Please provide valid coordinates', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lat < -90 || lat > 90 || lng < -180 || lng > 180) {
|
||||||
|
showNotification('Please provide valid coordinates within range', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if location is within Canberra range
|
||||||
|
if (!isWithinCanberraRange(lat, lng)) {
|
||||||
|
showNotification(`Location must be within ${MAX_DISTANCE_KM}km of Canberra`, 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Submit the location
|
// Submit the location
|
||||||
submitLocation(name, description, rating);
|
submitLocation(lat, lng, description, rating);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Initialize the map
|
||||||
|
function initMap() {
|
||||||
|
// Use the global Canberra coordinates
|
||||||
|
map = L.map('map-container').setView([CANBERRA_LAT, CANBERRA_LNG], 12);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Initialize the input map
|
||||||
|
inputMap = L.map('input-map-container').setView([CANBERRA_LAT, CANBERRA_LNG], 12);
|
||||||
|
|
||||||
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
}).addTo(inputMap);
|
||||||
|
|
||||||
|
// Draw a 50km radius circle around Canberra to visualize the allowed area
|
||||||
|
const canberraCircle = L.circle([CANBERRA_LAT, CANBERRA_LNG], {
|
||||||
|
color: 'blue',
|
||||||
|
fillColor: '#3388ff',
|
||||||
|
fillOpacity: 0.1,
|
||||||
|
radius: MAX_DISTANCE_KM * 1000 // Convert to meters
|
||||||
|
}).addTo(map);
|
||||||
|
|
||||||
|
// Add the same circle to the input map
|
||||||
|
const inputCanberraCircle = L.circle([CANBERRA_LAT, CANBERRA_LNG], {
|
||||||
|
color: 'blue',
|
||||||
|
fillColor: '#3388ff',
|
||||||
|
fillOpacity: 0.1,
|
||||||
|
radius: MAX_DISTANCE_KM * 1000 // Convert to meters
|
||||||
|
}).addTo(inputMap);
|
||||||
|
|
||||||
|
// Add click handler to the input map to update selected location
|
||||||
|
inputMap.on('click', function(e) {
|
||||||
|
updateSelectedLocation(e.latlng.lat, e.latlng.lng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to set and update the selected location marker
|
||||||
|
function updateSelectedLocation(lat, lng) {
|
||||||
|
// Check if the location is within Canberra range
|
||||||
|
if (!isWithinCanberraRange(lat, lng)) {
|
||||||
|
showNotification(`Location must be within ${MAX_DISTANCE_KM}km of Canberra`, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update form inputs (hidden from user but needed for form submission)
|
||||||
|
document.getElementById('location-lat').value = lat.toFixed(6);
|
||||||
|
document.getElementById('location-lng').value = lng.toFixed(6);
|
||||||
|
|
||||||
|
// Update or create the marker on the input map
|
||||||
|
if (locationMarker) {
|
||||||
|
locationMarker.setLatLng([lat, lng]);
|
||||||
|
} else {
|
||||||
|
// Create a draggable marker with a different icon than the location markers
|
||||||
|
const markerIcon = L.icon({
|
||||||
|
iconUrl: 'https://raw.githubusercontent.com/pointhi/leaflet-color-markers/master/img/marker-icon-2x-blue.png',
|
||||||
|
shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/images/marker-shadow.png',
|
||||||
|
iconSize: [25, 41],
|
||||||
|
iconAnchor: [12, 41],
|
||||||
|
popupAnchor: [1, -34],
|
||||||
|
shadowSize: [41, 41]
|
||||||
|
});
|
||||||
|
|
||||||
|
locationMarker = L.marker([lat, lng], {
|
||||||
|
icon: markerIcon,
|
||||||
|
draggable: true
|
||||||
|
}).addTo(inputMap);
|
||||||
|
|
||||||
|
// Update form when marker is dragged
|
||||||
|
locationMarker.on('dragend', function(e) {
|
||||||
|
const position = locationMarker.getLatLng();
|
||||||
|
updateSelectedLocation(position.lat, position.lng);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the input map on the selected location
|
||||||
|
inputMap.setView([lat, lng], inputMap.getZoom());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user's current location
|
||||||
|
function getUserLocation() {
|
||||||
|
const locationBtn = document.getElementById('get-location-btn');
|
||||||
|
|
||||||
|
if (!navigator.geolocation) {
|
||||||
|
showNotification('Geolocation is not supported by your browser', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
locationBtn.disabled = true;
|
||||||
|
locationBtn.textContent = 'Getting location...';
|
||||||
|
|
||||||
|
navigator.geolocation.getCurrentPosition(
|
||||||
|
(position) => {
|
||||||
|
const lat = position.coords.latitude;
|
||||||
|
const lng = position.coords.longitude;
|
||||||
|
|
||||||
|
// Check if the user's location is within Canberra range
|
||||||
|
if (!isWithinCanberraRange(lat, lng)) {
|
||||||
|
showNotification(`Your current location is outside the ${MAX_DISTANCE_KM}km Canberra radius. Using Canberra center instead.`, 'warning');
|
||||||
|
// Default to Canberra center if user is too far
|
||||||
|
updateSelectedLocation(CANBERRA_LAT, CANBERRA_LNG);
|
||||||
|
} else {
|
||||||
|
// Update selected location marker and form inputs
|
||||||
|
updateSelectedLocation(lat, lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center input map on valid location
|
||||||
|
inputMap.setView([document.getElementById('location-lat').value, document.getElementById('location-lng').value], 15);
|
||||||
|
|
||||||
|
locationBtn.disabled = false;
|
||||||
|
locationBtn.textContent = 'Get My Location';
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
showNotification('Unable to get location: ' + error.message + '. Using Canberra center instead.', 'error');
|
||||||
|
// Default to Canberra center if geolocation fails
|
||||||
|
updateSelectedLocation(CANBERRA_LAT, CANBERRA_LNG);
|
||||||
|
locationBtn.disabled = false;
|
||||||
|
locationBtn.textContent = 'Get My Location';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Function to show notifications instead of alerts
|
// Function to show notifications instead of alerts
|
||||||
function showNotification(message, type = 'success') {
|
function showNotification(message, type = 'success') {
|
||||||
const notification = document.getElementById('notification');
|
const notification = document.getElementById('notification');
|
||||||
@@ -95,6 +299,10 @@
|
|||||||
fetch('/api/locations')
|
fetch('/api/locations')
|
||||||
.then(response => response.json())
|
.then(response => response.json())
|
||||||
.then(locations => {
|
.then(locations => {
|
||||||
|
// Clear existing markers
|
||||||
|
markers.forEach(marker => map.removeLayer(marker));
|
||||||
|
markers = [];
|
||||||
|
|
||||||
locationsList.innerHTML = '';
|
locationsList.innerHTML = '';
|
||||||
|
|
||||||
if (locations.length === 0) {
|
if (locations.length === 0) {
|
||||||
@@ -105,21 +313,26 @@
|
|||||||
// Sort locations with newest first
|
// Sort locations with newest first
|
||||||
locations.sort((a, b) => new Date(b.date_added) - new Date(a.date_added));
|
locations.sort((a, b) => new Date(b.date_added) - new Date(a.date_added));
|
||||||
|
|
||||||
locations.forEach(location => {
|
// Filter locations to only show last 48 hours
|
||||||
const date = new Date(location.date_added);
|
const cutoffTime = new Date();
|
||||||
const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
cutoffTime.setHours(cutoffTime.getHours() - 48);
|
||||||
|
|
||||||
const locationElement = document.createElement('div');
|
const recentLocations = locations.filter(location =>
|
||||||
locationElement.className = 'location-item';
|
new Date(location.date_added) > cutoffTime
|
||||||
locationElement.innerHTML = `
|
);
|
||||||
<h3>${location.name}</h3>
|
|
||||||
<p>${location.description || 'No notes provided'}</p>
|
|
||||||
<p class="date">Added on: ${formattedDate}</p>
|
|
||||||
<p class="rating">Rating: ${location.rating}</p>
|
|
||||||
`;
|
|
||||||
|
|
||||||
locationsList.appendChild(locationElement);
|
if (recentLocations.length === 0) {
|
||||||
|
locationsList.innerHTML = '<p>No locations added in the last 48 hours.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add markers for each location
|
||||||
|
recentLocations.forEach(location => {
|
||||||
|
addLocationToMap(location);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Update the list with summary
|
||||||
|
locationsList.innerHTML = `<p>Showing ${recentLocations.length} locations from the last 48 hours. Click on markers to see details.</p>`;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error('Error loading locations:', error);
|
console.error('Error loading locations:', error);
|
||||||
@@ -127,8 +340,42 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a location marker to the map
|
||||||
|
function addLocationToMap(location) {
|
||||||
|
// Rating determines color: 1=red, 2=orange, 3=yellow, 4=light green, 5=bright green
|
||||||
|
const colors = ['#ff0000', '#ff8800', '#ffff00', '#88ff00', '#00ff00'];
|
||||||
|
const color = colors[location.rating - 1] || '#ffaa00';
|
||||||
|
|
||||||
|
const date = new Date(location.date_added);
|
||||||
|
const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
|
||||||
|
|
||||||
|
// Create marker with custom color
|
||||||
|
const markerIcon = L.divIcon({
|
||||||
|
className: 'custom-marker',
|
||||||
|
html: `<div style="background-color: ${color};" class="marker-pin"></div>`,
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 30]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (location.latitude && location.longitude) {
|
||||||
|
const marker = L.marker([location.latitude, location.longitude], { icon: markerIcon }).addTo(map);
|
||||||
|
|
||||||
|
// Create popup content
|
||||||
|
const popupContent = `
|
||||||
|
<div class="popup-content">
|
||||||
|
<p class="rating">Rating: ${location.rating} stars</p>
|
||||||
|
<p>${location.description || 'No notes provided'}</p>
|
||||||
|
<p class="date">Added on: ${formattedDate}</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
marker.bindPopup(popupContent);
|
||||||
|
markers.push(marker);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Function to submit a new location
|
// Function to submit a new location
|
||||||
function submitLocation(name, description, rating) {
|
function submitLocation(latitude, longitude, description, rating) {
|
||||||
const submitButton = document.getElementById('submit-button');
|
const submitButton = document.getElementById('submit-button');
|
||||||
submitButton.disabled = true;
|
submitButton.disabled = true;
|
||||||
submitButton.textContent = 'Submitting...';
|
submitButton.textContent = 'Submitting...';
|
||||||
@@ -138,7 +385,7 @@
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ name, description, rating })
|
body: JSON.stringify({ latitude, longitude, description, rating })
|
||||||
})
|
})
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -151,7 +398,6 @@
|
|||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// Clear form
|
// Clear form
|
||||||
document.getElementById('location-name').value = '';
|
|
||||||
document.getElementById('location-description').value = '';
|
document.getElementById('location-description').value = '';
|
||||||
document.getElementById('rating').value = 3;
|
document.getElementById('rating').value = 3;
|
||||||
document.getElementById('rating-value').textContent = 3;
|
document.getElementById('rating-value').textContent = 3;
|
||||||
|
|||||||
Reference in New Issue
Block a user