Putting it all together, part 3

Now we make our code restrict to things near the user when they allow geolocation…but the results aren’t really what we’re shooting for.

def earthquake_display(request):
    search_origin = False
    if request.GET.get('lat') and request.GET.get('lon'):
        search_origin = Point(float(request.GET.get('lon')), float(request.GET.get('lat')))

    weekago = datetime.datetime.now() - datetime.timedelta(days=7)
    quakes = Quake.objects.filter(datetime__gte=weekago).order_by('-datetime')

    last_check = cache.get('usgs-poll-last-finished', datetime.datetime(2000, 1, 1))
    checking = cache.get('usgs-poll-in-progress', False)
    if not checking:
        cache.set('usgs-poll-in-progress', True)
        latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
        latest_check_ago = datetime.datetime.now() - last_check
        if latest_quake_ago > datetime.timedelta(minutes=90) and latest_check_ago > datetime.timedelta(minutes=90):
            from django.core import management
            management.call_command('load_quakes')
            cache.set('usgs-poll-last-finished', datetime.datetime.now())
            # and refresh the QuerySet to get the new data
            quakes = Quake.objects.filter(datetime__gte=weekago).order_by('-datetime')
        cache.delete('usgs-poll-in-progress')
        checking = False

    if search_origin:
        quakes = quakes.distance(search_origin).order_by('distance', '-datetime')

    quakes = quakes[:25]

    return render_to_response('earthquakes.html', {
        'object_list': quakes,
        'checking': checking,
    }, context_instance=RequestContext(request))
<html>
<head>
  <script src='http://media.adamfast.com/QueryData.compressed.js'></script>
  <script language='JavaScript'>

    function checkLocation() {
      var getData = new QueryData();
      if ('lat' in getData) { }
      else {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(
            function (ppos) {
              window.location.href = window.location.href + '?lat=' + ppos.coords.latitude + '&lon=' + ppos.coords.longitude;
            },
            function (err) {
              switch(err.code) {

                case err.TIMEOUT:
                  alert('Attempts to retrieve location timed out.')
                  break;

                case err.POSITION_UNAVAILABLE:
                  alert("Your browser doesn't know where you are.")
                  break;

                case err.PERMISSION_DENIED:
                  alert('You have to give us permission!')
                  break;

                case err.UNKNOWN_ERROR:
                  alert('Unknown error returned.')
                  break;

                default:
                  alert(err + ' ' + err.code)

              }
            }
          );
        }
      }
    }
  </script>
</head>
<body{% block body_override %} onLoad="javascript:checkLocation();"{% endblock body_override %}>
{% block content %}
    {% for object in object_list %}
    {{ object }}<br>
    {% endfor %}
{% endblock content %}
</body>
</html>

