JAVA新实战4:用JAVA编写地图瓦片下载程序,并分级合并瓦片大图

        本文干活满满,直接用代码,给大家完整展示如何用java编写一个自主可控的地图瓦片下载程序,并分级合并瓦片为大图,以便在geoserver中部署自己的瓦片地图。

本文涉及的环境如下:

操作系统:windows 11

Java JDK:OpenJDK21

构建工具:Gradle 8.4

开发工具:VsCode - Visual Studio Code 1.84.1

关键组件:

1、org.apache.httpcomponents.client5:httpclient5:5.1.3 网络请求组件、下载文件等

2、opencv 4.8 图片处理组件

第三方资源:天地图 

1、项目代码结构

        核心代码由一个入口程序类、两个工具类、一个模型类组成。

 

2、配置项目build.gradle

        按以下配置设置,以支持org.apache.httpcomponents.client5:httpclient5和opencv,关于opencv的详情引入方式可参考本人另一篇文章《JAVA新实战3:opencv+java应用初探》

        build.gradle完整代码如下:

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.1.5'
	id 'io.spring.dependency-management' version '1.1.3'
}

group = 'com.jojava'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '21'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	//阿里
	maven { url 'https://maven.aliyun.com/nexus/content/groups/public/' }
	mavenCentral()
}

dependencies {

	implementation 'commons-io:commons-io:2.+'
	implementation 'org.apache.httpcomponents.client5:httpclient5:5.1.3'
	implementation(files("lib/opencv/opencv-480.jar"))

	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	
}

3、编写瓦片任务模型类

        创建包“com.jojava.joMapTile.model”,在包下创建“TaskBlockDivide”类,该模型类定义任务块实体结构,代码如下:

package com.jojava.joMapTile.model;

import java.util.ArrayList;

import lombok.Data;

@Data
public class TaskBlockDivide {

    private Long countX;
    private Long countY;
    private ArrayList<Long[]> divideX;
    private ArrayList<Long[]> divide;

}

4、编写瓦片xy坐标生成工具类

        创建包“com.jojava.joMapTile.utils”,在包下创建“TaskUtils”类,该工具类用于生成瓦片xy坐标和瓦片序号,代码如下:

package com.jojava.joMapTile.utils;

import java.util.ArrayList;

import com.jojava.joMapTile.model.TaskBlockDivide;

/**
 * 瓦片任务块xy坐标、编号生成
 * 
 * @author lyd
 * @date 2023-11-08
 */

public class TaskUtils {

    public static TaskBlockDivide blockDivide(long xStart, long xEnd, long yStart, long yEnd, double d) {
        TaskBlockDivide divide = new TaskBlockDivide();
        long countX = xEnd - xStart + 1;
        long countY = yEnd - yStart + 1;
        int eachX = (int) Math.ceil(countX / d);
        int eachY = (int) Math.ceil(countY / d);
        if (countX <= d) {
            eachX = (int) Math.ceil(countX / 2);
            eachX = eachX == 0 ? 1 : eachX;
        }
        if (countY <= d) {
            eachY = (int) Math.ceil(countY / 2);
            eachY = eachY == 0 ? 1 : eachY;
        }
        ArrayList<Long[]> divideX = new ArrayList<>();
        ArrayList<Long[]> divideY = new ArrayList<>();
        if (countX / eachX <= 1) {
            Long arr[] = { 0L, countX };
            divideX.add(arr);
        } else {
            long cnt = (int) Math.floor(countX / eachX);
            long e = countX % eachX;
            for (var i = 0L; i < cnt; i++) {
                Long arr[] = { i * eachX, (i + 1) * eachX - 1 };
                divideX.add(arr);
            }
            if (cnt * eachX < cnt * eachX + e) {
                Long arrEnd[] = { cnt * eachX, cnt * eachX + e - 1 };
                divideX.add(arrEnd);
            }
        }
        if (countY / eachY <= 1) {
            Long arr[] = { 0L, countY };
            divideY.add(arr);
        } else {
            long cnt = (int) Math.floor(countY / eachY);
            long e = countY % eachY;
            for (var i = 0L; i < cnt; i++) {
                Long arr[] = { i * eachY, (i + 1) * eachY - 1 };
                divideY.add(arr);
            }
            if (cnt * eachY < cnt * eachY + e) {
                Long arrEnd[] = { cnt * eachY, cnt * eachY + e - 1 };
                divideY.add(arrEnd);
            }
        }
        divide.setCountX(countX);
        divide.setCountY(countY);
        divide.setDivideX(divideX);
        divide.setDivideY(divideY);
        return divide;
    }

}

