#
#  gpsman --- GPS Manager: a manager for GPS receiver data
#
#  Copyright (c) 2007 Miguel Filgueiras (mig@ncc.up.pt) / Universidade do Porto
#
#    This program is free software; you can redistribute it and/or modify
#      it under the terms of the GNU General Public License as published by
#      the Free Software Foundation; either version 2 of the License, or
#      (at your option) any later version.
#
#      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.
#
#  File: compute.tcl
#  Last change:  30 April 2007
#
# Includes contributions by
#  - Matt Martin (matt.martin@ieee.org) marked "MGM contribution"
#  - Valere Robin (valere.robin@wanadoo.fr) marked "VR contribution"
#

## Some formulae kindly supplied by
#     Luisa Bastos, Universidade do Porto
#     Gil Goncalves, Universidade de Coimbra
#  Computation of area of spherical polygon adapted from sph_poly.c in
#   "Graphics Gems IV", edited by Paul Heckbert, Academic Press, 1994.
#  Formula for ellipsoid radius from
#   "Ellipsoidal Area Computations of Large Terrestrial Objects"
#   by Hrvoje Lukatela
#   http://www.geodyssey.com/papers/ggelare.html
#  Algorithm for finding a point at given distance and bearing from another
#   taken from the program "forward" available from
#   ftp://www.ngs.noaa.gov/pub/pcsoft/for_inv.3d/
#   It uses the modified Rainsford's Method with Helmert's elliptical terms,
#   and is effective in any azimuth and at any distance short of antipodal.
##

### positions and coordinates

proc Coord {pformt coord negh} {
    # convert coordinate $coord in format $pformt, with negative heading $negh,
    #  to signed degrees
    # $pformt in {DMS, DMM, DDD, GRA}

    set coord [string trim $coord]
    set sign 1
    set h [string index "$coord" 0]
    if { ! [regexp {[0-9]} $h] } {
	if { $h == $negh || $h == "-" } { set sign -1 }
	set coord [string range "$coord" 1 end]
    }
    switch $pformt {
	DMS {
	    scan "$coord" "%d %d %f" d m s
	    return [expr $sign*($d+$m/60.0+$s/3600.0)]
	}
	DMM {
	    scan "$coord" "%d %f" d m
	    return [expr $sign*($d+$m/60.0)]
	}
	DDD {
	    return [expr $sign*$coord]
	}
	GRA {
	    return [expr $sign*0.9*$coord]
	}
    }
}

proc FormatPosition {latd longd datum reqpfmt pfdatum args} {
    # produce formatted position from $latd and $longd in decimal
    #  degrees for $datum
    #  $reqpfmt is the required position format
    #  $pfdatum is the datum to use with this format
    #     if $pfdatum=="", $datum is used unless $reqpfmt needs a fixed datum
    #     otherwise $pfdatum must be compatible with $reqpfmt; if it is
    #     not a call to BUG will be made
    #  $args is either "" or is an alternative format to be used with $datum
    #     when conversion to $reqpfmt fails (out of grid scope)
    # result is a list with: position representation, position format
    #  used and datum used
    # if $args=="" and conversion to $reqpfmt yields an out of grid scope
    #  result ("--" in the zone or x-field), this result is used in
    #  building the position representation
    # a position representation is a list whose first two elements
    #  are lat and long in degrees, and whose rest is dependent on format
    #  type:
    #       ==latlong: lat and long in external format
    #       ==utm: zones east and north and x y coordinates
    #       ==grid: zone name and x y coordinates
    #       ==nzgrid: x y coordinates
    #       ==mh: Maidenhead-locator (6 characters)
    global POSTYPE

    set la $latd ; set lo $longd
    if { $pfdatum == "" } {
	set udatum $datum
    } else {
	set udatum $pfdatum
	if { $pfdatum != $datum } {
	    foreach "la lo" [ToDatum $la $lo $datum $pfdatum] {	break }
	}
    }
    switch [set ptype $POSTYPE($reqpfmt)] {
	latlong {
	    set p [FormatLatLong $la $lo $reqpfmt]
	}
	utm {
	    set p [linsert [DegreesToUTM $la $lo $udatum] 0 $la $lo]
	}
	grid -  nzgrid {
	    if { [set gd [GridDatum $reqpfmt $udatum]] != $udatum } {
		if { $pfdatum != "" } {
		    BUG calling FormatPosition with wrong pfdatum
		}
		set udatum $gd
		foreach "la lo" [ToDatum $latd $longd $datum $gd] { break }
	    }
	    if { $ptype == "grid" } {
		set p [DegreesTo$reqpfmt $la $lo $udatum]
	    } else { set p [DegreesToNZGrid $reqpfmt $la $lo $udatum] }
	    if { [lindex $p 0] == "--" && $args != "" } {
		# out of scope
		return [FormatPosition $latd $longd $datum $args ""]
	    }
	    set p [linsert $p 0 $la $lo]
	}
	mh {
	    set p [list $la $lo [DegreesToMHLoc $la $lo]]
	}
    }
    return [list $p $reqpfmt $udatum]
}

proc FormatLatLong {latd longd pfmt} {
    # produce formatted position from $latd and $longd in decimal
    #  degrees in latitude/longitude position format
    # result is the position representation: a list whose first two elements
    #  are lat and long in degrees, followed by lat and long in external format
    global COUTFMT

    if { $latd < 0 } {
	# VR contribution: 0-$latd instead of -$latd; same for $longd
	set lax [expr 0-$latd] ; set hlat S
    } else { set lax $latd ; set hlat N }
    if { $longd < 0 } {
	set lox [expr 0-$longd] ; set hlng W
    } else { set lox $longd ; set hlng E }
    if { $pfmt == "GRA" } {
	set lax [format $COUTFMT(GRA) [expr $latd/0.9]]
	set lox [format $COUTFMT(GRA) [expr $longd/0.9]]
	return [list $latd $longd $lax $lox]
    }
    return [list $latd $longd "$hlat[ExtDegrees $pfmt $lax]" \
		"$hlng[ExtDegrees $pfmt $lox]"]
}

