原创 JustinNeil 2024-12-20 08:30 重庆
点击关注公众号,“技术干货” 及时达!
前言
在数字化时代,地图服务已成为各类应用的标配,无论是导航、位置分享还是商业分析,地图都扮演着不可或缺的角色。然而,高质量的地图服务往往伴随着不菲的授权费用。公司原先使用的是国内某知名地图服务,但随着业务的扩展和成本的考量,老板决定寻找一种成本更低的解决方案。于是,我们的目光转向了免费的地图服务——天地图。
天地图简介
天地图(http://lbs.tianditu.gov.cn/server/guide.html)是中国领先的在线地图服务之一,提供全面的地理信息服务。它的API支持地理编码、逆地理编码、周边搜索等多种功能,且完全免费。这正是我们需要的。
具体实现代码
为了将天地图集成到我们的系统中,我们需要进行一系列的开发工作。以下是实现过程中的关键代码段。
逆地理编码
逆地理编码是将经纬度转换为可读的地址。在天地图中,这一功能可以通过以下代码实现:
public static MapLocation reverseGeocode(String longitude, String latitude) {Request request = new Request();LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));longitude = String.valueOf(locateInfo.getLongitude());latitude = String.valueOf(locateInfo.getLatitude());String postStr = String.format(REVERSE_GEOCODE_POST_STR, longitude, latitude);String encodedPostStr = null;try {encodedPostStr = URLEncoder.encode(postStr, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}String url = REVERSE_GEOCODE_URL + "?tk=" + TK + "&type=" + GEOCODE + "&postStr=" + encodedPostStr;request.setUrl(url);Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);if (response.getSuccess()) {String body = response.getBody();JSONObject jsonObject = JSON.parseObject(body);String status = jsonObject.getString("status");if (!"0".equals(status)) {return null;}JSONObject resultObject = jsonObject.getJSONObject("result");MapLocation mapLocation = new MapLocation();String formattedAddress = resultObject.getString("formatted_address");mapLocation.setAddress(formattedAddress);String locationStr = resultObject.getString("location");JSONObject location = JSON.parseObject(locationStr);String lon = location.getString("lon");String lat = location.getString("lat");locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lat), Double.valueOf(lon));lon = String.valueOf(locateInfo.getLongitude());lat = String.valueOf(locateInfo.getLatitude());mapLocation.setLongitude(lon);mapLocation.setLatitude(lat);JSONObject addressComponent = resultObject.getJSONObject("addressComponent");String address = addressComponent.getString("address");mapLocation.setName(address);mapLocation.setCity(addressComponent.getString("city"));return mapLocation;}return null;}
周边搜索
周边搜索允许我们根据一个地点的经纬度搜索附近的其他地点。实现代码如下:
public static List<MapLocation> nearbySearch(String query, String longitude, String latitude, String radius) {LocateInfo locateInfo = GCJ02_WGS84Utils.gcj02_To_Wgs84(Double.valueOf(latitude), Double.valueOf(longitude));longitude = String.valueOf(locateInfo.getLongitude());latitude = String.valueOf(locateInfo.getLatitude());Request request = new Request();String longLat = longitude + "," + latitude;String postStr = String.format(NEARBY_SEARCH_POST_STR, query, Integer.valueOf(radius), longLat);String encodedPostStr = null;try {encodedPostStr = URLEncoder.encode(postStr, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;request.setUrl(url);Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);List<MapLocation> list = new ArrayList<>();if (response.getSuccess()) {String body = response.getBody();JSONObject jsonObject = JSON.parseObject(body);JSONObject statusObject = jsonObject.getJSONObject("status");String infoCode = statusObject.getString("infocode");if (!"1000".equals(infoCode)) {return new ArrayList<>();}String resultType = jsonObject.getString("resultType");String count = jsonObject.getString("count");if (!"1".equals(resultType) || "0".equals(count)) {return new ArrayList<>();}JSONArray poisArray = jsonObject.getJSONArray("pois");for (int i = 0; i < poisArray.size(); i++) {JSONObject poiObject = poisArray.getJSONObject(i);MapLocation mapLocation = new MapLocation();mapLocation.setName(poiObject.getString("name"));mapLocation.setAddress(poiObject.getString("address"));String lonlat = poiObject.getString("lonlat");String[] lonlatArr = lonlat.split(",");locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));String lon = String.valueOf(locateInfo.getLongitude());String lat = String.valueOf(locateInfo.getLatitude());mapLocation.setLongitude(lon);mapLocation.setLatitude(lat);list.add(mapLocation);}}return list;}
文本搜索
文本搜索功能允许用户根据关键词搜索地点。实现代码如下:
public static List<MapLocation> searchByText(String query, String mapBound) {Request request = new Request();String postStr = String.format(SEARCH_BY_TEXT_POST_STR, query, mapBound);String encodedPostStr = null;try {encodedPostStr = URLEncoder.encode(postStr, "UTF-8");} catch (UnsupportedEncodingException e) {e.printStackTrace();}String url = SEARCH_URL + "?tk=" + TK + "&type=" + QUERY + "&postStr=" + encodedPostStr;request.setUrl(url);Response<String> response = HttpClientHelper.getWithoutUserAgent(request, null);List<MapLocation> list = new ArrayList<>();if (response.getSuccess()) {String body = response.getBody();JSONObject jsonObject = JSON.parseObject(body);JSONObject statusObject = jsonObject.getJSONObject("status");String infoCode = statusObject.getString("infocode");if (!"1000".equals(infoCode)) {return new ArrayList<>();}String resultType = jsonObject.getString("resultType");String count = jsonObject.getString("count");if (!"1".equals(resultType) || "0".equals(count)) {return new ArrayList<>();}JSONArray poisArray = jsonObject.getJSONArray("pois");for (int i = 0; i < poisArray.size(); i++) {JSONObject poiObject = poisArray.getJSONObject(i);MapLocation mapLocation = new MapLocation();mapLocation.setName(poiObject.getString("name"));mapLocation.setAddress(poiObject.getString("address"));String lonlat = poiObject.getString("lonlat");String[] lonlatArr = lonlat.split(",");LocateInfo locateInfo = GCJ02_WGS84Utils.wgs84_To_Gcj02(Double.valueOf(lonlatArr[1]), Double.valueOf(lonlatArr[0]));String lon = String.valueOf(locateInfo.getLongitude());String lat = String.valueOf(locateInfo.getLatitude());mapLocation.setLongitude(lon);mapLocation.setLatitude(lat);list.add(mapLocation);}}return list;}
坐标系转换
由于天地图使用的是WGS84坐标系,而国内常用的是GCJ-02坐标系,因此我们需要进行坐标转换。以下是坐标转换的工具类:
/*** WGS-84:是国际标准,GPS坐标(Google Earth使用、或者GPS模块)* GCJ-02:中国坐标偏移标准,Google Map、高德、腾讯使用* BD-09:百度坐标偏移标准,Baidu Map使用(经由GCJ-02加密而来)* <p>* 这些坐标系是对真实坐标系统进行人为的加偏处理,按照特殊的算法,将真实的坐标加密成虚假的坐标,* 而这个加偏并不是线性的加偏,所以各地的偏移情况都会有所不同,具体的内部实现是没有对外开放的,* 但是坐标之间的转换算法是对外开放,在网上可以查到的,此算法的误差在0.1-0.4之间。*/public class GCJ02_WGS84Utils {public static double pi = 3.1415926535897932384626;//圆周率public static double a = 6378245.0;//克拉索夫斯基椭球参数长半轴apublic static double ee = 0.00669342162296594323;//克拉索夫斯基椭球参数第一偏心率平方/*** 从GPS转高德* isOutOfChina 方法用于判断经纬度是否在中国范围内,如果不在中国范围内,则直接返回原始的WGS-84坐标。* transformLat 和 transformLon 是辅助函数,用于进行经纬度的转换计算。* 最终,wgs84ToGcj02 方法返回转换后的GCJ-02坐标系下的经纬度。*/public static LocateInfo wgs84_To_Gcj02(double lat, double lon) {LocateInfo info = new LocateInfo();if (isOutOfChina(lat, lon)) {info.setChina(false);info.setLatitude(lat);info.setLongitude(lon);} else {double dLat = transformLat(lon - 105.0, lat - 35.0);double dLon = transformLon(lon - 105.0, lat - 35.0);double radLat = lat / 180.0 * pi;double magic = Math.sin(radLat);magic = 1 - ee * magic * magic;double sqrtMagic = Math.sqrt(magic);dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);double mgLat = lat + dLat;double mgLon = lon + dLon;info.setChina(true);info.setLatitude(mgLat);info.setLongitude(mgLon);}return info;}//从高德转到GPSpublic static LocateInfo gcj02_To_Wgs84(double lat, double lon) {LocateInfo info = new LocateInfo();LocateInfo gps = transform(lat, lon);double lontitude = lon * 2 - gps.getLongitude();double latitude = lat * 2 - gps.getLatitude();info.setChina(gps.isChina());info.setLatitude(latitude);info.setLongitude(lontitude);return info;}// 判断坐标是否在国外private static boolean isOutOfChina(double lat, double lon) {if (lon < 72.004 || lon > 137.8347)return true;if (lat < 0.8293 || lat > 55.8271)return true;return false;}//转换private static LocateInfo transform(double lat, double lon) {LocateInfo info = new LocateInfo();if (isOutOfChina(lat, lon)) {info.setChina(false);info.setLatitude(lat);info.setLongitude(lon);return info;}double dLat = transformLat(lon - 105.0, lat - 35.0);double dLon = transformLon(lon - 105.0, lat - 35.0);double radLat = lat / 180.0 * pi;double magic = Math.sin(radLat);magic = 1 - ee * magic * magic;double sqrtMagic = Math.sqrt(magic);dLat = (dLat * 180.0) / ((a * (1 - ee)) / (magic * sqrtMagic) * pi);dLon = (dLon * 180.0) / (a / sqrtMagic * Math.cos(radLat) * pi);double mgLat = lat + dLat;double mgLon = lon + dLon;info.setChina(true);info.setLatitude(mgLat);info.setLongitude(mgLon);return info;}//转换纬度所需private static double transformLat(double x, double y) {double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y+ 0.2 * Math.sqrt(Math.abs(x));ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;ret += (20.0 * Math.sin(y * pi) + 40.0 * Math.sin(y / 3.0 * pi)) * 2.0 / 3.0;ret += (160.0 * Math.sin(y / 12.0 * pi) + 320 * Math.sin(y * pi / 30.0)) * 2.0 / 3.0;return ret;}//转换经度所需private static double transformLon(double x, double y) {double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1* Math.sqrt(Math.abs(x));ret += (20.0 * Math.sin(6.0 * x * pi) + 20.0 * Math.sin(2.0 * x * pi)) * 2.0 / 3.0;ret += (20.0 * Math.sin(x * pi) + 40.0 * Math.sin(x / 3.0 * pi)) * 2.0 / 3.0;ret += (150.0 * Math.sin(x / 12.0 * pi) + 300.0 * Math.sin(x / 30.0 * pi)) * 2.0 / 3.0;return ret;}}
结论
通过上述代码,我们成功地将天地图集成到了我们的系统中,不仅满足了功能需求,还大幅降低了成本。这一过程中,我们深入理解了地图服务的工作原理,也提升了团队的技术能力。
注意事项
确保在使用天地图API时遵守其服务条款,尤其是在商业用途中。
由于网络或其他原因,天地图API可能存在访问延迟或不稳定的情况,建议在生产环境中做好异常处理和备用方案。
坐标系转换是一个复杂的过程,确保使用可靠的算法和工具进行转换,以保证定位的准确性。
通过这次集成,我们不仅为公司节省了成本,还提升了系统的稳定性和用户体验。在未来的开发中,我们将继续探索更多高效、低成本的技术解决方案。
点击关注公众号,“技术干货” 及时达!
