//help array; let Caches = new Array(); //cache //let downloadRecord = new Array(); let dm = new Array(); //尝试增加下载管理 downloadmanager let alltilecount = 0; let downtilecount = 0; function ViewPort(canvasId, options) { // allow without 'new' if (!(this instanceof ViewPort)) return new ViewPort(canvasId, options); this.TILESIZE = 256; //实际的底图大小 //init this.canvas = document.getElementById(canvasId); this.canvas.width = this.canvas.clientWidth; this.canvas.height = this.canvas.clientHeight; this.ctx = this.canvas.getContext('2d'); this.width = this.canvas.clientWidth; this.height = this.canvas.clientHeight; if (!options) options = {}; //options could be null or undefine this.minZoomLevel = options.minZoomLevel ?? 0; this.maxZoomLevel = options.maxZoomLevel ?? 21; this.zoomLevel = options.zoomLevel ?? 0; this.dzl = options.dzl ?? 0.25; // zoomlevel offset value this.rtSize = calTileSize(this.TILESIZE, this.zoomLevel); //named rtSize because real time tile size const defaultCenterX = Math.pow(2, this.zoomLevel) * this.TILESIZE / 2; const defaultCenterY = Math.pow(2, this.zoomLevel) * this.TILESIZE / 2; this.xoffset = this.width / 2 - defaultCenterX; this.yoffset = this.height / 2 - defaultCenterY; //如果在options中有指定地图中心点 //options.centerLng && options.centerLat if (options.centerLat != null && options.centerLng != null && !isNaN(options.centerLat) && !isNaN(options.centerLng)) { console.log('指定中心点'); try { ({ lat: lat, lng: lng } = this.latlngPreCheck(options.centerLat, options.centerLng)); const my = lat2y(lat, this.zoomLevel) * this.TILESIZE; const mx = lon2x(lng, this.zoomLevel) * this.TILESIZE; this.xoffset = this.width / 2 - mx; this.yoffset = this.height / 2 - my; //console.log('check ', this.viewport2Latlng(this.width / 2, this.height / 2)); } catch (error) { console.log('使用默认中心点'); } } this.needtiles = new Array(); //需要加载的底图 实时变化 //move let moveflag = false; let downpoing = { x: 0, y: 0 }; this.canvas.onmousedown = (e) => { moveflag = true; [downpoing.x, downpoing.y] = [e.offsetX, e.offsetY]; //坐标转换 debug //console.log(this.zoomLevel, e.offsetX, e.offsetY); const wgs84 = this.viewport2Latlng(e.offsetX, e.offsetY); console.log('clicked', wgs84); } this.canvas.onmouseup = (e) => { moveflag = false; } this.canvas.onmouseout = () => { moveflag = false; //从元素范围外移动回来时不会继续移动,必须松开左键,按下左键后再进行移动 } this.canvas.onmousemove = (e) => { if (moveflag) { const movex = downpoing.x - e.offsetX; const movey = downpoing.y - e.offsetY; //实现移动逻辑 this.xoffset -= movex; this.yoffset -= movey; this.drawTiles(); downpoing.x = e.offsetX; downpoing.y = e.offsetY; } } //scale this.canvas.onwheel = (e) => { const dzl = this.dzl; const zoomleveloffset = (e.deltaY > 0) ? 1 * dzl : -1 * dzl; const oldz = this.zoomLevel; this.zoomLevel = this.zoomLevel - zoomleveloffset; if (this.zoomLevel < this.minZoomLevel) { this.zoomLevel = this.minZoomLevel; return; } if (this.zoomLevel > this.maxZoomLevel) { this.zoomLevel = this.maxZoomLevel; return; } //console.log(this.zoomLevel, e.offsetX, e.offsetY); this.updateViewport(e.offsetX, e.offsetY, oldz, this.zoomLevel); this.drawTiles(); } this.drawTiles(); // setInterval(() => { // debug(); // }, 1000); } //扩展一下Array 用以辅助进行加载底图 Array.prototype.remove = function (val) { const index = this.indexOf(val); if (index > -1) { return this.splice(index, 1); } return this; } ViewPort.prototype = { latlngPreCheck(lat, lng) { //数据超界处理 经度 -180 180 //纬度 < 85.0511287798066 (2*Math.atan(Math.pow(Math.E,Math.PI))-Math.PI/2)/Math.PI*180 const latlimit = 85.0511287798066; //取近似值 const lngp = lng - Math.floor((lng + 180) / 360) * 360; if (lat < (0 - latlimit) || lat > latlimit) { throw "OutMapRange"; } return { lat: lat, lng: lngp }; }, mplatPreCheck(mx, my) { //数据超界处理, 这里是对应缩放等级的地图的大小,底图大小*底图块数 注意底图块数始终为整数level时的底图块数 //实际只改变的底图大小,没有改变块数 let limit = calTileSize(this.TILESIZE, this.zoomLevel) * Math.pow(2, this.zoomLevel); if (this.zoomLevel != Math.floor(this.zoomLevel)) { limit = calTileSize(this.TILESIZE, this.zoomLevel) * Math.pow(2, Math.floor(this.zoomLevel + 1)); } if (mx <= 0 || my <= 0 || mx >= limit || my >= limit) { throw "OutMapRange"; } return { x: mx, y: my }; }, //------------------------------------- //这几个函数是坐标转换函数 mplat2Viewport(mx, my) { const vx = mx + this.xoffset; const vy = my + this.yoffset; return { x: vx, y: vy }; }, viewport2Mplat(vx, vy) { const mx = vx - this.xoffset; const my = vy - this.yoffset; return { x: mx, y: my }; }, viewport2Latlng(vx, vy) { let mplatc = this.viewport2Mplat(vx, vy); //mplat coordinates mplatc = this.mplatPreCheck(mplatc.x, mplatc.y); const lat = tile2lat(mplatc.y / this.TILESIZE, this.zoomLevel); const lng = tile2long(mplatc.x / this.TILESIZE, this.zoomLevel); return { lat: lat, lng: lng } }, latlng2Viewport(lat, lng) { ({ lat: lat, lng: lng } = this.latlngPreCheck(lat, lng)); //尝试使用解构赋值 const my = lat2y(lat, this.zoomLevel) * this.TILESIZE; const mx = lon2x(lng, this.zoomLevel) * this.TILESIZE; const vp = this.mplat2Viewport(mx, my); return { x: vp.x, y: vp.y }; }, //---------------------------------------- //更新偏移值 updateViewport(scrollx, scrolly, oldz, newz) { //计算出缩放后坐标系的偏移值 // xv = xm + offset ; offset = xv - xm const scrollm = this.viewport2Mplat(scrollx, scrolly); const basescale = Math.pow(2, newz - oldz); const newscrollm = { x: scrollm.x * basescale, y: scrollm.y * basescale }; this.xoffset = scrollx - newscrollm.x; this.yoffset = scrolly - newscrollm.y; }, //过滤需要加载的底图 tilesFilter(tails) { let news = new Array(); let max = Math.pow(2, this.zoomLevel); if (this.zoomLevel != Math.floor(this.zoomLevel)) { max = Math.pow(2, Math.floor(this.zoomLevel + 1)); } for (let i = 0; i < tails.length; i++) { const e = tails[i]; if (e.x >= 0 && e.y >= 0 && e.x < max && e.y < max) { news.push(e); } } return news; }, //一些绘制函数 clearCanvas() { this.ctx.clearRect(0, 0, this.width, this.height); }, drawRect(x, y, width, height) { this.ctx.strokeStyle = "white" this.ctx.strokeRect(x, y, width, height); }, drawtext(text, x, y) { this.ctx.fillStyle = "white" this.ctx.font = "18px serif"; this.ctx.textAlign = "center"; this.ctx.textBaseline = "middle"; this.ctx.fillText(text, x, y); }, drawTiles() { this.clearCanvas(); const viewport_left_top = this.viewport2Mplat(0, 0); const viewport_right_bottom = this.viewport2Mplat(this.width, this.height); const mixnum = calTileNum(viewport_left_top.x, this.TILESIZE, this.zoomLevel); const miynum = calTileNum(viewport_left_top.y, this.TILESIZE, this.zoomLevel); const maxnum = calTileNum(viewport_right_bottom.x, this.TILESIZE, this.zoomLevel); const maynum = calTileNum(viewport_right_bottom.y, this.TILESIZE, this.zoomLevel); const horicount = maxnum - mixnum + 1; const verticount = maynum - miynum + 1; let tails = new Array(); for (let i = 0; i < horicount; i++) { for (let j = 0; j < verticount; j++) { const x = mixnum + i; const y = miynum + j; tails.push({ x: x, y: y }); } } this.needtiles = this.tilesFilter(tails); //获取当前视口要渲染的底图 let z = this.zoomLevel; //这里的几行代码的逻辑是用到了5个函数里面,后面可以想一下怎么重构 if (this.zoomLevel != Math.floor(this.zoomLevel)) { z = Math.floor(this.zoomLevel + 1); } //尝试在这里移除不需要继续下载的内容 let indexs = new Array(); dm.forEach(d => { if (d.z !=z) { if(this.needtiles.find(e=>e.x==d.x&&e.y==d.y)==undefined){ d.xhr.abort(); console.log('中断请求'); dm.remove(d); } } }); alltilecount += this.needtiles.length; //for debug //console.log(this.needtiles); this.needtiles.forEach(et => { const etsize = calTileSize(this.TILESIZE, this.zoomLevel); const tilev = this.mplat2Viewport(et.x * etsize, et.y * etsize); this.drawRect(tilev.x, tilev.y, etsize, etsize); const text = `${z}/${et.x}/${et.y}.png`; // console.log(text); this.drawtext(text, tilev.x + etsize / 2, tilev.y + etsize / 2); this.drawTileImg(z, et.x, et.y); }); //debug(); }, drawTileImg(z, x, y) { const img = getImg(this, z, x, y); img.then((e) => { if (isTileShouldDownload(this, z, x, y)) { //对e添加判断,似乎不是每一个e都是一个图片 if (e instanceof ImageBitmap) { const etsize = calTileSize(this.TILESIZE, this.zoomLevel); const tilev = this.mplat2Viewport(x * etsize, y * etsize); this.ctx.drawImage(e, tilev.x, tilev.y, etsize, etsize); } } }).catch((e) => { console.log(e); }); }, } //计算实时底图大小 function calTileSize(tilesize, zoomLevel) { let rtSize = tilesize; if (zoomLevel != Math.floor(zoomLevel)) { rtSize = tilesize * Math.pow(2, zoomLevel - Math.floor(zoomLevel + 1)); } return rtSize; } //计算当前坐标值所在的底图编号 function calTileNum(coord, tilesize, zoomLevel) { return Math.floor(coord / calTileSize(tilesize, zoomLevel)); } //=====coord about 地理坐标与墨卡托投影的相关转换方法===== function lon2tile(lon, z) { return (Math.floor((lon + 180) / 360 * Math.pow(2, z))); } function lat2tile(lat, z) { return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z))); } function lon2x(lon, z) { return ((lon + 180) / 360 * Math.pow(2, z)); } function lat2y(lat, z) { return ((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, z)); } function tile2long(x, z) { return (x / Math.pow(2, z) * 360 - 180); } function tile2lat(y, z) { var n = Math.PI - 2 * Math.PI * y / Math.pow(2, z); return (180 / Math.PI * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)))); } //=========下载图片相关================== //将其和viewpoint分离,方便后续修复问题和增加新的下载方式或者底图源 //判断一张底图需不需要继续下载,这里采用保守一点的下载策略,必须是先判定为需要加载的底图才开始下载 //传入vp以获取相关参数 function isTileShouldDownload(vp, z, x, y) { let fz = vp.zoomLevel; if (vp.zoomLevel != Math.floor(vp.zoomLevel)) { fz = Math.floor(vp.zoomLevel + 1); } if (vp.needtiles.find(t => z == fz && t.x == x && t.y == y)) { return true; } return false; } //add autonavi function downloadimg(z, x, y) { return new Promise((resolve) => { //const url = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`; const url = 'http://webrd01.is.autonavi.com/' + 'appmaptile?lang=zh_cn&size=1&scale=1&style=8' + `&x=${x}&y=${y}&z=${z}`; //add downloadcheck const obj = { z: z, x: x, y: y }; downloadRecord.push(obj); //下载前加入记录 let img = new Image(); const dmobj = { img: img, z: z, x: x, y: y }; img.onload = () => { //console.log('downloaded' + z + '/' + x + '/' + y + '.png'); downloadRecord.remove(obj); //下载完成后从记录中移除 dm.remove(dmobj); Caches.push({ img: img, z: z, x: x, y: y }); //下载完成后加入缓存 downtilecount += 1; resolve(img); } img.src = url; dm.push(dmobj); //尝试添加管理 }); } //尝试优化图片下载机制 //采用下载速度比较慢的openstreetmap 测试 function xhrdownloadimg(z, x, y) { return new Promise((resolve, reject) => { const url = `https://b.tile.openstreetmap.org/${z}/${x}/${y}.png`; let imgxhr = new XMLHttpRequest(); imgxhr.open('GET', url, true); //add downloadcheck //const obj = { z: z, x: x, y: y }; //downloadRecord.push(obj); //下载前加入一条记录 const dmobj = { xhr: imgxhr, z: z, x: x, y: y }; //imgxhr.withCredentials = true; imgxhr.responseType = 'blob'; imgxhr.onload = function (e) { if (this.status === 200 && imgxhr.response) { let blob = new Blob([imgxhr.response], { type: 'image/png' }); let img = createImageBitmap(blob); Caches.push({ img: img, z: z, x: x, y: y }); downtilecount += 1; //downloadRecord.remove(obj); //下载完成后将记录移除 dm.remove(dmobj); console.log('dm下载完成减少'); resolve(img); } else { reject('下载似乎失败了'); dm.remove(dmobj); console.log('dm下载失败减少'); } }; dm.push(dmobj); //保存xhr对象 和xyz索引 imgxhr.send(null); }); } async function getImg(vp, z, x, y) { const found = Caches.find(e => (e.z == z && e.x == x && e.y == y)); //查找缓存 if (found == undefined) { //缓存中没有 console.log('缓存中没有' + z + '/' + x + '/' + y + '.png'); //这里不需要判断是否继续加载,在还未开始下载图片前,这里可以说是立即执行的 console.log(isTileShouldDownload(vp, z, x, y));//always true const dmobj = dm.find(e => (e.z == z && e.x == x && e.y == y)); if (dmobj == undefined) { //如果缓存中没有,并且没有下载记录 发起下载请求并等待结果 let p = await xhrdownloadimg(z, x, y); return p; } else {//有下载记录就不用继续下载了 console.log(vp.zoomLevel, vp.xoffset, vp.yoffset); return Promise.reject('需要等待下载完成'); } } else { //缓存中有直接返回 console.log('缓存中有' + z + '/' + x + '/' + y + '.png'); return found.img; } } function debug() { console.log('=============debug================='); console.log('alltilecount', alltilecount); console.log('downtilecount', downtilecount); console.log('cachecount', Caches.length); console.log('dm count', dm.length); console.log('=============debug================='); }