File Coverage

blib/lib/Geo/SpatialDB.pm
Criterion Covered Total %
statement 39 116 33.6
branch 1 38 2.6
condition 3 39 7.6
subroutine 12 18 66.6
pod 1 4 25.0
total 56 215 26.0


line stmt bran cond sub pod time code
1             package Geo::SpatialDB;
2             $Geo::SpatialDB::VERSION = '0.000_001'; # TRIAL
3              
4 2     2   33635 $Geo::SpatialDB::VERSION = '0.000001';use Moo 2;
  2         17166  
  2         8  
5 2     2   2565 use Geo::SpatialDB::BBox;
  2         2  
  2         40  
6 2     2   609 use Geo::SpatialDB::Location;
  2         3  
  2         52  
7 2     2   643 use Geo::SpatialDB::Path;
  2         3  
  2         44  
8 2     2   684 use Geo::SpatialDB::RouteSegment;
  2         3  
  2         45  
9 2     2   625 use Geo::SpatialDB::Route;
  2         4  
  2         49  
10 2     2   621 use Geo::SpatialDB::Area;
  2         4  
  2         45  
11 2     2   9 use Module::Runtime 'require_module';
  2         2  
  2         7  
12 2     2   444 use Log::Any '$log';
  2         7556  
  2         12  
13             sub _croak { require Carp; goto &Carp::croak }
14 2     2   3023 use namespace::clean;
  2         3  
  2         6  
