Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
81.82% |
9 / 11 |
CRAP | |
97.86% |
137 / 140 |
| GeoDNA | |
0.00% |
0 / 1 |
|
81.82% |
9 / 11 |
41 | |
97.86% |
137 / 140 |
| encode | |
100.00% |
1 / 1 |
7 | |
100.00% |
27 / 27 |
|||
| decode | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
| boundingBox | |
100.00% |
1 / 1 |
5 | |
100.00% |
12 / 12 |
|||
| addVector | |
100.00% |
1 / 1 |
1 | |
100.00% |
5 / 5 |
|||
| pointFromPointBearingAndDistance | |
100.00% |
1 / 1 |
2 | |
100.00% |
10 / 10 |
|||
| distanceInKm | |
0.00% |
0 / 1 |
3.10 | |
77.78% |
7 / 9 |
|||
| neighbours | |
100.00% |
1 / 1 |
5 | |
100.00% |
13 / 13 |
|||
| neighboursWithinRadius | |
100.00% |
1 / 1 |
5 | |
100.00% |
31 / 31 |
|||
| reduce | |
0.00% |
0 / 1 |
9 | |
95.45% |
21 / 22 |
|||
| normalise | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| mod | |
100.00% |
1 / 1 |
1 | |
100.00% |
1 / 1 |
|||
| <?php | |
| /** | |
| * @author Informática Asocaida SA de CV | |
| * @copyright 2017 | |
| * @license MIT | |
| */ | |
| namespace ia\Geo; | |
| /** | |
| * GeoDNA Generate, decode, get adjacent Geohashes | |
| * | |
| * from version 0.4 | |
| */ | |
| class GeoDNA { | |
| private $RADIUS_OF_EARTH = 6378100.00; | |
| private $ALPHABET = [ "g", "a", "t", "c", ]; | |
| private $DECODE_MAP = [ | |
| 'g' => 0, | |
| 'a' => 1, | |
| 't' => 2, | |
| 'c' => 3, | |
| ]; | |
| public function encode(float $latitude, float $longitude, array $options = [] ) { | |
| $precision = isset( $options['precision'] ) ? $options['precision'] : 22; | |
| if( !empty($options['radians']) ) { | |
| $latitude = rad2deg( $latitude ); | |
| $longitude = rad2deg( $longitude ); | |
| } | |
| $bits = $this->normalise( $latitude, $longitude ); | |
| $latitude = $bits[0]; | |
| $longitude = $bits[1]; | |
| if( $longitude < 0 ) { | |
| $geodna = 'w'; | |
| $loni = [ -180.00, 0.00 ]; | |
| } else { | |
| $geodna = 'e'; | |
| $loni = [ 0.00, 180.00 ]; | |
| } | |
| $lati = [ -90.00, 90.00 ]; | |
| while ( strlen($geodna) < $precision ) { | |
| $ch = 0; | |
| $mid = ( $loni[0] + $loni[1] ) / 2.00; | |
| if( $longitude > $mid ) { | |
| $ch |= 2; | |
| $loni = [ $mid, $loni[1] ]; | |
| } else { | |
| $loni = [ $loni[0], $mid ]; | |
| } | |
| $mid = ( $lati[0] + $lati[1] ) / 2.00; | |
| if( $latitude > $mid ) { | |
| $ch |= 1; | |
| $lati = [ $mid, $lati[1] ]; | |
| } else { | |
| $lati = [ $lati[0], $mid ]; | |
| } | |
| $geodna .= $this->ALPHABET[$ch]; | |
| } | |
| return $geodna; | |
| } | |
| public function decode(string $geodna, array $options = [] ) { | |
| $bits = $this->boundingBox( $geodna ); | |
| $lati = $bits[0]; | |
| $loni = $bits[1]; | |
| $lat = ( $lati[0] + $lati[1] ) / 2.0; | |
| $lon = ( $loni[0] + $loni[1] ) / 2.0; | |
| if(!empty($options['radians']) ) { | |
| return [ deg2rad( $lat ), deg2rad( $lon ) ]; | |
| } | |
| return [ $lat, $lon ]; | |
| } | |
| // locates the min/max $lat/lons around the $geo_dna | |
| public function boundingBox(string $geodna ) { | |
| $lati = [ '-90.0', '90.0' ]; | |
| $loni = $geodna[0] === 'w' ? [ -180.0, 0.0] : [0.0, 180.0 ]; | |
| $charLen = strlen($geodna); | |
| for( $i = 1; $i < $charLen; $i++ ) { | |
| $cd = $this->DECODE_MAP[$geodna[$i]]; | |
| if( $cd & 2 ) { | |
| // $loni = [ $this->avg($loni), $loni[1] ]; | |
| $loni = [ ($loni[0]+$loni[1])/2.00, $loni[1] ]; | |
| } else { | |
| $loni = [ $loni[0], ($loni[0]+$loni[1])/2.00 ]; | |
| // $loni = [ $loni[0], $this->avg($loni) ]; | |
| } | |
| if( $cd & 1 ) { | |
| $lati = [ ($lati[0]+$lati[1])/2.00, $lati[1] ]; | |
| //$lati = [ $this->avg($lati), $lati[1] ]; | |
| } else { | |
| $lati = [ $lati[0], ($lati[0]+$lati[1])/2.00 ]; | |
| //$lati = [ $lati[0], $this->avg($lati) ]; | |
| } | |
| } | |
| return [ $lati, $loni ]; | |
| } | |
| public function addVector(string $geodna, float $dy, float $dx ) { | |
| $bits = $this->decode( $geodna ); | |
| $lat = $bits[0]; | |
| $lon = $bits[1]; | |
| return [ | |
| $this->mod(( $lat + 90.0 + $dy ), 180.0 ) - 90.0, | |
| $this->mod(( $lon + 180.0 + $dx ), 360.0 ) - 180.0 | |
| ]; | |
| } | |
| public function pointFromPointBearingAndDistance( $geodna, $bearing, $distance, $options = [] ) { | |
| $distance = $distance * 1000; // make it metres instead of kilometres | |
| $precision = isset($options['precision']) ? $options['precision'] : strlen($geodna); | |
| $bits = $this->decode( $geodna, [ 'radians' => true ] ); | |
| $lat1 = $bits[0]; | |
| $lon1 = $bits[1]; | |
| $lat2 = asin( sin( $lat1 ) * cos( $distance / $this->RADIUS_OF_EARTH ) + | |
| cos( $lat1 ) * sin( $distance / $this->RADIUS_OF_EARTH ) * cos( $bearing ) ); | |
| $lon2 = $lon1 + atan2( sin( $bearing ) * sin( $distance / $this->RADIUS_OF_EARTH ) * cos( $lat1 ), | |
| cos( $distance / $this->RADIUS_OF_EARTH ) - sin( $lat1 ) * sin( $lat2 )); | |
| return $this->encode( $lat2, $lon2, [ 'precision' => $precision, 'radians' => true ] ); | |
| } | |
| public function distanceInKm(string $ga, string $gb ) { | |
| $a = $this->decode( $ga ); | |
| $b = $this->decode( $gb ); | |
| // if a[1] and b[1] have different signs, we need to trans$late | |
| // everything a bit in order for the formulae to work. | |
| if( $a[1] * $b[1] < 0.0 && abs( $a[1] - $b[1] ) > 180.0 ) { | |
| $a = $this->addVector( $ga, 0.0, 180.0 ); | |
| $b = $this->addVector( $gb, 0.0, 180.0 ); | |
| } | |
| $x = ( deg2rad($b[1]) - deg2rad($a[1]) ) * cos( ( deg2rad($a[0]) + deg2rad($b[0])) / 2 ); | |
| $y = ( deg2rad($b[0]) - deg2rad($a[0]) ); | |
| $d = sqrt( $x*$x + $y*$y ) * $this->RADIUS_OF_EARTH; | |
| return $d / 1000; | |
| } | |
| public function neighbours(string $geodna ) { | |
| $bits = $this->boundingBox( $geodna ); | |
| $lati = $bits[0]; | |
| $loni = $bits[1]; | |
| $width = abs( $loni[1] - $loni[0] ); | |
| $height = abs( $lati[1] - $lati[0] ); | |
| $options = [ 'precision' =>strlen($geodna) ]; | |
| $neighbours = []; | |
| for($i = -1; $i <= 1; $i++ ) { | |
| for( $j = -1; $j <= 1; $j++ ) { | |
| if( $i || $j ) { | |
| $bits = $this->addVector ( $geodna, $height * $i, $width * $j ); | |
| $neighbours[] = $this->encode( $bits[0], $bits[1], $options ); | |
| } | |
| } | |
| } | |
| return $neighbours; | |
| } | |
| // This is experimental!! | |
| // Totally unoptimised - use at your peril! | |
| public function neighboursWithinRadius(string $geodna, float $radius, array $options) { | |
| $piQuarter = pi()/4; | |
| $options['precision'] = isset( $options['precision'] ) ? $options['precision'] : strlen($geodna); | |
| $neighbours = []; | |
| $rh = $radius * sqrt(2); | |
| $start = $this->pointFromPointBearingAndDistance( $geodna, -$piQuarter, $rh, $options ); | |
| $end = $this->pointFromPointBearingAndDistance( $geodna, $piQuarter, $rh, $options ); | |
| $bbox = $this->boundingBox( $start ); | |
| $bits = $this->decode( $start ); | |
| $slon = $bits[1]; | |
| $bits = $this->decode( $end ); | |
| $elon = $bits[1]; | |
| $dheight = abs( $bbox[0][1] - $bbox[0][0] ); | |
| $dwidth = abs( $bbox[1][1] - $bbox[1][0] ); | |
| $n = $this->normalise( 0.0, abs( $elon - $slon ) ); | |
| $delta = abs($n[1]); | |
| $tlat = 0.0; | |
| $tlon = 0.0; | |
| $current = $start; | |
| while ( $tlat <= $delta ) { | |
| while ( $tlon <= $delta ) { | |
| $cbits = $this->addVector( $current, 0.0, $dwidth ); | |
| $current = $this->encode( $cbits[0], $cbits[1], $options ); | |
| $d = $this->distanceInKm( $current, $geodna ); | |
| if( $d <= $radius ) { | |
| $neighbours[] = $current; | |
| } | |
| $tlon = $tlon + $dwidth; | |
| } | |
| $tlat = $tlat + $dheight; | |
| $bits = $this->addVector( $start, -$tlat , 0.0 ); | |
| $current = $this->encode( $bits[0], $bits[1], $options ); | |
| $tlon = 0.0; | |
| } | |
| return $neighbours; | |
| } | |
| // This takes an array of GeoDNA codes and reduces it to its | |
| // minimal set of codes covering the same area. | |
| // Needs a more optimal impl. | |
| public function reduce(array $geodna_codes ) { | |
| // hash all the codes | |
| $codes = []; | |
| $geodna_codesLen = count($geodna_codes); | |
| for($i = 0; $i < $geodna_codesLen; $i++ ) { | |
| $codes[ $geodna_codes[$i] ] = 1; | |
| } | |
| $reduced = []; | |
| for ($i = 0; $i < $geodna_codesLen; $i++ ) { | |
| $code = $geodna_codes[$i]; | |
| if( array_key_exists($code, $codes)) { | |
| $parent = substr($code, 0, strlen($code) - 1 ); | |
| if( !empty($codes[ $parent . 'a' ]) | |
| && !empty($codes[ $parent . 't' ]) | |
| && !empty($codes[ $parent . 'g' ]) | |
| && !empty($codes[ $parent . 'c' ]) ){ | |
| $codes[ $parent . 'a' ] = null; | |
| $codes[ $parent . 't' ] = null; | |
| $codes[ $parent . 'g' ] = null; | |
| $codes[ $parent . 'c' ] = null; | |
| $reduced[] = $parent; | |
| } else { | |
| $reduced[] = $code; | |
| } | |
| } | |
| } | |
| if( $geodna_codesLen === count($reduced) ) { | |
| return $reduced; | |
| } | |
| return $this->reduce( $reduced ); | |
| } | |
| private function normalise(float $lat, float $lon ) { | |
| return [ | |
| $this->mod(( $lat + 90.0 ), 180.0 ) - 90.0, | |
| $this->mod(( $lon + 180.0 ), 360.0 ) - 180.0, | |
| ]; | |
| } | |
| private function mod($x, $m) { | |
| return fmod(fmod($x, $m) + $m, $m); | |
| } | |
| } |