proc ExtDegrees {pformt degs} {
    # from signed degrees $degs to external representation of format $pformt
    # $pformt in {DMS, DMM, DDD, DMSsimpl}
    #  DMSsimpl is similar to DMS, but for values less than 1 degree the
    #  representation is either MM'SS.S" or SS.S"
    global COUTFMT

    switch -glob $pformt {
	DMS* {
	      if { $degs < 0 } {
		  set degs [expr -$degs] ; set sign -1
	      } else { set sign 1 }
	      set d [expr int($degs)]
	      set degs [expr ($degs-$d)*60]
	      set d [expr $sign*$d]
	      set m [expr int($degs)]
	      set s [expr ($degs-$m)*60]
	      if { $s > 59.95 } { set s 0 ; incr m }
	      if { $m > 59 } { set m 0 ; incr d }
	      set sf $COUTFMT(sec)
	      if { $d > 0 || $pformt == "DMS" } {
		  return [format "%d %02d $sf" $d $m $s]
	      }
	      if { $m > 0 } {
		  return [format "%02d'$sf" $m $s]
	      }
	      return [format "$sf\"" $s]
	}
	DMM {
	      if { $degs < 0 } {
		  set degs [expr -$degs] ; set sign -1
	      } else { set sign 1 }
	      set d [expr int($degs)]
	      set degs [expr ($degs-$d)*60]
	      set d [expr $sign*$d]
	      if { $degs > 59.995 } { set degs 0 ; incr d }
	      return [format "%d $COUTFMT(min)" $d $degs]
	}
	DDD {
	      return [format $COUTFMT(deg) $degs]
	}
    }
}

proc ChangeCoordSign {pos} {

    if { $pos != "N" } { return W }
    return S
}

proc ConvertPos {pfrmt zone x y datum npfrmt} {
    # create position representation in format $npfrmt from position
    #  $x, $y (as decimal numbers) in format $pfrmt
    #         if $pfrmt in {DDD, GRA} take $x as long and $y as lat
    #  $zone is either a list with ze and zn for UTM/UPS, or the zone
    #    name for other grids (and is meaningless for other formats
    # values of $x, $y are checked, as well as suitability of $datum
    #  and of validity of new format
    # return -1 on error
    global POSTYPE MESS TXT COUTFMT

    switch [set ptype $POSTYPE($pfrmt)] {
	latlong {
	    # must be either DDD or GRA
	    if { $pfrmt == "GRA" } {
		set x [expr $x*0.9] ; set y [expr $y*0.9]
	    }
	    if { $y < 0 } {
		set la [expr -$y] ; set hlat S
	    } else {
		set la $y ; set hlat N
	    }
	    if { $x < 0 } {
		set lo [expr -$x] ; set hlng W
	    } else {
		set lo $x ; set hlng E
	    }
	    if { ! [CheckCoord GMMessage $pfrmt $la N 90] || \
		    ! [CheckCoord GMMessage $pfrmt $lo E 180] } {
		return -1
	    }
	    if { $npfrmt == "DDD" } {
		return [list $y $x "${hlat}[format $COUTFMT(deg) $la]" \
			           "${hlng}[format $COUTFMT(deg) $lo]"]
	    }
	    set la $y ; set lo $x
	}
	utm {
	    foreach "ze zn" $zone {}
	    foreach "la lo" [UTMToDegrees $ze $zn $x $y $datum] { break }
	    if { $npfrmt == $pfrmt } {
		return [list $la $lo $ze $zn $x $y]
	    }
	}
	grid -  nzgrid {
	    if { [BadDatumFor $pfrmt $datum GMMessage] != 0 } {
		return -1
	    }
	    if { [set p [GridToDegrees $pfrmt $zone $x $y $datum]] == 0 } {
		GMMessage $MESS(outofgrid)
		return -1
	    }
	    if { $npfrmt == $pfrmt } {
		if { $ptype == "grid" } {
		    return [linsert $p end $zone $x $y]
		} else { return [linsert $p end $x $y] }
	    }
	}
	default {
	    BUG calling ConvertPos with wrong format
	}
    }
    # $datum is suitable for $npfrmt
    set p [lindex [FormatPosition $la $lo $datum $npfrmt $datum] 0]
    if { [lindex $p 2] == "--" } {
	GMMessage $MESS(outofgrid)
	return -1
    }
    return $p
}

### distances and geographic bearings
# formulae from Spherical Trignometry
# the Law of Cosines, used below to compute distances and bearings, is known
#  to be unsuitable for computations because of excessive round-off errors;
#  alternative procedures are supplied in file acccomp.tcl and are loaded
#  if $ACCFORMULAE (accurate formulae option) is set

proc ComputeDist {p1 p2 datum} {
    # distance (km) between positions $p1 and $p2 with same datum
            # formulae kindly supplied by Luisa Bastos, Universidade do Porto
            #   and Gil Goncalves, Universidade de Coimbra

    set lad1 [lindex $p1 0] ; set lod1 [lindex $p1 1]
    set lad2 [lindex $p2 0] ; set lod2 [lindex $p2 1]
    if { $lad1==$lad2 && $lod1==$lod2 } { return 0 }
    set la1 [expr $lad1*0.01745329251994329576]
    set lo1 [expr $lod1*0.01745329251994329576]
    set la2 [expr $lad2*0.01745329251994329576]
    set lo2 [expr $lod2*0.01745329251994329576]
    return [expr 1e-3*[lindex [EllipsdData $datum] 0]* \
	         acos(cos($lo1-$lo2)*cos($la1)*cos($la2)+sin($la1)*sin($la2))]
}

