Files
giglez/static/js/upload.js
T
Trilltechnician 04bd80b25b 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>
2026-01-13 18:37:13 -08:00

441 lines
13 KiB
JavaScript

// GigLez - Upload Functionality
let selectedFiles = [];
let autoDetectedGPS = null; // Store GPS detected from filenames
// Initialize upload functionality
document.addEventListener('DOMContentLoaded', () => {
setupDropZone();
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');
dropZone.addEventListener('click', () => {
document.getElementById('file-input').click();
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.sub'));
addFiles(files);
});
}
function setupFileInput() {
const fileInput = document.getElementById('file-input');
fileInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
addFiles(files);
});
}
function addFiles(files) {
if (files.length === 0) {
alert('Please select .sub files');
return;
}
// 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';
// Render file list
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) => {
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>
`;
}).join('');
}
function removeFile(index) {
selectedFiles.splice(index, 1);
if (selectedFiles.length === 0) {
document.getElementById('file-list').style.display = 'none';
} else {
renderFileList();
}
}
function clearFiles() {
selectedFiles = [];
autoDetectedGPS = null; // Reset auto-detected GPS
document.getElementById('file-list').style.display = 'none';
document.getElementById('file-input').value = '';
}
async function uploadFiles() {
if (selectedFiles.length === 0) {
alert('Please select files to upload');
return;
}
// 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)) {
// 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;
}
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
alert('GPS coordinates out of range');
return;
}
// Hide file list, show progress
document.getElementById('file-list').style.display = 'none';
document.getElementById('upload-progress').style.display = 'block';
try {
// Build manifest
const sessionUuid = document.getElementById('session-uuid').value || generateUUID();
const accuracy = parseFloat(document.getElementById('default-accuracy').value) || 5.0;
const altitude = parseFloat(document.getElementById('default-altitude').value) || null;
const manifest = {
session_uuid: sessionUuid,
captures: selectedFiles.map(file => ({
filename: file.name,
latitude: lat,
longitude: lon,
accuracy: accuracy,
altitude: altitude,
timestamp: new Date().toISOString()
}))
};
// Build FormData
const formData = new FormData();
formData.append('manifest', JSON.stringify(manifest));
selectedFiles.forEach(file => {
formData.append('files', file);
});
// Upload
updateProgress(0, 'Uploading files...');
const response = await fetch('/api/v1/captures/upload', {
method: 'POST',
body: formData
});
updateProgress(100, 'Processing...');
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || 'Upload failed');
}
const result = await response.json();
// Show results
showUploadResults(result);
} catch (error) {
console.error('Upload error:', error);
alert(`Upload failed: ${error.message}`);
// Reset
document.getElementById('upload-progress').style.display = 'none';
document.getElementById('file-list').style.display = 'block';
}
}
function updateProgress(percent, text) {
document.getElementById('progress-fill').style.width = `${percent}%`;
document.getElementById('progress-text').textContent = text;
}
function showUploadResults(result) {
// Hide progress
document.getElementById('upload-progress').style.display = 'none';
// Show results
const resultsDiv = document.getElementById('upload-results');
const container = document.getElementById('results-container');
resultsDiv.style.display = 'block';
const successCount = result.successful?.length || 0;
const errorCount = result.failed?.length || 0;
let html = `
<div class="result-summary">
<p>Successfully uploaded: <strong>${successCount}</strong> files</p>
${errorCount > 0 ? `<p>Failed: <strong>${errorCount}</strong> files</p>` : ''}
</div>
`;
if (result.successful && result.successful.length > 0) {
html += '<h4>Successful Uploads:</h4>';
result.successful.forEach(item => {
html += `
<div class="result-item">
<div class="file-name">${item.filename}</div>
<div class="file-meta">
Frequency: ${(item.frequency / 1e6).toFixed(2)} MHz |
Protocol: ${item.protocol || 'RAW'}
</div>
</div>
`;
});
}
if (result.failed && result.failed.length > 0) {
html += '<h4>Failed Uploads:</h4>';
result.failed.forEach(item => {
html += `
<div class="result-item error">
<div class="file-name">${item.filename}</div>
<div class="file-meta">Error: ${item.error}</div>
</div>
`;
});
}
container.innerHTML = html;
}
function resetUpload() {
// Hide results
document.getElementById('upload-results').style.display = 'none';
// Clear files
clearFiles();
// Reset form
document.getElementById('default-lat').value = '';
document.getElementById('default-lon').value = '';
document.getElementById('session-uuid').value = '';
// Reset auto-detected GPS
autoDetectedGPS = null;
}
function useCurrentLocation() {
if (!navigator.geolocation) {
alert('Geolocation is not supported by your browser');
return;
}
navigator.geolocation.getCurrentPosition(
(position) => {
document.getElementById('default-lat').value = position.coords.latitude.toFixed(6);
document.getElementById('default-lon').value = position.coords.longitude.toFixed(6);
document.getElementById('default-accuracy').value = position.coords.accuracy.toFixed(1);
if (position.coords.altitude) {
document.getElementById('default-altitude').value = position.coords.altitude.toFixed(1);
}
},
(error) => {
alert(`Geolocation error: ${error.message}`);
}
);
}
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}