258 lines
8.6 KiB
JavaScript
258 lines
8.6 KiB
JavaScript
let demoData = [];
|
|
let map, markers = [], polylines = [], pins = [], pinMode = false;
|
|
let currentTheme = 'light';
|
|
|
|
// Initialize
|
|
function initMap() {
|
|
map = L.map('map').setView([34.0522, -118.2437], 12);
|
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
|
attribution: '© OpenStreetMap contributors'
|
|
}).addTo(map);
|
|
|
|
loadDemoData();
|
|
setupDragAndDrop();
|
|
}
|
|
|
|
async function loadDemoData() {
|
|
try {
|
|
const res = await fetch('/api/demo-data');
|
|
demoData = await res.json();
|
|
visualizeData(demoData);
|
|
} catch (err) {
|
|
console.error('Demo data load failed:', err);
|
|
// Fallback
|
|
demoData = [
|
|
{ lat: 34.0522, lng: -118.2437, time: '2025-11-05T09:00:00' },
|
|
{ lat: 34.0740, lng: -118.2706, time: '2025-11-05T10:15:00' },
|
|
{ lat: 34.0195, lng: -118.2913, time: '2025-11-05T14:30:00' }
|
|
];
|
|
visualizeData(demoData);
|
|
}
|
|
}
|
|
|
|
function visualizeData(data) {
|
|
// Clear map
|
|
markers.forEach(m => map.removeLayer(m));
|
|
polylines.forEach(p => map.removeLayer(p));
|
|
markers = []; polylines = [];
|
|
|
|
data.forEach(point => {
|
|
const marker = L.marker([point.lat, point.lng]).addTo(map)
|
|
.bindPopup(`Lat: ${point.lat.toFixed(4)}, Lng: ${point.lng.toFixed(4)}<br>Time: ${point.time}`);
|
|
markers.push(marker);
|
|
});
|
|
|
|
if (data.length > 1) {
|
|
const path = data.map(p => [p.lat, p.lng]);
|
|
L.polyline(path, {color: 'blue', weight: 3}).addTo(map);
|
|
}
|
|
|
|
updateTimeline();
|
|
updatePinsList();
|
|
}
|
|
|
|
// Fixed Upload Function (No Event Dependency, Strict Prevention)
|
|
async function uploadFile(e) {
|
|
if (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}
|
|
|
|
const fileInput = document.getElementById('fileUpload');
|
|
const file = fileInput.files[0];
|
|
if (!file) {
|
|
alert('Select a file!');
|
|
return;
|
|
}
|
|
|
|
const formData = new FormData();
|
|
formData.append('file', file);
|
|
|
|
const statusEl = document.getElementById('uploadStatus');
|
|
statusEl.textContent = 'Uploading...';
|
|
statusEl.style.color = '#007bff';
|
|
|
|
try {
|
|
const res = await fetch('/api/upload', {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'same-origin' // Ensure no CORS issues
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const errorText = await res.text();
|
|
throw new Error(`Server responded with ${res.status}: ${errorText}`);
|
|
}
|
|
|
|
const data = await res.json();
|
|
|
|
if (data.success) {
|
|
demoData = data.points;
|
|
visualizeData(demoData);
|
|
statusEl.textContent = `Loaded ${data.points.length} points!`;
|
|
statusEl.style.color = 'green';
|
|
runVisitDetection();
|
|
fileInput.value = ''; // Reset input
|
|
} else {
|
|
throw new Error(data.error || 'Unknown upload error');
|
|
}
|
|
} catch (err) {
|
|
console.error('Upload failed:', err);
|
|
statusEl.textContent = `Error: ${err.message}`;
|
|
statusEl.style.color = 'red';
|
|
}
|
|
}
|
|
|
|
// Drag & Drop Setup (Full Map Coverage)
|
|
function setupDragAndDrop() {
|
|
const sidebarDrop = document.querySelector('.section:nth-child(2) > div'); // Sidebar drop area
|
|
const mapEl = document.getElementById('map');
|
|
|
|
[mapEl, sidebarDrop].forEach(el => {
|
|
el.addEventListener('dragover', handleDragOver);
|
|
el.addEventListener('dragleave', handleDragLeave);
|
|
el.addEventListener('drop', handleDrop);
|
|
});
|
|
}
|
|
|
|
function handleDragOver(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.dataTransfer.dropEffect = 'copy';
|
|
e.currentTarget.style.background = '#e3f2fd'; // Visual feedback
|
|
}
|
|
|
|
function handleDragLeave(e) {
|
|
e.currentTarget.style.background = '';
|
|
}
|
|
|
|
function handleDrop(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
e.currentTarget.style.background = '';
|
|
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
document.getElementById('fileUpload').files = files;
|
|
uploadFile();
|
|
}
|
|
}
|
|
|
|
// Rest of the functions (pins, analytics, etc.) remain the same as previous
|
|
function togglePinMode() {
|
|
pinMode = !pinMode;
|
|
if (pinMode) {
|
|
map.on('click', addPin);
|
|
alert('Pin mode on - click map to add pins!');
|
|
} else {
|
|
map.off('click', addPin);
|
|
alert('Pin mode off.');
|
|
}
|
|
}
|
|
|
|
async function addPin(e) {
|
|
const color = prompt('Pin color?') || 'red';
|
|
const comment = prompt('Comment?') || '';
|
|
|
|
let address = 'Loading...';
|
|
try {
|
|
const res = await fetch(`/api/reverse-geocode/${e.latlng.lat}/${e.latlng.lng}`);
|
|
const geoData = await res.json();
|
|
address = geoData.address || 'Unknown';
|
|
} catch {}
|
|
|
|
const pin = { id: Date.now(), lat: e.latlng.lat, lng: e.latlng.lng, color, comment, address, visible: true };
|
|
pins.push(pin);
|
|
|
|
L.circleMarker([pin.lat, pin.lng], { color: pin.color, fillColor: pin.color, fillOpacity: 0.7, radius: 8 })
|
|
.addTo(map)
|
|
.bindPopup(`Pin: ${pin.comment}<br>Address: ${address}<br>Coords: ${pin.lat.toFixed(4)}, ${pin.lng.toFixed(4)}`);
|
|
|
|
updatePinsList();
|
|
}
|
|
|
|
function updatePinsList() {
|
|
document.getElementById('pinsList').innerHTML = pins.map(p => `
|
|
<div class="pin-item" style="display: flex; align-items: center; padding: 10px; border-bottom: 1px solid #eee;">
|
|
<div class="pin-color" style="width: 20px; height: 20px; border-radius: 50%; background: ${p.color}; margin-right: 10px;"></div>
|
|
${p.comment || 'Pin'} - ${p.address} (${p.lat.toFixed(4)}, ${p.lng.toFixed(4)})
|
|
<button class="btn btn-secondary" style="margin-left: auto; padding: 5px 10px;" onclick="togglePinVisibility(${p.id})">Toggle</button>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function togglePinVisibility(id) {
|
|
const pin = pins.find(p => p.id == id);
|
|
if (pin) pin.visible = !pin.visible;
|
|
updatePinsList();
|
|
}
|
|
|
|
function runVisitDetection() {
|
|
if (demoData.length === 0) return alert('Load data first!');
|
|
const visits = demoData.filter((_, i) => i % 2 === 0);
|
|
visits.forEach(v => L.circle([v.lat, v.lng], {radius: 100, color: 'green', fillOpacity: 0.3}).addTo(map));
|
|
document.getElementById('analyticsInfo').innerHTML = `Detected ${visits.length} visits.`;
|
|
}
|
|
|
|
function analyzeTrips() {
|
|
if (demoData.length < 2) return alert('Need 2+ points!');
|
|
let totalDist = 0;
|
|
for (let i = 1; i < demoData.length; i++) {
|
|
const dLat = demoData[i].lat - demoData[i-1].lat;
|
|
const dLng = demoData[i].lng - demoData[i-1].lng;
|
|
totalDist += Math.sqrt(dLat * dLat + dLng * dLng) * 111; // km
|
|
}
|
|
const avgDist = totalDist / (demoData.length - 1);
|
|
const mode = avgDist > 5 ? 'Driving' : avgDist > 1 ? 'Cycling' : 'Walking';
|
|
document.getElementById('analyticsInfo').innerHTML = `Total: ${totalDist.toFixed(2)} km | Mode: ${mode}`;
|
|
|
|
// Recolor path
|
|
polylines.forEach(p => map.removeLayer(p));
|
|
const color = { Driving: 'red', Cycling: 'orange', Walking: 'green' }[mode];
|
|
const path = demoData.map(p => [p.lat, p.lng]);
|
|
L.polyline(path, {color, weight: 4}).addTo(map);
|
|
}
|
|
|
|
function updateTimeline() {
|
|
document.getElementById('timeline').innerHTML = demoData.map(p =>
|
|
`<div style="padding: 5px; border-left: 3px solid blue; margin: 5px 0;">${new Date(p.time).toLocaleString()}: [${p.lat.toFixed(4)}, ${p.lng.toFixed(4)}]</div>`
|
|
).join('');
|
|
}
|
|
|
|
function applyFilters() {
|
|
const date = document.getElementById('dateFilter').value;
|
|
const search = document.getElementById('locationFilter').value.toLowerCase();
|
|
markers.forEach(marker => {
|
|
const content = marker.getPopup().getContent().toLowerCase();
|
|
const show = (!date || content.includes(date)) && (!search || content.includes(search));
|
|
if (show) marker.addTo(map); else map.removeLayer(marker);
|
|
});
|
|
}
|
|
|
|
function clearFilters() {
|
|
location.reload();
|
|
}
|
|
|
|
function exportPins() {
|
|
document.getElementById('exportContent').textContent = JSON.stringify({ pins, data: demoData }, null, 2);
|
|
document.getElementById('export').style.display = 'block';
|
|
document.getElementById('overlay').style.display = 'block';
|
|
}
|
|
|
|
function closeExport() {
|
|
document.getElementById('export').style.display = 'none';
|
|
document.getElementById('overlay').style.display = 'none';
|
|
}
|
|
|
|
function copyToClipboard() {
|
|
navigator.clipboard.writeText(document.getElementById('exportContent').textContent).then(() => alert('Copied!'));
|
|
}
|
|
|
|
function toggleTheme() {
|
|
document.body.classList.toggle('dark-theme');
|
|
}
|
|
|
|
// Prevent form submit on upload button (extra safety)
|
|
document.getElementById('uploadForm').addEventListener('submit', e => e.preventDefault());
|
|
|
|
initMap(); |