proc ComputeBear {p1 p2 datum} {
    # bearing from position $p1 to $p2 with same datum
    #  $datum not used here but kept for compatibility with replacement
    #    proc in acccomp.tcl
            # formulae kindly supplied by Luisa Bastos, Universidade do Porto
            #   and Gil Goncalves, Universidade de Coimbra

    set lad1 [lindex $p1 0] ; set lod1 [lindex $p1 1]
    set lad2 [lindex $p2 0] ; set lod2 [lindex $p2 1]
    if { $lad1==$lad2 && $lod1==$lod2 } { return 0 }
    set la1 [expr $lad1*0.01745329251994329576]
    set lo1 [expr $lod1*0.01745329251994329576]
    set la2 [expr $lad2*0.01745329251994329576]
    set lo2 [expr $lod2*0.01745329251994329576]
    set da [expr $la2-$la1] ; set do [expr $lo2-$lo1]
    if { [expr abs($da)] < 1e-20 } {
 	if { [expr abs($do)] < 1e-20 } {
 	    set b 0
 	} elseif { $do < 0 } {
 	    set b 270
 	} else { set b 90 }
    } elseif { [expr abs($do)] < 1e-20 } {
 	if { $da < 0 } {
 	    set b 180
 	} else { set b 0 }
    } else {
 	set b [expr round(atan2(sin($do), \
 		                tan($la2)*cos($la1)-sin($la1)*cos($do)) \
 			  *57.29577951308232087684)]
	if { $b < 0 } {
	    if { $do < 0 } { incr b 360 } else { incr b 180 }
	} elseif { $do < 0 } { incr b 180 }
    }
    return $b
}    

proc ComputeDistBear {p1 p2 datum} {
    # distance between and bearing from positions $p1 and $p2 with same datum
            # formulae kindly supplied by Luisa Bastos, Universidade do Porto
            #   and Gil Goncalves, Universidade de Coimbra

    set lad1 [lindex $p1 0] ; set lod1 [lindex $p1 1]
    set lad2 [lindex $p2 0] ; set lod2 [lindex $p2 1]
    if { $lad1==$lad2 && $lod1==$lod2 } { return "0 0" }
    set la1 [expr $lad1*0.01745329251994329576]
    set lo1 [expr $lod1*0.01745329251994329576]
    set la2 [expr $lad2*0.01745329251994329576]
    set lo2 [expr $lod2*0.01745329251994329576]
    set a [lindex [EllipsdData $datum] 0]
           # distance (km)
    set d [expr 1e-3*$a*acos(cos($lo1-$lo2)*cos($la1)*cos($la2)+ \
	                 sin($la1)*sin($la2))]
           # bearing
    set da [expr $la2-$la1] ; set do [expr $lo2-$lo1]
    if { [expr abs($da)] < 1e-20 } {
 	if { [expr abs($do)] < 1e-20 } {
 	    set b 0
 	} elseif { $do < 0 } {
 	    set b 270
 	} else { set b 90 }
    } elseif { [expr abs($do)] < 1e-20 } {
 	if { $da < 0 } {
 	    set b 180
 	} else { set b 0 }
    } else {
 	set b [expr round(atan2(sin($do), \
 		                tan($la2)*cos($la1)-sin($la1)*cos($do)) \
 			  *57.29577951308232087684)]
	if { $b < 0 } {
	    if { $do < 0 } { incr b 360 } else { incr b 180 }
	} elseif { $do < 0 } { incr b 180 }
    }
    return [list $d $b]
}

proc SetDatumData {datum} {
    # this proc sets datum parameters as global variables for repeated use
    #  in conversions; see procs ComputeDistFD, ComputeDistBearFD
    global DatumA DatumF FDDatum

    if { $FDDatum != $datum } {
	set dt [EllipsdData $datum]
	set DatumA [lindex $dt 0] ; set DatumF [lindex $dt 1]
	set FDDatum $datum
    }
    return
}

proc ComputeDistFD {p1 p2} {
    # compute distance (km) between positions $p1 and $p2 assuming datum
    #  parameters where set by calling SetDatumData
    global DatumA
            # formulae kindly supplied by Luisa Bastos, Universidade do Porto
            #   and Gil Goncalves, Universidade de Coimbra

    set lad1 [lindex $p1 0] ; set lod1 [lindex $p1 1]
    set lad2 [lindex $p2 0] ; set lod2 [lindex $p2 1]
    if { $lad1==$lad2 && $lod1==$lod2 } { return 0 }
    set la1 [expr $lad1*0.01745329251994329576]
    set lo1 [expr $lod1*0.01745329251994329576]
    set la2 [expr $lad2*0.01745329251994329576]
    set lo2 [expr $lod2*0.01745329251994329576]
    return [expr 1e-3*$DatumA*acos(cos($lo1-$lo2)*cos($la1)*cos($la2)+ \
	                      sin($la1)*sin($la2))]
}

proc ComputeDistBearFD {p1 p2} {
    # compute distance (km) between and bearing from positions $p1 and $p2
    #  assuming datum parameters where set by calling SetDatumData
    global DatumA
            # formulae kindly supplied by Luisa Bastos, Universidade do Porto
            #   and Gil Goncalves, Universidade de Coimbra

    set lad1 [lindex $p1 0] ; set lod1 [lindex $p1 1]
    set lad2 [lindex $p2 0] ; set lod2 [lindex $p2 1]
    if { $lad1==$lad2 && $lod1==$lod2 } { return "0 0" }
    set la1 [expr $lad1*0.01745329251994329576]
    set lo1 [expr $lod1*0.01745329251994329576]
    set la2 [expr $lad2*0.01745329251994329576]
    set lo2 [expr $lod2*0.01745329251994329576]
           # distance (km)
    set d [expr 1e-3*$DatumA*acos(cos($lo1-$lo2)*cos($la1)*cos($la2)+ \
	                 sin($la1)*sin($la2))]
           # bearing
    set da [expr $la2-$la1] ; set do [expr $lo2-$lo1]
    if { [expr abs($da)] < 1e-20 } {
 	if { [expr abs($do)] < 1e-20 } {
 	    set b 0
 	} elseif { $do < 0 } {
 	    set b 270
 	} else { set b 90 }
    } elseif { [expr abs($do)] < 1e-20 } {
 	if { $da < 0 } {
 	    set b 180
 	} else { set b 0 }
    } else {
 	set b [expr round(atan2(sin($do), \
 		                tan($la2)*cos($la1)-sin($la1)*cos($do)) \
 			  *57.29577951308232087684)]
	if { $b < 0 } {
	    if { $do < 0 } { incr b 360 } else { incr b 180 }
	} elseif { $do < 0 } { incr b 180 }
    }
    return [list $d $b]
}

