地图下载器工具-Java
瓦片下载
@Override
public void run() {
// 定义在try外面,用于在finally关闭资源链接
HttpURLConnection urlConnection = null;
InputStream inputStream = null;
OutputStream outputStream = null;
URL url = null;
try {
File file = new File(downloadDir + z + File.separator + x + File.separator + y + ".jpg");
url = new URL(String.format(link, (int) (Math.random() * 4), x, y, z));
// 如果本地没有图片
if (!file.exists()) {
urlConnection = (HttpURLConnection) url.openConnection();
urlConnection.setConnectTimeout(2000);
urlConnection.setRequestMethod("GET");
urlConnection.setRequestProperty("referer", "域名来源地址");
urlConnection.addRequestProperty("User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:68.0) Gecko/20100101 Firefox/68.0");
urlConnection.connect();
if (urlConnection.getResponseCode() == 200) {
if (!file.getParentFile().exists()) {
file.getParentFile().mkdirs();
}
file.createNewFile();
inputStream = urlConnection.getInputStream();
outputStream = new FileOutputStream(file);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
}
outputStream.close();
inputStream.close();
synchronized (failCount) {
successCount++;
}
} else {
synchronized (successCount) {
logger.info("下载失败瓦片链接" + url);
failCount++;
}
}
} else {
synchronized (successCount) {
existCount++;
}
}
} catch (Exception e) {
synchronized (successCount) {
logger.info("下载失败瓦片链接" + url);
failCount++;
}
}
}
-
要点2
- 根据经纬度与级别计算瓦片的编号。经过多次测试与计算,发现当最大经度为180时,会计算出多余并且不存在的瓦片编号,所以设置为179.99999;当纬度要是为-90或90的时候,由于极地的极大化,也可以认为是投影计算的规则,这里使用tan计算,所以会照成计算数据的不准确,按照正常的经纬度坐标系也是-85~85,所以这里做了限制。
/**
* 根据经纬度计算行列号
*
* @param lonlat
* @param level
* @return
*/
public static TileRowColAndCount getTileColRow(String[] lonlat, Integer level) {
double maxlon = Math.max(Double.valueOf(lonlat[0]), Double.valueOf(lonlat[2])) == 180.0 ? 179.99999
: Math.max(Double.valueOf(lonlat[0]), Double.valueOf(lonlat[2]));
double minlon = Math.min(Double.valueOf(lonlat[0]), Double.valueOf(lonlat[2]));
double maxlat = Math.max(Double.valueOf(lonlat[1]), Double.valueOf(lonlat[3])) == 90.0 ? 85.0
: Math.max(Double.valueOf(lonlat[1]), Double.valueOf(lonlat[3]));
double minlat = Math.min(Double.valueOf(lonlat[1]), Double.valueOf(lonlat[3])) == -90.0 ? -85.0
: Math.min(Double.valueOf(lonlat[1]), Double.valueOf(lonlat[3]));
long startCol = Math.round(Math.floor((minlon + 180.0) / 360.0 * (1 << level)));
long endCol = Math.round(Math.floor((maxlon + 180.0) / 360.0 * (1 << level)));
long startRow = Math.round(Math.floor((1.0
- Math.log(Math.tan(maxlat * Math.PI / 180.0) + 1.0 / Math.cos(maxlat * Math.PI / 180.0)) / Math.PI)
/ 2.0 * (1 << level)));
long endRow = Math.round(Math.floor((1.0
- Math.log(Math.tan(minlat * Math.PI / 180.0) + 1.0 / Math.cos(minlat * Math.PI / 180.0)) / Math.PI)
/ 2.0 * (1 << level)));
return new TileRowColAndCount(startRow, endRow, startCol, endCol);
}
- 根据计算的瓦片编号循环拼接成瓦片下载地址进行下载
/**
* 执行下载
*
* @param range 范围
* @param link 下载链接
* @param downloadDir 下载到本地的目录
* @param level 级别
*/
private void startDownload(TileRowColAndCount tileColRow, String link, String downloadDir, Integer level) {
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(20, 100, 500, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
// 遍历列行下载
for (long col = tileColRow.startCol; col <= tileColRow.endCol; col++) {
for (long row = tileColRow.startRow; row <= tileColRow.endRow; row++) {
// 线程池执行下载
threadPoolExecutor.execute(new DownloadMapTileThread(link, downloadDir, col, row, level));
}
}
threadPoolExecutor.shutdown(); // 关闭线程池
while (!threadPoolExecutor.isTerminated()) {
} // 所有任务被执行完毕时继续往下执行
}
瓦片拼接
- 原理很简单:使用BufferedImage创建一张新的图片,根据配置的下载信息,拿出对应瓦片编号的图片,添加到新图片的对应位置。
图片范围大小的限制,因为当图片太大,超过了的Integer范围,就会抛出异常。
/**
* 拼接
*
* @param tileColRow
* @param level
* @return
*/
private BufferedImage executeMergeImage(TileRowColAndCount tileColRow, int level) {
// 生成新图片
BufferedImage mergeImage = null;
try {
// 图片范围大小
if ((int) ((tileColRow.endCol - tileColRow.startCol + 1) * 256) < 46340
&& (int) ((tileColRow.endRow - tileColRow.startRow + 1) * 256) < 46340) {
mergeImage = new BufferedImage((int) ((tileColRow.endCol - tileColRow.startCol + 1) * 256),
(int) ((tileColRow.endRow - tileColRow.startRow + 1) * 256), BufferedImage.TYPE_INT_RGB);
// 合并所有子图片到新图片
int wx = 0;
int w1 = 0;
int h1 = 0;
for (long col = tileColRow.startCol; col <= tileColRow.endCol; col++) {
int wy = 0;
for (long row = tileColRow.startRow; row <= tileColRow.endRow; row++) {
File file = new File(
downloadDir + level + File.separator + col + File.separator + row + ".jpg");
if (file.exists() && (file.length() > 0)) {
BufferedImage img = ImageIO.read(file);
w1 = img.getWidth();
h1 = img.getHeight();
// 从图片中读取RGB
int[] ImageArrayOne = new int[w1 * h1];
ImageArrayOne = img.getRGB(0, 0, w1, h1, ImageArrayOne, 0, w1); // 逐行扫描图像中各个像素的RGB到数组中
mergeImage.setRGB(wx, wy, w1, h1, ImageArrayOne, 0, w1); // 设置上半部分或左半部分的RGB
} else {
continue;
}
wy += 256;
}
wx += 256;
}
} else {
logger.info("拼接后的图片超出范围,请小区域拼接再导入数据库融合!");
}
} catch (IOException e) {
e.printStackTrace();
}
return mergeImage;
}
使用gdal给拼接后的图片添加坐标信息
-
gdal的使用,这是一个第三方库,使用的时候非常的坑,相关资料又少,只有多次尝试总结,希望这篇文章能对使用者有所帮助。
1、下载完整的官网包,这里我使用的是3.0.0版本,直接点击SDKShell.bat文件便可开启工具,主要使用gdalinfo [图片绝对路径] 查看图片的坐标信息
2、Java程序配置gdal的使用,这一步坑最多,第一个是说需要配置gdal的依赖库,报错指明找不到gdalalljni.dll,则将改文件放到jdk/bin/目录下,发现依然报错,折腾半天,把所有的dll文件全部放到jdk/bin或jdk/lib/下,最后执行,能运行成功,但是还有报错说找不到proj.db文件,琢磨许久配置PROJ.LIB的环境变量,值为D:\map\release-1911-x64-gdal-3-0-0-mapserver-7-4-0\bin\proj6\share,依据自己目录来设置,这个时候再程序才能正常运行。以下是主要的程序代码:
- 知识要点
-
geoTransform,这是一个仿射变换六参数
- geoTransform[0]:左上角x坐标
- geoTransform[1]:东西方向空间分辨率
- geoTransform[2]:x方向旋转角
- geoTransform[3]:左上角y坐标
- geoTransform[4]:y方向旋转角
- geoTransform[5]:南北方向空间分辨率
影像中任意一个像素的坐标可以用下式计算:
默认情况下,a12、a21为0。
-
根据所下载的瓦片编号,计算出拼接后的大图对应的四个点的经纬度
-
SpatialReference坐标参考类来设置坐标信息
-
原来使用的是4326坐标系,但是在发布图层的时候发现图片纬度被压扁,照成纬度不对位,使用gdalinfo 查看猜测是有个映射属性导致,但是设置都不见生效,后面还是使用3857坐标系,这样就需要将经纬度转成实际的距离米(m)。
-
- 知识要点
gdal.AllRegister();
Dataset sourceDataset = gdal.Open(targetDir + level + ".tif", gdalconstConstants.GA_Update);
setGdalOfMercator(sourceDataset, tileColRow, level);
/**
* 给拼接后的图片设置位置信息,墨卡托
*/
public static void setGdalOfMercator(Dataset sourceDataset, TileRowColAndCount tileColRow, Integer level) {
// 六参数信息
double[] geoTransform = sourceDataset.GetGeoTransform();
// 设置要裁剪的起始像元位置,以及各方向的像元数
double startX = MercatorAndLatlonExchangeUtil.getMercatorLon((360.0 * (double) tileColRow.startCol) / (1 << level) - 180.0);
double endX = MercatorAndLatlonExchangeUtil.getMercatorLon((360.0 * (double) (tileColRow.startCol + (tileColRow.endCol - tileColRow.startCol + 1)))
/ (1 << level) - 180.0);
double startLat = (360 * Math.atan(Math.pow(Math.E,
(1 - Math.pow(2, 1.0 - level) * (tileColRow.startRow - (tileColRow.endRow - tileColRow.startRow + 1)
+ sourceDataset.GetRasterBand(1).getYSize() / 256.0)) * Math.PI)))
/ Math.PI - 90;
double endLat = (360 * Math.atan(Math.pow(Math.E,
(1 - Math.pow(2, 1.0 - level)
* (tileColRow.startRow + sourceDataset.GetRasterBand(1).getYSize() / 256.0)) * Math.PI)))
/ Math.PI - 90;
// 转成墨卡托坐标
double startY = MercatorAndLatlonExchangeUtil.getMercatorLat(startLat);
double endY = MercatorAndLatlonExchangeUtil.getMercatorLat(endLat);
// 图片的像素大小
int clipX = sourceDataset.GetRasterBand(1).getXSize();
int clipY = sourceDataset.GetRasterBand(1).getYSize();
// 左上角起始点
geoTransform[0] = startX;
geoTransform[3] = startY;
// 设置空间分辨率
geoTransform[1] = (endX - startX) / clipX;
geoTransform[5] = (endY - startY) / clipY;
// 偏率
geoTransform[2] = 0.0;
geoTransform[4] = 0.0;
SpatialReference ref = new SpatialReference();
ref.ImportFromEPSG(3857);
sourceDataset.SetGeoTransform(geoTransform);
sourceDataset.SetSpatialRef(ref);
// 释放资源
sourceDataset.delete();
}
- 不出意外,拼接后的图片用gdalinfo验证得出如下信息:
将程序打包
- 需将gdal的dll跟打包的jar放于同一目录下,设置PROJ.LIB的环境变量,值为D:\map\release-1911-x64-gdal-3-0-0-mapserver-7-4-0\bin\proj6\share,不想设置那么麻烦,我把proj6文件也放到jar所在目录,做了一个bat文件启动,downloadconfig.properties配置文件放于当前目录的上一级目录
@echo off
set SDK_ROOT=%~dp0
set SDK_ROOT=%SDK_ROOT:\\=\%
:setenv
SET "PROJ_LIB=%SDK_ROOT%proj6\SHARE"
java -jar ./downloadTileUtil.core-1.0.0-jar-with-dependencies.jar ../downloadconfig.properties
@pause
- 将jai_imageio-1.1.jar包放置jre/lib/ext/下,这是因为程序中生成tiff文件的时候用到这些依赖,而执行jar的运行环境没有这个jar包,所以需要单独添加jar包到运行环境中(有可能需要三个jai类型的jar),不然打包出来的程序运行拼接的时候会报错