Two Loon Software & Web Development
  541.708.1488

Bird Migration Maps

I added home-made date range maps to Birding Buddies. It was the most creative and interesting project I've worked on in a while.

You can see the result on Birding Buddies bird pages, like this one. (You will need to click "Load Range by Month" to load the dated range maps.)

Sample Range Map

I've been working hard on Birding Buddies. It's a bit raw design-wise, but I think the overall feature set is really coming together. I've been reviewing all the best functionality on other sites and trying to bring it all together in one place for the best online birding experience.

Last week I decided to step outside the box and create something that I couldn't find anywhere else: interactive range maps to show migration patterns.

I have raw data for millions of sightings (provided by eBird). My goal was to turn that raw data into a Google Map friendly format grouped by month.

I found that Keyhole Markup Language (KML) was the best format for the job. I quickly scripted out the logic to plot all the sighting points for a single bird. I wound up with a 100MB file. The bad news was that 100MB was too large. The good news was that meant that I got to do Math!

The problem: aggregate hundreds of thousands of points into a polygon that accurately represented their span.

First finding: Delaunay triangulation is very cool, very complicated, and totally impossible to calculate efficiently for hundreds of thousands of points.

Convex Hull
Convex Hull

Second finding: Convex Hull - Man, I was sure I'd found the answer to my solution. However, they're useless because they don't allow for any non-convex output (polygons with dimples or inlets).

#3 - Alpha shapes: Alpha shapes are COOL. They aren't what I was looking for because: there are no third party python implementations, implementing your own involves differential equations, and the output is to complicated to map to a KML document (it's possible, but barely).

At this point I was almost ready to give up, buy my interest in cartesian math kept me in the hunt.

#4 - Shapely: I let go of my mathematic ambitions and found the most popular plotting library for python. It's easy to use and allowed me to do some experimenting. I went through the following algorithms:

  • Built-in convex hull. Useless as explained above. It usually gave me big circles around the US.
  • Plot all the points as circles, but exclude points that would exist in previously plotted circles. Whoops.
  • Built in envelope function. Similar to the convex hull, but worse in that it only allowed 4 vertices.
  • Plot all points as circles of diameter D and merge into a polygon.

Well, that last one looked better than anything else I'd seen, but if I set fine accuracy there were all sorts of swiss-cheese holes in my plots (places people don't bird, not where the birds didn't exist).

The final solution: plot all points as circles of diameter 4d, then shrink the resulting polygon by 3d. This filled in gaps and left me with a nice representation of where the birds were actually spotted.

The resulting KML files are all under 45k; most of them are under 10k. Fantastic!

I used the Shapely and simplekml python libraries.

Here is my code:

while current_date.year == 2013:
	month_name = calendar.month_name[current_date.month]
	pts = []
	sightings = Sighting.objects.extra(select={'nwdistance': "nwdistance"}).filter(bird=self).filter(longitude__isnull=False).filter(latitude__isnull=False).filter(date__month=current_date.month).extra(order_by = ['-nwdistance'])

	for sighting in sightings:
		pts.append((sighting.latitude,sighting.longitude))

	if len(pts) == 0:
		current_date = current_date + relativedelta(months = +1)
		continue;

	single_kml = simplekml.Kml()

	group_size = 5000
	point_groups = []
	last = 0.0

	while last < len(pts):
		point_groups.append(pts[int(last):int(last + group_size)])
		last += group_size

	for point_group in point_groups:
		mp = MultiPoint(point_group).buffer(2).buffer(-1.5)
		if isinstance(mp, Polygon):
			mp = MultiPolygon([mp])

		for poly in mp:
			boundary = []
			i = 4
			for coord in poly.exterior.coords:
				if i % 4 == 0:
					boundary.append((coord[1], coord[0]))
				i = i + 1

			poll = single_kml.newpolygon(name=self.common_name + " in " + current_date.strftime("%B"))
			poll.outerboundaryis = boundary

	next_month = current_date + relativedelta(months = +1)

	drm = DatedRangeMap()
	drm.start_date = current_date
	drm.end_date = next_month
	drm.description = "Sighting map for " + month_name
	drm.bird = self
	drm.save()

	content = ContentFile(single_kml.kml())
	file_name = "{0}-{1}-{2}-{3}.kml".format(self.id, self.slug, current_date.month, month_name)
	drm.range_map_file.save(
		file_name,
		content,
		save=True
	)
	current_date = next_month
Oh, and I added Flickr integration, too. :)
< All Code Articles
logo

Welcome to Two Loon Software

Sorry, our website doesn't support your web-browser.
Maybe something more modern would do the trick.

download chrome