Code refactoring to convert GPS coordinates in DMS format into an array

14

You can enter the GPS coordinates in two ways, Decimal Degrees or Degrees, Minutes, Seconds :

            ┌─────────────────────────────────┬─────────────────────┐
            │ DMS (Degrees, Minutes, Seconds) │ DD (Decimal Degree) │
┌───────────┼─────────────────────────────────┼─────────────────────┤
│ Latitude  │ N 40° 11′ 48.055″               │ 40.196682           │
├───────────┼─────────────────────────────────┼─────────────────────┤
│ Longitude │ W 8° 25′ 52.134″                │ -8.431149           │
└───────────┴─────────────────────────────────┴─────────────────────┘

These values have to be treated so that they remain in a single format for conversion between them, as well as for storage in the database.

The problem is in the DMS format, where the coordinates entered can vary from several digits, different references to the hemisphere to the characters that identify the separation between the values.

To facilitate, the introduction of the coordinates, it is divided into two fields, Latitude and Longitude, and the spaces are blocked, which gives us:

            ┌─────────────────────────────────┬─────────────────────┐
            │ DMS (Degrees, Minutes, Seconds) │ DD (Decimal Degree) │
┌───────────┼─────────────────────────────────┼─────────────────────┤
│ Latitude  │ N40°11′48.055″                  │ 40.196682           │
├───────────┼─────────────────────────────────┼─────────────────────┤
│ Longitude │ W8°25′52.134″                   │ -8.431149           │
└───────────┴─────────────────────────────────┴─────────────────────┘

In operation

I'm trying to extract the value in degrees, minutes, and seconds to an array, so I can convert it to decimal format, but I find the whole process a little too long and susceptible to crashes: p>

Code

Assuming that the variable $entity_gps contains N40°11'43.44" W8°25'1.31" :

Note: The values, although entered separately, are stored in a single field separated by a space.

$coordinatesArr = array(
  "lat" => array(
    "hem" => '',
    "deg" => '',
    "min" => '',
    "sec" => ''
  ),
  "lng" => array(
    "hem" => '',
    "deg" => '',
    "min" => '',
    "sec" => ''
  )
);

if ($entity_gps!='') {

  $gpsArr = explode(' ', $entity_geo->gps);

  if (is_array($gpsArr)) {

    $i = 0;

    foreach ($gpsArr as $str) {

      // Extract hemisphere
      $hemisphere = mb_substr($str, 0, 1, 'UTF-8');

      // Validate hemisphere
      if (ctype_alpha($hemisphere)) {

        /* Store hemisphere
         */
        if ($i==0) {
          $coordinatesArr["lat"]["hem"] = $hemisphere;
        } else {
          $coordinatesArr["lng"]["hem"] = $hemisphere;
        }

        // Extract degrees
        $degree = mb_substr($str, 1, mb_strpos($str, '°')-1, 'UTF-8');

        // Validate degrees
        if (ctype_digit($degree)) {

          /* Store degrees
           */
          if ($i==0) {
            $coordinatesArr["lat"]["deg"] = $degree;
          } else {
            $coordinatesArr["lng"]["deg"] = $degree;
          }

          // Extract minutes
          $iniPos = mb_strpos($str, '°')+1;

          $minutes = mb_substr($str, $iniPos, mb_strpos($str, "'")-$iniPos, 'UTF-8');

          // Validate minutes
          if (ctype_digit($minutes)) {

            /* Store minutes
             */
            if ($i==0) {
              $coordinatesArr["lat"]["min"] = $minutes;
            } else {
              $coordinatesArr["lng"]["min"] = $minutes;
            }

            // Extract seconds
            $iniPos = mb_strpos($str, "'")+1;

            $seconds = mb_substr($str, $iniPos, mb_strpos($str, '"')-$iniPos, 'UTF-8');

            // Validate seconds
            if ($seconds!='') {

              /* Store seconds
               */
              if ($i==0) {
                $coordinatesArr["lat"]["sec"] = $seconds;
              } else {
                $coordinatesArr["lng"]["sec"] = $seconds;
              }

            } else {
              echo 'Erro ao identificar os segundos!';
            }

          } else {
            echo 'Erro ao identificar os minutos!';
          }

        } else {
          echo 'Erro ao identificar os graus!';
        }

      } else {
        echo "Erro ao identificar o hemisfério!";
      }

      $i++;
    }
  }
}

