LaVOZs

The World’s Largest Online Community for Developers

'; Custom latitude/longitude form field in Django - LavOzs.Com

One of my models has latitude and longitude fields which are stored in the database as floating point numbers. I like to keep it this way, because it allows me to work with them most efficiently.

I'd like the users to be able to edit them in the stock admin interface in this format: (+/-)DD MM SS.S (that's how most GPS devices present coordinates to the end user).

I've thought of three ways of implementing this:

  1. Use GeoDjango - too much overhead, I simply don't need the full framework only for two fields.
  2. Define a custom model field, somehow in this way. Seems like a lot of coding and I'm not entirely sure whether I would be able to access the floating point representation easily using the Django database interface.
  3. Use MultiValueField and MultiWidget - this wouldn't be an entirely bad solution, but is quite poorly documented and also involves a bit of coding and unnecessary widgets for degrees, minutes and seconds.

But ideally, I'd like to do this:

  • Use a custom form field which would use the standard TextInput form widget and standard FloatField model field.

I'm sure that the to_python() method could handle text input and convert it to float. But how do I tell Django to convert float to my lat/lng representation when editing the model? And how do I stick it all together?

Why not add some more fields to your model to hold the co-ordinate data, and then have the save() method of your model convert these to latitude and longitude figures? Then in the admin, make lat/lon read only so that the values can be viewed, but not edited. Or, you may decide not to show them at all!

For example:

class Location(models.Model):

    latitude = ...
    longitude = ...

    lat_degrees = models.IntegerField()
    lat_minutes = models.IntegerField()
    lat_seconds = models.FloatField()

    def save(self, *args, **kwargs):
        # Do the maths here to calculate lat/lon
        self.latitude = ... 
        self.longitude = ...
        super(Location, self).save(*args, **kwargs)

I assume you'll also need lon_degrees fields, I'm guessing, I'm no expert on co-ordinates. I've left those out of the example. You may also want to create a new widget for the admin to make it display nicely, or just override change_form.html to make the three fields appear on the same line, but that's slightly beyond the scope of this answer.

I recently had this requirement and got a little carried away, but I thought I'd share. (Django 2.0.)

I created a 30-character CharField to contain the coordinates as entered e.g. N 35º 44.265 W 41º 085.155 (I have no idea where that is, by the way ...) and arranged for the Model to store the field values.

import re
from django.core.exceptions import ValidationError

COORDINATES_REGEX = r'(?:[NS])\s*([0-9]{2})[\º\°]?\s+([0-9]{1,3}\.[0-9]{3})\s*(?:[EW])\s*([0-9]{2,3})[\º\°]?\s+([0-9]{2,3}\.[0-9]{3})'

def decode_coords_string(str):
    """
    Given a string, converts it to a decimal (lat, lng, 'OK', matched_string) tuple.
    If invalid, returns "(None, None, <some reason>, None)."

    Test for errors by checking that the coordinate is not 'None.'

    'matched_string' returns the actual extent of the matched string regardless of where in the input-string it was,
      for sanitizing the input when storing it in the database.  (If the input string contains only blanks, we should
      store an empty-string.)
    The model will replace the field-value with this matched-string.
    """
    # Dispose of empty input, returning an empty string(!) as the 'matched_string' in this case.
    r = re.compile(r'^\s*$')
    if r.match(str):
        return (None, None, 'Empty string', '')

    # Build the regex for coordinates.
    r = re.compile(COORDINATES_REGEX, re.IGNORECASE)

    # Try to match the string
    p = r.match(str)
    if p is None:
        return (None, None, 'Syntax error', None)

    # Get the pieces and expressly convert them to numeric types
    (lat_degs, lat_mins, lng_degs, lng_mins) = p.groups()

    lat_degs = int(lat_degs)
    lat_mins = float(lat_mins)
    lng_degs = int(lng_degs)
    lng_mins = float(lng_mins)

    # Throw out anything that simply does not make sense
    if (lat_degs > 180) or (lng_degs > 180) or (lat_mins > 60.0) or (lng_mins > 60.0):
        return (None, None, 'Degs/Mins value(s) out of bounds')

    latitude  =  float(lat_degs) + (lat_mins / 60.0)
    longitude = (float(lng_degs) + (lng_mins / 60.0)) * -1.0

    return (latitude, longitude, 'OK', p.group())


def validate_coords(str):
    """
    Simple validator for a coordinate string.
    """
    (lat, lng, reason, str2) = decode_coords_string(str)
    if lat is None:
        raise ValidationError('Invalid coordinates: ' + reason)

The input CharField specifies validators=[validate_coords] Notice also that the degrees-symbol can be specified several ways or omitted altogether.

And the Model includes the following short method:

def save(self, *args, **kwargs):
"""
Calculate and automatically populate the numeric lat/long figures.
This routine assumes that the string is either empty or that it has been validated.
An empty string – or, for that matter, an invalid one – will be (None, None).
"""

( lat, lng, reason, cleaned) = decode_coords_string(self.coordinate_str)

self.coordinate_str = cleaned
self.latitude       = lat
self.longitude      = lng
super().save(*args, **kwargs)

In the admin.py I exclude the latitude and longitude fields (both of which are Float fields) from view, to avoid confusing the user. The numeric fields are automatically calculated, but not displayed.

Related
Extending the User model with custom fields in Django
How do I add a custom column with a hyperlink in the django admin interface?
Writing custom Django form fields and widgets
Django- allowing math in form for DecimalField
differentiate null=True, blank=True in django
Using Django auth UserAdmin for a custom user model
django admin - add custom form fields that are not part of the model
Incorrect longitude/latitude saved to PointField via admin widget
How to split one form fields into multiple fields of a model in django?