rk
4 天以前 21bd711a3756850299b443848181ee60708c6377
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package com.doumee.core.utils.Tencent;
 
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.doumee.core.utils.Http;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
import java.io.IOException;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
 
/**
 * 腾讯地图工具类
 *
 * @Author : Rk
 * @create 2026/4/14 15:58
 */
@Slf4j
@Component
public class MapUtil {
 
    private static String tencentKey;
 
    /** 距离矩阵API */
    public static final String MATRIX_URL = "https://apis.map.qq.com/ws/distance/v1/matrix";
 
    /** 逆地理解析 */
    public static final String GEO_URL = "https://apis.map.qq.com/ws/geocoder/v1/";
 
    /** 支持的模式 */
    private static final List<String> SUPPORTED_MODES = Arrays.asList("driving", "bicycling");
 
    @Value("${tencent_key}")
    public void setTencentKey(String tencentKey) {
        MapUtil.tencentKey = tencentKey;
    }
 
    /**
     * 批量距离矩阵计算
     *
     * @param mode       模式:driving(驾车)、bicycling(自行车)
     * @param fromPoints 起点列表,格式:lat,lng(最多10个)
     * @param toPoints   终点列表,格式:lat,lng(最多20个)
     * @return result.rows 矩阵数据,每个元素包含 distance(米) 和 duration(秒)
     */
    public static JSONObject distanceMatrix(String mode, List<String> fromPoints, List<String> toPoints) {
        if (!SUPPORTED_MODES.contains(mode)) {
            throw new IllegalArgumentException("不支持的模式: " + mode + ",仅支持: " + SUPPORTED_MODES);
        }
        if (fromPoints == null || fromPoints.isEmpty() || toPoints == null || toPoints.isEmpty()) {
            throw new IllegalArgumentException("起点和终点列表不能为空");
        }
 
        String from = String.join(";", fromPoints);
        String to = String.join(";", toPoints);
 
        try {
            String url = MATRIX_URL
                    + "?key=" + tencentKey
                    + "&mode=" + mode
                    + "&from=" + URLEncoder.encode(from, "UTF-8")
                    + "&to=" + URLEncoder.encode(to, "UTF-8");
 
            log.info("腾讯地图矩阵API请求: mode={}, from={}, to={}", mode, from, to);
 
            JSONObject json = new Http().build(url)
                    .setConnectTimeout(5000)
                    .setReadTimeout(10000)
                    .get()
                    .toJSONObject();
 
            log.info("腾讯地图矩阵API响应: {}", json);
 
            if (json.getIntValue("status") != 0) {
                throw new RuntimeException("腾讯地图矩阵API调用失败: " + json.getString("message"));
            }
 
            return json.getJSONObject("result");
        } catch (IOException e) {
            log.error("腾讯地图矩阵API调用异常", e);
            throw new RuntimeException("腾讯地图矩阵API调用异常", e);
        }
    }
 
    /**
     * 单对距离计算(便捷方法)
     *
     * @param mode 模式:driving(驾车)、bicycling(自行车)
     * @param from 起点格式:lat,lng
     * @param to   终点格式:lat,lng
     * @return 第一个元素的 distance(米) 和 duration(秒)
     */
    public static JSONObject distanceSingle(String mode, String from, String to) {
        JSONObject result = distanceMatrix(mode,
                Arrays.asList(from),
                Arrays.asList(to));
        JSONArray rows = result.getJSONArray("rows");
        if (rows != null && !rows.isEmpty()) {
            JSONArray elements = rows.getJSONObject(0).getJSONArray("elements");
            if (elements != null && !elements.isEmpty()) {
                return elements.getJSONObject(0);
            }
        }
        return new JSONObject();
    }
 
    /**
     * 多起点到单终点(便捷方法)
     * 返回每个起点到终点的距离和耗时,顺序与fromPoints对应
     *
     * @param mode       模式
     * @param fromPoints 起点列表
     * @param to         单个终点
     * @return 距离耗时列表,顺序与fromPoints对应
     */
    public static List<JSONObject> distanceToOne(String mode, List<String> fromPoints, String to) {
        JSONObject result = distanceMatrix(mode, fromPoints, Arrays.asList(to));
        JSONArray rows = result.getJSONArray("rows");
        return rows == null ? Arrays.asList() : rows.stream()
                .map(row -> ((JSONObject) row).getJSONArray("elements").getJSONObject(0))
                .collect(Collectors.toList());
    }
 
    /**
     * 逆地理解析 - 根据经纬度获取地址信息
     *
     * @param lat 纬度
     * @param lng 经度
     * @return result.ad_info 中的 adcode(区划码)、city(城市)、district(区) 等信息
     */
    public static JSONObject reverseGeocode(double lat, double lng) {
        try {
            String url = GEO_URL
                    + "?key=" + tencentKey
                    + "&location=" + lat + "," + lng;
 
            log.info("腾讯地图逆地理解析请求: location={},{}", lat, lng);
 
            JSONObject json = new Http().build(url)
                    .setConnectTimeout(5000)
                    .setReadTimeout(10000)
                    .get()
                    .toJSONObject();
 
            log.info("腾讯地图逆地理解析响应: {}", json);
 
            if (json.getIntValue("status") != 0) {
                throw new RuntimeException("腾讯地图逆地理解析失败: " + json.getString("message"));
            }
 
            return json.getJSONObject("result");
        } catch (IOException e) {
            log.error("腾讯地图逆地理解析异常", e);
            throw new RuntimeException("腾讯地图逆地理解析异常", e);
        }
    }
 
    /**
     * 判断两个经纬度是否在同一个城市
     *
     * @param lat1 第一个点纬度
     * @param lng1 第一个点经度
     * @param lat2 第二个点纬度
     * @param lng2 第二个点经度
     * @return true=同城,false=不同城
     */
    public static boolean isSameCity(double lat1, double lng1, double lat2, double lng2) {
        JSONObject result1 = reverseGeocode(lat1, lng1);
        JSONObject result2 = reverseGeocode(lat2, lng2);
 
        String city1 = result1.getJSONObject("ad_info").getString("city");
        String city2 = result2.getJSONObject("ad_info").getString("city");
 
        log.info("判断同城: ({},{}) => city={}, ({},{}) => city={}", lat1, lng1, city1, lat2, lng2, city2);
 
        return city1 != null && city1.equals(city2);
    }
}