去年做了java下载天地图数据的工具,由于每次都要手动查找再更改经纬度范围,用着不太方便,正好前段时间学习了JS打包下载文件的技术,于是花了点时间做了这个前端配置选择地图信息并下载离线地图的工具。
不过经实际测试,天地图数据通过JS下载时会报403拒绝访问,因为天地图服务端会识别出数据pa取并拒绝访问(应该是请求头或者什么的有特殊的识别码)
不过fan墙下载谷歌卫星地图是非常奈斯的。
注:JS下载不能完美解决跨域问题,但是对于需要浏览器代理访问的地图源是非常友好的。
请看示例:
HTML部分代码:
<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>
<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部分代码:
<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>
JS部分代码(篇幅太长,截取小部分):
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){
}
}
完整代码请点击链接下载:https://download.csdn.net/download/qq_33427869/86790578