File Coverage

blib/lib/Geo/Elevation/HGT.pm
Criterion Covered Total %
statement 29 209 13.8
branch 0 94 0.0
condition 0 27 0.0
subroutine 10 21 47.6
pod 3 3 100.0
total 42 354 11.8


line stmt bran cond sub pod time code
1             package Geo::Elevation::HGT;
2              
3 1     1   52734 use 5.010;
  1         3  
4 1     1   5 use strict;
  1         2  
  1         17  
5 1     1   4 use warnings;
  1         1  
  1         20  
6 1     1   402 use POSIX ();
  1         5046  
  1         24  
7 1     1   6 use Carp;
  1         2  
  1         578  
8             # set the version for version checking
9             our $VERSION = '0.08';
10             # file-private lexicals
11             my $grid_size; # .hgt grid size = 3601x3601 for 1-minute DEMs or 7201x7201 for 0.5-minute DEMs or 1201x1201 for 3-minute DEMs
12             my @DEMnames;
13             my @default_DEMs;
14             my @want_DEMnames;
15             my $status_descr;
16             my $url;
17             my $folder;
18             my $cache_folder;
19             my $debug;
20             my $bicubic;
21             my $bicubic_current;
22             my $bicubic_mixed;
23             my $switch;
24             my $cache;
25             my $fail;
26              
27             sub new {
28 0     0 1   my ($class, %params) = @_;
29 0           %params = (
30             url => "https://elevation-tiles-prod.s3.amazonaws.com/skadi", # info at https://registry.opendata.aws/terrain-tiles/
31             folder => "https://elevation-tiles-prod.s3.amazonaws.com/skadi",
32             cache_folder => "",
33             debug => 0,
34             bicubic => 0,
35             bicubic_current => 0,
36             bicubic_mixed => 0,
37             %params
38             );
39 0           my $self = {};
40 0           while ( my($key,$value) = each %params ) {
41 0           $self->{$key} = $value;
42             }
43 0           bless $self, $class;
44 0           return $self;
45             }
46              
47             sub get_elevation_batch_hgt {
48 0     0 1   my ($self, $batch_latlon) = @_;
49 0           my @elegeh;
50 0           for my $latlon ( @$batch_latlon ) {
51 0           my ($lat, $lon) = @$latlon;
52 0           push (@elegeh, $self->get_elevation_hgt ($lat, $lon));
53             }
54 0           return \@elegeh;
55             }
56              
57             sub get_elevation_hgt {
58 0     0 1   my ($self, $lat, $lon) = @_;
59 0           $folder = $self->{folder};
60 0           $url = $self->{url};
61 0           $cache_folder = $self->{cache_folder};
62 0           $debug = $self->{debug};
63 0           $bicubic = $self->{bicubic};
64 0           $bicubic_current = $self->{bicubic_current};
65 0           $bicubic_mixed = $self->{bicubic_mixed};
66 0           $status_descr = "Memory";
67 0 0         say STDERR "get_elevation_hgt" if $debug;
68 0 0         say STDERR " Parameters: folder=>'$folder', url=>'$url', cache_folder=>'$cache_folder', debug=>$debug, bicubic=>$bicubic" if $debug;
69 0 0         say STDERR " Input lat lon: $lat $lon" if $debug;
70 0           my $flat = POSIX::floor $lat;
71 0           my $flon = POSIX::floor $lon;
72 0 0         my $ns = $flat < 0 ? "S" : "N";
73 0 0         my $ew = $flon < 0 ? "W" : "E";
74 0           my $lt = sprintf ("%02s", abs($flat));
75 0           my $ln = sprintf ("%03s", abs($flon));
76 0           my $DEMname = "$ns$lt$ew$ln";
77 0 0         say STDERR " Tile lat lon: $flat $flon" if $debug;
78             # read DEM unless already defined
79 0 0 0       say STDERR " Using DEM in memory: '$flat $flon'" if ($debug and defined $self->{DEMs}{$DEMname}{DEM});
80 0   0       $self->{DEMs}{$DEMname}{DEM} //= $self->_readDEM($DEMname); # //= Logical Defined-Or Assignment Operator
81 0           my $dem = $self->{DEMs}{$DEMname}{DEM};
82 0 0         say STDERR " Status: $status_descr" if $debug;
83 0           $self->{lat} = $lat;
84 0           $self->{lon} = $lon;
85 0           $self->{status_descr} = $status_descr;
86 0           $self->{switch} = $switch;
87 0           $self->{cache} = $cache;
88 0           $self->{fail} = $fail;
89 0           $self->{DEMname} = $DEMname;
90 0 0         if (ref($dem) eq "") {
91 0 0         say STDERR " No data in DEM: '$flat $flon' returning elevation 0" if $debug;
92 0           $self->{grid_size} = 0;
93 0           $self->{elevation} = 0;
94 0           return $self->{elevation};
95             }
96 0           $grid_size = sqrt (length ($$dem)/2); # grid size of DEM
97 0 0 0       unless ($grid_size == 3601 or $grid_size == 7201 or $grid_size == 1201) {
      0        
98 0           croak "Unknown tile format for '$self->{DEMs}{$DEMname}{DEMpath}': grid size is '$grid_size', should be 3601 or 7201 or 1201";
99             }
100             # the DEMs start in the NW corner with $grid_size - 1 intervals
101 0           my $ilat = (1 - ($lat - $flat)) * ($grid_size - 1);
102 0           my $ilon = ($lon - $flon) * ($grid_size - 1);
103 0 0         say STDERR " Grid size lat lon: $grid_size $ilat $ilon" if $debug;
104 0           $self->{grid_size} = $grid_size;
105 0 0         $self->{elevation} = $bicubic ? _interpolate_bicubic ($dem, $ilat, $ilon) : _interpolate_bilinear ($dem, $ilat, $ilon);
106 0           return $self->{elevation};
107             }
108              
109             sub _interpolate_bicubic {
110 1     1   6 use List::Util qw( min max );
  1         1  
  1         888  
111 0     0     my ($f, $x, $y) = @_;
112 0           my $x1 = POSIX::floor $x;
113 0           my $x2 = POSIX::ceil $x;
114 0           my $x0 = max ($x1-1, 0);
115 0           my $x3 = min ($x2+1, $grid_size-1);
116 0           my $y1 = POSIX::floor $y;
117 0           my $y2 = POSIX::ceil $y;
118 0           my $y0 = max ($y1-1, 0);
119 0           my $y3 = min ($y2+1, $grid_size-1);
120              
121 0           my $f00 = unpack ("s>*", substr ($$f, 2*($x0*$grid_size+$y0), 2)); # unpack signed big-endian 16-bit integer to elevation value
122 0           my $f01 = unpack ("s>*", substr ($$f, 2*($x0*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
123 0           my $f02 = unpack ("s>*", substr ($$f, 2*($x0*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
124 0           my $f03 = unpack ("s>*", substr ($$f, 2*($x0*$grid_size+$y3), 2)); # unpack signed big-endian 16-bit integer to elevation value
125              
126 0           my $f10 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y0), 2)); # unpack signed big-endian 16-bit integer to elevation value
127 0           my $f11 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
128 0           my $f12 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
129 0           my $f13 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y3), 2)); # unpack signed big-endian 16-bit integer to elevation value
130              
131 0           my $f20 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y0), 2)); # unpack signed big-endian 16-bit integer to elevation value
132 0           my $f21 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
133 0           my $f22 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
134 0           my $f23 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y3), 2)); # unpack signed big-endian 16-bit integer to elevation value
135              
136 0           my $f30 = unpack ("s>*", substr ($$f, 2*($x3*$grid_size+$y0), 2)); # unpack signed big-endian 16-bit integer to elevation value
137 0           my $f31 = unpack ("s>*", substr ($$f, 2*($x3*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
138 0           my $f32 = unpack ("s>*", substr ($$f, 2*($x3*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
139 0           my $f33 = unpack ("s>*", substr ($$f, 2*($x3*$grid_size+$y3), 2)); # unpack signed big-endian 16-bit integer to elevation value
140              
141 0 0         say STDERR " Grid corners: ($x0,$y0) ($x1,$y1) ($x2,$y2) ($x3,$y3)" if $debug;
142             # say STDERR " Elevation at corners: $f11 $f21 $f12 $f22" if $debug;
143 0 0         say STDERR " Elevation at corners: $f00 $f01 $f02 $f03" if $debug;
144 0 0         say STDERR " Elevation at corners: $f10 $f11 $f12 $f13" if $debug;
145 0 0         say STDERR " Elevation at corners: $f20 $f21 $f22 $f23" if $debug;
146 0 0         say STDERR " Elevation at corners: $f30 $f31 $f32 $f33" if $debug;
147             # Bicubic interpolation as per https://www.paulinternet.nl/?page=bicubic
148             # using the simplifying fact that ($x2-$x1)==1 and ($y2-$y1)==1
149 0           my $xx = $x - $x1;
150 0           my $yy = $y - $y1;
151 0           my $f0 = _bicubic ($f00, $f01, $f02, $f03, $yy);
152 0           my $f1 = _bicubic ($f10, $f11, $f12, $f13, $yy);
153 0           my $f2 = _bicubic ($f20, $f21, $f22, $f23, $yy);
154 0           my $f3 = _bicubic ($f30, $f31, $f32, $f33, $yy);
155 0 0         say STDERR " bicubic interpolated elevation: "._bicubic ($f0, $f1, $f2, $f3, $xx) if $debug;
156 0           return _bicubic ($f0, $f1, $f2, $f3, $xx);
157             }
158              
159             sub _bicubic {
160 0     0     my ($f0, $f1, $f2, $f3, $x) = @_;
161 0 0         if ($bicubic_current) {
    0          
162 0           return $f1 + $x*($f1 - $f0 + $x*(2.0*$f0 - 5.0*$f1 + 4.0*$f2 - $f3 + $x*(3.0*($f1 - $f2) + $f3 - $f0))); # use slope of a line between the previous and the «current» point as the derivative at a point, that is (p1-p0) and (p3-p2)
163             }
164             elsif ($bicubic_mixed) {
165 0           return $f1 + 0.75 * $x*($f2 - $f0 + $x*(2.0*$f0 - 5.0*$f1 + 4.0*$f2 - $f3 + $x*(3.0*($f1 - $f2) + $f3 - $f0))); # use «current» and «next» point «mixed» slope
166             }
167             else {
168 0           return $f1 + 0.5 * $x*($f2 - $f0 + $x*(2.0*$f0 - 5.0*$f1 + 4.0*$f2 - $f3 + $x*(3.0*($f1 - $f2) + $f3 - $f0))); # use slope of a line between the previous and the «next» point as the derivative at a point, that is (p2-p0)/2 and (p3-p1)/2
169             }
170             }
171              
172             sub _interpolate_bilinear {
173 0     0     my ($f, $x, $y) = @_;
174 0           my $x1 = POSIX::floor $x;
175 0           my $x2 = POSIX::ceil $x;
176 0           my $y1 = POSIX::floor $y;
177 0           my $y2 = POSIX::ceil $y;
178 0           my $f11 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
179 0           my $f21 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y1), 2)); # unpack signed big-endian 16-bit integer to elevation value
180 0           my $f12 = unpack ("s>*", substr ($$f, 2*($x1*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
181 0           my $f22 = unpack ("s>*", substr ($$f, 2*($x2*$grid_size+$y2), 2)); # unpack signed big-endian 16-bit integer to elevation value
182 0 0         say STDERR " Grid corners: ($x1,$y1) ($x2,$y1) ($x1,$y2) ($x2,$y2)" if $debug;
183 0 0         say STDERR " Elevation at corners: $f11 $f21 $f12 $f22" if $debug;
184             # bilinear interpolation as per https://github.com/racemap/elevation-service/blob/master/hgt.js
185             # using the simplifying fact that ($x2-$x1)==1 and ($y2-$y1)==1
186 0           my $xx = $x - $x1;
187 0           my $yy = $y - $y1;
188 0           my $f1 = _avg ($f11, $f21, $xx);
189 0           my $f2 = _avg ($f12, $f22, $xx);
190 0 0         say STDERR " bilinear interpolated elevation: "._avg ($f1, $f2, $yy) if $debug;
191 0           return _avg ($f1, $f2, $yy);
192             }
193              
194             sub _avg {
195 0     0     my ($f1, $f2, $x) = @_;
196 0           return $f1 + ($f2 - $f1) * $x;
197             }
198              
199             sub _readDEM {
200 1     1   1157 use IO::Uncompress::AnyUncompress qw(anyuncompress $AnyUncompressError);
  1         66880  
  1         106  
201 1     1   1161 use HTTP::Tiny;
  1         27942  
  1         464  
202 0     0     my ($self, $DEMname) = @_;
203 0           my $nslt = substr($DEMname,0,3);
204 0           my $path_to_hgt_gz = "$nslt/$DEMname.hgt.gz";
205 0           $switch = 0;
206 0           $cache = 0;
207 0           $fail = 0;
208 0           @default_DEMs = ("$path_to_hgt_gz", "$DEMname.zip");
209 0           @want_DEMnames = ("$DEMname.hgt.gz", "$DEMname.zip");
210 0           my $dem;
211 0 0         $status_descr = -d $folder ? "Folder" : "Url";
212 0 0         my $path = -d $folder ? _findDEM ($folder) : "$folder/$path_to_hgt_gz";
213 0 0         if (-e $path) {
214 0 0         say STDERR " Reading DEM '$path'" if $debug;
215 0           $self->{DEMs}{$DEMname}{DEMpath}=$path;
216 0 0         anyuncompress $path => \$dem or croak "anyuncompress failed on '$path': $AnyUncompressError";
217 0           return \$dem;
218             }
219 0 0         unless ($path =~ m/^https?:\/\//) {
220 0 0         say STDERR " DEM '$path' not found -> switch to '$url'" if $debug;
221 0           $status_descr .= "->Switch";
222 0           $switch = 1;
223             }
224 0 0         my $cache_path = $cache_folder ne "" ? _findDEM ($cache_folder) : undef;
225 0 0 0       if (defined $cache_path and -e $cache_path) {
226 0 0         say STDERR " Reading DEM from cache '$cache_path'" if $debug;
227 0           $self->{DEMs}{$DEMname}{DEMpath}=$cache_path;
228 0           $status_descr .= "->Cached";
229 0           $cache = 1;
230 0 0         anyuncompress $cache_path => \$dem or croak "anyuncompress failed on '$cache_path': $AnyUncompressError";
231 0           return \$dem;
232             }
233 0           $path = "$url/$path_to_hgt_gz";
234 0 0         say STDERR " Getting DEM '$path'" if $debug;
235 0 0         $status_descr .= "->Url" unless ($status_descr eq "Url");
236 0           my $response = HTTP::Tiny->new->get($path); # get gzip archive file .hgt.gz
237 0 0         unless ($response->{success}) {
238             # no success
239 0           carp " DEM '$path'- $response->{status} $response->{reason}. All of its elevations will read as 0";
240 0           $status_descr .= "->Failed";
241 0           $fail = 1;
242 0           return 0;
243             }
244 0 0 0       if ($cache_folder ne "" and -d $cache_folder) {
245 0 0         unless (-d "$cache_folder/$nslt") {mkdir "$cache_folder/$nslt", 0755}
  0            
246 0 0         open my $file_handle, '>', "$cache_path" or croak "'$cache_path' error opening: $!";
247 0           binmode $file_handle;
248 0           print $file_handle $response->{content};
249 0           close $file_handle;
250 0           $status_descr .= "->Saved";
251 0 0         say STDERR " Saved DEM cache file '$cache_path'" if $debug;
252             }
253 0           $self->{DEMs}{$DEMname}{DEMpath}=$path;
254 0 0         anyuncompress \$response->{content} => \$dem or croak "anyuncompress failed on '$path': $AnyUncompressError";
255 0           return \$dem;
256             }
257              
258             sub _findDEM {
259 1     1   8 use File::Find;
  1         2  
  1         199  
260 0     0     my ($folder) = @_;
261 0           for my $default (@default_DEMs) {
262 0 0 0       say STDERR " Found default DEM '$folder/$default'" if ($debug and -e "$folder/$default");
263 0 0         return "$folder/$default" if (-e "$folder/$default");
264             }
265 0           splice (@DEMnames,0);
266 0           find(\&_wanted, $folder);
267 0 0 0       say STDERR " Found wanted DEM '$DEMnames[0]'" if ($debug and defined $DEMnames[0]);
268 0   0       return $DEMnames[0] // "$folder/$default_DEMs[0]";
269             }
270              
271             sub _wanted {
272 1     1   13 use List::Util 'any';
  1         2  
  1         119  
273 0     0     my $filename = $_;
274 0 0   0     push (@DEMnames, $File::Find::name) if any {$_ =~ m/^$filename$/i} @want_DEMnames; # add file to list if matched
  0            
275             }
276              
277             1; # End of Geo::Elevation::HGT
278              
279             __END__