要实现根据经纬度返回省份的功能,核心是地理编码逆解析ReGeocoding(将经纬度转换为行政区划信息),最常用的方式是调用第三方地理编码 API(如高德、百度、腾讯地图 API)。
本文介绍一种不依赖三方接口的本地化实现方案。
本文源码地址:https://github.com/classfang/geo-json-demo
1 获取地理空间数据
阿里DataV提供了“国家”、“省份”、“城市”三个级别的地理位置空间数据。
网址:https://datav.aliyun.com/portal/school/atlas/area_selector
选择需要的数据粒度,点击下载GeoJSON即可,本文以“省份”粒度为例。
2 基于Java开发案例
2.1 创建一个Maven工程
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>geo-json-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Geo JSON Demo</name>
<description>Geo JSON Demo Project</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<junit.version>4.13.2</junit.version>
</properties>
<dependencies>
<!-- Gson for JSON parsing -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<!-- JUnit 4 for testing -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Maven Compiler Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<!-- Maven Surefire Plugin for running tests -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0</version>
</plugin>
</plugins>
</build>
</project>
2.2 创建数据模型相关类
GeoJSON Feature Properties 数据模型:
package com.example.geo;
import java.lang.reflect.Type;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonParseException;
import com.google.gson.annotations.JsonAdapter;
/**
* GeoJSON Feature Properties 数据模型
*/
public class FeatureProperties {
@JsonAdapter(AdcodeDeserializer.class)
private Integer adcode;
private String name;
private String level;
// 其他字段如 parent, center, centroid 等会被 Gson 自动忽略
public Integer getAdcode() {
return adcode;
}
public void setAdcode(Integer adcode) {
this.adcode = adcode;
}
/**
* 自定义 adcode 反序列化器,处理字符串和数字两种情况
*/
private static class AdcodeDeserializer implements JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
throws JsonParseException {
if (json.isJsonPrimitive()) {
if (json.getAsJsonPrimitive().isNumber()) {
return json.getAsInt();
} else if (json.getAsJsonPrimitive().isString()) {
String str = json.getAsString();
// 如果是字符串格式如 "100000_JD",尝试提取数字部分
try {
// 尝试直接解析
return Integer.parseInt(str);
} catch (NumberFormatException e) {
// 如果不是纯数字,尝试提取数字部分
String numPart = str.split("_")[0];
try {
return Integer.parseInt(numPart);
} catch (NumberFormatException ex) {
// 如果无法解析,返回 null
return null;
}
}
}
}
return null;
}
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
}
GeoJSON Feature 数据模型:
package com.example.geo;
/**
* GeoJSON Feature 数据模型
*/
public class GeoJsonFeature {
private String type;
private FeatureProperties properties;
private Geometry geometry;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public FeatureProperties getProperties() {
return properties;
}
public void setProperties(FeatureProperties properties) {
this.properties = properties;
}
public Geometry getGeometry() {
return geometry;
}
public void setGeometry(Geometry geometry) {
this.geometry = geometry;
}
}
GeoJSON FeatureCollection 数据模型:
package com.example.geo;
import java.util.List;
/**
* GeoJSON FeatureCollection 数据模型
*/
public class GeoJsonFeatureCollection {
private String type;
private List<GeoJsonFeature> features;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<GeoJsonFeature> getFeatures() {
return features;
}
public void setFeatures(List<GeoJsonFeature> features) {
this.features = features;
}
}
GeoJSON Geometry 数据模型:
package com.example.geo;
import com.google.gson.JsonElement;
import com.google.gson.JsonArray;
import java.util.ArrayList;
import java.util.List;
/**
* GeoJSON Geometry 数据模型
*/
public class Geometry {
private String type;
private JsonElement coordinates; // MultiPolygon coordinates - 使用 JsonElement 以便灵活解析
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public JsonElement getCoordinates() {
return coordinates;
}
public void setCoordinates(JsonElement coordinates) {
this.coordinates = coordinates;
}
/**
* 将坐标转换为 List 结构(MultiPolygon)
* @return MultiPolygon 坐标结构
*/
public List<List<List<List<Double>>>> getCoordinatesAsMultiPolygon() {
if (coordinates == null || !coordinates.isJsonArray()) {
return new ArrayList<>();
}
List<List<List<List<Double>>>> result = new ArrayList<>();
JsonArray polygonsArray = coordinates.getAsJsonArray();
for (JsonElement polygonElement : polygonsArray) {
if (!polygonElement.isJsonArray()) {
continue;
}
JsonArray ringsArray = polygonElement.getAsJsonArray();
List<List<List<Double>>> rings = new ArrayList<>();
for (JsonElement ringElement : ringsArray) {
if (!ringElement.isJsonArray()) {
continue;
}
JsonArray pointsArray = ringElement.getAsJsonArray();
List<List<Double>> points = new ArrayList<>();
for (JsonElement pointElement : pointsArray) {
if (!pointElement.isJsonArray()) {
continue;
}
JsonArray coordArray = pointElement.getAsJsonArray();
if (coordArray.size() >= 2) {
List<Double> point = new ArrayList<>();
point.add(coordArray.get(0).getAsDouble()); // 经度
point.add(coordArray.get(1).getAsDouble()); // 纬度
points.add(point);
}
}
rings.add(points);
}
result.add(rings);
}
return result;
}
}
2.3 创建省份信息类
省份信息类:
package com.example.geo;
/**
* 省份信息类
*/
public class ProvinceInfo {
private String name;
private Integer adcode;
private String level;
public ProvinceInfo(String name, Integer adcode, String level) {
this.name = name;
this.adcode = adcode;
this.level = level;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAdcode() {
return adcode;
}
public void setAdcode(Integer adcode) {
this.adcode = adcode;
}
public String getLevel() {
return level;
}
public void setLevel(String level) {
this.level = level;
}
@Override
public String toString() {
return "ProvinceInfo{" +
"name='" + name + '\'' +
", adcode=" + adcode +
", level='" + level + '\'' +
'}';
}
}
2.4 实现点在多边形内的判断算法
点在多边形内的判断算法类:
package com.example.geo;
import java.util.List;
/**
* 点在多边形内的判断算法
*/
public class PointInPolygon {
/**
* 判断点是否在多边形内(使用射线法)
*
* @param pointX 点的经度
* @param pointY 点的纬度
* @param polygon 多边形的顶点坐标列表 [[lon, lat], [lon, lat], ...]
* @return true 如果点在多边形内,false 否则
*/
public static boolean isPointInPolygon(double pointX, double pointY, List<List<Double>> polygon) {
if (polygon == null || polygon.size() < 3) {
return false;
}
boolean inside = false;
int j = polygon.size() - 1;
for (int i = 0; i < polygon.size(); i++) {
List<Double> pi = polygon.get(i);
List<Double> pj = polygon.get(j);
double xi = pi.get(0); // 经度
double yi = pi.get(1); // 纬度
double xj = pj.get(0);
double yj = pj.get(1);
// 射线法:从点向右水平发射射线,计算与多边形边的交点数
boolean intersect = ((yi > pointY) != (yj > pointY))
&& (pointX < (xj - xi) * (pointY - yi) / (yj - yi) + xi);
if (intersect) {
inside = !inside;
}
j = i;
}
return inside;
}
/**
* 判断点是否在 MultiPolygon 内
*
* @param pointX 点的经度
* @param pointY 点的纬度
* @param multiPolygon MultiPolygon 坐标结构
* @return true 如果点在 MultiPolygon 内,false 否则
*/
public static boolean isPointInMultiPolygon(double pointX, double pointY,
List<List<List<List<Double>>>> multiPolygon) {
if (multiPolygon == null || multiPolygon.isEmpty()) {
return false;
}
// MultiPolygon 包含多个 Polygon,每个 Polygon 包含多个环(第一个是外环,其余是内环/洞)
for (List<List<List<Double>>> polygon : multiPolygon) {
if (polygon == null || polygon.isEmpty()) {
continue;
}
// 检查是否在外环内
List<List<Double>> exteriorRing = polygon.get(0);
boolean inExterior = isPointInPolygon(pointX, pointY, exteriorRing);
if (!inExterior) {
continue; // 不在这个外环内,检查下一个 Polygon
}
// 检查是否在内环(洞)内
boolean inHole = false;
for (int i = 1; i < polygon.size(); i++) {
List<List<Double>> hole = polygon.get(i);
if (isPointInPolygon(pointX, pointY, hole)) {
inHole = true;
break;
}
}
// 如果在外环内但不在任何内环内,则点在多边形内
if (!inHole) {
return true;
}
}
return false;
}
}
2.5 实现根据经纬度查询省份的服务类
根据经纬度查询省份的服务类:
package com.example.geo;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
/**
* 根据经纬度查询省份的服务类
*/
public class ProvinceQueryService {
private GeoJsonFeatureCollection featureCollection;
private static final String GEOJSON_FILE = "/cn.geojson";
/**
* 构造函数,加载 GeoJSON 数据
*/
public ProvinceQueryService() {
loadGeoJsonData();
}
/**
* 从资源文件加载 GeoJSON 数据
*/
private void loadGeoJsonData() {
try {
InputStream inputStream = getClass().getResourceAsStream(GEOJSON_FILE);
if (inputStream == null) {
throw new RuntimeException("无法找到 GeoJSON 文件: " + GEOJSON_FILE);
}
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
Gson gson = new GsonBuilder()
.setLenient()
.create();
featureCollection = gson.fromJson(reader, GeoJsonFeatureCollection.class);
reader.close();
inputStream.close();
} catch (Exception e) {
throw new RuntimeException("加载 GeoJSON 数据失败", e);
}
}
/**
* 根据经纬度查询所在省份
*
* @param longitude 经度
* @param latitude 纬度
* @return 省份名称,如果未找到则返回 null
*/
public String queryProvince(double longitude, double latitude) {
if (featureCollection == null || featureCollection.getFeatures() == null) {
return null;
}
List<GeoJsonFeature> features = featureCollection.getFeatures();
// 遍历所有省份特征,查找包含该点的省份
for (GeoJsonFeature feature : features) {
if (feature == null || feature.getProperties() == null || feature.getGeometry() == null) {
continue;
}
// 只处理省份级别的特征
if (!"province".equals(feature.getProperties().getLevel())) {
continue;
}
Geometry geometry = feature.getGeometry();
if (!"MultiPolygon".equals(geometry.getType())) {
continue;
}
// 判断点是否在多边形内
if (PointInPolygon.isPointInMultiPolygon(longitude, latitude, geometry.getCoordinatesAsMultiPolygon())) {
return feature.getProperties().getName();
}
}
return null;
}
/**
* 根据经纬度查询所在省份(带详细信息)
*
* @param longitude 经度
* @param latitude 纬度
* @return 省份信息对象,如果未找到则返回 null
*/
public ProvinceInfo queryProvinceInfo(double longitude, double latitude) {
if (featureCollection == null || featureCollection.getFeatures() == null) {
return null;
}
List<GeoJsonFeature> features = featureCollection.getFeatures();
for (GeoJsonFeature feature : features) {
if (feature == null || feature.getProperties() == null || feature.getGeometry() == null) {
continue;
}
if (!"province".equals(feature.getProperties().getLevel())) {
continue;
}
Geometry geometry = feature.getGeometry();
if (!"MultiPolygon".equals(geometry.getType())) {
continue;
}
if (PointInPolygon.isPointInMultiPolygon(longitude, latitude, geometry.getCoordinatesAsMultiPolygon())) {
FeatureProperties props = feature.getProperties();
return new ProvinceInfo(props.getName(), props.getAdcode(), props.getLevel());
}
}
return null;
}
}
关键步骤:
- loadGeoJsonData 将地理位置数据文件加载到内存。
- 通过点在多边形内的判断算法判断坐标在哪个省份。
3 效果验证
3.1 效果验证程序
package com.example;
import com.example.geo.ProvinceInfo;
import com.example.geo.ProvinceQueryService;
/**
* Geo JSON Demo Application
* 演示根据经纬度查询省份的功能
*/
public class App {
public static void main(String[] args) {
System.out.println("=== Geo JSON Demo Application ===");
// 创建查询服务
ProvinceQueryService service = new ProvinceQueryService();
// 测试用例:一些城市的经纬度
double[][] testPoints = {
{ 116.407526, 39.904030 }, // 北京天安门
{ 121.473701, 31.230416 }, // 上海外滩
{ 113.264385, 23.129112 }, // 广州
{ 114.057868, 22.543099 }, // 深圳
{ 104.066541, 30.572269 }, // 成都
{ 108.948024, 34.341568 }, // 西安
{ 120.155070, 30.274084 }, // 杭州
{ 118.796877, 32.060255 }, // 南京
};
String[] cityNames = {
"北京天安门", "上海外滩", "广州", "深圳",
"成都", "西安", "杭州", "南京"
};
System.out.println("\n开始查询省份信息...\n");
for (int i = 0; i < testPoints.length; i++) {
double longitude = testPoints[i][0];
double latitude = testPoints[i][1];
// 查询省份名称
String provinceName = service.queryProvince(longitude, latitude);
// 查询详细信息
ProvinceInfo provinceInfo = service.queryProvinceInfo(longitude, latitude);
System.out.println("位置: " + cityNames[i]);
System.out.println(" 经纬度: (" + longitude + ", " + latitude + ")");
System.out.println(" 所在省份: " + (provinceName != null ? provinceName : "未找到"));
if (provinceInfo != null) {
System.out.println(" 详细信息: " + provinceInfo);
}
System.out.println();
}
// 交互式查询示例
if (args.length >= 2) {
try {
double lon = Double.parseDouble(args[0]);
double lat = Double.parseDouble(args[1]);
String province = service.queryProvince(lon, lat);
System.out.println("查询结果: 经度 " + lon + ", 纬度 " + lat + " -> " +
(province != null ? province : "未找到对应省份"));
} catch (NumberFormatException e) {
System.out.println("参数格式错误,请提供有效的经纬度数值");
}
} else {
System.out.println("\n提示: 可以通过命令行参数查询,例如:");
System.out.println(" java -cp target/classes com.example.App 116.407526 39.904030");
}
}
}
3.2 运行结果
=== Geo JSON Demo Application ===
开始查询省份信息...
位置: 北京天安门
经纬度: (116.407526, 39.90403)
所在省份: 北京市
详细信息: ProvinceInfo{name='北京市', adcode=110000, level='province'}
位置: 上海外滩
经纬度: (121.473701, 31.230416)
所在省份: 上海市
详细信息: ProvinceInfo{name='上海市', adcode=310000, level='province'}
位置: 广州
经纬度: (113.264385, 23.129112)
所在省份: 广东省
详细信息: ProvinceInfo{name='广东省', adcode=440000, level='province'}
位置: 深圳
经纬度: (114.057868, 22.543099)
所在省份: 广东省
详细信息: ProvinceInfo{name='广东省', adcode=440000, level='province'}
位置: 成都
经纬度: (104.066541, 30.572269)
所在省份: 四川省
详细信息: ProvinceInfo{name='四川省', adcode=510000, level='province'}
位置: 西安
经纬度: (108.948024, 34.341568)
所在省份: 陕西省
详细信息: ProvinceInfo{name='陕西省', adcode=610000, level='province'}
位置: 杭州
经纬度: (120.15507, 30.274084)
所在省份: 浙江省
详细信息: ProvinceInfo{name='浙江省', adcode=330000, level='province'}
位置: 南京
经纬度: (118.796877, 32.060255)
所在省份: 江苏省
详细信息: ProvinceInfo{name='江苏省', adcode=320000, level='province'}