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.
Files changed (3) hide show
  1. data/lib/humps.rb +342 -0
  2. data/lib/humps/version.rb +3 -0
  3. metadata +47 -0
@@ -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
@@ -0,0 +1,3 @@
1
+ module Humps
2
+ VERSION = "0.0.1"
3
+ 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: []