<?php

/**
 * Returnes true if user prefers Imperial Units
 *
 * @return boolean
 */
function isImperialUnitUser()
{
  return defined('IMPERIAL_UNITS') ? IMPERIAL_UNITS : $GLOBALS['tdSession']->isImperialUnitUser();
}

/**
 * Convert km to miles (or kmh to mph)
 *
 * @param  {float} value
 * @return float
 */
function convertKilometerToMile($value)
{
    return $value * 0.621371192;
}

/**
 * Convert meter to feet
 *
 * @param  {float} value
 * @return float
 */
function convertMeterToFeet($value)
{
    return $value * 3.2808399;
}

/**
 * Convert meter per second to miles per second
 *
 * @param  {float} value
 * @return float
 */
function convertMpsToMph($value)
{
    return $value * 2.23693629;
}

/**
 * Convert meter to yard
 *
 * @param  {float} value
 * @return float
 */
function convertMeterToYard($value)
{
    return $value * 1.0936133;
}

/**
 * Convert mm to inches
 *
 * @param  {float} value
 * @return float
 */
function convertMmToInch($value)
{
    return $value * 0.0393700787;
}

/**
 * Convert celcius to fahrenheit
 *
 * @param  {float} value
 * @return float
 */
function convertCelciusToFahrenheit($value)
{
    return $value * (9/5) + 32;
}

/**
 * Calculate wind chill and return celcius temperature
 *
 * @param  {float} temp Temperature in degrees celcius
 * @param  {float} speed Wind speed in m/s
 * @return float
 */
function calculateMetricWindChill($temp, $speed)
{
  // Convert windspeed m/s to Km/h
  $speed *= 3.6;

  if ($temp > 10) return null;
  if ($speed <= 4.8) return $temp;

  return 13.12 + (0.6215 * $temp) + ((0.3965 * $temp - 11.37) * pow($speed, 0.16));
}

/**
 * Calculate wind chill and return fahrenheit temperature
 *
 * @param  {float} temp Temperature in degrees celcius
 * @param  {float} speed Wind speed in m/s
 * @return float
 */
function calculateImperialWindChill($temp, $speed)
{
  $value = calculateMetricWindChill($temp, $speed);
  if ($value)
  {
    $value = convertCelciusToFahrenheit($value);
  }

  return $value;
}

/**
 * Convert hPa/mbar to mmhg
 *
 * @param  {float} value
 * @return float
 */
function convertMbarToMmhg($value)
{
    return $value * 0.75006375541921;
}

/**
 * Convert hPa/mbar to inches hg
 *
 * @param  {float} value
 * @return float
 */
function convertMbarToInchHg($value)
{
    return $value * 0.0295301;
}

/**
 * Convert a value based on its unit
 *
 * @param  {float} value
 * @param  {string} unit
 * @return float
 */
function universalDataUnitConvert($value, $unit)
{
  if (!isImperialUnitUser()) return array('value' => $value, 'unit' => $unit);
  $unit = trim($unit);

  if ($unit == 'C') return array('value' => round(convertCelciusToFahrenheit($value), 2), 'unit' => 'F');
  return array('value' => $value, 'unit' => $unit);
}

/**
 * Returnes true if value is float
 *
 * @param  mixed $value
 * @return boolean
 */
function isFloat($value)
{
    if (is_numeric($value))
    {
        // PHP automagically tries to coerce $value to a number
        return is_float($value + 0);
    }
    return false;
}

/**
 * Returnes true if value is int
 *
 * @param  mixed $value
 * @return boolean
 */
function isInt($value)
{
    return (ctype_digit(strval($value)));
}

/**
 * Replace first occurence
 *
 * @param  string $search
 * @param  string $replace
 * @param  string $string
 * @return string
 */
function str_replace_first($search, $replace, $string)
{
    $pos = strpos($string, $search);
    if ($pos !== false)
    {
        return substr_replace($string, $replace, $pos, strlen($search));
    }
    else
    {
        return $string;
    }
}

/**
 * Gets the APRS data source filter from an HTTP request
 *
 * @param  array $defaultValues (Default data source values to use if none set)
 * @param  string $method (INPUT_GET or INPUT_POST)
 * @param  varname $string (Key to look for, default 'source')
 * @return array (Data sources)
 */
function getAPRSFilterSourceFromRequest($defaultValues = [SOURCE_APRS,SOURCE_CWOP,SOURCE_CBAPRS,SOURCE_OGN], $method = INPUT_GET, $varname = 'source')
{
  $source = array_filter(array_unique(filter_input($method, $varname, FILTER_VALIDATE_INT, ['flags' => FILTER_REQUIRE_ARRAY | FILTER_NULL_ON_FAILURE | FILTER_FORCE_ARRAY, 'options' => ['min_range'=> SOURCE_APRS, 'max_range'=> SOURCE_OGN]]) ?: $defaultValues));
  if (sizeof($source) == 0) $source = $defaultValues;

  return $source;
}

/*
 * Returnes symbol description
 *
 * @param string $symbolTable
 * @param string $symbol
 * @param boolean $includeUndefinedOverlay
 * @return string
 */
