Making a Genealogy Map Using Google Maps

Introduction

I wanted to try out using the Google Maps API for a project, so I decided I’d try to use it in conjunction with some genealogy data to see what I could come up with.

The result is a family tree which is mapped out spatially rather than chronologically; once rendered, the following visual data is revealed:

  • Migration paths - see where particular people suddenly moved a large distance
  • Geographical density/immobility - see which areas the most family members were from
  • Most popular places - see which villages or cities successive generations lived in

Example

Click on the image below to view a live example of the project (using real data from my family tree) and then read on for an explanation of how the program works:

Features

Google Maps features:

  • Pan and zoom around the map using AJAX technology
  • Switch to vector mapping, satellite mapping or hybrid maps

Program features:

  • Different sizes and colours of bubble markers show how many ancestors were from each place
  • Click any bubble marker to see who was born in a particular place
  • Choose whether or not to draw interconnecting lines
  • Draw interconnecting lines in one colour or use a different colour for each generation
  • Maps automatically centres itself on the epicentre of all known places of birth

The only feature that I wanted to include and wasn’t able to was for the map to automatically zoom in or out in order to include all places of birth in the starting view. The API only has a concept of zoom levels rather than scale, so this was not possible.

By way of compromise, it is possible to adjust the zoom level via a variable at the top of the program though.

How the Program Works

The entire program is written in Javascript and essentially runs off two classes, and two arrays of each class.

Firstly, the program has an array of places and co-ordinates. This allows the program to locate where people were born and mark the place on the map. Setting up this data is the most time consuming part of setting up the program as it requires locating each place in Google Earth and then pasting the co-ordinates into the array:

function Place(name, lon, lat) 
{   
    this._name  = name;
    this._lat   = lat;
    this._lon   = lon;
    this._drawn = 0;
}

Place.prototype._name;   // Description
Place.prototype._lat;    // N-S
Place.prototype._lon;    // E-W
Place.prototype._drawn;  // has been drawn (bool)

Place.prototype.getName   = function() { return this._name;  }
Place.prototype.getLat    = function() { return this._lat;   }
Place.prototype.getLon    = function() { return this._lon;   }
Place.prototype.getDrawn  = function() { return this._drawn; }

Place.prototype.setDrawn  = function() { this._drawn = 1; }
var places = new Array(
      new Place("Appleton Wiske", -1.39952  , 54.436  ),
      new Place("Barnsley"      , -1.48127  , 53.5529 ),
      new Place("Bentley"       , -1.14611  , 53.5487 ),
      new Place("Billingford"   , 0.984553  , 52.7431 ),
      new Place("Billingham"    , -1.29179  , 54.5944 ),
      new Place("Blackburn"     , -2.48471  , 53.7501 )
      );

Secondly, the program has an array of people, where and when they were born and their kekulé number (see below).

function Person(kekule, name, year, born) 
{   
    this._kekule = kekule;
    this._name   = name;
    this._year   = year;
    this._born   = born;
}

Person.prototype._kekule; // Kekule number
Person.prototype._name;   // Name
Person.prototype._year;   // Birth Year
Person.prototype._born;   // Born

Person.prototype.getKekule  = function() { return this._kekule; }
Person.prototype.getName    = function() { return this._name;   }
Person.prototype.getYear    = function() { return this._year;   }
Person.prototype.getBorn    = function() { return this._born;   }
var people = new Array(
      // gen1
      new Person(1 , "Ian Atkinson"         , 1982, "Garforth"   ),

      // gen2 
      new Person(2 , "(Living) Atkinson"    , 1956, "Carcroft"   ),
      new Person(3 , "(Living) Bowerman"    , 1958, "Barnsley"   ),

      // gen3
      new Person(4 , "Aubrey Atkinson"      , 1919, "New Marske" ),
      new Person(5 , "Elizabeth Davies"     , 1922, "Bentley"    ),
      new Person(6 , "(Living) Bowerman"    , 1928, "Wibsey"     ),
      new Person(7 , "(Living) Waterhouse"  , 1930, "Manningham" )
      );

With this data in place, the program simply loops through each place adding up how many people were born there and drawing the appropriate bubble.

It then loops through the people drawing in the interconnecting lines, if required, using the kekulé mathematics.

Kekulé Numbers

