Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
81.82% covered (success)
81.82%
9 / 11
CRAP
97.86% covered (success)
97.86%
137 / 140
GeoDNA
0.00% covered (danger)
0.00%
0 / 1
81.82% covered (success)
81.82%
9 / 11
41
97.86% covered (success)
97.86%
137 / 140
 encode
100.00% covered (success)
100.00%
1 / 1
7
100.00% covered (success)
100.00%
27 / 27
 decode
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
8 / 8
 boundingBox
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
12 / 12
 addVector
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
5 / 5
 pointFromPointBearingAndDistance
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
10 / 10
 distanceInKm
0.00% covered (danger)
0.00%
0 / 1
3.10
77.78% covered (success)
77.78%
7 / 9
 neighbours
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
13 / 13
 neighboursWithinRadius
100.00% covered (success)
100.00%
1 / 1
5
100.00% covered (success)
100.00%
31 / 31
 reduce
0.00% covered (danger)
0.00%
0 / 1
9
95.45% covered (success)
95.45%
21 / 22
 normalise
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 mod
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
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);
    }
}