5、编写瓦片合并大图工具类

        在包“com.jojava.joMapTile.utils”下创建“TileMergeUtils”类,该工具类用于合并各层级瓦片图片为该层级单张大图,代码如下:

package com.jojava.joMapTile.utils;

import java.io.File;
import java.io.IOException;
import org.apache.commons.io.FileUtils;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.core.Range;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import lombok.Getter;

/**
 * 合并瓦片为大图 tiff
 * 
 * @author lyd
 * @date 2023-11-08
 */

public class TileMergeUtils {

    private Mat des = null;

    private int tileWidth = 256; // 瓦片大小 宽
    private int tileHeight = 256; // 瓦片大小 高

    @Getter
    private long allPixel = 0L;
    @Getter
    private long runPixel = 0L;

    public void init(int width, int height) {
        /*
         * CV_8uc1 单颜色通道 8位</br>
         * CV_8uc2 2颜色通道 16位</br>
         * CV_8uc3 3颜色通道 24位</br>
         * CV_8uc4 4颜色通道 32位</br>
         */
        // CV_8UC4为支持透明PNG的RGBA格式
        this.des = Mat.zeros(height, width, CvType.CV_8UC4);
        // 计算总像素数量
        this.allPixel = (long) width * height;
    }

    public void mergeToMat(String pathAndName, long x, long y) {
        // 读取图片
        var tileMat = Imgcodecs.imread(pathAndName, Imgcodecs.IMREAD_UNCHANGED);
        try {
            // 转换图片至RGBA格式
            Imgproc.cvtColor(tileMat, tileMat, Imgproc.COLOR_BGR2BGRA);
            // 确定坐标位置
            var rectForDes = this.des
                    .colRange(new Range((int) x, (int) x + tileWidth))
                    .rowRange(new Range((int) y, (int) y + tileHeight));
            // 填充至合并大图
            tileMat.copyTo(rectForDes);
        } catch (Exception ignored) {

        }
        // 完成后计算已合并的像素数量
        this.runPixel += (long) tileWidth * tileHeight;
    }

    public void output(String path, String name) throws IOException {
        String suffix = "tiff";
        String out = path + name + "." + suffix;
        FileUtils.createParentDirectories(new File(out));
        Imgcodecs.imwrite(out, this.des);
    }

    public void destroy() {
        this.des.release();
        this.des = null;
    }
}

5、编写项目启动运行程序

        编辑项目默认的JoMapTileApplication,本案瓦片下载和合并调用代码均写在这里,大家使用时可直接拿走用,也可自行进行封装,目前这里只实现了天地图的瓦片下载,其他注入百度地图、腾讯地图等瓦片下载原理差不多,后续将逐步补上。

        当前启动及瓦片下载程序代码如下:

package com.jojava.joMapTile;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpEntity;
import org.apache.hc.core5.http.HttpStatus;

import com.jojava.joMapTile.utils.TaskUtils;
import com.jojava.joMapTile.utils.TileMergeUtils;

public class JoMapTileApplication {

	// 引入 opencv_java480 组件
	static {
		// windows
		System.load(System.getProperty("user.dir") + "/lib/opencv/opencv_java480.dll");

		// mac
		// System.load(System.getProperty("user.dir") +
		// "/lib/opencv/opencv_java480.dylib");

		// linux
		// System.load(System.getProperty("user.dir") +
		// "/lib/opencv/opencv_java480.so");
	}

	// 地图品牌
	public static String mapBrand = "tianditu"; // 默认为天地图,当前主要支持

