## ## The cuttlefish Visualization Tool. ## Copyright (C) 2006 The Regents of the University of California. ## ## This program is free software; you can redistribute it and/or modify it ## under the terms of the GNU General Public License version 2 as published ## by the Free Software Foundation. ## ## This program is distributed in the hope that it will be useful, ## but WITHOUT ANY WARRANTY; without even the implied warranty of ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ## GNU General Public License for more details. ## ## You should have received a copy of the GNU General Public License ## along with this program; if not, write to the Free Software ## Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ## ## written by Bradley Huffaker ## ## documention by Joshua Polterock ## Bradley Huffaker ## Marina Fomenkova ## ## # Map # This class holds the information needed to draw a map. This # includes the labels displayed on the map. Labels will be displayed # relative to the map, not the whole image. # package Map; use GD; use Canvas; use Time::Local; use Carp; use Math::Trig; use Math::BigFloat; use POSIX; use Util; require Exporter; @ISA = qw( Exporter ); @EXPORT = qw(); @EXPORT_OK = qw(); use strict; my $PI= pi; # 3.14159265; my $JulianCentury= 36525.0; my $J2000= 2451545.0; my $default_node_size = 40; my $bar_width = 2; my $MAGIC_NUMBER = 9234987234; sub new { my ($this, $linenum, $parent) = @_; my $canvas = new Canvas(); my @day_color = $canvas->hsv2rgb(0,0,50); my @night_color = $canvas->hsv2rgb(0,0,30); my @background_color = $canvas->hsv2rgb(0,0,0); my @foreground_color = $canvas->hsv2rgb(0,0,100); my $map = bless { "type" => "Map", "linenum" => $linenum, "majic" => $MAGIC_NUMBER, "x" => 0, "y" => 0, "image" => undef, "canvas" => $canvas, "show_names" => undef, "j_time" => undef, "e_time" => undef, "lat_min" => undef, "lat_max" => undef, "long_min" => undef, "long_max" => undef, "width" => undef, "height" => undef, "points" => undef, "font" => gdSmallFont, "name-font" => gdSmallFont, "label-font" => gdSmallFont, "night-color" => \@night_color, "day-color" => \@day_color, "background-color" => \@background_color, "foreground-color" => \@foreground_color, "node-size" => $default_node_size, "node-size-scale" => "linear", "node-type" => "bar", "static" => undef, "image-loaded" => undef, "labels" => [] }, $this; if ($parent->{majic} == $MAGIC_NUMBER) { foreach my $key (keys %$parent) { if ($key ne "labels") { $map->{$key} = $parent->{$key}; } } } return $map; } sub loadCanvas() { my ($this) = @_; if (defined $this->{image}) { my $big_canvas = new Canvas($this->{image}); $this->{canvas} = $big_canvas->duplicate( $this->{width}, $this->{height}); my ($width, $height) = ($this->{width}, $this->{height}) = $this->{canvas}->getBounds(); $this->{"image-loaded"} = 1; } #print "loadCanvas:$this->{image} width:$width height:$height\n"; } sub check() { my ($this) = @_; my @missing; if (defined $this->{image}) { foreach my $key (qw(image lat_min lat_max long_min long_max)) { unless (defined $this->{$key}) { push @missing, $key; } } } my @errors; if ($#missing > -1) { push @errors, "doesn't have:".join(",",@missing); } return @errors; } ############################################################################ # Check Images and Values ########################################################################### sub setImage($) { my ($this, $file) = @_; unless (-f $file) { return ("Unable to find file $file:$!"); } $this->{"image"} = $file; return; } sub setValue { my ($this, $key, $value) = @_; if ($key eq "label") { return $this->addLabel($value); } elsif ($key eq "font" || $key =~ /-font/) { my $font = String2Font($value); unless (defined $font) { return "Map::setValue unknown font \"$value\""; } my @keys = ($key); @keys = qw(font label-font name-font) if ($key eq "font"); foreach my $k (@keys) { $this->{$k} = $font; } } elsif ($key eq "show-names" || $key eq "static") { if ($value eq "false") { undef $this->{$key}; } else { $this->{$key} = 1; } } elsif ($key =~ /-color$/) { return $this->setColor($key,$value); } elsif ($key eq "node-type" && $value ne "circle" && $value ne "bar") { return "Map::setValue node-type found \"$value\" wanted circle or bar"; } elsif ($key eq "node-size-scale" && $value ne "log" && $value ne "linear") { return "Map::setValue node-size-scale found \"$value\"". " wanted log or linear"; } elsif ($key eq "bar-height") { $this->{"node-size"} = $value; return "Map::setValue bar-height is deprecated by \"node-size\""; } else { $this->{$key} = $value; } return; } sub setColor { my ($this, $key, $value) = @_; my @values = split /\s+/, $value; my $error_msg; my $error_found; foreach my $v (0..2) { if ($v =~ /^\d+$/) { $error_msg .= "$v " } else { $error_msg .= "\"$v\" "; $error_found = 1; } } if (defined $error_found) { return "Map::setColor requires three integers, found $error_msg"; } $this->{$key} = \@values; return; } sub setLatLong($$$$$) { my @keys = qw(lat_min long_min lat_max long_max); my ($this, @values) = @_; my $error = $this->_setValues("setGeo", \@keys, \@values); return $error if (defined $error); my @errors; foreach my $type (qw(long lat)) { my $min_key = $type."_min"; my $max_key = $type."_max"; unless ($this->{$max_key} > $this->{$min_key}) { push @errors, "$type must have max greater then min"; } } if ($#errors > -1) { return "Map::setGeo error ".join(" ",@errors); } return; } sub setValueNumber { my ($this, $key, $value) = @_; return $this->_setValues("setValueNumber", [$key], [$value]); } sub _setValues { my ($this, $func, $keys, $values) = @_; my $num_keys = @$keys; foreach my $i (0..($num_keys-1)) { unless (defined $values->[$i] && $values->[$i] =~ /^-?\d+(\.\d+)?$/) { return "Map::$func(".join(",",@$keys)."); " ."illegal value for $keys->[$i]:$values->[$i]"; } $this->{$keys->[$i]} = $values->[$i]; } return; } sub addLabel { my ($this, $value) = @_; my $label = { "font" => $this->{"font"} }; my @errors; $value =~ s/^\s+//; while ($value =~ /([^:]+):([^\s]+)\s*(.*)/) { my ($key, $val, $remains) = ($1, $2, $3); if ($key eq "font") { my $font = String2Font($val); unless (defined $font) { push @errors, "unknown font \"$val\""; } $val = $font; } $label->{$key} = $val; $value = $remains; if ($key ne "x" && $key ne "y" && $key ne "font" && $key ne "type") { push @errors, "unknown type \"$key\""; } } $label->{"string"} = $value; if ($#errors >= 0) { return "Map::addLabel(".join(",",@errors).")"; } else { push @{$this->{"labels"}}, $label; } return; } ############################################################################ # Sets the current time of day ########################################################################### sub setTime($$) { my ($this, $time) = @_; my ($sec, $min, $hour, $day, $mon, $year) = gmtime($time); =cut $mon++; $year+=1900; if ($mon<= 2) { $mon+=12; $year-=1; } my $j_time = int(365.25*$year) + int(30.6001*($mon+1))-15 + 1720996.5 + $day + $hour/24.0 + $sec/(24.0*60.0); =cut $this->{"time"} = $time; my $j_time = dtm_unix_to_julian($time); #my $j_time = gregorian_to_jd($time); $this->{j_time} = $j_time; my $e_time = ($j_time - 2451545.0 ) / 36525; $this->{e_time} = $e_time; my $eps = $this->_obliqeq($j_time); $this->{eps} = $eps; } sub dtm_unix_to_julian ($) { my ($time) = @_; return 2440588.0 + ($time -43200)/(24*60*60); } =cut my $GREGORIAN_EPOCH = 1721425.5; sub gregorian_to_jd { my ($time) = @_; my ($sec, $min, $hour, $day, $month, $year) = gmtime($time); $year+=1900; $month++; return ($GREGORIAN_EPOCH - 1) + (365 * ($year - 1)) + int(($year - 1) / 4) + (-int(($year - 1) / 100)) + int(($year - 1) / 400) + int((((367 * $month) - 362) / 12) + (($month <= 2) ? 0 : (leap_gregorian($year) ? -1 : -2) ) + $day) + $hour/24.0 + $sec/(24.0*60); } sub leap_gregorian { my ($year) = @_; return ($year%4 == 0) && (($year%100 != 0) || ($year%400 == 0)); } =cut sub _obliqeq { my ($this, $jd) = @_; my @oterms; my @consts = ( -4680.93, -1.55, 1999.25, -51.38, -249.67, -39.05, 7.12, 27.87, 5.79, 2.45 ); foreach my $i (0..9) { $oterms[$i] = $consts[$i]/3600.0; } my $eps = 23 + (26 / 60.0) + (21.448 / 3600.0); my ($v,$u); $v = $u = ($jd - $J2000) / ($JulianCentury * 100); if (abs($u) < 1.0) { foreach my $i (0..9) { $eps += $oterms[$i] * $v; $v *= $u; } } return $eps; } ############################################################################ # This adds data to the points field ########################################################################### sub addDataPoint { my ($this, $time, $long, $lat, $value, $name) = @_; unless (defined $this->{"image-loaded"}) { return; } undef $this->{xy2name}; my ($x, $y) = $this->longlat2xy($long,$lat); if ($x >= 0 && $x <= $this->{"width"} && $y >= 0 && $y <= $this->{"height"}) { my $xy = "$x $y"; $this->{"points"}{$time}{$xy} += $value; $this->{"names"}{$xy}{$name} += $value; return 1; } return; } sub addFrame { my ($this, $time_start, $time, $time_end) = @_; my $points_start = $this->{points}{$time_start}; my $points_end = $this->{points}{$time_end}; $this->{points}{$time} = $this->{points}{$time_start}; return; unless (defined $points_start) { die("addFrame() Failed to find start time:$time_start"); } unless (defined $points_end) { die("addFrame() Failed to find start time:$time_end"); } if (defined $this->{$time}) { die("addFrame() time:$time already set"); } my @xy = $this->mergePoints(keys %{$points_start}, keys %{$points_end}); my $scaler = 1; unless ($time_end == $time_start) { $scaler = ($time-$time_start)/($time_end - $time_start); } foreach my $xy (@xy) { my $value_start = $points_start->{$xy}; my $value_end = $points_end->{$xy}; my $value = $scaler*($value_end-$value_start) + $value_start; if ($value > 1) { $this->{points}{$time}{$xy} = $value; } } } sub mergePoints { my @x_y = sort @_; my @clean; while ($#x_y >= 0) { push @clean, pop @x_y; while ($#x_y >= 0 && $clean[$#clean] eq $x_y[$#x_y]) { pop @x_y; } } return @clean; } sub getMinMax { my ($this, $value_min, $value_max) = @_; foreach my $time (keys %{$this->{points}}) { foreach my $xy (keys %{$this->{points}{$time}}) { my $value = $this->{points}{$time}{$xy}; unless (defined $value_min) { $value_min = $value_max = $value; } elsif ($value < $value_min) { $value_min = $value; } elsif ($value > $value_max) { $value_max = $value; } } } return ($value_min, $value_max); } ############################################################################ # This adds data to the points field ########################################################################### sub draw { my ($this, $time, $canvas, $value_min, $value_max) = @_; my ($x, $y) = ($this->{x}, $this->{y}); my ($w, $h) = ($this->{width}, $this->{height}); my $background_color = $canvas->getColor(@{$this->{"background-color"}}); $canvas->{IMAGE}->filledRectangle($x,$y,$x+$w,$y+$h,$background_color); $this->setTime($time); if (defined $this->{"image-loaded"}) { if (!defined $value_min || !defined $value_max) { ($value_min, $value_max) = $this->getMinMax($value_min, $value_max); } $this->drawWater($time, $canvas); $this->drawPoints($time, $canvas, $value_min, $value_max); } $this->drawLabels($canvas); } sub drawWater { my ($this, $time, $canvas) = @_; my $water_canvas = $this->{canvas}; my $x_shift = $this->{x}; my $y_shift = $this->{y}; my ($w,$h) = ($this->{width},$this->{height}); my $static = $this->{static}; my $night_color = $canvas->getColor(@{$this->{"night-color"}}); my $day_color = $canvas->getColor(@{$this->{"day-color"}}); foreach my $x (0..($w)) { foreach my $y (0..($h)) { my $color = $water_canvas->{IMAGE}->getPixel($x, $y); my ($r, $g, $b) = $water_canvas->{IMAGE}->rgb($color); if ($r < 200 || $g < 200 || $b < 200) { my $color; if ($this->IsNightXY($x, $y) || defined $static) { $color = $night_color; } else { $color = $day_color; } $canvas->{IMAGE}->setPixel($x+$x_shift, $y+$y_shift, $color); } } } } sub SortXY { my ($this, $xy2value) = @_; my @xy = keys %$xy2value; if ($this->{"node-type"} eq "circle") { @xy = reverse sort { $xy2value->{$a} <=> $xy2value->{$b} } @xy; } else { my %xy2y; foreach my $xy (@xy) { my ($x, $y) = split /\s+/, $xy; $xy2y{$xy} = $y; } @xy = sort {$xy2y{$a}<=>$xy2y{$b}} @xy; } } sub drawPoints { my ($this, $time, $canvas, $value_min, $value_max) = @_; my $show_names = $this->{"show-names"}; my $xy2value = $this->{"points"}{$time}; my @names; my @xy = $this->SortXY($xy2value); my ($x_shift, $y_shift) = ($this->{x},$this->{y}); foreach my $xy (@xy) { my ($x, $y) = split /\s+/, $xy; my $value = $xy2value->{$xy}; my $color = $this->{canvas}->getColorMinMax($value, $value_min, $value_max); my $size = $this->nodeSize($value,$value_min,$value_max); if ($this->{"node-type"} eq "circle") { if ($size == 1) { $canvas->{IMAGE}->line($x, $y, $x+1, $y, $color); } elsif ($size == 2) { $canvas->{IMAGE}->line($x-1, $y, $x+1, $y, $color); $canvas->{IMAGE}->line($x, $y-1, $x, $y+1, $color); } else { $canvas->{IMAGE}->filledArc($x,$y,$size, $size, 0,360, $color); } } else { $size--; $canvas->{IMAGE}->filledRectangle($x+$x_shift, $y-$size+$y_shift, $x+1+$x_shift, $y+$y_shift,$color); } my $name_box = {"x0"=>$x,"x1"=>$x + 1, "y0"=>$y-$size, "y1"=>$y}; push @names, $name_box; } if (defined $show_names) { my $xy2name = $this->getName(); my $foreground = $canvas->getColor(@{$this->{"foreground-color"}}); foreach my $xy (@xy) { if (defined $show_names && defined $xy2name->{$xy}) { my $name_font = $this->{"name-font"}; my ($x, $y) = split /\s+/, $xy; my $name = $xy2name->{$xy}; my $name_len = length($name)*$name_font->width; my $name_x = $x - ($name_len)/2; my $name_y = $y; my $name_box = {"x0"=>$name_x,"x1"=>$name_x + $name_len, "y0"=>$y+1, "y1"=>$y+1+$name_font->height}; if ($show_names eq "all" || CheckOverlap($name_box, @names)) { $canvas->{IMAGE}->string($name_font, $name_x+$x_shift, $name_y+$y_shift, $name, $foreground); push @names, $name_box; } } } } } sub nodeSize { my ($this, $value, $value_min, $value_max) = @_; my $size = $this->{"node-size"}; if ($value_min != $value_max) { if ($this->{"node-size-scale"} eq "log") { $size = $size *log($value-$value_min+1)/log($value_max-$value_min+1) + 1; } else { $size = $size*(($value-$value_min)/($value_max-$value_min)) +1; } $size = int($size); } return $size; } sub drawLabels { my ($this, $canvas) = @_; my $image = $canvas->{IMAGE}; my $foreground = $canvas->getColor(@{$this->{"foreground-color"}}); my @labels = @{$this->{labels}}; foreach my $label (@labels) { my $type = $label->{"type"}; my $string = $label->{"string"}; if ($type eq "date") { $string = POSIX::strftime( "\%Y-\%m-\%d \%H:\%M:\%S UTC \%A", gmtime($this->{"time"})); } my $x = $label->{"x"}; my $y = $label->{"y"}; my $font = $label->{"font"}; $x = 0 unless (defined $x); $y = 0 unless (defined $y); $x += $this->{x}; $y += $this->{y}; $y -= $font->height; $image->string($font, $x, $y, $string, $foreground); } } sub getName { my ($this) = @_; my $xy2name = $this->{xy2name}; if (defined $this->{xy2name}) { return $this->{xy2name}; } $xy2name = {}; my $names = $this->{names}; foreach my $xy (keys %{$names}) { my @names = reverse sort {$names->{$a}<=>$names->{$b}} keys %$names; $xy2name->{$xy} = $names[0]; } $this->{xy2name} = $xy2name; } sub CheckOverlap { my ($this, $box, @boxes) = @_; foreach my $box_exist (@boxes) { foreach my $boxes ([$box_exist, $box],[$box,$box_exist]) { my $x0 = $boxes->[1]->{x0}; my $y0 = $boxes->[1]->{y0}; my $x1 = $boxes->[1]->{x1}; my $y1 = $boxes->[1]->{y1}; foreach my $x ($boxes->[0]->{x0},$boxes->[0]->{x1}) { foreach my $y ($boxes->[0]->{y0},$boxes->[0]->{y1}) { if (($x >= $x0 && $x <= $x1) && ($y >= $y0 && $y <= $y1)) { return 1; } } } } } return; } ############################################################################ # This converts between the x and y of the canvas and the long # and lat of the map. ########################################################################### sub xy2longlat { my ($this, $x, $y) = @_; my $long = ($x/($this->{width}-1))*($this->{long_max}-$this->{long_min}) + $this->{long_min}; my $lat = $this->{lat_max} - ($y/($this->{height}-1)) *($this->{lat_max}-$this->{lat_min}); return ($long,$lat); } sub longlat2xy { my ($this, $long, $lat) = @_; my $x = (($long-$this->{long_min})*($this->{width}+1)) /($this->{long_max} - $this->{long_min}); my $y = ($this->{lat_max} - $lat)*($this->{height}+1)/ ($this->{lat_max} - $this->{lat_min}); $x = int($x); $y = int($y); return ($x,$y); } ############################################################################ # This handles the math to checks the earth shadow ########################################################################### sub IsNightXY { my ($this, $x, $y) = @_; return $this->IsNight($this->xy2longlat($x,$y)); } sub FixAngle { my ($value) = @_; my $retval = POSIX::fmod($value, 360); return ($retval < 0 ? $retval + 360 : $retval); } sub IsNight { my ($this,$long, $lat) = @_; my $jd = $this->{j_time}; my $t = $this->{e_time}; my $k = $PI/180; my $m = 357.52910 + 35999.05030*$t - 0.0001559*$t*$t - 0.00000048*$t*$t*$t; #i mean anomaly, degree my $l0 = 280.46645 + 36000.76983*$t + 0.0003032*$t*$t; # mean longitude, my $ld = (1.914600 - 0.004817*$t - 0.000014*$t*$t)*sin($k*$m) + (0.019993 - 0.000101*$t)*sin($k*2*$m) + 0.000290*sin($k*3*$m); my $e_long = $l0 + $ld; # true longitude degree $e_long = FixAngle($e_long); #my $e_long = ($l0 + $ld)%360; #$e_long = $e_long + (($l0 + $ld)-$e_long)/360; #print "t:$t\n"; #print "m:$m\n"; #print "l0:$l0\n"; #print "ld:$ld\n"; #print "long:$e_long\n"; $e_long = $e_long*$k; my $eps = 23.439351*$k; # obliquity of ecliptic #my $eps = $this->{eps};#23.439351; # obliquity of ecliptic my $X = cos($e_long); my $Y = cos($eps)*sin($e_long); my $Z = sin($eps)*sin($e_long); my $R = sqrt(1.0-($Z*$Z)); my $delta = (180/$PI)*atan($Z/$R); # in degrees my $RA = (360/$PI)*atan($Y/($X+$R)); # in hours #print "X:$X\n"; #print "Y:$Y\n"; #print "Z:$Z\n"; #print "R:$R\n"; #print "delta:$delta\n"; #print "RA:$RA\n"; my $theta = 280.46061837 + 360.98564736629*($jd-2451545.0) + 0.000387933*$t*$t - $t*$t*$t/38710000.0; $theta += $long; $theta = FixAngle($theta); my $tau = $theta - $RA; #print "j time:$jd\n"; #print "theta:$theta\n"; #print "tau:$tau\n"; $delta *= $k; $lat *= $k; $tau *= $k; my $h = (180/$PI)*asin(sin($lat)*sin($delta) + cos($lat)*cos($delta)*cos($tau)); my $az = atan(-sin($tau))/(cos($lat)*tan($delta) - sin($lat)*cos($tau)); #print "h:$h\n"; #print "az:$az\n"; #printf "- %.2f,%.2f - %.2f %.2f %.2f\n", $long, $lat, $e_long, $delta, $h; #if ($h < 0) { print "$h\n"; } return ($h < 0); } sub dtr { return ($_[0]*$PI)/180; } sub rtd { return ($_[0]*180)/$PI; } sub kepler { my ($m, $ecc) = @_; my ($e, $delta); $e = $m = dtr($m); my $EPSILON = 1e-6; do { $delta = $e - $ecc *sin($e) - $m; $e -= $delta / (1 - $ecc * cos($e)); } while (abs($delta) > $EPSILON); return $e; }