Files
thrillwiki_django_no_react/templates/moderation/partials/location_widget.html
pacnpal dfe6194039 ok
2024-11-14 04:15:24 +00:00

415 lines
18 KiB
HTML

{% load static %}
<style>
/* Ensure map container and its elements stay below other UI elements */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
z-index: 1 !important;
}
.leaflet-control {
z-index: 2 !important;
}
</style>
<div class="p-4 bg-white border rounded-lg dark:bg-gray-800 border-gray-200/50 dark:border-gray-700/50">
<h3 class="mb-4 text-lg font-semibold text-gray-900 dark:text-gray-300">Location</h3>
<div class="location-widget" id="locationWidget-{{ submission.id }}">
{# Search Form #}
<div class="relative mb-4">
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Search Location
</label>
<input type="text"
id="locationSearch-{{ submission.id }}"
class="relative w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
placeholder="Search for a location..."
autocomplete="off"
style="z-index: 10;">
<div id="searchResults-{{ submission.id }}"
style="position: absolute; top: 100%; left: 0; right: 0; z-index: 1000;"
class="hidden w-full mt-1 overflow-auto bg-white border rounded-md shadow-lg max-h-60 dark:bg-gray-700 dark:border-gray-600">
</div>
</div>
{# Map Container #}
<div class="relative mb-4" style="z-index: 1;">
<div id="locationMap-{{ submission.id }}"
class="h-[300px] w-full rounded-lg border border-gray-300 dark:border-gray-600"></div>
</div>
{# Location Form Fields #}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Street Address
</label>
<input type="text"
name="street_address"
id="streetAddress-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.street_address }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
City
</label>
<input type="text"
name="city"
id="city-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.city }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
State/Region
</label>
<input type="text"
name="state"
id="state-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.state }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Country
</label>
<input type="text"
name="country"
id="country-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.country }}">
</div>
<div>
<label class="block mb-2 text-sm font-medium text-gray-700 dark:text-gray-300">
Postal Code
</label>
<input type="text"
name="postal_code"
id="postalCode-{{ submission.id }}"
class="w-full px-4 py-2 text-gray-900 bg-white border rounded-lg dark:text-gray-300 dark:bg-gray-700 dark:border-gray-600 form-input"
value="{{ submission.changes.postal_code }}">
</div>
</div>
{# Hidden Coordinate Fields #}
<div class="hidden">
<input type="hidden" name="latitude" id="latitude-{{ submission.id }}" value="{{ submission.changes.latitude }}">
<input type="hidden" name="longitude" id="longitude-{{ submission.id }}" value="{{ submission.changes.longitude }}">
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
let maps = {};
let markers = {};
const searchInput = document.getElementById('locationSearch-{{ submission.id }}');
const searchResults = document.getElementById('searchResults-{{ submission.id }}');
let searchTimeout;
// Initialize form fields with existing values
const fields = {
city: '{{ submission.changes.city|default:"" }}',
state: '{{ submission.changes.state|default:"" }}',
country: '{{ submission.changes.country|default:"" }}',
postal_code: '{{ submission.changes.postal_code|default:"" }}',
street_address: '{{ submission.changes.street_address|default:"" }}',
latitude: '{{ submission.changes.latitude|default:"" }}',
longitude: '{{ submission.changes.longitude|default:"" }}'
};
Object.entries(fields).forEach(([field, value]) => {
const element = document.getElementById(`${field}-{{ submission.id }}`);
if (element) {
element.value = value;
}
});
// Set initial search input value if location exists
if (fields.street_address || fields.city) {
const parts = [
fields.street_address,
fields.city,
fields.state,
fields.country
].filter(Boolean);
searchInput.value = parts.join(', ');
}
function normalizeCoordinate(value, maxDigits, decimalPlaces) {
if (!value) return null;
try {
const rounded = Number(value).toFixed(decimalPlaces);
const strValue = rounded.replace('.', '').replace('-', '');
const strippedValue = strValue.replace(/0+$/, '');
if (strippedValue.length > maxDigits) {
return Number(Number(value).toFixed(decimalPlaces - 1));
}
return rounded;
} catch (error) {
console.error('Coordinate normalization failed:', error);
return null;
}
}
function validateCoordinates(lat, lng) {
const normalizedLat = normalizeCoordinate(lat, 9, 6);
const normalizedLng = normalizeCoordinate(lng, 10, 6);
if (normalizedLat === null || normalizedLng === null) {
throw new Error('Invalid coordinate format');
}
const parsedLat = parseFloat(normalizedLat);
const parsedLng = parseFloat(normalizedLng);
if (parsedLat < -90 || parsedLat > 90) {
throw new Error('Latitude must be between -90 and 90 degrees.');
}
if (parsedLng < -180 || parsedLng > 180) {
throw new Error('Longitude must be between -180 and 180 degrees.');
}
return { lat: normalizedLat, lng: normalizedLng };
}
function initMap() {
const submissionId = '{{ submission.id }}';
const mapId = `locationMap-${submissionId}`;
const mapContainer = document.getElementById(mapId);
if (!mapContainer) {
console.error(`Map container ${mapId} not found`);
return;
}
// If map already exists, remove it
if (maps[submissionId]) {
maps[submissionId].remove();
delete maps[submissionId];
delete markers[submissionId];
}
// Create new map
maps[submissionId] = L.map(mapId);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap contributors'
}).addTo(maps[submissionId]);
// Initialize with existing coordinates if available
const initialLat = fields.latitude;
const initialLng = fields.longitude;
if (initialLat && initialLng) {
try {
const normalized = validateCoordinates(initialLat, initialLng);
maps[submissionId].setView([normalized.lat, normalized.lng], 13);
addMarker(normalized.lat, normalized.lng);
} catch (error) {
console.error('Invalid initial coordinates:', error);
maps[submissionId].setView([0, 0], 2);
}
} else {
maps[submissionId].setView([0, 0], 2);
}
// Handle map clicks
maps[submissionId].on('click', async function(e) {
try {
const normalized = validateCoordinates(e.latlng.lat, e.latlng.lng);
const response = await fetch(`/parks/search/reverse-geocode/?lat=${normalized.lat}&lon=${normalized.lng}`);
if (!response.ok) {
throw new Error('Geocoding request failed');
}
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateLocation(normalized.lat, normalized.lng, data);
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
});
}
function addMarker(lat, lng) {
const submissionId = '{{ submission.id }}';
if (markers[submissionId]) {
markers[submissionId].remove();
}
markers[submissionId] = L.marker([lat, lng]).addTo(maps[submissionId]);
maps[submissionId].setView([lat, lng], 13);
}
function updateLocation(lat, lng, data) {
try {
const normalized = validateCoordinates(lat, lng);
const submissionId = '{{ submission.id }}';
// Update coordinates
document.getElementById(`latitude-${submissionId}`).value = normalized.lat;
document.getElementById(`longitude-${submissionId}`).value = normalized.lng;
// Update marker
addMarker(normalized.lat, normalized.lng);
// Update form fields with English names where available
const address = data.address || {};
document.getElementById(`streetAddress-${submissionId}`).value =
`${address.house_number || ''} ${address.road || address.street || ''}`.trim() || '';
document.getElementById(`city-${submissionId}`).value =
address.city || address.town || address.village || '';
document.getElementById(`state-${submissionId}`).value =
address.state || address.region || '';
document.getElementById(`country-${submissionId}`).value = address.country || '';
document.getElementById(`postalCode-${submissionId}`).value = address.postcode || '';
// Update search input
const locationString = [
document.getElementById(`streetAddress-${submissionId}`).value,
document.getElementById(`city-${submissionId}`).value,
document.getElementById(`state-${submissionId}`).value,
document.getElementById(`country-${submissionId}`).value
].filter(Boolean).join(', ');
searchInput.value = locationString;
} catch (error) {
console.error('Location update failed:', error);
alert(error.message || 'Failed to update location. Please try again.');
}
}
function selectLocation(result) {
if (!result) return;
try {
const lat = parseFloat(result.lat);
const lon = parseFloat(result.lon);
if (isNaN(lat) || isNaN(lon)) {
throw new Error('Invalid coordinates in search result');
}
const normalized = validateCoordinates(lat, lon);
// Create a normalized address object
const address = {
name: result.display_name || result.name || '',
address: {
house_number: result.address ? result.address.house_number : '',
road: result.address ? (result.address.road || result.address.street) : '',
city: result.address ? (result.address.city || result.address.town || result.address.village) : '',
state: result.address ? (result.address.state || result.address.region) : '',
country: result.address ? result.address.country : '',
postcode: result.address ? result.address.postcode : ''
}
};
updateLocation(normalized.lat, normalized.lng, address);
searchResults.classList.add('hidden');
searchInput.value = address.name;
} catch (error) {
console.error('Location selection failed:', error);
alert(error.message || 'Failed to select location. Please try again.');
}
}
// Handle location search
searchInput.addEventListener('input', function() {
clearTimeout(searchTimeout);
const query = this.value.trim();
if (!query) {
searchResults.classList.add('hidden');
return;
}
searchTimeout = setTimeout(async function() {
try {
const response = await fetch(`/parks/search/location/?q=${encodeURIComponent(query)}`);
if (!response.ok) {
throw new Error('Search request failed');
}
const data = await response.json();
if (data.results && data.results.length > 0) {
const resultsHtml = data.results.map((result, index) => `
<div class="p-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-600"
data-result-index="${index}">
<div class="font-medium text-gray-900 dark:text-white">${result.display_name || result.name || ''}</div>
<div class="text-sm text-gray-500 dark:text-gray-400">
${(result.address && result.address.city) ? result.address.city + ', ' : ''}${(result.address && result.address.country) || ''}
</div>
</div>
`).join('');
searchResults.innerHTML = resultsHtml;
searchResults.classList.remove('hidden');
// Store results data
searchResults.dataset.results = JSON.stringify(data.results);
// Add click handlers
searchResults.querySelectorAll('[data-result-index]').forEach(el => {
el.addEventListener('click', function() {
const results = JSON.parse(searchResults.dataset.results);
const result = results[this.dataset.resultIndex];
selectLocation(result);
});
});
} else {
searchResults.innerHTML = '<div class="p-2 text-gray-500 dark:text-gray-400">No results found</div>';
searchResults.classList.remove('hidden');
}
} catch (error) {
console.error('Search failed:', error);
searchResults.innerHTML = '<div class="p-2 text-red-500 dark:text-red-400">Search failed. Please try again.</div>';
searchResults.classList.remove('hidden');
}
}, 300);
});
// Hide search results when clicking outside
document.addEventListener('click', function(e) {
if (!searchResults.contains(e.target) && e.target !== searchInput) {
searchResults.classList.add('hidden');
}
});
// Initialize map when the element becomes visible
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer && window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
observer.disconnect();
}
}
});
});
const mapContainer = document.getElementById(`locationMap-{{ submission.id }}`);
if (mapContainer) {
observer.observe(mapContainer.parentElement.parentElement, { attributes: true });
// Also initialize immediately if the container is already visible
if (window.getComputedStyle(mapContainer).display !== 'none') {
initMap();
}
}
});
</script>