Junki
Junki
Published on 2025-12-09 / 9 Visits
0
0

地理编码逆解析的本地化实现方案(根据经纬度查询所在区划)

要实现根据经纬度返回省份的功能,核心是地理编码逆解析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;
    }
}

关键步骤:

  1. loadGeoJsonData 将地理位置数据文件加载到内存。
  2. 通过点在多边形内的判断算法判断坐标在哪个省份。

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'}

Comment