Display items around based on geolocation into a Ionic 4 application

November 2019

In previous tutorials, we focused on users so now it's time to manage our bikes: to create some with our django admin, to get them based on the geolocation of the user. In our Ionic application, we want to display bikes around the user on a google map.
As a reminder here how our bikes model looks like:

class Bike(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    reference = models.CharField(max_length=100,db_index=True)
    qrCode = models.CharField(max_length=100, null=True, blank=True, db_index=True)
    picture = models.ImageField(upload_to="media/%Y/%m/%d",null=True, blank=True)
    location = models.PointField(null=True, blank=True)
    available = models.BooleanField(default=True)
    valid = models.BooleanField(default=True)
    class Meta:
        verbose_name = ('Bike')
        verbose_name_plural = ('Bikes')

Geolocate items in django backoffice

First we will use our Django admin interface, to add some bikes in our database.
And because we would like to manage/sse our bikes directly on a google map inside the django admin interface, we will use this wonderful django library. So we add:

django-map-widgets==0.2.0

in our requirements.txt file, and mapwidgets in the list of our installed applications:


INSTALLED_APPS = [
    'jet',
    'backoffice.apps.BackofficeConfig',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_docs',
    'oauth2_provider',
    'mapwidgets',
]

We will also add the MAP_WIDGETS configuration inside our settings.py file to declare the Google Map API Key that we get from here.

MAP_WIDGETS = {
    "GooglePointFieldWidget": (
        ("zoom", 15),
        ("mapCenterLocationName", "paris"),
        ("GooglePlaceAutocompleteOptions", {'componentRestrictions': {'country': 'fr'}}),
        ("markerFitZoom", 12),
    ),
    "GOOGLE_MAP_API_KEY": "yourapikey"
}

Now in our admin.py we will declare a new class BikeAdmin to better describe how we want Django to manage and display our models.

from django.contrib import admin
# *coding: utf-8*
from django.contrib import admin
from .models import *
from mapwidgets.widgets import GooglePointFieldInlineWidget
from django.contrib.gis.db.models import PointField
class BikeAdmin(admin.ModelAdmin):
    formfield_overrides = {
        PointField: {"widget": GooglePointFieldInlineWidget}
    }
    fieldsets = [
        (None, {'fields': ['reference','qrCode','picture','location','available','valid']}),
    ]
    list_display = ('location','available','valid',)
    list_filter = ('available','valid',)
admin.site.register(User)
admin.site.register(Bike,BikeAdmin)

We are using the mapwidgets library to display a google map helping us to manage the location field.
If everything goes well, you should be able when editing or adding a new Bike inside the Django admin, to see a google map and to search location by address using the google map search input box (you need to activate the google places service to do this).
This is awesome.

Please notice that if your google map api key is not valid, you will see a "For development purposes only" on the google map

Ok so now you can create as many bikes as you want into the backend.

Get geolocated items by distance and location using Django rest framework

The goal of our Ionic application is to display bikes around the user on a google map or thru a listing page. To do this, we will have to geolocate our user but also to ask our django backend to send us back, bikes ordered by location and distance. It is obvious than we need to order items by distance, since we don't want to display a bike located on New York if the user is in Paris...
Hopefully there is another great django library to manage it : Django Rest Framework Gis.
Using this add-on we will add geographic features to standard Django rest framework.

Let's add the library to our requiments.txt file.

pip install djangorestframework-gis

and add the rest_framework_gis to our installed apps after the rest_framework library


INSTALLED_APPS = [
    'jet',
    'backoffice.apps.BackofficeConfig', # <===== this is the important line
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework_gis',
    'rest_framework_docs',
    'oauth2_provider',
    'mapwidgets',
]

Ok so now we will add a new endpoint into our urls.py file inside our api directory, to get the bikes list

from django.urls import path
from django.conf.urls import include,url
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
from api.views import *
urlpatterns = [
    path('users/', UserListCreateView.as_view(), name='users_list'),
   # url(r'^user/(?P<pk>[0-9A-Fa-f-]+)/$', UserDetailView.as_view(), name='user_detail'),
    path('user/<uuid:pk>/',  UserDetailView.as_view(), name='user_detail'),
    path('', include(router.urls)),
    url(r'^createpaymentintent/$', createpaymentintent,name='createpaymentintent'),
    path('bikes/', BikeListView.as_view(), name='bikes_list'),
]

i added the bikes/ endpoint and define a new method BikeListView which will give us a list of bikes. You can notice that this time it will not be possible to create a bike thru the api.

class BikeListView(generics.ListAPIView):
    """
            get:
                Get list of bikes
    """
    permission_classes = [permissions.IsAuthenticated]
    queryset = Bike.objects.all()
    serializer_class = BikeSerializer

And we need to implement the BikeSerializer

class BikeSerializer(serializers.ModelSerializer):
    class Meta:
        model = Bike
        fields = '__all__'

Now you should be able to call the new api endpoint

curl -X GET \
  http://127.0.0.1:8000/api/bikes/ \
  -H 'Accept: */*' \
  -H 'Accept-Encoding: gzip, deflate' \
  -H 'Authorization: Bearer 3milTrOHBWoAOB4rBX8FWvpfOEETkO' \
  -H 'Cache-Control: no-cache' \
  -H 'Connection: keep-alive' \
  -H 'Host: 127.0.0.1:8000' \
  -H 'Postman-Token: 4c277988-a06e-4e89-8edc-5b2707ebb91a,bf96cb77-c3ce-47c2-88bd-8e604fa0d89b' \
  -H 'User-Agent: PostmanRuntime/7.18.0' \
  -H 'cache-control: no-cache'

And get as results a list of bikes

{
    "count": 5,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "7e06fe27-3d44-41e7-a578-abc3bb5cf21b",
            "reference": "B1",
            "qrCode": "B1",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.345623970031738,
                    48.86242796650965
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "df9aec74-990b-4698-902a-bd3d6e56cfec",
            "reference": "B2",
            "qrCode": "B2",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.35231876373291,
                    48.85734582508245
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "f2513216-1f16-43bc-8ad8-705c500e037a",
            "reference": "B3",
            "qrCode": "b3",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.321891784667969,
                    48.84212454876025
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "9c17a5c0-9713-4b94-b016-7a22a6318344",
            "reference": "B4",
            "qrCode": "b4",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.3590216,
                    48.8726678
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "a3bc6e4d-caf1-4721-8b7c-0d17da45dfb2",
            "reference": "B5",
            "qrCode": "B5",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.24153463575874,
                    48.838539720383366
                ]
            },
            "available": true,
            "valid": true
        }
    ]
}

