test
This commit is contained in:
448
nocodb-map-viewer/app/public/js/map.js
Normal file
448
nocodb-map-viewer/app/public/js/map.js
Normal file
@@ -0,0 +1,448 @@
|
||||
// Global configuration
|
||||
const CONFIG = {
|
||||
DEFAULT_LAT: parseFloat(document.querySelector('meta[name="default-lat"]')?.content) || 53.5461,
|
||||
DEFAULT_LNG: parseFloat(document.querySelector('meta[name="default-lng"]')?.content) || -113.4938,
|
||||
DEFAULT_ZOOM: parseInt(document.querySelector('meta[name="default-zoom"]')?.content) || 11,
|
||||
REFRESH_INTERVAL: 30000, // 30 seconds
|
||||
MAX_ZOOM: 19,
|
||||
MIN_ZOOM: 2
|
||||
};
|
||||
|
||||
// Application state
|
||||
let map = null;
|
||||
let markers = [];
|
||||
let userLocationMarker = null;
|
||||
let isAddingLocation = false;
|
||||
let refreshInterval = null;
|
||||
|
||||
// Initialize application when DOM is loaded
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initializeMap();
|
||||
loadLocations();
|
||||
setupEventListeners();
|
||||
checkConfiguration();
|
||||
|
||||
// Set up auto-refresh
|
||||
refreshInterval = setInterval(loadLocations, CONFIG.REFRESH_INTERVAL);
|
||||
});
|
||||
|
||||
// Initialize Leaflet map
|
||||
function initializeMap() {
|
||||
// Create map instance
|
||||
map = L.map('map', {
|
||||
center: [CONFIG.DEFAULT_LAT, CONFIG.DEFAULT_LNG],
|
||||
zoom: CONFIG.DEFAULT_ZOOM,
|
||||
zoomControl: true,
|
||||
attributionControl: true
|
||||
});
|
||||
|
||||
// Add OpenStreetMap tiles
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
|
||||
maxZoom: CONFIG.MAX_ZOOM,
|
||||
minZoom: CONFIG.MIN_ZOOM
|
||||
}).addTo(map);
|
||||
|
||||
// Add scale control
|
||||
L.control.scale({
|
||||
position: 'bottomleft',
|
||||
metric: true,
|
||||
imperial: false
|
||||
}).addTo(map);
|
||||
|
||||
// Hide loading overlay
|
||||
document.getElementById('loading').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Set up event listeners
|
||||
function setupEventListeners() {
|
||||
// Geolocation button
|
||||
document.getElementById('geolocate-btn').addEventListener('click', handleGeolocation);
|
||||
|
||||
// Add location button
|
||||
document.getElementById('add-location-btn').addEventListener('click', toggleAddLocation);
|
||||
|
||||
// Refresh button
|
||||
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||||
showStatus('Refreshing locations...', 'info');
|
||||
loadLocations();
|
||||
});
|
||||
|
||||
// Fullscreen button
|
||||
document.getElementById('fullscreen-btn').addEventListener('click', toggleFullscreen);
|
||||
|
||||
// Form submission
|
||||
document.getElementById('location-form').addEventListener('submit', handleLocationSubmit);
|
||||
|
||||
// Map click handler for adding locations
|
||||
map.on('click', handleMapClick);
|
||||
}
|
||||
|
||||
// Check API configuration
|
||||
async function checkConfiguration() {
|
||||
try {
|
||||
const response = await fetch('/api/config-check');
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.configured) {
|
||||
showStatus('Warning: API not fully configured. Check your .env file.', 'warning');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Configuration check failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load locations from API
|
||||
async function loadLocations() {
|
||||
try {
|
||||
const response = await fetch('/api/locations');
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
displayLocations(data.locations);
|
||||
updateLocationCount(data.count);
|
||||
} else {
|
||||
throw new Error(data.error || 'Failed to load locations');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading locations:', error);
|
||||
showStatus('Failed to load locations. Check your connection.', 'error');
|
||||
updateLocationCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Display locations on map
|
||||
function displayLocations(locations) {
|
||||
// Clear existing markers
|
||||
markers.forEach(marker => map.removeLayer(marker));
|
||||
markers = [];
|
||||
|
||||
// Add new markers
|
||||
locations.forEach(location => {
|
||||
if (location.latitude && location.longitude) {
|
||||
const marker = createLocationMarker(location);
|
||||
markers.push(marker);
|
||||
}
|
||||
});
|
||||
|
||||
// Fit map to show all markers if there are any
|
||||
if (markers.length > 0) {
|
||||
const group = new L.featureGroup(markers);
|
||||
map.fitBounds(group.getBounds().pad(0.1));
|
||||
}
|
||||
}
|
||||
|
||||
// Create marker for location
|
||||
function createLocationMarker(location) {
|
||||
const marker = L.marker([location.latitude, location.longitude], {
|
||||
title: location.title || 'Location',
|
||||
riseOnHover: true
|
||||
}).addTo(map);
|
||||
|
||||
// Create popup content
|
||||
const popupContent = createPopupContent(location);
|
||||
marker.bindPopup(popupContent);
|
||||
|
||||
return marker;
|
||||
}
|
||||
|
||||
// Create popup content for marker
|
||||
function createPopupContent(location) {
|
||||
let content = '<div class="popup-content">';
|
||||
|
||||
// Handle name from either 'title' field or 'First Name'/'Last Name' combination
|
||||
let displayName = '';
|
||||
if (location.title) {
|
||||
displayName = location.title;
|
||||
} else if (location['First Name'] || location['Last Name']) {
|
||||
const firstName = location['First Name'] || '';
|
||||
const lastName = location['Last Name'] || '';
|
||||
displayName = `${firstName} ${lastName}`.trim();
|
||||
}
|
||||
|
||||
if (displayName) {
|
||||
content += `<h3>${escapeHtml(displayName)}</h3>`;
|
||||
}
|
||||
|
||||
if (location.description) {
|
||||
content += `<p>${escapeHtml(location.description)}</p>`;
|
||||
}
|
||||
|
||||
if (location.category) {
|
||||
content += `<p><strong>Category:</strong> ${escapeHtml(location.category)}</p>`;
|
||||
}
|
||||
|
||||
if (location['Email']) {
|
||||
content += `<p><strong>Email:</strong> ${escapeHtml(location['Email'])}</p>`;
|
||||
}
|
||||
|
||||
if (location['Unit Number']) {
|
||||
content += `<p><strong>Unit:</strong> ${escapeHtml(location['Unit Number'])}</p>`;
|
||||
}
|
||||
|
||||
content += '<div class="popup-meta">';
|
||||
content += `<p><strong>Coordinates:</strong> ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}</p>`;
|
||||
|
||||
if (location.created_at) {
|
||||
const date = new Date(location.created_at);
|
||||
content += `<p><strong>Added:</strong> ${date.toLocaleDateString()}</p>`;
|
||||
}
|
||||
|
||||
content += '</div>';
|
||||
content += '</div>';
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// Handle geolocation
|
||||
function handleGeolocation() {
|
||||
if (!navigator.geolocation) {
|
||||
showStatus('Geolocation is not supported by your browser', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
showStatus('Getting your location...', 'info');
|
||||
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
const { latitude, longitude, accuracy } = position.coords;
|
||||
|
||||
// Center map on user location
|
||||
map.setView([latitude, longitude], 15);
|
||||
|
||||
// Remove existing user marker
|
||||
if (userLocationMarker) {
|
||||
map.removeLayer(userLocationMarker);
|
||||
}
|
||||
|
||||
// Add user location marker
|
||||
userLocationMarker = L.marker([latitude, longitude], {
|
||||
icon: L.divIcon({
|
||||
html: '<div style="background-color: #2c5aa0; width: 20px; height: 20px; border-radius: 50%; border: 3px solid white; box-shadow: 0 2px 5px rgba(0,0,0,0.3);"></div>',
|
||||
className: 'user-location-marker',
|
||||
iconSize: [20, 20],
|
||||
iconAnchor: [10, 10]
|
||||
}),
|
||||
title: 'Your location'
|
||||
}).addTo(map);
|
||||
|
||||
// Add accuracy circle
|
||||
L.circle([latitude, longitude], {
|
||||
radius: accuracy,
|
||||
color: '#2c5aa0',
|
||||
fillColor: '#2c5aa0',
|
||||
fillOpacity: 0.1,
|
||||
weight: 1
|
||||
}).addTo(map);
|
||||
|
||||
showStatus(`Location found (±${Math.round(accuracy)}m accuracy)`, 'success');
|
||||
},
|
||||
(error) => {
|
||||
let message = 'Unable to get your location';
|
||||
|
||||
switch (error.code) {
|
||||
case error.PERMISSION_DENIED:
|
||||
message = 'Location permission denied';
|
||||
break;
|
||||
case error.POSITION_UNAVAILABLE:
|
||||
message = 'Location information unavailable';
|
||||
break;
|
||||
case error.TIMEOUT:
|
||||
message = 'Location request timed out';
|
||||
break;
|
||||
}
|
||||
|
||||
showStatus(message, 'error');
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 10000,
|
||||
maximumAge: 0
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Toggle add location mode
|
||||
function toggleAddLocation() {
|
||||
isAddingLocation = !isAddingLocation;
|
||||
|
||||
const btn = document.getElementById('add-location-btn');
|
||||
const crosshair = document.getElementById('crosshair');
|
||||
|
||||
if (isAddingLocation) {
|
||||
btn.textContent = '✕ Cancel';
|
||||
btn.classList.remove('btn-success');
|
||||
btn.classList.add('btn-secondary');
|
||||
crosshair.classList.remove('hidden');
|
||||
map.getContainer().style.cursor = 'crosshair';
|
||||
} else {
|
||||
btn.textContent = '➕ Add Location Here';
|
||||
btn.classList.remove('btn-secondary');
|
||||
btn.classList.add('btn-success');
|
||||
crosshair.classList.add('hidden');
|
||||
map.getContainer().style.cursor = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Handle map click
|
||||
function handleMapClick(e) {
|
||||
if (!isAddingLocation) return;
|
||||
|
||||
const { lat, lng } = e.latlng;
|
||||
|
||||
// Toggle off add location mode
|
||||
toggleAddLocation();
|
||||
|
||||
// Show modal with coordinates
|
||||
showAddLocationModal(lat, lng);
|
||||
}
|
||||
|
||||
// Show add location modal
|
||||
function showAddLocationModal(lat, lng) {
|
||||
const modal = document.getElementById('add-modal');
|
||||
const latInput = document.getElementById('location-lat');
|
||||
const lngInput = document.getElementById('location-lng');
|
||||
|
||||
// Set coordinates
|
||||
latInput.value = lat.toFixed(8);
|
||||
lngInput.value = lng.toFixed(8);
|
||||
|
||||
// Clear other fields
|
||||
document.getElementById('first-name').value = '';
|
||||
document.getElementById('last-name').value = '';
|
||||
document.getElementById('location-email').value = '';
|
||||
document.getElementById('location-unit').value = '';
|
||||
|
||||
// Show modal
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
// Focus on first name input
|
||||
setTimeout(() => {
|
||||
document.getElementById('first-name').focus();
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// Close modal
|
||||
function closeModal() {
|
||||
document.getElementById('add-modal').classList.add('hidden');
|
||||
}
|
||||
|
||||
// Handle location form submission
|
||||
async function handleLocationSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const data = Object.fromEntries(formData);
|
||||
|
||||
// Validate required fields - either first name or last name should be provided
|
||||
if ((!data['First Name'] || !data['First Name'].trim()) &&
|
||||
(!data['Last Name'] || !data['Last Name'].trim())) {
|
||||
showStatus('Either First Name or Last Name is required', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/locations', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (response.ok && result.success) {
|
||||
showStatus('Location added successfully!', 'success');
|
||||
closeModal();
|
||||
|
||||
// Reload locations
|
||||
loadLocations();
|
||||
|
||||
// Center map on new location
|
||||
map.setView([data.latitude, data.longitude], map.getZoom());
|
||||
} else {
|
||||
throw new Error(result.error || 'Failed to add location');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error adding location:', error);
|
||||
showStatus(error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle fullscreen
|
||||
function toggleFullscreen() {
|
||||
const app = document.getElementById('app');
|
||||
const btn = document.getElementById('fullscreen-btn');
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
app.requestFullscreen().then(() => {
|
||||
app.classList.add('fullscreen');
|
||||
btn.textContent = '✕ Exit Fullscreen';
|
||||
|
||||
// Invalidate map size after transition
|
||||
setTimeout(() => map.invalidateSize(), 300);
|
||||
}).catch(err => {
|
||||
showStatus('Unable to enter fullscreen', 'error');
|
||||
});
|
||||
} else {
|
||||
document.exitFullscreen().then(() => {
|
||||
app.classList.remove('fullscreen');
|
||||
btn.textContent = '⛶ Fullscreen';
|
||||
|
||||
// Invalidate map size after transition
|
||||
setTimeout(() => map.invalidateSize(), 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update location count
|
||||
function updateLocationCount(count) {
|
||||
const countElement = document.getElementById('location-count');
|
||||
countElement.textContent = `${count} location${count !== 1 ? 's' : ''}`;
|
||||
}
|
||||
|
||||
// Show status message
|
||||
function showStatus(message, type = 'info') {
|
||||
const container = document.getElementById('status-container');
|
||||
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.className = `status-message ${type}`;
|
||||
messageDiv.textContent = message;
|
||||
|
||||
container.appendChild(messageDiv);
|
||||
|
||||
// Auto-remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
messageDiv.remove();
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
// Escape HTML to prevent XSS
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Handle window resize
|
||||
window.addEventListener('resize', () => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
|
||||
// Clean up on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (refreshInterval) {
|
||||
clearInterval(refreshInterval);
|
||||
}
|
||||
});
|
||||
|
||||
// Make closeModal function global for onclick handler
|
||||
window.closeModal = closeModal;
|
||||
Reference in New Issue
Block a user