GPS Auto-Extraction + First Successful Upload Complete
Major milestone: GPS coordinates now auto-extract from filenames and uploads appear on map with full end-to-end workflow functional! ✨ GPS Auto-Extraction Features: - JavaScript GPS extractor class matching Python patterns - Supports 3 filename formats: * N/S/E/W: 34.0478N_118.2349W_filename.sub * lat/lon prefix: lat34.0478lon-118.2348_filename.sub * Signed decimal: -34.0478_118.2348_filename.sub - Auto-populates latitude/longitude form fields on file drop - Green notification toast shows detected coordinates - File list shows GPS badge for files with coordinates 🗺️ Web Interface Improvements: - Upload endpoint now stores captures in-memory - Query endpoint returns uploaded captures for map display - Stats endpoint shows real-time upload counts - Map displays uploaded captures as markers - Color-coded by frequency band 📁 Updated Files: - static/js/upload.js: GPS extraction + auto-population - src/api/main_simple.py: In-memory storage + endpoints - src/parser/gps_extractor.py: Backend GPS extraction (Python) - scripts/test_gps_extraction.py: Python test suite - test_gps_extraction.html: Browser test suite 📊 T-Embed Files Updated: - 34.0478N_118.2348W_1637_raw_8.sub: 315 MHz Princeton - 34.0478N_118.2349W_1351_raw_10.sub: 433.92 MHz Princeton - 34.0478N_118.2349W_1650_test_raw.sub: 433.92 MHz RAW - All now have proper Flipper SubGhz headers ✅ Tested Features: - GPS extraction from filename: 34.0478N_118.2349W → 34.0478, -118.2349 - Auto-population of GPS fields in upload form - File upload with GPS validation - Capture appears on map after upload - Statistics update in real-time - Frequency distribution calculated correctly 🎯 End-to-End Flow Working: 1. User drops .sub file with GPS in filename 2. GPS auto-detected and form fields populate 3. User clicks Upload 4. Server parses RF data + GPS coordinates 5. Capture stored in memory 6. Map refreshes and displays new marker 7. Stats update with new counts 🚀 Demo: http://localhost:8000 Upload 34.0478N_118.2349W_1351_raw_10.sub and watch it appear on map! 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
+177
-12
@@ -1,6 +1,7 @@
|
||||
// GigLez - Upload Functionality
|
||||
|
||||
let selectedFiles = [];
|
||||
let autoDetectedGPS = null; // Store GPS detected from filenames
|
||||
|
||||
// Initialize upload functionality
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
@@ -8,6 +9,90 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
setupFileInput();
|
||||
});
|
||||
|
||||
// GPS Extraction from Filenames
|
||||
// Matches patterns like: 34.0478N_118.2349W_1650_test_raw.sub
|
||||
class GPSFilenameExtractor {
|
||||
constructor() {
|
||||
// Pattern 1: Decimal degrees with N/S/E/W (most common)
|
||||
this.PATTERN_NSEW = /(\d+\.?\d*)([NS])_(\d+\.?\d*)([EW])/i;
|
||||
|
||||
// Pattern 2: lat/lon prefix format
|
||||
this.PATTERN_LATLON = /lat(-?\d+\.?\d+)lon(-?\d+\.?\d+)/i;
|
||||
|
||||
// Pattern 3: Signed decimal degrees
|
||||
this.PATTERN_SIGNED = /(-?\d+\.?\d+)_(-?\d+\.?\d+)/;
|
||||
}
|
||||
|
||||
extract(filename) {
|
||||
// Try each pattern
|
||||
let coords = this.tryNSEW(filename) ||
|
||||
this.tryLatLon(filename) ||
|
||||
this.trySigned(filename);
|
||||
|
||||
return coords;
|
||||
}
|
||||
|
||||
tryNSEW(filename) {
|
||||
const match = filename.match(this.PATTERN_NSEW);
|
||||
if (!match) return null;
|
||||
|
||||
let lat = parseFloat(match[1]);
|
||||
let lon = parseFloat(match[3]);
|
||||
const latDir = match[2].toUpperCase();
|
||||
const lonDir = match[4].toUpperCase();
|
||||
|
||||
// Apply direction (N=positive, S=negative, E=positive, W=negative)
|
||||
if (latDir === 'S') lat = -lat;
|
||||
if (lonDir === 'W') lon = -lon;
|
||||
|
||||
if (!this.validateCoordinates(lat, lon)) return null;
|
||||
|
||||
return {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
source: 'filename_nsew'
|
||||
};
|
||||
}
|
||||
|
||||
tryLatLon(filename) {
|
||||
const match = filename.match(this.PATTERN_LATLON);
|
||||
if (!match) return null;
|
||||
|
||||
const lat = parseFloat(match[1]);
|
||||
const lon = parseFloat(match[2]);
|
||||
|
||||
if (!this.validateCoordinates(lat, lon)) return null;
|
||||
|
||||
return {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
source: 'filename_latlon'
|
||||
};
|
||||
}
|
||||
|
||||
trySigned(filename) {
|
||||
const match = filename.match(this.PATTERN_SIGNED);
|
||||
if (!match) return null;
|
||||
|
||||
const lat = parseFloat(match[1]);
|
||||
const lon = parseFloat(match[2]);
|
||||
|
||||
if (!this.validateCoordinates(lat, lon)) return null;
|
||||
|
||||
return {
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
source: 'filename_signed'
|
||||
};
|
||||
}
|
||||
|
||||
validateCoordinates(lat, lon) {
|
||||
return lat >= -90 && lat <= 90 && lon >= -180 && lon <= 180;
|
||||
}
|
||||
}
|
||||
|
||||
const gpsExtractor = new GPSFilenameExtractor();
|
||||
|
||||
function setupDropZone() {
|
||||
const dropZone = document.getElementById('drop-zone');
|
||||
|
||||
@@ -51,6 +136,23 @@ function addFiles(files) {
|
||||
// Add new files
|
||||
selectedFiles.push(...files);
|
||||
|
||||
// Try to extract GPS from first file with coordinates
|
||||
if (!autoDetectedGPS) {
|
||||
for (const file of files) {
|
||||
const coords = gpsExtractor.extract(file.name);
|
||||
if (coords) {
|
||||
autoDetectedGPS = coords;
|
||||
// Auto-populate GPS fields
|
||||
document.getElementById('default-lat').value = coords.latitude.toFixed(6);
|
||||
document.getElementById('default-lon').value = coords.longitude.toFixed(6);
|
||||
|
||||
// Show notification
|
||||
showGPSDetectionNotification(file.name, coords);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show file list
|
||||
document.getElementById('file-list').style.display = 'block';
|
||||
|
||||
@@ -58,20 +160,72 @@ function addFiles(files) {
|
||||
renderFileList();
|
||||
}
|
||||
|
||||
function showGPSDetectionNotification(filename, coords) {
|
||||
const notification = document.createElement('div');
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: #10b981;
|
||||
color: white;
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
z-index: 10000;
|
||||
max-width: 400px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
`;
|
||||
|
||||
notification.innerHTML = `
|
||||
<strong>📍 GPS Auto-Detected!</strong><br>
|
||||
<small>From: ${filename}</small><br>
|
||||
<small>Coordinates: ${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)}</small>
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
// Add animation
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
@keyframes slideIn {
|
||||
from { transform: translateX(400px); opacity: 0; }
|
||||
to { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
|
||||
// Remove after 5 seconds
|
||||
setTimeout(() => {
|
||||
notification.style.animation = 'slideIn 0.3s ease-out reverse';
|
||||
setTimeout(() => notification.remove(), 300);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
function renderFileList() {
|
||||
const container = document.getElementById('files-container');
|
||||
|
||||
container.innerHTML = selectedFiles.map((file, index) => `
|
||||
<div class="file-item" data-index="${index}">
|
||||
<div class="file-info">
|
||||
<div class="file-name">${file.name}</div>
|
||||
<div class="file-meta">${formatFileSize(file.size)}</div>
|
||||
container.innerHTML = selectedFiles.map((file, index) => {
|
||||
const coords = gpsExtractor.extract(file.name);
|
||||
const hasGPS = coords !== null;
|
||||
|
||||
return `
|
||||
<div class="file-item" data-index="${index}">
|
||||
<div class="file-info">
|
||||
<div class="file-name">
|
||||
${file.name}
|
||||
${hasGPS ? '<span style="color: #10b981; margin-left: 0.5rem;">📍 GPS</span>' : ''}
|
||||
</div>
|
||||
<div class="file-meta">
|
||||
${formatFileSize(file.size)}
|
||||
${hasGPS ? ` | ${coords.latitude.toFixed(4)}, ${coords.longitude.toFixed(4)}` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="removeFile(${index})">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="file-actions">
|
||||
<button type="button" class="btn btn-secondary" onclick="removeFile(${index})">Remove</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function removeFile(index) {
|
||||
@@ -86,6 +240,7 @@ function removeFile(index) {
|
||||
|
||||
function clearFiles() {
|
||||
selectedFiles = [];
|
||||
autoDetectedGPS = null; // Reset auto-detected GPS
|
||||
document.getElementById('file-list').style.display = 'none';
|
||||
document.getElementById('file-input').value = '';
|
||||
}
|
||||
@@ -96,12 +251,19 @@ async function uploadFiles() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate GPS coordinates
|
||||
// Validate GPS coordinates (allow auto-detected or manual entry)
|
||||
const lat = parseFloat(document.getElementById('default-lat').value);
|
||||
const lon = parseFloat(document.getElementById('default-lon').value);
|
||||
|
||||
if (isNaN(lat) || isNaN(lon)) {
|
||||
alert('Please provide valid GPS coordinates');
|
||||
// Check if any file has GPS in filename
|
||||
const hasGPSInFilename = selectedFiles.some(file => gpsExtractor.extract(file.name) !== null);
|
||||
|
||||
if (hasGPSInFilename) {
|
||||
alert('GPS detected in filename but not populated. Please refresh and try again.');
|
||||
} else {
|
||||
alert('Please provide GPS coordinates (manually or via filename like: 34.0478N_118.2349W_filename.sub)');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -236,6 +398,9 @@ function resetUpload() {
|
||||
document.getElementById('default-lat').value = '';
|
||||
document.getElementById('default-lon').value = '';
|
||||
document.getElementById('session-uuid').value = '';
|
||||
|
||||
// Reset auto-detected GPS
|
||||
autoDetectedGPS = null;
|
||||
}
|
||||
|
||||
function useCurrentLocation() {
|
||||
|
||||
Reference in New Issue
Block a user