JS download map offline data, front-end download Google offline map

Last year, I made a java tool for downloading Tiantu map data. Since I have to manually search and change the latitude and longitude range every time, it is not convenient to use. I just learned the technology of JS packaging and downloading files a while ago, so I spent some time making this front end. Configure tools for selecting map information and downloading offline maps.

However, after actual testing, when Tiandi map data is downloaded through JS, it will report 403 access denied, because the Tiandi map server will recognize the data pa fetch and deny access (it should be a request header or something with a special identification code)

However, it is very nice to download Google satellite maps from the fan wall.

Note: JS download cannot perfectly solve cross-domain problems, but it is very friendly to map sources that require browser proxy access.

See example:


 HTML part code:

<body>
	<div class="content">
		<div class="title">地图源设置</div>
		<div class="optionGroup">
			<div class="option">
				<div class="left">地图URL:</div>
				<div class="right">
					<input type="text" style="width:500px" id="mapUrl" value="https://khms{s}.google.com/kh/v=930?x={x}&y={y}&z={z}"/>
					<label class="detail">示例:https://khms{s}.google.com/kh/v=930?x={x}&y={y}&z={z}。其中{s}是可选的,若存在将使用主机编号值;{x}、{y}、{z}是必不可少的。</label>
				</div>
			</div>
			<div class="option">
				<div class="left">主机编号[开始]:</div>
				<div class="right">
					<input type="text" id="minServer" style="width:80px;" value="0" oninput="this.value=this.value.replace(/[^\d]/g,'')">
				</div>
			</div>
			<div class="option">
				<div class="left">主机编号[结束]:</div>
				<div class="right">
					<input type="text" id="maxServer" style="width:80px;" value="3" oninput="this.value=this.value.replace(/[^\d]/g,'')">
				</div>
			</div>
			<div class="option">
				<div class="left">最大级别:</div>
				<div class="right">
					<select id="maxZoom">
						<option value="1">1</option>
						<option value="2">2</option>
						<option value="3">3</option>
						<option value="4">4</option>
						<option value="5">5</option>
						<option value="6">6</option>
						<option value="7">7</option>
						<option value="8">8</option>
						<option value="9">9</option>
						<option value="10">10</option>
						<option value="11">11</option>
						<option value="12">12</option>
						<option value="13">13</option>
						<option value="14">14</option>
						<option value="15">15</option>
						<option value="16">16</option>
						<option value="17">17</option>
						<option value="18" selected="selected">18</option>
						<option value="19">19</option>
						<option value="20">20</option>
						<option value="21">21</option>
						<option value="22">22</option>
						<option value="23">23</option>
						<option value="24">24</option>
						<option value="25">25</option>
					</select>
				</div>
			</div>
			<div class="option">
				<div class="left">投影类型:</div>
				<div class="right">
					<input type="radio" name="projection" value="mkt" checked="cheched"><label>墨卡托</label>
					&nbsp;&nbsp;
					<input type="radio" name="projection" value="lonlat"><label>等经纬度</label>
				</div>
			</div>
			<div class="option">
				<div class="left">图片大小:</div>
				<div class="right">
					<input type="radio" name="tileSize" value="256" checked="cheched">256像素
				</div>
			</div>
		</div>
		<div class="title">下载设置</div>
		<div class="optionGroup">
			<div class="option">
				<div class="left">下载级别[开始]:</div>
				<div class="right">
					<input type="text" id="startZoom" style="width:80px;" value="1" oninput="this.value=this.value.replace(/[^\d]/g,'')">
				</div>
			</div>
			<div class="option">
				<div class="left">下载级别[结束]:</div>
				<div class="right">
					<input type="text" id="endZoom" style="width:80px;" value="7" oninput="this.value=this.value.replace(/[^\d]/g,'')">
				</div>
			</div>
			<div class="option" style="height:120px;">
				<div class="left">下载范围:</div>
				<div class="right" style="height:120px;">
					<div style="float:left;width:200px;height:120px;border-right:dotted 1px #888;">
						左上角纬度:
						<input type="text" id="maxLat" style="width:80px;" value="41" oninput="this.value=this.value.replace(/[^\d.]/g,'')"></br>
						左上角经度:
						<input type="text" id="minLon" style="width:80px;" value="115" oninput="this.value=this.value.replace(/[^\d.]/g,'')"></br>
						右下角纬度:
						<input type="text" id="minLat" style="width:80px;" value="39" oninput="this.value=this.value.replace(/[^\d.]/g,'')"></br>
						右下角经度:
						<input type="text" id="maxLon" style="width:80px;" value="118" oninput="this.value=this.value.replace(/[^\d.]/g,'')"></br>
					</div>
					<div class="button" id="chooesRange" style="float:left;width:auto;height:26px;padding:0px 10px;margin-top:90px;font:normal 14px/26px '微软雅黑';color:#333;background:#ccc;border-radius:3px;cursor:pointer">框选地图范围</div>
				</div>
			</div>
			<div class="option">
				<div class="left">保存格式:</div>
				<div class="right">
					<input type="radio" name="imageFormat" value="jpg" checked="cheched">jpg
				</div>
			</div>
			<div class="option">
				<label class="detail" style="color:red;">注:文件将{yyyyMMddHHmmss}.zip格式命名打包下载,其中瓦片数据文件存放规则为{z}/{y}/{x}.jpg</label>
			</div>
		</div>
		
		<div class="buttonGroup">
			<div class="button" id="checkMap">验证地图配置</div>
			<div class="button" id="downloadMap">开始下载</div>
		</div>
	</div>
	
	<div class="mark" id="mark"></div>
	<div class="mapView" id="mapView">
		<div class="view-top">
			<div class="label" id="view-title">验证地图配置</div>
			<div class="view-close" id="view-close"></div>
		</div>
		<div class="view-content">
			<div id="mapDiv"></div>
			<div class="drawUtil" id="drawUtil">
				<div class="button" id="submit">确定</div>
				<div class="button" id="redraw">重新框选</div>
			</div>
			<div class="bottomUtil">
				<div id="currentZoom"></div>
				<div id="currentCoordinate"></div>
			</div>
		</div>
	</div>
	<div class="waitMark" id="waitMark">
		<div class="waitInfo" id="waitInfo">处理中,请稍等...</div>
	</div>
