DEV Community

george_pollock
george_pollock

Posted on • Originally published at hvitis.dev on

Geolocation tutorial project with Geodjango and GIS data to build using REST api.

Boilerplate with Geodjango and Leaflet - learn while using it and deploy to Heroku

The topic we will cover here. Click to go to code examples:

Plus useful snippets:

ExTRA :

Important: We will be using vuejs with geodjango (connecting django rest framework api with javascript) throughout this tutorial - django-rest-framework-gis. This django package is compatible with both currently used django versions - Django 2 and Django 3. For more compatibility check geodjango documentation.

Get geolocation from the IP

There are 3 ways of obtaining geolocation based on IP address: (easy) ⚪ Call an external geocoding webservices API that returns location data (done from frontend or backend) (harder) 🟠 Use geo IP database with mapped IP.

First let’s get IP address from the django request. Let’s install django-ipware

pip install django-ipware
Enter fullscreen mode Exit fullscreen mode

and use it in class views :

from rest_framework.generics import ListAPIView
from rest_framework.response import Response
from rest_framework import status
from ipware import get_client_ip

class ShowUsersIP(ListPIView):
    """
    Sample API endpoint that shows user's IP
    """
    queryset = Message.objects.all()
    serializer_class = UserSerializer

    def get(self, request):
        client_ip, is_routable = get_client_ip(request)
        print('IP_INFO: ',client_ip, is_routable)
        data = {
            'client_ip': client_ip,
            'is_routable': is_routable
        }
        return Response(data, status=status.HTTP_200_OK)
Enter fullscreen mode Exit fullscreen mode

or function based views :

from django.http.response import JsonResponse
from ipware import get_client_ip

def show_users_ip(request):
    client_ip, is_routable = get_client_ip(request)
    data = {
        'client_ip': client_ip,
        'is_routable': is_routable
    }
    return JsonResponse(data)
Enter fullscreen mode Exit fullscreen mode

(The easy one) ⚪

Now that we know how to get the IP of the user we can track it’s location. With ip.info we get 50k calls on freemium which is more than enough. In order to obtain the location you need to pass the IP to the request:

import sys
import urllib.request
import json

ACCESS_TOKEN = 'YOUR_TOKEN'

def get_lat_lng_from_ip(ip_address):
    """Returns array that consists of latitude and longitude"""
    URL = 'https://ipinfo.io/{}?token={}'.format(ip_address, ACCESS_TOKEN)
    try:
        result = urllib.request.urlopen(URL).read()
        result = json.loads(result)
        result = result['loc']
        lat, lng = result.split(',')
        return [lat, lng]
    except:
        print("Could not load: ", URL)
        return None

location = get_lat_lng_from_ip('208.80.152.201')
print('Latitude: {0}, Longitude: {1}'.format(*location))
# Prints: Latitude: 32.7831, Longitude: -96.8067
Enter fullscreen mode Exit fullscreen mode

The response we get and transform to get lat and lng is this:

{
  "ip": "208.80.152.201",
  "city": "Dallas",
  "region": "Texas",
  "country": "US",
  "loc": "32.7831,-96.8067",
  "org": "AS14907 Wikimedia Foundation Inc.",
  "postal": "75270",
  "timezone": "America/Chicago"
}
Enter fullscreen mode Exit fullscreen mode

Responses vary between providers. Some of other service providers are: Google, OpenCage

Now let’s save the latitude and longitude we got from user’s IP to his account ! All examples are shown in the boilerplate code. The User model we have there extends Abstract user.

# User model
class User(AbstractUser):
    #...
    coordinates = models.PointField(blank=True, null=True, srid=4326)

# Update view
# ...
from rest_framework_gis.pagination import GeoJsonPagination
from backend.accounts.api.serializers import UpdateLocationSerializer
from backend.utils.coordinates import get_lat_lng_from_ip
from rest_framework.response import Response
from django.contrib.gis.geos import GEOSGeometry
from django.contrib.gis.geos import Point
from rest_framework import status
import json

class UpdateLocation(UpdateAPIView):
    """ Updates location of a user using it's IP taken from request"""
    model = User
    serializer_class = UpdateLocationSerializer
    pagination_class = GeoJsonPagination

    def update(self, request, *args, **kwargs):
        """Here we:
        - get location from the IP
        - change coordinates to a randomly
        close one in order to anonymize users location.
        """

        # Using ipware library
        client_ip, is_routable = get_client_ip(request)
        # Using our function created previously in utils
        latitude, longitude = get_lat_lng_from_ip(client_ip)
        if not latitude:
            return Response({'message': 'IP or location was not found.'}, status=status.HTTP_406_NOT_ACCEPTABLE)
        # Anonimizing users location before saving
        latitude, longitude = self.anonymize_location(latitude, longitude)
        print('Users latitude {} and longitude {}'.format(latitude, longitude))

        # For GeoDjango PointField we define y and x
        # more in docs: https://docs.djangoproject.com/en/3.1/ref/contrib/gis/geos/#point
        point = Point(float(longitude), float(latitude), srid=4326)

        # Requires at least 1 user in DB (e.g. admin)
        user_obj = User.objects.all().first()

        # Using partial update to not create a new user obj
        anonymize_profile = UpdateLocationSerializer(user_obj, data={'coordinates': point}, partial=True)
        if not anonymize_profile.is_valid():
            return Response(anonymize_profile.errors, status=status.HTTP_400_BAD_REQUEST)
        anonymize_profile.save()

        pnt = GEOSGeometry(point)
        geojson = json.loads(pnt.geojson)
        return Response({'coordinates': reversed(geojson['coordinates'])}, status=status.HTTP_201_CREATED)
