This will be a step by step on how I have setup my Storm Chasing Current Location Map here on my website to utilize GPSGate Splitter software and a NMEA GPS puck to transmit my location every 30 seconds to my website and display it on a Google map in real time.

Requirements

You will need a hardware GPS Puck which is sending NMEA data to the computer. The computer need to run the GPSGate Splitter software

Configure GPSGate Splitter

Once GPSGate Splitter is installed, you will want to get the input setup. There are plenty of documents to get it to work with your specific GPS puck. Once you have good input, you will receive a message of “GPS data with valid position” such as this

Setting up Output Tab

Setup the output tab with the URL you will be putting the perl script below. First click on GpsGate.com (Send) and click add.

You will be given a dialogue to enter the URL. Click Connect Options on the next screen.

Now in connect options you need to choose HTTP for the protocol. Under server put in your URL to the PHP script and choose 80 for the port. Click OK and then next. For the username and password you will want to make something up.

GPSGate.php Script to process GPS Data

There are a few variables to replace, including database credentials for caching API calls and logging GPS data to the database. You’ll also want to specify XML and JSON file paths and create authentication credentials. Just make something up. For the Google API Key, you will need to create a Google Maps API key to use with this project.

<?php
// Configuration
$db_name = "[YOUR_DB_NAME]"; // Replace with your database name
$db_user = "[YOUR_DB_USER]"; // Replace with your database username
$db_pass = "[YOUR_DB_PASS]"; // Replace with your database password
$db_host = "[YOUR_DB_HOST]"; // Replace with your database host

$xml_filename = "[YOUR_XML_FILE_PATH]"; // Replace with path to XML file
$json_filename = "[YOUR_JSON_FILE_PATH]"; // Replace with path to JSON file
$auth = ['[YOUR_USERNAME]' => '[YOUR_PASSWORD]']; // Replace with your authentication credentials
$google_api_key = '[YOUR_GOOGLE_API_KEY]'; // Replace with your Google Maps API key

// Cache durations (in seconds)
$cache_duration_geocode = 24 * 60 * 60; // 24 hours for geocoding
$cache_duration_metar = 15 * 60; // 15 minutes for METAR

// Get CGI input
$in = $_REQUEST;

// Format data
format_data();

// Initialize database and ensure cache and gps_data tables exist
init_db();

// Set content type
header("Content-type: text/html");

// Authentication check
if (isset($auth[$username]) && $password === invertString($auth[$username])) {
    write_db_entry();
    write_xml_file();
    write_json_file();
    echo "Great Success";
} else {
    echo "Failure";
}