proc CompWPDistBear {wp1 wp2} {
    # distance (km) between and bearing from WPs with names $wp1, $wp2
    global WPPFrmt WPPosn WPDatum

    set i1 [IndexNamed WP $wp1]
    set i2 [IndexNamed WP $wp2]
    if { $i1<0 || $i2<0 } { return "--- ---" }
    if { $i1 == $i2 } { return "0 0" }
    return [CompDistBearDatums $WPPosn($i1) $WPDatum($i1) \
	                          $WPPosn($i2) $WPDatum($i2)]
}

proc CompDistBearDatums {p1 d1 p2 d2} {
    # distance (km) between and bearing from positions $p1 and $p2
    #  with datums $d1 and $d2
    if { $d1 != $d2 } {
	set p2 [ToDatum [lindex $p2 0] [lindex $p2 1] $d2 $d1]
    }
    return [ComputeDistBear $p1 $p2 $d1]
}

## point at given distance and bearing from another point

proc CoordsAtDistBear {pos dist bear datum} {
    # compute coordinates of point at given distance (km) and bearing
    #  (0-360 degrees) from another point
    # algorithm taken from the program "forward" available from
    #  ftp://www.ngs.noaa.gov/pub/pcsoft/for_inv.3d/
    #  It uses the modified Rainsford's Method with Helmert's elliptical
    #  terms, and is effective in any azimuth and at any distance short of
    #  antipodal.
    #  $dist is constrained here to be less than pi*semi-major axis
    global MESS

    foreach "lat0 long0" $pos { break }
    foreach "a f" [EllipsdData $datum] { break }
    if { [set dist [expr $dist*1000]] >= 3.1415926*$a } {
	GMMessage $MESS(distlarge)
	return [list $lat $long]
    }
    set phi [expr $lat0*0.01745329251994329576]
    set lam [expr $long0*0.01745329251994329576]
    set azm [expr $bear*0.01745329251994329576]
    set r [expr 1-$f]
    set tu [expr $r*sin($phi)/cos($phi)]
    set sf [expr sin($azm)] ; set cf [expr cos($azm)]
    set baz 0
    if { [set cf [expr cos($azm)]] != 0 } {
	set baz [expr atan2($tu,$cf)*2]
    }
    set cu [expr 1.0/sqrt($tu*$tu+1)]
    set su [expr $tu*$cu]
    set sa [expr $cu*$sf]
    set c2a [expr -$sa*$sa+1]
    set x [expr sqrt((1.0/$r/$r-1)*$c2a+1)+1]
    set x [expr ($x-2.0)/$x]
    set c [expr 1-$x]
    set c [expr ($x*$x/4.0+1.0)/$c]
    set d [expr (0.375*$x*$x-1)*$x]
    set y [set tu [expr 1.0*$dist/$r/$a/$c]]
    while 1 {
	set sy [expr sin($y)] ; set cy [expr cos($y)]
	set cz [expr cos($baz+$y)]
	set e [expr $cz*$cz*2-1]
	set c $y
	set x [expr $e*$cy]
	set y [expr ((($sy*$sy*4-3)*($e+$e-1)*$cz*$d/6.0+$x)*$d/4.0-$cz)* \
		    $sy*$d+$tu]
	if { abs($y-$c) <= 5e-14 } { break }
    }
    set baz [expr $cu*$cy*$cf-$su*$sy]
    set c [expr $r*sqrt($sa*$sa+$baz*$baz)]
    set d [expr $su*$cy+$cu*$sy*$cf]
    set phi2 [expr atan2($d,$c)]
    set c [expr $cu*$cy-$su*$sy*$cf]
    set x [expr atan2($sy*$sf,$c)]
    set c [expr ((-3*$c2a+4)*$f+4)*$c2a*$f/16.0]
    set d [expr (($e*$cy*$c+$cz)*$sy*$c+$y)*$sa]
    set lam2 [expr $lam+$x-(1-$c)*$d*$f]
    # set baz [expr atan2($sa,$baz)+3.14159265358979323846]

    set lat1 [expr $phi2*57.29577951308232087684]
    set long1 [expr $lam2*57.29577951308232087684]
    return [list $lat1 $long1]
}

### area

proc ProjectedArea {wpixs} {
    # compute area of polygon whose boundary is the polyline formed by the
    #  WPs in given list
    # polygon cannot be self-intersecting (no test on this!)
    # return value in square km
    global WPPosn WPDatum ASKPROJPARAMS

    set ix [lindex $wpixs 0]
    set datum $WPDatum($ix) ; set p $WPPosn($ix)
    set ps [list [list [set lat [lindex $p 0]] [lindex $p 1] $datum]]
    foreach ix [lreplace $wpixs 0 0] {
	set dt $WPDatum($ix) ; set p $WPPosn($ix)
	if { $dt != $datum } {
	    set p [ToDatum [lindex $p 0] [lindex $p 1] $dt $datum]
	}
	lappend ps [list [lindex $p 0] [lindex $p 1] $datum]
    }
    if { $lat>=-80 && $lat<=84 } {
	set proj TM
    } else {
	# in fact this will use UPS
	set proj UTM
    }
    set ask $ASKPROJPARAMS
    set ASKPROJPARAMS 0
    set xy [ProjInit $proj AProj $datum $ps]
    set ASKPROJPARAMS $ask
    set xmin [set x0 [lindex $xy 0]] ; set ymin [set y0 [lindex $xy 1]]
    set xs "" ; set ys ""
    foreach p [lreplace $ps 0 0] {
	set xy [eval Proj${proj}Point AProj $p]
	set x [lindex $xy 0] ; set y [lindex $xy 1]
	lappend xs $x ; lappend ys $y
	if { $x < $xmin } { set xmin $x }
	if { $y < $ymin } { set ymin $y }
    }
    set sum 0
    set x0 [set x [expr $x0-$xmin]] ; set y0 [set y [expr $y0-$ymin]]
    foreach x1 $xs y1 $ys {
	set x1 [expr $x1-$xmin] ; set y1 [expr $y1-$ymin]
	set sum [expr $sum+$x*$y1-$x1*$y]
	set x $x1 ; set y $y1
    }
    set sum [expr $sum+$x*$y0-$x0*$y]
    return [expr abs(0.5e-6*$sum)]
}

