Add a Google map to Django admin to get latitude and longitude

While writing a Django app that contains a model with latitude and longitude fields I wanted to use a map in the Django Admin to set those values. I couldn’t find anything quite like what I wanted so, inevitably, I wrote my own.

There are alternatives, like:

  • django-geoposition. I haven’t tried it but this module provides a new field type (GeopositionField) which has a map in the admin. This sounds like the simplest solution unless you love extra work, like I do.

  • GeoDjango has maps in the admin, using OpenLayers maps which can be replaced, e.g., with django-map-widgets. In my app the locations aren’t very important and GeoDjango seemed like overkill.

The below is all from this app on GitHub in case you prefer to find the code there; I’ll link to the specific files below. NOTE: The example on GitHub also uses Google Geocoder to make an address string (like “Ripley, Derbyshire, England”) and add that and a country code (“GB”, “US”, etc.) to additional fields on the model.

Screenshot

1. Make or update your model

For our example we have a basic model called Venue, in models.py (on GitHub):

from django.db import models


class Venue(models.Model):

    name = models.CharField(max_length=255)

    latitude = models.DecimalField(
                max_digits=9, decimal_places=6, null=True, blank=True)

    longitude = models.DecimalField(
                max_digits=9, decimal_places=6, null=True, blank=True)

2. Get an API key

I’m using Google Maps because it’s relatively easy.

Get a Google Maps Javascript API key.

Put it in your Django project’s settings.py file like this:

GOOGLE_MAPS_API_KEY = 'your-key-goes-here'

3. Add the admin CSS

In your app’s static files add this CSS file. Ours is at appname/static/css/admin/location_picker.css (on GitHub).

.setloc-map {
    max-width: 100%;
    height: 400px;
    margin-top: 1em;
    border: 1px solid #eee;
}

4. Add the admin JavaScript

There are some variables near the top of this that you might want to change:

  • prev_el_selector — By default our map will be positioned below the input for our model’s longitude field. If you want it elsewhere, or you’ve changed the name of your longitude field, you’ll want to change this.

  • lat_input_selector and lon_input_selector — If your model’s latitude and longitude fields are called something other than latitude and longitude, you wierdo, you’ll have to change the values of these variables to reflect those.

  • initial_lat and initial_lon — The default centres the map in London, if the model has no location set. You’ll want to change these values if you live elsewhere or it’ll get really annoying.

  • initial_zoom and initial_with_loc_zoom — These are the map zoom levels we start with if the model has no location, or if it has a location, respectively. Feel free to tweak.

We put this file at appname/static/js/admin/location_picker.js (on GitHub, with additional fields and geocoding).

/**
 * Create a map with a marker.
 * Creating or dragging the marker sets the latitude and longitude values
 * in the input fields.
 */