function init_db() {
    global $db_name, $db_user, $db_pass, $db_host;

    try {
        $dsn = "mysql:host=$db_host;dbname=$db_name;charset=utf8";
        $pdo = new PDO($dsn, $db_user, $db_pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]);

        // Create api_cache table if it doesn't exist
        $query_api_cache = "
            CREATE TABLE IF NOT EXISTS `api_cache` (
                `id` INT AUTO_INCREMENT PRIMARY KEY,
                `api_type` VARCHAR(50) NOT NULL,
                `request_key` VARCHAR(255) NOT NULL,
                `response_data` TEXT NOT NULL,
                `created_at` DATETIME NOT NULL,
                `expires_at` DATETIME NOT NULL,
                INDEX `idx_api_type_key` (`api_type`, `request_key`),
                INDEX `idx_expires_at` (`expires_at`)
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
        ";
        $pdo->exec($query_api_cache);

        // Create gps_data table if it doesn't exist
        $query_gps_data = "
            CREATE TABLE IF NOT EXISTS `gps_data` (
                `id` INT(11) NOT NULL AUTO_INCREMENT,
                `time` DATETIME NOT NULL,
                `gps_time` VARCHAR(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                `gps_date` VARCHAR(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                `lat` DECIMAL(10,6) NOT NULL,
                `lon` DECIMAL(10,6) NOT NULL,
                `alt` FLOAT NOT NULL,
                `speed` DECIMAL(5,3) NOT NULL,
                `heading` INT(11) NOT NULL,
                `dir` VARCHAR(4) NOT NULL,
                `user` VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                PRIMARY KEY (`id`)
            ) ENGINE=InnoDB DEFAULT CHARSET=latin1 COLLATE=latin1_swedish_ci
        ";
        $pdo->exec($query_gps_data);
    } catch (PDOException $e) {
        die("Database initialization error: " . $e->getMessage());
    }
}

function get_cached_response($api_type, $request_key, $cache_duration) {
    global $db_name, $db_user, $db_pass, $db_host;

    try {
        $dsn = "mysql:host=$db_host;dbname=$db_name;charset=utf8";
        $pdo = new PDO($dsn, $db_user, $db_pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]);

        $query = "SELECT response_data FROM `api_cache` WHERE api_type = :api_type AND request_key = :request_key AND expires_at > NOW()";
        $stmt = $pdo->prepare($query);
        $stmt->execute(['api_type' => $api_type, 'request_key' => $request_key]);
        $result = $stmt->fetch();

        if ($result) {
            return json_decode($result['response_data'], true);
        }
        return null;
    } catch (PDOException $e) {
        error_log("Cache read error: " . $e->getMessage());
        return null;
    }
}

function cache_response($api_type, $request_key, $response_data, $cache_duration) {
    global $db_name, $db_user, $db_pass, $db_host;

    try {
        $dsn = "mysql:host=$db_host;dbname=$db_name;charset=utf8";
        $pdo = new PDO($dsn, $db_user, $db_pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]);

        // Delete expired or duplicate entries
        $delete_query = "DELETE FROM `api_cache` WHERE api_type = :api_type AND request_key = :request_key";
        $delete_stmt = $pdo->prepare($delete_query);
        $delete_stmt->execute(['api_type' => $api_type, 'request_key' => $request_key]);

        // Insert new cache entry
        $insert_query = "INSERT INTO `api_cache` (api_type, request_key, response_data, created_at, expires_at) VALUES (:api_type, :request_key, :response_data, NOW(), DATE_ADD(NOW(), INTERVAL :cache_duration SECOND))";
        $insert_stmt = $pdo->prepare($insert_query);
        $insert_stmt->execute([
            'api_type' => $api_type,
            'request_key' => $request_key,
            'response_data' => json_encode($response_data),
            'cache_duration' => $cache_duration
        ]);
    } catch (PDOException $e) {
        error_log("Cache write error: " . $e->getMessage());
    }
}

function write_db_entry() {
    global $db_name, $db_user, $db_pass, $db_host, $currenttime, $gps_time, $gps_date, $lat, $lng, $alt, $speed, $heading, $direction, $username;

    $currenttime = time();
    try {
        $dsn = "mysql:host=$db_host;dbname=$db_name;charset=utf8";
        $pdo = new PDO($dsn, $db_user, $db_pass, [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
        ]);

        $query = "INSERT INTO `$db_name`.`gps_data` (`time`, `gps_time`, `gps_date`, `lat`, `lon`, `alt`, `speed`, `heading`, `dir`, `user`) VALUES (FROM_UNIXTIME(:currenttime), :gps_time, :gps_date, :lat, :lon, :alt, :speed, :heading, :dir, :user)";
        $stmt = $pdo->prepare($query);
        $stmt->execute([
            'currenttime' => $currenttime,
            'gps_time' => $gps_time,
            'gps_date' => $gps_date,
            'lat' => $lat,
            'lon' => $lng,
            'alt' => $alt,
            'speed' => $speed,
            'heading' => $heading,
            'dir' => $direction,
            'user' => $username
        ]);
    } catch (PDOException $e) {
        die("Database error: " . $e->getMessage());
    }
}

function load_stations() {
    $stations_file = '[YOUR_STATIONS_FILE_PATH]'; // Replace with path to stations JSON file
    if (!file_exists($stations_file)) {
        error_log("Stations file not found: $stations_file");
        return [];
    }
    $stations_data = file_get_contents($stations_file);
    $stations = json_decode($stations_data, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        error_log("Error decoding stations JSON: " . json_last_error_msg());
        return [];
    }
    return $stations;
}