</body>

CSS part code:

<style>
	body{min-width:720px;padding:20px;position:relative;}
	.title{width:100%;height:40px;text-align:left;font:bold 18px/40px '微软雅黑';}
	.optionGroup{width:100%;height:auto;padding:10px;}
	.option{width:100%;height:auto;min-height:30px;}
	.option .left{float:left;width:160px;height:auto;min-height:30px;text-align:right;font:normal 16px/30px '微软雅黑';}
	.option .right{float:left;width:calc(100% - 305px);padding-left:5px;height:auto;min-height:30px;font:normal 14px/30px '微软雅黑';}
	.option .right input[type="text"]{height:22px;padding:1px 2px;margin-top:2px;border:solid 1px #555;border-radius:3px;}
	.option .detail{width:auto;height:auto;min-height:30px;height:30px;text-align:left;padding-left:10px;font:normal 14px/30px '微软雅黑';color:#888;}
	.buttonGroup{width:100%;height:30px;padding:10px;margin-top:20px;}
	.button{float:left;width:auto;height:30px;padding:0px 20px;margin-left:40px;font:normal 16px/30px '微软雅黑';color:#ffffff;background:#0075ff;border-radius:3px;cursor:pointer}
	
	.mark{width:100%;height:100%;background:rgba(0,0,0,0.7);position:fixed;top:0px;left:0px;display:none;z-index:100;}
	.mapView{width:90%;height:80%;background:#fff;position:fixed;top:10%;left:5%;border-radius:3px;overflow:hidden;display:none;z-index:1001;}
	.mapView .view-top{width:100%;height:29px;background:#e4e7eb;border-bottom:solid 1px #fff;}
	.mapView .view-top .label{float:left;width:auto;height:29px;margin-left:10px;font:normal 14px/30px '微软雅黑';color:#333;}
	.mapView .view-top .view-close{float:right;width:30px;height:30px;font:normal 18px/30px '微软雅黑';color:#888;text-align:center;cursor:pointer;}
	.mapView .view-top .view-close:before{content:"\2716";font:normal 18px/30px '微软雅黑';color:#888;text-align:center;}
	.mapView .view-top .view-close:hover:before{content:"\2716";font:normal 18px/30px '微软雅黑';color:#f00;text-align:center;}
	.mapView .view-content{width:100%;height:calc(100% - 30px);position:relative;}
	.mapView .view-content #mapDiv{width:100%;height:100%;}
	.mapView .view-content .drawUtil{width:100%;height:50px;position:absolute;right:0px;bottom:30px;z-index:1999;pointer-events:none;display:none;}
	.mapView .view-content .drawUtil #submit{float:right;margin:0px;margin-right:20px;pointer-events:auto;}
	.mapView .view-content .drawUtil #redraw{float:right;margin:0px;margin-right:20px;pointer-events:auto;}
	.mapView .view-content .bottomUtil{width:100%;height:30px;position:absolute;left:0px;bottom:0px;z-index:1999;pointer-events:none;background:rgba(255,255,255,0.5);}
	.mapView .view-content .bottomUtil #currentZoom{float:left;width:auto;height:30px;margin-left:30px;font:normal 14px/30px '微软雅黑';color:#000;}
	.mapView .view-content .bottomUtil #currentCoordinate{float:left;width:auto;height:30px;margin-left:30px;font:normal 14px/30px '微软雅黑';color:#000;}

	.waitMark{width:100%;height:100%;background:rgba(0,0,0,0.7);position:fixed;top:0px;left:0px;display:none;cursor:wait;z-index:100;}
	.waitMark .waitInfo{width:100%;height:30px;position:fixed;top:calc(50% - 15px);left:0px;font:normal 14px/30px '微软雅黑';color:#fff;text-align:center;z-index:1001;}
</style>

Part of the JS code (too long, intercept a small part):

function downloadMap(){
	$("#waitInfo").html("处理中,请稍等...");
	var mapUrl = $("#mapUrl").val();
	var minServer = $("#minServer").val();
	var maxServer = $("#maxServer").val();
	var maxZoom = $("#maxZoom").val();
	var projection = $("input[name='projection']").val();
	var serverArr = [];
	if(typeof(mapUrl)=="undefined" || mapUrl==null || mapUrl==""){
		alert("请输入地图连接");
		return false;
	}
	if(mapUrl.indexOf("{s}") > -1){
		var isServerOk = true;
		try{
			minServer = parseInt(minServer);
			maxServer = parseInt(maxServer);
		}catch(e){
			isServerOk = false;
		}
		if(isServerOk){
			for(var i=minServer; i<=maxServer; i++){
				serverArr.push(i+"");
			}
		}else{
			alert("主机编号设置错误");
			return false;
		}
		if(serverArr.length == 0){
			alert("主机编号设置错误");
			return false;
		}
	}
	var imageFormat = $("input[name='imageFormat']").val();
	var startLat = parseFloat($("#maxLat").val());//左上角纬度:开始纬度(从北到南)
	var endLat = parseFloat($("#minLat").val());//右下角纬度:结束纬度(从北到南)
	var startLon = parseFloat($("#minLon").val());//左上角经度:开始经度(从西到东)
	var endLon = parseFloat($("#maxLon").val());//右下角经度:结束经度(从西到东)
	var minZoom = parseInt($("#startZoom").val());//下载级别[开始]
	var maxZoom = parseInt($("#endZoom").val());//下载级别[结束]
	
	if(typeof(startLat)=="undefined" || startLat==null || startLat==""){
		alert("请输入左上角纬度");
		return false;
	}
	if(typeof(startLon)=="undefined" || startLon==null || startLon==""){
		alert("请输入左上角经度");
		return false;
	}
	if(typeof(endLat)=="undefined" || endLat==null || endLat==""){
		alert("请输入右下角纬度");
		return false;
	}
	if(typeof(minZoom)=="undefined" || minZoom==null || minZoom==""){
		alert("请输入下载级别[开始]");
		console.log(22)
		return false;
	}
	if(typeof(maxZoom)=="undefined" || maxZoom==null || maxZoom==""){
		alert("请输入下载级别[结束]");
		return false;
	}
	
	zip = new JSZip();
	zipName = new Date().format('yyyyMMddHHmmss');
	fileFolder = zip.folder(zipName);
	fileList = [];
	fileCount = 0;
	allCount = 0;

	promiseArr = [];//异步代码集合

	if(projection == "lonlat"){//等经纬度
		//先计算出一共需要下载多少个
		for(var z=minZoom; z<=maxZoom; z++){
			var deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
			var startX = parseInt((startLon + 180) / deg / 256);//减数取整
			var endX = parseInt((endLon + 180) / deg / 256);//加数取整
			var startY = parseInt((90 - startLat) / deg / 256);
			var endY = parseInt((90 - endLat) / deg / 256);
			allCount += (endY-startY+1) * (endX-startX+1);
		}
		//再执行下载,下载是异步的,需要通过allCount判断是否全部下载完毕
		for(var z=minZoom; z<=maxZoom; z++){
			var deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
			var startX = parseInt((startLon + 180) / deg / 256);//减数取整
			var endX = parseInt((endLon + 180) / deg / 256);//加数取整
			var startY = parseInt((90 - startLat) / deg / 256);
			var endY = parseInt((90 - endLat) / deg / 256);
			for(var y=startY; y<=endY; y++){
				for(var x=startX; x<=endX; x++){
					var url = mapUrl.replace("{s}", serverArr[parseInt(Math.random()*serverArr.length)]).replace("{z}", z+"").replace("{y}", y+"").replace("{x}", x+"");
					var name = z+"/"+y+"/"+x+"."+imageFormat;
					savePictureZip(url, name);
					//savePictureZip2(url, name);
				}
			}
		}
	}else{//墨卡托
		//先计算出一共需要下载多少个
		for(var z=minZoom; z<=maxZoom; z++){
			var deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
			var startX = parseInt((startLon + 180) / deg / 256);
			var endX = parseInt((endLon + 180) / deg / 256);
			var startY = parseInt((parseInt(Math.pow(2, z) * 256 / 2) - parseInt((Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256);
			var endY = parseInt((parseInt(Math.pow(2, z) * 256 / 2) - parseInt((Math.log(Math.tan((90 + endLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256);
			allCount += (endY-startY+1) * (endX-startX+1);
		}
		//再执行下载,下载是异步的,需要通过allCount判断是否全部下载完毕
		for(var z=minZoom; z<=maxZoom; z++){
			var deg = 360.0 / Math.pow(2, z) / 256;//一个像素点代表多少度
			var startX = parseInt((startLon + 180) / deg / 256);
			var endX = parseInt((endLon + 180) / deg / 256);
			var startY = parseInt((parseInt(Math.pow(2, z) * 256 / 2) - parseInt((Math.log(Math.tan((90 + startLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256);
			var endY = parseInt((parseInt(Math.pow(2, z) * 256 / 2) - parseInt((Math.log(Math.tan((90 + endLat) * Math.PI / 360)) / (Math.PI / 180)) / (360/Math.pow(2, z)/256) + 0.5)) / 256);
			for(var y=startY; y<=endY; y++){
				for(var x=startX; x<=endX; x++){
					var url = mapUrl.replace("{s}", serverArr[parseInt(Math.random()*serverArr.length)]).replace("{z}", z+"").replace("{y}", y+"").replace("{x}", x+"");
					var name = z+"/"+y+"/"+x+"."+imageFormat;
					savePictureZip(url, name);
					//savePictureZip2(url, name);
				}
			}
		}
	}
	var t = window.setInterval(function(){
		if(fileCount == allCount){
			$("#waitMark").css("display", "none");
			if(fileList.length < fileCount){
				alert("部分文件下载失败,请重新尝试下载");
			}
			window.clearInterval(t);
		}
	}, 50);
}

function savePictureZip(url, name){
	try{
		var xhr = new XMLHttpRequest();
		xhr.open('get', url, true);
		xhr.responseType = 'blob';//二进制对象,binary
		xhr.onload = function(){
			fileCount++;
			$("#waitInfo").html("正在下载:"+url);
			var blob = xhr.response;//response 属性返回响应的正文,取决于 responseType 属性。
			if(blob.type.indexOf("image") > -1){
				fileList.push({name: name, content: blob});
				if (fileCount==allCount && fileList.length==fileCount) {
					if (fileList.length > 0) {
						$("#waitInfo").html("正在打包");
						for (var k = 0; k < fileList.length; k++) {
							// 往文件夹中,添加个文件的数据
							fileFolder.file(fileList[k].name, fileList[k].content, {
								binary: true //二进制
							})
						}
						zip.generateAsync({type: 'blob'}).then(function(content){
							saveAs(content, zipName+'.zip');
						})
					}else{
						alert("没有文件");
					}
					$("#waitMark").css("display", "none");
				};
			}
		};
		xhr.ontimeout = function(){
			fileCount++;
			$("#waitInfo").html("正在下载:"+url);
		};
		xhr.onerror = function(){
			fileCount++;
			$("#waitInfo").html("正在下载:"+url);
		};
		xhr.send(null);
	}catch(e){
	}
}

Please click the link to download the complete code: https://download.csdn.net/download/qq_33427869/86790578

Java downloads Tiandi map data, Tiandi map offline map, you can specify the latitude and longitude range_RainingTime's blog-CSDN blog_Tiandi map offline map requires the Tiandi map developer key. Pay attention to the limit on the number of accesses to the Tiantu map API. Code sharing: import java.io.BufferedOutputStream;import java.io.File;import java.io.FileOutputStream;import java.io.InputStream;import java.io.OutputStream;import java.util.concurrent.ExecutorService;import java. util.concur.. https://blog.csdn.net/qq_33427869/article/details/120347939

Guess you like

Origin blog.csdn.net/qq_33427869/article/details/127422496