	// 地图key
	public static String mapKey = "4913045c9f99f6b423d4027d5fb9658b"; // 有些地图需要key才能使用,如:天地图,请自行申请
																		// http://lbs.tianditu.gov.cn/server/MapService.html

	// 地图类型
	public static String mapType = "img_w"; // 各个地图品牌都有各自不同的地图类型

	// 瓦片下载保存的本地主目录
	public static String basePath = "E:/tianditu";

	public static String[] servers = { "t0", "t1", "t2", "t3", "t4", "t5", "t6", "t7" };

	public static int minZoom = 1; // 下载开始层级
	public static int maxZoom = 18; // 下载结束层级
	// 天安门 116.320303,39.964566 116.465182,39.871597
	public static double startLat = 39.964566;// 开始纬度(从北到南)
	public static double endLat = 39.871597;// 结束纬度(从北到南)
	public static double startLon = 116.320303;// 开始经度(从西到东)
	public static double endLon = 116.465182;// 结束经度(从西到东)

	// 坐标
	// public static double startLat = 44.092424;// 开始纬度(从北到南)
	// public static double endLat = 43.940986;// 结束纬度(从北到南)
	// public static double startLon = 126.045746;// 开始经度(从西到东)
	// public static double endLon = 126.516601;// 结束经度(从西到东)

	public static void main(String[] args) {
		// SpringApplication.run(TiandituApplication.class, args);

		switch (mapBrand) {

			case "tianditu":
				TianDiTu();
				break;

			case "baidu":
				BaiduMap();
				break;

			case "gaode":
				GaodeMap();
				break;

			case "tencent":
				TencentMap();
				break;

			default:
				TianDiTu();

		}

	}

