241 lines
11 KiB
JavaScript
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)');
|
|
});
|