You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

437 lines
16 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

//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=================');
}