Enter fullscreen mode Exit fullscreen mode

For your purposes there is a high chance you should use the first solution. There is many providers with free tiers offering many calls with high degree of accuracy.

(The hard one) 🟠

This option is based on downloading databases of IP and locations and querying it with the obtained IP of the user. You can check IP2location page to see how to do it. Install python package for querying the DB and download the BINs with data to query.

(for more loading dbsync data check out this realpython django tutorial)

Searching via Address

Here we talk about geocoding and reverse geocoding. You can imagine that although with IP we could have location data in our DB - here, with addresses it is not going to be possible because we need much higher accuracy and the amount of data that is needed to achieve it is a lot bigger. You will be forced to use external service’s API.

What if you don’t want to use Google Geocoding API? There is a geopy library that provides easy API access to Nominatim geocoding information that is open source. You just need to specify the name when using it. It can be name of your app.

Let’s see how easy it is:

>>> from geopy.geocoders import Nominatim
>>> geolocator = Nominatim(user_agent="mysuperapp")
>>> location = geolocator.geocode("Warsaw Culture Palace")
>>> print(location.address)
Pałac Kultury, 1, Plac Defilad, Śródmieście Północne, Warszawa, województwo mazowieckie, 00-110, Polska
>>> print((location.latitude, location.longitude))
(52.2317641, 21.005799675616117)
Enter fullscreen mode Exit fullscreen mode

In order to implement it we need to follow good REST API practices! How to implement it into django search endpoint?

First it’s important to follow a good URL naming practices:

yourapp.io/coordinates?address=QUERY_ADDRESS
Enter fullscreen mode Exit fullscreen mode

In Django we can implement it like this starting from urls.py:

# urls.py

from django.urls import path
from backend.accounts.api.views import GetCoordinatesFromAddress

app_name = 'accounts'

urlpatterns = [
    # ...
     path("coordinates", # the rest will be in views: ?address=QUERY_ADDRESS
          GetCoordinatesFromAddress.as_view(), name="get-coordinates"),
]
Enter fullscreen mode Exit fullscreen mode

and in views.py:

from geopy.geocoders import Nominatim
# ...

class GetCoordinatesFromAddress(ListAPIView, GeoFilterSet):
    """ Shows nearby Users"""

    def get(self, *args, **kwargs):
        # We can check on the server side the location of the users, using request
        # point = self.request.user.coordinates
        # ?address=QUERY_ADDRESS
        # QUERY_ADDRESS is the information user passes to the query
        QUERY_ADDRESS = self.request.query_params.get('address', None)

        if QUERY_ADDRESS not in [None, '']:
            # here we can use the geopy library:
            geolocator = Nominatim(user_agent="mysuperapp")
            location = geolocator.geocode(QUERY_ADDRESS)
            return Response({'coordinates': [location.latitude ,location.longitude]}, status=status.HTTP_200_OK)
        else:
            return Response({'message': 'No address was passed in the query'}, status=status.HTTP_400_BAD_REQUEST)
Enter fullscreen mode Exit fullscreen mode

Now if we call our app’s API with:

https://geodjango-rest-vue-boilerplate.herokuapp.com/api/accounts/nearbyusers?address=warsaw
Enter fullscreen mode Exit fullscreen mode

We get in response body:

{
  "coordinates": [52.2319581, 21.0067249]
}
Enter fullscreen mode Exit fullscreen mode

There is a front end example if you want to [try it][] or [see the code][].

Adding new objects on map

🚧🏗️👷 It will be here soon!

Closest object

🚧🏗️👷 It will be here soon!

Drawing polygons on map

🚧🏗️👷 It will be here soon!

List of objects within radius

from geopy.geocoders import Nominatim
from rest_framework_gis.filterset import GeoFilterSet
from rest_framework_gis import filters as geofilters
from django.db.models import Q
from django.contrib.gis.measure import Distance
# ...

class ListNearbyUsers(ListAPIView, GeoFilterSet):
    """ Shows nearby Users"""
    model = User
    serializer_class = NearbyUsersSerializer
    pagination_class = GeoJsonPagination
    contains_geom = geofilters.GeometryFilter(name='coordinates', lookup_expr='exists')

    def get_queryset(self, *args, **kwargs):
        QUERY_ADDRESS = self.request.query_params.get('address', None)
        queryset = []

        if QUERY_ADDRESS not in [None, '']:
            queryset = User.objects.all()
            # here we can use the geopy library:
            geolocator = Nominatim(user_agent="mysuperapp.com")
            location = geolocator.geocode(QUERY_ADDRESS)

            # Let's use the obtained information to create a geodjango Point
            point = Point(float(location.longitude), float(location.latitude), srid=4326)
            # and query for 10 Users objects to find active users within radius
            queryset = queryset.filter(Q(coordinates__distance_lt=(
                point, Distance(km=settings.RADIUS_SEARCH_IN_KM))) & Q(is_active=True)).order_by('coordinates')[0:10]
            return queryset
        else:
            return queryset
Enter fullscreen mode Exit fullscreen mode

Calculate distance between two points

🚧🏗️👷 It will be here soon!

Deployable boilerplate with all above

🚧🏗️👷 It will be here soon!


Did you make any mistakes when using REPLACETHIS or you’ve seen one here? Tell me about your insights. Leave a comment with YOUR opinion.

Top comments (0)