Generating astrotags for Flickr photos

In December, I talked at London Web Standards about tagging astronomy photos with position and name information. I mentioned that around 400 photos have been tagged already on Flickr but this is only a tiny fraction of the 4,900 photos that have been solved by It would be great if the remaining 4,500 photos could also be tagged, and it ought to be straightforward to generate tags for those photos too. Inspired by the iNaturalist Taxonomic Tagging Tool, I’ve written a little astrotagging form for Flickr photos.

When the robot solves a photo on Flickr, it leaves a comment identifying the coordinates of the photo and listing the names of objects in the field.

Hello, this is the blind astrometry solver. Your results are:
(RA, Dec) center:(82.4668973542, 6.33857270637) degrees
(RA, Dec) center (H:M:S, D:M:S):(05:29:52.055, +6:20:18.862)
Orientation:161.45 deg E of N

Pixel scale:67.93 arcsec/pixel

Parity:Reverse (“Left-handed”)
Field size :53.14 x 39.85 degrees

Your field contains:
The star Rigel (βOri)
The star Betelgeuse (αOri)
The star Aldebaran (αTau)
The star Bellatrix (γOri)
The star Alnilam (εOri)
The star Alhena (γGem)
The star Alnitak (ζOri)
The star Saiph (κOri)
The star Mintaka (δOri)
The star Cursa (βEri)
IC 2118 / IC 2118 / Witch Head nebula
NGC 1976 / NGC 1976 / Great Nebula in Orion / M 42
NGC 1990 / NGC 1990
IC 434 / IC 434 / Horsehead nebula
IC 443 / IC 443
NGC 2264 / NGC 2264 / Christmas Tree cluster / Cone nebula

View in World Wide Telescope

If you would like to have other images solved, please submit them to the astrometry group.
Posted 3 weeks ago. ( permalink | delete )

These comments are always in the same format, so it’s straightforward to parse them and extract the astrometry metadata as a list of tags. I’ve written a small form which does this, using YQL to grab the comments from a Flickr photo then parsing them using standard DOM traversal and manipulation methods.

If you have a photo which has been solved, generating tags is straightforward. Copy the address of a Flickr photo page into the tagging form and press the big blue ‘Get astrotags’ button. The script should find the comment from and print out the tags for celestial coordinates and names, which you can then paste into the ‘Add a tag’ form on Flickr.

The code to do this is fairly simple, and reproduced below. After initialising the page, we can take advantage of YQL’s HTML parser to fetch all of the comments for a Flickr photo page by selecting all paragraphs inside divs with a class of ‘comment-content’ at that URL.

select * from html where url='' and xpath='//div[@class="comment-content"]/p'

We then loop through the results of this query, looking for paragraphs which contain the text ‘blind astrometry solver’. If we have a match, we add this paragraph to the DOM so we can parse it with standard DOM methods. The code then loops through the child nodes of the comment paragraph, running regular expression matches against any text nodes it finds to extract the coordinates of the photo.

Names are slightly more tricky. For those, we grab every line of text between ‘Your field contains:’ and the line ‘—–‘ above the signature, strip out whitespace, split each line on ‘/’ to get individual names and store these in an associative array, keyed on name to remove duplicates. That done, we can then just loop through the arrays of coordinates and names and print them out.

Here’s the full code:

 var url = "";
 var comment_holder;
 var position_output;
 var names_output;
 function init() {
   var url_input = document.getElementById('photoURL');
   var url_button = document.getElementById('updateURL');
   comment_holder = document.getElementById('comment');
   position_output = document.getElementById('position');
   names_output = document.getElementById('names');
   url_input.disabled = false;
   url_input.value = url;
   url_button.disabled = false;
   position_output.disabled = false;
   names_output.disabled = false;
   addEvent(url_button, 'click', function(e) {
     return false;
   addEvent(url_input, 'focus', function(e) {;
   addEvent(position_output, 'focus', function(e) {;
   addEvent(names_output, 'focus', function(e) {;
   // Mark up nodes which this script updates as
   // ARIA live regions.
   comment_holder.setAttribute('aria-live', 'polite');
   position_output.setAttribute('aria-live', 'polite');
   names_output.setAttribute('aria-live', 'polite');

 function getFlickrPhotoComments(url) {

   // YQL query to get all comments from a Flickr photo page.
   var yql = "select * from html where url='"+url+"' and xpath='//div[@class=\"comment-content\"]/p'";
   var yql_url = ''+escape(yql)+'&format=xml&callback=getAstrometryComment&diagnostics=false';
   position_output.value = '';
   names_output.value = '';
   comment_holder.innerHTML = 'Looking up '+url;
 function makeYQLRequest(yql_url) {
   var script=document.getElementById('yqlscript');
   var newscript=document.createElement('script');
   newscript.type = 'text/javascript';

function getAstrometryComment(data) {
  var results = data.results;
  var comment = 'Sorry, that photo has not been solved by <a href=""></a>.';
  for (var i in results) {
    var text = results[i];
    // Comments left by the solver contain the text 'blind astrometry solver'.
    if(text.match(/blind astrometry solver/gi)) {
      comment = text;

function parseComment(comment) {
  var astro = {};
  var names = {};
  var parsing_names = false;
  comment_holder.innerHTML = comment;
  var children = comment_holder.firstChild.childNodes;
  for (var i in children) {
    var child = children[i];
    var text = '';
    text =;
    if (text) {
      if (text.match(/(RA, Dec)/g) && text.match(/degrees/g)) {
        astro.RA = text[0];
        astro.Dec = text[1];
      } else if (text.match(/Orientation/g)) {
        text = text.match(/[-0-9\.]+/g);
        astro.orientation = text[0];
      } else if (text.match(/Pixel scale/g)) {
        text = text.match(/[0-9\.]+/g);
        astro.pixelScale = text[0];
      } else if(text.match(/Field size/g)) {
        text = text.match(/[0-9\.]+ x [0-9\.]+ (degrees|arcminutes|arcseconds)/g);
        astro.fieldsize = text[0];
      } else if(text.match(/Your field contains:/g)) {
        parsing_names = true;
      } else if (text.match(/-----/g)) {
        parsing_names = false;

      if (parsing_names) {
        names = addNames(names, text);

  if (astro.RA) {

function addNames(names, text) {
 text = trim(text);
 text = text.split('/');
 for (var j in text) {
   var name = text[j];
   name = trim(name);
   if (name && name !='Your field contains:'){
     names[name] = name;
 return names;

function renderPositionTags(astro) {
  position_output.value = '';
   for (var tag in astro) {
     position_output.value += 'astro:'+tag+'="'+astro[tag]+'" ';

function renderNameTags(names) {

  names_output.value = '';
   for (var name in names) {
     names_output.value += 'astro:name="'+name+'" '

function trim(text) {
  // Trim leading and trailing whitespace from a string.
  text = text.replace(/^\s+/, '');
  text = text.replace(/\s+$/,'');
  return text;

function addEvent(obj, evType, fn) {
  if (obj.addEventListener) {
    obj.addEventListener(evType, fn, false);
    return true;
  } else if (obj.attachEvent) {
    var r = obj.attachEvent("on" + evType, fn);
    return r;
  } else {
    return false;