function haversine_distance($lat1, $lon1, $lat2, $lon2) {
    $lat1 = deg2rad($lat1);
    $lon1 = deg2rad($lon1);
    $lat2 = deg2rad($lat2);
    $lon2 = deg2rad($lon2);
    $dlat = $lat2 - $lat1;
    $dlon = $lon2 - $lon1;
    $a = sin($dlat / 2) ** 2 + cos($lat1) * cos($lat2) * sin($dlon / 2) ** 2;
    $c = 2 * atan2(sqrt($a), sqrt(1 - $a));
    $earth_radius = 3958.8; // Earth's radius in miles
    return $earth_radius * $c;
}

function get_closest_station($lat, $lon, $stations) {
    $min_distance = PHP_INT_MAX;
    $closest_station = null;
    foreach ($stations as $station) {
        $distance = haversine_distance($lat, $lon, $station['lat'], $station['lon']);
        if ($distance < $min_distance) {
            $min_distance = $distance;
            $closest_station = $station;
        }
    }
    return $closest_station;
}

function get_metar_data($lat, $lng) {
    global $cache_duration_metar;

    // Truncate to 2 decimal places for cache key
    $lat = number_format($lat, 2, '.', '');
    $lng = number_format($lng, 2, '.', '');
    $request_key = "metar:$lat,$lng";

    // Check cache
    $cached = get_cached_response('metar', $request_key, $cache_duration_metar);
    if ($cached !== null) {
        return $cached;
    }

    // Load METAR stations
    $stations = load_stations();
    if (empty($stations)) {
        error_log("No METAR stations available");
        return null;
    }

    // Find the closest station
    $closest_station = get_closest_station($lat, $lng, $stations);
    if (!$closest_station) {
        error_log("Could not determine closest METAR station");
        return null;
    }

    $station_id = $closest_station['icao'];
    // Fetch METAR for the closest station
    $metar_url = "https://aviationweather.gov/api/data/metar?ids=$station_id&format=json";
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $metar_url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $metar_response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = null;
    if ($http_code !== 200) {
        error_log("METAR data API error for station $station_id: HTTP $http_code, Response: $metar_response");
    } else {
        $metar_data = json_decode($metar_response, true);
        if (!empty($metar_data) && isset($metar_data[0])) {
            $metar = $metar_data[0];
            $result = [
                'station' => $station_id,
                'temperature' => round($metar['temp'] * 9/5 + 32), // Convert C to F
                'dewpoint' => round($metar['dewp'] * 9/5 + 32),  // Convert C to F
                'wind_speed' => round($metar['wspd'] * 1.15078), // Convert knots to MPH
                'wind_direction' => $metar['wdir'] ?? 0
            ];
            // error_log("METAR data retrieved for $station_id: " . json_encode($result));
        } else {
            error_log("METAR data API: No valid METAR data for station $station_id, Response: " . json_encode($metar_data));
        }
    }

    // Cache the result (including null if no data retrieved)
    cache_response('metar', $request_key, $result, $cache_duration_metar);
    return $result;
}

function get_nearby_location($lat, $lng) {
    global $google_api_key, $cache_duration_geocode;

    // Truncate to 2 decimal places for cache key
    $lat = number_format($lat, 2, '.', '');
    $lng = number_format($lng, 2, '.', '');
    $request_key = "geocode:$lat,$lng";

    // Check cache
    $cached = get_cached_response('geocode', $request_key, $cache_duration_geocode);
    if ($cached !== null) {
        return $cached;
    }

    // Make API call
    $url = "https://maps.googleapis.com/maps/api/geocode/json?latlng=$lat,$lng&key=$google_api_key";
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $response = curl_exec($ch);
    $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($http_code !== 200) {
        error_log("Geocode API error: HTTP $http_code, Response: $response");
        return ['city' => 'Unknown', 'state' => 'Unknown'];
    }

    $data = json_decode($response, true);

    $result = ['city' => 'Unknown', 'state' => 'Unknown'];
    if ($data['status'] === 'OK') {
        foreach ($data['results'] as $result_data) {
            foreach ($result_data['address_components'] as $component) {
                if (in_array('locality', $component['types'])) {
                    $result['city'] = $component['long_name'];
                }
                if (in_array('administrative_area_level_1', $component['types'])) {
                    $result['state'] = $component['short_name'];
                }
            }
            if (isset($result['city']) && $result['city'] !== 'Unknown' && isset($result['state']) && $result['state'] !== 'Unknown') {
                break;
            }
        }
    } else {
        error_log("Geocode API error: Status {$data['status']}, Response: " . json_encode($data));
    }

    // Cache the result
    cache_response('geocode', $request_key, $result, $cache_duration_geocode);
    return $result;
}

