=begin = その日の天気プラグイン / Weather-of-today plugin((-$Id$-)) Records the weather when the diary is first updated for the date and displays it. その日の天気を、その日の日記を最初に更新する時に取得して保存し、それぞれ の日の日記の上部に表示します。 == Acknowledgements その日の天気プラグインのアイディアを提供してくださったhsbtさん、実装のヒ ントを提供してくださったzoeさんに感謝します。また、NOAAの情報を提供して くださったkotakさんに感謝します。 The author appreciates National Weather Service (()) making such valuable data available in public domain as described in (()). == Copyright Copyright 2003 zunda Permission is granted for use, copying, modification, distribution, and distribution of modified versions of this work under the terms of GPL version 2 or later. =end =begin ChangeLog * Mon Sep 29, 2003 zunda - Japanese resources divided into a separate file, English resource created * Thu Jul 24, 2003 zunda - Syntax error in drizzle fixed * Mon Jul 21, 2003 zunda - changed regexp literals from %r|..| to %r[..] for Ruby 1.8.x. * Fri Jul 17, 2003 zunda - WWW configuration interface * Thu Jun 5, 2003 zunda - checks the age of data * Tue Jun 3, 2003 zunda - ignores `... in the vicinity', thank you kosaka-san. - now tests translations if executed as a stand alone script. * Mon May 26, 2003 zunda - fix typo on weaHTer.show_mobile and weHTer.show_error, thank you halchan. * Thu May 8, 2003 zunda - A with B, observed, * Mon May 5, 2003 zunda - mobile agent * Fri Mar 28, 2003 zunda - overcast, Thanks kotak san. * Fri Mar 21, 2003 zunda - mist: kiri -> kasumi, Thanks kotak san. * Sun Mar 16, 2003 zunda - option weather.tz, appropriate handling of timezone * Tue Mar 11, 2003 zunda - records: windchill, winddir with 'direction variable', gusting wind * Mon Mar 10, 2003 zunda - WeatherTranslator module * Sat Mar 8, 2003 zunda - values with units * Fri Mar 7, 2003 zunda - edited to work with NOAA/NWS * Fri Feb 28, 2003 zunda - first draft =end require 'net/http' Net::HTTP.version_1_1 require 'nkf' require 'cgi' require 'timeout' =begin == Classes and methods === WeatherTranslator module We want Japanese displayed in a diary written in Japanese. --- WeatherTranslator::S < String Extension of String class. It translates itself. --- WeatherTranslator::S.translate( table ) Translates self according to ((|table|)). =end module WeatherTranslator class S < String def translate( table ) return '' if not self or self.empty? table.each do |x| if x[0] =~ self then return S.new( S.new( $` ).translate( table ) + eval( x[1] ) + S.new( $' ).translate( table ) ) end end self end def compact S.new( self.split( /\/+/ ).uniq.join( '/' ) ) end end end =begin === Weather class Weather of a date. --- Weather( date ) A Weather is a weather datum for a ((|date|)) (a Time object). --- Weather.get( url, header, items ) Gets a WWW page from the ((|url|)) providing HTTP header in the ((|header|)) hash. The page is parsed calling Weahter.parse_html. Returns self. --- Weather.parse_html( html, items ) Parses an HTML page ((|html|)) and stores the data into @data according to ((|items|)). --- Weather.to_s Creates a line to be stored into the cache file which will be parsed with Weather.parse method. Data are stored with the following sequence and separated with a tab: date(string), url, acquisition time(UNIX time) timezone, error (or empty string), item, value, ... Each record is terminated with a new line. --- Weather.parse( string ) --- Weather::parse( string ) Parses the ((|string|)) made by Weather.to_s and returns the resulting Weather. --- Weather::date_to_s( date ) Returns ((|date|)) formatted as a String used in to_s method. Used to find a record for the date from a file. --- Weather.to_html( show_error = false ) Returns an HTML fragment for the weather. When show_error is true, returns an error message as an HTML fragment in case an error occured when getting the weather. --- Weather.to_i_html Returns a CHTML fragment for the weather. =end class Weather attr_reader :date, :time, :url, :error, :data, :tz # magic numbers HTML_START = '
' HTML_END = '
' I_HTML_START = '

' I_HTML_END = '

' WAITTIME = 10 MAXREDIRECT = 10 # edit this method according to the HTML we will get def parse_html( html, items ) htmlitems = Hash.new # weather data is in the 4th table in the HTML from weather.noaa.gov table = html.scan( %r[(.*?)]mi )[3][0] table.scan( %r[(.*?)]mi ).collect {|a| a[0]}.each do |row| # *item* -> downcased *value* if %r[(.*?)\s*(.*?)]mi =~ row then item = $1 value = $2 item = item.gsub( /
/i, '/' ).gsub( /<.*?>/m , '').strip.downcase value = value.gsub( /
/i, '/' ).gsub( /<.*?>/m , '').strip # unit conversion settings units = [] case item when 'conditions at' # we have to convert the UTC time into UNIX time if /(\d{4}).(\d\d).(\d\d)\s*(\d\d)(\d\d)\s*UTC$/ =~ value then value = Time::utc( $1, $2, $3, $4, $5 ).to_i.to_s else raise StandardError, 'Parse error in "Conditions at"' end when 'visibility' # we want to preserve adjective phrase if possible if /(.*)([\d.]+)\s*mile(\(s\))?/i =~ value then htmlitems["#{item}(km)"] = sprintf( '%s %.3f', $1.strip, $2.to_f * 1.610 ) htmlitems["#{item}(mile)"] = sprintf( '%s %s', $1.strip, $2 ) end when 'wind' # we want to preserve adjective phrase if possible speed = value.scan( /([\d.]+)\s*MPH/i ).collect { |x| x[0] } htmlitems["#{item}(MPH)"] = speed.join(',') htmlitems["#{item}(m/s)"] = speed.collect {|s| sprintf( '%.4f', s.to_f * 0.4472222 ) }.join(',') if /([\d.]+)\s*degrees?/i =~ value then htmlitems["#{item}(deg)"] = $1 end if /from\s+(the\s+)?(\w+)/i =~ value then htmlitems["#{item}dir"] = $2 + ($3 ? " #{$3}" : '') end if /(\(direction variable\))/i =~ value then htmlitems["#{item}dir"] << " #{$1}" end # just have to parse the value with the units when 'temperature' units = ['C', 'F'] when 'windchill' units = ['C', 'F'] when 'dew point' units = ['C', 'F'] when 'relative humidity' units = ['%'] when 'pressure (altimeter)' units = ['hPa'] end # parse the value with the units if preferred and possible units.each do |unit| if /(-?[\d.]+)\s*\(?#{unit}\b/i =~ value then htmlitems["#{item}(#{unit})"] = $1 end end # record the value as read from the HTML htmlitems[item] = value end # if %r[(.*?)\s*(.*?)]mi =~ row end # table.scan( %r[(.*?)]mi ) ... do |row| # translate the parsed HTML into the Weather hash with more generic key items.each do |from, to| if htmlitems[from] then # as specified in items @data[to] = htmlitems[from] elsif f = from.dup.sub!( /\([^)]+\)$/, '' ) \ and htmlitems[f] \ and t = to.dup.sub!( /\([^)]+\)$/, '' ) then # remove the units and try again if not found @data[t] = htmlitems[f] end end @time = Time::now end # check age of data def check_age( oldest_sec = nil ) if oldest_sec and @time and @data['timestamp'] and @data['timestamp'].to_i + oldest_sec < @time.to_i then @error = 'data too old' end end def initialize( date = nil, tz = nil ) @date = date or Time.now @data = Hash.new @error = nil @url = nil if tz and not tz.empty? then @tz = tz elsif ENV['TZ'] @tz = ENV['TZ'] else @tz = nil end end def get( url, header = nil, items = {} ) @url = url.gsub(/[\t\n]/, '') @error = nil @url =~ %r host = $1 path = $2 redirect = 0 begin timeout( WAITTIME ) do begin d = '' Net::HTTP.start( host, 80 ) do |http| response , = http.get( path, header) d = NKF::nkf( '-e', response.body ) end parse_html( d, items ) rescue Net::ProtoRetriableError => err if m = %r.match( err.response['location'] ) then host = m[1].strip path = m.post_match redirect += 1 retry if redirect < MAXREDIRECT raise StandardError, 'Too many redirections' end raise StandardError, 'Error in redirection' end end rescue TimeoutError @error = 'Timeout' rescue @error = NKF::nkf( '-e', $!.message.gsub( /[\t\n]/, ' ' ) ) end self end def to_s tzstr = @tz ? " #{tz}" : '' r = "#{Weather::date_to_s( @date )}\t#{@url}\t#{@time.to_i}#{tzstr}\t#{@error}" @data.each do |item, value| r << "\t#{item}\t#{value}" if value and not value.empty? end r << "\n" end def parse( string ) i = string.chomp.split( /\t/ ) y, m, d = i.shift.scan( /^(\d{4})(\d\d)(\d\d)$/ )[0] @date = Time::local( y, m, d ) @url = i.shift itime, @tz = i.shift.split( / +/, 2 ) @time = Time::at( itime.to_i ) error = i.shift if error and not error.empty? then @error = error else @error = nil end @data.clear while not i.empty? do @data[i.shift] = i.shift end self end def to_html( show_error = false ) @error ? (show_error ? error_html_string : '') : html_string end def to_i_html @error ? '' : i_html_string end def store( path, date ) ddir = File.dirname( Weather::file_path( path, date ) ) # mkdir_p logic copied from fileutils.rb # Copyright (c) 2000,2001 Minero Aoki # and edited (zunda.freeshell.org does not have fileutils.rb T_T dirstack = [] until FileTest.directory?( ddir ) do dirstack.push( ddir ) ddir = File.dirname( ddir ) end dirstack.reverse_each do |dir| Dir.mkdir dir end # finally we can write a file File::open( Weather::file_path( path, date ), 'a' ) do |fh| fh.puts( to_s ) end end class << self def parse( string ) new.parse( string ) end def date_to_s( date ) date.strftime( '%Y%m%d' ) end def file_path( path, date ) date.strftime( "#{path}/%Y/%Y%m.weather" ).gsub( /\/\/+/, '/' ) end def restore( path, date ) r = nil datestring = Weather::date_to_s( date ) begin File::open( file_path( path, date ), 'r' ) do |fh| fh.each( "\n" ) do |l| if /^#{datestring}\t/ =~ l then r = l # will use the last/newest data found in the file end end end rescue Errno::ENOENT end r ? Weather::parse( r ) : nil end end end =begin === Methods as a plugin weather method also can be used as a usual plug-in in your diary body. Please note that the argument is not a String but a Time object. --- weather( date = nil ) Returns an HTML flagment of the weather for the date. This will be provoked as a body_enter_proc. @date is used when ((|date|)) is nil. --- get_weather Access the URL to get the current weather information when: * @mode is append or replace, * @date is today, and * There is no cached data without an error for today This will be provoked as an update_proc. =end Weather_default_path = "#{@cache_path}/weather" Weather_default_items = { # UNIX time 'conditions at' => 'timestamp', # English phrases 'sky conditions' => 'condition', 'weather' => 'weather', # Direction (e.g. SE) 'winddir' => 'winddir', # English phrases when unit conversion failed, otherwise, key with (unit) 'wind(m/s)' => 'wind(m/s)', 'wind(deg)' => 'wind(deg)', 'visibility(km)' => 'visibility(km)', 'temperature(C)' => 'temperature(C)', 'windchill(C)' => 'windchill(C)', 'dew point(C)' => 'dewpoint(C)', 'relative humidity(%)' => 'humidity(%)', 'pressure (altimeter)(hPa)' => 'pressure(hPa)', } # shows weather def weather( date = nil ) return '' if @conf.bot? and not @options['weather.show_robot'] path = @options['weather.dir'] || Weather_default_path w = Weather::restore( path, date || @date ) if w then unless @cgi.mobile_agent? then w.to_html( @options['weather.show_error'] ) else w.to_i_html if @options['weather.show_mobile'] end else '' end end # gets weather when the diary is updated def get_weather return unless @options['weather.url'] return unless @mode == 'append' or @mode == 'replace' return unless @date.strftime( '%Y%m%d' ) == Time::now.strftime( '%Y%m%d' ) path = @options['weather.dir'] || Weather_default_path w = Weather::restore( path, @date ) if not w or w.error then items = @options['weather.items'] || Weather_default_items w = Weather.new( @date, @options['weather.tz'] ) w.get( @options['weather.url'], @options['weather.header'], items ) if @options.has_key?( 'weather.oldest' ) then oldest = @options['weather.oldest'] else oldest = 21600 end w.check_age( oldest ) w.store( path, @date ) end end # www configuration interface def configure_weather if( @mode == 'saveconf' ) then # weather.url @conf['weather.url'] = @cgi.params['weather.url'][0] # weather.tz tz = @cgi.params['weather.tz'][0] unless tz.empty? then # need more checks @conf['weather.tz'] = tz else @conf['weather.tz'] = '' end # weather.show_mobile case @cgi.params['weather.show_mobile'][0] when 'true' @conf['weather.show_mobile'] = true when 'false' @conf['weather.show_mobile'] = false end # weather.show_robot case @cgi.params['weather.show_robot'][0] when 'true' @conf['weather.show_robot'] = true when 'false' @conf['weather.show_robot'] = false end end weather_configure_html( @conf ) end add_body_enter_proc do |date| weather( date ) end add_update_proc do get_weather end add_conf_proc( 'weather', @weather_plugin_name ) do configure_weather end