Result:

Result when you make a var_dump() to the array $coordinatesArr , it contains the values as expected:

array(2) {
  ["lat"]=>
  array(4) {
    ["hem"]=>
    string(1) "N"
    ["deg"]=>
    string(2) "40"
    ["min"]=>
    string(2) "11"
    ["sec"]=>
    string(5) "43.44"
  }
  ["lng"]=>
  array(4) {
    ["hem"]=>
    string(1) "W"
    ["deg"]=>
    string(1) "8"
    ["min"]=>
    string(2) "25"
    ["sec"]=>
    string(4) "1.31"
  }
}

Problem

In addition to the density of code, which can be passed to individual functions, there are a number of problems mainly caused by the separators entered by the user.

Likewise, this check is to be reused when processing GPS coordinates in DMS format from other sources.

  • Instead of ° another one is present.
  • Instead of ' ´ or other is present.
  • Instead of " ¨ or other is present.

Question

How can I simplify and refine the result of this code in order to convert the GPS coordinates in DMS format into an array?

    
asked by anonymous 10.01.2014 / 17:09

6 answers

8

It seems like a good case to apply a regular expression, which already validates and captures the part that imports from the entry, ignoring the tabs.

I thought of something in this line (but I'm sure the regex experts can improve it):

^([NSWE])(\d\d?).(\d\d?).(\d\d?(?:[.,]\d+)?).$

link

In PHP you would need something like:

$arr = array();
$result = preg_match('/^([NSWE])(\d\d?).(\d\d?).(\d\d?(?:[.,]\d+)?).$/u', "W8°25′52.134″", $arr);

Result in $arr :

Array
(
    [0] => W8°25′52.134″
    [1] => W
    [2] => 8
    [3] => 25
    [4] => 52.134
)

link

    
10.01.2014 / 17:32
5

I created a function that I believe do what you want with less difficulty, based also on regular expressions.

Regular Expression

The expression created was:

([NSWE])(\d{1,2})[^\d](\d{1,2})[^\d]([\d\.]{1,10})[^\d\s]

It is made up of the following excerpts:

  • [NSWE] - one of the letters 'N', 'S', 'W' or 'E'
  • \d{1,2} - one or two numeric digits (0-9)
  • [^\d] - any non-numeric character
  • [\d\.]{1,10} - 1 to 10 numeric digits, including also the endpoint character ( . )

Basically the expression as a whole asks:

  • One letter
  • A non-numeric character
  • A one- or two-digit number
  • A non-numeric character
  • A one- or two-digit number
  • A non-numeric character
  • 1 to 10 numbers and periods
  • and end with a non-numeric character or a space
  • The final part that retrieves numbers with points could be incremented to allow only one point. However, I believe that this validation should be done in another place where a specific message about the format can be issued to the user.

    Just to leave the example of an expression that only accepts valid numbers, we can change the [\d\.]{1,10} stretch to something like [\d]{1,2}(?:\.[\d]{1,3})? :

    ([NSWE])(\d{1,2})[^\d](\d{1,2})[^\d]([\d]{1,2}(?:\.[\d]{1,3})?)[^\d\.]
    

    The above expression does not accept entries like 4.3.2 , 3. or 2..1 , since the new excerpt has the following constraints:

    • [\d]{1,2} - Allows a number of 1 or 2 digits
    • (?:\.[\d]{1,3})? - Followed by a period and a number of 1 to 3 digits, with both the optional point and the number

    PHP function

    The function is as follows:

    function get_coordinates_array($entity_gps) {
        $items = array();
        $res = preg_match_all(
            '/([NSWE])(\d{1,2})[^\d](\d{1,2})[^\d]([\d\.]{1,10})[^\d\s]/ui', 
            $entity_gps, $items, PREG_SET_ORDER);
        if ($res === 2) {
            return array(
                "lat" => array_slice($items[0], 1, 4),
                "lng" => array_slice($items[1], 1, 4)
            );
        } else {
            return null;
        }
    }
    

    To call it, just pass the two coordinates in a String:

    $coordinatesArr = get_coordinates_array($entity_gps);
    

    And the return will be exactly like the example of the question:

    Array
    (
        [lat] => Array
            (
                [0] => N
                [1] => 40
                [2] => 11
                [3] => 43.44
            )
    
        [lng] => Array
            (
                [0] => W
                [1] => 8
                [2] => 25
                [3] => 1.31
            )
    
    )
    

    See the working example on ideone .

        
    28.01.2014 / 16:10
    0

    I still can not add comments

    The intent of the regular expression is to find patterns, and somehow validate them.

    This way it would be interesting to know if the string received is valid.

    ^([NSWE])(\d\d?).(\d\d?).(\d\d?(?:[.,]\d+)?).$
    

    This regular expression putting . (locator of any character) would not validate the string eg "W8251300" it would pass as

    [0]=W
    [1]=82
    [2]=1
    [3]=0
    

    This regular expression

    ([NSWE])(\d{1,2})[^\d](\d{1,2})[^\d]([\d\.]{1,10})[^\d\s]
    

    I would accept things like W8°25'1.3.1.1" .

    [0]=W
    [1]=8
    [2]=25
    [3]=1.3.1.1
    

    The right thing would be something close to

    "^([NSWE])(\d\d?)[^\d](\d\d?)[^\d](\d\d?(?:[.,]\d+)?)[^\d]$"
    

    This way you need

  • The first character is 'N' or 'S' or 'W' or 'E' array[0]
  • One or two digits array[1]
  • A character other than digit
  • One or two digits array[2]
  • A character other than digit
  • One or two digits containing or not the decimal part that must be separated by '.' or ',' this decimal part can contain 1 or N digits. array[3]
  • 31.01.2014 / 20:29
    0
      

    There are a number of problems mainly caused by user-entered tabs

    In this case it might be better to change the user interface so they do not have as much freedom to enter the values. I imagine for each coordinate you can have a series of controls: a drop down for N / S or W / E, and separate text boxes for degrees, minutes, and seconds.

      

    Likewise, this check is to be reused when processing GPS coordinates in DMS format from other sources.

    Well, in that case a really regular expression seems like a good way to solve the problem. After taking a look at on this page , and with your information that the spaces were previously removed, you can use the following < in> regex (with the option case insensitive - i ):

    ([nswe])?(\d{1,3})\D+(\d\d?)\D+(\d\d?(?:\.\d+)?)(?:\S?([nswe]))?
    

    ( regexplained )

    This regular expression takes into account that the orientation (N / S / W / E) can be both at the beginning and at the end of the text and also that the degrees can have up to 3 numbers and works with these coordinates: p>

    N40°11'43.44" W8°25'1.31"
    40:26:46.302N 079:58:55.903W
    40°26′46″N 079°58′56″W
    40d26′46″N 079d58′56″W
    N40:26:46.302 W079:58:55.903
    N40°26′46″ W079°58′56″
    N40d26′46″ W079d58′56″
    
        
    31.01.2014 / 21:53
    0

    If you are going to deal more intensively with geographic data, the suggestion is to use a framework or library with these things already in place. There are very sophisticated standards and precision questions, but that's fine with libraries.

    For complex things and maps:

    • Database: PostgreSQL with PostGIS , I use and recommend for any calculation or geographic conversion.

    • Javascript Interface: OpenLayers

    Specific:

    31.01.2014 / 23:54
    0

    I think this might solve your problem:

    <?php
    
    function DMStoDEC($deg,$min,$sec)
    {
    
    // Converts DMS ( Degrees / minutes / seconds ) 
    // to decimal format longitude / latitude
    
        return $deg+((($min*60)+($sec))/3600);
    }    
    
    function DECtoDMS($dec)
    {
    
    // Converts decimal longitude / latitude to DMS
    // ( Degrees / minutes / seconds ) 
    
    // This is the piece of code which may appear to 
    // be inefficient, but to avoid issues with floating
    // point math we extract the integer part and the float
    // part by using a string function.
    
        $vars = explode(".",$dec);
        $deg = $vars[0];
        $tempma = "0.".$vars[1];
    
        $tempma = $tempma * 3600;
        $min = floor($tempma / 60);
        $sec = $tempma - ($min*60);
    
        return array("deg"=>$deg,"min"=>$min,"sec"=>$sec);
    }    
    
    ?>
    

    This link has an example: link

        
    01.02.2014 / 06:16