But at this stage, your data is not geolocated and you don't know how the results are ordered.
So let's fix this, using the rest-framwork-gis that we imported into our django backend

from rest_framework_gis.filters import DistanceToPointFilter
class BikeListView(generics.ListAPIView):
    """
            get:
                Get list of bikes
    """
    permission_classes = [permissions.IsAuthenticated]
    queryset = Bike.objects.all()
    serializer_class = BikeSerializer
    distance_filter_field = 'location'
    filter_backends = (DistanceToPointFilter,)
    distance_filter_convert_meters = True 

The important parameters are:

distance_filter_field=location We indicate that the our geocoded field is location into our django models
filter_backends = (DistanceToPointFilter,) We want to filter by distance
distance_filter_convert_meters = True We indicate that the value passed to the API is meters

Now it is super easy to call the api by passing the position of our user (latitude and longitude) and a distance value (expressed in meters) and telling django to send back the bikes that are within the distance

http://127.0.0.1:8000/api/bikes/?dist=1000&point=2.345623970031738,48.86242796650965&format=json
dist The value expressed in meters where we want to search. Here 1km
point=2.345623970031738,48.86242796650965 The longitude/latitude around which we want to search

If a call the api with these parameters, this time my list of bikes will be shorter since in specified i only wanted results within 1000 meters

