© 2011, Sean B. Palmer
Licensed under the Apache License 2.0

This is a CoffeeScript program used in my Coordinate Conversion page. This converts equatorial coordinates, which give the positions of celestial objects, to horizontal coordinates, which give the positions of things in the local sky relative to an observer. You can get the source and the compiled versions:

The Javascript file requires jQuery and the Google Maps API to run. If you're using the CoffeeScript file instead, that also needs coffee-script.js to run.

The greenwichSiderealTime function just gives us the current Greenwich Mean Sidereal Time. We need this to be able to calculate the Local Sidereal Time, and we need that to convert RA and to an Hour Angle. All very simple. The magic values are from a Wikipedia equation.

greenwichSiderealTime = ->
   now = new Date
   seconds = (now.getTime() / 1000) - 946728000
   D = seconds / (24 * 3600)
   GMST = 18.697374558 + 24.06570982441908 * D
   GMST % 24

Takes a longitude in degrees, and gives the Local Sidereal Time. According to the USNO, this is just the observer's longitude in hours added to the Greenwich Mean Sidereal Time. To get hours from degrees, we just divide by fifteen.

localSiderealTime = (longitude) ->
   hours = longitude / 15
   greenwichSiderealTime() + hours

updateLocation is called when the Google Map which we show the user to select their location gets clicked on. The event gets passed to this function, and we pull out the latitude and longitude and store them in some form input fields. We also then make sure that the rest of the values are kept in sync with these.

updateLocation = (location) ->
   $('#lat').val round(, 5)
   $('#lon').val round(location.lng(), 5)

These are just helper functions.

degrees = (n) -> n * 180 / Math.PI
radians = (n) -> n * Math.PI / 180
square = (n) -> n * n
round = (n, x) -> Math.round(n * Math.pow(10, x)) / Math.pow(10, x)
{sin, cos} = Math

polar converts from polar to rectangular coordinates. I was inspired by some ruby code that I found on the web. Apparently we do this because, says Wikipedia, it is best “not to use the functions arcsin and arccos when possible, because of their limited 180° only range, and also because of the low accuracy the former gets around ±90° and the latter around 0° and 180°”.

polar = (x, y) ->
   r = Math.sqrt square(x) + square(y)
   t = Math.atan2(y, x)
   [r, degrees(t)]

This does the actual mathematical work of converting the equatorial coordinates, expressed as latitude, declination, and hour angle arguments all in degrees, to horizontal coordinates. The algorithm is one described by Montenbruck and Pfleger.

equatorialToHorizontal = (lat, dec, ha) ->
   A = (sin(lat) * sin(dec)) + (cos(lat) * cos(dec) * cos(ha))
   B = (cos(lat) * sin(dec)) - (sin(lat) * cos(dec) * cos(ha))
   C = - (cos(dec) * sin(ha))

One calculation gives us the azimuth, and the next one gives us the altitude. We add 360 to the angle and modulo 360 because sometimes the angle comes out as a negative value.

   [radius1, angle1] = polar(B, C)
   direction = (angle1 + 360) % 360

   [radius2, angle2] = polar(radius1, A)
   [direction, angle2, radius2]

updateValues does the work of converting the input values, which are represented by form fields, to the output values, represented by the table cells so that they can't be edited making them look like input values.

updateValues = ->

Update the Local Sidereal Hour from the longitude, and the Hour Angle from the Local Sidereal Hour and RA.

   $('#hour').val round(localSiderealTime($('#lon').val()), 5)
   $('.ha').text round($('#hour').val() - $('#ra').val(), 3)

Get the latitude, declination, and Hour Angle from the calculated form field values. We need only these to convert to the horizontal coordinates.

   lat = radians($('#lat').val())
   dec = radians($('#dec').val())
   ha = radians($('.ha').text() * 15)

The horizontal coordinates are returned along with a radius from the polar conversion, which helps us to check that we did the calculation correctly. The radius should be 1, and if it's not then there's a bug.

   [az, alt, radius] = equatorialToHorizontal lat, dec, ha
   $('.azimuth').text round(az, 3)
   $('.altitude').text round(alt, 3)

Do the little bugcheck, and scare the user if we messed up.

   if round(radius, 3) != 1
   then $('.error').text 'There was an error!'

Create a Google Map. This uses v3 of the API, which is actually quite sensible. It's literally five lines of code in CoffeeScript if you just want to display a map. You have to use the ID map_canvas, by the way, apparently. The marker variable is used to store the only marker that we place down.

createGoogleMap = ->
   marker = null

   map = new google.maps.Map $('#map_canvas')[0], 
      center: new google.maps.LatLng(52,0)
      mapTypeId: google.maps.MapTypeId.ROADMAP
      zoom: 5

Here we make a listener so that when somebody clicks on the map, we can draw a marker on there and recentre the map there. When the marker is drawn, we pass the position to the updateLocation function so that it can populate and recalculate the values. Got hints on this from Heavens Above, and the Maps API documentation (Overlays, Markers).

   google.maps.event.addListener map, 'click', (e) ->
     marker?.setMap null
     marker = new google.maps.Marker
         position: e.latLng
         map: map
     map.setCenter e.latLng
     updateLocation e.latLng

This is the jQuery initialisation function. When the document loads we just create the Google Map and the event handler that it has, and then make sure that all of the controls have change event callbacks so that everything is kept in sync.

$ ->

The #update element is just an HTML5 button. I looked at the jQuery Docs to check button support.

   $('#update').click updateValues

These are the form controls. I don't really know what all the events do, for example change seems to work very strangely, so I figured I'd just bind them all. Super programming, I know.

   $('input').change updateValues
   $('input').focus updateValues
   $('input').blur updateValues

Don't do this!
$('').load ' div#container'