Parfois, vous devrez peut-être exécuter des fonctions géospatiales au sein de votre application, telles que la cartographie des emplacements des utilisateurs ou l'analyse de données géographiques. Il existe de nombreuses bibliothèques spécifiques au langage disponibles pour ces tâches, telles que GDAL, Shapely et Geopandas for Python.
Alternativement, la fonctionnalité géospatiale peut être mise en œuvre via des bases de données ; par exemple, vous pouvez utiliser l'extension PostGIS avec une base de données relationnelle comme PostgreSQL, ou tirer parti de la prise en charge native des types de données spatiales dans une base de données distribuée comme Azure CosmosDB.
Toutefois, si votre stockage back-end, tel que Redis ou Google Spanner, ne prend pas en charge de manière native les requêtes spatiales et que vous devez gérer des requêtes géospatiales à grande échelle, cet article est conçu pour vous.
Vous pouvez toujours créer un autre microservice pour gérer les données spatiales, mais cette option implique souvent la surcharge liée à la maintenance d'une application supplémentaire. Une autre approche consiste à utiliser des bibliothèques de géo-indexation comme S2 et H3. S2, développé par Google, est basé sur la courbe de Hilbert, tandis que H3, développé par Uber, est basé sur un système de grille globale géodésique discrète. S2 et H3 partagent de nombreuses similitudes : tous deux divisent une région donnée en cellules et utilisent des entiers de 64 bits pour indexer ces cellules.
Cependant, la principale différence réside dans la forme des cellules ; S2 utilise des cellules de forme carrée, tandis que H3 utilise des cellules de forme hexagonale. Pour certaines applications, H3 peut offrir de meilleures performances. Cependant, dans l’ensemble, l’une ou l’autre bibliothèque devrait suffire. Dans cet article, nous utiliserons S2, mais vous pouvez exécuter des fonctions similaires en utilisant H3.
Niveaux de cellules : la hiérarchie permet différents niveaux de détail, des grandes régions aux petites zones précises. Chaque niveau représente une résolution différente :
Niveau 0 : Les plus grandes cellules, couvrant une partie importante de la surface terrestre.
Niveaux supérieurs : les cellules sont progressivement subdivisées en quadrants plus petits. Par exemple, les cellules de niveau 1 sont chacune divisées en quatre cellules de niveau 2, et ainsi de suite.
Résolution et zone : des niveaux plus élevés correspondent à des résolutions plus fines et à des zones de cellules plus petites. Cette hiérarchie permet une indexation et des requêtes précises à différents niveaux de détail.
Le tableau ci-dessous montre différents niveaux de cellules ainsi que leurs zones correspondantes.
niveau | superficie minimale | superficie maximale | superficie moyenne | unités | Nombre de cellules |
---|---|---|---|---|---|
00 | 85011012.19 | 85011012.19 | 85011012.19 | km2 | 6 |
01 | 21252753.05 | 21252753.05 | 21252753.05 | km2 | 24 |
02 | 4919708.23 | 6026521.16 | 5313188.26 | km2 | 96 |
03 | 1055377.48 | 1646455.50 | 1328297.07 | km2 | 384 |
04 | 231564.06 | 413918.15 | 332074.27 | km2 | 1536 |
05 | 53798.67 | 104297.91 | 83018.57 | km2 | 6K |
06 | 12948.81 | 26113.30 | 20754.64 | km2 | 24K |
07 | 3175.44 | 6529.09 | 5188.66 | km2 | 98 Ko |
08 | 786.20 | 1632.45 | 1297.17 | km2 | 393 Ko |
09 | 195,59 | 408.12 | 324.29 | km2 | 1573K |
dix | 48,78 | 102.03 | 81.07 | km2 | 6M |
11 | 12h18 | 25.51 | 20.27 | km2 | 25M |
12 | 3.04 | 6.38 | 5.07 | km2 | 100M |
13 | 0,76 | 1,59 | 1.27 | km2 | 402M |
14 | 0,19 | 0,40 | 0,32 | km2 | 1610M |
15 | 47520.30 | 99638.93 | 79172.67 | m2 | 6B |
16 | 11880.08 | 24909.73 | 19793.17 | m2 | 25B |
17 | 2970.02 | 6227.43 | 4948.29 | m2 | 103B |
18 | 742,50 | 1556.86 | 1237.07 | m2 | 412B |
19 | 185,63 | 389.21 | 309.27 | m2 | 1649B |
20 | 46.41 | 97h30 | 77.32 | m2 | 7T |
21 | 11h60 | 24h33 | 19h33 | m2 | 26T |
22 | 2,90 | 6.08 | 4,83 | m2 | 105T |
23 | 0,73 | 1,52 | 1.21 | m2 | 422T |
24 | 0,18 | 0,38 | 0,30 | m2 | 1689T |
25 | 453.19 | 950.23 | 755.05 | cm2 | 7e15 |
26 | 113h30 | 237,56 | 188,76 | cm2 | 27e15 |
27 | 28h32 | 59.39 | 47.19 | cm2 | 108e15 |
28 | 7.08 | 14h85 | 11h80 | cm2 | 432e15 |
29 | 1,77 | 3,71 | 2,95 | cm2 | 1729e15 |
30 | 0,44 | 0,93 | 0,74 | cm2 | 7e18 |
D'après le tableau fourni, il est évident que vous pouvez obtenir une précision de cartographie allant jusqu'à 0,44 cm^2 en utilisant S2. Dans chaque carré d'une cellule S2, il existe une cellule enfant qui partage le même parent, indiquant une structure hiérarchique. Le niveau de la cellule peut être une valeur statique (même niveau appliqué à toutes les cellules) ou peut être dynamique où S2 décide quelle résolution fonctionne le mieux.
Commençons par un exemple. Considérez que nous écrivons une application qui fournit des fonctionnalités de type service de proximité pour la région de Seattle. Nous souhaitons renvoyer une liste de cafés dans le voisinage donné. Pour réaliser ces opérations, nous diviserons cette tâche en 4 sous-tâches :
Pour charger une carte Google, nous utiliserions la bibliothèque gmplot. Cette bibliothèque nécessite une clé API Google Maps pour être chargée. Pour générer la clé API, suivez les instructions ici .
import gmplot import const # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) # Draw the map to an HTML file: gmap.draw('map.html')
Le code ci-dessus génère un fichier map.html comme indiqué ci-dessous :
Maintenant que nous avons la carte, dessinons quelques cellules S2 pour les cartes :
from s2 import * import gmplot # plot seattle with zoom level 13 gmap = gmplot.GoogleMapPlotter(47.6061, -122.3328, 13, apikey=const.API_KEY) areatobeplotted = [ (47.64395531736767,-122.43597221319135), (47.51369277846956,-122.43597221319135), (47.51369277846956,-122.24156866779164), (47.64395531736767,-122.24156866779164), (47.64395531736767,-122.43597221319135) ] region_rect = S2LatLngRect( S2LatLng.FromDegrees(47.51369277846956,-122.43597221319135), S2LatLng.FromDegrees(47.64395531736767, -122.24156866779164)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) geoms = 0 for cellid in covering: new_cell = S2Cell(cellid) vertices = [] for i in range(0, 4): vertex = new_cell.GetVertex(i) latlng = S2LatLng(vertex) vertices.append((latlng.lat().degrees(), latlng.lng().degrees())) gmap.polygon(*zip(*vertices), face_color='pink', edge_color='cornflowerblue', edge_width=5) geoms+=1 gmap.polygon(*zip(*areatobeplotted), face_color='red', edge_color='green', edge_width=5) print(f"Total Geometries: {geoms}") gmap.draw('/tmp/map.html')
Output: Total Geometries: 273
Dans le code ci-dessus, nous centrons d'abord le traceur Google Map autour de la région de Seattle. Dans S2RegionCoverer
, nous initialisons le couvreur de région pour avoir des niveaux dynamiques compris entre un niveau minimum de 8 et un niveau maximum de 15. Cela permet à S2 d'ajuster dynamiquement toutes les cellules dans des tailles de cellules spécifiques pour un ajustement optimal. La méthode GetCovering
renvoie le revêtement d'un rectangle autour de la région de Seattle.
Ensuite, nous parcourons chaque cellule, calculons les sommets des cellules et les traçons sur la carte. Nous maintenons le nombre de cellules générées à environ 273. Enfin, nous traçons le rectangle d'entrée en rouge. Ce code tracera les cellules S2 sur la carte de Seattle dans /tmp/map.html
, comme indiqué ci-dessous :
Générons une base de données de cafés avec leurs identifiants de cellules S2. Vous pouvez stocker ces cellules dans n'importe quelle base de données de votre choix. Pour ce tutoriel, nous utiliserons une base de données SQLite. Dans l'exemple de code ci-dessous, nous nous connectons à la base de données SQLite pour créer une table CoffeeShops
avec 3 champs Id
, name
et cell_id
.
Semblable à l'exemple précédent, nous utilisons S2RegionCoverer
pour calculer les cellules mais cette fois, nous utilisons un niveau fixe pour tracer les points. Enfin, l'ID calculé est converti en chaîne et stocké dans la base de données.
import sqlite3 from s2 import S2CellId,S2LatLng,S2RegionCoverer # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Create a table to store cell IDs cursor.execute('''CREATE TABLE IF NOT EXISTS CoffeeShops ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, cell_id TEXT )''') coverer = S2RegionCoverer() # Function to generate S2 cell ID for a given latitude and longitude def generate_cell_id(latitude, longitude, level=16): cell=S2CellId(S2LatLng.FromDegrees(latitude, longitude)) return str(cell.parent(level)) # Function to insert cell IDs into the database def insert_cell_ids(name,lat,lng): cell_id = generate_cell_id(lat, lng) cursor.execute("INSERT INTO CoffeeShops (name, cell_id) VALUES (?, ?)", (name, cell_id)) conn.commit() # Insert cell IDs into the database insert_cell_ids("Overcast Coffee", 47.616656277302155, -122.31156460382837) insert_cell_ids("Seattle Sunshine", 47.67366852914391, -122.29051997415843) insert_cell_ids("Sip House", 47.6682364706238, -122.31328618043693) insert_cell_ids("Victoria Coffee",47.624408595334536, -122.3117362652041) # Close connection conn.close()
À ce stade, nous disposons d'une base de données qui stocke les cafés ainsi que leurs identifiants de cellule, déterminés par la résolution sélectionnée pour le niveau de cellule.
Enfin, recherchons des cafés dans la région du district universitaire.
import sqlite3 from s2 import S2RegionCoverer,S2LatLngRect, S2LatLng # Connect to SQLite database conn = sqlite3.connect('/tmp/sqlite_cells.db') cursor = conn.cursor() # Function to query database for cells intersecting with the given polygon def query_intersecting_cells(start_x,start_y,end_x,end_y): # Create S2RegionCoverer region_rect = S2LatLngRect( S2LatLng.FromDegrees(start_x,start_y), S2LatLng.FromDegrees(end_x,end_y)) coverer = S2RegionCoverer() coverer.set_min_level(8) coverer.set_max_level(15) covering = coverer.GetCovering(region_rect) # Query for intersecting cells intersecting_cells = set() for cell_id in covering: cursor.execute("SELECT name FROM CoffeeShops WHERE cell_id >= ? and cell_id<=?", (str(cell_id.range_min()),str(cell_id.range_max()),)) intersecting_cells.update(cursor.fetchall()) return intersecting_cells # Query for intersecting cells intersecting_cells = query_intersecting_cells(47.6527847,-122.3286438,47.6782181, -122.2797203) # Print intersecting cells print("Intersecting cells:") for cell_id in intersecting_cells: print(cell_id[0]) # Close connection conn.close()
Output: Intersecting cells: Sip House Seattle Sunshine
Vous trouverez ci-dessous une représentation visuelle des cellules. Par souci de brièveté, le code de visualisation ci-dessous n’est pas ajouté.
Étant donné que toutes les cellules enfants et parents partagent un préfixe, nous pouvons rechercher des plages de cellules comprises entre min et max pour obtenir toutes les cellules comprises entre ces deux valeurs. Dans notre exemple, nous utilisons le même principe pour interroger un café
Dans cet article, nous avons montré comment utiliser la géo-indexation pour stocker et interroger des données géospatiales dans des bases de données qui ne prennent pas en charge les requêtes géospatiales. Cela peut être étendu à plusieurs cas d'utilisation tels que le calcul du routage entre 2 points ou l'obtention des voisins les plus proches.
En règle générale, pour les requêtes de bases de données géo-indexées, vous devrez effectuer un post-traitement supplémentaire sur les données. Une attention particulière à la logique d'interrogation et de post-traitement est nécessaire pour garantir que nous ne submergeons pas le nœud.