function write_xml_file() {
    global $xml_filename, $gps_datetime, $lat, $lng, $alt, $heading, $direction;

    // Get nearby location and METAR data
    $location = get_nearby_location($lat, $lng);
    $metar = get_metar_data($lat, $lng);

    // Create XML using DOMDocument
    $dom = new DOMDocument('1.0', 'UTF-8');
    $dom->formatOutput = true;
    $dom->preserveWhiteSpace = false;

    $gpsdata = $dom->createElement('gpsdata');
    $entry = $dom->createElement('gpsdata');
    $entry->setAttribute('time', $gps_datetime);
    $entry->setAttribute('lat', $lat);
    $entry->setAttribute('lng', $lng);
    $entry->setAttribute('alt', $alt);
    $entry->setAttribute('heading', $heading);
    $entry->setAttribute('direction', $direction);
    $entry->setAttribute('city', $location['city']);
    $entry->setAttribute('state', $location['state']);

    if ($metar) {
        $entry->setAttribute('weather_station', $metar['station']);
        $entry->setAttribute('temperature_f', $metar['temperature']);
        $entry->setAttribute('dewpoint_f', $metar['dewpoint']);
        $entry->setAttribute('wind_speed_mph', $metar['wind_speed']);
        $entry->setAttribute('wind_direction', $metar['wind_direction']);
    }

    $gpsdata->appendChild($entry);
    $dom->appendChild($gpsdata);

    $xml = $dom->saveXML();
    file_put_contents($xml_filename, $xml) or die("Cannot write to $xml_filename");
}

function write_json_file() {
    global $json_filename, $gps_datetime, $lat, $lng, $alt, $heading, $direction;

    // Get nearby location and METAR data (reuse from XML to avoid redundant API calls)
    $location = get_nearby_location($lat, $lng);
    $metar = get_metar_data($lat, $lng);

    // Create JSON data structure
    $data = [
        'gpsdata' => [
            'time' => $gps_datetime,
            'lat' => $lat,
            'lng' => $lng,
            'alt' => $alt,
            'heading' => $heading,
            'direction' => $direction,
            'city' => $location['city'],
            'state' => $location['state']
        ]
    ];

    if ($metar) {
        $data['gpsdata'] += [
            'weather_station' => $metar['station'],
            'temperature_f' => $metar['temperature'],
            'dewpoint_f' => $metar['dewpoint'],
            'wind_speed_mph' => $metar['wind_speed'],
            'wind_direction' => $metar['wind_direction']
        ];
    }

    // Write JSON to file
    $json = json_encode($data, JSON_PRETTY_PRINT);
    file_put_contents($json_filename, $json) or die("Cannot write to $json_filename");
}

