GIS导航

在线版

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)。

六、测试验证

  1. 启动 SpringBoot 应用,访问前端页面:http://localhost:8080/index.html
  2. 点击「开始定位」,页面显示当前 GCJ02 坐标并标记在地图上;
  3. 点击「规划导航」,后端调用天地图路径规划 API,前端渲染红色导航轨迹;
  4. 如需适配百度地图,调用/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 协议,需保留数据来源声明。

七、测试验证

  1. 断开网络(关闭 WiFi / 移动数据);
  2. 启动 SpringBoot 应用,访问前端离线页面:http://localhost:8080/offline-nav.html
  3. 前端能正常显示离线地图(无加载失败);
  4. GPS 定位成功(需户外环境),地图标记定位点;
  5. 调用离线路径规划接口,能渲染导航轨迹,无在线 API 调用。
相关推荐
xunyan62342 小时前
面向对象(下)-接口应用:代理模式 && 工厂模式
android·java·学习
青云交2 小时前
Java 大视界 -- Java 大数据在智能教育学习成果评估体系完善与教育质量提升中的深度应用(434)
java·hive·spark·智能教育·学习成果评估·教育质量提升·实时评估
GEM的左耳返2 小时前
互联网大厂Java求职面试题解析与实战指导:涵盖核心技术栈与业务场景
java·数据库·spring boot·安全·微服务·消息队列·面试题
毕设源码-邱学长2 小时前
【开题答辩全过程】以 高校就业分析与可视化系统为例,包含答辩的问题和答案
java·eclipse
橙序员小站2 小时前
Springboot3.0并不能拯救你的屎山
java·后端·架构
憧憬少2 小时前
通过切换Service实现类来切换看板数据来源
java·spring boot
YJlio2 小时前
Active Directory 工具学习笔记(10.13):AdRestore——把误删“拉回现场”的最快姿势
java·笔记·学习
小黄编程快乐屋2 小时前
Python 期末复习知识点汇总
java·服务器·python
千寻技术帮2 小时前
10400_基于Springboot的职业教育管理系统
java·spring boot·后端·毕设·文档·职业教育