[Fixed]-Latitude/longitude widget for pointfield?

11πŸ‘

βœ…

Here’s my working custom field and widget:

class LatLongWidget(forms.MultiWidget):
    """
    A Widget that splits Point input into two latitude/longitude boxes.
    """

    def __init__(self, attrs=None, date_format=None, time_format=None):
        widgets = (forms.TextInput(attrs=attrs),
                   forms.TextInput(attrs=attrs))
        super(LatLongWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            return tuple(reversed(value.coords))
        return (None, None)

class LatLongField(forms.MultiValueField):

    widget = LatLongWidget
    srid = 4326

    default_error_messages = {
        'invalid_latitude' : _('Enter a valid latitude.'),
        'invalid_longitude' : _('Enter a valid longitude.'),
    }

    def __init__(self, *args, **kwargs):
        fields = (forms.FloatField(min_value=-90, max_value=90), 
                  forms.FloatField(min_value=-180, max_value=180))
        super(LatLongField, self).__init__(fields, *args, **kwargs)

    def compress(self, data_list):
        if data_list:
            # Raise a validation error if latitude or longitude is empty
            # (possible if LatLongField has required=False).
            if data_list[0] in validators.EMPTY_VALUES:
                raise forms.ValidationError(self.error_messages['invalid_latitude'])
            if data_list[1] in validators.EMPTY_VALUES:
                raise forms.ValidationError(self.error_messages['invalid_longitude'])
            # SRID=4326;POINT(1.12345789 1.123456789)
            srid_str = 'SRID=%d'%self.srid
            point_str = 'POINT(%f %f)'%tuple(reversed(data_list))
            return ';'.join([srid_str, point_str])
        return None
πŸ‘€ArturM

9πŸ‘

The answers are great for building your own solution, but if you want a simple solution that looks good and is easy to use, try:
http://django-map-widgets.readthedocs.io/en/latest/index.html

You can:

  • enter coordinates
  • select directly on the map
  • look up items from Google places.

Screen Shot from Django Map Widgets

πŸ‘€sww314

8πŸ‘

@Ramast, approaches the same issue in a more pythonic way. I have incorporated the changes to my code, it feels great to always improve.


This is how I managed -at last- to keep separate fields for lattitude and longitude without having to save them in the database since the values are already saved in the PointField.

The idea is :

  • If we are inserting a new entry, the latitude and longitude fields will be used to set the PointField
  • If we open an existing PointField entry, it will be used to provide the latitude and longitude values in the relevant FormFields.

models.py

from django.contrib.gis.db import models as geomodels


class Entry(geomodels.Model):
    point = geomodels.PointField(
        srid=4326,
        blank=True,
        )

admin.py

from myapp.forms import EntryForm
from django.contrib import admin


class EntryAdmin(admin.ModelAdmin):
    form = EntryForm


admin.site.register(Entry, EntryAdmin)

forms.py

from django import forms
from myapp.models import Entry
from django.contrib.gis.geos import Point


class MarketEntryForm(forms.ModelForm):

    latitude = forms.FloatField(
        min_value=-90,
        max_value=90,
        required=True,
    )
    longitude = forms.FloatField(
        min_value=-180,
        max_value=180,
        required=True,
    )

    class Meta(object):
        model = MarketEntry
        exclude = []
        widgets = {'point': forms.HiddenInput()}

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        coordinates = self.initial.get('point', None)
        if isinstance(coordinates, Point):
            self.initial['longitude'], self.initial['latitude'] = coordinates.tuple

    def clean(self):
        data = super().clean()
        latitude = data.get('latitude')
        longitude = data.get('longitude')
        point = data.get('point')
        if latitude and longitude and not point:
            data['point'] = Point(longitude, latitude)
        return data
πŸ‘€raratiru

4πŸ‘

This is based on @rara_tiru’s solution but with some improvements

class GisForm(forms.ModelForm):
    """
    This form can be used by any model that has "coordinates" field.
    It will show a better looking map than the default one
    """
    lat = forms.FloatField(required=False)
    lng = forms.FloatField(required=False)
    coordinates = PointField(widget=CustomPointWidget(), required=False, srid=4326)

    def __init__(self, *args, **kwargs):
        super(GisForm, self).__init__(*args, **kwargs)
        coordinates = self.initial.get("coordinates", None)
        if isinstance(coordinates, Point):
            self.initial["lng"], self.initial["lat"] = coordinates.tuple

    def clean(self):
        data = super(GisForm, self).clean()
        if "lat" in self.changed_data or "lng" in self.changed_data:
            lat, lng = data.pop("lat", None), data.pop("lng", None)
            data["coordinates"] = Point(lng, lat, srid=4326)

        if not (data.get("coordinates") or data.get("lat")):
            raise forms.ValidationError({"coordinates": "Coordinates is required"})
        return data

This will basically allow users to either set location on map or manually put lat/lng values. once saved the map will reflect the specificed lat/lng value

πŸ‘€Ramast

0πŸ‘

Create a custom widget for this, you can get inspired by SelectDateWidget here.

πŸ‘€Mounir

0πŸ‘

I myself was also looking for an answer to this. And being a beginner, I am quite confused by many different solutions. But I was able to achieve it with below code.

  1. Only Lat & Long text input is required to populate PointField and no map input is required and neither a map is seen in the form
  2. PointField can have null value
  3. Validates if the combination of the Lat Long value is set correct i.e., either both should be not null or both should be null.

Model Class

class DataSet(models.Model):
    id = models.AutoField(primary_key=True)
    name = models.CharField(max_length=20, blank=False, null=False, unique=True)
    latlong = geo_models.PointField(null=True,blank=True)

ModelForm Class

class DataSetForm(ModelForm):
lat = forms.FloatField(
    min_value=-90,
    max_value=90,
    required=False,
)
lng = forms.FloatField(
    min_value=-180,
    max_value=180,
    required=False,
)

class Meta:
    model = DataSet
    fields = ["name"]
    labels = {"name": "Name", "lat": "Latitude", "lng": "Longitude"}

def clean(self):
    super().clean()
    data = self.cleaned_data
    if (data['lat'] is None and data['lng'] is not None) or ( 
      data['lat'] is not None and data['lng'] is None):
       raise forms.ValidationError(
          _('Both Lat Long Values must be set'), code='invalid')

View Class

class AddDataSet(CreateView):
    model = DataSet
    form_class = DataSetForm
    template_name = "create.html"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context.update({"title": "Create Dataset"})
        return context

    def form_valid(self, form):
        lat = form.cleaned_data['lat']
        lng = form.cleaned_data['lng']
        response = super(CreateView, self).form_valid(form)
        if (lat is not None and lng is not None):
          self.object.latlong = Point(lat, lng)
        self.object.save()
        return response

I am not sure if there is a pitfall in this method. But it works as far as I tested.

πŸ‘€GP92

Leave a comment