function format_data() {
    global $in, $lng, $lat, $alt, $speed, $heading, $gps_date, $gps_time, $username, $password, $gps_datetime, $date_time_cdt, $direction, $dspdirection;

    // Assign variables
    $lng = isset($in['longitude']) ? $in['longitude'] : null;
    $lat = isset($in['latitude']) ? $in['latitude'] : null;
    $alt = isset($in['altitude']) ? $in['altitude'] : 0;
    $speed = isset($in['speed']) ? $in['speed'] : 0;
    $heading = isset($in['heading']) ? $in['heading'] : 0;
    $gps_date = isset($in['date']) ? $in['date'] : null;
    $gps_time = isset($in['time']) ? $in['time'] : null;
    $username = isset($in['username']) ? $in['username'] : null;
    $password = isset($in['pw']) ? $in['pw'] : null;

    // Validate GPS information for lat & lon
    if (!preg_match('/^([+-]?)(?:180(?:\.0+)?|1[0-7]\d(?:\.\d+)?|[0-9]?\d(?:\.\d+)?)$/', $lng) || $lng < -180 || $lng > 180) {
        die("Invalid longitude");
    }
    if (!preg_match('/^([+-]?)(?:90(?:\.0+)?|[0-8]?\d(?:\.\d+)?)$/', $lat) || $lat < -90 || $lat > 90) {
        die("Invalid latitude");
    }

    // Log input coordinates for debugging
    // error_log("Input coordinates: lat=$lat, lng=$lng");

    // Truncate decimals
    $gps_time = preg_replace('/\.\d*/', '', $gps_time);
    $heading = preg_replace('/\.\d*/', '', $heading);

    // Convert altitude from meters to feet and truncate decimals
    $alt = (float)$alt * 3.28084;
    $alt = (int)$alt;

    // Convert speed from knots to MPH and truncate decimals
    $speed = (float)$speed * 1.15078;
    $speed = (int)$speed;

    // Format time correctly
    $gps_time = preg_replace('/(\d{2})(\d{2})(\d{2})/', '$1:$2:$3', $gps_time);
    $gps_date = preg_replace('/(\d{4})(\d{2})(\d{2})/', '$1-$2-$3', $gps_date);

    // Combine date and time
    $gps_datetime = "$gps_date $gps_time";
    $date_time_cdt = utc_to_cdt($gps_datetime);

    // Set heading direction
    $direction = heading_to_direction($heading);

    $dspdirection = ($speed < 3) ? "STATIONARY" : $direction;
}

function utc_to_cdt($utc_date) {
    if (!preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $utc_date)) {
        return null;
    }

    $dt = new DateTime($utc_date, new DateTimeZone('UTC'));
    $dt->setTimezone(new DateTimeZone('America/Chicago'));
    return $dt->format('Y-m-d H:i:s');
}

function heading_to_direction($heading) {
    $heading = $heading % 360;
    if ($heading < 0) {
        $heading += 360;
    }

    $directions = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'];
    $index = (int)(($heading + 11.25) / 22.5) % 16;
    return $directions[$index];
}

function invertString($str) {
    $newStr = '';
    for ($i = strlen($str) - 1; $i >= 0; $i--) {
        $char = $str[$i];
        if (preg_match('/[0-9]/', $char)) {
            $newStr .= chr(9 - (ord($char) - ord('0')) + ord('0'));
        } elseif (preg_match('/[a-z]/', $char)) {
            $newStr .= chr((ord('z') - ord('a')) - (ord($char) - ord('a')) + ord('A'));
        } elseif (preg_match('/[A-Z]/', $char)) {
            $newStr .= chr((ord('Z') - ord('A')) - (ord($char) - ord('A')) + ord('a'));
        }
    }
    return $newStr;
}
?>

This needs to go on a web server running HTTP port 80. HTTPS (443) will not work.

Google Map HTML

<!DOCTYPE html>
<html>
<head>
  <title>Location Tracker</title> <!-- Generic title -->
  <meta name="viewport" content="initial-scale=1.0">
  <meta charset="utf-8">
  <style>
    #map {
      height: calc(100% - 100px); /* Adjust height to account for update div */
      width: 100%;
    }

    html, body {
      height: 100%;
      margin: 0;
      padding: 0;
      overflow: hidden; /* Prevent scrollbars on the page */
    }

    #update {
      height: 100px; /* Fixed height for the update div */
      box-sizing: border-box;
      padding: 10px;
      font-family: Arial, sans-serif;
      font-size: 14px;
      background-color: #f9f9f9;
      border-top: 1px solid #ddd;
      overflow: auto; /* Allow scrolling within update if needed */
    }
    #update p {
      margin: 5px 0;
    }
    #update strong {
      color: #333;
    }
    .error {
      color: red;
    }
    .custom-marker {
      width: 26px;
      height: 26px;
      display: block;
    }
  </style>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <!-- Use public jQuery CDN -->
