#!/usr/bin/ruby require 'rubygems' require 'sqlite3' DB='/usr/local/share/wmo9/wmo9.sqlite' class Time def rfc1123 utc.strftime('%a, %d %b %Y %H:%M:%S GMT') end end class App def initialize @path = ENV['PATH_INFO'] @status = @title = @result = nil @geohack = nil @lastmod = File.stat(DB).mtime.rfc1123 @obuf = [] end def dir_root @result = [ {:link => 'vola/', :name => 'Vol.A', :desc => 'Observing Stations'}, {:link => 'volc1/', :name => 'Vol.C1', :desc => 'Bulletins'} ] end def dir_vola @result = [ {:link => 'block/', :desc => 'top two digits of station identifier'}, {:link => 'cntid/', :desc => 'country code'} ] end def query *sql, &hdl db = SQLite3::Database.new(DB) r = db.execute(*sql) db.close @result = r.map(&hdl) end def dir_vola_block sql = <<-SQL SELECT CAST(idxnum/1000 AS INTEGER) AS b, count(*) FROM vola GROUP BY b ORDER BY b ASC SQL query(sql) {|b, n| {:link => "#{'%02u' % b.to_i}/", :desc => "(#{n} entries)" } } end def dir_vola_block_n(b) i1 = b.to_i * 1000 i2 = b.to_i * 1000 + 999 sql = <<-SQL SELECT idxnum, idxsub, stnname, cntname FROM vola WHERE idxnum BETWEEN ? AND ? ORDER BY idxnum ASC SQL query(sql, i1, i2) {|idxnum, idxsub, stnname, cntname| i = format('%05u-%01u', idxnum, idxsub).sub(/-0/, '') {:link => "../../idxnum/#{i}", :name => i, :desc => [stnname, cntname].compact.join(', ') } } end def dir_vola_cntid sql = <<-SQL SELECT cntid, cntname FROM vola GROUP BY cntid ORDER BY cntname ASC SQL query(sql) {|cntid, cntname| {:link => "#{cntid.to_i}/", :desc => cntname } } end def dir_vola_cntid_n(n) cntid = n.to_i sql = <<-SQL SELECT idxnum, idxsub, stnname, cntname FROM vola WHERE cntid = ? ORDER BY idxnum ASC SQL query(sql, cntid) {|idxnum, idxsub, stnname, cntname| i = format('%05u-%01u', idxnum, idxsub).sub(/-0/, '') {:link => "../../idxnum/#{i}", :name => i, :desc => [stnname, cntname].compact.join(', ') } } end def page_vola_idxnum(n, s) n = n.to_i s = s.to_s.sub(/\A-/, '').to_i sql = <<-SQL SELECT * FROM vola WHERE idxnum = ? AND idxsub = ? LIMIT 1 SQL db = SQLite3::Database.new(DB) cols, *rows = db.execute2(sql, n, s) db.close #STDERR.puts [cols, rows].inspect row = rows.first if row @result = (0...(cols.size)).map{|i| { :head => cols[i], :body => row[i] } } @geohack = [row[cols.index('lat')], row[cols.index('lon')]] else @result = ["Station #{n} not found."] end nm = format('%05u', n) @result.push({ :head => 'Link', :body => [{ :link => "../../volc1/content/#{nm}/", :name => "Vol. C1 entries having #{nm} in content" }]}) end def dir_volc1 @result = [ {:link => 'cccc/', :name => 'CCCC', :desc => 'originating centres'}, {:link => 't1/', :name => 'T1', :desc => 'data types'}, {:link => 'tt/', :name => 'T1T2', :desc => 'data types (detail)'}, {:link => 'area/', :name => 'Area', :desc => 'A1 (GRIB), A2 (BUFR), or A1A2'} ] end def dir_volc1_t1 sql = <<-SQL SELECT t1, text, n FROM ( SELECT SUBSTR(ttaaii,1,1) AS t1, COUNT(*) AS n FROM volc1 GROUP BY t1 ) NATURAL LEFT JOIN datatype WHERE t2 = '?' GROUP BY t1 ORDER BY t1 ASC SQL query(sql) {|tt, text, count| link = "../tt/#{tt.gsub(/\W/, '_')}" {:link => link, :name => "#{tt} - #{text}", :desc => "(#{count} records)" } } end def dir_volc1_tt(t1 = nil) sql = <<-SQL SELECT t1 || t2, text, n FROM ( SELECT SUBSTR(ttaaii,1,1) AS t1, SUBSTR(ttaaii,2,1) AS t2, COUNT(*) AS n FROM volc1 GROUP BY t1, t2 ) NATURAL LEFT JOIN datatype @WHERE@ GROUP BY t1, t2 ORDER BY t1 ASC, t2 ASC SQL if t1 then sql.sub!(/@WHERE@/, 'WHERE t1 = :t1') bind = {:t1 => t1} else sql.sub!(/@WHERE@/, '') bind = {} end query(sql, bind) {|tt, text, count| link = tt.gsub(/\W/, '_') + '/' {:link => link, :desc => "#{text} (#{count} records)" } } end def dir_volc1_tt_n(tt) return boo("501 bad TT") unless /\A[A-Z]{2}\z/ === tt sql = <<-SQL SELECT SUBSTR(ttaaii, 1, 4) AS ttaa, COUNT(*) FROM volc1 WHERE ttaaii GLOB ? GROUP BY ttaa ORDER BY ttaa ASC SQL query(sql, "#{tt}*") {|ttaa, count| link = "../../ttaa/#{ttaa}/" {:link => link, :name => ttaa, :desc => "#{count} records" } } end def dir_volc1_ttaa_n(ttaa) sql = <<-SQL SELECT ttaaii, cccc FROM volc1 WHERE ttaaii GLOB ? ORDER BY ttaaii ASC SQL query(sql, "#{ttaa}*") {|ttaaii, cccc| ahl = format('%6.6s%4.4s', ttaaii, cccc).gsub(/ /, '_') { :link => "../../ahl/#{ahl}", :name => ahl } } end def page_volc1_ahl(ttaaii, cccc) n = n.to_i s = s.to_s.sub(/\A-/, '').to_i sql = <<-SQL SELECT * FROM volc1 WHERE ttaaii = ? AND cccc = ? LIMIT 1 SQL db = SQLite3::Database.new(DB) cols, *rows = db.execute2(sql, ttaaii, cccc) db.close row = rows.first if row @result = (0...(cols.size)).map{|i| if cols[i] == 'content' and /\A[ \w]+\z/ === row[i] then content = row[i].strip.split(/\s+/).map {|stn| case stn when /\A\d{5}\z/ then { :link => "../../vola/idxnum/#{stn}", :name => stn } when /\A[A-Z]{4}\z/ then { :link => "../../locid/locid/#{stn}", :name => stn } else stn end } { :head => cols[i], :body => content } elsif cols[i] == 'date' and row[i] then t = Time.at((row[i].to_f - 2440587.5) * 86400).rfc1123 { :head => cols[i], :body => t } else { :head => cols[i], :body => row[i] } end } else @result = ["Bulletin #{ttaaii} #{cccc} not found"] end if /\A\w+\z/ === ttaaii and /\A\w+\z/ === cccc then @result.push({ :head => 'Link', :body => [{ :link => "http://www.wis-jma.go.jp/meta/sru.jsp?version=1.1&query=#{ttaaii}+and+#{cccc}&operation=searchRetrieve", :name => "WIS Metadata for #{ttaaii} #{cccc}" }] }) end end def dir_volc1_area(aaglob = nil) if aaglob then aa = case aaglob when /\A_[A-Z]\z/ then aaglob.sub(/_/, "[IJK]??").sub(/\z/, '*') when /\A[A-Z]_\z/ then aaglob.sub(/\A/, "[DGHOXY]").sub(/_/, '*') else "[ABCEFLMNPQRSTUVWZ]?#{aaglob}*" end sql = <<-SQL SELECT SUBSTR(ttaaii, 1, 4) AS ttaa, COUNT(*) FROM volc1 WHERE ttaa GLOB ? GROUP BY ttaa SQL query(sql, aa) {|ttaa, count| {:link => "../ttaa/#{ttaa}/", :name => ttaa, :desc => "(#{count} records)" } } else sql = <<-SQL SELECT DISTINCT a1||a2, text FROM place SQL query(sql) {|aa, text| link = aa.gsub(/\W/, '_') {:link => link, :name => aa, :desc => text } } end end def dir_volc1_cccc sql = <<-SQL SELECT cccc, country, COUNT(*) FROM volc1 GROUP BY cccc ORDER BY cccc ASC SQL query(sql) {|cccc, cntname, count| {:link => "#{cccc}/", :name => cccc, :desc => "#{cntname} (#{count} records)" } } end def dir_volc1_cccc_n(cccc) return boo("501 bad CCCC") unless /\A[A-Z][A-Z0-9]{3}\z/ === cccc sql = <<-SQL SELECT SUBSTR(ttaaii, 1, 2) AS tt, COUNT(*) FROM volc1 WHERE cccc = ? GROUP BY tt ORDER BY tt ASC SQL query(sql, cccc) {|tt, count| { :link => "#{tt}/", :name => tt, :desc => "(#{count} records)" } } end def dir_volc1_cccc_tt_n(cccc, tt) return boo("501 bad CCCC") unless /\A[A-Z][A-Z0-9]{3}\z/ === cccc return boo("501 bad TT") unless /\A[A-Z]{2}\z/ === tt sql = <<-SQL SELECT ttaaii FROM volc1 WHERE cccc = ? AND ttaaii like ? GROUP BY ttaaii ORDER BY ttaaii ASC SQL query(sql, cccc, "#{tt}%") {|ttaaii| ahl = format('%6.6s%4.4s', ttaaii, cccc).gsub(/ /, '_') { :link => "../../../ahl/#{ahl}", :name => "#{ttaaii} #{cccc}" } } end def dir_volc1_content_n(content) sql = <<-SQL SELECT ttaaii, cccc FROM volc1 WHERE content LIKE ? ORDER BY cccc ASC, ttaaii ASC SQL query(sql, "%#{content}%") {|ttaaii, cccc| ahl = format('%6.6s%4.4s', ttaaii, cccc).gsub(/ /, '_') { :link => "../../ahl/#{ahl}", :name => "#{ttaaii} #{cccc}" } } end def page_bbox(s) begin @geohack = s.split(/,/).map{|f| Float(f)} rescue ArgumentError return boo("501 malformatted numbers") end @title = "box #{s}" @result = s end def page_locid(n) sql = <<-SQL SELECT * FROM cccc WHERE icaoid = ? LIMIT 1 SQL db = SQLite3::Database.new(DB) cols, *rows = db.execute2(sql, n) db.close row = rows.first #STDERR.puts [cols, row].inspect if row then @result = (0...(cols.size)).map{|i| { :head => cols[i], :body => row[i] } } @geohack = [row[cols.index('lat')], row[cols.index('lon')]] else @result = "Station #{n} not found" end end def dispatch case @path when /\A\/vola/ then @title = 'WMO Pub. No.9 Vol.A' when /\A\/volc1/ then @title = 'WMO Pub. No.9 Vol.C1' when /\A\/locid/ then @title = 'non-WMO sources compiled' else @title = 'WMO Pub. No.9' end case @path when /\A\/\z/ then dir_root when /\A\/vola\/\z/ then dir_vola when /\A\/vola\/idxnum\/(\d+)(-\d)?\z/ then page_vola_idxnum($1, $2) when /\A\/vola\/block\/\z/ then dir_vola_block when /\A\/vola\/block\/(\d+)\/\z/ then dir_vola_block_n($1) when /\A\/vola\/cntid\/\z/ then dir_vola_cntid when /\A\/vola\/cntid\/(\d+)\/\z/ then dir_vola_cntid_n($1) when /\A\/volc1\/\z/ then dir_volc1 when /\A\/volc1\/ahl\/([A-Z]{4}\d*)([A-Z]{4})\z/ then page_volc1_ahl($1, $2) when /\A\/volc1\/t1\/\z/ then dir_volc1_t1 when /\A\/volc1\/tt\/([A-Z])?\z/ then dir_volc1_tt($1) when /\A\/volc1\/tt\/([A-Z]{2})\/\z/ then dir_volc1_tt_n($1) when /\A\/volc1\/ttaa\/([A-Z]{4})\/\z/ then dir_volc1_ttaa_n($1) when /\A\/volc1\/area\/([A-Z_]{2})?\z/ then dir_volc1_area($1) when /\A\/volc1\/cccc\/\z/ then dir_volc1_cccc when /\A\/volc1\/cccc\/([A-Z][A-Z0-9]{3})\/\z/ then dir_volc1_cccc_n($1) when /\A\/volc1\/cccc\/([A-Z][A-Z0-9]{3})\/([A-Z]{2})\/\z/ then dir_volc1_cccc_tt_n($1, $2) when /\A\/volc1\/content\/(\w+)\// then dir_volc1_content_n($1) when /\A\/locid\/locid\/([A-Z0-9]{4})\z/ then page_locid($1) when /\A\/bbox\/([-+.0-9,]+)\z/ then page_bbox($1) else boo('404 Not found - path') end end def visualize obj return if @status if Array === obj puts "" elsif obj.is_a?(Hash) and link = obj[:link] name = obj[:name] || obj[:link] puts "#{name}" puts " #{obj[:desc]}" if obj[:desc] elsif obj.is_a?(Hash) and head = obj[:head] puts "#{head}: " visualize(obj[:body]) puts "" elsif obj.is_a?(String) or obj.is_a?(Integer) puts obj elsif obj.is_a?(Float) puts format('%+7.4f', obj) elsif obj.nil? puts "missing" else raise "unkown object #{obj.class.to_s}" end end def report begin visualize(@result) rescue Exception => e boo('501 Internal Server Error', e.message + "\n" + e.backtrace.join("\t\n")) end flush end def puts str @obuf.push str.to_s end def boo status, msg = nil @status = status puts "
"
    puts(msg || status)
    puts "
" end def geohack part unless @geohack return case part when 1 then "" when 2 then "" end end case part when 1 then if @geohack.size == 4 then lat1, lat2, lon1, lon2 = @geohack[0,4].map{|s| s.to_f} lonc = 0.5 * (lon1 + lon2) latc = 0.5 * (lat1 + lat2) < SCRIPT else lat = @geohack[0].to_f lon = @geohack[1].to_f < SCRIPT end when 2 then <