function getSymbolDescription($symbolTable, $symbol, $includeUndefinedOverlay)
{
    if ($symbolTable == '/')
    {
        switch ($symbol)
        {
        case '!':
            return 'Police, Sheriff';
        case '"':
            return 'No Symbol';
        case '#':
            return 'Digipeater';
        case '$':
            return 'Phone';
        case '%':
            return 'DX Cluster';
        case '&':
            return 'HF Gateway';
        case '\'':
            return 'Small Aircraft';
        case '(':
            return 'Mobile Satellite Station';
        case ')':
            return 'Wheelchair (handicapped)';
        case '*':
            return 'Snowmobile';
        case '+':
            return 'Red Cross';
        case ',':
            return 'Scout (Boy Scout)';
        case '-':
            return 'House (VHF)';
        case '.':
            return 'X';
        case '/':
            return 'Red Dot';
        case '0':
            return 'Circle (0)';
        case '1':
            return 'Circle (1)';
        case '2':
            return 'Circle (2)';
        case '3':
            return 'Circle (3)';
        case '4':
            return 'Circle (4)';
        case '5':
            return 'Circle (5)';
        case '6':
            return 'Circle (6)';
        case '7':
            return 'Circle (7)';
        case '8':
            return 'Circle (8)';
        case '9':
            return 'Circle (9)';
        case ':':
            return 'Fire';
        case ';':
            return 'Campground or Portable operation';
        case '<':
            return 'Motorcycle';
        case '=':
            return 'Railroad Engine';
        case '>':
            return 'Car';
        case '?':
            return 'File server';
        case '@':
            return 'Hurricane prediction';
        case 'A':
            return 'Aid Station';
        case 'B':
            return 'BBS or PBBS';
        case 'C':
            return 'Canoe';
        case 'D':
            return 'No Symbol'; // was originally undefined
        case 'E':
            return 'Event (Special live event)'; // Eyeball
        case 'F':
            return 'Farm Vehicle (tractor)';
        case 'G':
            return 'Grid Square (6 digit)';
        case 'H':
            return 'Hotel (blue bed symbol)';
        case 'I':
            return 'TCP/IP network station';
        case 'J':
            return 'No Symbol';
        case 'K':
            return 'School';
        case 'L':
            return 'Logged-ON user (or PC User)';
        case 'M':
            return 'MacAPRS';
        case 'N':
            return 'NTS Station';
        case 'O':
            return 'Balloon';
        case 'P':
            return 'Police';
        case 'Q':
            return 'TBD';
        case 'R':
            return 'Recreational Vehicle';
        case 'S':
            return 'Shuttle';
        case 'T':
            return 'SSTV';
        case 'U':
            return 'Bus';
        case 'V':
            return 'ATV';
        case 'W':
            return 'National Weather Service';
        case 'X':
            return 'Helicopter';
        case 'Y':
            return 'Yacht (sail)';
        case 'Z':
            return 'WinAPRS';
        case '[':
            return 'Human/Person';
        case '\\':
            return 'Triangle (DF station)';
        case ']':
            return 'Mail/PostOffice';
        case '^':
            return 'Large Aircraft';
        case '_':
            return 'Weather Station';
        case '`':
            return 'Dish Antenna';
        case 'a':
            return 'Ambulance';
        case 'b':
            return 'Bike';
        case 'c':
            return 'Incident Command Post';
        case 'd':
            return 'Fire Station';
        case 'e':
            return 'Horse (equestrian)';
        case 'f':
            return 'Fire Truck';
        case 'g':
            return 'Glider';
        case 'h':
            return 'Hospital';
        case 'i':
            return 'IOTA (islands on the air)';
        case 'j':
            return 'Jeep';
        case 'k':
            return 'Truck';
        case 'l':
            return 'Laptop';
        case 'm':
            return 'Mic-E Repeater';
        case 'n':
            return 'Node (black bulls-eye)';
        case 'o':
            return 'EOC';
        case 'p':
            return 'Rover (puppy, or dog)';
        case 'q':
            return 'Grid square';
        case 'r':
            return 'Antenna';
        case 's':
            return 'Power Boat';
        case 't':
            return 'Truck Stop';
        case 'u':
            return 'Truck (18 wheeler)';
        case 'v':
            return 'Van';
        case 'w':
            return 'Water Station';
        case 'x':
            return 'xAPRS (Unix)';
        case 'y':
            return 'Yagi @ QTH';
        case 'z':
            return 'Shelter';
        case '{':
            return 'No Symbol';
        case '|':
            return 'TNC Stream Switch';
        case '}':
            return 'No Symbol';
        case '~':
            return 'TNC Stream Switch';
        }

    }
    else
    {
        switch ($symbol)
        {
        case '!':
            switch ($symbolTable)
            {
            case "\\":
                return 'Emergency!';
            case 'E':
                return 'ELT or EPIRB';
            case 'V':
                return 'Volcanic Eruption or Lava';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Emergency (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '"':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '#':
            switch ($symbolTable)
            {
            case '\\':
                return 'Digipeater';
            case 'A':
                return 'Digipeater (An Alt-Freq input digi)';
            case 'R':
                return 'Digipeater (RELAY only)';
            case 'W':
                return 'Digipeater (RELAY and WIDE)';
            case 'T':
                return 'Digipeater (PacComm RELAY,WIDE and TRACE)';
            case 'N':
                return 'Digipeater (WIDEn-N and relay,wide,trace)';
            case 'I':
                return 'Digipeater (digipeater is also an IGate)';
            case 'L':
                return 'Digipeater (LIMITED New n-N Paradigm digi)';
            case 'S':
                return 'Digipeater (New n-N Paradigm digi)';
            case 'P':
                return 'Digipeater (PacComm, New n-N Paradigm)';
            case 'U':
                return 'Digipeater (UI-DIGI firmware)';
            case 'D':
                return 'Digipeater (DIGI_NED)';
            case '1':
                return 'Digipeater (Fill-in digi, 1-hop limit)';
            case 'X':
                return 'Digipeater (Experimental)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Digipeater (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '$':
            switch ($symbolTable)
            {
            case '\\':
                return 'Bank or ATM';
            case 'U':
                return 'ATM (US dollars)';
            case 'L':
                return 'ATM (Brittish Pound)';
            case 'Y':
                return 'ATM (Japanese Yen)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Bank or ATM (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case '%':
            switch ($symbolTable)
            {
            case '\\':
                return 'Power/Energy';
            case 'C':
                return 'Power/Energy (Coal)';
            case 'E':
                return 'Power/Energy (Emergency)';
            case 'G':
                return 'Power/Energy (Geothermal)';
            case 'H':
                return 'Power/Energy (Hydroelectric)';
            case 'N':
                return 'Power/Energy (Nuclear)';
            case 'P':
                return 'Power/Energy (Portable)';
            case 'S':
                return 'Power/Energy (Solar)';
            case 'T':
                return 'Power/Energy (Turbine)';
            case 'W':
                return 'Power/Energy (Wind)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Power/Energy';
                }
                else
                {
                    return null;
                }
            }
        case '&':
            switch ($symbolTable)
            {
            case '\\':
                return 'Gateway';
            case 'I':
                return 'Igate (Generic)';
            case 'R':
                return 'Receive only IGate';
            case 'P':
                return 'PSKmail node';
            case 'T':
                return 'TX Igate (with path set to 1 hop)';
            case 'W':
                return 'WIRES-X';
            case '2':
                return 'TX igate (with path set to 2 hops)';
            case 'D':
                return 'D-STAR (D-PRS IGate)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Gateway (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '\'':
            switch ($symbolTable)
            {
            case '\\':
                return 'Incident site (Airplane Crash Site)';
            case 'A':
                return 'Incident site (Automobile crash site)';
            case 'H':
                return 'Incident site (Hazardous incident)';
            case 'M':
                return 'Incident site (Multi-Vehicle crash site)';
            case 'P':
                return 'Incident site (Pileup)';
            case 'T':
                return 'Incident site (Truck wreck)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Incident site';
                }
                else
                {
                    return null;
                }
            }
        case '(':
            switch ($symbolTable)
            {
            case "\\":
                return 'Cloudy';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Cloudy';
                }
                else
                {
                    return null;
                }
            }

        case ')':
            switch ($symbolTable)
            {
            case "\\":
                return 'Firenet MEO (Modis Earth Observatory)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Firenet MEO (Modis Earth Observatory)';
                }
                else
                {
                    return null;
                }
            }

        case '*':
            switch ($symbolTable)
            {
            case "\\":
                return 'Snow';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Snow';
                }
                else
                {
                    return null;
                }
            }

        case '+':
            switch ($symbolTable)
            {
            case "\\":
                return 'Church';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Church';
                }
                else
                {
                    return null;
                }
            }

        case ',':
            switch ($symbolTable)
            {
            case "\\":
                return 'Scout (Girl Scout)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Scout (Girl Scout)';
                }
                else
                {
                    return null;
                }
            }

        case '-':
            switch ($symbolTable)
            {
            case '\\':
                return 'House (HF)';
            case '5':
                return 'House (50 Hz mains power)';
            case '6':
                return 'House (60 Hz mains power)';
            case 'B':
                return 'House (Backup Battery Power)';
            case 'C':
                return 'House (Ham club)';
            case 'E':
                return 'House (Emergency power)';
            case 'G':
                return 'House (Geothermal)';
            case 'H':
                return 'House (Hydro powered)';
            case 'O':
                return 'House (Operator Present)';
            case 'S':
                return 'House (Solar Powered)';
            case 'W':
                return 'House (Wind powered)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'House (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case '.':
            switch ($symbolTable)
            {
            case "\\":
                return 'Unknown Position';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Unknown Position';
                }
                else
                {
                    return null;
                }
            }

        case '/':
            switch ($symbolTable)
            {
            case "\\":
                return 'Waypoint Destination';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Waypoint Destination';
                }
                else
                {
                    return null;
                }
            }

        case '0':
            switch ($symbolTable)
            {
            case '\\':
                return 'Circle';
            case 'A':
                return 'Allstar Node';
            case 'E':
                return 'Echolink Node';
            case 'I':
                return 'IRLP repeater';
            case 'S':
                return 'Staging Area';
            case 'V':
                return 'Echolink and IRLP (VOIP)';
            case 'W':
                return 'WIRES (Yaesu VOIP)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Circle (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '1':
        case '2':
        case '3':
        case '4':
        case '5':
        case '6':
        case '7':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '8':
            switch ($symbolTable)
            {
            case '\\':
                return 'No Symbol';
            case '8':
                return '802.11 network node';
            case 'G':
                return '802.11G';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '9':
            switch ($symbolTable) {
            case "\\":
                return 'Gas Station';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Gas Station';
                }
                else
                {
                    return null;
                }
            }

        case ':':
            switch ($symbolTable)
            {
            case "\\":
                return 'Hail (weather condition)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Hail (weather condition)';
                }
                else
                {
                    return null;
                }
            }

        case ';':
            switch ($symbolTable)
            {
            case '\\':
                return 'Portable, Park or Picnic';
            case 'F':
                return 'Field Day';
            case 'I':
                return 'Islands on the air';
            case 'S':
                return 'Summits on the air';
            case 'W':
                return 'WOTA';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Portable';
                }
                else
                {
                    return null;
                }
            }

        case '<':
            switch ($symbolTable)
            {
            case "\\":
                return 'Advisory (single gale flag)';
            default:
                if ($includeUndefinedOverlay) {
                    return 'Advisory (single gale flag)';
                }
                else
                {
                    return null;
                }
            }

        case '=':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '>':
            switch ($symbolTable)
            {
            case '\\':
                return 'Car';
            case '1':
            case '2':
            case '3':
            case '4':
            case '5':
            case '6':
            case '7':
            case '8':
            case '9':
            case '0':
                return 'Numbered Car (' . $symbolTable . ')';
            case 'E':
                return 'Car (Electric)';
            case 'H':
                return 'Car (Hybrid)';
            case 'S':
                return 'Car (Solar powered)';
            case 'V':
                return 'Car (GM Volt)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Car (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case '?':
            switch ($symbolTable)
            {
            case "\\":
                return 'Info Kiosk';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Info Kiosk';
                }
                else
                {
                    return null;
                }
            }

        case '@':
            switch ($symbolTable)
            {
            case "\\":
                return 'Hurricane/Tropical storm';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Hurricane/Tropical storm (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'A':
            switch ($symbolTable)
            {
            case '\\':
                return 'Box';
            case '9':
                return 'Mobile DTMF user';
            case '7':
                return 'HT DTMF user';
            case 'H':
                return 'House DTMF user';
            case 'E':
                return 'Echolink DTMF report';
            case 'I':
                return 'IRLP DTMF report';
            case 'R':
                return 'RFID report';
            case 'A':
                return 'AllStar DTMF report';
            case 'D':
                return 'D-Star report';
            case 'X':
                return 'OLPC Laptop XO';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Box (' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'B':
            switch ($symbolTable)
            {
            case "\\":
                return 'Blowing snow';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Blowing snow';
                }
                else
                {
                    return null;
                }
            }

        case 'C':
            switch ($symbolTable)
            {
            case "\\":
                return 'Coast Guard';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Coast Guard';
                }
                else
                {
                    return null;
                }
            }

        case 'D':
            switch ($symbolTable)
            {
            case '\\':
                return 'Depot'; // drizzle rain moved to ' ovlyD
            case 'A':
                return 'Airport';
            case 'F':
                return 'Ferry Landing';
            case 'H':
                return 'Heloport';
            case 'R':
                return 'Rail Depot';
            case 'B':
                return 'Bus Depot';
            case 'L':
                return 'LIght Rail or Subway';
            case 'S':
                return 'Seaport Depot';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Depot (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'E':
            switch ($symbolTable)
            {
            case '\\':
                return 'Smoke';
            case 'H':
                return 'Haze';
            case 'S':
                return 'Smoke';
            case 'B':
                return 'Blowing Snow'; // was \B
            case 'D':
                return 'Blowing Dust or Sand'; // was \b
            case 'F':
                return 'Fog'; // was \{
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Smoke (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'F':
            switch ($symbolTable)
            {
            case "\\":
                return 'Freezing rain';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Freezing rain';
                }
                else
                {
                    return null;
                }
            }

        case 'G':
            switch ($symbolTable) {
            case "\\":
                return 'Snow Shower';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Snow Shower';
                }
                else
                {
                    return null;
                }
            }

        case 'H':
            switch ($symbolTable)
            {
            case '\\':
                return 'Haze';
            case 'R':
                return 'Radiation detector';
            case 'W':
                return 'Hazardous Waste';
            case 'X':
                return 'Skull&Crossbones';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Hazard (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'I':
            switch ($symbolTable)
            {
            case "\\":
                return 'Rain Shower';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Rain Shower';
                }
                else
                {
                    return null;
                }
            }

        case 'J':
            switch ($symbolTable)
            {
            case "\\":
                return 'Lightning';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Lightning';
                }
                else
                {
                    return null;
                }
            }

        case 'K':
            switch ($symbolTable)
            {
            case "\\":
                return 'Kenwood HT';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Kenwood HT';
                }
                else
                {
                    return null;
                }
            }

        case 'L':
            switch ($symbolTable)
            {
            case "\\":
                return 'Lighthouse';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Lighthouse (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'M':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case 'N':
            switch ($symbolTable)
            {
            case "\\":
                return 'Navigation Buoy';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Navigation Buoy (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'O':
            switch ($symbolTable)
            {
            case '\\':
                return 'Rocket (amateur)';
            case 'B':
                return 'Blimp (non-rigid airship)';
            case 'M':
                return 'Manned Balloon';
            case 'T':
                return 'Teathered Balloon';
            case 'C':
                return 'Constant Pressure Balloon - Long duration';
            case 'R':
                return 'Rocket bearing Balloon (Rockoon)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Balloon (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'P':
            switch ($symbolTable)
            {
            case "\\":
                return 'Parking';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Parking (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'Q':
            switch ($symbolTable)
            {
            case "\\":
                return 'Quake';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Quake';
                }
                else
                {
                    return null;
                }
            }

        case 'R':
            switch ($symbolTable)
            {
            case '\\':
                return 'Restaurant (generic)';
            case '7':
                return 'Restaurant (7/11)';
            case 'K':
                return 'Restaurant (KFC)';
            case 'M':
                return 'Restaurant (McDonalds)';
            case 'T':
                return 'Restaurant (Taco Bell)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Restaurant (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'S':
            switch ($symbolTable)
            {
            case "\\":
                return 'Satellite/Pacsat';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Satellite/Pacsat (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'T':
            switch ($symbolTable)
            {
            case "\\":
                return 'Thunderstorm';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Thunderstorm (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'U':
            switch ($symbolTable)
            {
            case "\\":
                return 'Sunny';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Sunny (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'V':
            switch ($symbolTable)
            {
            case "\\":
                return 'VORTAC Navigation Aid';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'VORTAC Navigation Aid';
                }
                else
                {
                    return null;
                }
            }

        case 'W':
            switch ($symbolTable)
            {
            case "\\":
                return 'National Weather Service';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'National Weather Service (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'X':
            switch ($symbolTable)
            {
            case "\\":
                return 'Pharmacy';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Pharmacy';
                }
                else
                {
                    return null;
                }
            }

        case 'Y':
            switch ($symbolTable)
            {
            case '\\':
                return 'Radio/APRS Device';
            case 'A':
                return 'Alinco';
            case 'B':
                return 'Byonics';
            case 'I':
                return 'Icom';
            case 'K':
                return 'Kenwood';
            case 'Y':
                return 'Yaesu/Standard';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Radio/APRS Device'; // or "No Symbol"?
                }
                else
                {
                    return null;
                }
            }
        case 'Z':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '[':
            switch ($symbolTable)
            {
            case '\\':
                return 'Wall Cloud (or pedestal cloud)';
            case 'B':
                return 'Baby on board (stroller, pram etc)';
            case 'S':
                return 'Skier';
            case 'R':
                return 'Runner';
            case 'H':
                return 'Hiker';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Human/Person (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '\\':
            switch ($symbolTable)
            {
            case '\\':
                return 'No Symbol';
            case 'A':
                return 'Avmap G5';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }
        case ']':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '^':
            switch ($symbolTable)
            {
            case '\\':
                return 'Aircraft'; // top-view originally intended to point in direction of flight
            case 'A':
                return 'Aircraft (A = Autonomous)';
            case 'D':
                return 'Aircraft (D = Drone)';
            case 'E':
                return 'Aircraft (E = Electric aircraft)';
            case 'G':
                return 'Aircraft (G = Glider aircraft)';
            case 'H':
                return 'Aircraft (H = Hovercraft)';
            case 'J':
                return 'Aircraft (J = JET)';
	    case 'L':
                return 'Paraglider'; // Not official definition
            case 'M':
                return 'Aircraft (M = Missle)';
            case 'P':
                return 'Aircraft (P = Propeller)';
            case 'R':
                return 'Aircraft (R = Remotely Piloted)';
            case 'S':
                return 'Aircraft (S = Solar Powered)';
            case 'V':
                return 'Aircraft (V = Vertical takeoff)';
            case 'X':
                return 'Aircraft (X = Experimental)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Aircraft (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '_':
            switch ($symbolTable)
            {
            case "\\":
                return 'Weather Station';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Weather Station (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '`':
            switch ($symbolTable)
            {
            case "\\":
                return 'Rain';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Rain';
                }
                else
                {
                    return null;
                }
            }

        case 'a':
            switch ($symbolTable)
            {
            case '\\':
                return 'Red Diamond';
            case 'A':
                return 'ARES';
            case 'D':
                return 'D-STAR (previously Dutch ARES)';
            case 'G':
                return 'RSGB Radio Society of Great Brittan';
            case 'R':
                return 'RACES';
            case 'S':
                return 'SATERN Salvation Army';
            case 'W':
                return 'WinLink';
            case 'Y':
                return 'C4FM Yaesu repeaters';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Red Diamond (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'b':
            switch ($symbolTable)
            {
            case "\\":
                return 'Dust blowing';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Dust blowing';
                }
                else
                {
                    return null;
                }
            }

        case 'c':
            switch ($symbolTable)
            {
            case '\\':
                return 'Civil Defense';
            case 'D':
                return 'Decontamination';
            case 'R':
                return 'RACES';
            case 'S':
                return 'SATERN mobile canteen';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Civil Defense (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'd':
            switch ($symbolTable)
            {
            case "\\":
                return 'DX Spot';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'DX Spot';
                }
                else
                {
                    return null;
                }
            }

        case 'e':
            switch ($symbolTable)
            {
            case "\\":
                return 'Sleet';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Sleet';
                }
                else
                {
                    return null;
                }
            }

        case 'f':
            switch ($symbolTable)
            {
            case "\\":
                return 'Funnel Cloud';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Funnel Cloud';
                }
                else
                {
                    return null;
                }
            }

        case 'g':
            switch ($symbolTable)
            {
            case "\\":
                return 'Gale warning flags';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Gale warning flags (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'h':
            switch ($symbolTable)
            {
            case '\\':
                return 'Store/Ham Store';
            case 'F':
                return 'Hamfest';
            case 'H':
                return 'Home Dept etc..';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Other store (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 'i':
            switch ($symbolTable)
            {
            case "\\":
                return 'Point of interest';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Point of interest (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'j':
            switch ($symbolTable)
            {
            case "\\":
                return 'Work Zone';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Work Zone';
                }
                else
                {
                    return null;
                }
            }

        case 'k':
            switch ($symbolTable)
            {
            case '\\':
                return 'Special Vehicle (SUV)';
            case '4':
                return 'Special Vehicle (4x4)';
            case 'A':
                return 'Special Vehicle (ATV)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Special Vehicle (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'l':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case 'm':
            switch ($symbolTable)
            {
            case "\\":
                return 'Milepost';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Milepost';
                }
                else
                {
                    return null;
                }
            }

        case 'n':
            switch ($symbolTable)
            {
            case "\\":
                return 'Red Triangle';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Red Triangle (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'o':
            switch ($symbolTable)
            {
            case "\\":
                return 'Small Circle';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Small Circle (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'p':
            switch ($symbolTable)
            {
            case "\\":
                return 'Partly Cloudy';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Partly Cloudy';
                }
                else
                {
                    return null;
                }
            }

        case 'q':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case 'r':
            switch ($symbolTable)
            {
            case "\\":
                return 'Restrooms';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Restrooms';
                }
                else
                {
                    return null;
                }
            }

        case 's':
            switch ($symbolTable)
            {
            case '\\':
                return 'Ship/Boat';
            case '6':
                return 'Ship/Boat (Shipwreck "deep6")';
            case 'B':
                return 'Ship/Boat (Pleasure Boat)';
            case 'C':
                return 'Ship/Boat (Cargo)';
            case 'D':
                return 'Ship/Boat (Diving)';
            case 'E':
                return 'Ship/Boat (Emergency or Medical transport)';
            case 'F':
                return 'Ship/Boat (Fishing)';
            case 'H':
                return 'Ship/Boat (High-speed Craft)';
            case 'J':
                return 'Ship/Boat (Jet Ski)';
            case 'L':
                return 'Ship/Boat (Law enforcement)';
            case 'M':
                return 'Ship/Boat (Miltary)';
            case 'O':
                return 'Ship/Boat (Oil Rig)';
            case 'P':
                return 'Ship/Boat (Pilot Boat)';
            case 'Q':
                return 'Ship/Boat (Torpedo)';
            case 'S':
                return 'Ship/Boat (Search and Rescue)';
            case 'T':
                return 'Ship/Boat (Tug)';
            case 'U':
                return 'Ship/Boat (Underwater ops or submarine)';
            case 'W':
                return 'Ship/Boat (Wing-in-Ground effect or Hovercraft)';
            case 'X':
                return 'Ship/Boat (Passenger, Ferry)';
            case 'Y':
                return 'Ship/Boat (Sailing, large ship)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Ship/Boat (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }
        case 't':
            switch ($symbolTable)
            {
            case "\\":
                return 'Tornado';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Tornado';
                }
                else
                {
                    return null;
                }
            }

        case 'u':
            switch ($symbolTable)
            {
            case '\\':
                return 'Truck';
            case 'B':
                return 'Truck (Buldozer/construction/Backhoe)';
            case 'G':
                return 'Truck (Gas)';
            case 'P':
                return 'Truck (Plow or SnowPlow)';
            case 'T':
                return 'Truck (Tanker)';
            case 'C':
                return 'Truck (Chlorine Tanker)';
            case 'H':
                return 'Truck (Hazardous)';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Truck (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'v':
            switch ($symbolTable)
            {
            case "\\":
                return 'Van';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Van (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case 'w':
            switch ($symbolTable)
            {
            case "\\":
                return 'Flooding';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Flooding';
                }
                else
                {
                    return null;
                }
            }

        case 'x':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case 'y':
            switch ($symbolTable)
            {
            case "\\":
                return 'Skywarn';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Skywarn';
                }
                else
                {
                    return null;
                }
            }

        case 'z':
            switch ($symbolTable)
            {
            case '\\':
                return 'Shelter';
            case 'C':
                return 'Clinic';
            case 'G':
                return 'Government building';
            case 'M':
                return 'Morgue';
            case 'T':
                return 'Triage';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Building (with overlay ' . $symbolTable . ')';
                }
                else
                {
                    return null;
                }
            }

        case '{':
            switch ($symbolTable)
            {
            case "\\":
                return 'Fog';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'Fog';
                }
                else
                {
                    return null;
                }
            }

        case '|':
            switch ($symbolTable)
            {
            case "\\":
                return 'TNC Stream Switch';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'TNC Stream Switch';
                }
                else
                {
                    return null;
                }
            }

        case '}':
            switch ($symbolTable)
            {
            case "\\":
                return 'No Symbol';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'No Symbol';
                }
                else
                {
                    return null;
                }
            }

        case '~':
            switch ($symbolTable)
            {
            case "\\":
                return 'TNC Stream Switch';
            default:
                if ($includeUndefinedOverlay)
                {
                    return 'TNC Stream Switch';
                }
                else
                {
                    return null;
                }
            }
        }
    }
}


/**
 * Returnes true if we may show data older than 24h
 *
 * @return boolean
 */
function isAllowedToShowOlderData()
{
    $isAllowedToShowOlderData = false;
    $config = getConfig('application_server');

    if (isset($config) && is_array($config))
    {
        if (isset($config['allow_time_travel']))
        {
            if ($config['allow_time_travel'] == '1')
            {
                $isAllowedToShowOlderData = true;
            }
        }
    }

    return $isAllowedToShowOlderData;
}

/**
 * Returnes valid view path
 *
 * @param {string} $request
 * @return string
 */
function getView($request)
{
  if (is_null($request)) return null;
  $parts = explode("/", trim($request, '/'));
  if (count($parts) >= 2)
  {
    $page = array_pop($parts);
    $dir = array_pop($parts);
    if ($page && ($dir == 'views' || $dir == 'station' || $dir == 'account' || $dir == 'events'))
    {
      $path = ROOT . '/public/' . $dir . '/' . $page;
      if (file_exists($path)) return $path;
    }
  }
  return null;
}

/**
 * Helper function to return an assoc array containing the bootstrap config
 *
 * @param {string} $key
 * @return string
 * */
function getBootstrapConfig()
{
  global $bootstrap_config;
  return $bootstrap_config['bootstrap'];
}

/**
 * Helper function to return an assoc array containing website related values from config
 *
 * @param {string} $key
 * @return string
 * */
function getWebsiteConfig($key)
{
    return getConfig('website', $key);
}

/**
 * Helper function to return an assoc array containing backend database related values from config
 *
 * @param {string} $key
 * @return string
 * */
function getBackendDatabaseConfig($key)
{
  return getConfig('backend_database', $key);
}

/**
 * Helper function to return an assoc array containing database related values from config
 *
 * @param {string} $key
 * @return string
 * */
function getDatabaseConfig($key)
{
  return getConfig('database', $key);
}

/**
 * Helper function to return an assoc array containing websocket related values from config
 *
 * @param {string} $key
 * @return string
 * */
function getWebsocketConfig($key)
{
  return getConfig('application_server', $key);
}

/**
 * Helper function to return an assoc array containing SMTP related values from config
 *
 * @param {string} $key
 * @return string
 * */
function getEmailConfig($key)
{
    return getConfig('email', $key);
}

/**
 * Returns an associative array containing related key values from the specified section in the configuration.
 *
 * @param string $section The section of the configuration to retrieve values from.
 * @param string|null $key The specific key to retrieve. If null, the entire section is returned.
 * @return array|null Returns the value associated with the key, the entire section, or null if not found.
 */
function getConfig(string $section, ?string $key = null): mixed
{
    global $config;

    if (isset($config[$section]))
    {
        if ($key === null)
        {
            return $config[$section];
        }

        if (isset($config[$section][$key]))
        {
            return $config[$section][$key];
        }
    }

    return null; // Explicitly return null if the section or key is not found
}


/**
 * Echo a language phrase from $id to the browser
 *
 * @param {string} $id - The phrase key
 * @param {int} $itemCount - Number of items the phrase refers to (for pluralization)
 * @return none
 */
function _L($id, $itemCount = 1)
{
  global $language;
  if (isset($language['en-us']["$id"]))
  {
    if ($itemCount == 1)
      echo $language['en-us'][$id]['s'] ?? 'literal missing';
    else
      echo $language['en-us'][$id]['p'] ?? 'literal missing';
  }
  else echo 'literal missing';
}


/**
 * Returns a language phrase from $id
 *
 * @param {string} $id - The phrase key
 * @param {int} $itemCount - Number of items the phrase refers to (for pluralization)
 * @return {string} - The phrase from the language
 */
function _LANG($id, $itemCount = 1)
{
  global $language;
  if (isset($language['en-us']["$id"]))
  {
    if ($itemCount == 1)
      return $language['en-us'][$id]['s'];
    else
      return $language['en-us'][$id]['p'];
  }
  else return 'literal missing';
}


/**
 * Convert coordinate to pixel position in heatmap image
 *
 * @param {float} $lat
 * @param {int} $zoom
 * @param {int} $imageTileSize
 * @return int
 */
function getLatPixelCoordinate($lat, $zoom, $imageTileSize)
{
    $pixelGlobeSize = $imageTileSize * pow(2, $zoom);
    $yPixelsToRadiansRatio = $pixelGlobeSize / (2 * M_PI);
    $halfPixelGlobeSize = $pixelGlobeSize / 2;
    $pixelGlobeCenterY = $halfPixelGlobeSize;
    $degreesToRadiansRatio = 180 / M_PI;
    $siny = sin($lat * M_PI / 180);

    # Truncating to 0.9999 effectively limits latitude to 89.189. This is
    # about a third of a tile past the edge of the world tile.
    if ($siny < -0.9999)
    {
        $siny = -0.9999;
    }
    if ($siny > 0.9999)
    {
        $siny = 0.9999;
    }
    $latY = round($pixelGlobeCenterY + 0.5 * log((1 + $siny) / (1 - $siny)) * -$yPixelsToRadiansRatio);
    return $latY;
}

/**
 * Convert coordinate to pixel position in heatmap image
 *
 * @param {float} $lng
 * @param {int} $zoom
 * @param {int} $imageTileSize
 * @return int
 */
function getLngPixelCoordinate($lng, $zoom, $imageTileSize)
{
    $scale = 1 << $zoom;
    $lngX = floor($imageTileSize * (0.5 + $lng / 360) * $scale);
    return $lngX;
}

/**
 * Convert pixel position in heatmap image to coordinate
 *
 * @param {float} $latPixelCoord
 * @param {int} $zoom
 * @param {int} $imageTileSize
 * @return float
 */
function getLatFromLatPixelCoordinate($latPixelCoord, $zoom, $imageTileSize)
{
    $pixelGlobeSize = $imageTileSize * pow(2, $zoom);
    $yPixelsToRadiansRatio = $pixelGlobeSize / (2 * M_PI);
    $halfPixelGlobeSize = $pixelGlobeSize / 2;
    $pixelGlobeCenterY = $halfPixelGlobeSize;
    $degreesToRadiansRatio = 180 / M_PI;
    $lat = (2 * atan(exp(($latPixelCoord - $pixelGlobeCenterY) / -$yPixelsToRadiansRatio)) - M_PI / 2) * $degreesToRadiansRatio;
    return $lat;
}

/**
 * Convert pixel position in heatmap image to coordinate
 *
 * @param {float} $lngPixelCoord
 * @param {int} $zoom
 * @param {int} $imageTileSize
 * @return float
 */
function getLngFromLngPixelCoordinate($lngPixelCoord, $zoom, $imageTileSize)
{
    $scale = 1 << $zoom;
    $lng = ((($lngPixelCoord / $scale) / $imageTileSize) - 0.5) * 360;
    return $lng;
}

/**
 * Get the closest timezone from coordinates
 * Original credit to ben@jp
 * https://stackoverflow.com/questions/3126878/get-php-timezone-name-from-latitude-and-longitude
 *
 * @param {float} $latitude
 * @param {float} $longitude
 * @param {string} $countryCode (optional)
 * @return string
 */
function getNearestTimezone($latitude, $longitude, $countryCode = '')
{
  if (is_null($latitude) || is_null($longitude)) return null;

  $countryCode = strtoupper(trim($countryCode));
  $timezone_ids = ($countryCode) ? DateTimeZone::listIdentifiers(DateTimeZone::PER_COUNTRY, $countryCode) : DateTimeZone::listIdentifiers();
  if($timezone_ids && is_array($timezone_ids) && isset($timezone_ids[0]))
  {
    $time_zone = '';
    $tz_distance = 0;
    $timezone_obj = null;

    //only one identifier?
    if (count($timezone_ids) == 1)
    {
      $time_zone = $timezone_ids[0];
    }
    else
    {
      foreach($timezone_ids as $timezone_id)
      {
        $timezone = new DateTimeZone($timezone_id);
        $location = $timezone->getLocation();
        $tz_lat   = $location['latitude'];
        $tz_long  = $location['longitude'];

        $theta    = $longitude - $tz_long;
        $distance = (sin(deg2rad($latitude)) * sin(deg2rad($tz_lat))) + (cos(deg2rad($latitude)) * cos(deg2rad($tz_lat)) * cos(deg2rad($theta)));
        $distance = acos($distance);
        $distance = abs(rad2deg($distance));

        if (!$time_zone || $tz_distance > $distance)
        {
          $time_zone   = $timezone_id;
          $tz_distance = $distance;
          $timezone_obj = $timezone;
        }
      }
    }
    return $timezone_obj;
  }
  return null;
}


/**
 * Get the maindenhead grid square from latitiude and longitude
 *
 * @param {float} $latitude
 * @param {float} $longitude
 * @param {int} $precision (optional, min/default = 3, max = 5)
 * @return string
 */
 define('ORD_A', ord('A'));
 define('ORD_0', ord('0'));
 function getMaidenheadFromCoordinates($latitude, $longitude, $precision = 3)
 {
     if ($precision < 3 || $precision > 5) $precision = 3;

     $lat = $latitude + 90;
     $lon = $longitude + 180;

     // Pair 1
     $gs = chr(ORD_A + floor($lon / 20)) . chr(ORD_A + floor($lat / 10));
     $lon = fmod($lon, 20);
     if ($lon < 0) $lon += 20;
     $lat = fmod($lat, 10);
     if ($lat < 0) $lat += 10;

     // Pair 2
     $gs .= chr(ORD_0 + floor($lon / 2)) . chr(ORD_0 + floor($lat / 1));
     $lon = fmod($lon, 2);
     if ($lon < 0) $lon += 2;
     $lat = fmod($lat, 1);
     if ($lat < 0) $lat += 1;

     // Pair 3
     $gs .= chr(ORD_A + floor($lon * 12)) . chr(ORD_A + floor($lat * 24));
     $lon = fmod($lon, (1/12));
     if ($lon < 0) $lon += (1/12);
     $lat = fmod($lat, (1/24));
     if ($lat < 0) $lat += (1/24);

     // Pair 4
     if ($precision > 3)
     {
       $gs .= chr(ORD_0 + floor($lon * 120)) . chr(ORD_0 + floor($lat * 240));
       $lon = fmod($lon, (1/120));
       if ($lon < 0) $lon += (1/120);
       $lat = fmod($lat, (1/240));
       if ($lat < 0) $lat += (1/240);
     }

     // Pair 5
     if ($precision > 4)
     {
       $gs .= chr(ORD_A + floor($lon * 120 * 24)) . chr(ORD_A + floor($lat * 240 * 24));
       $lon = fmod($lon, (1/120/24));
       if ($lon < 0) $lon += (1/120/24);
       $lat = fmod($lat, (1/240/24));
       if ($lat < 0) $lat += (1/240/24);
     }
     return $gs;
 }

 // Imported from repository...  Object fetch is slower, so we will not use it here
 function getTelemetryValue($telemetryPacket, $valNumber, $rootTelemetryPacket)
 {
     $val = $telemetryPacket["val{$valNumber}"];

     $eqns = $rootTelemetryPacket->getEqnsValue($valNumber);

     $a = $eqns[0];
     $b = $eqns[1];
     $c = $eqns[2];

     if ($a == null && $b == null)
     {
         // User has sent us a faulty eqns, just return raw value
         return $val;

     }
     else
     {
         $result = 0;

         if ($a != null)
         {
             $result += $a * $val * $val;
         }

         if ($b != null)
         {
             $result += $b * $val;
         }

         if ($c != null)
         {
             $result += $c;
         }

         return $result;
     }
 }


/**
* Converts degrees to cardinal directions
*
* @param {int} $degrees
* @return string
*/
function convertDegreesToCardinalDirection($degrees)
{
  if (!isInt($degrees) || $degrees > 360) return '';
	return ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW', 'N'][round($degrees / 22.5)];
}


/**
* Parses a Mic-E Destination Address
*
* @param {char} $c
* @return array
* */
function getInfoForDestinationAddressChar($c)
{
 //0 - 0, 1 - 1 custom, 2 - 1 std
 $asciiCode = ord($c);
 $ret = ['latDigit'=> 0, 'message'=> 0, 'isNorth'=> false, 'longOffset'=> 0, 'isWest'=> false];

 if ($c >= '0' && $c <= '9')
 {
   $ret['latDigit'] = $asciiCode - ord('0');
 }
 else if ($c >= 'A' && $c <= 'J')
 {
   $ret['latDigit'] = $asciiCode - ord('A');
   $ret['message'] = 1;
   $ret['isNorth'] = null;
   $ret['longOffset'] = null;
   $ret['isWest'] = null;
 }
 else if ($c == 'K')
 {
   $ret['latDigit'] = 0;
   $ret['message'] = 1;
   $ret['isNorth'] = null;
   $ret['longOffset'] = null;
   $ret['isWest'] = null;
 }
 else if ($c == 'L')
 {
   $ret['latDigit'] = 0;
   $ret['message'] = 0;
 }
 else if ($c >= 'P' && $c <= 'Y')
 {
   $ret['latDigit'] = $asciiCode - ord('P');
   $ret['message'] = 2;
   $ret['isNorth'] = true;
   $ret['longOffset'] = 100;
   $ret['isWest'] = true;
 }
 else if ($c == 'Z')
 {
   $ret['latDigit'] = 0;
   $ret['message'] = 2;
   $ret['isNorth'] = true;
   $ret['longOffset'] = 100;
   $ret['isWest'] = true;
 }

 return $ret;
}


const MicEStates =
[
 'M0'=> 'off duty',
 'M1'=> 'en route',
 'M2'=> 'in service',
 'M3'=> 'returning',
 'M4'=> 'commited',
 'M5'=> 'special',
 'M6'=> 'priority',
 'C0'=> 'custom-0',
 'C1'=> 'custom-1',
 'C2'=> 'custom-2',
 'C3'=> 'custom-3',
 'C4'=> 'custom-4',
 'C5'=> 'custom-5',
 'C6'=> 'custom-6',
 'EMERGENCY'=> 'emergency',
 'UNKNOWN'=> 'unknown'
];


/**
* Gets the Mic-E state (i.e. In Service) from the Mic-E data
* Note Message: array, 3 elements, values: 0 - 0, 1 - 1 custom, 2 - 1 std
* @param {string} $message
* @return {string}
*/
function getStateFromMicE($message)
{
 $state = null;

 if (sizeof($message) != 3) return null; // Message array should contain 3 elements

 if ($message[0] == 0 && $message[1] == 0 && $message[2] == 0)
 {
   $state = MicEStates['EMERGENCY'];
 }
 else
 {
   $isStd = $message[0] == 2 || $message[1] == 2 || $message[2] == 2;
   $isCustom = $message[0] == 1 || $message[1] == 1 || $message[2] == 1;

   if ($isStd && $isCustom)
   {
     $state = MicEStates['UNKNOWN'];
   }
   else
   {
     $message[0] = $message[0] == 2 ? 1 : $message[0];
     $message[1] = $message[1] == 2 ? 1 : $message[1];
     $message[2] = $message[2] == 2 ? 1 : $message[2];

     if ($message == [1, 1, 1])
       $state = $isStd ? MicEStates['M0'] : MicEStates['C0'];
     else if ($message == [1, 1, 0])
       $state = $isStd ? MicEStates['M1'] : MicEStates['C1'];
     else if ($message == [1, 0, 1])
       $state = $isStd ? MicEStates['M2'] : MicEStates['C2'];
     else if ($message == [1, 0, 0])
       $state = $isStd ? MicEStates['M3'] : MicEStates['C3'];
     else if ($message == [0, 1, 1])
       $state = $isStd ? MicEStates['M4'] : MicEStates['C4'];
     else if ($message == [0, 1, 0])
       $state = $isStd ? MicEStates['M5'] : MicEStates['C5'];
     else if ($message == [0, 0, 1])
       $state = $isStd ? MicEStates['M6'] : MicEStates['C6'];
     else
       $state =  MicEStates['UNKNOWN'];
   }
 }

 return $state;
}


/**
* Generates a random numerical code $length digits long
*
* @param {int} $length (Length of the random code to generate, default 6)
* @return {string} (Generated code)
*/
function generateSecureCode($length = 6)
{
  // Generate a {$length} digit random code
  $code = [];
  for ($x = 0; $x < $length; ++$x)
  {
    $code[] = strval(random_int(0, 9));
  }
  return implode('', $code);
}


/**
* Generates a random binary token $length digits long
*
* @param {int} $length (Length of the random token to generate, default 20)
* @return {string} (Generated code)
*/
function generateSecureToken($length = 20)
{
  return bin2hex(random_bytes($length));
}


/**
* Generates a base URL to the site taking into account http vs https and domain name
*
* @return {string} (Full URL to site including http/https)
*/
function getWebsiteURL()
{
  return (getWebsiteConfig('cookie_secure') == 1 ? "https" : "http") . "://" . getWebsiteConfig('cookie_domain');
}


/**
* Outputs to the client a fully rendered navigation menu for the station modal
*
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getStationMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'station.php' => ['title' => 'Station Details', 'text' => 'Station'],
    'statistics.php' => ['title' => 'Station Statistics', 'text' => 'Statistics'],
    'trail.php' => ['title' => 'Trail Chart', 'text' => 'Trail Chart'],
    'weather.php' => ['title' => 'Weather Conditions, History, and Graphs', 'text' => 'Weather'],
    'telemetry.php' => ['title' => 'Current and Historical Telemetry and Graphs', 'text' => 'Telemetry'],
    'raw.php' => ['title' => 'Raw Packets', 'text' => 'Raw Packets'],
    'live.php' => ['title' => 'Live Packet Feed', 'text' => 'Live Feed'],
    'messages.php' => ['title' => 'Station Messages', 'text' => 'Messages'],
    'bulletins.php' => ['title' => 'Station Bulletins', 'text' => 'Bulletins']
  ];

  return renderMenu('station-menu', '/station/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered navigation menu for the account management modal
*
* @param {string} $selectedIndexL (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getAccountMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'map_settings.php' => ['title' => 'Map Settings', 'text' => 'Map Settings'],
    'site_settings.php' => ['title' => 'Site Settings', 'text' => 'Site Settings'],
    'my_stations.php' => ['title' => 'My Stations', 'text' => 'My Stations'],
    'favorites.php' => ['title' => 'Favorite Stations', 'text' => 'Favorite Stations'],
    'aprs_thursday.php' => ['title' => 'My APRSThursday Profile', 'text' => 'APRSThursday Profile'],
    'mapshots.php' => ['title' => 'Map Snapshots (Mapshots)', 'text' => 'Map Snapshots'],
    'my_api.php' => ['title' => 'Personal API', 'text' => 'Personal API'],
    'account.php' => ['title' => 'My Account', 'text' => 'My Account']
  ];

  return renderMenu('account-menu', '/account/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered general navigation menu for the account management modal
*
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getGeneralMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'about.php' => ['title' => 'More information about this APRS website', 'text' => 'About'],
    'faq.php' => ['title' => 'Frequently Asked Questions about APRS', 'text' => 'FAQ'],
    'site_statistics.php' => ['title' => 'Site and Database Statistics', 'text' => 'Statistics'],
    'changelog.php' => ['title' => 'APRS Site News and Updates', 'text' => 'What\'s New?'],
    'server_health.php' => ['title' => 'Check the Health of systems supporting this APRS map.', 'text' => 'Server Status'],
    'data_availability.php' => ['title' => 'Check the availability of the data avaiable to the map.', 'text' => 'Data Availability']
  ];

  if (getWebsiteConfig('aprs_is_status_url')) $pages['aprs_status.php'] = ['title' => 'APRS Server Status and Metrics', 'text' => 'APRS Server Status'];
  if (getWebsiteConfig('cwop_is_status_url')) $pages['cwop_status.php'] = ['title' => 'CWOP Server Status and Metrics', 'text' => 'CWOP Server Status'];
  if (getWebsiteConfig('ogn_is_status_url')) $pages['ogn_status.php'] = ['title' => 'OGN Server Status and Metrics', 'text' => 'OGN Server Status'];
  if (getWebsiteConfig('cbaprs_is_status_url')) $pages['cbaprs_status.php'] = ['title' => 'CBAPRS Server Status and Metrics', 'text' => 'CBAPRS Server Status'];

  return renderMenu('about-menu', '/views/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered search window navigation menu
*
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getSearchMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'search.php' => ['title' => 'Search for a station/object/airplane, etc.', 'text' => 'Station Search'],
    'search_grid.php' => ['title' => 'Maidenhead Locator Grid Search', 'text' => 'Maidenhead Grid Search'],
    'search_location.php' => ['title' => 'Search for any place (address, business, park) or location', 'text' => 'Location Search'],
    'search_license.php' => ['title' => 'Search the FCC and Canadian License Databases', 'text' => 'License Search']
  ];

  return renderMenu('search-menu', '/views/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered legal (ToS) window navigation menu
*
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getLegalMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'contact.php' => ['title' => 'Contact us with questions or to report a problem', 'text' => 'Contact Us'],
    'terms_of_service.php' => ['title' => 'Terms of Service Agreement', 'text' => 'Terms of Service'],
    'privacy_policy.php' => ['title' => 'Our Commitment to your Privacy', 'text' => 'Privacy Policy']
  ];

  return renderMenu('legal-menu', '/views/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered #APRSThursday window navigation menu
*
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function getAPRSThursdayMenu($selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  $pages =
  [
    'aprs_thursday.php' => ['title' => 'All about the #APRSThursday Weekly Net', 'text' => 'About #APRSThursday'],
    'aprs_thursday_checkins.php' => ['title' => 'View check-ins for the most recent nets.', 'text' => 'Recent Check-Ins'],
    'aprs_thursday_statistics.php' => ['title' => 'Historical statistics since the beginning of #APRSThursday.', 'text' => 'Statistics'],
    'aprs_thursday_leaderboard.php' => ['title' => 'View the participation leaderboard.', 'text' => 'Leaderboard']
  ];

  return renderMenu('APRSThursday-menu', '/events/', $pages, $selectedIndex, $urlParams, $extraHTML);
}


/**
* Outputs to the client a fully rendered navigation menu
*
* @param {string} $selectId (ID of the select tag for the menu)
* @param {string} $baseURL (Root path of the items in the menu)
* @param {array} $pages (Array of pages to use as menu items)
* @param {string} $selectedIndex (Base name of the file being displayed i.e. station.php)
* @param {array} $urlParams (Array of parameters to pass in each generated URL)
* @param {string} $extraHTML (Extra menu HTML to include at the end of the menu)
* @return {string} HTML Menu
*/
function renderMenu($selectId, $baseURL, $pages, $selectedIndex, $urlParams = [], $extraHTML = null)
{
  global $tdSession;

  // Mobile Menu
  $menu = '<div><span class="mobile-menu">Menu: <select id="' . $selectId . '" onchange="$t.ui.loadModal($(this).val());return false;">';
  foreach ($pages as $page => $meta)
  {
    $menu .= '<option value="' . $tdSession->getURL($baseURL . $page, $urlParams) . '" ' . ($selectedIndex == $page ? 'selected="selected"' : '') . ' title="' . $meta['title'] . '">' . $meta['text'] . '</option>';
  }
  $menu .= '</select></span>';

  // Normal Menu
  $menu .= '<div class="modal-inner-content-menu">';
  foreach ($pages as $page => $meta)
  {
    if ($selectedIndex == $page) $menu .= '<span>' . $meta['text'] . '</span>';
    else $menu .= '<a class="mdWin" title="' . $meta['title'] . '" href="' . $tdSession->getURL($baseURL . $page, $urlParams) . '">' . $meta['text'] . '</a>';
  }
  $menu .= '</div>' . $extraHTML . '</div>';

  return $menu;
}


/**
* Outputs to the client a fully rendered navigation menu
*
* @param {string} $colorName (CSS Color Name)
* @param {string} $alpha (Alpha transparency value 0-.99)
* @return {string} Complete rgbA CSS tag
*/
function cssColorToRgbATag($colorName, $alpha)
{
  $cssColors = [
    'black'   => '0,0,0',
    'silver'  => '192, 192, 192',
    'gray'    => '128, 128, 128',
    'white'   => '255, 255, 255',
    'maroon'  => '128, 0, 0',
    'red'     => '255, 0, 0',
    'purple'  => '128, 0, 128',
    'fuchsia' => '255, 0, 255',
    'green'   => '0, 128, 0',
    'lime'    => '0, 255, 0',
    'olive'   => '128, 128, 0',
    'yellow'  => '255, 255, 0',
    'navy'    => '0, 0, 128',
    'blue'    => '0, 0, 255',
    'teal'    => '0, 128, 128',
    'aqua'    => '0, 255, 255'
  ];

  // Return black if no color is specififed
  if (is_null($colorName) || $colorName == '') return 'rgba(' . $cssColors["black"] . ", $alpha)";
  return 'rgba(' . $cssColors["$colorName"] . ", $alpha)";
}


/**
* Wrapper function to return a station object from various URI critera
*
* @return {Station} Station object (uninitialized if no station found)
*/
function getStationFromURI()
{
  if (isset($_GET['c']))
  {
    $name = filter_input(INPUT_GET, 'c', FILTER_DEFAULT);
    return StationRepository::getInstance()->getObjectByName(strtoupper(trim($name)));
  }

  // New ID format with sourceId as the first digit
  else if (isset($_GET['s']))
  {
    $station_id = filter_input(INPUT_GET, 's', FILTER_VALIDATE_INT, ['options' => ['flags' => FILTER_NULL_ON_FAILURE]]);
    if (!is_null($station_id))
    {
      $sid = strval($station_id);
      if (strlen($sid) >= 2)
      {
        $source_id = $sid[0];
        $station_id = substr($sid, 1);
        return StationRepository::getInstance()->getObjectById($station_id, $source_id);
      }
    }
    return new Station(0);
  }

  // Legacy ID format
  else
  {
    $station_id = filter_input(INPUT_GET, 'id', FILTER_VALIDATE_INT, ['options' => ['flags' => FILTER_NULL_ON_FAILURE]]);
    return StationRepository::getInstance()->getObjectById($station_id);
  }

  return new Station(0);
}