15              
16             # ABSTRACT: Generic reverse-geocoding engine on top of key/value storage
17              
18              
19             has zoom_levels => is => 'rw', default => sub { [
20             # tiles per circle, microdegrees per tile
21             [ 360*4, int(1_000_000/4) ],
22             [ 360*32, int(1_000_000/32) ],
23             [ 360*128, int(1_000_000/128) ],
24             ] };
25             has latlon_precision => is => 'rw', default => sub { 1_000_000 };
26             has storage => is => 'lazy', coerce => \&_build_storage;
27              
28             sub _build_storage {
29 1 50 33 1   14 if (!$_[0] || ref($_[0]) eq 'HASH') {
    0 0        
30 1   50     1 my %cfg= %{ $_[0] // {} };
  1         5  
31 1   50     7 my $class= delete $cfg{CLASS} // 'LMDB_Storable';
32 1         2 $class= "Geo::SpatialDB::Storage::$class";
33 1         2 require_module($class);
34 0           $class->new(%cfg);
35             }
36             elsif ($_[0] && ref($_[0])->can('get')) {
37 0           $_[0]
38             } else {
39 0           _croak("Can't coerce $_[0] to Storage instance");
40             }
41             }
42              
43             sub tile_for_lat_lon {
44 0     0 0   my ($self, $lat, $lon, $tile_udeg)= @_;
45 2     2   1476 use integer;
  2         16  
  2         6  
46 0           $lat= $lat % 360_000_000;
47 0 0         $lat += 360_000_000 if $lat < 0;
48 0           $lon= $lon % 360_000_000;
49 0 0         $lon += 360_000_000 if $lon < 0;
50 0           return ($lat / $tile_udeg, $lon / $tile_udeg);
51             }
52              
53             sub _register_entity_within {
54 0     0     my ($self, $ent, $lat0, $lon0, $lat1, $lon1)= @_;
55 0           my $stor= $self->storage;
56             # Convert radius to arc degrees
57 0           my $level= $#{ $self->zoom_levels };
  0            
58 0   0       $level-- while $level && ($lat1 - $lat0 > $self->zoom_levels->[$level][1]);
59 0           my ($tile_per_circle, $tile_udeg)= @{ $self->zoom_levels->[$level] };
  0            
60 0           my ($lat_key_0, $lon_key_0)= $self->tile_for_lat_lon($lat0, $lon0, $tile_udeg);
61 0           my ($lat_key_1, $lon_key_1)= $self->tile_for_lat_lon($lat1-1, $lon1-1, $tile_udeg);
62            
63             # TODO: correctly handle wrap-around at lon=0, and edge cases at the poles
64             # or, choose an entirely different bucket layout
65 0           for my $lat_k ($lat_key_0 .. $lat_key_1) {
66 0           for my $lon_k ($lon_key_0 .. $lon_key_1) {
67             # Load detail node, add new entity ref, and save detail node
68 0           my $bucket_key= ":$level,$lat_k,$lon_k";
69 0   0       my $bucket= $stor->get($bucket_key) // {};
70 0           my %seen;
71 0   0       $bucket->{ent}= [ grep { !$seen{$_}++ } @{ $bucket->{ent}//[] }, $ent->id ];
  0            
  0            
72 0           $stor->put($bucket_key, $bucket);
73             }
74             }
75             }
76              
77             sub add_entity {
78 0     0 0   my ($self, $e)= @_;
79             # If it's a location, index the point. Use radius to determine what level to include it in.
80 0 0         if ($e->isa('Geo::SpatialDB::Location')) {
    0          
    0          
81 0   0       my ($lat, $lon, $rad)= ($e->lat, $e->lon, $e->rad//0);
82             # Convert radius to lat arc degrees and lon arc degrees
83 0 0         my $dLat= $rad? ($rad / 111000 * $self->latlon_precision) : 0;
84             # Longitude is affected by latitude
85 0 0         my $dLon= $rad? ($rad / (111699 * cos($lat / (360*$self->latlon_precision)))) : 0;
86 0           $self->storage->put($e->id, $e);
87 0           $self->_register_entity_within($e, $lat - $dLat, $lon - $dLon, $lat + $dLat, $lon + $dLon);
88             }
89             elsif ($e->isa('Geo::SpatialDB::RouteSegment')) {
90 0 0         unless (@{ $e->path }) {
  0            
91 0           $log->warn("RouteSegment with zero-length path...");
92             }
93 0           my ($lat0, $lon0, $lat1, $lon1);
94 0           for my $pt (@{ $e->path }) {
  0            
95 0 0 0       $lat0= $pt->[0] if !defined $lat0 or $lat0 > $pt->[0];
96 0 0 0       $lat1= $pt->[0] if !defined $lat1 or $lat1 < $pt->[0];
97 0 0 0       $lon0= $pt->[1] if !defined $lon0 or $lon0 > $pt->[1];
98 0 0 0       $lon1= $pt->[1] if !defined $lon1 or $lon1 < $pt->[1];
99             }
100 0           $self->storage->put($e->id, $e);
101 0           $self->_register_entity_within($e, $lat0, $lon0, $lat1, $lon1);
102             }
103             elsif ($e->isa('Geo::SpatialDB::Route')) {
104             # Routes don't get added to positional buckets. Just their segments.
105 0           $self->storage->put($e->id, $e);
106             }
107             else {
108 0           $log->warn("Ignoring entity ".$e->id);
109             }
110             }
111              
112             # min_rad - the minimum radius (meters) of object that we care to see
113             sub _get_bucket_keys_for_area {
114 0     0     my ($self, $bbox, $min_dLat)= @_;
115 0           my @keys;
116 0 0         $log->debugf(" sw %d,%d ne %d,%d min arc %d",
117             $bbox->lat0,$bbox->lon0, $bbox->lat1,$bbox->lon1, $min_dLat)
118             if $log->is_debug;
119              
120 0           for my $level (0 .. $#{ $self->zoom_levels }) {
  0            
121 0           my ($tile_per_circle, $tile_udeg)= @{ $self->zoom_levels->[$level] };
  0            
122 0 0         last if $tile_udeg < $min_dLat;
123             # Iterate south to north, west to east
124 0           my ($lat_key_0, $lon_key_0)= $self->tile_for_lat_lon($bbox->lat0, $bbox->lon0, $tile_udeg);
125 0           my ($lat_key_1, $lon_key_1)= $self->tile_for_lat_lon($bbox->lat1-1, $bbox->lon1-1, $tile_udeg);
126             # TODO: correctly handle wrap-around at lon=0, and edge cases at the poles
127             # or, choose an entirely different bucket layout
128 0           for my $lat_key ($lat_key_0 .. $lat_key_1) {
129             push @keys, ":$level,$lat_key,$_"
130 0           for $lon_key_0 .. $lon_key_1;
131             }
132             }
133 0           return @keys;
134             }
135              
136              
137             sub find_at {
138 0     0 1   my ($self, $lat, $lon, $radius, $filter)= @_;
139             # Convert radius to lat arc degrees and lon arc degrees
140 0 0         my $dLat= $radius? ($radius / 111000 * $self->latlon_precision) : 0;
141             # Longitude is affected by latitude
142 0 0         my $dLon= $radius? ($radius / (111699 * cos($lat / (360*$self->latlon_precision)))) : 0;
143 0           $self->find_in(
144             Geo::SpatialDB::BBox->new($lat-$dLat, $lon-$dLon, $lat+$dLat, $lon+$dLon),
145             $dLat/200
146             );
147             }
148              
149             sub find_in {
150 0     0 0   my ($self, $bbox, $min_arc)= @_;
151 0           $bbox= Geo::SpatialDB::BBox->coerce($bbox);
152 0   0       $min_arc //= $bbox->dLat/200;
153 0           my @keys= $self->_get_bucket_keys_for_area($bbox, $min_arc);
154 0           my %result= ( bbox => $bbox->clone );
155 0           $log->debugf(" searching buckets: %s", \@keys);
156 0           for (@keys) {
157 0 0         my $bucket= $self->storage->get($_)
158             or next;
159 0   0       for (@{ $bucket->{ent} // [] }) {
  0            
160 0   0       $result{entities}{$_} //= $self->storage->get($_);
161             }
162             }
163 0           \%result;
164             }
165              
166             1;
167              
168             __END__