</head>
<body>
  <div id="map"></div>
  <div id="update"></div>
  <script>
    let map;
    let markers = [];
    let lastCenter = null;

    function initMap() {
      map = new google.maps.Map(document.getElementById('map'), {
        center: { lat: 35, lng: -97 },
        zoom: 10,
        mapId: '[YOUR_MAP_ID]' // Replace with your Google Maps Map ID
      });
      loadMarker();
      refresh();
    }

    function refresh() {
      setInterval(() => {
        if (!document.hidden) {
          loadMarker();
        }
      }, 60000);
    }

    function deleteMarkers() {
      markers.forEach(marker => { marker.map = null; });
      markers = [];
    }

    function convertUTCDateToLocalDate(date) {
      const newDate = new Date(date.getTime() + date.getTimezoneOffset() * 60 * 1000);
      const offset = date.getTimezoneOffset() / 60;
      newDate.setHours(date.getHours() - offset);
      return newDate;
    }

    function formatDate(date) {
      return date.toLocaleString('en-US', {
        month: 'long',
        day: 'numeric',
        year: 'numeric',
        hour: 'numeric',
        minute: '2-digit',
        hour12: true
      });
    }

    function getCompassDirection(degrees) {
      const directions = [
        'N', 'NNE', 'NE', 'ENE', 
        'E', 'ESE', 'SE', 'SSE', 
        'S', 'SSW', 'SW', 'WSW', 
        'W', 'WNW', 'NW', 'NNW'
      ];
      const index = Math.round((degrees % 360) / 22.5) % 16;
      return directions[index];
    }

    async function loadMarker() {
      const page = new Date().getTime();
      $.ajax({
        url: "[YOUR_JSON_URL]", // Replace with your JSON data URL
        dataType: 'json',
        data: { page },
        success: async function (data) {
          deleteMarkers();
          const gpsData = data.gpsdata;
          let updateInfo = '';

          if (gpsData) {
            const { time, lat, lng, alt, city, state, heading, direction, temperature_f, dewpoint_f, wind_speed_mph, wind_direction, weather_station } = gpsData;
            const localTime = convertUTCDateToLocalDate(new Date(time));
            const latNum = parseFloat(lat);
            const lngNum = parseFloat(lng);
            const windCompass = getCompassDirection(wind_direction);

            if (!lastCenter || lastCenter.lat !== latNum || lastCenter.lng !== lngNum) {
              map.setCenter({ lat: latNum, lng: lngNum });
              lastCenter = { lat: latNum, lng: lngNum };
            }

            const markerImage = document.createElement('img');
            markerImage.src = '[YOUR_MARKER_ICON_URL]'; // Replace with your marker icon URL
            markerImage.className = 'custom-marker';

            const marker = new google.maps.marker.AdvancedMarkerElement({
              map,
              position: { lat: latNum, lng: lngNum },
              title: time,
              content: markerImage
            });

            markers.push(marker);

            updateInfo = `
              <p><strong>Last Updated:</strong> ${formatDate(localTime)} // <strong>UTC Time:</strong> ${time}</p>
              <p><strong>Location:</strong> ${city || 'Unknown'}, ${state || 'Unknown'} (Lat: ${latNum.toFixed(2)}, Lng: ${lngNum.toFixed(2)}, Altitude: ${alt} ft, Heading: ${heading}° (${direction}))</p>
              <p><strong>Weather (Station: ${weather_station || 'Unknown'}):</strong> Temperature: ${temperature_f}°F, Dewpoint: ${dewpoint_f}°F, Wind: ${windCompass} at ${wind_speed_mph} mph</p>
            `;
          }

          document.getElementById('update').innerHTML = updateInfo || '<p>No data available</p>';
        },
        error: function () {
          document.getElementById('update').innerHTML = 
            '<p class="error">Failed to load location data. Please try again later.</p>';
        }
      });
    }
  </script>
  <script 
    src="https://maps.googleapis.com/maps/api/js?key=[YOUR_API_KEY]&libraries=marker&callback=initMap" 
    async defer>
  </script> <!-- Replace [YOUR_API_KEY] with your Google Maps API key -->
</body>
</html>

More details coming soon..