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:
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>