humps 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/humps.rb +342 -0
- data/lib/humps/version.rb +3 -0
- metadata +47 -0
data/lib/humps.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
require 'rexml/document'
|
2
|
+
require 'open-uri'
|
3
|
+
|
4
|
+
class Humps
|
5
|
+
SRTM_PATH = "/var/gisdata/srtm/"
|
6
|
+
NED_PATH = "/var/gisdata/ned/ned_13_arc_second_1_degree_tiles/"
|
7
|
+
GI_PATH = "/var/gisdata/gi/"
|
8
|
+
ASTER_PATH = "/var/gisdata/aster/"
|
9
|
+
|
10
|
+
|
11
|
+
#fall through various sources in order of importance
|
12
|
+
def self.get_ele(x,y)
|
13
|
+
return -9999 unless x && y
|
14
|
+
if y > 59.9 #this is ghetto, but might be GI related, so high latitude
|
15
|
+
#ele = self.bilinear_gi(x, y)
|
16
|
+
ele = self.bilinear_aster(x, y)# if ele.nil? or ele < -420
|
17
|
+
else
|
18
|
+
ele = self.bilinear_ned(x,y)
|
19
|
+
ele = self.bilinear_srtm(x, y) if ele.nil? or ele < -420
|
20
|
+
ele = self.bilinear_aster(x, y) if ele.nil? or ele < -420
|
21
|
+
end
|
22
|
+
return ele
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.get_hdr(source, x, y)
|
26
|
+
hdr_fn, dem_fn = get_file_names(source, x, y)
|
27
|
+
return nil unless hdr_fn && File.exists?(hdr_fn)
|
28
|
+
|
29
|
+
hdr = {}
|
30
|
+
File.read(hdr_fn).split("\n").collect { |a| a.split }.each { |d| hdr[d.first] = d.last }
|
31
|
+
return {
|
32
|
+
:min_x => hdr['xllcorner'].to_f,
|
33
|
+
:min_y => hdr['yllcorner'].to_f,
|
34
|
+
:cellsize => hdr['cellsize'].to_f,
|
35
|
+
:ncols => hdr['ncols'].to_i,
|
36
|
+
:nrows => hdr['nrows'].to_i,
|
37
|
+
:data_file => dem_fn
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
############ ASTER related calls #################
|
45
|
+
def self.nn_aster(x,y)
|
46
|
+
hdr = self.get_hdr(:aster, x, y)
|
47
|
+
return -9999 if hdr.nil?
|
48
|
+
return nn(hdr, x, y, 2, 'ss')
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.bilinear_aster(x, y)
|
52
|
+
hdr = self.get_hdr(:aster, x, y)
|
53
|
+
return -9999 if hdr.nil?
|
54
|
+
return bilinear(hdr, x, y, 2, 'ss')
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.cubic_aster(x, y)
|
58
|
+
hdr = get_hdr(:aster, x,y)
|
59
|
+
return -9999 if hdr.nil?
|
60
|
+
return cubic(hdr, x, y, 2, 'ssss')
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
####### GI related calls ################
|
65
|
+
def self.nn_gi(x,y)
|
66
|
+
hdr = self.get_hdr(:gi, x, y)
|
67
|
+
return -9999 if hdr.nil?
|
68
|
+
return nn(hdr, x, y, 4, 'f')
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.bilinear_gi(x, y)
|
72
|
+
hdr = self.get_hdr(:gi, x, y)
|
73
|
+
return -9999 if hdr.nil?
|
74
|
+
return bilinear(hdr, x, y, 4, 'ff')
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.cubic_gi(x, y)
|
78
|
+
hdr = get_hdr(:gi, x,y)
|
79
|
+
return -9999 if hdr.nil?
|
80
|
+
return cubic(hdr, x, y, 4, 'ffff')
|
81
|
+
end
|
82
|
+
|
83
|
+
|
84
|
+
############# SRTM related calls ##############
|
85
|
+
def self.nn_srtm(x, y)
|
86
|
+
hdr = self.get_hdr(:srtm, x, y)
|
87
|
+
return -9999 if hdr.nil?
|
88
|
+
return nn(hdr, x, y, 2, 's')
|
89
|
+
end
|
90
|
+
|
91
|
+
def self.bilinear_srtm(x, y)
|
92
|
+
hdr = self.get_hdr(:srtm, x, y)
|
93
|
+
return -9999 if hdr.nil?
|
94
|
+
return bilinear(hdr, x, y, 2, 'ss')
|
95
|
+
end
|
96
|
+
|
97
|
+
def self.cubic_srtm(x,y)
|
98
|
+
hdr = get_hdr(:srtm, x,y)
|
99
|
+
return -9999 if hdr.nil?
|
100
|
+
return cubic(hdr, x, y, 2, 'ssss')
|
101
|
+
end
|
102
|
+
|
103
|
+
|
104
|
+
######## NED related calls ################
|
105
|
+
def self.nn_ned(x,y)
|
106
|
+
hdr = get_hdr(:ned, x,y)
|
107
|
+
return -9999 if hdr.nil?
|
108
|
+
return nn(hdr, x, y, 4, 'ff')
|
109
|
+
end
|
110
|
+
|
111
|
+
def self.bilinear_ned(x, y)
|
112
|
+
hdr = get_hdr(:ned, x, y)
|
113
|
+
return -9999 if hdr.nil?
|
114
|
+
return bilinear(hdr, x, y, 4, 'ff')
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.cubic_ned(x,y)
|
118
|
+
hdr = get_hdr(:ned, x,y)
|
119
|
+
return -9999 if hdr.nil?
|
120
|
+
return cubic(hdr, x, y, 4, 'ffff')
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
########## base methods ############33333
|
125
|
+
def self.nn(hdr, x, y, bytes, pack)
|
126
|
+
begin
|
127
|
+
x_offset = ((x-hdr[:min_x]) / hdr[:cellsize]).round
|
128
|
+
y_offset = (((hdr[:min_y] + hdr[:cellsize]*(hdr[:ncols]-1)) - y) / hdr[:cellsize]).round
|
129
|
+
byte_offset = bytes*(x_offset + hdr[:ncols]*y_offset)
|
130
|
+
ele = IO.read(hdr[:data_file], bytes, byte_offset).unpack(pack)[0]
|
131
|
+
return (10 * ele).round / 10.0
|
132
|
+
rescue
|
133
|
+
return -9999
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.bilinear(hdr, x, y, bytes, pack)
|
138
|
+
x_offset_abs = (x - hdr[:min_x]) / hdr[:cellsize]
|
139
|
+
y_offset_abs = ((hdr[:min_y] + hdr[:cellsize]*(hdr[:ncols]-1)) - y) / hdr[:cellsize]
|
140
|
+
x_offset = x_offset_abs.floor
|
141
|
+
y_offset = y_offset_abs.floor
|
142
|
+
x_index = x_offset_abs - x_offset
|
143
|
+
y_index = y_offset_abs - y_offset
|
144
|
+
num_bytes_to_read = bytes*2
|
145
|
+
|
146
|
+
byte_offset1 = bytes*(x_offset + hdr[:ncols] * y_offset)
|
147
|
+
byte_offset2 = bytes*(x_offset + hdr[:ncols] * (y_offset+1))
|
148
|
+
|
149
|
+
#puts "(#{x_offset_abs}, #{y_offset_abs}), (#{x_offset}, #{y_offset}), (#{x_index}, #{y_index})"
|
150
|
+
|
151
|
+
begin
|
152
|
+
a1, a2 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset1).unpack(pack)
|
153
|
+
a3, a4 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset2).unpack(pack)
|
154
|
+
rescue
|
155
|
+
#we wrapped around our DEM edges, let's just get nearest neighbor for simplicity in this rare case
|
156
|
+
return self.nn_gi(x, y)
|
157
|
+
end
|
158
|
+
|
159
|
+
#if all values are nodata, then it's not a local fluke/void, let's return nodata value
|
160
|
+
#otherwise, not all cells are nodata, which is a local void and probably sea level (passing over bridge)
|
161
|
+
return -9999 if(a1 < -420 && a1 == a2 && a1 == a3 && a1 == a4)
|
162
|
+
a1 = 0 if a1 < -420
|
163
|
+
a2 = 0 if a2 < -420
|
164
|
+
a3 = 0 if a3 < -420
|
165
|
+
a4 = 0 if a4 < -420
|
166
|
+
|
167
|
+
res = a1 + (a2 - a1)*x_index + (a3 - a1)*y_index + (a1 - a2 - a3 + a4)*x_index*y_index
|
168
|
+
(10 * res).round / 10.0
|
169
|
+
end
|
170
|
+
|
171
|
+
def self.cubic(hdr, x, y, bytes, pack)
|
172
|
+
ty = hdr[:min_y] + hdr[:cellsize] * (hdr[:nrows] - 1)
|
173
|
+
x_offset_abs = (x - hdr[:min_x]) / hdr[:cellsize]
|
174
|
+
y_offset_abs = (ty - y) / hdr[:cellsize]
|
175
|
+
x_offset = x_offset_abs.floor
|
176
|
+
y_offset = y_offset_abs.floor
|
177
|
+
x = x_offset_abs - x_offset
|
178
|
+
y = y_offset_abs - y_offset
|
179
|
+
num_bytes_to_read = bytes*4
|
180
|
+
|
181
|
+
byte_offset0 = bytes * (x_offset - 1 + (y_offset-1) * hdr[:ncols])
|
182
|
+
byte_offset1 = bytes * (x_offset - 1 + (y_offset ) * hdr[:ncols])
|
183
|
+
byte_offset2 = bytes * (x_offset - 1 + (y_offset+1) * hdr[:ncols])
|
184
|
+
byte_offset3 = bytes * (x_offset - 1 + (y_offset+2) * hdr[:ncols])
|
185
|
+
|
186
|
+
#we are performing 4 interpolations in the x direction, to get the four elevation points
|
187
|
+
#that will makeup a column of interpolated elevations. This column is then used to
|
188
|
+
#do one more interpolation to get the elevation point in center of grid.
|
189
|
+
begin
|
190
|
+
row0 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset0).unpack(pack).map { |p| p < -420 ? 0 : p }
|
191
|
+
row1 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset1).unpack(pack).map { |p| p < -420 ? 0 : p }
|
192
|
+
row2 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset2).unpack(pack).map { |p| p < -420 ? 0 : p }
|
193
|
+
row3 = IO.read(hdr[:data_file], num_bytes_to_read, byte_offset3).unpack(pack).map { |p| p < -420 ? 0 : p }
|
194
|
+
rescue
|
195
|
+
#probably wrapped off edge of file
|
196
|
+
return self.nn_srtm(x, y)
|
197
|
+
end
|
198
|
+
|
199
|
+
i0 = cubic_interpolate(row0, x)
|
200
|
+
i1 = cubic_interpolate(row1, x)
|
201
|
+
i2 = cubic_interpolate(row2, x)
|
202
|
+
i3 = cubic_interpolate(row3, x)
|
203
|
+
|
204
|
+
#now we have a column of interpolated elevations, we can do one more cubic interpolation
|
205
|
+
#and get a final elevation value.
|
206
|
+
#puts row3.inspect
|
207
|
+
#puts row2.inspect
|
208
|
+
#puts row1.inspect
|
209
|
+
#puts row0.inspect
|
210
|
+
(10 * self.cubic_interpolate([i0, i1, i2, i3], y)).round / 10.0
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
|
219
|
+
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
|
225
|
+
|
226
|
+
|
227
|
+
def self.usgs_ele(x,y)
|
228
|
+
begin
|
229
|
+
xml = open("http://gisdata.usgs.gov/XMLWebServices/TNM_Elevation_Service.asmx/getElevation?X_Value=#{x}&Y_Value=#{y}&Elevation_Units=METERS&Source_Layer=&Elevation_Only=true").read
|
230
|
+
rescue Timeout::Error
|
231
|
+
return false
|
232
|
+
end
|
233
|
+
|
234
|
+
doc = REXML::Document.new(xml)
|
235
|
+
#ele = (10.0 * doc.elements['double'].text.to_f).round / 10.0
|
236
|
+
ele = doc.elements['double'].text.to_f
|
237
|
+
if ele < -1000
|
238
|
+
#usgs retrns a very large negative, -1.79e308 or so when can't find a value, and
|
239
|
+
#since the lowest place on earth is -422 meters (dead sea), checking < -1000 works
|
240
|
+
return -9999
|
241
|
+
end
|
242
|
+
return (10.0 * ele).round / 10.0 #geto round!
|
243
|
+
end
|
244
|
+
|
245
|
+
def self.geonames_ele(x, y)
|
246
|
+
return self.geonames_eles([{'x' => x, 'y' => y}])[0]
|
247
|
+
end
|
248
|
+
|
249
|
+
def self.geonames_eles(points)
|
250
|
+
eles = []
|
251
|
+
points.each_slice(20) do |pts|
|
252
|
+
lats = []
|
253
|
+
lngs = []
|
254
|
+
pts.each { |p| lats << p['y']; lngs << p['x'] }
|
255
|
+
|
256
|
+
res = open("http://ws.geonames.org/srtm3?lats=#{lats.join(',')}&lngs=#{lngs.join(',')}")
|
257
|
+
eles += res.read.split("\n").map do |e|
|
258
|
+
parsed = e.strip.to_i
|
259
|
+
parsed < -400 ? -9999 : parsed
|
260
|
+
end
|
261
|
+
sleep(1)
|
262
|
+
end
|
263
|
+
return eles
|
264
|
+
end
|
265
|
+
|
266
|
+
def self.get_grid(ll_x, ll_y, ur_x, ur_y, num_x, num_y)
|
267
|
+
x_spacing = (ur_x - ll_x) / num_x
|
268
|
+
y_spacing = (ur_y - ll_y) / num_y
|
269
|
+
grid = {:vertices => []}
|
270
|
+
max_ele = -50000;
|
271
|
+
min_ele = 50000;
|
272
|
+
|
273
|
+
(num_x + 1).times do |x|
|
274
|
+
lng = ll_x + x_spacing*x
|
275
|
+
column = []
|
276
|
+
(num_y + 1).times do |y|
|
277
|
+
lat = ll_y + y_spacing*y
|
278
|
+
ele = get_ele(lng, lat)
|
279
|
+
|
280
|
+
max_ele = ele if ele > max_ele
|
281
|
+
min_ele = ele if ele < min_ele
|
282
|
+
|
283
|
+
column << {:lng => lng, :lat => lat, :ele => ele}
|
284
|
+
end
|
285
|
+
grid[:vertices] << column
|
286
|
+
end
|
287
|
+
|
288
|
+
return grid.merge!({:min_ele => min_ele, :max_ele => max_ele})
|
289
|
+
end
|
290
|
+
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
def self.cubic_interpolate(row, x)
|
295
|
+
a0 = row[3] - row[2] - row[0] + row[1]
|
296
|
+
a1 = row[0] - row[1] - a0
|
297
|
+
a2 = row[2] - row[0]
|
298
|
+
|
299
|
+
(a0*x*x*x) + (a1*x*x) + a2*x + row[1]
|
300
|
+
end
|
301
|
+
|
302
|
+
def self.get_file_names(source, x, y)
|
303
|
+
case source.to_sym
|
304
|
+
when :aster
|
305
|
+
#each file is a 1x1 degree chunk, named ASTGTM_[N|S][lat.floor][E|W][lng.floor].flt
|
306
|
+
n = y >= 0 ? "N" : "S"
|
307
|
+
e = x >= 0 ? "E" : "W"
|
308
|
+
#ok, so it took me a while to figure this out, but these aren't too magic
|
309
|
+
#of numbers...each tile doesn't start on a clean number. The min-y of
|
310
|
+
#a tile may be 46.999861. So we have to offset our number by the remainder.
|
311
|
+
x_offset = (x-0.000139).floor.abs
|
312
|
+
y_offset = (y-0.000139).floor.abs
|
313
|
+
fn_base = "#{ASTER_PATH}ASTGTM_#{n}%02d#{e}%03d_dem" % [y_offset, x_offset]
|
314
|
+
when :gi
|
315
|
+
#each file is a 1x1 degree chunk, named n[lat.floor]e[lng.floor.flt
|
316
|
+
x_offset = (x+0.00005).floor.abs
|
317
|
+
y_offset = (y+0.000075).floor.abs
|
318
|
+
fn_base = "#{GI_PATH}n#{y_offset}e0%02d" % x_offset
|
319
|
+
return fn_base + '.hdr', fn_base + '.flt'
|
320
|
+
when :srtm
|
321
|
+
#these aren't that magical, we have a 5 degree tile system so we have to
|
322
|
+
#do a little math to make sure we chunk by degrees of five. The magic looking
|
323
|
+
#offset numbers are because each tile does not start/end on a clean decimal
|
324
|
+
#boundary, so we have to offset by the amount of overlap between degrees.
|
325
|
+
filex = ((x + 179.999584) / 5.0).ceil
|
326
|
+
filey = 24 - ((y + 60.0004168) / 5.0).floor
|
327
|
+
fn_base = "#{SRTM_PATH}srtm_%.2d_%.2d" % [filex, filey]
|
328
|
+
when :ned
|
329
|
+
return nil if x > 0 || y < 0 #we only have a certain range in the NED
|
330
|
+
x_offset = (x+0.000555555).abs.ceil
|
331
|
+
y_offset = (y+0.000555555).abs.ceil
|
332
|
+
fn_base = NED_PATH + "dem#{x_offset}#{y_offset}"
|
333
|
+
#key_x = (x.to_i - 1).abs.to_s #file name doesn't have a negative
|
334
|
+
#key_y = (y.to_i + 1).to_s
|
335
|
+
#the file format is dem[lng.floor.abs][lat.floor].hdr
|
336
|
+
#fn_base = NED_PATH + "dem#{key_x + key_y}"
|
337
|
+
else
|
338
|
+
return nil
|
339
|
+
end
|
340
|
+
return fn_base + '.hdr', fn_base
|
341
|
+
end
|
342
|
+
end
|
metadata
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: humps
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Cullen King
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-27 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Fast and simple lookups for elevation data in GridFloat DEM files.
|
15
|
+
email:
|
16
|
+
- cullen@ridewithgps.com
|
17
|
+
executables: []
|
18
|
+
extensions: []
|
19
|
+
extra_rdoc_files: []
|
20
|
+
files:
|
21
|
+
- lib/humps.rb
|
22
|
+
- lib/humps/version.rb
|
23
|
+
homepage: http://cullenking.com
|
24
|
+
licenses: []
|
25
|
+
post_install_message:
|
26
|
+
rdoc_options: []
|
27
|
+
require_paths:
|
28
|
+
- lib
|
29
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
30
|
+
none: false
|
31
|
+
requirements:
|
32
|
+
- - ! '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
36
|
+
none: false
|
37
|
+
requirements:
|
38
|
+
- - ! '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
requirements: []
|
42
|
+
rubyforge_project: humps
|
43
|
+
rubygems_version: 1.8.10
|
44
|
+
signing_key:
|
45
|
+
specification_version: 3
|
46
|
+
summary: Library for extracting elevations from GridFloat DEMs
|
47
|
+
test_files: []
|