	// 天地图 - 目前仅支持这一款
	public static void TianDiTu() {

		// 注意天地图API访问次数限制
		String tk = mapKey;

		String type = mapType;

		// 影像 - 墨卡托
		String img_w = "http://{server}.tianditu.gov.cn/" + type
				+ "/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={tk}";

		// String img_w =
		// "http://{server}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={tk}";

		// 平面 - 经纬度投影
		// 影像标注 - 墨卡托
		// public static String img_w =
		// "http://{server}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk={tk}";

		String[] urlArr = { img_w };// 要下载的图层

		// 各级瓦片范围
		Map<String, Object> tilesMap = new HashMap<String, Object>();

		// 启动多线程
		ExecutorService exe = Executors.newFixedThreadPool(6);
		// 等经纬度第一层是1x2,纬度数量是2^0,经度数量是2^1
		// 墨卡托投影第一层是2x2,纬度数量是2^1,经度数量是2^1
		for (int i = 0; i < urlArr.length; i++) {
			String url = urlArr[i].replace("{tk}", tk);
			System.out.println(url);
			String layerName = url.split("tianditu.gov.cn/")[1].split("/wmts?")[0];
			for (int z = minZoom; z <= maxZoom; z++) {
				double deg = 360.0 / Math.pow(2, z) / 256;// 一个像素点代表多少度
				int startX = (int) ((startLon + 180) / deg / 256);
				int endX = (int) ((endLon + 180) / deg / 256);
				int startY = (((int) Math.pow(2, z) * 256 / 2)
						- (int) ((Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180))
								/ (360 / Math.pow(2, z) / 256) + 0.5))
						/ 256;
				int endY = (((int) Math.pow(2, z) * 256 / 2)
						- (int) ((Math.log(Math.tan((90 + endLat) * Math.PI / 360)) / (Math.PI / 180))
								/ (360 / Math.pow(2, z) / 256) + 0.5))
						/ 256;

				// 将计算好的瓦片编号范围存
				tilesMap.put(String.valueOf(z), String.valueOf(startX) + "," + String.valueOf(endX) + ","
						+ String.valueOf(startY) + "," + String.valueOf(endY));
				// 循环下载瓦片
				for (int x = startX; x <= endX; x++) {
					for (int y = startY; y <= endY; y++) {
						final String newUrl = url.replace("{server}", servers[(int) (Math.random() * servers.length)])
								.replace("{z}", z + "").replace("{x}", x + "").replace("{y}", y + "");
						System.out.println(newUrl);
						final String filePath = basePath + "/" + layerName + "/" + z + "/" + x + "/" + y + ".png";
						exe.execute(new Runnable() {
							@Override
							public void run() {
								File file = new File(filePath);
								if (!file.exists()) {
									if (!file.getParentFile().exists()) {
										file.getParentFile().mkdirs();
									}
									boolean loop = true;
									int count = 0;
									while (loop && count < 5) {// 下载出错进行重试,最多5次
										count++;
										try {
											InputStream in = getFileInputStream(newUrl);
											OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
											byte[] b = new byte[8192];
											int len = 0;
											while ((len = in.read(b)) > -1) {
												out.write(b, 0, len);
												out.flush();
											}
											out.close();
											in.close();
											loop = false;
										} catch (Exception e) {
											loop = true;
										}
									}
									if (loop) {
										System.out.println("下载失败:" + newUrl);
									}
								}
							}
						});
					}
				}

				// 合并本层级瓦片
				System.out.println("本层最后一组 " + endX);
				mergeTileImage(layerName, z, startX, endX, startY, endY);
			}
		}
		exe.shutdown();
		while (true) {
			try {
				Thread.sleep(1000L);// 主线程休眠1秒,等待线程池运行结束,同时避免一直死循环造成CPU浪费
			} catch (InterruptedException e) {
			}
			if (exe.isTerminated()) {// 线程池所有线程都结束运行
				break;
			}
		}
	}

	// 百度地图
	public static void BaiduMap() {
		// 暂略
	}

	// 腾讯地图
	public static void TencentMap() {
		// 暂略
	}

	// 高德地图
	public static void GaodeMap() {
		// 暂略
	}

	// 获取文件下载流
	public static InputStream getFileInputStream(String url) throws Exception {
		InputStream is = null;

		CloseableHttpClient httpclient = HttpClients.createDefault();
		HttpGet request = new HttpGet(url);
		request.setHeader("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
		CloseableHttpResponse response = httpclient.execute(request);
		response.setHeader("Content-Type", "application/octet-stream");
		int statusCode = response.getCode();
		if (statusCode == HttpStatus.SC_OK) {
			HttpEntity entity = response.getEntity();
			is = entity.getContent();
		}
		return is;
	}

	// 合并图片 指定瓦片范围
	public static void mergeTileImage(String layerName, int zoom, long xStart, long xEnd, long yStart,
			long yEnd) {

		TileMergeUtils mat = new TileMergeUtils();

		int tileWidth = 256; // 瓦片大小 宽
		int tileHeight = 256; // 瓦片大小 高

		String savePath = basePath + "/" + layerName;
		if (zoom == 0) {
			return;
		}
		try {
			System.out.print("正在合并第" + zoom + "级地图");

			int z = zoom;
			long mergeImageWidth = tileWidth * (xEnd - xStart + 1);
			long mergeImageHeight = tileHeight * (yEnd - yStart + 1);
			if (mergeImageWidth >= Integer.MAX_VALUE || mergeImageHeight >= Integer.MAX_VALUE) {
				System.out.print("合并后的图片width:" + mergeImageWidth + ",height:" + mergeImageHeight + ",宽度或高度大于int最大值"
						+ Integer.MAX_VALUE + ",不予合并。");
				return;
			}
			var WH = mergeImageWidth * mergeImageHeight;
			System.out.print("合并后的图片width:" + mergeImageWidth + ",height:" + mergeImageHeight + ",像素大小:"
					+ WH);
			if (WH > (long) Integer.MAX_VALUE) {
				System.out
						.print("该" + zoom + "级地图合并后像素大小大于int最大值" + Integer.MAX_VALUE + ",合并时间可能会稍长,建议低配置电脑不要执行超大尺寸合并");
			}
			// 开启线程
			mat.init((int) mergeImageWidth, (int) mergeImageHeight);
			var cpuCoreCount = Runtime.getRuntime().availableProcessors();
			var d = Math.floor(Math.sqrt(cpuCoreCount));
			var divide = TaskUtils.blockDivide(xStart, xEnd, yStart, yEnd, d);
			var divideX = divide.getDivideX();
			var divideY = divide.getDivideY();
			// 循环读取图层,进行合并
			for (var i = 0; i < divideX.size(); i++) {
				for (var j = 0; j < divideY.size(); j++) {

					// 计算点像素位置
					long topLeftX = xStart, topLeftY = yStart;
					long xStartPosit = xStart + divideX.get(i)[0];
					long xEndPosit = xStart + divideX.get(i)[1];
					long yStartPosit = yStart + divideY.get(j)[0];
					long yEndPosit = yStart + divideY.get(j)[1];
					//
					System.out.println("xEndPosit: " + xEndPosit);
					for (var x = xStartPosit; x < xEndPosit; x++) {
						if (Thread.currentThread().isInterrupted()) {
							break;
						}
						for (var y = yStartPosit; y <= yEndPosit; y++) {
							if (Thread.currentThread().isInterrupted()) {
								break;
							}
							var positionX = tileWidth * (x - topLeftX);
							var positionY = tileWidth * (y - topLeftY);
							var filePathAndName = savePath + "/" + z + "/" + x + "/" + y + ".png";
							System.out.println(filePathAndName);
							mat.mergeToMat(filePathAndName, positionX, positionY);
						}
					}

				}
			}

			System.out.print("正在写入至硬盘...");
			// 合并后图片的保存位置
			var outPath = savePath + "/merge" + "/";
			var outName = "z" + z;
			// 输出合并后的图片
			mat.output(outPath, outName);
			mat.destroy();
			System.out.print("第" + zoom + "级地图合并完成");
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

6、启动运行程序

        点击启动程序中的“Run”启动运行,即可实现自动下载瓦片,并在每个层级瓦片下载完毕后执行合并。

 7、代码仓

        本文章项目代码已开源到gitee,请进入下载。

joMapTile: 一个基于java编写的地图瓦片下载及瓦片合并大图小工具 (gitee.com)icon-default.png?t=N7T8https://gitee.com/duihao/jomaptile

 8、结语

        本人正在努力持续不断为大家分享JAVA相关的各种实战经验,所有技术尽可能采用较新且成熟的技术架构,期待能够帮助到您。

        如果您觉得本文对你有所帮助和启发,感谢您不吝赐教,也动动您发财的小手给予作者鼓励,谢谢,你的每一个点赞、评论、收藏都是对我莫大的鼓励,不足之处也请朋友们多多赐教。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java是一种非常流行的编程语言,可以用它来下载地图瓦片数据到本地。首先,我们需要利用Java的网络编程功能来发送HTTP请求到地图数据的服务器。我们可以使用Java中的URLConnection或者HttpClient来实现这一步。通过向服务器发送请求,我们可以获取地图瓦片的数据。 接下来,我们需要将获取到的地图瓦片数据保存到本地。我们可以使用Java的IO流来实现这一步。我们可以将获取到的数据保存为图片文件,例如PNG格式。可以通过Java中的FileOutputStream来创建一个文件输出流,然后将获取到的数据写入到文件中。 另外,我们还需要考虑到地图瓦片数据的存储结构。通常地图瓦片数据是以一定的网格方式组织的,我们需要考虑到这种结构来正确地保存数据。我们可以将每个瓦片数据保存到对应的文件夹中,或者使用数据库来存储地图瓦片数据的索引信息。 在下载地图瓦片数据到本地的过程中,我们还需要考虑到网络连接的稳定性和下载速度的优化。我们可以使用多线程或者异步方式来进行数据的下载,以提高下载速度和效率。 总之,通过利用Java的网络编程和IO流功能,我们可以很方便地实现地图瓦片数据的下载到本地的功能。同时,我们还可以考虑到下载速度的优化和数据结构的存储,来提高下载效率和方便地使用地图瓦片数据。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值