=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