Skip to content

Latest commit

 

History

History
1285 lines (1022 loc) · 33.9 KB

File metadata and controls

1285 lines (1022 loc) · 33.9 KB

ATAK-Style Tactical Mapping for EmergencyBox

Offline tactical mapping inspired by ATAK (Android Tactical Assault Kit) for disaster relief and emergency coordination


Table of Contents


Overview

What is ATAK-Style Mapping?

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.

Key Features

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

Use Cases

  • 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

Hardware Capabilities

ASUS RT-AC68U Specifications

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

Can the Router Handle ATAK Features?

Short Answer: YES ✅

The AC68U is surprisingly capable for tactical mapping when designed correctly.

Performance by Feature

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 ⚠️ Throttle to 10-30 second intervals
Complex route calculation HIGH - CPU intensive ⚠️ Do on client-side (JavaScript)
Many markers (500+) MEDIUM - Memory usage ⚠️ Use marker clustering
Server-side rendering VERY HIGH - Too CPU heavy ❌ Avoid - use client-side rendering

Architecture Design

Client-Heavy Design Philosophy

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                            │
└───────────────────────────────────────────────────┘

Technology Stack

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

Database Schema

-- 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
);

Performance Analysis

Scenario 1: 20 Users, Light Usage

CPU: 5-8%
RAM: 80MB
Bandwidth: ~5 Mbps (initial tile load)
           ~50 Kbps steady state (marker updates)

✅ EASY - Router barely notices

Scenario 2: 50 Users, Active Usage

CPU: 15-25%
RAM: 150MB
Bandwidth: ~15 Mbps (initial loads)
           ~200 Kbps steady state

✅ TOTALLY FINE - Well within capacity

Scenario 3: 50 Users, Heavy Usage

CPU: 25-40%
RAM: 180MB
Bandwidth: ~500 Kbps sustained

⚠️ OK - But optimize marker updates (batch them)

Bandwidth Analysis

Initial Page Load (Per User)

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

Active Usage (All Users)

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

Map Pan/Zoom

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

Stress Test Results

Worst Case: All Users Load Map Simultaneously

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

Worst Case: Marker Spam

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

Feature Implementation Guide

Core Features

1. Offline Map Tiles

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)

2. Tactical Markers

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)}`
        );
    }
}

3. Distance Measurement

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`;
    }
}

4. Area Measurement

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²`;
    }
}

5. Coordinate Sharing in Chat

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);
}

6. Photo Attachment to Markers

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);
    }
}

7. Geolocation Sharing

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;
    }
}

8. Marker Clustering (Performance)

// 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 markers

9. Layer Filtering

const 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);
}

Storage Requirements

Map Tile Storage Calculations

Example: Downtown Area (5km x 5km)

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

Recommended Coverage Areas

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

Database Storage

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

Optimization Strategies

1. Lazy Loading Markers

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);

2. Update Batching

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;
}

3. Marker Clustering

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 elements

4. Tile Preloading

Problem: 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
});

5. Icon Sprite Sheets

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; }

6. Throttle Location Updates

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;
    }
});

Implementation Roadmap

Phase 1: Core Mapping (Week 1)

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


Phase 2: Tactical Features (Week 2)

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


Phase 3: Measurement & Planning (Week 3)

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


Phase 4: Advanced Features (Week 4)

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


Quick Start Guide

1. Download Map Tiles

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:

2. Integrate Leaflet.js

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.css

3. Initialize Map

Create 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
});

4. Create Marker API

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()]);
}
?>

5. Initialize Database

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
    )
");

Additional Resources

Recommended Libraries

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)

Map Tile Sources

Documentation


Comparison: EmergencyBox vs ATAK

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

Performance Tuning Checklist

  • 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

Security Considerations

Data Privacy

  • GPS locations are sensitive - make opt-in
  • Allow users to delete their location history
  • Consider marker moderation for public deployments

Access Control

  • Add admin authentication for marker deletion
  • Rate-limit marker creation (prevent spam)
  • Validate all coordinates (prevent injection)

Offline Security

  • No internet = reduced attack surface
  • Still sanitize all inputs
  • Use HTTPS if deploying over WAN

Future Enhancements

Potential Additions

  • 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

Conclusion

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