mirror of
https://github.com/fhem/fhem-mirror.git
synced 2025-01-31 18:59:33 +00:00
e01304c50c
This reverts commit 924268bc89116b47335274b20370af8669432829. git-svn-id: https://svn.fhem.de/fhem/trunk@7638 2b470e98-0d58-463d-a4d8-8e2adae1ed80
1082 lines
35 KiB
Perl
1082 lines
35 KiB
Perl
#
|
|
#
|
|
# 02_RSS.pm
|
|
# written by Dr. Boris Neubert 2012-03-24
|
|
# e-mail: omega at online dot de
|
|
#
|
|
##############################################
|
|
# $Id$
|
|
|
|
package main;
|
|
use strict;
|
|
use warnings;
|
|
use GD;
|
|
use feature qw/switch/;
|
|
use vars qw(%data);
|
|
use HttpUtils;
|
|
#require "98_SVG.pm"; # enable use of plotAsPng()
|
|
sub plotAsPng(@); # forward declaration will be enough
|
|
# to ensure correct function
|
|
# and will avoid reloading 98_SVG.pm
|
|
# during fhem startup/rereadcfg
|
|
|
|
my @cmd_halign= qw(halign thalign ihalign);
|
|
my @cmd_valign= qw(valign tvalign ivalign);
|
|
my @valid_valign = qw(top center base bottom);
|
|
my @valid_halign = qw(left center right justified);
|
|
|
|
# we can
|
|
# use vars qw(%FW_types); # device types,
|
|
# use vars qw($FW_RET); # Returned data (html)
|
|
# use vars qw($FW_wname); # Web instance
|
|
# use vars qw($FW_subdir); # Sub-path in URL for extensions, e.g. 95_FLOORPLAN
|
|
# use vars qw(%FW_pos); # scroll position
|
|
# use vars qw($FW_cname); # Current connection name
|
|
|
|
# http://blogs.perl.org/users/mike_b/2013/06/a-little-nicer-way-to-use-smartmatch-on-perl-518.html
|
|
no if $] >= 5.017011, warnings => 'experimental::smartmatch';
|
|
|
|
#########################
|
|
sub
|
|
RSS_addExtension($$$) {
|
|
my ($func,$link,$friendlyname)= @_;
|
|
|
|
my $url = "/" . $link;
|
|
$data{FWEXT}{$url}{FUNC} = $func;
|
|
$data{FWEXT}{$url}{LINK} = $link;
|
|
$data{FWEXT}{$url}{NAME} = $friendlyname;
|
|
$data{FWEXT}{$url}{FORKABLE} = 0;
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_Initialize($) {
|
|
my ($hash) = @_;
|
|
$hash->{DefFn} = "RSS_Define";
|
|
#$hash->{AttrFn} = "RSS_Attr";
|
|
$hash->{AttrList}= "size bg bgcolor tmin refresh areas";
|
|
$hash->{SetFn} = "RSS_Set";
|
|
|
|
RSS_addExtension("RSS_CGI","rss","RSS");
|
|
|
|
return undef;
|
|
}
|
|
|
|
|
|
##################
|
|
sub
|
|
RSS_readLayout($) {
|
|
|
|
my ($hash)= @_;
|
|
my $filename= $hash->{fhem}{filename};
|
|
my $name= $hash->{NAME};
|
|
|
|
my ($err, @layoutfile) = FileRead($filename);
|
|
if($err) {
|
|
Log 1, "RSS $name: $err";
|
|
$hash->{fhem}{layout}= ("text 0.1 0.1 'Error: $err'");
|
|
} else {
|
|
$hash->{fhem}{layout}= join("\n", @layoutfile);
|
|
$hash->{fhem}{layout} =~ s/\n\n/\n/g;
|
|
}
|
|
return;
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_Define($$) {
|
|
|
|
my ($hash, $def) = @_;
|
|
|
|
my @a = split("[ \t]+", $def);
|
|
|
|
return "Usage: define <name> RSS jpg|png hostname filename" if(int(@a) != 5);
|
|
my $name= $a[0];
|
|
my $style= $a[2];
|
|
my $hostname= $a[3];
|
|
my $filename= $a[4];
|
|
|
|
$hash->{fhem}{style}= $style;
|
|
$hash->{fhem}{hostname}= $hostname;
|
|
$hash->{fhem}{filename}= $filename;
|
|
$hash->{LAYOUTFILE} = $filename;
|
|
|
|
eval "use GD::Text::Align";
|
|
$hash->{fhem}{useTextAlign} = ($@ ? 0 : 1 );
|
|
if(!($hash->{fhem}{useTextAlign})) {
|
|
Log3 $hash, 2, "$name: Cannot use text alignment: $@";
|
|
}
|
|
|
|
eval "use GD::Text::Wrap";
|
|
$hash->{fhem}{useTextWrap} = ($@ ? 0 : 1 );
|
|
if(!($hash->{fhem}{useTextWrap})) {
|
|
Log3 $hash, 2, "$name: Cannot use text wrapping: $@";
|
|
}
|
|
|
|
RSS_readLayout($hash);
|
|
|
|
$hash->{STATE} = 'defined'; #$name;
|
|
return undef;
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_Set() {
|
|
|
|
my ($hash, @a) = @_;
|
|
my $name = $a[0];
|
|
|
|
# usage check
|
|
my $usage= "Unknown argument, choose one of rereadcfg:noArg";
|
|
if((@a == 2) && ($a[1] eq "rereadcfg")) {
|
|
RSS_readLayout($hash);
|
|
return undef;
|
|
} else {
|
|
return $usage;
|
|
}
|
|
}
|
|
|
|
####################
|
|
#
|
|
sub
|
|
RSS_getURL($) {
|
|
my ($hostname)= @_;
|
|
# http://hostname:8083/fhem
|
|
my $proto = (AttrVal($FW_wname, 'HTTPS', 0) == 1) ? 'https' : 'http';
|
|
return $proto."://$hostname:" . $defs{$FW_wname}{PORT} . $FW_ME;
|
|
}
|
|
|
|
# ##################
|
|
# sub
|
|
# RSS_Attr(@)
|
|
# {
|
|
# my @a = @_;
|
|
# my $attr= $a[2];
|
|
#
|
|
# if($a[0] eq "set") { # set attribute
|
|
# if($attr eq "bgdir") {
|
|
# }
|
|
# }
|
|
# elsif($a[0] eq "del") { # delete attribute
|
|
# if($attr eq "bgdir") {
|
|
# }
|
|
# }
|
|
#
|
|
# return undef;
|
|
#
|
|
# }
|
|
|
|
##################
|
|
# list all RSS devices
|
|
sub
|
|
RSS_Overview {
|
|
|
|
my ($name, $url);
|
|
my $html= RSS_HTMLHead("RSS Overview", undef) . "<body>\n";
|
|
foreach my $def (sort keys %defs) {
|
|
if($defs{$def}{TYPE} eq "RSS") {
|
|
$name= $defs{$def}{NAME};
|
|
$url= RSS_getURL($defs{$def}{fhem}{hostname});
|
|
$html.= "$name<br>\n<ul>";
|
|
$html.= "<a href='$url/rss/$name.rss'>RSS</a><br>\n";
|
|
$html.= "<a href='$url/rss/$name.html'>HTML</a><br>\n";
|
|
$html.= "<a href='$url/rss/$name.png'>Portable Network Graphics</a><br>\n";
|
|
$html.= "<a href='$url/rss/$name.jpg'>JPEG Graphics</a><br>\n";
|
|
$html.= "</ul>\n<p>\n";
|
|
}
|
|
}
|
|
$html.="</body>\n" . RSS_HTMLTail();
|
|
|
|
return ("text/html; charset=utf-8", $html);
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_splitRequest($) {
|
|
|
|
# http://hostname:8083/fhem/rss
|
|
# http://hostname:8083/fhem/rss/myDeviceName.rss
|
|
# http://hostname:8083/fhem/rss/myDeviceName.jpg
|
|
# |--------- url ----------| |---name --| ext
|
|
|
|
my ($request) = @_;
|
|
|
|
if($request =~ /^.*\/rss$/) {
|
|
# http://localhost:8083/fhem/rss
|
|
return (undef,undef); # name, ext
|
|
} else {
|
|
# http://hostname:8083/fhem/rss/myDeviceName.rss
|
|
# http://hostname:8083/fhem/rss/myDeviceName.jpg
|
|
# http://hostname:8083/fhem/rss/myDeviceName.png
|
|
# http://hostname:8083/fhem/rss/myDeviceName.html
|
|
my $call= $request;
|
|
$call =~ s/^.*\/rss\/([^\/]*)$/$1/;
|
|
my $name= $call;
|
|
$name =~ s/^(.*)\.(jpg|png|rss|html)$/$1/;
|
|
my $ext= $call;
|
|
$ext =~ s/^$name\.(.*)$/$1/;
|
|
return ($name,$ext);
|
|
}
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_returnRSS($) {
|
|
my ($name) = @_;
|
|
|
|
my $url= RSS_getURL($defs{$name}{fhem}{hostname});
|
|
my $type = $defs{$name}{fhem}{style};
|
|
my $mime = ($type eq 'png')? 'image/png' : 'image/jpeg';
|
|
my $now = time();
|
|
my $code = "<rss version='2.0' xmlns:media='http://search.yahoo.com/mrss/'><channel><title>$name</title><ttl>1</ttl><item><media:content url='$url/rss/$name.$type' type='$mime'/><guid isPermaLink='false'>item_$now</guid></item></channel></rss>";
|
|
|
|
return ("application/xml; charset=utf-8", $code);
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_getScript() {
|
|
|
|
my $scripts= "";
|
|
my $jsTemplate = '<script type="text/javascript" src="%s"></script>';
|
|
if(defined($data{FWEXT})) {
|
|
foreach my $k (sort keys %{$data{FWEXT}}) {
|
|
my $h = $data{FWEXT}{$k};
|
|
next if($h !~ m/HASH/ || !$h->{SCRIPT});
|
|
my $script = $h->{SCRIPT};
|
|
$script = ($script =~ m,^/,) ? "$FW_ME$script" : "$FW_ME/pgm2/$script";
|
|
$scripts .= sprintf($jsTemplate, $script) . "\n";
|
|
}
|
|
}
|
|
return $scripts;
|
|
}
|
|
|
|
sub
|
|
RSS_HTMLHead($$) {
|
|
my ($title,$refresh) = @_;
|
|
|
|
my $doctype= '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">';
|
|
my $xmlns= 'xmlns="http://www.w3.org/1999/xhtml"';
|
|
my $r= defined($refresh) ? "<meta http-equiv=\"refresh\" content=\"$refresh\"/>\n" : "";
|
|
# css and js header output should be coded only in one place
|
|
my $css= "";
|
|
#my $cssTemplate = "<link href=\"$FW_ME/%s\" rel=\"stylesheet\"/>\n";
|
|
#$css= sprintf($cssTemplate, "pgm2/style.css");
|
|
#$css.= sprintf($cssTemplate, "pgm2/jquery-ui.min.css");
|
|
#map { $css.= sprintf($cssTemplate, $_); } split(" ", AttrVal($FW_wname, "CssFiles", ""));
|
|
my $scripts= RSS_getScript();
|
|
my $code= "$doctype\n<html $xmlns>\n<head>\n<title>$title</title>\n$r$css$scripts</head>\n";
|
|
}
|
|
|
|
sub
|
|
RSS_HTMLTail() {
|
|
return "</html>";
|
|
}
|
|
|
|
sub
|
|
RSS_returnHTML($) {
|
|
my ($name) = @_;
|
|
|
|
my $url= RSS_getURL($defs{$name}{fhem}{hostname});
|
|
my $type = $defs{$name}{fhem}{style};
|
|
my $img= "$url/rss/$name.$type";
|
|
my $refresh= AttrVal($name, 'refresh', 60);
|
|
my $areas= AttrVal($name, 'areas', "");
|
|
my $code= RSS_HTMLHead($name, $refresh) .
|
|
"<body topmargin=\"0\" leftmargin=\"0\" margin=\"0\" padding=\"0\">\n<img src=\"$img\" usemap=\"#map\"/>\n<map name=\"map\" id=\"map\">\n$areas\n</map>\n</body>\n" .
|
|
RSS_HTMLTail();
|
|
return ("text/html; charset=utf-8", $code);
|
|
}
|
|
|
|
##################
|
|
# Library
|
|
##################
|
|
|
|
sub
|
|
RSS_xy {
|
|
my ($S,$x,$y,%params)= @_;
|
|
|
|
$x = $params{x} if($x eq 'x');
|
|
$y = $params{y} if($y eq 'y');
|
|
|
|
if((-1 < $x) && ($x < 1)) { $x*= $S->width; }
|
|
if((-1 < $y) && ($y < 1)) { $y*= $S->height; }
|
|
|
|
return($x,$y);
|
|
}
|
|
|
|
sub
|
|
RSS_color {
|
|
my ($S,$rgb)= @_;
|
|
my $alpha = 0;
|
|
my @d= split("", $rgb);
|
|
if(length($rgb) == 8) {
|
|
$alpha = hex("$d[6]$d[7]");
|
|
$alpha = ($alpha < 127) ? $alpha : 127;
|
|
}
|
|
return $S->colorAllocateAlpha(hex("$d[0]$d[1]"),hex("$d[2]$d[3]"),hex("$d[4]$d[5]"),$alpha);
|
|
}
|
|
|
|
sub
|
|
RSS_itemText {
|
|
my ($S,$x,$y,$text,%params)= @_;
|
|
return unless(defined($text));
|
|
|
|
if($params{useTextAlign}) {
|
|
my $align = GD::Text::Align->new($S,
|
|
color => RSS_color($S, $params{rgb}),
|
|
valign => $params{tvalign},
|
|
halign => $params{thalign},
|
|
);
|
|
$align->set_font($params{font}, $params{pt});
|
|
$align->set_text($text);
|
|
$align->draw($x, $y, 0);
|
|
} else {
|
|
$S->stringFT(RSS_color($S,$params{rgb}),$params{font},$params{pt},0,$x,$y,$text);
|
|
}
|
|
}
|
|
|
|
sub
|
|
RSS_itemTextBox {
|
|
my ($S,$x,$y,$boxwidth,$text,%params)= @_;
|
|
return unless(defined($text));
|
|
|
|
if($params{useTextWrap}) {
|
|
if((0 < $boxwidth) && ($boxwidth < 1)) { $boxwidth*= $S->width; }
|
|
my $wrapbox = GD::Text::Wrap->new($S,
|
|
color => RSS_color($S, $params{rgb}),
|
|
line_space => $params{linespace},
|
|
text => $text,
|
|
);
|
|
$wrapbox->set_font($params{font}, $params{pt});
|
|
$wrapbox->set(align => $params{thalign}, width => $boxwidth);
|
|
my ($left, $top, $right, $bottom) = $wrapbox->draw($x, $y);
|
|
return $bottom;
|
|
} else {
|
|
RSS_itemText($S,$x,$y,$text,%params);
|
|
return $y;
|
|
}
|
|
}
|
|
|
|
sub
|
|
RSS_itemTime {
|
|
my ($S,$x,$y,%params)= @_;
|
|
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
|
RSS_itemText($S,$x,$y,sprintf("%02d:%02d", $hour, $min),%params);
|
|
}
|
|
|
|
sub
|
|
RSS_itemSeconds {
|
|
my ($S,$x,$y,$format,%params)= @_;
|
|
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
|
if ($format eq "colon")
|
|
{
|
|
RSS_itemText($S,$x,$y,sprintf(":%02d", $sec),%params);
|
|
}
|
|
else
|
|
{
|
|
RSS_itemText($S,$x,$y,sprintf("%02d", $sec),%params);
|
|
}
|
|
}
|
|
sub
|
|
RSS_itemDate {
|
|
my ($S,$x,$y,%params)= @_;
|
|
my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime(time);
|
|
RSS_itemText($S,$x,$y,sprintf("%02d.%02d.%04d", $mday, $mon+1, $year+1900),%params);
|
|
|
|
}
|
|
|
|
sub
|
|
RSS_itemImg {
|
|
my ($S,$x,$y,$scale,$imgtype,$srctype,$arg,%params)= @_;
|
|
return unless(defined($arg));
|
|
return if($arg eq "");
|
|
my $I;
|
|
if($srctype eq "url" || $srctype eq "urlq") {
|
|
my $data;
|
|
if($srctype eq "url") {
|
|
$data= GetFileFromURL($arg,3,undef,1);
|
|
} else {
|
|
$data= GetFileFromURLQuiet($arg,3,undef,1);
|
|
}
|
|
if($imgtype eq "gif") {
|
|
$I= GD::Image->newFromGifData($data);
|
|
} elsif($imgtype eq "png") {
|
|
$I= GD::Image->newFromPngData($data);
|
|
} elsif($imgtype eq "jpeg") {
|
|
$I= GD::Image->newFromJpegData($data);
|
|
} else {
|
|
return;
|
|
}
|
|
} elsif($srctype eq "file") {
|
|
if($imgtype eq "gif") {
|
|
$I= GD::Image->newFromGif($arg);
|
|
} elsif($imgtype eq "png") {
|
|
$I= GD::Image->newFromPng($arg);
|
|
} elsif($imgtype eq "jpeg") {
|
|
$I= GD::Image->newFromJpeg($arg);
|
|
} else {
|
|
return;
|
|
}
|
|
} elsif($srctype eq "data") {
|
|
if($imgtype eq "gif") {
|
|
$I= GD::Image->newFromGifData($arg);
|
|
} elsif($imgtype eq "png") {
|
|
$I= GD::Image->newFromPngData($arg);
|
|
} elsif($imgtype eq "jpeg") {
|
|
$I= GD::Image->newFromJpegData($arg);
|
|
} else {
|
|
return;
|
|
}
|
|
} else {
|
|
return;
|
|
}
|
|
eval {
|
|
my ($width,$height)= $I->getBounds();
|
|
if ($scale =~ s/([wh])([\d]*)/$2/) { # get the digit from width/hight to pixel entry
|
|
#Debug "RSS scale $scale (1: $1 / 2: $2)contais px after Digit - width: $width / height: $height";
|
|
if ($1 eq "w") {
|
|
$scale=$scale/$width;
|
|
} else {
|
|
$scale=$scale/$height;
|
|
}
|
|
}
|
|
my ($swidth,$sheight)= (int($scale*$width), int($scale*$height));
|
|
|
|
given ($params{ihalign}) {
|
|
when('center') { $x -= $swidth/2; }
|
|
when('right') { $x -= $swidth; }
|
|
default { } # nothing to do
|
|
}
|
|
given ($params{ivalign}) {
|
|
when('center') { $y -= $sheight/2; }
|
|
when('base') { $y -= $sheight; }
|
|
when('bottom') { $y -= $sheight; }
|
|
default { } # nothing to do
|
|
}
|
|
|
|
#Debug "RSS placing $arg ($swidth x $sheight) at ($x,$y)";
|
|
$S->copyResampled($I,$x,$y,0,0,$swidth,$sheight,$width,$height);
|
|
};
|
|
if($@) {
|
|
Log3 undef, 2, "RSS: cannot create image $srctype $imgtype '$arg': $@";
|
|
}
|
|
}
|
|
|
|
sub
|
|
RSS_itemLine {
|
|
my ($S,$x1,$y1,$x2,$y2,$th,%params)= @_;
|
|
$S->setThickness($th);
|
|
$S->line($x1,$y1,$x2,$y2,RSS_color($S,$params{rgb}));
|
|
}
|
|
|
|
sub
|
|
RSS_itemRect {
|
|
my ($S,$x1,$y1,$x2,$y2,$filled,%params)= @_;
|
|
if($filled) {
|
|
$S->filledRectangle($x1,$y1,$x2,$y2,RSS_color($S,$params{rgb}));
|
|
} else {
|
|
$S->rectangle($x1,$y1,$x2,$y2,RSS_color($S,$params{rgb}));
|
|
}
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_evalLayout($$@) {
|
|
my ($S,$name,$layout)= @_;
|
|
|
|
my @layout= split("\n", $layout);
|
|
|
|
my %params;
|
|
$params{font}= "Arial";
|
|
$params{pt}= 12;
|
|
$params{rgb}= "ffffff";
|
|
$params{halign} = 'left';
|
|
$params{valign} = 'base';
|
|
$params{condition} = 1;
|
|
# we need two pairs of align parameters
|
|
# due to different default values for text and img
|
|
$params{useTextAlign}= $defs{$name}{fhem}{useTextAlign};
|
|
$params{useTextWrap}= $defs{$name}{fhem}{useTextWrap};
|
|
$params{ihalign} = 'left';
|
|
$params{ivalign} = 'top';
|
|
$params{thalign} = 'left';
|
|
$params{tvalign} = 'base';
|
|
$params{linespace} = 0;
|
|
$params{x}= 0;
|
|
$params{y}= 0;
|
|
|
|
|
|
my ($x,$y,$x1,$y1,$x2,$y2,$scale,$boxwidth,$text,$imgtype,$srctype,$arg,$format);
|
|
|
|
my $cont= "";
|
|
foreach my $line (@layout) {
|
|
# kill trailing newline
|
|
chomp $line;
|
|
# kill comments and blank lines
|
|
$line=~ s/\#.*$//;
|
|
$line=~ s/\s+$//;
|
|
$line= $cont . $line;
|
|
if($line=~ s/\\$//) { $cont= $line; undef $line; }
|
|
next unless($line);
|
|
$cont= "";
|
|
#Debug "$name: evaluating >$line<";
|
|
# split line into command and definition
|
|
my ($cmd, $def)= split("[ \t]+", $line, 2);
|
|
#Debug "CMD= \"$cmd\", DEF= \"$def\"";
|
|
|
|
# separate condition handling
|
|
if($cmd eq 'condition') {
|
|
$params{condition} = AnalyzePerlCommand(undef, $def);
|
|
next;
|
|
}
|
|
next unless($params{condition});
|
|
|
|
#Debug "before command $line: x= " . $params{x} . ", y= " . $params{y};
|
|
|
|
eval {
|
|
if($cmd eq "rgb") {
|
|
$def= "\"$def\"" if(length($def) == 6 && $def =~ /[[:xdigit:]]{6}/);
|
|
$params{rgb}= AnalyzePerlCommand(undef, $def);
|
|
} elsif($cmd eq "font") {
|
|
$params{font}= $def;
|
|
} elsif($cmd eq "pt") {
|
|
$def= AnalyzePerlCommand(undef, $def);
|
|
if($def =~ m/^[+-]/) {
|
|
$params{pt} += $def;
|
|
} else {
|
|
$params{pt} = $def;
|
|
}
|
|
$params{pt} = 6 if($params{pt} < 0);
|
|
} elsif($cmd eq "moveto") {
|
|
my ($tox,$toy)= split('[ \t]+', $def, 2);
|
|
my ($x,$y)= RSS_xy($S, $tox,$toy,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
} elsif($cmd eq "moveby") {
|
|
my ($byx,$byy)= split('[ \t]+', $def, 2);
|
|
my ($x,$y)= RSS_xy($S, $byx,$byy,%params);
|
|
$params{x} += $x;
|
|
$params{y} += $y;
|
|
} elsif($cmd ~~ @cmd_halign) {
|
|
my $d = AnalyzePerlCommand(undef, $def);
|
|
if($d ~~ @valid_halign) {
|
|
$params{ihalign}= $d unless($cmd eq "thalign");
|
|
$params{thalign}= $d unless($cmd eq "ihalign");
|
|
} else {
|
|
Log3 $name, 2, "$name: Illegal horizontal alignment $d";
|
|
}
|
|
} elsif($cmd ~~ @cmd_valign) {
|
|
my $d = AnalyzePerlCommand(undef, $def);
|
|
if( $d ~~ @valid_valign) {
|
|
$params{ivalign}= $d unless($cmd eq "tvalign");
|
|
$params{tvalign}= $d unless($cmd eq "ivalign");
|
|
} else {
|
|
Log3 $name, 2, "$name: Illegal vertical alignment $d";
|
|
}
|
|
} elsif($cmd eq "linespace") {
|
|
$params{linespace}= $def;
|
|
} elsif($cmd eq "text") {
|
|
($x,$y,$text)= split("[ \t]+", $def, 3);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
my $txt= AnalyzePerlCommand(undef, $text);
|
|
#Debug "$name: ($x,$y) $txt";
|
|
RSS_itemText($S,$x,$y,$txt,%params);
|
|
} elsif($cmd eq "textbox") {
|
|
($x,$y,$boxwidth,$text)= split("[ \t]+", $def, 4);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
my $txt= AnalyzePerlCommand(undef, $text);
|
|
#Debug "$name: ($x,$y) $txt";
|
|
$y= RSS_itemTextBox($S,$x,$y,$boxwidth,$txt,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
} elsif($cmd eq "line") {
|
|
($x1,$y1,$x2,$y2,$format)= split("[ \t]+", $def, 5);
|
|
($x1,$y1)= RSS_xy($S, $x1,$y1,%params);
|
|
($x2,$y2)= RSS_xy($S, $x2,$y2,%params);
|
|
$format //= 1; # set format to 1 as default thickness for the line
|
|
RSS_itemLine($S,$x1,$y1,$x2,$y2, $format,%params);
|
|
} elsif($cmd eq "rect") {
|
|
($x1,$y1,$x2,$y2,$format)= split("[ \t]+", $def, 5);
|
|
($x1,$y1)= RSS_xy($S, $x1,$y1,%params);
|
|
($x2,$y2)= RSS_xy($S, $x2,$y2,%params);
|
|
$format //= 0; # set format to 0 as default (not filled)
|
|
RSS_itemRect($S,$x1,$y1,$x2,$y2, $format,%params);
|
|
} elsif($cmd eq "time") {
|
|
($x,$y)= split("[ \t]+", $def, 2);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
RSS_itemTime($S,$x,$y,%params);
|
|
} elsif($cmd eq "seconds") {
|
|
($x,$y,$format) = split("[ \+]", $def,3);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
RSS_itemSeconds($S,$x,$y,$format,%params);
|
|
} elsif($cmd eq "date") {
|
|
($x,$y)= split("[ \t]+", $def, 2);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
RSS_itemDate($S,$x,$y,%params);
|
|
} elsif($cmd eq "img") {
|
|
($x,$y,$scale,$imgtype,$srctype,$arg)= split("[ \t]+", $def,6);
|
|
($x,$y)= RSS_xy($S, $x,$y,%params);
|
|
$params{x} = $x;
|
|
$params{y} = $y;
|
|
my $arg= AnalyzePerlCommand(undef, $arg);
|
|
RSS_itemImg($S,$x,$y,$scale,$imgtype,$srctype,$arg,%params);
|
|
} else {
|
|
Log3 $name, 2, "$name: Illegal command $cmd in layout definition.";
|
|
}
|
|
|
|
#Debug "after command $line: x= " . $params{x} . ", y= " . $params{y};
|
|
};
|
|
if($@) {
|
|
my $msg= "$name: Error from line \'$line\' in layout definition: $@";
|
|
chomp $msg;
|
|
Log3 $name, 2, $msg;
|
|
}
|
|
}
|
|
}
|
|
|
|
##################
|
|
sub
|
|
RSS_returnIMG($$) {
|
|
my ($name,$type)= @_;
|
|
|
|
my ($width,$height)= split(/x/, AttrVal($name,"size","800x600"));
|
|
|
|
#
|
|
# increase counter
|
|
#
|
|
if(defined($defs{$name}{fhem}) && defined($defs{$name}{fhem}{counter})) {
|
|
$defs{$name}{fhem}{counter}++;
|
|
} else {
|
|
$defs{$name}{fhem}{counter}= 1;
|
|
}
|
|
|
|
# true color
|
|
GD::Image->trueColor(1);
|
|
|
|
#
|
|
# create the image
|
|
#
|
|
our $S;
|
|
# let's create a blank image, we will need it in most cases.
|
|
$S= GD::Image->newTrueColor($width,$height);
|
|
my $bgcolor = AttrVal($name,'bgcolor','000000'); #default bg color = black
|
|
$bgcolor = RSS_color($S, $bgcolor);
|
|
# $S->colorAllocate(0,0,0); # other colors seem not to work (issue with GD)
|
|
$S->fill(0,0,$bgcolor);
|
|
# wrap to make problems with GD non-lethal
|
|
|
|
eval {
|
|
#
|
|
# set the background
|
|
#
|
|
# check if background directory is set
|
|
my $reason= "?"; # remember reason for undefined image
|
|
my $bgdir= AttrVal($name,"bg","undef");
|
|
if(defined($bgdir)){
|
|
my $bgnr; # item number
|
|
if(defined($defs{$name}{fhem}) && defined($defs{$name}{fhem}{bgnr})) {
|
|
$bgnr= $defs{$name}{fhem}{bgnr};
|
|
} else {
|
|
$bgnr= 0;
|
|
}
|
|
# check if at least tmin seconds have passed
|
|
my $t0= 0;
|
|
my $tmin= AttrVal($name,"tmin",0);
|
|
if(defined($defs{$name}{fhem}) && defined($defs{$name}{fhem}{t})) {
|
|
$t0= $defs{$name}{fhem}{t};
|
|
}
|
|
my $t1= time();
|
|
if($t1-$t0>= $tmin) {
|
|
$defs{$name}{fhem}{t}= $t1;
|
|
$bgnr++;
|
|
}
|
|
# detect pictures
|
|
if(opendir(BGDIR, $bgdir)){
|
|
my @bgfiles= grep {$_ !~ /^\./} readdir(BGDIR);
|
|
|
|
#foreach my $f (@bgfiles) {
|
|
# Debug sprintf("File \"%s\"\n", $f);
|
|
#}
|
|
closedir(BGDIR);
|
|
# get item number
|
|
if($#bgfiles>=0) {
|
|
if($bgnr > $#bgfiles) { $bgnr= 0; }
|
|
$defs{$name}{fhem}{bgnr}= $bgnr;
|
|
my $bgfile= $bgdir . "/" . $bgfiles[$bgnr];
|
|
my $filetype =(split(/\./,$bgfile))[-1];
|
|
my $bg;
|
|
$bg= newFromGif GD::Image($bgfile) if $filetype =~ m/^gif$/i;
|
|
$bg= newFromJpeg GD::Image($bgfile) if $filetype =~ m/^jpe?g$/i;
|
|
$bg= newFromPng GD::Image($bgfile) if $filetype =~ m/^png$/i;
|
|
if(defined($bg)) {
|
|
my ($bgwidth,$bgheight)= $bg->getBounds();
|
|
if($bgwidth != $width or $bgheight != $height) {
|
|
# we need to resize
|
|
my ($w,$h);
|
|
my ($u,$v)= ($bgwidth/$width, $bgheight/$height);
|
|
if($u>$v) {
|
|
$w= $width;
|
|
$h= $bgheight/$u;
|
|
} else {
|
|
$h= $height;
|
|
$w= $bgwidth/$v;
|
|
}
|
|
$S->copyResized($bg,($width-$w)/2,($height-$h)/2,0,0,$w,$h,$bgwidth,$bgheight);
|
|
} else {
|
|
# size is as required
|
|
# kill the predefined image and take the original
|
|
undef $S;
|
|
$S= $bg;
|
|
}
|
|
} else {
|
|
undef $S;
|
|
$reason= "Something was wrong with background image \"$bgfile\".";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#
|
|
# evaluate layout
|
|
#
|
|
if(defined($S)) {
|
|
RSS_evalLayout($S, $name, $defs{$name}{fhem}{layout});
|
|
} else {
|
|
Log3 $name, 2, "$name: Could not create image. $reason";
|
|
$S= GD::Image->newTrueColor($width,$height); # return empty image
|
|
}
|
|
$defs{$name}{STATE} = localtime();
|
|
|
|
|
|
}; #warn $@ if $@;
|
|
if($@) {
|
|
my $msg= $@;
|
|
chomp $msg;
|
|
Log3 $name, 2, $msg;
|
|
}
|
|
|
|
#
|
|
# return image
|
|
#
|
|
return ("image/jpeg; charset=utf-8", $S->jpeg) if($type eq 'jpg');
|
|
return ("image/png; charset=utf-8", $S->png) if($type eq 'png');
|
|
}
|
|
|
|
##################
|
|
#
|
|
# here we answer any request to http://host:port/fhem/rss and below
|
|
sub
|
|
RSS_CGI(){
|
|
|
|
my ($request) = @_; # /rss or /rss/name.rss or /rss/name.jpg or /rss/name.png
|
|
|
|
my ($name,$ext)= RSS_splitRequest($request); # name, ext (rss, jpg, png)
|
|
|
|
if(defined($name)) {
|
|
if($ext eq "") {
|
|
return("text/plain; charset=utf-8", "Illegal extension.");
|
|
}
|
|
if(!defined($defs{$name})) {
|
|
return("text/plain; charset=utf-8", "Unknown RSS device: $name");
|
|
}
|
|
|
|
if($ext eq "jpg") {
|
|
return RSS_returnIMG($name,'jpg');
|
|
} elsif($ext eq "png") {
|
|
return RSS_returnIMG($name,'png');
|
|
} elsif($ext eq "rss") {
|
|
return RSS_returnRSS($name);
|
|
} elsif($ext eq "html") {
|
|
return RSS_returnHTML($name);
|
|
}
|
|
} else {
|
|
return RSS_Overview();
|
|
}
|
|
|
|
}
|
|
|
|
|
|
#
|
|
|
|
1;
|
|
|
|
|
|
|
|
|
|
=pod
|
|
=begin html
|
|
|
|
<a name="RSS"></a>
|
|
<h3>RSS</h3>
|
|
<ul>
|
|
Provides a freely configurable RSS feed and HTML page.<p>
|
|
|
|
The media RSS feed delivers status pictures either in JPEG or PNG format.
|
|
|
|
This media RSS feed can be used to feed a status display to a
|
|
network-enabled photo frame.<p>
|
|
|
|
In addition, a periodically refreshing HTML page is generated that shows the picture
|
|
with an optional HTML image map.<p>
|
|
|
|
You need to have the perl module <code>GD</code> installed. This module is most likely not
|
|
available for small systems like Fritz!Box.<p>
|
|
RSS is an extension to <a href="#FHEMWEB">FHEMWEB</a>. You must install FHEMWEB to use RSS.<p>
|
|
|
|
Beginners might find the <a href="http://forum.fhem.de/index.php/topic,22520.0.html">RSS Workshop</a> useful.<p>
|
|
|
|
A note on colors: Colors are specified as 6- or 8-digit hex numbers,
|
|
every 2 digits determining the red, green and blue color components as in HTML
|
|
color codes. The optional 7th and 8th digit code the alpha channel (transparency from
|
|
00 to 7F). Examples: <code>FF0000</code> for red, <code>C0C0C0</code> for light
|
|
gray, <code>1C1C1C40</code> for semi-transparent gray.<p>
|
|
|
|
<a name="RSSdefine"></a>
|
|
<b>Define</b>
|
|
<ul>
|
|
<code>define <name> RSS jpg|png <hostname> <filename></code><br><br>
|
|
|
|
Defines the RSS feed. <code>jpg</code> and <code>png</code> are fixed literals to select output format.
|
|
<code><hostname></code> is the hostname of the fhem server as
|
|
seen from the consumer of the RSS feed. <code><filename></code> is the
|
|
name of the file that contains the <a href="RSSlayout">layout definition</a>.<p>
|
|
|
|
Examples
|
|
<ul>
|
|
<code>define FrameRSS RSS jpg host.example.org /etc/fhem/layout</code><br>
|
|
<code>define MyRSS RSS png 192.168.1.222 /var/fhem/conf/layout.txt</code><br>
|
|
</ul>
|
|
<br>
|
|
|
|
The RSS feeds are at
|
|
<ul>
|
|
<code>http://host.example.org:8083/fhem/rss/FrameRSS.rss</code><br>
|
|
<code>http://192.168.1.222:8083/fhem/rss/MyRSS.rss</code><br>
|
|
</ul>
|
|
<br>
|
|
|
|
The pictures are at
|
|
<ul>
|
|
<code>http://host.example.org:8083/fhem/rss/FrameRSS.jpg</code><br>
|
|
<code>http://192.168.1.222:8083/fhem/rss/MyRSS.png</code><br>
|
|
</ul>
|
|
<br>
|
|
|
|
The HTML pages are at
|
|
<ul>
|
|
<code>http://host.example.org:8083/fhem/rss/FrameRSS.html</code><br>
|
|
<code>http://192.168.1.222:8083/fhem/rss/MyRSS.html</code><br>
|
|
</ul>
|
|
<br>
|
|
|
|
</ul>
|
|
|
|
<a name="RSSset"></a>
|
|
<b>Set</b>
|
|
<ul>
|
|
<code>set <name> rereadcfg</code>
|
|
<br><br>
|
|
Rereads the <a href="RSSlayout">layout definition</a> from the file. Useful to enable
|
|
changes in the layout on-the-fly.
|
|
<br><br>
|
|
</ul>
|
|
|
|
<a name="RSSattr"></a>
|
|
<b>Attributes</b>
|
|
<br><br>
|
|
<ul>
|
|
<li>size<br>The dimensions of the picture in the format
|
|
<code><width>x<height></code>.</li><br>
|
|
<li>bg<br>The directory that contains the background pictures (must be in JPEG, GIF or PNG format, file
|
|
format is guessed from file name extension).</li><br>
|
|
<li>bgcolor <color><br>Sets the background color. </li><br>
|
|
<li>tmin<br>The background picture is shown at least <code>tmin</code> seconds,
|
|
no matter how frequently the RSS feed consumer accesses the page.</li><br>
|
|
<li>refresh<br>Time after which the HTML page is automatically reloaded.</li><br>
|
|
<li>areas<br>HTML code that goes into the image map.<br>
|
|
Example: <code>attr FrameRSS areas <area shape="rect" coords="0,0,200,200" href="http://fhem.de"/><area shape="rect" coords="600,400,799,599" href="http://has:8083/fhem" target="_top"/></code>
|
|
</li><br>
|
|
</ul>
|
|
<br><br>
|
|
|
|
<b>Usage information</b>
|
|
<br><br>
|
|
<ul>
|
|
If a least one RSS feed is defined, the menu entry <code>RSS</code> appears in the FHEMWEB
|
|
side menu. If you click it you get a list of all defined RSS feeds. The URL of any such is
|
|
RSS feed is <code>http://hostname:port/fhem/rss/name.rss</code> with <code>hostname</code> and
|
|
<code>name</code> from the RSS feed's <a href="RSSdefine">definition</a> and the <code>port</code>
|
|
(usually 8083) and literal <code>/fhem</code> from the underlying <a href="#FHEMWEB">FHEMWEB</a>
|
|
definition.<p>
|
|
|
|
Example:
|
|
<ul><code>http://host.example.org:8083/fhem/rss/FrameRSS.rss</code></ul><p>
|
|
|
|
The media RSS feed points to a dynamically generated picture. The URL of the picture
|
|
belonging to the RSS can be found by replacing the extension ".rss" in feed's URL by ".jpg" or ".png"
|
|
depending on defined output format,<p>
|
|
|
|
Example:
|
|
<ul><code>http://host.example.org:8083/fhem/rss/FrameRSS.jpg</code></ul><p>
|
|
<ul><code>http://192.168.100.200:8083/fhem/rss/FrameRSS.png</code></ul><p>
|
|
|
|
To render the picture the current, or, if <code>tmin</code> seconds have elapsed, the next
|
|
JPEG picture from the directory <code>bg</code> is chosen and scaled to the dimensions given
|
|
in <code>size</code>. The background is black if no usable JPEG picture can be found. Next the
|
|
script in the <a href="RSSlayout">layout definition</a> is used to superimpose items on
|
|
the background.<p>
|
|
|
|
You can directly access the URL of the picture in your browser. Reload the page to see
|
|
how it works.<p>
|
|
|
|
The media RSS feed advertises to refresh after 1 minute (ttl). Some photo frames ignore it and
|
|
use their preset refresh rate. Go for a photo frame with an adjustable refresh rate (e.g
|
|
every 5 seconds) if you have the choice!<p>
|
|
|
|
This is how the fhem config part might look like:<p>
|
|
<code>
|
|
define ui FHEMWEB 8083 global<br><br>
|
|
|
|
define FrameRSS RSS jpg host.example.org /etc/fhem/layout<br>
|
|
attr FrameRSS size 800x600<br>
|
|
attr FrameRSS bg /usr/share/pictures<br>
|
|
attr FrameRSS tmin 10<br>
|
|
</code>
|
|
|
|
</ul>
|
|
|
|
<a name="RSSlayout"></a>
|
|
<b>Layout definition</b>
|
|
<br><br>
|
|
<ul>
|
|
The layout definition is a script for placing items on the background. It is read top-down.
|
|
It consists of layout control commands and items placement commands. Layout control
|
|
commands define the appearance of subsequent items. Item placement commands actually
|
|
render items.<p>
|
|
|
|
Everything after a # is treated as a comment and ignored. You can fold long lines by
|
|
putting a \ at the end.<p>
|
|
|
|
<i>General notes</i><br>
|
|
<ol>
|
|
<li>Use double quotes to quote literal text if perl specials are allowed.</li>
|
|
<li>Text alignment requires the Perl module GD::Text::Align to be installed. Text wrapping (in text boxes) require GD::Text::Wrap to be installed. Debian-based systems can install both with <code>apt-get install libgd-text-perl</code>.</li>
|
|
</ol>
|
|
<p>
|
|
<i>Notes on coordinates</i><br>
|
|
<ol>
|
|
<li>(0,0) is the upper left corner.</li>
|
|
<li>Coordinates equal or greater than 1 are considered to be absolute pixels, coordinates between 0 and 1 are considered to
|
|
be relative to the total width or height of the picture.</li>
|
|
<li>Literal <code>x</code> and <code>y</code> evaluate to the most recently used x- and y-coordinate. See also moveto and moveby below.</li>
|
|
<!--<li>You can use <code>{ <a href="#perl"><perl special></a> }</code> for x and for y.</li>-->
|
|
</ol>
|
|
<p>
|
|
|
|
|
|
<i>Layout control commands</i><p>
|
|
|
|
<ul>
|
|
<li>moveto <x> <y><br>Moves most recently used x- and y-coordinate to the given absolute or relative position.</li><br>
|
|
|
|
<li>moveby <x> <y><br>Moves most recently used x- and y-coordinate by the given absolute or relative amounts.</li><br>
|
|
|
|
<li>font "<font>"<br>Sets the font. <font> is the name of a TrueType font (e.g.
|
|
<code>Arial</code>) or the full path to a TrueType font
|
|
(e.g. <code>/usr/share/fonts/truetype/arial.ttf</code>),
|
|
whatever works on your system.</li><br>
|
|
|
|
<li>rgb "<color>"<br>Sets the color. You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> for <color>.</li><br>
|
|
|
|
<li>pt <pt><br>Sets the font size in points. A + or - sign in front of the the number given
|
|
for <pt> signifies a change of the font size relative to the current size. Otherwise the absolute
|
|
size is set. You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> for <pt>.</li><br>
|
|
|
|
<li>thalign|ihalign|halign "left"|"center"|"right"<br>Sets the horizontal alignment of text, image or both. Defaults to left-aligned. You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> instead of the literal alignment control word.</li><br>
|
|
|
|
<li>tvalign|ivalign|valign "top"|"center"|"base"|"bottom"<br>Sets the vertical alignment of text, image or both. Defaults to base-aligned for text and
|
|
top-aligned for image. You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> instead of the literal alignment control word.</li><br>
|
|
|
|
<li>linespace <space><br>Sets the line spacing in pixels for text boxes (see textbox item below).</li><br>
|
|
|
|
<li>condition <condition><br>Subsequent layout control and item placement commands except for another condition command
|
|
are ignored if and only if <condition>
|
|
evaluates to false.</li><br>
|
|
</ul>
|
|
|
|
<i>Item placement commands</i><p>
|
|
<ul>
|
|
<li>text <x> <y> <text><br>Renders the text <text> at the
|
|
position (<x>, <y>) using the current font, font size and color.
|
|
You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> for <text> to fully
|
|
access device readings and do some programming on the fly. See below for examples.</li><br>
|
|
<li>textbox <x> <y> <boxwidth> <text><br>Same as before but text is rendered in a box of horizontal width <boxwidth>.</li><br>
|
|
<li>time <x> <y><br>Renders the current time in HH:MM format.</li><br>
|
|
<li>seconds <x> <y> <format><br>Renders the curent seconds. Maybe useful for a RSS Clock.</li><br>
|
|
<li>date <x> <y><br>Renders the current date in DD:MM:YYY format.</li><br>
|
|
<li>line <x1> <y1> <x2> <y2> [<thickness>]<br>Draws a line from position (<x1>, <y1>) to position (<x2>, <y2>) with optional thickness (default=1).</li><br>
|
|
<li>rect <x1> <y1> <x2> <y2> [<filled>]<br>Draws a rectangle with corners at positions (<x1>, <y1>) and (<x2>, <y2>), which is filled if the <filled> parameter is set and not zero.</li><br>
|
|
<li>img <x> <y> <['w' or 'h']s> <imgtype> <srctype> <arg> <br>Renders a picture at the
|
|
position (<x>, <y>). The <imgtype> is one of <code>gif</code>, <code>jpeg</code>, <code>png</code>.
|
|
The picture is scaled by the factor <s> (a decimal value). If 'w' or 'h' is in front of scale-value the value is used to set width or height to the value in pixel. If <srctype> is <code>file</code>, the picture
|
|
is loaded from the filename <arg>, if <srctype> is <code>url</code> or <code>urlq</code>, the picture
|
|
is loaded from the URL <arg> (with or without logging the URL), if <srctype> is <code>data</code>, the picture
|
|
is piped in from data <arg>. You can use
|
|
<code>{ <a href="#perl"><perl special></a> }</code> for <arg>. See below for example.
|
|
Notice: do not load the image from URL that is served by fhem as it leads to a deadlock.<br></li>
|
|
<br>
|
|
</ul>
|
|
|
|
<i>Example</i><p>
|
|
This is how a layout definition might look like:<p>
|
|
<code>
|
|
font /usr/share/fonts/truetype/arial.ttf # must be a TrueType font<br>
|
|
rgb "c0c0c0" # HTML color notation, RGB<br>
|
|
pt 48 # font size in points<br>
|
|
time 0.10 0.90<br>
|
|
pt 24<br>
|
|
text 0.10 0.95 { ReadingsVal("MyWeather","temperature","?"). "C" }<br>
|
|
moveby 0 -25<br>
|
|
text x y "Another text"<br>
|
|
img 20 530 0.5 png file { "/usr/share/fhem/www/images/weather/" . ReadingsVal("MyWeather","icon","") . ".png" }<br>
|
|
</code>
|
|
<p>
|
|
|
|
<i>Special uses</i><p>
|
|
|
|
You can display <a href="#SVG">SVG</a> plots with the aid of the helper function <code>plotAsPng(<name>[,<zoom>[,<offset>]])</code> (in 98_SVG.pm). Examples:<p>
|
|
<code>
|
|
img 20 30 0.6 png data { plotAsPng("mySVGPlot") }<BR>
|
|
img 20 30 0.6 png data { plotAsPng("mySVGPlot","qday",-1) }
|
|
</code>
|
|
<p>
|
|
This requires the perl module Image::LibRSVG and librsvg. Debian-based systems can install these with <code>apt-get install libimage-librsvg-perl</code>.
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
=end html
|
|
=cut
|