proc SphericalArea {wpixs} {
    # compute area of polygon whose boundary is the polyline formed by the
    #  WPs in given list
    # computation is based on a spherical approximation
    # return negative value if there are precision errors
    #  otherwise value in square km
    global WPPosn WPDatum

    set ix [lindex $wpixs 0]
    set datum $WPDatum($ix) ; set p [set p0 $WPPosn($ix)]
    set phi0 [set phi [expr [lindex $p 0]*0.01745329251994329576]]
    set lam0 [set lam [expr [lindex $p 1]*0.01745329251994329576]]
    set c0 [expr cos($phi0)]
    # computation of mean radius of the ellipsoid at a point
    #  as described in Hrvoje Lukatela, "Ellipsoidal Area Computations of
    #  Large Terrestrial Objects", http://www.geodyssey.com/papers/ggelare.html
    set dt [EllipsdData $datum]
    set a [lindex $dt 0] ; set b [expr $a*(1-[lindex $dt 1])]
    set s [expr sin($phi)]
    set a2 [expr $a*$a]
    # radius in km
    set r [expr 1e-3*$a2*$b/($a2*$c0*$c0+$b*$b*$s*$s)]
    # computation of area adapted from sph_poly.c in
    #  "Graphics Gems IV", edited by Paul Heckbert, Academic Press, 1994.
    set srad 0
    SetDatumData $datum
    foreach ixn [lreplace $wpixs 0 0] {
	set pos $WPPosn($ixn)
	if { $WPDatum($ixn) != $datum } {
	    set pos [ToDatum [lindex $pos 0] [lindex $pos 1] \
		    $WPDatum($ixn) $datum]
	}
	set phi1 [expr [lindex $pos 0]*0.01745329251994329576]
	set lam1 [expr [lindex $pos 1]*0.01745329251994329576]
	set c1 [expr cos($phi1)]
	if { $lam0 != $lam1 } {
	    set HavA [expr ((1-cos($phi1-$phi0))/2.0)+ \
		           ((1-cos($lam1-$lam0))/2.0)*$c0*$c1]
	    set A [expr 2*asin(sqrt($HavA))]
	    set B [expr 1.5707963267948966192313-$phi1]
	    set C [expr 1.5707963267948966192313-$phi0]
	    set S [expr 0.5*($A+$B+$C)]
            set T [expr tan($S/2.0)*tan(($S-$A)/2.0)* \
		    tan(($S-$B)/2.0)*tan(($S-$C)/2.0)]
	    if { abs($T) < 1e-8 } { return -1 }
	    set E [expr abs(4*atan(sqrt(abs($T))))]
	    if { $lam1 < $lam0 } { set E [expr -$E] }
	    set srad [expr $srad+$E]
	}
	set phi0 $phi1 ; set lam0 $lam1 ; set c0 $c1
    }
    set lam1 $lam
    if { $lam0 != $lam1 } {
	set phi1 $phi ; set c1 [expr cos($lam1)]
	set HavA [expr ((1-cos($phi1-$phi0))/2.0)+ \
		           ((1-cos($lam1-$lam0))/2.0)*$c0*$c1]
	set A [expr 2*asin(sqrt($HavA))]
	set B [expr 1.5707963267948966192313-$phi1]
	set C [expr 1.5707963267948966192313-$phi0]
	set S [expr 0.5*($A+$B+$C)]
	set T [expr tan($S/2.0)*tan(($S-$A)/2.0)* \
		    tan(($S-$B)/2.0)*tan(($S-$C)/2.0)]
	if { abs($T) < 1e-8 } { return -1 }
        set E [expr abs(4*atan(sqrt(abs($T))))]
	if { $lam1 < $lam0 } { set E [expr -$E] }
	set srad [expr $srad+$E]
    }
    return [expr abs($r*$r*$srad)]
}

### datums

proc ChangeTPsDatum {tps datum1 datum2} {
    # convert position of TR points on list $tps from $datum1 to $datum2
    # change only the first 4 elements (position) of each TP representation
    # this depends on DataIndex (see main.tcl, Storage), not used here
    #  for speed

    if { $datum1 == $datum2 } { return $tps }
    set l ""
    foreach tp $tps {
	set np [lindex [FormatPosition [lindex $tp 0] [lindex $tp 1] \
			    $datum1 DMS $datum2] 0]
	lappend l [concat $np [lrange $tp 4 end]]
    }
    return $l
}

proc ChangeLPsDatum {lps odatum datum pformt} {
    # change datum of LN list of points
    # change only the first element (position) of each LP representation
    # this depends on DataIndex (see main.tcl, Storage), not used here
    #  for speed
    # return -1 if the current position format requires a fixed datum
    #  different from $datum
    global MESS POSTYPE

    if { $odatum == $datum } { return $lps }
    if { [set gd [BadDatumFor $pformt $datum Ignore]] != 0 } {
	GMMessage [format $MESS(cantchggriddatum) $gdatum]
	return -1
    }
    set l ""
    foreach lp $lps {
	foreach "latd longd" [lindex $lp 0] { break }
	lappend l [lreplace $lp 0 0 \
		       [lindex [FormatPosition $latd $longd $odatum \
				    $pformt $datum] 0]]
    }
    return $l
}

