Skip to content

Commit f958da0

Browse files
Merge pull request #4 from Vectorial1024/olc_x86_x64
Use "long" int calculators where possible
2 parents c62302a + c10d4f4 commit f958da0

6 files changed

+363
-124
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ via Composer:
2727
composer require vectorial1024/open-location-code-php
2828
```
2929

30-
**Special Notice**: to ensure 32-bit PHP compatibility, this library uses `float` variables instead of `int` variables to calculate Open Location Codes.
31-
This may cause inaccuracies in some unforseen edge cases, but generally speaking, there should be no problems.
30+
**Special Notice**: to ensure 32-bit PHP compatibility, this library will check the PHP runtime and, if it detects a 32-bit PHP runtime, will use `float` variables instead of `int` variables to calculate Open Location Codes.
31+
This may cause some unintended inaccuracies in 32-bit PHP, but generally speaking, there should be no problems.
3232

3333
## Example code
3434

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
namespace Vectorial1024\OpenLocationCodePhp\CodeCalculator;
4+
5+
use Vectorial1024\OpenLocationCodePhp\CodeArea;
6+
use Vectorial1024\OpenLocationCodePhp\OpenLocationCode;
7+
8+
/**
9+
* An abstract class to provide a unified API for the 32-bit and 64-bit Open Location Code calculators.
10+
*/
11+
abstract class AbstractCodeCalculator
12+
{
13+
// Value to multiple latitude degrees to convert it to an integer with the maximum encoding
14+
// precision. I.e. ENCODING_BASE**3 * GRID_ROWS**GRID_CODE_LENGTH
15+
protected const int LAT_INTEGER_MULTIPLIER = 8000 * 3125;
16+
17+
// Value to multiple longitude degrees to convert it to an integer with the maximum encoding
18+
// precision. I.e. ENCODING_BASE**3 * GRID_COLUMNS**GRID_CODE_LENGTH
19+
protected const int LNG_INTEGER_MULTIPLIER = 8000 * 1024;
20+
21+
/**
22+
* Assuming the given latitude and longitude are valid, encode these coordinates into an Open Location Code.
23+
* @param float $latitude The latitude in decimal degrees.
24+
* @param float $longitude The longitude in decimal degrees.
25+
* @param int $codeLength The desired number of digits in the code.
26+
* @return string The resulting (string) Open Location Code.
27+
*/
28+
public function encode(float $latitude, float $longitude, int $codeLength): string
29+
{
30+
// Let the calculator correctly generate the reversed code
31+
$revCode = static::generateRevOlcCode($latitude, $longitude, $codeLength);
32+
// Reverse the code
33+
$code = strrev($revCode);
34+
35+
// If we need to pad the code, replace some of the digits.
36+
if ($codeLength < OpenLocationCode::SEPARATOR_POSITION) {
37+
for ($i = $codeLength; $i < OpenLocationCode::SEPARATOR_POSITION; $i++) {
38+
$code[$i] = OpenLocationCode::PADDING_CHARACTER;
39+
}
40+
}
41+
$finalCode = substr($code, 0, max(OpenLocationCode::SEPARATOR_POSITION + 1, $codeLength + 1));
42+
return $finalCode;
43+
}
44+
45+
/**
46+
* Performs the necessary calculation to generate a reversed (and possibly incomplete) Open Location Code from the given coordinates.
47+
* The exact implementation depends on whether the 32-bit/64-bit calculator is used, but the idea should be the same.
48+
* @param float $latitude The latitude in decimal degrees.
49+
* @param float $longitude The longitude in decimal degrees.
50+
* @param int $codeLength The desired number of digits in the code.
51+
* @return string The resulting (string) Open Location Code.
52+
*/
53+
abstract protected function generateRevOlcCode(float $latitude, float $longitude, int $codeLength): string;
54+
55+
/**
56+
* Assuming the given Open Location Code is valid, decodes it into a CodeArea object encapsulating latitude/longitude bounding box.
57+
* @param string $code The stripped Open Location Code.
58+
* @return CodeArea A CodeArea object.
59+
*/
60+
public function decode(string $code): CodeArea
61+
{
62+
// Strip padding and separator characters out of the code.
63+
$clean = str_replace([OpenLocationCode::SEPARATOR, OpenLocationCode::PADDING_CHARACTER], "", $code);
64+
65+
return $this->generateCodeArea($clean);
66+
}
67+
68+
/**
69+
* Performs the necessary calculation to generate a CodeArea object from the given (stripped) Open Location Code.
70+
* @param string $strippedCode The stripped Open Location Code.
71+
* @return CodeArea A CodeArea object.
72+
*/
73+
abstract protected function generateCodeArea(string $strippedCode): CodeArea;
74+
75+
/**
76+
* Returns a static instance of the appropriate code calculator depending on whether the current PHP is 32-bit/64-bit.
77+
* 32-bit PHP will get a calculator that uses floats, while 64-bit PHP will get a calculator that uses "long" ints.
78+
*
79+
* This is required to allow 32-bit PHP environments to correctly calculate Open Location Codes, while preserving
80+
* the performance advantage of 64-bit PHP by using integer operations.
81+
* @return CodeCalculatorFloat|CodeCalculatorInt The appropriate code calculator.
82+
*/
83+
public static function getDefaultCalculator(): CodeCalculatorFloat|CodeCalculatorInt
84+
{
85+
// check PHP environment to give int calculator where possible
86+
// 32-bit PHP has 32-bit int, which uses 4 bytes i.e. PHP_INT_SIZE = 4
87+
// initializing static variables with constructors is only allowed for PHP 8.3+
88+
static $defaultCalculator = PHP_INT_SIZE > 4 ? new CodeCalculatorInt() : new CodeCalculatorFloat();
89+
return $defaultCalculator;
90+
}
91+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace Vectorial1024\OpenLocationCodePhp\CodeCalculator;
4+
5+
use Vectorial1024\OpenLocationCodePhp\CodeArea;
6+
use Vectorial1024\OpenLocationCodePhp\OpenLocationCode;
7+
8+
/**
9+
* A Open Location Code calculator that uses float.
10+
* As such, this is usable on any PHP version. but may have unforeseen inaccuracy.
11+
*/
12+
class CodeCalculatorFloat extends AbstractCodeCalculator
13+
{
14+
// Value of the most significant latitude digit after it has been converted to an integer.
15+
// Note: to ensure 32bit PHP compatibility, this is now a precisely-represented float.
16+
public const float LAT_MSP_VALUE = self::LAT_INTEGER_MULTIPLIER * OpenLocationCode::ENCODING_BASE * OpenLocationCode::ENCODING_BASE;
17+
18+
// Value of the most significant longitude digit after it has been converted to an integer.
19+
// Note: to ensure 32bit PHP compatibility, this is now a precisely-represented float.
20+
public const float LNG_MSP_VALUE = self::LNG_INTEGER_MULTIPLIER * OpenLocationCode::ENCODING_BASE * OpenLocationCode::ENCODING_BASE;
21+
22+
protected function generateRevOlcCode(float $latitude, float $longitude, int $codeLength): string
23+
{
24+
// PHP has native support for string concatenation, and string reversal is quite fast.
25+
$revCode = "";
26+
27+
// Compute the code.
28+
// The idea of this approach is to convert each value to an integer
29+
// after multiplying it by the final precision.
30+
// This allows us to use only integer operations, so
31+
// avoiding any accumulation of floating point representation errors.
32+
// However, it must also be noted that the calculation may poduce 10-digit integers
33+
// that begins with 6, which overflows the 32-bit PHP int type.
34+
// The good news is, this relatively small bignum can be precisely represented
35+
// by the double-precision float type. We just need to be careful when calculating.
36+
37+
// Multiply values by their precision and convert to positive.
38+
// Rounding avoids/minimizes errors due to floating-point precision.
39+
// Since the numbers are positive, floor() is equivalent to intval().
40+
$latVal = floor(round(($latitude + OpenLocationCode::LATITUDE_MAX) * self::LAT_INTEGER_MULTIPLIER * 1e6) / 1e6);
41+
$lngVal = floor(round(($longitude + OpenLocationCode::LONGITUDE_MAX) * self::LNG_INTEGER_MULTIPLIER * 1e6) / 1e6);
42+
43+
// Compute the grid part of the code if necessary.
44+
if ($codeLength > OpenLocationCode::PAIR_CODE_LENGTH) {
45+
for ($i = 0; $i < OpenLocationCode::GRID_CODE_LENGTH; $i++) {
46+
$latDigit = (int) fmod($latVal, OpenLocationCode::GRID_ROWS);
47+
$lngDigit = (int) fmod($lngVal, OpenLocationCode::GRID_COLUMNS);
48+
$ndx = $latDigit * OpenLocationCode::GRID_COLUMNS + $lngDigit;
49+
$revCode .= OpenLocationCode::CODE_ALPHABET[$ndx];
50+
$latVal = floor($latVal / OpenLocationCode::GRID_ROWS);
51+
$lngVal = floor($lngVal / OpenLocationCode::GRID_COLUMNS);
52+
}
53+
unset($i, $latDigit, $lngDigit, $ndx);
54+
} else {
55+
$latVal = floor($latVal / pow(OpenLocationCode::GRID_ROWS, OpenLocationCode::GRID_CODE_LENGTH));
56+
$lngVal = floor($lngVal / pow(OpenLocationCode::GRID_COLUMNS, OpenLocationCode::GRID_CODE_LENGTH));
57+
}
58+
59+
// Compute the pair section of the code.
60+
for ($i = 0; $i < intdiv(OpenLocationCode::PAIR_CODE_LENGTH, 2); $i++) {
61+
$revCode .= OpenLocationCode::CODE_ALPHABET[(int) fmod($lngVal, OpenLocationCode::ENCODING_BASE)];
62+
$revCode .= OpenLocationCode::CODE_ALPHABET[(int) fmod($latVal, OpenLocationCode::ENCODING_BASE)];
63+
$latVal = floor($latVal / OpenLocationCode::ENCODING_BASE);
64+
$lngVal = floor($lngVal / OpenLocationCode::ENCODING_BASE);
65+
// If we are at the separator position, add the separator
66+
if ($i == 0) {
67+
$revCode .= OpenLocationCode::SEPARATOR;
68+
}
69+
}
70+
71+
return $revCode;
72+
}
73+
74+
protected function generateCodeArea(string $strippedCode): CodeArea
75+
{
76+
// Initialize the values.
77+
// We will assume these values are floats to ensure 32bit PHP compatibility.
78+
// See relevant comments in encode() above.
79+
$latVal = -OpenLocationCode::LATITUDE_MAX * self::LAT_INTEGER_MULTIPLIER;
80+
$lngVal = -OpenLocationCode::LONGITUDE_MAX * self::LNG_INTEGER_MULTIPLIER;
81+
// Define the place value for the digits. We'll divide this down as we work through the code.
82+
$latPlaceVal = self::LAT_MSP_VALUE;
83+
$lngPlaceVal = self::LNG_MSP_VALUE;
84+
for ($i = OpenLocationCode::PAIR_CODE_LENGTH; $i < min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT); $i += 2) {
85+
$latPlaceVal = floor($latPlaceVal / OpenLocationCode::ENCODING_BASE);
86+
$lngPlaceVal = floor($lngPlaceVal / OpenLocationCode::ENCODING_BASE);
87+
$latVal += strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i]) * $latPlaceVal;
88+
$lngVal += strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i + 1]) * $lngPlaceVal;
89+
}
90+
unset($i);
91+
for ($i = OpenLocationCode::PAIR_CODE_LENGTH; $i < min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT); $i++) {
92+
$latPlaceVal = floor($latPlaceVal / OpenLocationCode::GRID_ROWS);
93+
$lngPlaceVal = floor($lngPlaceVal / OpenLocationCode::GRID_COLUMNS);
94+
$digit = strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i]);
95+
$row = intdiv($digit, OpenLocationCode::GRID_COLUMNS);
96+
$col = $digit % OpenLocationCode::GRID_COLUMNS;
97+
$latVal += $row * $latPlaceVal;
98+
$lngVal += $col * $lngPlaceVal;
99+
unset($digit);
100+
}
101+
unset($i);
102+
$latitudeLo = $latVal / self::LAT_INTEGER_MULTIPLIER;
103+
$longitudeLo = $lngVal / self::LNG_INTEGER_MULTIPLIER;
104+
$latitudeHi = ($latVal + $latPlaceVal) / self::LAT_INTEGER_MULTIPLIER;
105+
$longitudeHi = ($lngVal + $lngPlaceVal) / self::LNG_INTEGER_MULTIPLIER;
106+
return new CodeArea(
107+
$latitudeLo,
108+
$longitudeLo,
109+
$latitudeHi,
110+
$longitudeHi,
111+
min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT),
112+
);
113+
}
114+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
namespace Vectorial1024\OpenLocationCodePhp\CodeCalculator;
4+
5+
use RuntimeException;
6+
use Vectorial1024\OpenLocationCodePhp\CodeArea;
7+
use Vectorial1024\OpenLocationCodePhp\OpenLocationCode;
8+
9+
/**
10+
* A Open Location Code calculator that uses "long" int.
11+
* As such, this has ensured accuracy, but cannot be used in 32-bit PHP.
12+
*/
13+
class CodeCalculatorInt extends AbstractCodeCalculator
14+
{
15+
// Value of the most significant latitude digit after it has been converted to an integer.
16+
// Note: since we are using 64-bit PHP, this can be an int.
17+
public const int LAT_MSP_VALUE = self::LAT_INTEGER_MULTIPLIER * OpenLocationCode::ENCODING_BASE * OpenLocationCode::ENCODING_BASE;
18+
19+
// Value of the most significant longitude digit after it has been converted to an integer.
20+
// Note: since we are using 64-bit PHP, this can be an int.
21+
public const int LNG_MSP_VALUE = self::LNG_INTEGER_MULTIPLIER * OpenLocationCode::ENCODING_BASE * OpenLocationCode::ENCODING_BASE;
22+
23+
public function __construct()
24+
{
25+
if (PHP_INT_SIZE < 8) {
26+
// 32-bit PHP has 32-bit int, which uses 4 bytes i.e. PHP_INT_SIZE = 4
27+
throw new RuntimeException("CodeCalculatorInt cannot be used due to bad PHP_INT_MAX value. Use CodeCalculatorFloat instead.");
28+
}
29+
}
30+
31+
protected function generateRevOlcCode(float $latitude, float $longitude, int $codeLength): string
32+
{
33+
// PHP has native support for string concatenation, and string reversal is quite fast.
34+
$revCode = "";
35+
36+
// Compute the code.
37+
// The idea of this approach is to convert each value to an integer
38+
// after multiplying it by the final precision.
39+
// This allows us to use only integer operations, so
40+
// avoiding any accumulation of floating point representation errors.
41+
42+
// Multiply values by their precision and convert to positive.
43+
// Rounding avoids/minimises errors due to floating point precision.
44+
$latVal = intdiv((int) round(($latitude + OpenLocationCode::LATITUDE_MAX) * self::LAT_INTEGER_MULTIPLIER * 1e6), 1e6);
45+
$lngVal = intdiv((int) round(($longitude + OpenLocationCode::LONGITUDE_MAX) * self::LNG_INTEGER_MULTIPLIER * 1e6), 1e6);
46+
47+
// Compute the grid part of the code if necessary.
48+
if ($codeLength > OpenLocationCode::PAIR_CODE_LENGTH) {
49+
for ($i = 0; $i < OpenLocationCode::GRID_CODE_LENGTH; $i++) {
50+
$latDigit = $latVal % OpenLocationCode::GRID_ROWS;
51+
$lngDigit = $lngVal % OpenLocationCode::GRID_COLUMNS;
52+
$ndx = $latDigit * OpenLocationCode::GRID_COLUMNS + $lngDigit;
53+
$revCode .= OpenLocationCode::CODE_ALPHABET[$ndx];
54+
$latVal = intdiv($latVal, OpenLocationCode::GRID_ROWS);
55+
$lngVal = intdiv($lngVal, OpenLocationCode::GRID_COLUMNS);
56+
}
57+
unset($i, $latDigit, $lngDigit, $ndx);
58+
} else {
59+
$latVal = (int) ($latVal / pow(OpenLocationCode::GRID_ROWS, OpenLocationCode::GRID_CODE_LENGTH));
60+
$lngVal = (int) ($lngVal / pow(OpenLocationCode::GRID_COLUMNS, OpenLocationCode::GRID_CODE_LENGTH));
61+
}
62+
63+
// Compute the pair section of the code.
64+
for ($i = 0; $i < intdiv(OpenLocationCode::PAIR_CODE_LENGTH, 2); $i++) {
65+
$revCode .= OpenLocationCode::CODE_ALPHABET[$lngVal % OpenLocationCode::ENCODING_BASE];
66+
$revCode .= OpenLocationCode::CODE_ALPHABET[$latVal % OpenLocationCode::ENCODING_BASE];
67+
$latVal = intdiv($latVal, OpenLocationCode::ENCODING_BASE);
68+
$lngVal = intdiv($lngVal, OpenLocationCode::ENCODING_BASE);
69+
// If we are at the separator position, add the separator
70+
if ($i == 0) {
71+
$revCode .= OpenLocationCode::SEPARATOR;
72+
}
73+
}
74+
75+
return $revCode;
76+
}
77+
78+
protected function generateCodeArea(string $strippedCode): CodeArea
79+
{
80+
// Initialize the values.
81+
$latVal = -OpenLocationCode::LATITUDE_MAX * self::LAT_INTEGER_MULTIPLIER;
82+
$lngVal = -OpenLocationCode::LONGITUDE_MAX * self::LNG_INTEGER_MULTIPLIER;
83+
// Define the place value for the digits. We'll divide this down as we work through the code.
84+
$latPlaceVal = self::LAT_MSP_VALUE;
85+
$lngPlaceVal = self::LNG_MSP_VALUE;
86+
for ($i = OpenLocationCode::PAIR_CODE_LENGTH; $i < min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT); $i += 2) {
87+
$latPlaceVal = intdiv($latPlaceVal, OpenLocationCode::ENCODING_BASE);
88+
$lngPlaceVal = intdiv($lngPlaceVal, OpenLocationCode::ENCODING_BASE);
89+
$latVal += strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i]) * $latPlaceVal;
90+
$lngVal = strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i + 1]) * $lngPlaceVal;
91+
}
92+
unset($i);
93+
for ($i = OpenLocationCode::PAIR_CODE_LENGTH; $i < min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT); $i++) {
94+
$latPlaceVal = intdiv($latPlaceVal, OpenLocationCode::GRID_ROWS);
95+
$lngPlaceVal = intdiv($lngPlaceVal, OpenLocationCode::GRID_COLUMNS);
96+
$digit = strpos(OpenLocationCode::CODE_ALPHABET, $strippedCode[$i]);
97+
$row = intdiv($digit, OpenLocationCode::GRID_COLUMNS);
98+
$col = $digit % OpenLocationCode::GRID_COLUMNS;
99+
$latVal += $row * $latPlaceVal;
100+
$lngVal += $col * $lngPlaceVal;
101+
unset($digit);
102+
}
103+
unset($i);
104+
$latitudeLo = $latVal / self::LAT_INTEGER_MULTIPLIER;
105+
$longitudeLo = $lngVal / self::LNG_INTEGER_MULTIPLIER;
106+
$latitudeHi = ($latVal + $latPlaceVal) / self::LAT_INTEGER_MULTIPLIER;
107+
$longitudeHi = ($lngVal + $lngPlaceVal) / self::LNG_INTEGER_MULTIPLIER;
108+
return new CodeArea(
109+
$latitudeLo,
110+
$longitudeLo,
111+
$latitudeHi,
112+
$longitudeHi,
113+
min(strlen($strippedCode), OpenLocationCode::MAX_DIGIT_COUNT),
114+
);
115+
}
116+
}

0 commit comments

Comments
 (0)