04bd80b25b
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>
177 lines
6.2 KiB
HTML
177 lines
6.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Test GPS Extraction</title>
|
|
<style>
|
|
body {
|
|
font-family: system-ui, -apple-system, sans-serif;
|
|
max-width: 800px;
|
|
margin: 50px auto;
|
|
padding: 20px;
|
|
background: #f5f5f5;
|
|
}
|
|
.test-case {
|
|
background: white;
|
|
padding: 15px;
|
|
margin: 10px 0;
|
|
border-radius: 8px;
|
|
border-left: 4px solid #2563eb;
|
|
}
|
|
.filename {
|
|
font-family: monospace;
|
|
background: #f0f0f0;
|
|
padding: 5px 10px;
|
|
border-radius: 4px;
|
|
margin: 5px 0;
|
|
}
|
|
.result {
|
|
margin-top: 10px;
|
|
padding: 10px;
|
|
background: #f0fdf4;
|
|
border-left: 3px solid #10b981;
|
|
border-radius: 4px;
|
|
}
|
|
.result.fail {
|
|
background: #fef2f2;
|
|
border-left-color: #ef4444;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>GPS Filename Extraction Test</h1>
|
|
<p>Testing the same patterns as Python implementation</p>
|
|
|
|
<div id="results"></div>
|
|
|
|
<script>
|
|
// Copy the GPS extraction code from upload.js
|
|
class GPSFilenameExtractor {
|
|
constructor() {
|
|
this.PATTERN_NSEW = /(\d+\.?\d*)([NS])_(\d+\.?\d*)([EW])/i;
|
|
this.PATTERN_LATLON = /lat(-?\d+\.?\d+)lon(-?\d+\.?\d+)/i;
|
|
this.PATTERN_SIGNED = /(-?\d+\.?\d+)_(-?\d+\.?\d+)/;
|
|
}
|
|
|
|
extract(filename) {
|
|
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();
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// Test cases
|
|
const testCases = [
|
|
{ filename: "34.0478N_118.2348W_1637_raw_8.sub", expected: { lat: 34.0478, lon: -118.2348 } },
|
|
{ filename: "34.0478N_118.2349W_1351_raw_10.sub", expected: { lat: 34.0478, lon: -118.2349 } },
|
|
{ filename: "34.0478N_118.2349W_1650_test_raw.sub", expected: { lat: 34.0478, lon: -118.2349 } },
|
|
{ filename: "lat34.0478lon-118.2348_test.sub", expected: { lat: 34.0478, lon: -118.2348 } },
|
|
{ filename: "-34.0478_118.2348_capture.sub", expected: { lat: -34.0478, lon: 118.2348 } },
|
|
{ filename: "raw_7.sub", expected: null },
|
|
{ filename: "raw_8.sub", expected: null }
|
|
];
|
|
|
|
const extractor = new GPSFilenameExtractor();
|
|
const resultsDiv = document.getElementById('results');
|
|
|
|
testCases.forEach((test, index) => {
|
|
const coords = extractor.extract(test.filename);
|
|
|
|
const testDiv = document.createElement('div');
|
|
testDiv.className = 'test-case';
|
|
|
|
let success = false;
|
|
let resultHTML = '';
|
|
|
|
if (test.expected === null) {
|
|
// Expect no GPS
|
|
success = coords === null;
|
|
resultHTML = coords === null ?
|
|
`<div class="result">✅ Correctly detected: No GPS in filename</div>` :
|
|
`<div class="result fail">❌ False positive: ${coords.latitude}, ${coords.longitude}</div>`;
|
|
} else {
|
|
// Expect GPS
|
|
if (coords) {
|
|
const latMatch = Math.abs(coords.latitude - test.expected.lat) < 0.0001;
|
|
const lonMatch = Math.abs(coords.longitude - test.expected.lon) < 0.0001;
|
|
success = latMatch && lonMatch;
|
|
|
|
resultHTML = success ?
|
|
`<div class="result">✅ GPS Detected: ${coords.latitude.toFixed(6)}, ${coords.longitude.toFixed(6)} (${coords.source})</div>` :
|
|
`<div class="result fail">❌ Wrong coordinates: Got ${coords.latitude}, ${coords.longitude} | Expected ${test.expected.lat}, ${test.expected.lon}</div>`;
|
|
} else {
|
|
resultHTML = `<div class="result fail">❌ Failed to extract GPS (expected ${test.expected.lat}, ${test.expected.lon})</div>`;
|
|
}
|
|
}
|
|
|
|
testDiv.innerHTML = `
|
|
<div><strong>Test ${index + 1}</strong></div>
|
|
<div class="filename">${test.filename}</div>
|
|
${resultHTML}
|
|
`;
|
|
|
|
resultsDiv.appendChild(testDiv);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|