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

Asked

Viewed 1,877 times

14

The user can enter 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 processed 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 for 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 introduction of spaces has been 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 for a matrix so that I can convert it to decimal format, but I’m finding the whole process a little too extensive and prone to failure:

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

Upshot:

Result when performing a var_dump() to the matrix $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 code density, which can be passed to individual functions, there is a whole series of problems caused mainly by user-introduced separators.

Similarly, this check is to be reused when we are processing GPS coordinates in DMS format that are sourced from other sources.

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

Question

How can I simplify and improve the result of this code in order to convert GPS coordinates into DMS format into a matrix?

6 answers

8

It seems to me a good case to apply a regular expression, which already validates and captures the importing part of the input, ignoring the separators.

I thought of something along these lines (but I’m sure the regex experts can improve):

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

http://regexr.com?3856r

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
)

http://ideone.com/VsgrRZ

5

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

Regular Expression

The created expression was:

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

It consists of the following excerpts:

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

Basically the expression as a whole asks:

  1. A letter
  2. A non-numerical character
  3. A number with one or two digits
  4. A non-numerical character
  5. A number with one or two digits
  6. A non-numerical character
  7. 1 to 10 numbers and points
  8. and ends with a non-numeric character or a blank

The final part that recovers numbers with dots 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 sent to the user.

Just to leave the example of an expression that only accepts valid numbers, we can change the snippet [\\d\\.]{1,10} for 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 as 4.3.2, 3. or 2..1, as the new section has the following restrictions:

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

PHP function

The function was 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 functional example on ideone.

  • 1

    @Zuul I complemented the answer with additional explanations and another example of regular expression.

  • Rs a long time ago, but this expression vc copied from PHP code if used in javascript for example will be understood as the character '' ([NSWE])(\\d{1,2})[^\\d](\\d{1,2})[^\\d]([\\d\\.]{1,10})[^\\d\\s] for ([NSWE])(\d{1,2})[^\d](\d{1,2})[^\d]([\d\.]{1,10})[^\d\s] :)

  • @Leandroamorim That’s right. Duplicate bars are necessary to give escape in the PHP String. If you are going to use it in some other way, you have to adapt it to the context, considering if you need escape and something changes in the engine of regular expressions. For example, not all implementations support character limiters such as {1,10}.

0

I still can’t add comments

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

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

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

That regular expression by putting . (finder of any character) would not validate the string for example "W8251300" he would pass as

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

That regular expression

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

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]$"

In this way it is necessary that

  1. The first character is 'N' or’S' or 'W' or 'E' array[0]
  2. One or two digits array[1]
  3. A different character of digit
  4. One or two digits array[2]
  5. A different character of digit
  6. One or two digits containing or not the decimal part which must necessarily be separated by '.' or ',' that decimal part may contain 1 or N digits. array[3]

0

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

In this case perhaps it would be better to change the user interface so that they do not have as much freedom to enter the values. I imagine that 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.

Similarly, this check is to be reused when we are processing GPS coordinates in DMS format that are sourced from other sources.

Well, in that case a really regular expression seems to be a good way to solve the problem. After taking a look on that page, and with your information that the spaces have been removed previously, you can use the following 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 at both the beginning and the end of the text and also that the grades can have up to 3 numbers, and works with these coordinates:

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″

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 embedded. There are very sophisticated standards and precision issues, but all well solved in libraries.

For complex things and maps:

  • Database: Postgresql with Postgis, I use and recommend for anything, any calculation or geographical conversion.

  • Javascript interface: Openlayers

Specific:

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

?>

In this link is an example: http://www.web-max.ca/PHP/misc_6.php

Browser other questions tagged

You are not signed in. Login or sign up in order to post.