Files
kite-watch/templates/index.html
Nathan Woodburn b856448247
All checks were successful
Build Docker / BuildImage (push) Successful in 41s
feat: Add map to make site easier to use
2025-05-26 16:56:53 +10:00

426 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Kite Watch</title>
<link rel="icon" href="/assets/img/favicon.png" type="image/png">
<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>
<body>
<div class="header">
<h1>Kite Watch</h1>
<p class="tagline">Find perfect places to fly your kite!</p>
</div>
<div class="container">
<div class="card">
<h2>Add a Kite Flying Location</h2>
<div id="notification" class="notification hidden"></div>
<form id="location-form" novalidate>
<div class="form-group">
<label>Select Your Location</label>
<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 class="form-group">
<label for="location-description">Notes</label>
<textarea id="location-description" placeholder="Wind conditions, accessibility, tips, etc."></textarea>
</div>
<div class="form-group">
<label for="rating">Rating (1-5):</label>
<div class="rating-container">
<input type="range" id="rating" name="rating" min="1" max="5" value="3" step="1" class="form-control">
<span id="rating-value">3</span>
</div>
</div>
<button type="submit" id="submit-button">Submit Location</button>
</form>
</div>
<div class="card">
<h2>Great Places to Fly Kites in Canberra</h2>
<div id="map-container"></div>
<div id="locations-list">
<p class="loading">Loading locations...</p>
</div>
</div>
</div>
<footer class="footer">
<div class="footer-content">
<p>Created by <a href="https://nathan.woodburn.au" target="_blank">Nathan.Woodburn/</a></p>
</div>
</footer>
<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
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();
// 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
document.getElementById('location-form').addEventListener('submit', (e) => {
e.preventDefault();
const lat = parseFloat(document.getElementById('location-lat').value);
const lng = parseFloat(document.getElementById('location-lng').value);
const description = document.getElementById('location-description').value;
const rating = parseInt(document.getElementById('rating').value);
if (isNaN(lat) || isNaN(lng)) {
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;
}
// Submit the location
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: '&copy; <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: '&copy; <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 showNotification(message, type = 'success') {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.className = `notification ${type}`;
// Auto hide after 5 seconds
setTimeout(() => {
notification.className = 'notification hidden';
}, 5000);
}
// Function to load locations from the API
function loadLocations() {
const locationsList = document.getElementById('locations-list');
fetch('/api/locations')
.then(response => response.json())
.then(locations => {
// Clear existing markers
markers.forEach(marker => map.removeLayer(marker));
markers = [];
locationsList.innerHTML = '';
if (locations.length === 0) {
locationsList.innerHTML = '<p>No locations added yet. Be the first!</p>';
return;
}
// Sort locations with newest first
locations.sort((a, b) => new Date(b.date_added) - new Date(a.date_added));
// Filter locations to only show last 48 hours
const cutoffTime = new Date();
cutoffTime.setHours(cutoffTime.getHours() - 48);
const recentLocations = locations.filter(location =>
new Date(location.date_added) > cutoffTime
);
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 => {
console.error('Error loading locations:', error);
locationsList.innerHTML = '<p>Error loading locations. Please try again.</p>';
});
}
// 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 submitLocation(latitude, longitude, description, rating) {
const submitButton = document.getElementById('submit-button');
submitButton.disabled = true;
submitButton.textContent = 'Submitting...';
fetch('/api/locations', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ latitude, longitude, description, rating })
})
.then(response => {
if (!response.ok) {
// Parse the error response to get the specific error message
return response.json().then(errorData => {
throw new Error(errorData.error || 'Failed to submit location');
});
}
return response.json();
})
.then(data => {
// Clear form
document.getElementById('location-description').value = '';
document.getElementById('rating').value = 3;
document.getElementById('rating-value').textContent = 3;
// Reload locations
loadLocations();
showNotification('Location added successfully!');
})
.catch(error => {
console.error('Error submitting location:', error);
// Display the specific error message from the server
showNotification(error.message || 'Failed to add location. Please try again.', 'error');
})
.finally(() => {
submitButton.disabled = false;
submitButton.textContent = 'Submit Location';
});
}
document.getElementById('rating').addEventListener('input', function() {
document.getElementById('rating-value').textContent = this.value;
});
</script>
</body>
</html>