在线版
SpringBoot 集成天地图实现定位导航(基于 GIS 坐标体系)
基于前文的坐标系转换能力,定位导航核心分为前端地图渲染 + 定位 、后端坐标处理 + 路径规划 、多端坐标系适配三部分。以下是完整的定位导航实现方案,包含天地图前端集成、定位接口、路径规划、导航轨迹纠偏等关键能力。
一、整体架构
plaintext
前端(Web/APP)→ 定位(GPS/天地图定位)→ 坐标上传后端 → 坐标系转换 → 路径规划(天地图/第三方)→ 导航指令返回 → 前端渲染导航轨迹
核心依赖:
- 前端:天地图 JS API(渲染地图、定位、导航轨迹)
- 后端:SpringBoot(坐标转换、接口封装、路径规划调用)
- 坐标系:统一后端用 GCJ02 处理,前端按地图类型(百度 / 高德 / 天地图)自动转换
二、前端集成天地图(核心:定位 + 地图渲染)
天地图提供 JS API,支持浏览器 / 移动端定位、地图标绘、轨迹渲染,是导航的基础。
1. 前端页面(HTML+JS)
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>天地图定位导航</title>
<!-- 引入天地图JS API(需替换为自己的Key) -->
<script src="http://api.tianditu.gov.cn/api?v=4.0&tk=你的天地图Key"></script>
<style>
#map {width: 100%; height: 800px;}
.nav-panel {padding: 10px; background: #fff; position: absolute; top: 10px; right: 10px; z-index: 999;}
</style>
</head>
<body>
<div class="nav-panel">
<button onclick="startLocate()">开始定位</button>
<button onclick="startNav()">规划导航(起点→终点)</button>
<div id="location-info"></div>
</div>
<div id="map"></div>
<script>
// 初始化天地图(GCJ02坐标系)
let map = new T.Map("map");
map.centerAndZoom(new T.LngLat(116.403874, 39.914885), 15); // 北京中心点
map.enableScrollWheelZoom(true); // 开启滚轮缩放
// 定位标记点
let locateMarker = null;
// 导航轨迹线
let navPolyline = null;
/**
* 1. 前端定位(基于浏览器/移动端定位)
*/
function startLocate() {
// 天地图定位控件
let geolocation = new T.Geolocation();
geolocation.getCurrentPosition(function (pos) {
// 定位结果(GCJ02坐标系)
let lng = pos.lnglat.getLng();
let lat = pos.lnglat.getLat();
document.getElementById("location-info").innerText = `当前定位:${lng}, ${lat}`;
// 在地图上标记定位点
if (locateMarker) map.removeOverLay(locateMarker);
locateMarker = new T.Marker(new T.LngLat(lng, lat));
map.addOverLay(locateMarker);
map.panTo(new T.LngLat(lng, lat)); // 地图中心移到定位点
// 可选:将定位坐标上传后端(如需后端处理)
uploadLocation(lng, lat);
}, function (error) {
alert("定位失败:" + error.message);
}, {enableHighAccuracy: true}); // 高精度定位
}
/**
* 2. 上传定位坐标到后端
*/
function uploadLocation(lng, lat) {
fetch('/api/location/upload', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({lng: lng, lat: lat, type: 'GCJ02'})
}).then(res => res.json())
.then(data => console.log("坐标上传成功:", data))
.catch(err => console.error("上传失败:", err));
}
/**
* 3. 导航路径规划(调用后端接口)
*/
function startNav() {
// 示例:起点(当前定位)、终点(天安门)
let startLng = 116.403874; // 实际替换为定位的lng
let startLat = 39.914885; // 实际替换为定位的lat
let endLng = 116.403988;
let endLat = 39.915144;
fetch(`/api/nav/plan?startLng=${startLng}&startLat=${startLat}&endLng=${endLng}&endLat=${endLat}`, {
method: 'GET'
}).then(res => res.json())
.then(path => {
// 渲染导航轨迹
if (navPolyline) map.removeOverLay(navPolyline);
let points = path.map(p => new T.LngLat(p.lng, p.lat));
navPolyline = new T.Polyline(points, {
color: "#FF0000", // 轨迹线颜色
weight: 5, // 线宽
opacity: 0.8
});
map.addOverLay(navPolyline);
// 缩放地图到轨迹范围
map.setViewport(points);
});
}
</script>
</body>
</html>
2. 前端适配其他地图(百度 / 高德)
如果前端需用百度 / 高德地图展示导航,需在前端调用后端的坐标转换接口:
javascript
运行
// 示例:后端GCJ02转BD09(百度地图)
function convertGcj02ToBd09(lng, lat) {
return fetch(`/gcj02/to/bd09?lng=${lng}&lat=${lat}`)
.then(res => res.json())
.then(bd => {
// 百度地图渲染:使用bd[0](经度)、bd[1](纬度)
return {lng: bd[0], lat: bd[1]};
});
}
三、后端核心功能实现(定位 + 导航)
1. 定位接口(接收前端定位坐标)
java
运行
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/location")
public class LocationController {
@Resource
private CoordinateConvertUtil coordinateConvertUtil;
/**
* 接收前端定位坐标(支持多坐标系入参)
*/
@PostMapping("/upload")
public Map<String, Object> uploadLocation(@RequestBody LocationDTO locationDTO) {
Map<String, Object> result = new HashMap<>();
double lng = locationDTO.getLng();
double lat = locationDTO.getLat();
String type = locationDTO.getType();
// 统一转换为GCJ02(天地图基准)
double[] gcj02;
switch (type) {
case "WGS84":
gcj02 = coordinateConvertUtil.wgs84ToGcj02(lng, lat);
break;
case "BD09":
gcj02 = coordinateConvertUtil.bd09ToGcj02(lng, lat);
break;
default: // GCJ02
gcj02 = new double[]{lng, lat};
}
// 可选:存储定位坐标到数据库(示例)
// locationService.save(gcj02[0], gcj02[1]);
result.put("success", true);
result.put("gcj02Lng", gcj02[0]);
result.put("gcj02Lat", gcj02[1]);
// 按需返回其他坐标系(如百度)
result.put("bd09", coordinateConvertUtil.gcj02ToBd09(gcj02[0], gcj02[1]));
return result;
}
// 定位DTO
public static class LocationDTO {
private double lng; // 经度
private double lat; // 纬度
private String type; // 坐标系类型:WGS84/GCJ02/BD09
// getter/setter
}
}
2. 路径规划接口(导航核心)
天地图本身提供路径规划 API(驾车 / 步行 / 骑行),后端封装接口并处理坐标系转换:
2.1 路径规划配置(application.yml)
yaml
tianditu:
key: 你的天地图Key
# 天地图路径规划接口
nav:
drive: http://api.tianditu.gov.cn/navigation?ds={"startlon":"%s","startlat":"%s","endlon":"%s","endlat":"%s","type":"0"}&tk=%s # 驾车
walk: http://api.tianditu.gov.cn/navigation?ds={"startlon":"%s","startlat":"%s","endlon":"%s","endlat":"%s","type":"1"}&tk=%s # 步行
ride: http://api.tianditu.gov.cn/navigation?ds={"startlon":"%s","startlat":"%s","endlon":"%s","endlat":"%s","type":"2"}&tk=%s # 骑行
2.2 路径规划服务实现
java
运行
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
@Service
public class NavService {
@Resource
private TiandituProperties tiandituProperties;
/**
* 路径规划(返回导航轨迹点列表)
* @param startLng 起点经度(GCJ02)
* @param startLat 起点纬度(GCJ02)
* @param endLng 终点经度(GCJ02)
* @param endLat 终点纬度(GCJ02)
* @param type 导航类型:0-驾车 1-步行 2-骑行
*/
public List<NavPointDTO> planNav(double startLng, double startLat, double endLng, double endLat, int type) throws IOException {
// 1. 拼接天地图路径规划接口URL
String navUrl = switch (type) {
case 1 -> String.format(tiandituProperties.getNav().getWalk(), startLng, startLat, endLng, endLat, tiandituProperties.getKey());
case 2 -> String.format(tiandituProperties.getNav().getRide(), startLng, startLat, endLng, endLat, tiandituProperties.getKey());
default -> String.format(tiandituProperties.getNav().getDrive(), startLng, startLat, endLng, endLat, tiandituProperties.getKey());
};
// 2. 调用天地图API
String result = doGet(navUrl);
JSONObject jsonResult = JSON.parseObject(result);
// 3. 解析轨迹点(天地图返回的轨迹点为GCJ02)
JSONArray pathArray = jsonResult.getJSONObject("route").getJSONArray("path");
List<NavPointDTO> navPoints = new ArrayList<>();
for (Object obj : pathArray) {
String[] lngLat = obj.toString().split(",");
NavPointDTO point = new NavPointDTO();
point.setLng(Double.parseDouble(lngLat[0]));
point.setLat(Double.parseDouble(lngLat[1]));
navPoints.add(point);
}
return navPoints;
}
// HTTP GET请求工具
private String doGet(String url) throws IOException {
CloseableHttpClient client = HttpClients.createDefault();
HttpGet httpGet = new HttpGet(url);
try (CloseableHttpResponse response = client.execute(httpGet)) {
return EntityUtils.toString(response.getEntity(), "UTF-8");
}
}
// 导航轨迹点DTO
public static class NavPointDTO {
private double lng; // 经度
private double lat; // 纬度
// getter/setter
}
}
2.3 导航接口控制器
java
运行
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
@RestController
@RequestMapping("/api/nav")
public class NavController {
@Resource
private NavService navService;
@Resource
private CoordinateConvertUtil coordinateConvertUtil;
/**
* 路径规划接口
* @param startLng 起点经度(默认GCJ02,支持WGS84/BD09)
* @param startLat 起点纬度
* @param endLng 终点经度
* @param endLat 终点纬度
* @param type 导航类型:0-驾车 1-步行 2-骑行
* @param coordType 入参坐标系类型:GCJ02(默认)/WGS84/BD09
*/
@GetMapping("/plan")
public List<NavService.NavPointDTO> planNav(
@RequestParam double startLng,
@RequestParam double startLat,
@RequestParam double endLng,
@RequestParam double endLat,
@RequestParam(defaultValue = "0") int type,
@RequestParam(defaultValue = "GCJ02") String coordType
) throws IOException {
// 1. 转换入参为GCJ02(天地图要求)
double[] startGcj02 = convertCoord(startLng, startLat, coordType);
double[] endGcj02 = convertCoord(endLng, endLat, coordType);
// 2. 调用路径规划
return navService.planNav(startGcj02[0], startGcj02[1], endGcj02[0], endGcj02[1], type);
}
// 坐标系转换(统一转GCJ02)
private double[] convertCoord(double lng, double lat, String coordType) {
return switch (coordType) {
case "WGS84" -> coordinateConvertUtil.wgs84ToGcj02(lng, lat);
case "BD09" -> coordinateConvertUtil.bd09ToGcj02(lng, lat);
default -> new double[]{lng, lat};
};
}
}
四、进阶功能:导航轨迹纠偏 + 实时定位
1. 轨迹纠偏(解决定位漂移)
实际定位中,GPS/WiFi 定位会有漂移,可通过天地图轨迹纠偏 API优化:
java
运行
/**
* 轨迹纠偏(天地图API)
* @param points 原始轨迹点(GCJ02)
*/
public List<NavPointDTO> correctTrack(List<NavPointDTO> points) throws IOException {
// 拼接轨迹点字符串:"lng1,lat1;lng2,lat2;..."
StringBuilder pointStr = new StringBuilder();
for (NavPointDTO point : points) {
pointStr.append(point.getLng()).append(",").append(point.getLat()).append(";");
}
String pointsParam = pointStr.substring(0, pointStr.length() - 1);
// 天地图轨迹纠偏接口
String correctUrl = String.format(
"http://api.tianditu.gov.cn/trackcorrection?ds={\"points\":\"%s\",\"type\":\"0\"}&tk=%s",
pointsParam, tiandituProperties.getKey()
);
// 解析返回的纠偏后轨迹点
String result = doGet(correctUrl);
JSONObject jsonResult = JSON.parseObject(result);
JSONArray correctPoints = jsonResult.getJSONArray("points");
List<NavPointDTO> resultPoints = new ArrayList<>();
for (Object obj : correctPoints) {
String[] lngLat = obj.toString().split(",");
NavPointDTO point = new NavPointDTO();
point.setLng(Double.parseDouble(lngLat[0]));
point.setLat(Double.parseDouble(lngLat[1]));
resultPoints.add(point);
}
return resultPoints;
}
2. 实时导航(WebSocket 推送轨迹)
实时导航需要后端主动推送最新轨迹 / 导航指令到前端,使用 WebSocket 实现:
java
运行
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException;
import java.util.List;
/**
* 实时导航WebSocket处理器
*/
public class NavWebSocketHandler extends TextWebSocketHandler {
@Resource
private NavService navService;
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
// 解析前端发送的导航参数(起点/终点/类型)
JSONObject param = JSON.parseObject(message.getPayload());
double startLng = param.getDouble("startLng");
double startLat = param.getDouble("startLat");
double endLng = param.getDouble("endLng");
double endLat = param.getDouble("endLat");
// 1. 规划路径
List<NavService.NavPointDTO> navPoints = navService.planNav(startLng, startLat, endLng, endLat, 0);
// 2. 模拟实时推送轨迹点(实际项目中按定位频率推送)
for (NavService.NavPointDTO point : navPoints) {
session.sendMessage(new TextMessage(JSON.toJSONString(point)));
Thread.sleep(1000); // 模拟1秒推送一个点
}
}
}
五、关键注意事项
1. 天地图 API 权限
- 路径规划、轨迹纠偏等接口需要在天地图开发者平台开通对应权限(部分接口需审核);
- 注意接口调用频率限制(免费版有 QPS 限制,高并发需申请商用版)。
2. 坐标系一致性
- 后端统一用 GCJ02 处理所有逻辑,前端根据地图类型(天地图 / 百度 / 高德)转换后渲染;
- 定位时优先使用前端天地图定位(直接返回 GCJ02),避免多次转换导致精度损失。
3. 定位精度优化
- 移动端优先使用 GPS + 网络定位(高精度),浏览器端使用 HTML5 Geolocation;
- 对定位结果做防抖处理(如连续 3 次定位取平均值),减少漂移。
4. 离线导航(可选)
- 如需离线导航,需下载天地图离线瓦片包,后端预生成离线路径规划数据;
- 离线场景下,坐标系转换仍使用本地算法(无需调用 API)。
六、测试验证
- 启动 SpringBoot 应用,访问前端页面:
http://localhost:8080/index.html; - 点击「开始定位」,页面显示当前 GCJ02 坐标并标记在地图上;
- 点击「规划导航」,后端调用天地图路径规划 API,前端渲染红色导航轨迹;
- 如需适配百度地图,调用
/gcj02/to/bd09接口转换坐标后,在百度地图上渲染。
离线版
SpringBoot 集成天地图实现离线定位导航(完整方案)
离线导航的核心是摆脱对天地图在线 API 的依赖,需提前下载离线地图瓦片、预生成导航路径数据、本地化坐标系转换,并基于离线地理数据完成定位和导航计算。以下是从「离线资源准备」到「后端 + 前端实现」的全流程方案。
一、离线导航核心原理
离线导航无需调用天地图在线接口,核心依赖 3 类离线资源:
| 资源类型 | 作用 | 获取方式 |
|---|---|---|
| 地图瓦片 | 前端渲染离线地图(无网络时显示地图) | 天地图离线瓦片下载工具 / 官方授权 |
| 离线 POI / 路网数据 | 后端路径规划(驾车 / 步行路线计算) | 开源路网数据(如 OSM)/ 商用 GIS 数据 |
| 坐标系转换算法 | 本地化坐标转换(无网络也能转 GCJ02/BD09) | 复用前文的CoordinateConvertUtil |
二、离线资源准备
1. 离线地图瓦片下载(前端渲染用)
天地图瓦片采用 WMTS 规范,需下载指定区域(如城市)的瓦片文件(格式:png/jpg),按「层级 (z)/ 行 (y)/ 列 (x)」目录存储。
1.1 下载工具推荐
- 开源工具:
TiandituTileDownloader(GitHub)、水经注万能地图下载器(支持天地图瓦片); - 下载参数:
- 坐标系:GCJ02(和天地图一致);
- 层级:建议下载 1-18 级(1-10 级为概览,11-18 级为详细道路);
- 区域:按经纬度范围下载(如北京市:115.7°E-117.4°E,39.4°N-41.6°N)。
1.2 瓦片存储结构
下载后按天地图瓦片 URL 规则组织目录(前端可直接读取):
plaintext
offline-tiles/
├── vec_w/ # 矢量地图瓦片
│ ├── 1/ # 层级1
│ │ ├── 0/ # 行y
│ │ │ └── 0.png # 列x
│ ├── 2/
│ └── ...
└── cva_w/ # 注记瓦片(道路名称/POI)
├── 1/
└── ...
2. 离线路网数据(后端路径规划用)
在线路径规划依赖天地图 API,离线场景需替换为本地路网数据:
2.1 开源路网数据来源
- OpenStreetMap(OSM):免费开源的全球路网数据,可下载指定区域的
.osm文件(https://www.openstreetmap.org/export); - 处理工具:用
osm2pgrouting将 OSM 数据导入 PostgreSQL+PostGIS(GIS 数据库),便于路径计算。
2.2 本地化路径规划引擎
- 轻量方案:使用
GraphHopper(Java 开源导航引擎,支持离线路径规划,兼容 OSM 数据); - 集成方式:将 GraphHopper 嵌入 SpringBoot,预加载 OSM 路网数据,本地计算最短路径。
三、后端离线导航实现
1. 核心依赖(新增离线导航引擎)
xml
<!-- PostGIS(可选,存储路网数据) -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.6.0</version>
</dependency>
<!-- GraphHopper(离线路径规划) -->
<dependency>
<groupId>com.graphhopper</groupId>
<artifactId>graphhopper-core</artifactId>
<version>8.0</version>
</dependency>
<!-- OSM数据解析 -->
<dependency>
<groupId>com.graphhopper</groupId>
<artifactId>graphhopper-reader-osm</artifactId>
<version>8.0</version>
</dependency>
2. 离线路径规划配置(GraphHopper)
java
运行
import com.graphhopper.GraphHopper;
import com.graphhopper.config.CHProfile;
import com.graphhopper.config.Profile;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 离线导航引擎配置(GraphHopper)
*/
@Configuration
public class OfflineNavConfig {
/**
* 初始化GraphHopper引擎(预加载OSM路网数据)
*/
@Bean
public GraphHopper graphHopper() {
GraphHopper hopper = new GraphHopper();
// 1. 设置OSM离线路网文件路径(提前下载的北京市OSM文件)
String osmFile = "data/osm/beijing-latest.osm.pbf";
hopper.setOSMFile(osmFile);
// 2. 设置缓存目录(存储路网索引)
hopper.setGraphHopperLocation("data/graphhopper-cache");
// 3. 配置导航模式(驾车/步行/骑行)
hopper.setProfiles(
new Profile("car").setVehicle("car").setWeighting("fastest"), // 驾车(最快路线)
new Profile("foot").setVehicle("foot").setWeighting("shortest"), // 步行(最短路线)
new Profile("bike").setVehicle("bike").setWeighting("balanced") // 骑行
);
// 4. 启用Contraction Hierarchies(加速路径计算)
hopper.getCHPreparationHandler().setCHProfiles(new CHProfile("car"), new CHProfile("foot"));
// 5. 构建路网图(首次启动耗时较长,后续复用缓存)
hopper.importOrLoad();
return hopper;
}
}
3. 离线路径规划服务(替代天地图在线 API)
java
运行
import com.graphhopper.GHRequest;
import com.graphhopper.GHResponse;
import com.graphhopper.GraphHopper;
import com.graphhopper.PathWrapper;
import com.graphhopper.util.PointList;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* 离线导航路径规划服务
*/
@Service
public class OfflineNavService {
@Resource
private GraphHopper graphHopper;
@Resource
private CoordinateConvertUtil coordinateConvertUtil;
/**
* 离线路径规划(输入WGS84/GCJ02/BD09坐标,统一转WGS84计算)
* @param startLng 起点经度
* @param startLat 起点纬度
* @param endLng 终点经度
* @param endLat 终点纬度
* @param type 导航类型:car/foot/bike
* @param coordType 入参坐标系:WGS84/GCJ02/BD09
*/
public List<NavPointDTO> planOfflineNav(double startLng, double startLat, double endLng, double endLat,
String type, String coordType) {
// 1. 统一转换为WGS84(GraphHopper默认使用WGS84)
double[] startWgs84 = convertToWgs84(startLng, startLat, coordType);
double[] endWgs84 = convertToWgs84(endLng, endLat, coordType);
// 2. 构建离线导航请求
GHRequest request = new GHRequest(startWgs84[1], startWgs84[0], endWgs84[1], endWgs84[0])
.setProfile(type) // 导航模式:car/foot/bike
.setLocale("zh-CN");
// 3. 执行离线路径计算
GHResponse response = graphHopper.route(request);
if (response.hasErrors()) {
throw new RuntimeException("离线路径规划失败:" + response.getErrors());
}
// 4. 解析轨迹点(WGS84),按需转回GCJ02/BD09
PathWrapper path = response.getBest();
PointList pointList = path.getPoints();
List<NavPointDTO> navPoints = new ArrayList<>();
for (int i = 0; i < pointList.size(); i++) {
double wgsLng = pointList.getLon(i);
double wgsLat = pointList.getLat(i);
// 转回GCJ02(前端天地图渲染用)
double[] gcj02 = coordinateConvertUtil.wgs84ToGcj02(wgsLng, wgsLat);
NavPointDTO point = new NavPointDTO();
point.setLng(gcj02[0]);
point.setLat(gcj02[1]);
// 可选:返回距离、耗时
point.setDistance(path.getDistance());
point.setTime(path.getTime() / 1000); // 秒
navPoints.add(point);
}
return navPoints;
}
/**
* 任意坐标系转WGS84(GraphHopper计算用)
*/
private double[] convertToWgs84(double lng, double lat, String coordType) {
return switch (coordType) {
case "GCJ02" -> coordinateConvertUtil.gcj02ToWgs84(lng, lat);
case "BD09" -> coordinateConvertUtil.bd09ToWgs84(lng, lat);
default -> new double[]{lng, lat}; // WGS84
};
}
// 导航轨迹点DTO(扩展距离/耗时)
public static class NavPointDTO {
private double lng; // 经度(GCJ02)
private double lat; // 纬度(GCJ02)
private double distance; // 累计距离(米)
private double time; // 累计耗时(秒)
// getter/setter
}
}
4. 离线定位接口(无网络定位)
离线定位依赖设备本地定位能力(GPS / 北斗),后端仅接收定位坐标并做本地化处理:
java
运行
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* 离线定位接口(接收GPS原始坐标)
*/
@RestController
@RequestMapping("/api/offline/location")
public class OfflineLocationController {
@Resource
private CoordinateConvertUtil coordinateConvertUtil;
@PostMapping("/upload")
public Map<String, Object> uploadOfflineLocation(@RequestBody OfflineLocationDTO dto) {
Map<String, Object> result = new HashMap<>();
// 1. GPS原始坐标(WGS84)转GCJ02(前端天地图渲染)
double[] gcj02 = coordinateConvertUtil.wgs84ToGcj02(dto.getLng(), dto.getLat());
// 2. 可选:转BD09(百度地图渲染)
double[] bd09 = coordinateConvertUtil.gcj02ToBd09(gcj02[0], gcj02[1]);
result.put("success", true);
result.put("gcj02", new double[]{gcj02[0], gcj02[1]});
result.put("bd09", bd09);
// 离线场景:无需调用天地图API,直接返回转换结果
return result;
}
// 离线定位DTO
public static class OfflineLocationDTO {
private double lng; // GPS原始经度(WGS84)
private double lat; // GPS原始纬度(WGS84)
private long time; // 定位时间戳
// getter/setter
}
}
四、前端离线导航实现
1. 离线地图瓦片加载(替换在线瓦片)
天地图 JS API 支持加载离线瓦片,修改前端地图初始化逻辑:
html
预览
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>离线导航</title>
<!-- 引入天地图离线版JS API(需下载到本地) -->
<script src="js/tianditu-api-4.0.min.js"></script>
<style>
#map {width: 100%; height: 800px;}
</style>
</head>
<body>
<div id="map"></div>
<script>
// 1. 自定义离线瓦片图层(替换在线URL)
var OfflineTileLayer = T.TileLayer.extend({
getTileUrl: function (tilePoint) {
// 离线瓦片路径:按z/y/x组织
var z = tilePoint.z;
var y = tilePoint.y;
var x = tilePoint.x;
// 本地瓦片文件路径(需部署到静态资源目录)
return `./offline-tiles/vec_w/${z}/${y}/${x}.png`;
},
// 瓦片范围(和下载的区域一致)
maxZoom: 18,
minZoom: 1,
projection: "EPSG:4326" // GCJ02坐标系
});
// 2. 初始化离线地图
var map = new T.Map("map");
// 添加离线矢量图层
var offlineLayer = new OfflineTileLayer();
map.addLayer(offlineLayer);
// 添加离线注记图层(可选)
var offlineCvaLayer = new T.TileLayer.extend({
getTileUrl: function (tilePoint) {
return `./offline-tiles/cva_w/${tilePoint.z}/${tilePoint.y}/${tilePoint.x}.png`;
}
})();
map.addLayer(offlineCvaLayer);
// 3. 地图定位(离线:仅使用GPS/北斗,不调用在线接口)
map.centerAndZoom(new T.LngLat(116.403874, 39.914885), 15);
map.enableScrollWheelZoom(true);
// 4. 离线定位(仅读取设备GPS,无网络)
function startOfflineLocate() {
// 移动端:调用原生GPS接口(如Android/iOS的LocationManager)
// 浏览器端:仅支持HTTPS+HTML5 Geolocation(无网络时依赖GPS硬件)
if (navigator.geolocation) {
navigator.geolocation.watchPosition(function (pos) {
// GPS原始坐标(WGS84)
var wgsLng = pos.coords.longitude;
var wgsLat = pos.coords.latitude;
// 前端本地转换为GCJ02(复用后端的坐标转换算法,前端实现JS版)
var gcj02 = wgs84ToGcj02(wgsLng, wgsLat);
// 在离线地图上标记定位点
var marker = new T.Marker(new T.LngLat(gcj02[0], gcj02[1]));
map.addOverLay(marker);
map.panTo(new T.LngLat(gcj02[0], gcj02[1]));
}, function (err) {
alert("离线定位失败:" + err.message);
}, {
enableHighAccuracy: true, // 高精度(仅GPS)
maximumAge: 0, // 不使用缓存
timeout: 5000
});
}
}
// 5. 前端JS版WGS84转GCJ02(和后端算法一致,离线使用)
function wgs84ToGcj02(lng, lat) {
var PI = 3.1415926535897932384626;
var A = 6378245.0;
var EE = 0.00669342162296594323;
if (outOfChina(lng, lat)) {
return [lng, lat];
}
var dLat = transformLat(lng - 105.0, lat - 35.0);
var dLng = transformLng(lng - 105.0, lat - 35.0);
var radLat = lat / 180.0 * PI;
var magic = Math.sin(radLat);
magic = 1 - EE * magic * magic;
var sqrtMagic = Math.sqrt(magic);
dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI);
dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);
return [lng + dLng, lat + dLat];
}
// 前端辅助函数(和后端一致)
function transformLat(x, y) { /* 复制后端transformLat逻辑 */ }
function transformLng(x, y) { /* 复制后端transformLng逻辑 */ }
function outOfChina(lng, lat) { /* 复制后端outOfChina逻辑 */ }
// 6. 调用离线路径规划接口
function planOfflineNav(startLng, startLat, endLng, endLat) {
fetch('/api/offline/nav/plan', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
startLng: startLng,
startLat: startLat,
endLng: endLng,
endLat: endLat,
type: 'car',
coordType: 'GCJ02'
})
}).then(res => res.json())
.then(points => {
// 渲染离线导航轨迹
var pathPoints = points.map(p => new T.LngLat(p.lng, p.lat));
var polyline = new T.Polyline(pathPoints, {
color: "#FF0000",
weight: 5
});
map.addOverLay(polyline);
});
}
// 启动离线定位
startOfflineLocate();
// 示例:规划离线导航(起点当前定位,终点天安门)
planOfflineNav(116.403874, 39.914885, 116.403988, 39.915144);
</script>
</body>
</html>
2. 前端离线资源部署
- 将下载的离线瓦片目录
offline-tiles放到 SpringBoot 的resources/static目录下; - 天地图 JS API 下载到本地(
static/js/tianditu-api-4.0.min.js),避免在线加载; - 移动端需将瓦片打包到 APP 安装包(如 Android 的
assets目录)。
五、离线导航关键优化
1. 路网数据轻量化
- OSM 原始数据较大(如北京市约 1GB),可使用
osmfilter过滤无关数据(仅保留道路、POI); - GraphHopper 支持「按需加载」,仅加载当前区域的路网数据,减少内存占用。
2. 定位精度优化(离线)
- 无网络时,GPS 定位易受遮挡(如室内),可结合惯性导航(IMU)补位;
- 对 GPS 坐标做「卡尔曼滤波」,减少漂移(前端 / 后端均可实现)。
3. 离线瓦片压缩
- 使用
gzip压缩瓦片文件(减少存储体积),前端加载时自动解压; - 合并小瓦片为大图块(如将 4 个 18 级瓦片合并为 1 个),减少文件 IO。
4. 离线更新策略
- 定期更新离线瓦片和路网数据(如每月),通过本地文件 / 蓝牙传输更新包;
- 支持「增量更新」:仅下载变化的瓦片 / 路网,避免全量更新。
六、离线导航部署注意事项
1. 后端部署
- 离线导航引擎(GraphHopper)首次启动需构建路网索引(耗时较长),建议预构建后打包部署;
- 生产环境需分配足够内存(建议 4GB 以上),避免路网数据加载失败。
2. 移动端适配
- Android/iOS 需申请定位权限(
ACCESS_FINE_LOCATION/NSLocationWhenInUseUsageDescription); - 离线瓦片建议存储在 SD 卡,减少内置存储占用。
3. 法律合规
- 天地图瓦片仅供非商用,商用需获得天地图授权;
- OSM 数据遵循 ODbL 协议,需保留数据来源声明。
七、测试验证
- 断开网络(关闭 WiFi / 移动数据);
- 启动 SpringBoot 应用,访问前端离线页面:
http://localhost:8080/offline-nav.html; - 前端能正常显示离线地图(无加载失败);
- GPS 定位成功(需户外环境),地图标记定位点;
- 调用离线路径规划接口,能渲染导航轨迹,无在线 API 调用。