### altitude

proc UserAltitude {altlst} {
    # return altitude in user units from internal representation
    global AltUnit ALSCALE

    if { $altlst == "" } { return "" }
    if { $AltUnit == "M" } { return [lindex $altlst 0] }
    if { $AltUnit == [lindex $altlst 2] } { return [lindex $altlst 1] }
    return [expr [lindex $altlst 0]/$ALSCALE]
}

proc AltitudeList {a} {
    # check value for altitude and return internal representation as list
    # return nil on error
    global AltUnit ALSCALE

    if { $a == "" } { return "" }
    if { ! [CheckSignedFloat Ignore $a] } { return nil }
    if { $AltUnit == "M" } { return $a }
    return [list [expr $a*$ALSCALE] $a $AltUnit]
}

### dates

proc Today {dformt} {
    # build representation of current date under format $dformt

    set dt [clock format [clock seconds] -format "%Y %m %d %H %M %S"]
    scan $dt %d%0d%0d%0d%0d%0d y m d h mn s
    return [FormatDate $dformt $y $m $d $h $mn $s]
}

proc TodayUTC {dformt} {
    # VR contribution
    # build representation of current date under format $dformt

    set dt [clock format [clock seconds] -format "%Y %m %d %H %M %S"]
    scan $dt %d%0d%0d%0d%0d%0d y m d h mn s
    return [eval FormatDate $dformt [LocalTimeAndUTC $y $m $d $h $mn $s Local]]
}

proc FormatDay {dformt y m d} {
    # build representation of date (without time of day) under format $dformt
    # (see proc FormatDate for date with time of day)
    #  $dformt in
    #   {YYYYMMDD, YYYY-MM-DD, MMDDYYYY, DDMMMYYYY, YYYY/MM/DD, ISO8601}
    # changes in proc FormatDate are likely to affect this proc!

    set cdate [FormatDate $dformt $y $m $d 0 0 0]
    if { $dformt == "YYYY/MM/DD" } {
	return [string range $cdate 0 9]
    }
    return [lindex $cdate 0]
}

proc FormatDate {dformt y m d h mn s} {
    # build representation of date (including time of day) under format $dformt
    # (see proc FormatDay for date without time of day)
    #  $dformt either in
    #   {YYYYMMDD, YYYY-MM-DD, MMDDYYYY, DDMMMYYYY, YYYY/MM/DD, ISO8601}
    #   or DDMMYY in which case the date will have no time of day
    # changes here must be reflected in proc FormatDay and in proc ScanDate
    # when adding new formats here, array DATEW must be updated in main.tcl
    global MONTHNAMES

    # make sure there are no leading 0's
    foreach v "y m d h mn s" {
	scan [set $v] %0d $v
    }
    set h [format "%02d:%02d:%02d" $h $mn $s]
    switch $dformt {
	DDMMYY {
	    # MGM contribution
	    return [format "%02d%02d%02d" $d $m [expr $y % 100]]
	}
	ISO8601 {
	    # VR contribution
	    return [format "%4d-%02d-%02dT%sZ" $y $m $d $h]
	}
    	YYYYMMDD { return [format "%4d.%02d.%02d %s" $y $m $d $h] }
	YYYY-MM-DD { return [format "%4d-%02d-%02d %s" $y $m $d $h] }
	MMDDYYYY { return [format "%02d/%02d/%4d %s" $m $d $y $h] }
	DDMMMYYYY {
	    incr m -1 ; set m [lindex $MONTHNAMES $m]
	    return [format "%02d-%s-%4d %s" $d $m $y $h]
	}
	YYYY/MM/DD { return [format "%4d/%02d/%02d-%s" $y $m $d $h] }
    }
}

proc FormatTime {secs args} {
    # build represention of seconds in hours:minutes:seconds format
    #  $secs may be a float or an integer
    # if $args!="" it gives the number of decimal places for the seconds field
    #  that defaults to 0 with $secs rounded to integer

    if { $args != "" && $args > 0 } {
	set fss "%02.${args}f" ; set fss0 "%8.${args}f"
    } else {
	set secs [expr round($secs)]
	set fss "%02d" ; set fss0 "%8d"
    }
    set s0 [expr int($secs)]
    set s [expr $s0%60] ; set x [expr ($s0-$s)/60]
    set s [expr $s+$secs-$s0]
    set mn [expr $x%60] ; set h [expr ($x-$mn)/60]
    if { $h > 0 } { return [format "%2d:%02d:$fss" $h $mn $s] }
    if { $mn > 0 } { return [format "%5d:$fss" $mn $s] }
    return [format $fss0 $s]
}

proc TimeToSecs {time} {
    # convert represention of seconds in hours:minutes:seconds format
    #  to seconds (hours and minutes fields may be missing)
    # return integer or float according to seconds field

    set n [llength [set l [split $time ":"]]]
    set s [lindex $l end]
    if { [string first 0 $s] == 0 } { set s [string range $s 1 end] }
    if { $n == 1 } { return $s }
    set ll ""
    foreach f $l {
	scan $f %0d f
	lappend ll $f
    }
    scan [lindex $l 0] %0d f
    if { $n == 2 } {
	return [expr $s+60*$f]
    }
    scan [lindex $l 1] %0d g
    return [expr $s+60*$g+3600*$f]
}

proc NowTZ {} {
    # current date under default format with time zone appended
    global DateFormat
    
    return "[Today $DateFormat] ([clock format 0 -format %Z])"
}

proc Now {} {
    # current date under default format
    global DateFormat
    
    return [Today $DateFormat]
}

