Geolocation tutorial project with Geodjango and GIS data to build using REST api.
Published: 2020-11-16 16:57:43

---
title: Geolocation tutorial project with Geodjango and GIS data to build using REST api.
published: true
date: 2020-11-09 00:00:00 UTC
tags: 
canonical_url: https://hvitis.dev/geolocation-tutorial-geodjango-demo-and-gis-postgis-data-to-build-app-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:

- [**Get geolocation from the IP**](#geolocation-from-ip) - we will get coordinates of the IP that we extract from the request itself. I mention loading IP data from other DB to not use external API.
- [**Searching via Address**](#searching-via-address) - we will use [q django query](https://docs.djangoproject.com/en/3.1/topics/db/queries/#complex-lookups-with-q-objects) to search location fields.
- [**Adding new objects on map**](#adding-new-objects) - we will add a few objects using demo VueJS component with api from django rest framework.
- [**Closest object**](#closest-object) - we will use geodjango point to find closest object

Plus useful snippets:

- [**Drawing polygons on map**](#drawing-polygons) - we check here geodjango area.
- [**List of objects within radius**](#list-of-objects)
- [**Calculate distance between two points**](#calculate-points-distance) - backend and frontend solution.

**ExTRA** :

- [**Deployable boilerplate with all above**](#deployable-boilerplate) - how to prepare PostgreSQL database for geodjango on Heroku.

_Important:_ We will be using vuejs with geodjango (connecting django rest framework api with javascript) throughout this tutorial - [django-rest-framework-gis](https://github.com/openwisp/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](https://github.com/openwisp/django-rest-framework-gis#compatibility-with-drf-django-and-python).

- [Intercepting requests](#intercepting-requests-using-proxy-and-interceptor) - using proxy and interceptor to hack your workflow ( no [swagger](https://github.com/openwisp/django-rest-framework-gis#compatibility-with-drf-django-and-python)? no problem! )

## 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**](https://github.com/un33k/django-ipware)

```
pip install django-ipware
```

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)
```

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)
```

#### (The _easy_ one) ⚪

Now that we know how to get the IP of the user we can track it’s location. With [ip.info](https://github.com/un33k/django-ipware) 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
```

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"
}
```

_Responses vary between providers. Some of other service providers are: Google, [OpenCage](https://opencagedata.com/api)_

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](https://github.com/hvitis/geodjango-rest-vue-boilerplate). The User model we have [there extends Abstract user](https://github.com/hvitis/geodjango-rest-vue-boilerplate/blob/main/backend/accounts/models.py#L56).

```
# 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)
```

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](https://www.ip2location.com/development-libraries/ip2location/python) 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](https://realpython.com/location-based-app-with-geodjango-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](https://developers.google.com/maps/documentation/geocoding/overview)? There is a [geopy library](https://pypi.org/project/geopy/) that provides easy API access to [Nominatim geocoding information](https://nominatim.openstreetmap.org/ui/search.html?q=malaga+refino) 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)
```

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](https://hvitis.dev/rest-api-design-best-practices#querystring):

```
yourapp.io/coordinates?address=QUERY_ADDRESS
```

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"),
]
```

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)
```

Now if we call our app’s API with:

```
https://geodjango-rest-vue-boilerplate.herokuapp.com/api/accounts/nearbyusers?address=warsaw
```

We get in response body:

```
{
  "coordinates": [52.2319581, 21.0067249]
}
```

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
```

## Calculate distance between two points

🚧🏗️👷 It will be here soon!

## Deployable boilerplate with all above

🚧🏗️👷 It will be here soon!

<!-- - [**Build a Google X Near Me! X can be Restaurant, ATM Booth etc.**](#)

## <a name=""></a> -->
* * *

_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._