;(function($) {

  // We'll insert the map after this element:
  var prev_el_selector = '.form-row.field-longitude';

  // The input elements we'll put lat/lon into and use
  // to set the map's initial lat/lon.
  var lat_input_selector = '#id_latitude',
      lon_input_selector = '#id_longitude';

  // If we don't have a lat/lon in the input fields,
  // this is where the map will be centered initially.
  var initial_lat = 51.516448,
      initial_lon = -0.130463;

  // Initial zoom level for the map.
  var initial_zoom = 6;

  // Initial zoom level if input fields have a location.
  var initial_with_loc_zoom = 12;

  // Global variables. Nice.
  var map, marker, $lat, $lon;

  /**
   * Create HTML elements, display map, set up event listeners.
   */
  function initMap() {
    var $prevEl = $(prev_el_selector);

    if ($prevEl.length === 0) {
      // Can't find where to put the map.
      return;
    };

    $lat = $(lat_input_selector);
    $lon = $(lon_input_selector);

    var has_initial_loc = ($lat.val() && $lon.val());

    if (has_initial_loc) {
      // There is lat/lon in the fields, so centre the map on that.
      initial_lat = parseFloat($lat.val());
      initial_lon = parseFloat($lon.val());
      initial_zoom = initial_with_loc_zoom;
    };

    $prevEl.after( $('<div class="js-setloc-map setloc-map"></div>') );

    var mapEl = document.getElementsByClassName('js-setloc-map')[0];

    map = new google.maps.Map(mapEl, {
      zoom: initial_zoom,
      center: {lat: initial_lat, lng: initial_lon}
    });

    // Create but don't position the marker:
    marker = new google.maps.Marker({
      map: map,
      draggable: true,
    });

    if (has_initial_loc) {
      // There is lat/lon in the fields, so centre the marker on that.
      setMarkerPosition(initial_lat, initial_lon);
    };

    google.maps.event.addListener(map, 'click', function(ev) {
      setMarkerPosition(ev.latLng.lat(), ev.latLng.lng());
    });

    google.maps.event.addListener(marker, 'dragend', function() {
      setInputValues(marker.getPosition().lat(), marker.getPosition().lng());
    });
  };

  /**
   * Re-position marker and set input values.
   */
  function setMarkerPosition(lat, lon) {
    marker.setPosition({lat: lat, lng: lon});
    setInputValues(lat, lon);
  };

  /**
   * Set both lat and lon input values.
   */
  function setInputValues(lat, lon) {
    setInputValue($lat, lat);
    setInputValue($lon, lon);
  };

  /**
   * Set the value of $input to val, with the correct decimal places.
   * We work out decimal places using the <input>'s step value, if any.
   */
  function setInputValue($input, val) {
    // step should be like "0.000001".
    var step = $input.prop('step');
    var dec_places = 0;

    if (step) {
      if (step.split('.').length == 2) {
        dec_places = step.split('.')[1].length;
      };

      val = val.toFixed(dec_places);
    };

    $input.val(val);
  };

  $(document).ready(function(){
    initMap();
  });

})(django.jQuery);

5. Load the CSS and JavaScript

In your model’s admin you’ll need to include the CSS and JavaScript. So, in our app’s admin.py (on GitHub) we have:

from django.conf import settings
from django.contrib import admin


@admin.register(Venue)
class VenueAdmin(admin.ModelAdmin):
    list_display = ('name', 'latitude', 'longitude',)
    search_fields = ('name',)

    fieldsets = (
        (None, {
            'fields': ( 'name', 'latitude', 'longitude',)
        }),
    )

    class Media:
        if hasattr(settings, 'GOOGLE_MAPS_API_KEY') and settings.GOOGLE_MAPS_API_KEY:
            css = {
                'all': ('css/admin/location_picker.css',),
            }
            js = (
                'https://maps.googleapis.com/maps/api/js?key={}'.format(settings.GOOGLE_MAPS_API_KEY),
                'js/admin/location_picker.js',
            )

It’s only really the class Media bit that’s new/unusual there.

6. Use it

Click on the map to create the marker. Click somewhere else to move the marker. Or drag it. The latitude and longitude values will update with the marker’s position.

If you want to remove the latitude and longitude from the model, delete the values from the fields as normal; don’t worry about there still being a marker on the map.

This solution was inspired by this blog post by Random Sequence which is a little out of date now. As mine will be one day.

Commenting is disabled on posts once they’re 30 days old.

16 Mar 2017 at Twitter

  • 5:42pm: Yeah I’m probably making life hard for myself again, doing more work than I need to, re-inventing a wheel. Whatever. It’s me.
  • 5:41pm: How I added a Google map in Django admin to set my model’s latitude and longitude gyford.com/phil/writing/2…

16 Mar 2017 in Links

Music listened to most that week

  1. Mammal Hands (7)
  2. Sports (6)
  3. Quasi (2)
  4. Pixies (2)
  5. Azealia Banks (2)
  6. De La Soul (2)
  7. Elastica (2)
  8. Art Brut (2)
  9. The Delgados (2)
  10. Burial (2)

More at Last.fm...