Kekulé numbering is a popular, mathematical data model used in genealogy. In a nut shell, the starting person (shown with a blue balloon on the map) is given number 1. That person’s father is number 2, their mother number 3. The third generation are numbers 4-7 and so on:

kekule numbering

With Kekulé numbering in place, given a person k the kekulé number of their father is always 2k and their mother is always 2k+1.

All males (excepting k=1) have even kekulé numbers, and all females odd kekulé numbers.

Code

Should you wish to use this program yourself you can download the necessary starter files here.

Note that you will have to generate a Google Maps Key for your own domain and change line 7 accordingly. It would then simply be a matter of populating the places and people arrays with your own data.

A full listing of the code is also presented here ( view this example starter map):

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
   "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml">
   <head>
      <meta http-equiv="content-type" content="text/html; charset=utf-8"/>
      <title>Ancestry Map</title>
      <script src="http://maps.google.com/maps?file=api&v=2&key=biglongkey"
         type="text/javascript"></script>
      <script type="text/javascript">

//<![CDATA[

function load()
{
   if (GBrowserIsCompatible()) 
   {
      // Javascript Ancestry Map using Google Maps API
      // by Ian Atkinson, March 2008
      
      // Vars

      var colourCounter = 0    ; // leave as is
      var lineThickness = 4    ; // change line thickness
      var lineOpacity   = 0.7  ; // change line opacity 0.0 - 1.0 (solid)
      var mapZoomLevel  = 8    ; // how zoomed in map is
      var rotateColours = 1    ; // bool for rainbow lines
      var drawLines     = 1    ; // bool for drawing interconnect lines

      // Place class ---------------------------

      function Place(name, lon, lat) 
      {   
          this._name  = name;
          this._lat   = lat;
          this._lon   = lon;
          this._drawn = 0;
      }

      Place.prototype._name;   // Description
      Place.prototype._lat;    // N-S
      Place.prototype._lon;    // E-W
      Place.prototype._drawn;  // has been drawn (bool)

      Place.prototype.getName   = function() { return this._name;  }
      Place.prototype.getLat    = function() { return this._lat;   }
      Place.prototype.getLon    = function() { return this._lon;   }
      Place.prototype.getDrawn  = function() { return this._drawn; }
      
      Place.prototype.setDrawn  = function() { this._drawn = 1; }

      // Person class --------------------------

      function Person(kekule, name, year, born) 
      {   
          this._kekule = kekule;
          this._name   = name;
          this._year   = year;
          this._born   = born;
      }

      Person.prototype._kekule; // Kekule number
      Person.prototype._name;   // Name
      Person.prototype._year;   // Birth Year
      Person.prototype._born;   // Born

      Person.prototype.getKekule  = function() { return this._kekule; }
      Person.prototype.getName    = function() { return this._name;   }
      Person.prototype.getYear    = function() { return this._year;   }
      Person.prototype.getBorn    = function() { return this._born;   }

      // create array of places to use

      var places = new Array(
         new Place("Appleton Wiske", -1.39952  , 54.436    ),
         new Place("Barnsley"      , -1.48127  , 53.5529   ),
         new Place("Bentley"       , -1.14611  , 53.5487   )
      );
      
      // create array of people to map

      var people = new Array(
         // gen1
         new Person(1 , "Person 1", 1960, "Appleton Wiske"),

         // gen2 
         new Person(2 , "Person 2", 1940, "Bentley"       ),
         new Person(3 , "Person 3", 1939, "Barnsley"      )
      );
      
      // create array of colours for lines
   
      var colours = new Array(
         "#000000", // black
         "#0000ff", // blue
         "#4B0082", // purple
         "#5E2612", // sepia
         "#330000"  // dark cherry
      );
      
      // create an array of power of two to hack around the problem
      // of how difficult it is to work out what generation a 
      // kekule is in!

      var powers = new Array(2, 4, 8, 16, 32, 64, 128, 256, 1024, 2048, 4096, 8192);
      var powerPos = 0;
   
      // functions
      
      function getCoord(axis, placeName)
      {      
         // Get a co-ordinate for a given place

         for (var a=0 ; a<places.length ; a++) 
         {
            if (places[a].getName() == placeName)
            {
               if      (axis == "lon")
                  return places[a].getLon();

               else if (axis == "lat")
                  return places[a].getLat();
            }
         }
      
         alert(placeName + " not found in list!");
         return 0;
      }

      function getMeanLat() 
      {   
         // Get mean latitude over places data

         var lat = 0;
         for (i=0 ; i<places.length ; i++) lat += places[i].getLat();
         return lat/places.length;
      }
      
      function getMeanLon()
      {   
         // Get mean longitude over places data

         var lon = 0;
         for (i=0 ; i<places.length ; i++) lon += places[i].getLon();
         return lon/places.length;
      }
      
      function beenDrawn(p)
      {
         // has a place been drawn
         
         for (var a=0 ; a < places.length ; a++)
         {
            if (places[a].getName() == p)
            {
               if (places[a].getDrawn() == 1)
               {
                  return true;
               }

               else
               {   
                  places[a].setDrawn();
                  return false;
               }
            }
         }
      }

      function getMaxKekule()
      {
         // Get max kekule number in people data

         var maxKek = 0;
      
         for (var a=0 ; a < people.length ; a++)
            if (people[a].getKekule() > maxKek)
               maxKek = people[a].getKekule();

         return maxKek;
      }
   
      function getBornForKekule(kek)
      {
         // Get a birthplace for a given kekule entity

         for (var a=0 ; a < people.length ; a++)
            if (people[a].getKekule() == kek)
               return people[a].getBorn();

         alert(kek + " <-- kekule number not found in list!");
         return null;
      }
      
      function getIndexForKekule(kek)
      {
         // Get a people[] index for a given kekule entity

         for (var a=0 ; a < people.length ; a++)
            if (people[a].getKekule() == kek)
               return a;

         return -1;
      }
      
      function getColour(k)
      {      
         // get a line colour
        
         if (rotateColours == 1)
         { 
            if (k >= powers[powerPos] && powerPos < powers.length-1)
            {
               powerPos++;
               colourCounter++;

               if (colourCounter == colours.length)
                  colourCounter = 0;
            }

            return colours[colourCounter];
         }

         else
         {
            return colours[0];
         }
      }
      
      function getPeople(place, countType)
      {
         // get <li> items of people from a place

         var items = "";
         var num = 0;

         for (var b=people.length-1 ; b >= 0 ; b--)
         {
            if (people[b].getBorn() == place)
            {
               items += 
                  "<li> b." 
                  + people[b].getYear() 
                  + " "
                  + people[b].getName() 
                  + " (k: " + people[b].getKekule()  
                  + ")</li>";
               num++;
            }
         }
   
         if (countType == "names")
            return items;
         
         else
            return num;
      }
      
      //////////////////////////////////////////////////////////////////////
      //////////////////////////////////////////////////////////////////////

      // initialise map

      var map = new GMap2(document.getElementById("map"));
      map.addControl(new GLargeMapControl());
      map.addControl(new GMapTypeControl());
      map.setCenter(new GLatLng(getMeanLat(), getMeanLon()), mapZoomLevel);
      map.addControl(new GScaleControl());
      map.enableScrollWheelZoom();
      
      // markers

      var baseIcon              = new GIcon();
      baseIcon.shadow           = "newShadow.png";
      baseIcon.image            = "1.png";
      baseIcon.iconSize         = new GSize(20, 35);
      baseIcon.shadowSize       = new GSize(21, 19);
      baseIcon.iconAnchor       = new GPoint(9, 34);
      baseIcon.infoWindowAnchor = new GPoint(9, 2);
      baseIcon.infoShadowAnchor = new GPoint(18, 25);
   
      function createMarker(point, place, peopleIndex) 
      {
         // create new icon
         var myIcon = new GIcon(baseIcon);
         markerOptions = { icon:myIcon };
          
         // make icon bigger for more people               
         
         switch(getPeople(place, "num"))
         {   
            case 1:
                 myIcon.iconSize   = new GSize(20, 35);
                 myIcon.iconAnchor = new GPoint(9, 34);
                 myIcon.image      = "1.png";
            break;

            case 2:
                 myIcon.iconSize   = new GSize(23, 40);
                 myIcon.iconAnchor = new GPoint(11, 39);
                 myIcon.image      = "2.png";
            break;

            case 3:
                 myIcon.iconSize   = new GSize(24, 42);
                 myIcon.iconAnchor = new GPoint(11, 41);
                 myIcon.image      = "3.png";
            break;

            case 4:
                 myIcon.iconSize   = new GSize(27, 48);
                 myIcon.iconAnchor = new GPoint(13, 47);
                 myIcon.image      = "4.png";
            break;

            default:
                 myIcon.iconSize   = new GSize(29, 48);
                 myIcon.iconAnchor = new GPoint(13, 48);
                 myIcon.image      = "5.png";
            break;
         }

         // change colour if kekule 1
         if (peopleIndex == 0)
            myIcon.image = "blue.png";

         // create marker

         var marker = new GMarker(point, markerOptions);

         // show who's from each place on click

          GEvent.addListener(marker, "click", function() 
         {
            marker.openInfoWindowHtml("<h3 style=\"font-size:14px;\">" 
                                       + place 
                                       + "</h3><ol style=\"font-size:12px;\">" 
                                       + getPeople(place, "names") 
                                       + "</ol>");
          });

          return marker;
        } 

      // 1. draw people into map
      
      var myPoint;

      for (var i = 0 ; i < people.length ; i++)
      {   
         if (!beenDrawn(people[i].getBorn())) // only drawn each marker once
         {
            myPoint = new GLatLng(getCoord("lat", people[i].getBorn()), 
                                  getCoord("lon", people[i].getBorn()));

            map.addOverlay(createMarker(myPoint, people[i].getBorn(), i));
         }
      }
      
      // 2. draw connections into map
         
      if (drawLines == 1)
      {
         for (var k = 1 ; k <= getMaxKekule() ; k++)
         {
            if (getIndexForKekule(k) >= 0)
            {
               // person exists
   
               var fk = k * 2  ; // father's kekule
               var mk = fk + 1 ; // mother's kekule
               
               var colour = getColour(k);

               // draw father if born in different place and in array
               if (getIndexForKekule(fk) >= 0 &&
 
                   people[getIndexForKekule(k)].getBorn() != 
                   people[getIndexForKekule(fk)].getBorn())
               {   
                  var fatherLine = new GPolyline([
                     new GLatLng(getCoord("lat", people[getIndexForKekule(k)].getBorn())  , 
                                 getCoord("lon", people[getIndexForKekule(k)].getBorn())) ,

                     new GLatLng(getCoord("lat", people[getIndexForKekule(fk)].getBorn()) , 
                                 getCoord("lon", people[getIndexForKekule(fk)].getBorn())),
                  ], colour, lineThickness, lineOpacity);

                  map.addOverlay(fatherLine);
               }
               
               
               if (getIndexForKekule(fk) >= 0)
               {
                  // draw mother if in array, born in different place 
                  // and not born in same place as father
                  
                  if (getIndexForKekule(mk) >= 0 && 

                      people[getIndexForKekule(k)].getBorn() != 
                      people[getIndexForKekule(mk)].getBorn() && 

                      people[getIndexForKekule(mk)].getBorn() != 
                      people[getIndexForKekule(fk)].getBorn())
                  {   
                     var motherLine = new GPolyline([
                        new GLatLng(getCoord("lat", people[getIndexForKekule(k)].getBorn())  , 
                                    getCoord("lon", people[getIndexForKekule(k)].getBorn())) ,

                        new GLatLng(getCoord("lat", people[getIndexForKekule(mk)].getBorn()) , 
                                    getCoord("lon", people[getIndexForKekule(mk)].getBorn())),
                     ], colour, lineThickness, lineOpacity);

                     map.addOverlay(motherLine);
                  }
               }
   
               else
               {
                  // draw mother if in array and born in different place
                  if (getIndexForKekule(mk) >= 0 &&
 
                      people[getIndexForKekule(k)].getBorn() != 
                      people[getIndexForKekule(mk)].getBorn()) 
                  {   
                     var motherLine = new GPolyline([
                        new GLatLng(getCoord("lat", people[getIndexForKekule(k)].getBorn())  , 
                                    getCoord("lon", people[getIndexForKekule(k)].getBorn())) ,

                        new GLatLng(getCoord("lat", people[getIndexForKekule(mk)].getBorn()) , 
                                    getCoord("lon", people[getIndexForKekule(mk)].getBorn())),
                     ], colour, lineThickness, lineOpacity);

                     map.addOverlay(motherLine);
                  }
               }
            }
         }
      }   
   }
}

//]]>
      </script>
   </head>

   <body onload="load()" onunload="GUnload()">
      <div id="map" style="width: 100%; height: 800px"></div>
   </body>
</html>