背景
最近各大地图商齐刷刷的开始对地图的一些接口收费,特别是对商业用户。我在一些论坛上看到有水友吐槽,自己的APP用到了逆地址解析接口来获取当前城市,现在都要面临既收费、又限制调用频率和次数的问题,于是萌生了做一个国内城市逆地址解析接口的想法。
具体想法
具体实现并不难,主要分以下几步:
1、获取国内省市的地理轮廓
2、使用geo库解析轮廓
3、根据用户输入的坐标,按照“国——省——市”的顺序,找出坐标落在哪个市级范围内
开始实现
开发环境
操作系统:ubuntu 18.04
python版本: 3.8
django版本:2.2.4
postgrsql版本:10.23
开发步骤
1、下载省市轮廓,下载地址http://www.geojson.cn/preview,我是用python脚本下载的,格式是geojson
#!/user/bin/pyhton
import os
import urllib.request
import json
BASE_URL = 'https://geojson.cn/api/data'
TAR_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data', 'geojson')
def download_file(file_url, file_path):
urllib.request.urlretrieve(file_url, file_path)
if __name__ == '__main__':
cina_url = '/'.join([BASE_URL, '100000.json'])
cina_filename = os.path.join(TAR_DIR, '100000.json')
download_file(cina_url, cina_filename)
with open(cina_filename, 'r', encoding='UTF-8') as fd:
cina_data = json.load(fd)
prv_list = cina_data['features']
for index, prv_item in enumerate(prv_list):
props = prv_item['properties']
if 'code' in props:
print('index: %d, name: %s, code: %d' % (index, props['name'], props['code']))
try:
download_file('/'.join([BASE_URL, '%d.json' % props['code']]), os.path.join(TAR_DIR, '%d.json' % props['code']))
except:
print('[error]index: %d, name: %s, code: %d' % (index, props['name'], props['code']))
else:
print('index: %d, props: %s' % (index, props))
2、在django内构造省市区域的model
from django.contrib.gis.db import models
from common.base_model import BaseModel
class AdArea(BaseModel):
"""行政区域
Args:
BaseModel (_type_): _description_
"""
code = models.IntegerField(unique=True, null=False, verbose_name='区域编码')
name = models.CharField(max_length=256, verbose_name='名称')
fullname = models.CharField(max_length=256, verbose_name='区域全名')
center = models.PointField(verbose_name='地理中心')
children_num = models.IntegerField(verbose_name='子区域个数')
level = models.CharField(max_length=64, verbose_name='级别')
bbox = models.PolygonField(verbose_name='区域矩形边框')
parent_code = models.IntegerField(verbose_name='父区域编码')
mpoly = models.MultiPolygonField(verbose_name='区域地理边界')
mpoly2 = models.GeometryCollectionField(null=True, blank=True, verbose_name='区域地理边界2', help_text='mpoly无效几何的修正结果')
def __str__(self):
return self.name
class Meta:
indexes = [
models.Index(fields=["level"])
]
3、将geojson数据导入到postgresql数据库
4、构造rest api用于逆地址解析
from django.shortcuts import render
from django.contrib.gis.geos import Point
from django.contrib.gis.geos.error import GEOSException
from rest_framework import status
from rest_framework.decorators import api_view
from rest_framework.response import Response
from .models import AdArea
from .serializers import AdAreaSerializer
def is_in_china_bbox(latitude, longitude):
china_bbox = (73.502355, 17.98689826522479, 135.09567, 53.563269)
if latitude < china_bbox[1] or latitude > china_bbox[3]:
return False
if longitude < china_bbox[0] or longitude > china_bbox[2]:
return False
return True
def is_in_area(location, m):
if not m.bbox:
return True
try:
if not m.bbox.contains(location):
return False
if m.mpoly2 is not None:
return m.mpoly2.contains(location)
else:
return m.mpoly.contains(location)
except GEOSException:
print('=================geo contains error==================')
for poly in m.mpoly:
for ring in poly:
print(list(ring))
print('=================geo contains error==================')
raise
@api_view(['GET'])
def reverse_city(request):
"""经纬度逆解析-获取当前所在城市
Args:
http://localhost:8000/api/zzgeo/reverse_city/?longitude=120.592528&latitude=31.310623
Returns:
{
'code': 320500,
'name': '苏州',
'level': 'city'
}
"""
if request.method == 'GET':
latitude = float(request.GET['latitude'])
longitude = float(request.GET['longitude'])
if not is_in_china_bbox(latitude, longitude):
return Response(status=status.HTTP_404_NOT_FOUND)
city_location = Point(longitude, latitude)
province_list = AdArea.objects.filter(level='province')
for province_item in province_list:
if not is_in_area(city_location, province_item):
continue
city_list = AdArea.objects.filter(parent_code=province_item.code, level='city')
if not city_list:
return Response(AdAreaSerializer(province_item).data)
for city_item in city_list:
if not is_in_area(city_location, city_item):
continue
return Response(AdAreaSerializer(city_item).data)
return Response(status=status.HTTP_404_NOT_FOUND)
接口展示
总结
使用geodjango,将下载的省市geojson格式的轮廓导入到系统,构造rest api,在用户发起查询时,按照“国——省——市”的顺序,找出坐标落在哪个市级范围内。
本文为抛砖引玉,geo库有非常多,覆盖了几乎所有的编程语言,例如nodejs可以使用turfjs。
欢迎大家参与讨论。