hyteck-blog/content/post/django-geocoding/index.md

154 lines
5.8 KiB
Markdown
Raw Permalink Normal View History

2024-09-28 10:02:10 +00:00
---
2024-09-28 11:33:15 +00:00
title: "Where are you? - Part 2 - Geocoding with Django to empower area search "
2024-10-04 12:15:53 +00:00
date: 2024-10-04T14:05:10+02:00
2024-09-28 10:02:10 +00:00
draft: false
2024-09-28 11:33:15 +00:00
image: "uploads/django_geocoding2.png"
2024-09-28 10:02:10 +00:00
categrories: ['English']
tags: ['django', 'geocoding', 'nominatim', 'OpenStreetMap', 'osm', 'traefik', 'mash-playbook', 'docker', 'docker-compose']
---
# Introduction
2024-10-04 12:14:41 +00:00
In the [previous post](geocoding-with-django/) I outlined how to set up a Nominatim server that allows us to find a geolocation for any address on the planet. Now let's use our newfound power in Django. Again, all code snippets are [CC0](https://creativecommons.org/public-domain/cc0/) so make free use of them. But I'd be very happy if you tell me if you use them for something cool!
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
## Prerquisites
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
* You have a working geocoding server or use a public one
* You have a working django app
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
If you want to do geocoding in a different environment you will still be able to use a lot of the the following examples, just skip the Django-specifics and configure the `GEOCODING_API_URL` according to your needs.
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
# Using the Geocoding API
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
First of all, let's define the geocoding API URL in our settings. This enables us to switch easily if a service is not available. Add the following to you `settings.py`
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
```python
# appname/settings.py
""" GEOCODING """
GEOCODING_API_URL = config.get("geocoding", "api_url", fallback="https://nominatim.hyteck.de/search") # Adjust if needed
```
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
We can then add a class that interacts with the API.
```python
import logging
import requests
import json
from APPNAME import __version__ as app_version
from APPNAME import settings
class GeoAPI:
api_url = settings.GEOCODING_API_URL
# Set User-Agent headers as required by most usage policies (and it's the nice thing to do)
headers = {
'User-Agent': f"APPNAME {app_version}",
'From': 'info@example.org'
}
def __init__(self, debug=False):
self.requests = requests # ignore why we do this for now
def get_coordinates_from_query(self, location_string):
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers).json()[0]
return result["lat"], result["lon"]
def _get_raw_response(self, location_string):
result = self.requests.get(self.api_url, {"q": location_string, "format": "jsonv2"}, headers=self.headers)
return result.content
def get_geojson_for_query(self, location_string):
try:
result = self.requests.get(self.api_url,
{"q": location_string,
"format": "jsonv2"},
headers=self.headers).json()
except Exception as e:
logging.warning(f"Exception {e} when querying Nominatim")
return None
if len(result) == 0:
logging.warning(f"Couldn't find a result for {location_string} when querying Nominatim")
return None
return result
```
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
The wrapper is a synchronous interface to our geocoding server and will wait until the server returns a response or times out. This impacts the user experienc, as a site will take longer to load. But it's much easier to code, so here we are. If anyone wants to write a async interface for this I'll not stop them!
Fornow, let's start by adding `Location` to our `models.py`
```python
class Location(models.Model):
place_id = models.IntegerField()
latitude = models.FloatField()
longitude = models.FloatField()
name = models.CharField(max_length=2000)
def __str__(self):
return f"{self.name} ({self.latitude:.5}, {self.longitude:.5})"
@staticmethod
def get_location_from_string(location_string):
geo_api = geo.GeoAPI()
geojson = geo_api.get_geojson_for_query(location_string)
if geojson is None:
return None
result = geojson[0]
if "name" in result:
name = result["name"]
else:
name = result["display_name"]
location = Location.objects.create(
place_id=result["place_id"],
latitude=result["lat"],
longitude=result["lon"],
name=name,
)
return location
```
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
*Don't forget to make&run migrations after this*
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
An finally we can use the API!
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
```python
location = Location.get_location_from_string("Berlin")
print(location)
# Berlin, Deutschland (52.51, 13.38)
2024-09-28 10:02:10 +00:00
```
2024-10-04 12:14:41 +00:00
Looking good!
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
# Area search
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
Now wee have the coordinates - great! But how can we get the distance between coordinates? Lukily we are not the first people with that question and there is the [Haversine Formula](https://en.wikipedia.org/wiki/Haversine_formula) that we can use. It's not a perfect fomula, for example it assumes the erth is perfectly round which the earth is not. But for most use cases of area search this should be irrelevant for the final result.
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
Here is my implementation
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
```python
def calculate_distance_between_coordinates(position1, position2):
"""
Calculate the distance between two points identified by coordinates
It expects the coordinates to be a tuple (lat, lon)
2024-09-28 10:16:35 +00:00
2024-10-04 12:14:41 +00:00
Based on https://en.wikipedia.org/wiki/Haversine_formula
"""
earth_radius_km = 6371 # As per https://en.wikipedia.org/wiki/Earth_radius
latitude1 = float(position1[0])
longitude1 = float(position1[1])
latitude2 = float(position2[0])
longitude2 = float(position2[1])
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
distance_lat = radians(latitude2 - latitude1)
distance_long = radians(longitude2 - longitude1)
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
a = pow(sin(distance_lat / 2), 2) + cos(radians(latitude1)) * cos(radians(latitude2)) * pow(sin(distance_long / 2),
2)
c = 2 * atan2(sqrt(a), sqrt(1 - a))
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
distance_in_km = earth_radius_km * c
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
return distance_in_km
```
2024-09-28 10:02:10 +00:00
2024-10-04 12:14:41 +00:00
And with that we have a functioning area search 🎉