This is returning results with too much emphasis on distance and not enough on how long ago the activity at that distance was. So I tried a few things:

  • Restrict to activity only in the past 4 hours instead of a week (not bad! helps quite a bit.)
  • Restrict to activity within 1000 miles (maybe restricts too much? This left me with no results for Missouri where I write this, but there are no quakes recently near here.
Between them I got a pretty usable app. If it can’t find anything, “Nope.” is returned. For now, if anything was found it returns a list. If we wanted we could hook up GeoIP as a fallback, but since I’m playgrounding this on ep.io I don’t have c extensions to use the module and have to call it good with just HTML5.
The “final” view, if you’d like to see it:
def earthquake_display(request):
    search_origin = False
    if request.GET.get('lat') and request.GET.get('lon'):
        search_origin = Point(float(request.GET.get('lon')), float(request.GET.get('lat')))
        search_origin.transform(900913)
        search_poly = search_origin.buffer(1000 * 2172.344)
        search_poly.transform(4326)
        search_origin.transform(4326)

#    weekago = datetime.datetime.now() - datetime.timedelta(days=7)
#    quakes = Quake.objects.filter(datetime__gte=weekago).order_by('-datetime')
    last_4_hrs = datetime.datetime.now() - datetime.timedelta(hours=4)
    quakes = Quake.objects.filter(datetime__gte=last_4_hrs).order_by('-datetime')

    last_check = cache.get('usgs-poll-last-finished', datetime.datetime(2000, 1, 1))
    checking = cache.get('usgs-poll-in-progress', False)
    if not checking:
        cache.set('usgs-poll-in-progress', True)
        latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
        latest_check_ago = datetime.datetime.now() - last_check
        if latest_quake_ago > datetime.timedelta(minutes=90) and latest_check_ago > datetime.timedelta(minutes=90):
            from django.core import management
            management.call_command('load_quakes')
            cache.set('usgs-poll-last-finished', datetime.datetime.now())
            # and refresh the QuerySet to get the new data

#            quakes = Quake.objects.filter(datetime__gte=weekago).order_by('-datetime')
            quakes = Quake.objects.filter(datetime__gte=last_4_hrs).order_by('-datetime')

        cache.delete('usgs-poll-in-progress')
        checking = False

    if search_origin:
        quakes = quakes.filter(point__within=search_origin)
        quakes = quakes.distance(search_origin).order_by('distance', '-datetime')

    quakes = quakes[:10]

    return render_to_response('earthquakes.html', {
        'object_list': quakes,
        'checking': checking,
    }, context_instance=RequestContext(request))

With that we’ve built a small app with a data source, views, templates, and a “real” purpose all the way through. I hope it’s been helpful for several of you and apologize that some things wound up basically just being repeated from earlier topics, but hopefully after the in-depth back then it was easier to walk along as something was built from it.

A day of thanks

In the spirit of the holiday (in the US we’re celebrating Thanksgiving) I thought I’d offer my appreciation / thanks to the projects I use and lean on to make my applications work. This isn’t a canonical list, I’m sure I’ve forgotten libraries here and there. And regardless of whether I use your code right now or not, thanks for releasing it where I can someday if it does something I need. I contribute everything I can when I can because everybody else has contributed so much that makes me more productive I want to follow suit.

If you blog, I’d ask you to consider something alone these lines of your own. As software authors there are lots of instances where our code is doing things we have no idea about until we’re told.

This is in addition to all the OTHER stuff I use besides libraries – GitX, VLC, Audacity, QGIS, TileMill and others I’m sure.

Open source software authors, give yourselves a pat on the back. Open source users, thank the authors who give you something to use in the first place.

Putting it all together, part 2

We’ve got our quakes app installed (update it if you’re following along at home, I committed a bunch of code changes for this entry) and our initial data imported.

However, we need this data to be updated somewhat regularly for the service to be useful. Sean shipped django-quakes with Celery support (which I commented out of the requirements file because I’m running this app on the neat ep.io auto-scaling webapp service‘s free tier and don’t have Celery access. If you run Celery just re-enable it, if you don’t use Celery or even know what it is (check it out!) you can use cron to schedule it just as well.

To avoid polling too often and bugging the USGS I’d set the interval at hourly or more under normal circumstances. We only care about quakes when our users do. I’d run it at least daily though so you keep history.

Since we care when our users do and earthquakes can’t be convinced to ONLY hit at certain minutes after the hour, what do we do? How about we check the latest quake we have and see if it’s beyond a certain age to force a refresh?

This is more of that “please don’t do this” code – yes, it works, but it’s bad. If you’re like me and want the site to auto-retrieve at a faster schedule when users are using it, you might be tempted to do this:

latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
if latest_quake_ago > datetime.timedelta(minutes=5):
    from django.core import management
    management.call_command('load_quakes')

This is bad. Why? Our webapp isn’t a single-file line. When there’s an earthquake, people start going nuts. The resulting traffic will be somewhat of a flood, so imagine a bunch of these views all running at once. USGS will not be happy with us, and we’re doing all kinds of extra processing that just gets thrown out. (NOTE: If you’re trying this in the default dev server, it won’t work – it’s single-threaded so both will never be going at once.)

So what’s our next thought? Lock files (virtually, in cache instead of filesystem)!

checking = cache.get('usgs-poll-in-progress', False)
if not checking:
    print('spawning check')
    cache.set('usgs-poll-in-progress', True)
    latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
    if latest_quake_ago > datetime.timedelta(minutes=5):
        from django.core import management
        management.call_command('load_quakes')
    cache.delete('usgs-poll-in-progress')
    print('check done.')
else:
    print('in progress')

We’re still not quite there. After all, at the time of this writing there hasn’t been an earthquake in 45 minutes – so every access kicks off a poll. Let’s add caching to store the last time a poll was done.

last_check = cache.get('usgs-poll-last-finished', datetime.datetime(2000, 1, 1))
checking = cache.get('usgs-poll-in-progress', False)
if not checking:
    cache.set('usgs-poll-in-progress', True)
    latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
    latest_check_ago = datetime.datetime.now() - last_check
    if latest_quake_ago > datetime.timedelta(minutes=5) and latest_check_ago > datetime.timedelta(minutes=5):
        print('spawning check')
        from django.core import management
        management.call_command('load_quakes')
        cache.set('usgs-poll-last-finished', datetime.datetime.now())
        print('check done.')
    cache.delete('usgs-poll-in-progress')
else:
    print('in progress')

But what about people who hit the site while a poll is in progress? How about a time.sleep(0.5) loop?

Did you just throw your mouse? I dodged it. Yeah, that’s a bad idea. After all, it does tie up our Python processes at the moment when we’ve got hordes of eager, shaken (pun only slightly intended) users ringing the doorbell.

So what’s a programmer to do? Let’s set a context flag in the template and use it to trigger something in the template about “we just heard, we’re checking into it!”with what we already have.

def earthquake_display(request):
    weekago = datetime.datetime.now() - datetime.timedelta(days=7)
    quakes = Quake.objects.filter(datetime__gte=weekago).order_by('-datetime')

    last_check = cache.get('usgs-poll-last-finished', datetime.datetime(2000, 1, 1))
    checking = cache.get('usgs-poll-in-progress', False)
    if not checking:
        cache.set('usgs-poll-in-progress', True)
        latest_quake_ago = datetime.datetime.now() - quakes[0].datetime
        latest_check_ago = datetime.datetime.now() - last_check
        if latest_quake_ago > datetime.timedelta(minutes=5) and latest_check_ago > datetime.timedelta(minutes=5):
            from django.core import management
            management.call_command('load_quakes')
            cache.set('usgs-poll-last-finished', datetime.datetime.now())
        cache.delete('usgs-poll-in-progress')
        checking = False

    return render_to_response('earthquakes.html', {
        'object_list': quakes,
        'checking': checking,
    }, context_instance=RequestContext(request))

We’re getting there! We still need geolocation, geographic filtering, and a frontend.

Putting it all together: A sample, simple project, part 1

We’ve discussed all sorts of things this month, now it’s time to start putting them together more completely than snippet-for-this, snippet-for-that. I’m not going to inline command + output on everything but I am going to spell out all of the steps – if you have trouble, let me know in the comments.

To accomplish this we’re going to build a simple web service I’ve snagged “isthatanearthquake.com” for.

First order of business is creating the project.

Because virtualenv and virtualenvwrapper rocks (`pip install virtualenv virtualenvwrapper`), I started with `mkvirtualenv isthatanearthquake`. My global virtualenv `postactivate` script includes the line `cd $VIRTUAL_ENV` which drops me in the env whenever I activate it. Next I created a `src` directory, created and cd’ed into `isthatanearthquake-git`, created `templates` and created requirements.txt with these contents:

django==1.3.1

psycopg2==2.4.2

-e git+git://github.com/adamfast/django-quakes.git@4d9fcdb4978a28f35d1e32406a81f3b8b5d58845#egg=quakes

pip install -r requirements.txt gets me Django, Postgres and the reusable app django-quakes (thanks Sean Bleier, who I’ve forked the library from for this example) installed in my virtualenv.

Create the Django project with `python $VIRTUAL_ENV/lib/python2.?/site-packages/django/bin/django-admin.py startproject isthatanearthquake` (note you will need to specify your python version in this command)

`add2virtualenv isthatanearthquake` will make the new project show up on your PYTHONPATH.

Open isthatanearthquake/settings.py and `import os` at the top. Somewhere in the file (I tend to put it nearer the bottom define

PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__))

Set up a database: DATABASES['default']['ENGINE'] should be ‘django.contrib.gis.db.backends.postgis’ and ['NAME'] should be whatever you want to call the new database.

Set your template dirs to use the templates folder created just outside the project from earlier:

TEMPLATE_DIRS = (
    os.path.join(PROJECT_ROOT, "../templates"),
)

Add ‘django.contrib.gis’, and ‘quakes’, to INSTALLED_APPS. Uncomment ‘django.contrib.admin’ (and ‘django.contrib.admindocs’, though that one isn’t strictly required it’s just my habit.) We’re done with settings.py for now.

Uncomment the line in urls.py including admin.site.urls. If you uncommented django.contrib.admindocs in INSTALLED_APPS, uncomment its include statement as well.

Create the database you configured for with `createdb -T template_postgis <dbname>`

`python manage.py syncdb` will create the necessary tables, create a superuser of your choice, and `python manage.py load_quakes` will grab the last seven days of earthquakes from USGS.

`python manage.py runserver` will pop up the development server, and now with a web browser we can hit http://localhost:8000/admin/, login and choose “Quakes” to see a list of what came in.

Finding Data: Often harder than using it

One of the hardest parts of doing geo projects is getting the data you need to do it in the first place. In the US at least there are mountains of data at the federal level, some at the state level and who knows what at the local level. There isn’t a single place I can go to for anything outside of census-type stuff. Which school for a given grade level would a child at this point attend? Maybe it’s published, maybe it’s not.

Most cities or counties will have a GIS department. Some of them will be great, and helpful towards your goal as a developer. Others won’t. Our world of the tools and technologies are leapfrogging traditional methods.

So here’s some of my favorite sources of different kinds of data, worth looking at for your next project.

TIGER/LINE: Gigabytes of shapefiles of things the US Federal government collects. I tend to look here first if I need basic location data.

SimpleGeo Places DB: SimpleGeo put a dataset of 21 million places (12ish million in the US) into the public domain last summer, and links to the file here. I have played with the data some and it’s pretty clean but often categorized inconsistently (but hey, it’s free for all to use) There are discussions online about various ways of getting it imported, shapefile conversion didn’t work for me and neither did wrapping it in a feature collection in GeoJSON – it’s just too big – nearly 8GB of JSON. The method I got to actually work was taking it line by line, deserializing and then processing. I split into a TON of files containing 20,000 places each and ran several processes to get it imported. It’s a BIG database, and pretty slow in PostGIS so be warned. I have no plans to put anything in production from it so speed isn’t that big of an issue. As a side note, people importing this dataset are probably their best sales tactic toward paying for their SAAS version.

Flickr Shapefiles: Also public domain, potentially useful if you need to bring photos into the mix.

Timezones: Full shapefile of timezones of the world. Useful for auto-detecting your users’ time by their location and auto-shifting times to them. Public domain.

Free IP/Location database: Similar to GeoIP but community built. Likely not as comprehensive, it had no idea where I was for instance.

data.gov: It’s hard to find what you want and sometimes hard to figure out how to use the format it’s in, but data.gov is as close to a one-stop-shop as you may come.

data.nasa.gov: Datasets published by NASA. This one’s still a work in progress, but has a better search than data.gov and includes most NASA data listed on data.gov.

Ordnance Survey: Open (see licenses) data for the UK.

FCC GIS: Information on communications systems you can import.

FCC APIs: APIs to interact with the FCC’s data through their systems, includes data gathered for the national broadband map.

FreeGIS: Links to other datasets and software related to geo

NAIP Imagery: Satellite photos from flyovers during growing seasons.

Land Cover: Determining classification of land – forested, open, urban, etc.

National Hydrography Dataset: Information on our water.

Cartographic Boundaries: Outlines of various types of drawn boundaries.

 

Have you got a favorite or just plain useful dataset missed here? Leave a comment and let us know about them!

Polygons and overlays with Google Maps v3 API

I stopped short of displaying an equivalent overlay in Google Maps v3 yesterday, but wanted to circle back and show that it’s doable as well. It’s far more verbose than the magic-y “pass it a polygon and it does the rest”, but you have more control too.

View, pretty much just like the one we used for points only with a model that has a polygon:

def gmap3_poly(request):
    states = State.objects.filter(mpoly__distance_lte=(Point((-94, 37)), D(mi=100)))

    return render_to_response('gmap3.html', {
        'object_list': states,
    }, context_instance=RequestContext(request))

For the template, I actually modified yesterday’s to auto-detect the geometry coming through and just do the right thing.

{% extends "gmap3base.html" %}

{% block page-title %}Requested Items{% endblock %}

{% block head_override %}
<script type="text/javascript">
  var bounds = new google.maps.LatLngBounds();

  function buildMarker(map, latitude, longitude, name, color) {
    var latlng = new google.maps.LatLng(latitude, longitude);
    var marker = new google.maps.Marker({
          position: latlng,
          map: map,
          title:name,
    });
    marker.setIcon('http://maps.google.com/mapfiles/ms/icons/' + color + '-dot.png');
    bounds.extend(latlng);
    return marker
  }

  function mapInitialize() {
    var myOptions = {
      zoom: 6,
      center: new google.maps.LatLng(0, 0),
      mapTypeId: google.maps.MapTypeId.SATELLITE
    };
    var map = new google.maps.Map(document.getElementById("map_canvas"),
        myOptions);

    {% for object in object_list %}
    {% if object.point %}marker{{ object.pk }} = buildMarker(map, {{ object.point.y }}, {{ object.point.x }}, "{{ object }}", 'red');
    {% endif %}
    {% if object.mpoly %}
    var polygon{{ object.pk }}Coords = [
    {% with object.mpoly.0.0 as the_polygon %}
    {% for point in the_polygon %}
        new google.maps.LatLng({{ point.1 }}, {{ point.0 }}),
    {% endfor %}
    {% endwith %}
    ];

    var the_polygon{{ object.pk }} = new google.maps.Polygon({
      paths: polygon{{ object.pk }}Coords,
      strokeColor: "#FF0000",
      strokeOpacity: 0.8,
      strokeWeight: 2,
      fillColor: "#FF0000",
      fillOpacity: 0.35
    });

    for (i = 0; i < polygon{{ object.pk }}Coords.length; i++) {
      bounds.extend(polygon{{ object.pk }}Coords[i]);
    }

    the_polygon{{ object.pk }}.setMap(map);
    {% endif %}
    {% endfor %}

    map.fitBounds(bounds);
  }

</script>
{% endblock head_override %}

{% block body_override %} onload="mapInitialize()"{% endblock body_override %}

{% block content %}
<div id="map_canvas" style='width:600px;height:400px;'></div>
{% endblock content %}

Rendered out, the (4.4MB) file looks like this.

But we want our apps to be more interactive – so let’s quickly add popup windows when something is clicked on. The blog isn’t going to handle a diff very well, and that’s the best way to show it, so check out this gist. It’s not very difficult at all. Now we get popups when somebody clicks a marker, without writing a single line of view code. I tend to build methods on my models for map_display_html() or something similar. If I’m really returning HTML I’ll put it in a template and use the template language to make it work, if it’s just a name and hyperlink or something similar I just leave it in the model.

Mapping better – Google Maps API v3

I tend to do my Google v3 work purely in my templates.

For this example I’ve got a view that will return the objects to be displayed in `object_list` in the context – same dataset as last time, only instead of building things inside the view we’ll do it in our Django template.

from uscampgrounds.models import Campground

def gmap3(request):
    campgrounds = Campground.objects.filter(point__distance_lte=(Point((-94, 37)), D(mi=100)))

    return render_to_response('gmap3.html', {
        'object_list': campgrounds,
    }, context_instance=RequestContext(request))

For our base template – defining a map area, loading the initial Google JavaScript: (I named it gmap3base.html because there are several base and normal templates in the project I’m building from)

<html>
    <head>
        <title>{% block page-title %}Simple Google Maps API v3{% endblock %}</title>
        <script type="text/javascript" src="http://maps.google.com/maps/api/js?sensor=false"></script>
        {% block head_override %}{% endblock head_override %}
    </head>
    <body{% block body_override %}{% endblock body_override %}>
        {% block content %}
        {% endblock content %}
    </body>
</html>

Next our map creating template (I named mine gmap3.html):

{% extends "gmap3base.html" %}

{% block page-title %}Requested Items{% endblock %}

{% block head_override %}
<script type="text/javascript">
  var bounds = new google.maps.LatLngBounds();

  function buildMarker(map, latitude, longitude, name, color) {
    var latlng = new google.maps.LatLng(latitude, longitude);
    var marker = new google.maps.Marker({
          position: latlng,
          map: map,
          title:name,
    });
    marker.setIcon('http://maps.google.com/mapfiles/ms/icons/' + color + '-dot.png');
    bounds.extend(latlng);
    return marker
  }

  function mapInitialize() {
    var myOptions = {
      zoom: 6,
      center: new google.maps.LatLng(0, 0),
      mapTypeId: google.maps.MapTypeId.SATELLITE
    };
    var map = new google.maps.Map(document.getElementById("map_canvas"),
        myOptions);

    {% for object in object_list %}marker{{ object.pk }} = buildMarker(map, {{ object.point.y }}, {{ object.point.x }}, "{{ object }}", 'red');
    {% endfor %}

    map.fitBounds(bounds);
  }

</script>
{% endblock head_override %}

{% block body_override %} onload="mapInitialize()"{% endblock body_override %}

{% block content %}
<div id="map_canvas" style='width:600px;height:400px;'></div>
{% endblock content %}

This doesn’t do anything fancy – just the simple behavior we got from using GeoDjango’s map generator – but since we generated the JavaScript ourselves, we know what things will be named and can add on our own JavaScript much easier – and this works without modification. No API key creating, switching based on what site we’re on, it just works.

Since we generated normal JavaScript we also have the benefit of being able to use tutorials and examples found all over the internet to fine tune things. Want an onClick event that opens an infoWindow? It’s quite easy. Want to do zooming differently, say to a range from the search point instead of making every point visible? You can do that too. [you will need another geometry passed into the context for the search location in order to do this]

On the Map

So far we’ve done all kinds of wearying and information – but never left a Python console. I suppose it’s time we fall into the web mapping world and look at our options.

On the left of the easy-to-hard scale is Google Maps, API v2. This is the deprecated version, but is the only frontend generation available to us out of the box. It’s not mentioned in the documentation, but is well represented in the class’ docstring. (django.contrib.gis.maps.google)

Follow the doctoring to get your template set up appropriately.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{{ google.xhtml }}
<head>
 <title>Google Maps via GeoDjango</title>
 {{ google.style }}
 {{ google.scripts }}
</head>
{{ google.body }}
<div id="{{ google.dom_id }}" style="width:600px;height:400px;"></div>
</body>
</html>

We create markers from the points we have with this:

Then instantiate the map with a markers kwarg (you can include key=’api_key_here’ in the class instantiation or it will fall back to settings.GOOGLE_MAPS_API_KEY), pass it into the template and watch in amazement as a map materializes before your eyes!

from django.contrib.gis.geos import Point
from django.contrib.gis.maps.google import GoogleMap, GMarker, GPolygon
from django.contrib.gis.measure import D

from uscampgrounds.models import Campground

def gmap2(request):
	campgrounds = Campground.objects.filter(point__distance_lte=(Point((-94, 37)), D(mi=100)))

	markers = []
	for campground in campgrounds:
		markers.append(GMarker(campground.point, title=campground.name))

	the_map = GoogleMap(markers=markers)

	return render_to_response('googlemap.html', {
		'google': the_map,
	}, context_instance=RequestContext(request))

You can do this with polygons too, as this code illustrates.

from django.contrib.gis.geos import Point
from django.contrib.gis.maps.google import GoogleMap, GMarker, GPolygon
from django.contrib.gis.measure import D

from tigerline.models import State

def gmap2_poly(request):
	states = State.objects.filter(mpoly__distance_lte=(Point((-94, 37)), D(mi=100)))

	polygons = []
	for state in states:
		polygons.append(GPolygon(state.mpoly[0].simplify()))

	the_map = GoogleMap(polygons=polygons)

	return render_to_response('googlemap.html', {
		'google': the_map,
	}, context_instance=RequestContext(request))

The resulting HTML, even after running simplify() on the geometry, is 2.4MB so I won’t display it here – but here’s a link. Warning: displaying polygons gets big. I’m asking for trouble here, after all – every single point that makes up the boundary of four states is going to be a bunch.

django.contrib.gis.maps.google is great for quick, don’t need a lot of details or fancy overlays maps. But it leaves out an easy way to do click events on markers and labeling, and API keys are inconvenient.

IF you do need onClick events, infoWindows from markers or any of that stuff there are hacks – but it involves looping over the items in the template as well and generating JavaScript that infers what it knows this map generation code will name things. Best to bite the bullet and build API v3 code in a template on your own (coming soon!)

Faking it: Geospatial search without the prereqs or infrastructure

First, a public service announcement: tomorrow, Nov. 16th, is “GIS Day” which means universities all over will be holding day lectures, in most cases for free. Many of them will be over our heads since we’re but neogeographers, but inspiration can come from anywhere – and it’s a good opportunity to meet others interested in this stuff. I’ll be at the one held at the University of Kansas here in Lawrence – why not look it up and see if your nearby university is having a program? My apologies on the short notice for this though, hopefully you can still make something happen as a professional development day.

I’ve shown more than a few ways we can use the ORM with GeoDjango to do real geographic searches of various types. But what if your limitations aren’t so generous and geospatial libraries and databases are off limits?

What if there were a way we could FAKE it?

There’s the incredibly nasty way which I won’t even put in code. Seriously, don’t do this. For completeness I’ll put it in semi-pseudocode that at least conveys the basic idea.

Create a set, loop over all objects in the database and use our
multi-tool GeoPy's vincentydistance (correct capitalization) method
to calculate the distance to each of them. Store the distance and a
record identifier in a dictionary and append it as a new "row" to our
set. Sort the set, and WOW, we have a really slow, potentially
disastrous in memory usage, but pure Python and minimal prerequisite
implementation of geographic searching.

Still listening? Please don’t do this.

Somebody smarter at math than me had this problem and thought about a way to solve it. It’s called Geohash and essentially it’s goal is to have a way of defining geography with varying degrees of accuracy with the variance in the number of characters. So 9yum8 will match 9yum8yef3vds6 (Lawrence, KS) and we can use a CharField to store, and query with the __startswith operator.

It’s not perfect, though – items JUST across the line which do overlap, but are not centered inside the hash we search for won’t be retured. To get them we need to calculate the neighbors of our hash. (in the case of python-geohash a method called “expand” will return neighbors + our origin box)

There’s a good library called python-geohash (use that name to pip install it, “geohash” on pip is not the right thing) that will do both of these calculations for us – pure python with C for speed if that works on your system.

>>> import geohash
>>> geohash.encode(38.9716689, -95.2352501)
'9yum8yef3vds'
>>> geohash.expand('9yum8yef3vds')
['9yum8yef3vdk', '9yum8yef3vdu', '9yum8yef3vde', '9yum8yef3vd7', '9yum8yef3vdg', '9yum8yef3vdt', '9yum8yef3vdm', '9yum8yef3vdv', '9yum8yef3vds']
>>> from django.db.models import Q
q_object = Q()
>>> for hash in geohash.expand('9yum8yef3vds'):
...     q_object.add(Q(geohash__startswith=hash), Q.OR)
>>> print(q_object)
(OR: ('geohash__startswith', '9yum8yef3vdk'), ('geohash__startswith', '9yum8yef3vdu'), ('geohash__startswith', '9yum8yef3vde'), ('geohash__startswith', '9yum8yef3vd7'), ('geohash__startswith', '9yum8yef3vdg'), ('geohash__startswith', '9yum8yef3vdt'), ('geohash__startswith', '9yum8yef3vdm'), ('geohash__startswith', '9yum8yef3vdv'), ('geohash__startswith', '9yum8yef3vds'))

So what we’ve done is create a Q object you can use on a queryset to fake a geographic search around the general area. This isn’t a very wide area, though. Trial and error will help. Passing a “precision=” kwarg into .encode() to request less accuracy (and a wider search area) like this:

>>> geohash.encode(38.9716689, -95.2352501, precision=4)
'9yum'
>>> geohash.expand('9yum')
['9yuj', '9yut', '9yuk', '9yuh', '9yus', '9yuq', '9yun', '9yuw', '9yum']
>>> q_object = Q()
>>> for hash in geohash.expand('9yum'):
...     q_object.add(Q(geohash__startswith=hash), Q.OR)
...
>>> print(q_object)
(OR: ('geohash__startswith', '9yuj'), ('geohash__startswith', '9yut'), ('geohash__startswith', '9yuk'), ('geohash__startswith', '9yuh'), ('geohash__startswith', '9yus'), ('geohash__startswith', '9yuq'), ('geohash__startswith', '9yun'), ('geohash__startswith', '9yuw'), ('geohash__startswith', '9yum'))

According to Wikipedia’s explanation of the algorithm (see under “Worked Example”) accuracy ranges from +/- 2500km at 1 character to +/- 0.019km at 8 characters. A length of 5 is +/- 2.4km, close to what we were using for circle radius generation in previous work. But drop to 4 and you’re grabbing +/- 20km worth of records.

It’s far from perfect, but when there’s nothing available but plain text search, it’s better than nothing!

Where is my user? Part 2, Browser Geolocation

As we saw last week GeoIP can be pretty inaccurate for mobile users – the exact audience we may be trying hardest to serve with a geographically aware website. But the W3C saw, or was made to see, the writing on the wall and built a set of standard APIs into HTML5 for just this case and most modern browsers have picked it up.

The draft for the spec is http://dev.w3.org/geo/api/spec-source.html if you want to read it through or need further info.

The API is pretty marvelously simple. This implementation changes the URL to return latitude and longitude when they are available, which we can use in our Django view. Plus, the same code works on mobile devices (at least the iOS ones I carry) with no changes.

So let’s dive right in, and make the campgrounds dataset grab the nearest results to the user.

The view:

from django.contrib.gis.geos import Point
from django.shortcuts import render_to_response
from django.template import RequestContext

from uscampgrounds.models import *

def nearby_campgrounds(request):
    if request.GET.get('lat') and request.GET.get('lon'):
        origin = Point(float(request.GET.get('lon')), float(request.GET.get('lat')))
        camps = Campground.objects.all().distance(origin).order_by('distance')
    else:
        camps = Campground.objects.all().order_by('name')

    return render_to_response('uscampgrounds/nearby.html', {
        'object_list': camps
    }, context_instance=RequestContext(request))

uscampgrounds/nearby.html template:

<html>
<head>
  <script src='http://media.adamfast.com/QueryData.compressed.js'></script>
  <script language='JavaScript'>

    function checkLocation() {
      var getData = new QueryData();
      if ('lat' in getData) { }
      else {
        if (navigator.geolocation) {
          navigator.geolocation.getCurrentPosition(
            function (ppos) {
              window.location.href = window.location.href + '?lat=' + ppos.coords.latitude + '&lon=' + ppos.coords.longitude;
            },
            function (err) {
              switch(err.code) {

                case err.TIMEOUT:
                  alert('Attempts to retrieve location timed out.')
                  break;

                case err.POSITION_UNAVAILABLE:
                  alert("Your browser doesn't know where you are.")
                  break;

                case err.PERMISSION_DENIED:
                  alert('You have to give us permission!')
                  break;

                case err.UNKNOWN_ERROR:
                  alert('Unknown error returned.')
                  break;

                default:
                  alert(err + ' ' + err.code)

              }
            }
          );
        }
      }
    }
  </script>
</head>
<body{% block body_override %} onLoad="javascript:checkLocation();"{% endblock body_override %}>
{% block content %}

{% endblock content %}
</body>
</html>

It’s using QueryData.js, a small library to make getting data out of the current page’s querystring easier.

Other than that we check to see if the querystring has location info available – if not we request it from the browser and register a callback to bring the user back to the page with the right querystring args. The second function passed into getCurrentPosition() is the error-handling callback. In this case I’ve just set it to alert in the various cases for simplicity’s sake.