array set DAYSOF {
     1 31   2 28   3 31  4 30  5 31  6 30  7 31  8 31  9 30
    10 31  11 30  12 31
}

proc ScanDate {date} {
    # convert date string to a list with year, month (in 1..12), day,
    #  hours, minutes and seconds
    # if there is time zone information, return local time
    # return empty list on error
    # see proc FormatDate for possible date formats
    # changes in proc FormatDate are likely to affect this proc!

    set date [string trim $date]
    if { [regexp {^([0-9][0-9])([0-9][0-9])([0-9][0-9])$} $date \
	      x d m y] } {
	# DDMMYY, no time of day
	#  YY taken as 2000+YY if YY<70, and 1900+YY otherwise
	foreach v "d m y" {
	    scan [set $v] %02d $v
	}
	if { $y < 70 } {
	    incr y 2000
	} else { incr y 1900 }
	return [list $y $m $d 0 0 0]
    }
    set goodn -1
    if { [regexp {^ *([-0-9]+)T([0-9:.]+)([^ ]+) *$} $date m ymd hms tail] } {
	# coping with ISO8601 format as in:
	#   2000-01-01T12:00:00Z
	#   2000-01-01T12:00:00.00000Z
	#   2004-06-07T13:51:27.2515650-07:00
	set n 1
	if { [scan $hms %02d:%02d:%0f h mn s] == 3 && \
		 [scan $ymd %4d-%02d-%02d y m d] == 3 } {
	    set s [expr round($s)]
	    if { [scan $tail %03d:%02d hz mz] == 2 } {
		set goodn 1
		# compute UTC date
		if { $hz > 0 } { set mz [expr -$mz] }
		set hz [expr -$hz]
		if { [incr mn $mz] > 60 } {
		    incr h ; set mn [expr $mn-60]
		} elseif { $mn < 0 } {
		    incr h -1 ; incr mn 60
		}
		if { [incr h $hz] > 23 } {
		    incr h -24
		    foreach "y m d" [NextDay $y $m $d] {}
		} elseif { $h < 0 } {
		    incr h 24
		    foreach "y m d" [PreviousDay $y $m $d] {}
		}
		# convert to local date
		foreach "y m d h mn s" \
		    [LocalTimeAndUTC $y $m $d $h $mn $s UTC] {}
	    } elseif { $tail == "Z" } { set goodn 1 }
	}
    } else {
	set l [split $date -] ; set n [llength $l]
	if { $n == 1 } {
	    # no "-"s
	    set l [split $date /] ; set n [llength $l]
	    if { $n == 1 } {
		# neither "-"s nor "/"s
		set l [split $date .] ; set n [llength $l]
		if { $n == 3 } {
		    # YYYY.MM.DD HH:MM:SS
		    set y [lindex $l 0]
		    set m [string trimleft [lindex $l 1] 0]
		    set n [scan [lindex $l 2] "%0d %0d:%0d:%0d" d h mn s]
		    set goodn 4
		}
	    } elseif { $n == 3 } {
		# MM/DD/YYYY HH:MM:SS
		set m [string trimleft [lindex $l 0] 0]
		set d [string trimleft [lindex $l 1] 0]
		set n [scan [lindex $l 2] "%0d %0d:%0d:%0d" y h mn s]
		set goodn 4
	    }
	} elseif { $n == 3 } {
	    set mt [lindex $l 1]
	    if { [set m [Month $mt]] } {
		# DD-MMM-YYYY HH:MM:SS
		set d [string trimleft [lindex $l 0] 0]
		set n [scan [lindex $l 2] "%d %0d:%0d:%0d" y h mn s]
		set goodn 4
	    } elseif { [scan $mt %0d m] } {
		# YYYY-MM-DD HH:MM:SS
		set y [lindex $l 0]
		set n [scan [lindex $l 2] "%0d %0d:%0d:%0d" d h mn s]
		set goodn 4
	    }
	} elseif { $n == 2 } {
	    # YYYY/MM/DD-HH:MM:SS
	    set n [scan [lindex $l 0] "%0d/%0d/%0d" y m d]
	    incr n [scan [lindex $l 1] "%0d:%0d:%0d" h mn s]
	    set goodn 6
	}
    }
    if { $n == $goodn && [CheckDateEls $y $m $d $h $mn $s] } {
	return [list $y $m $d $h $mn $s]
    }
    return ""
}

proc DateToSecs {y m d h mn s} {
    # convert date to seconds ellapsed since beginning of $YEAR0, not
    #  necessarily a leap year
    global YEAR0

    return [DateToSecsFrom $y $m $d $h $mn $s $YEAR0]
}

proc DateToSecsFrom {y m d h mn s year0} {
    # convert date to seconds ellapsed since beginning of $year0, not
    #  necessarily a leap year
    global DAYSOF

    # make sure there are no leading 0's
    foreach v "y m d h mn s" {
	scan [set $v] %0d $v
    }
    # days in current month and in whole years except February 29ths
    set days [expr 365*($y-$year0)+$d-1]
    set yy [expr $year0+$year0%4]
    while { $yy < $y } {
	if { $yy%100!=0 || $yy%400==0 } { incr days }
	incr yy 4
    }
    # add days in whole months of current year
    if { $m>2 && $y%4==0 && ($y%100!=0 || $y%400==0) } { incr days }
    incr m -1
    while { $m > 0 } { incr days $DAYSOF($m) ; incr m -1 }
    return [expr $days*86400+$h*3600+$mn*60+$s]
}

proc DateFromSecs {secs} {
    # build date from seconds ellapsed since beginning of $YEAR0, not
    #  necessarily a leap year, using default format
    global DateFormat

    return [eval FormatDate $DateFormat [DateIntsFromSecs $secs]]
}