{
    "count": 2,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "7e06fe27-3d44-41e7-a578-abc3bb5cf21b",
            "reference": "B1",
            "qrCode": "B1",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.345623970031738,
                    48.86242796650965
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "df9aec74-990b-4698-902a-bd3d6e56cfec",
            "reference": "B2",
            "qrCode": "B2",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.35231876373291,
                    48.85734582508245
                ]
            },
            "available": true,
            "valid": true
        }
    ]
}

Results are automatically ordered by distance (closer to farest).
Et voila how to filter items based on geographic location.

As you could notice there is not distance field in the results of json, so if we want to display the distance inside the ionic application, we will have to calculate it.

That's why i'm going to show you how to get results ordered by distance using only standard Django framework.
We will modify our /bikes endpoint like this:

class BikeListView(generics.ListAPIView):
    """
            get:
                Get list of bikes
    """
    permission_classes = [permissions.IsAuthenticated]
    queryset = Bike.objects.all()
    serializer_class = BikeSerializer
    """
    distance_filter_field = 'location'
    filter_backends = (DistanceToPointFilter,)
    distance_filter_convert_meters = True
    """
    def get_queryset(self):
        latitude = self.request.query_params.get('latitude', None)
        longitude = self.request.query_params.get('longitude', None)
        max_distance = self.request.query_params.get('max_distance', None)
        if latitude and longitude:
            point_of_user = Point(float(longitude), float(latitude), srid=4326)
            # Here we're actually doing the query, notice we're using the Distance class fom gis.measure
            queryset = Bike.objects.filter(
                location__distance_lte=(
                    point_of_user,
                    Distance(km=float(max_distance))
                )
            ).annotate(distance_to_user = DistanceModel("location",point_of_user)).order_by('distance_to_user')
        else:
            queryset = Bike.objects.all()
        return queryset

i'm overriding the get_queryset method to get a latitude,longitude and max_distance parameters. The max_distance parameters will contain our value to filtered data expressed in Km this time

Next i'm declaring the queryset asking Django to get all data with distance less than the max_distance and to add this distance (annotate) into a field named distance_to_user

Now we need to modify our serializer to get this new field distance_to_user and to add it in the json response list

class BikeSerializer(serializers.ModelSerializer):
    distance = serializers.SerializerMethodField()
    class Meta:
        model = Bike
        fields = '__all__'
    def get_distance(self, obj):
        return obj.distance_to_user.km

Now if we call our endpoint with new parameters http://127.0.0.1:8000/api/bikes/?longitude=2.345623970031738&latitude=48.86242796650965&max_distance=2.5, we wil obtain:

Please notice that this time no need to invert latitude and longitude in your api call

{
    "count": 3,
    "next": null,
    "previous": null,
    "results": [
        {
            "id": "7e06fe27-3d44-41e7-a578-abc3bb5cf21b",
            "distance": 0.0,
            "reference": "B1",
            "qrCode": "B1",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.345623970031738,
                    48.86242796650965
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "df9aec74-990b-4698-902a-bd3d6e56cfec",
            "distance": 0.7478063848100001,
            "reference": "B2",
            "qrCode": "B2",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.35231876373291,
                    48.85734582508245
                ]
            },
            "available": true,
            "valid": true
        },
        {
            "id": "9c17a5c0-9713-4b94-b016-7a22a6318344",
            "distance": 1.5022571817400001,
            "reference": "B4",
            "qrCode": "b4",
            "picture": null,
            "location": {
                "type": "Point",
                "coordinates": [
                    2.3590216,
                    48.8726678
                ]
            },
            "available": true,
            "valid": true
        }
    ]
}

And as you can see the distance (expressed in km) is part of our results.

Ok now that we know how to get items around a specificied geographic position, we can switch to our ionic application, and display results on a google map.