Files
location-history-vz/server.js

241 lines
11 KiB
JavaScript

const express = require('express');
const multer = require('multer');
const path = require('path');
const cors = require('cors');
const app = express();
const PORT = process.env.PORT || 3000;
// Middleware
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(express.static('public'));
app.use(express.json({ limit: '100mb' }));
// Multer setup
const storage = multer.memoryStorage();
const upload = multer({
storage,
limits: { fileSize: 100 * 1024 * 1024 }, // 100MB
fileFilter: (req, file, cb) => {
const ext = path.extname(file.originalname).toLowerCase();
if (['.csv', '.json', '.gpx', '.kml'].includes(ext)) {
cb(null, true);
} else {
cb(new Error('Unsupported file type. Use CSV, JSON, GPX, or KML.'), false);
}
}
});
// Demo Data
app.get('/api/demo-data', (req, res) => {
res.json([
{ 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' }
]);
});
// Reverse Geocoding
app.get('/api/reverse-geocode/:lat/:lng', async (req, res) => {
const { lat, lng } = req.params;
try {
const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&addressdetails=1&accept-language=en`;
const response = await fetch(url);
if (!response.ok) throw new Error(`Geocoding API error: ${response.status}`);
const data = await response.json();
res.json({ address: data.display_name || 'Address not found' });
} catch (err) {
console.error('Geocoding error:', err.message);
res.status(500).json({ error: 'Geocoding service unavailable' });
}
});
// Aggressive Sampling & Timeout for Upload
app.post('/api/upload', upload.single('file'), async (req, res) => {
if (!req.file) {
console.log('Upload failed: No file received');
return res.status(400).json({ success: false, error: 'No file received' });
}
const filename = req.file.originalname;
const ext = path.extname(filename).toLowerCase();
const bufferStr = req.file.buffer.toString('utf-8');
console.log(`Processing upload: ${filename} (${ext}, size: ${req.file.size} bytes)`);
console.log('Starting aggressive sampling mode...');
let points = [];
let uniqueDates = new Set();
let startTime = Date.now();
const timeout = 20000; // 20s timeout
const maxPoints = 2000; // Aggressive cap for preview
try {
if (ext === '.json') {
console.log('Parsing JSON with aggressive sampling...');
const parsed = JSON.parse(bufferStr);
if (!Array.isArray(parsed)) {
// Google Timeline format
if (parsed.timelineObjects && Array.isArray(parsed.timelineObjects)) {
console.log('Parsing Google Timeline JSON (aggressive)...');
let count = 0;
for (let obj of parsed.timelineObjects) {
if (Date.now() - startTime > timeout) {
console.log('Timeout in timelineObjects - sampling partial');
break;
}
if (obj.placeVisit && obj.placeVisit.location) {
const loc = obj.placeVisit.location;
const lat = parseFloat(loc.latitudeE7) / 1e7;
const lng = parseFloat(loc.longitudeE7) / 1e7;
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
const time = new Date(parseInt(obj.placeVisit.duration.startTimestampMs)).toISOString();
const date = time.split('T')[0];
points.push({ lat, lng, time });
uniqueDates.add(date);
count++;
if (count % 100 === 0) setImmediate(() => {}); // Yield
if (points.length >= maxPoints) break;
}
} else if (obj.location && obj.location.latitudeE7) {
const loc = obj.location;
const lat = parseFloat(loc.latitudeE7) / 1e7;
const lng = parseFloat(loc.longitudeE7) / 1e7;
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
const time = new Date(parseInt(loc.timestampMs)).toISOString();
const date = time.split('T')[0];
points.push({ lat, lng, time });
uniqueDates.add(date);
count++;
if (count % 100 === 0) setImmediate(() => {}); // Yield
if (points.length >= maxPoints) break;
}
}
}
console.log(`Extracted ${points.length} points from Google JSON (capped at ${maxPoints})`);
} else {
throw new Error('Invalid JSON structure - not an array or Google Timeline');
}
} else {
// Simple array
points = parsed.filter((p, i) => i % 10 === 0) // Sample every 10th
.filter(p => {
const lat = parseFloat(p.lat || p.latitudeE7 / 1e7);
const lng = parseFloat(p.lng || p.longitudeE7 / 1e7);
const time = p.time || new Date().toISOString();
const date = new Date(time).toISOString().split('T')[0];
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
uniqueDates.add(date);
return true;
}
return false;
});
points = points.slice(0, maxPoints);
}
} else if (ext === '.csv') {
console.log('Parsing CSV with sampling...');
const lines = bufferStr.split('\n').filter(line => line.trim());
if (lines.length < 2) throw new Error('Invalid CSV');
const headers = lines[0].toLowerCase().split(',').map(h => h.trim());
const latIdx = headers.findIndex(h => h.includes('lat') || h.includes('latitude'));
const lngIdx = headers.findIndex(h => h.includes('lng') || h.includes('longitude'));
const timeIdx = headers.findIndex(h => h.includes('time') || h.includes('timestamp'));
if (latIdx === -1 || lngIdx === -1) throw new Error('CSV missing lat/lng columns');
let count = 0;
for (let i = 1; i < lines.length; i++) {
if (Date.now() - startTime > timeout) break;
if (count % 10 === 0) setImmediate(() => {}); // Yield every 10 lines
const line = lines[i];
const cols = line.split(',').map(c => c.trim());
if (cols.length < Math.max(latIdx, lngIdx) + 1) continue;
const lat = parseFloat(cols[latIdx]);
const lng = parseFloat(cols[lngIdx]);
const time = cols[timeIdx] || new Date().toISOString();
const date = new Date(time).toISOString().split('T')[0];
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
points.push({ lat, lng, time });
uniqueDates.add(date);
count++;
if (points.length >= maxPoints) break;
}
}
} else if (ext === '.kml') {
console.log('Parsing KML with sampling...');
const placemarkRegex = /<Placemark>[\s\S]*?<coordinates>([^<]+)<\/coordinates>[\s\S]*?<\/Placemark>/gi;
const whenRegex = /<when>([^<]+)<\/when>/i;
let match;
let count = 0;
while ((match = placemarkRegex.exec(bufferStr)) !== null && points.length < maxPoints) {
if (Date.now() - startTime > timeout) break;
if (count % 50 === 0) setImmediate(() => {});
const coords = match[1].trim().split(',');
if (coords.length >= 2) {
const lng = parseFloat(coords[0]);
const lat = parseFloat(coords[1]);
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
// Extract time from when tag if nearby
const whenMatch = bufferStr.slice(match.index, match.index + 1000).match(whenRegex);
const time = whenMatch ? whenMatch[1] : new Date().toISOString();
const date = new Date(time).toISOString().split('T')[0];
points.push({ lat, lng, time });
uniqueDates.add(date);
count++;
}
}
}
} else if (ext === '.gpx') {
console.log('Parsing GPX with sampling...');
const trkptRegex = /<trkpt lat="([^"]+)" lon="([^"]+)"[^>]*>/gi;
let match;
let count = 0;
while ((match = trkptRegex.exec(bufferStr)) !== null && points.length < maxPoints) {
if (Date.now() - startTime > timeout) break;
if (count % 50 === 0) setImmediate(() => {});
const lat = parseFloat(match[1]);
const lng = parseFloat(match[2]);
const time = new Date().toISOString(); // Default
const date = new Date(time).toISOString().split('T')[0];
if (!isNaN(lat) && !isNaN(lng) && lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) {
points.push({ lat, lng, time });
uniqueDates.add(date);
count++;
}
}
}
if (points.length === 0) {
throw new Error('No valid points found - file may be empty or invalid format');
}
const datesArray = Array.from(uniqueDates).sort();
console.log(`Success: ${points.length} points, ${datesArray.length} dates in ${Date.now() - startTime}ms`);
res.json({ success: true, points, uniqueDates: datesArray, count: points.length, sampled: points.length < 10000 });
} catch (err) {
console.error(`Upload error for ${filename}:`, err.message);
console.error('Stack:', err.stack);
res.status(400).json({ success: false, error: err.message });
}
});
// Load Full Data (for large files)
app.post('/api/load-full', (req, res) => {
// Placeholder - load from memory or file if needed
res.json({ success: false, error: 'Full load not implemented yet - use sampling' });
});
// Root route
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// Error Handler
app.use((err, req, res, next) => {
console.error('Global error:', err.message, req.method, req.url);
res.status(500).json({ success: false, error: 'Server error - check logs' });
});
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log('Aggressive anti-stuck mode enabled (20s timeout, 2k point cap)');
});