proc DateIntsFromSecs {secs} {
    # compute date from seconds ellapsed since beginning of $YEAR0,
    #  not necessarily a leap year
    # return list with year, month, day, hour, minutes, seconds as integers
    global DAYSOF YEAR0

    set s [expr $secs%60] ; set x [expr ($secs-$s)/60]
    set mn [expr $x%60] ; set x [expr ($x-$mn)/60]
    set h [expr $x%24] ; set x [expr ($x-$h)/24]
    set y [expr int($x/365)] ; set yd [expr $y*365]
    set yy [expr $YEAR0+$YEAR0%4] ; incr y $YEAR0
    while { $yy < $y } {
	if { $yy%100!=0 || $yy%400==0 } { incr yd }
	incr yy 4
    }
    if { $yd > $x } {
	incr y -1
	if { $y%4==0 && ($y%100!=0 || $y%400==0) } { incr yd -1 }
	incr yd -365
    }
    incr x [expr 1-$yd] ; set m 1
    while { $x > $DAYSOF($m) } {
	if { $m==2 && $y%4==0 && ($y%100!=0 || $y%400==0) } {
	    if { $x == 29 } { break }
	    incr x -1
	}
	incr x -$DAYSOF($m) ; incr m
    }
    return [list $y $m $x $h $mn $s]
}

proc DateFromSecsFmt {secs fmt} {
    # build date from seconds ellapsed since beginning of $YEAR0, not
    #  necessarily a leap year, using given format
    global DAYSOF YEAR0

    set s [expr $secs%60] ; set x [expr ($secs-$s)/60]
    set mn [expr $x%60] ; set x [expr ($x-$mn)/60]
    set h [expr $x%24] ; set x [expr ($x-$h)/24]
    set y [expr int($x/365)] ; set yd [expr $y*365]
    set yy [expr $YEAR0+$YEAR0%4] ; incr y $YEAR0
    while { $yy < $y } {
	if { $yy%100!=0 || $yy%400==0 } { incr yd }
	incr yy 4
    }
    if { $yd > $x } {
	incr y -1
	if { $y%4==0 && ($y%100!=0 || $y%400==0) } { incr yd -1 }
	incr yd -365
    }
    incr x [expr 1-$yd] ; set m 1
    while { $x > $DAYSOF($m) } {
	if { $m==2 && $y%4==0 && ($y%100!=0 || $y%400==0) } {
	    if { $x == 29 } { break }
	    incr x -1
	}
	incr x -$DAYSOF($m) ; incr m
    }
    return [FormatDate $fmt $y $m $x $h $mn $s]
}

proc NextDay {y m d} {
    # return list with year, month and day for the day following the
    #  given date
    global DAYSOF

    if { [incr d] > $DAYSOF($m) } {
	if { $m==2 && $y%4==0 && ($y%100!=0 || $y%400==0) } {
	    if { $d != 29 } {
		set d 1 ; set m 3
	    }
	} else {
	    set d 1
	    if { [incr m] == 13 } {
		incr y ; set m 1
	    }
	}
    }
    return [list $y $m $d]
}

proc PreviousDay {y m d} {
    # return list with year, month and day for the day before the
    #  given date
    global DAYSOF

    if { [incr d -1] == 0 } {
	if { [incr m -1] == 0 } {
	    set m 12 ; incr y -1
	}
	if { $m==2 && $y%4==0 && ($y%100!=0 || $y%400==0) } {
	    set d 29
	} else {
	    set d $DAYSOF($m)
	}
    }
    return [list $y $m $d]
}

proc Month {name} {
    # from month name to 0 (error) or 1..12
    global ALLMONTH

    for {set m 1} { $m<13 } { incr m } {
	if { [lsearch -exact $ALLMONTH($m) $name] != -1 } { return $m }
    }
    return 0
}

proc LocalTimeAndUTC {y m d h mn s origin} {
    # convert from/to UTC to/from local time
    #  $origin =="UTC"  converts from UTC
    #          =="local"  converts to UTC
    # return list with same elements as in the args
    global TimeOffset DAYSOF

    if { $origin == "UTC" } {
	incr h $TimeOffset
    } else { set h [expr $h-$TimeOffset] }
    if { $h < 0 } {
	incr h 24
	if { [incr d -1] == 0 } {
	    if { [incr m -1] == 0 } {
		set m 12 ; set d 31 ; incr y -1
	    } else {
		if { $m == 2 && $y%4 == 0 && \
			($y%100 != 0 || $y%400 == 0) } {
		    set d 29
		} else {
		    set d $DAYSOF($m)
		}
	    }
	}
    } elseif { $h > 23 } {
	incr h -24
	if { [incr d] > $DAYSOF($m) } {
	    if { $m == 2 } {
		if { $y%4 != 0 || ($y%100 == 0 && $y%400 != 0) } {
		    set m 3 ; set d 1
		}
	    } elseif { $m == 12 } {
		set m 1 ; set d 1 ; incr y
	    } else {
		incr m ; set d 1
	    }
	}
    }
    return [list $y $m $d $h $mn $s]
}

proc CompatibleDates {y0 m0 d0 secs1} {
    # check whether two dates are compatible
    #  $y0, $m0, $d0 give one date (year, month name and day) and may
    #  be * (to match anything)
    #  $secs1 is the other date in seconds since $YEAR0
    # return 1 if compatible

    if { $m0 != "*" && [set m0 [Month $m0]] == 0 } {
	BUG calling CompatibleDates with bad month name
    }
    foreach "y1 m1 d1 h1 mn1 s1" [DateIntsFromSecs $secs1] {}
    if { [string match $d0-$m0-$y0 $d1-$m1-$y1] } { return 1 }
    return 0
}

### varia

proc VectorBearing {vx vy} {
    # compute bearing (angle from y-axis clockwise, in 0..359) of vector
    #  with given components
    # return "_" if components are too small

    if { abs($vy)+abs($vx) > 1e-20 } {
	set b [expr round(atan2($vx,$vy)*57.29577951308232087684)]
	if { $b < 0 } { incr b 360 }
    } else { set b "_" }
    return $b
}


