Offline tactical mapping inspired by ATAK (Android Tactical Assault Kit) for disaster relief and emergency coordination
- Overview
- Hardware Capabilities
- Architecture Design
- Performance Analysis
- Feature Implementation Guide
- Storage Requirements
- Optimization Strategies
- Implementation Roadmap
- Quick Start Guide
ATAK (Android Tactical Assault Kit) is a military-grade mapping and geospatial tool used for situational awareness and tactical coordination. This document outlines how to implement ATAK-inspired features in EmergencyBox for disaster relief scenarios.
Core Functionality:
- 📍 Offline Map Tiles - Pre-downloaded OpenStreetMap data
- 🎯 Tactical Markers - Hazards, safe zones, resources, waypoints
- 📏 Measurement Tools - Distance, area, bearing calculations
- 🗺️ Route Planning - Multi-point routes with distance
- 📱 Geolocation - Optional GPS position sharing
- 💬 Chat Integration - Share coordinates directly in chat
- 📸 Photo Markers - Attach images to map locations
- 🔄 Real-Time Sync - All users see same tactical picture
- Disaster Relief - Mark hazards, safe zones, water sources
- Search & Rescue - Coordinate teams, mark search grids
- Emergency Response - Medical stations, evacuation routes
- Field Operations - Remote area coordination without internet
CPU: Broadcom BCM4708A0 - Dual-core ARM Cortex-A9 @ 800MHz
RAM: 256MB (some models 512MB)
Storage: 128MB NAND flash + USB drive (8GB - 128GB)
WiFi: 802.11ac (1300Mbps on 5GHz, 600Mbps on 2.4GHz)
USB: 1x USB 3.0, 1x USB 2.0
Short Answer: YES ✅
The AC68U is surprisingly capable for tactical mapping when designed correctly.
| Feature | Router Load | Verdict |
|---|---|---|
| Serving map tiles | LOW - Static file serving | ✅ Perfect - lighttpd handles this easily |
| Storing markers (SQLite) | LOW - Simple CRUD operations | ✅ No problem - tiny database queries |
| 20-50 concurrent map viewers | MEDIUM - Tile requests | ✅ Fine - tiles are cached by browsers |
| Marker updates | LOW - Small JSON responses | ✅ Easy - kilobytes of data |
| Photo markers | MEDIUM - Image serving | ✅ OK - already handling file uploads |
| Real-time location tracking | MEDIUM - Frequent updates | |
| Complex route calculation | HIGH - CPU intensive | |
| Many markers (500+) | MEDIUM - Memory usage | |
| Server-side rendering | VERY HIGH - Too CPU heavy | ❌ Avoid - use client-side rendering |
Key Principle: Router is a dumb file server, clients do the heavy lifting.
┌─────────────────────────────────────────────────┐
│ User's Phone/Laptop (Client) │
│ ┌───────────────────────────────────────────┐ │
│ │ Leaflet.js Map Library (runs in browser) │ │
│ │ - Renders all map tiles │ │
│ │ - Calculates routes │ │
│ │ - Draws markers │ │
│ │ - Handles zoom/pan │ │
│ │ - Does distance calculations │ │
│ └───────────────────────────────────────────┘ │
│ ▲ │ │
│ │ Get tiles │ Update marker │
│ │ Get markers │ │
└──────────────┼─────────────────┼─────────────────┘
│ ▼
┌──────────────┼─────────────────┼─────────────────┐
│ │ Router (Server)│ │
│ ┌───────────┴─────────────────┴───────────────┐ │
│ │ Just serves: │ │
│ │ 1. Static map tiles (pre-downloaded .png) │ │
│ │ 2. Marker JSON (from SQLite) │ │
│ │ 3. HTML/CSS/JS (Leaflet library) │ │
│ └─────────────────────────────────────────────┘ │
│ CPU Usage: 5-10% with 50 users │
│ RAM Usage: ~100MB │
└───────────────────────────────────────────────────┘
| Component | Technology | Purpose |
|---|---|---|
| Frontend | Leaflet.js (~40KB) | Map rendering, interactions |
| Backend | PHP 8.4.7 | Marker CRUD API |
| Database | SQLite3 | Marker storage |
| Tiles | OpenStreetMap (pre-downloaded) | Offline map data |
| Web Server | lighttpd | Static file serving |
-- Tactical markers table
CREATE TABLE map_markers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL, -- hazard, safe_zone, water, medical, meeting, user
lat REAL NOT NULL,
lon REAL NOT NULL,
title TEXT NOT NULL,
description TEXT,
severity INTEGER DEFAULT 1, -- 1=info, 2=warning, 3=critical
created_by TEXT,
photo_id INTEGER, -- Link to files table
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
active INTEGER DEFAULT 1 -- For soft delete
);
-- Routes table
CREATE TABLE map_routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
waypoints TEXT NOT NULL, -- JSON array of {lat, lon}
distance REAL, -- In meters
created_by TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- User locations table (optional)
CREATE TABLE user_locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL,
accuracy REAL, -- GPS accuracy in meters
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(username) -- One location per user
);CPU: 5-8%
RAM: 80MB
Bandwidth: ~5 Mbps (initial tile load)
~50 Kbps steady state (marker updates)
✅ EASY - Router barely notices
CPU: 15-25%
RAM: 150MB
Bandwidth: ~15 Mbps (initial loads)
~200 Kbps steady state
✅ TOTALLY FINE - Well within capacity
CPU: 25-40%
RAM: 180MB
Bandwidth: ~500 Kbps sustained
⚠️ OK - But optimize marker updates (batch them)
HTML/CSS/JS: ~100 KB
Map tiles (viewport): 20-30 tiles = ~500 KB
Marker data: ~10 KB
Total: ~610 KB
Time @ 5GHz WiFi (100 Mbps): <1 second
Time @ 2.4GHz WiFi (30 Mbps): ~2 seconds
New marker created: 500 bytes JSON
50 users receiving update: 25 KB total
Updates every 10 seconds: 2.5 KB/sec = 20 Kbps
✅ Negligible bandwidth
Load 10 new tiles: ~250 KB per user
10 users panning simultaneously: 2.5 MB burst
Router bandwidth available: 1300 Mbps
✅ No problem at all
50 users connect at once
Each loads 30 tiles = 1,500 tile requests
Tile size: 20 KB average
Total: 30 MB burst
Router USB 3.0 read speed: ~100 MB/s
Time to serve all: 0.3 seconds
lighttpd concurrent requests: 100+
✅ PASSES - Barely a hiccup
50 users each drop 1 marker/second (unrealistic)
50 database inserts/second
50 broadcast updates/second
SQLite capacity: 10,000+ inserts/sec
Marker JSON size: 500 bytes
Broadcast: 25 KB/sec = 200 Kbps
✅ PASSES - Though rate-limit this in practice
Implementation:
// Initialize map with offline tiles
const map = L.map('tactical-map').setView([lat, lon], 13);
// Use pre-downloaded tiles from router
L.tileLayer('/map_tiles/{z}/{x}/{y}.png', {
maxZoom: 17,
minZoom: 10,
attribution: '© OpenStreetMap contributors'
}).addTo(map);Tile Directory Structure:
/opt/share/www/map_tiles/
├── 10/ # Zoom level 10 (city overview)
├── 11/
├── 12/
├── 13/ # Zoom level 13 (neighborhood)
├── 14/
├── 15/ # Zoom level 15 (street level)
├── 16/
└── 17/ # Zoom level 17 (building detail)
Marker Types:
const markerTypes = {
hazard: {
icon: '🔴',
color: '#ff006e',
label: 'Hazard',
description: 'Building collapse, fire, flood, danger'
},
safe_zone: {
icon: '🟢',
color: '#06ffa5',
label: 'Safe Zone',
description: 'Shelter, refuge, evacuation point'
},
water: {
icon: '💧',
color: '#00f5ff',
label: 'Water Source',
description: 'Drinking water, well, hydrant'
},
medical: {
icon: '⚕️',
color: '#ffbe0b',
label: 'Medical',
description: 'First aid, hospital, medic station'
},
meeting: {
icon: '📍',
color: '#9d4edd',
label: 'Meeting Point',
description: 'Rally point, staging area'
},
food: {
icon: '🍽️',
color: '#06ffa5',
label: 'Food/Supplies',
description: 'Food distribution, supplies'
},
user: {
icon: '👤',
color: '#00f5ff',
label: 'User Location',
description: 'Team member position'
}
};Add Marker on Click:
map.on('click', (e) => {
const { lat, lng } = e.latlng;
// Show modal to get marker details
showMarkerModal({
lat: lat,
lon: lng,
callback: (markerData) => {
saveMarker(markerData);
}
});
});
async function saveMarker(data) {
const response = await fetch('/api/map/add_marker.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
// Add marker to map
addMarkerToMap(result.marker);
// Share to chat
sendChatMessage(
`📍 New ${data.type}: ${data.title} at ${data.lat.toFixed(5)}, ${data.lon.toFixed(5)}`
);
}
}Client-Side Calculation (No Router Load):
let measureMode = false;
let measurePoints = [];
function enableMeasureTool() {
measureMode = true;
map.on('click', measureClick);
}
function measureClick(e) {
measurePoints.push(e.latlng);
// Draw line between points
if (measurePoints.length > 1) {
const polyline = L.polyline(measurePoints, {
color: '#00f5ff',
weight: 3,
dashArray: '10, 5'
}).addTo(map);
// Calculate distance
const distance = calculateDistance(measurePoints);
// Show popup
const popup = L.popup()
.setLatLng(e.latlng)
.setContent(`Distance: ${formatDistance(distance)}`)
.openOn(map);
}
}
function calculateDistance(points) {
let total = 0;
for (let i = 1; i < points.length; i++) {
total += points[i-1].distanceTo(points[i]);
}
return total;
}
function formatDistance(meters) {
if (meters < 1000) {
return `${meters.toFixed(0)}m`;
} else {
return `${(meters / 1000).toFixed(2)}km`;
}
}function enableAreaTool() {
let areaPoints = [];
map.on('click', (e) => {
areaPoints.push(e.latlng);
if (areaPoints.length > 2) {
const polygon = L.polygon(areaPoints, {
color: '#ff006e',
fillOpacity: 0.2
}).addTo(map);
// Calculate area using Leaflet GeometryUtil
const area = L.GeometryUtil.geodesicArea(areaPoints);
L.popup()
.setLatLng(e.latlng)
.setContent(`Area: ${formatArea(area)}`)
.openOn(map);
}
});
}
function formatArea(sqMeters) {
if (sqMeters < 10000) {
return `${sqMeters.toFixed(0)}m²`;
} else {
return `${(sqMeters / 1000000).toFixed(2)}km²`;
}
}function shareCoordinateToChat(lat, lon, description) {
const coordText = `📍 ${description}\n` +
`Coordinates: ${lat.toFixed(5)}, ${lon.toFixed(5)}\n` +
`[View on Map](#map:${lat},${lon})`;
sendChatMessage(coordText);
}
// In chat rendering, detect coordinate links
function renderChatMessage(message) {
// Parse [View on Map](#map:lat,lon)
const coordRegex = /#map:([-\d.]+),([-\d.]+)/;
const match = message.match(coordRegex);
if (match) {
const [_, lat, lon] = match;
message = message.replace(
coordRegex,
`<a href="#" onclick="centerMap(${lat}, ${lon}); return false;">
View on Map
</a>`
);
}
return message;
}
function centerMap(lat, lon) {
map.setView([lat, lon], 16);
// Add temporary marker
const marker = L.marker([lat, lon], {
icon: L.icon({
iconUrl: '/img/ping-marker.png',
iconSize: [32, 32]
})
}).addTo(map);
// Remove after 5 seconds
setTimeout(() => marker.remove(), 5000);
}async function attachPhotoToMarker(markerId, photoFile) {
// First upload photo to file system
const formData = new FormData();
formData.append('file', photoFile);
formData.append('category', 'map_photos');
const uploadResponse = await fetch('/api/upload.php', {
method: 'POST',
body: formData
});
const uploadResult = await uploadResponse.json();
if (uploadResult.success) {
// Link photo to marker
await fetch('/api/map/attach_photo.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
marker_id: markerId,
photo_id: uploadResult.file_id
})
});
// Update marker popup with photo
updateMarkerPopup(markerId);
}
}let locationWatchId = null;
function startLocationSharing() {
if ('geolocation' in navigator) {
locationWatchId = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
// Throttle updates (only send every 30 seconds)
updateUserLocation(latitude, longitude, accuracy);
},
(error) => {
console.error('Geolocation error:', error);
},
{
enableHighAccuracy: true,
maximumAge: 30000, // 30 seconds
timeout: 10000
}
);
}
}
async function updateUserLocation(lat, lon, accuracy) {
const response = await fetch('/api/map/update_location.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
username: getCurrentUsername(),
lat: lat,
lon: lon,
accuracy: accuracy
})
});
}
function stopLocationSharing() {
if (locationWatchId) {
navigator.geolocation.clearWatch(locationWatchId);
locationWatchId = null;
}
}// Use Leaflet.markercluster plugin
const markers = L.markerClusterGroup({
maxClusterRadius: 50,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true
});
// Add markers to cluster group
markers.addLayer(L.marker([lat, lon]));
// Add cluster group to map
map.addLayer(markers);
// When zoomed out: Shows "50" cluster
// When zoomed in: Shows individual markersconst layerGroups = {
hazards: L.layerGroup(),
safeZones: L.layerGroup(),
medical: L.layerGroup(),
water: L.layerGroup(),
users: L.layerGroup()
};
// Add all layers to map
Object.values(layerGroups).forEach(layer => map.addLayer(layer));
// Layer control
const overlays = {
"🔴 Hazards": layerGroups.hazards,
"🟢 Safe Zones": layerGroups.safeZones,
"⚕️ Medical": layerGroups.medical,
"💧 Water": layerGroups.water,
"👤 Users": layerGroups.users
};
L.control.layers(null, overlays).addTo(map);
// Add marker to specific layer
function addMarker(type, lat, lon, data) {
const marker = L.marker([lat, lon]);
layerGroups[type].addLayer(marker);
}Zoom Levels:
- Level 10: Regional view (1 tile)
- Level 11: City view (4 tiles)
- Level 12: District view (16 tiles)
- Level 13: Neighborhood view (64 tiles)
- Level 14: Street view (256 tiles)
- Level 15: Building view (1024 tiles)
- Level 16: Detail view (4096 tiles)
- Level 17: High detail (16384 tiles)
Storage for 5km x 5km:
Zoom 10-13: ~100 tiles × 25 KB = 2.5 MB
Zoom 14-15: ~1,280 tiles × 20 KB = 25 MB
Zoom 16-17: ~20,480 tiles × 15 KB = 300 MB
Total: ~330 MB for 5km × 5km area (all zoom levels)
Recommended: Skip zoom 17 unless needed
Total without Z17: ~30 MB
| Area Size | Zoom Levels | Storage | Use Case |
|---|---|---|---|
| 5km × 5km | 10-16 | ~30 MB | Small town, single neighborhood |
| 10km × 10km | 10-16 | ~150 MB | Medium city, disaster zone |
| 20km × 20km | 10-15 | ~400 MB | Large city, county |
| 50km × 50km | 10-14 | ~800 MB | Metro area, region |
Recommendation for EmergencyBox:
- Primary area: 10km × 10km @ zoom 10-16 (~150 MB)
- Extended area: 50km × 50km @ zoom 10-13 (~50 MB)
- Total: ~200 MB leaves plenty of room on 32GB USB
Markers: ~500 bytes each
500 markers = 250 KB
5,000 markers = 2.5 MB
Routes: ~1 KB each
100 routes = 100 KB
User locations: ~200 bytes each
50 users = 10 KB
Total database: <5 MB for heavy usage
✅ Negligible compared to map tiles
Problem: Loading 1,000+ markers at once is slow
Solution: Only load markers in viewport
map.on('moveend', () => {
const bounds = map.getBounds();
const north = bounds.getNorth();
const south = bounds.getSouth();
const east = bounds.getEast();
const west = bounds.getWest();
fetchMarkersInBounds(north, south, east, west);
});
async function fetchMarkersInBounds(n, s, e, w) {
const response = await fetch(
`/api/map/get_markers.php?n=${n}&s=${s}&e=${e}&w=${w}`
);
const markers = await response.json();
renderMarkers(markers);
}Backend (PHP):
// api/map/get_markers.php
$north = floatval($_GET['n']);
$south = floatval($_GET['s']);
$east = floatval($_GET['e']);
$west = floatval($_GET['w']);
$stmt = $db->prepare('
SELECT * FROM map_markers
WHERE lat BETWEEN :south AND :north
AND lon BETWEEN :west AND :east
AND active = 1
');
$stmt->bindValue(':north', $north);
$stmt->bindValue(':south', $south);
$stmt->bindValue(':east', $east);
$stmt->bindValue(':west', $west);Problem: Too many individual marker updates
Solution: Batch updates every 10 seconds
const pendingUpdates = [];
let batchTimer = null;
function queueMarkerUpdate(marker) {
pendingUpdates.push(marker);
if (!batchTimer) {
batchTimer = setTimeout(sendBatchUpdates, 10000);
}
}
async function sendBatchUpdates() {
if (pendingUpdates.length === 0) return;
await fetch('/api/map/batch_update.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ markers: pendingUpdates })
});
pendingUpdates.length = 0;
batchTimer = null;
}Problem: 500+ markers visible = slow rendering
Solution: Group nearby markers when zoomed out
// Use Leaflet.markercluster plugin (40 KB)
const markers = L.markerClusterGroup({
maxClusterRadius: 50,
disableClusteringAtZoom: 16, // Show all at street level
spiderfyOnMaxZoom: true
});
// Performance improvement:
// 500 markers → 20 clusters @ zoom 13
// Rendering: 500 DOM elements → 20 DOM elementsProblem: Black tiles while panning
Solution: Preload adjacent tiles
const tileLayer = L.tileLayer('/map_tiles/{z}/{x}/{y}.png', {
keepBuffer: 4, // Keep 4 tiles in each direction
updateWhenIdle: false,
updateWhenZooming: false
});Problem: Loading 100 marker icon images
Solution: Use CSS sprite sheet
.marker-icon {
width: 32px;
height: 32px;
background-image: url('/img/marker-sprites.png');
}
.marker-hazard { background-position: 0 0; }
.marker-safe { background-position: -32px 0; }
.marker-medical { background-position: -64px 0; }Problem: GPS updates 10+ times per second
Solution: Only send every 30 seconds
let lastLocationUpdate = 0;
const UPDATE_INTERVAL = 30000; // 30 seconds
navigator.geolocation.watchPosition((position) => {
const now = Date.now();
if (now - lastLocationUpdate > UPDATE_INTERVAL) {
updateUserLocation(position.coords);
lastLocationUpdate = now;
}
});Goal: Basic offline map with marker system
Tasks:
- Download map tiles for target area
- Integrate Leaflet.js library
- Create map container in UI
- Implement tile serving endpoint
- Create marker database schema
- Build marker CRUD API
- Add marker placement on map click
- Implement marker type selection
Deliverable: Users can view offline map and place basic markers
Goal: ATAK-style marker types and coordination
Tasks:
- Implement tactical marker types (hazard, safe zone, etc.)
- Add marker severity levels
- Create marker detail modal
- Integrate coordinate sharing in chat
- Add photo attachment to markers
- Implement marker filtering/layers
- Build marker search functionality
- Add marker editing/deletion
Deliverable: Full tactical marking system with chat integration
Goal: Distance, area, and route planning tools
Tasks:
- Implement distance measurement tool
- Add area measurement tool
- Create bearing/heading calculator
- Build route planning system
- Add waypoint management
- Implement route sharing
- Create KML/GPX export
- Add drawing tools (circles, polygons)
Deliverable: Complete measurement and planning toolkit
Goal: Geolocation, optimization, and polish
Tasks:
- Implement geolocation sharing
- Add marker clustering for performance
- Build lazy loading for markers
- Create layer control panel
- Add offline tile downloader utility
- Implement marker sync optimization
- Build admin moderation tools
- Mobile responsive optimization
- Write documentation
Deliverable: Production-ready ATAK-style mapping system
Using OpenStreetMap Tile Downloader:
# Install tile downloader
opkg install python3 python3-pip
pip3 install pyrosm
# Download tiles for area
python3 download_tiles.py \
--lat 37.7749 \
--lon -122.4194 \
--radius 10 \
--zoom-min 10 \
--zoom-max 16 \
--output /opt/share/www/map_tiles/Alternative: Manual Download
Use online tools:
Add to www/index.html:
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<!-- Map container -->
<div id="tactical-map" style="height: 600px;"></div>For offline use, download Leaflet.js locally:
cd /opt/share/www/js/
wget https://unpkg.com/leaflet@1.9.4/dist/leaflet.js
wget https://unpkg.com/leaflet@1.9.4/dist/leaflet.css -O ../css/leaflet.cssCreate www/js/map.js:
// Initialize map
const map = L.map('tactical-map').setView([37.7749, -122.4194], 13);
// Add offline tile layer
L.tileLayer('/map_tiles/{z}/{x}/{y}.png', {
maxZoom: 16,
minZoom: 10,
attribution: '© OpenStreetMap'
}).addTo(map);
// Add click handler for markers
map.on('click', (e) => {
console.log('Clicked at:', e.latlng);
// Show marker creation modal
});Create www/api/map/add_marker.php:
<?php
require_once '../config.php';
header('Content-Type: application/json');
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit(json_encode(['error' => 'Method not allowed']));
}
$input = json_decode(file_get_contents('php://input'), true);
// Validate input
if (!isset($input['lat']) || !isset($input['lon']) || !isset($input['type'])) {
http_response_code(400);
exit(json_encode(['error' => 'Missing required fields']));
}
$lat = floatval($input['lat']);
$lon = floatval($input['lon']);
$type = $input['type'];
$title = $input['title'] ?? 'Untitled';
$description = $input['description'] ?? '';
$severity = intval($input['severity'] ?? 1);
$created_by = $input['username'] ?? 'Anonymous';
try {
$db = getDB();
$stmt = $db->prepare('
INSERT INTO map_markers (type, lat, lon, title, description, severity, created_by)
VALUES (:type, :lat, :lon, :title, :description, :severity, :created_by)
');
$stmt->bindValue(':type', $type);
$stmt->bindValue(':lat', $lat);
$stmt->bindValue(':lon', $lon);
$stmt->bindValue(':title', $title);
$stmt->bindValue(':description', $description);
$stmt->bindValue(':severity', $severity);
$stmt->bindValue(':created_by', $created_by);
$result = $stmt->execute();
if ($result) {
echo json_encode([
'success' => true,
'marker' => [
'id' => $db->lastInsertRowID(),
'type' => $type,
'lat' => $lat,
'lon' => $lon,
'title' => $title,
'description' => $description,
'severity' => $severity,
'created_by' => $created_by
]
]);
} else {
http_response_code(500);
echo json_encode(['error' => 'Failed to save marker']);
}
$db->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
?>Create www/api/map/get_markers.php:
<?php
require_once '../config.php';
header('Content-Type: application/json');
try {
$db = getDB();
// Optional: Filter by bounds
if (isset($_GET['n']) && isset($_GET['s']) && isset($_GET['e']) && isset($_GET['w'])) {
$stmt = $db->prepare('
SELECT * FROM map_markers
WHERE lat BETWEEN :south AND :north
AND lon BETWEEN :west AND :east
AND active = 1
ORDER BY timestamp DESC
');
$stmt->bindValue(':north', floatval($_GET['n']));
$stmt->bindValue(':south', floatval($_GET['s']));
$stmt->bindValue(':east', floatval($_GET['e']));
$stmt->bindValue(':west', floatval($_GET['w']));
} else {
// Get all markers
$stmt = $db->prepare('SELECT * FROM map_markers WHERE active = 1 ORDER BY timestamp DESC');
}
$result = $stmt->execute();
$markers = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$markers[] = $row;
}
echo json_encode(['success' => true, 'markers' => $markers]);
$db->close();
} catch (Exception $e) {
http_response_code(500);
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
}
?>Add to www/api/init_db.php:
// Create map_markers table
$db->exec("
CREATE TABLE IF NOT EXISTS map_markers (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
lat REAL NOT NULL,
lon REAL NOT NULL,
title TEXT NOT NULL,
description TEXT,
severity INTEGER DEFAULT 1,
created_by TEXT,
photo_id INTEGER,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
active INTEGER DEFAULT 1
)
");
$db->exec("
CREATE TABLE IF NOT EXISTS map_routes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
waypoints TEXT NOT NULL,
distance REAL,
created_by TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
");
$db->exec("
CREATE TABLE IF NOT EXISTS user_locations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
lat REAL NOT NULL,
lon REAL NOT NULL,
accuracy REAL,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
");| Library | Size | Purpose |
|---|---|---|
| Leaflet.js | 40 KB | Core mapping library |
| Leaflet.markercluster | 40 KB | Marker clustering |
| Leaflet.draw | 60 KB | Drawing tools |
| Leaflet.GeometryUtil | 15 KB | Distance/area calculations |
| Leaflet.Control.Geocoder | 30 KB | Address search (optional) |
Total: ~185 KB (gzipped: ~60 KB)
- OpenStreetMap: https://www.openstreetmap.org/
- Humanitarian OSM: https://www.hotosm.org/
- USGS Topo Maps: https://www.usgs.gov/
- Offline Tile Tools: https://mobac.sourceforge.io/
- Leaflet.js Docs: https://leafletjs.com/reference.html
- OpenStreetMap Wiki: https://wiki.openstreetmap.org/
- ATAK Info: https://www.civtak.org/
| Feature | Military ATAK | EmergencyBox ATAK-Lite |
|---|---|---|
| Platform | Android native app | Web-based (any device) |
| Map rendering | Client-side | Client-side |
| Tile storage | On-device | On-router (shared) |
| Marker sync | Mesh network | WiFi + SQLite |
| Users | 100-1000s | 20-50 |
| Route calc | Server-side | Client-side JS |
| Geolocation | GPS + GLONASS | GPS (if available) |
| Update rate | 10+ Hz real-time | 0.1 Hz (every 10s) |
| Complexity | Very high | Medium |
| Installation | App store | Web browser |
| Cost | Free (gov) / $$ (civilian) | Free and open source |
- Use marker clustering for 100+ markers
- Implement viewport-based marker loading
- Batch marker updates (10-second intervals)
- Throttle geolocation updates (30+ seconds)
- Enable browser tile caching
- Use CSS sprite sheets for icons
- Compress tile images (PNG optimization)
- Lazy load marker details
- Disable animations on low-end devices
- Monitor SQLite query performance
- GPS locations are sensitive - make opt-in
- Allow users to delete their location history
- Consider marker moderation for public deployments
- Add admin authentication for marker deletion
- Rate-limit marker creation (prevent spam)
- Validate all coordinates (prevent injection)
- No internet = reduced attack surface
- Still sanitize all inputs
- Use HTTPS if deploying over WAN
- WebSocket real-time marker sync
- Multi-router mesh synchronization
- Offline geocoding (address search)
- Custom map overlays (weather, satellite)
- Voice annotations on markers
- AR marker viewing (mobile)
- Track recording (breadcrumb trails)
- 3D terrain visualization
- Integration with AIS/APRS data
- Emergency broadcast alerts on map
ATAK-style tactical mapping is absolutely feasible on the ASUS RT-AC68U router. The key is smart architecture: let the router serve static files and simple data, while browsers do the heavy lifting.
Expected Performance:
- ✅ 50 concurrent users
- ✅ 1-3 second initial map load
- ✅ <100ms marker updates
- ✅ 10-25% CPU usage
- ✅ ~150MB RAM usage
This transforms EmergencyBox from a chat/file sharing tool into a true tactical coordination platform for disaster relief.
Last Updated: 2026-01-11 Version: 1.0 Author: EmergencyBox Community License: MIT