整理项目

按结构将文件分开到单独的js中,使用module的方式组织
增加设置中心点和记住查看位置的功能
main
nxiaoxiao 1 year ago
parent d2f59ebc42
commit d4169af3ea

@ -1,23 +1,13 @@
对接他人提供的底图服务
然后是开发自己的底图服务,结构可以参考现成已有的
## 参考链接
https://xcsf.github.io/blog/2020/06/12/%E7%93%A6%E7%89%87Tile%E5%9C%B0%E5%9B%BE%E5%8E%9F%E7%90%86/
https://xcsf.github.io/blog/2020/06/12/%E7%93%A6%E7%89%87Tile%E8%A1%8C%E5%88%97%E5%8F%B7%E8%AE%A1%E7%AE%97%E6%96%B9%E6%B3%95/
https://github.com/RLwu/GIS
https://switch2osm.org/using-tiles/getting-started-with-leaflet/
https://leafletjs.cn/
https://zh.wikipedia.org/wiki/%E9%BA%A5%E5%8D%A1%E6%89%98%E6%8A%95%E5%BD%B1%E6%B3%95
https://zh.wikipedia.org/wiki/Web%E5%A2%A8%E5%8D%A1%E6%89%98%E6%8A%95%E5%BD%B1
https://web.archive.org/web/20220421120137/https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames
https://en.wikipedia.org/wiki/Web_Mercator_projection
https://web.archive.org/web/20141009142830/http://earth-info.nga.mil/GandG/wgs84/web_mercator/(U)%20NGA_SIG_0011_1.0.0_WEBMERC.pdf
https://www.openstreetmap.org/#map=15/22.7602/114.3888&layers=D
https://en.wikipedia.org/wiki/Eccentricity_(mathematics)#:~:text=The%20eccentricity%20of%20an%20ellipse%20is%2C%20most%20simply%2C%20the%20ratio,of%20the%20semimajor%20axis%20a.&text=(Flattening%20may%20be%20denoted%20by,if%20f%20is%20linear%20eccentricity.)
http://wiki.gis.com/wiki/index.php/Geodetic_system
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/textBaseline
https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/textAlign
## 项目概述
为地图类网页app添加底图支持目前仅支持加载web墨卡托的瓦片底图
暂时选择的是gaode的普通底图可无缝切换为openstreetmap底图服务
## Features
将屏幕坐标系映射到投影平面为坐标转化提供支持见viewport
将viewport和底图加载的逻辑分离开
事件和绘制逻辑在basemap中对外直接使用basemap挂载到canvas就行 参考index.js
支持设置初始位置,和记忆上次查看位置
## tips
坐标转化 是直接进行像素级的映射
缩放在瓦片地图那一层已经定义好了,不需要再额外转化

@ -1,32 +0,0 @@
web:
image: nginx
ports:
- 1080:80
volumes:
- ./nginx_conf/nginx:/etc/nginx
- ./dist:/usr/share/nginx/html:ro
#include /etc/nginx/conf.d/*.conf;
server {
listen 80;
#listen [::]:80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
location /m {
rewrite .* /index.html permanent;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}

@ -1,305 +0,0 @@
//椭球相关参数计算
const a = 6378137; //长半轴 a
const f = 1 / 298.257223563; //扁率 (a-b)/a
const b = a * (1 - f);//短半轴 b
const e = Math.sqrt(a * a - b * b) / a;//椭球第一偏心率
// console.log(a);
// console.log(f);
// console.log(b);
// console.log(e);
// console.log(e*e);
function degToRad(deg) {
return deg / 180 * Math.PI;
}
function radToDeg(rad) {
return rad / Math.PI * 180;
}
function wgs84ToWebMercator(lat, lon) {
let x = a * lon;
let y = a * Math.log(Math.tan(Math.PI / 4 + lat / 2));
return { x: x, y: y };
}
function webMercatorToWgs84(x, y) {
let lon = x / a;
let lat = 2 * Math.atan(Math.pow(Math.E, y / a)) - Math.PI / 2;
return { lon: lon, lat: lat }
}
// console.log(wgs84ToWebMercator(degToRad(50), degToRad(50)));
// console.log(wgs84ToWebMercator(degToRad(49.99999999999999), degToRad(49.99999999999999)));
// let wgs = webMercatorToWgs84(5565974.539663678, 6446275.841017158);
// console.log(wgs);
// console.log('lat: ' + radToDeg(wgs.lat) + ','
// + 'lon:' + radToDeg(wgs.lon));
// console.log(2 * Math.atan(Math.pow(Math.E, 0 / a)) - Math.PI / 2);
// console.log(radToDeg(webMercatorToWgs84(5565974.539663678, 0).lat));
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))));
}
//test
//22.7490073, 114.3781213 z=15
//let mappiece = Math.pow(2, zoom_level); //一个方向的地图块数 1块256个像素
// console.log(lon2tile(long, zoom_level));
// console.log(lat2tile(lat, zoom_level));
// console.log(lon2x(long, zoom_level));
// console.log(lat2y(lat, zoom_level));
// console.log('----------------------');
// console.log(tile2long(26794, zoom_level));
// console.log(tile2long(26794 + 1 / 256, zoom_level));
//https://tile.openstreetmap.org/z/x/y.png
//https://tile.openstreetmap.org/15/26794/14256.png
//已知条件 lat long zoom_level 显示范围的宽高
let zoom_level = 15;
let lat = 22.7490073;
let long = 114.3781213;
let width = 1600;
let height = 900;
function calBasetile(lat, long, zoom_level) {
let basetile = {};
basetile.x = lon2x(long, zoom_level);//对应zoomlevel下long的x坐标
basetile.y = lat2y(lat, zoom_level);//对应zoomlevel下lat的y坐标
basetile.tilex = lon2tile(long, zoom_level);//对应zoomlevel下long的x tile编号
basetile.tiley = lat2tile(lat, zoom_level);//对应zoomlevel下lat的y tile编号
basetile.offsetx = Math.floor((basetile.x - basetile.tilex) * 256);//相对于左上角其点的x偏移值
basetile.offsety = Math.floor((basetile.y - basetile.tiley) * 256);//相对于左上角其点的y偏移值
basetile.minx = width / 2 - basetile.offsetx;
basetile.maxx = basetile.minx + 256;
basetile.miny = height / 2 - basetile.offsety;
basetile.maxy = basetile.miny + 256;
basetile.p1 = Math.floor(basetile.maxy / 256 + 0.5);
basetile.p2 = Math.floor((height - basetile.miny) / 256 + 0.5);
basetile.p3 = Math.floor(basetile.maxx / 256 + 0.5);
basetile.p4 = Math.floor((width - basetile.minx) / 256 + 0.5);
console.log(basetile);
return basetile;
}
let basetile = calBasetile(lat, long, zoom_level);
// let x = lon2x(long, zoom_level);
// let y = lat2y(lat, zoom_level);
// let tilex = lon2tile(long, zoom_level);
// let tiley = lat2tile(lat, zoom_level);
// let offsetx = Math.floor((x - tilex) * 256);
// let offsety = Math.floor((y - tiley) * 256);
// //计算出瓦片地图的基准坐标,假设显示的宽高为1600*900
// let minx = width / 2 - offsetx;
// let maxx = minx + 256;
// let miny = height / 2 - offsety;
// let maxy = miny + 256;
//计算出四个方向各需要的块数
//p1 p2 p3 p4 上 下 左 右
// let p1 = Math.floor(maxy / 256 + 0.5);
// let p2 = Math.floor((height - miny) / 256 + 0.5);
// let p3 = Math.floor(maxx / 256 + 0.5);
// let p4 = Math.floor((width - minx) / 256 + 0.5);
//基于已知条件计算需要的各个tile
function needtiles(basetile, p1, p2, p3, p4) {
let tiles = new Array();
let tilex = basetile.tilex;
let tiley = basetile.tiley;
tiles.push({ x: tilex, y: tiley });
for (let i = 0; i < p4; i++) {
tiles.push({ x: tilex + i + 1, y: tiley });
}
for (let i = 0; i < p3; i++) {
tiles.push({ x: tilex - i - 1, y: tiley });
}
for (let i = 0; i < p1; i++) {
for (let j = 0; j < p3 + p4 + 1; j++) {
tiles.push({ x: tilex - p3 + j, y: tiley - i - 1 });
}
}
for (let i = 0; i < p2; i++) {
for (let j = 0; j < p3 + p4 + 1; j++) {
tiles.push({ x: tilex - p3 + j, y: tiley + i + 1 });
}
}
//console.log(tiles);
//console.log(tiles.length);
return tiles;
}
let tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
//渲染问题
//通过与基准之间的位置关系计算渲染的坐标
//minx miny 即为基准tile当前渲染位置
// for (let i = 0; i < tiles.length; i++) {
// const element = tiles[i];
// let renderx = (element.x - tilex) * 256 + minx;
// let rendery = (element.y - tiley) * 256 + miny;
// console.log({ x: renderx, y: rendery });
// }
let canvas = document.getElementById('map');
canvas.width = map.clientWidth;
canvas.height = map.clientHeight;
let ctx = canvas.getContext('2d');
function clearcanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function drawPoint(x, y) {
ctx.fillStyle = "white"
ctx.strokeStyle = "#a35312"
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function drawRect(x, y) {
ctx.strokeStyle = "white"
ctx.strokeRect(x, y, 256, 256);
}
function drawtext(text, x, y) {
ctx.fillStyle = "white"
ctx.font = "18px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function drawgrid(basetile, tiles) {
let minx = basetile.minx;
let miny = basetile.miny;
let offsetx = basetile.offsetx;
let offsety = basetile.offsety;
let tilex = basetile.tilex;
let tiley = basetile.tiley;
clearcanvas();
drawPoint(minx + offsetx, miny + offsety);
for (let i = 0; i < tiles.length; i++) {
const element = tiles[i];
let renderx = (element.x - tilex) * 256 + minx;
let rendery = (element.y - tiley) * 256 + miny;
//console.log({ x: renderx, y: rendery });
drawPoint(renderx, rendery);
drawRect(renderx, rendery);
const showtext = 'x:' + element.x + ' ' + 'y:' + element.y;
//drawtext(showtext, renderx + 256 / 2, rendery + 256 / 2);
drawtext(showtext, renderx + showtext.length / 4 * 18 + 4, rendery + 9);
}
}
drawgrid(basetile, tiles);
// drawPoint(width / 2, height / 2);
// const element = tiles[0];
// let renderx = (element.x - tilex) * 256 + minx;
// let rendery = (element.y - tiley) * 256 + miny;
// drawPoint(renderx, rendery);
// drawPoint(renderx + 256 / 2, rendery + 256 / 2);
// drawRect(renderx, rendery);
// drawtext(showtext, renderx + 256 / 2, rendery + 256 / 2);
// console.log({ x: renderx, y: rendery });
//实现移动缩放方法
//缩放
map.onwheel = (e) => {
//console.log(e);
const zoomleveloffset = (e.deltaY > 0) ? 1 : -1;
zoom_level -= zoomleveloffset;
if (zoom_level < 0) {
zoom_level = 0;
return;
}
if (zoom_level > 19) {
zoom_level = 19;
return;
}
console.log(zoom_level);
//clearcanvas();
//重新计算basetile
basetile = calBasetile(lat, long, zoom_level);
tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
drawgrid(basetile, tiles);
}
let moveflag = false;
let downpoing = { x: 0, y: 0 };
map.onmousedown = (e) => {
console.log('鼠标按下');
moveflag = true;
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
map.onmouseup = (e) => {
console.log('鼠标松开');
moveflag = false;
}
map.onmousemove = (e) => {
if (moveflag) {
console.log('有效位移');
const movex = downpoing.x - e.offsetX;
const movey = downpoing.y - e.offsetY;
//实现移动逻辑
let xp = basetile.x + movex / 256;//注意像素移动要除以256
let yp = basetile.y + movey / 256;
long = tile2long(xp, zoom_level);
lat = tile2lat(yp, zoom_level);
basetile = calBasetile(lat, long, zoom_level);
tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
drawgrid(basetile, tiles);
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
}

@ -1,43 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tilerendertest</title>
<style>
*{
margin: 0;
padding: 0;
}
.main{
display: flex;
justify-content: center;
align-items: center;
background-color: white;
width: 100vw;
height: 100vh;
}
.container{
flex:1;
display: flex;
justify-content: center;
align-items: center;
}
.map{
/* background-color: #ddd; */
background-color: #4c2626;
width: 1600px;
height: 900px;
}
</style>
</head>
<body>
<div class="main">
<div class="container">
<canvas class="map" id="map"></canvas>
</div>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>tilerendertest</title>
<style>
* {
margin: 0;
padding: 0;
}
.main {
display: flex;
justify-content: center;
align-items: center;
background-color: #ebebeb;
width: 100vw;
height: 100vh;
}
.container {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
}
.map {
/* background-color: #ddd; */
background-color: #cdcdcd;
width: 100vw;
height: 100vh;
/* width: 512px;
height: 512px; */
}
</style>
</head>
<body>
<div class="main">
<div class="container">
<canvas class="map" id="map"></canvas>
</div>
<script src="../ellipsode.js"></script>
</body>
</div>
<script src="./index.js" type="module"></script>
</body>
</html>

@ -0,0 +1,10 @@
import * as Basemap from './src/basemap.js'
document.addEventListener('contextmenu', function (event) {
event.preventDefault() // 阻止浏览器默认右键菜单的显示
})
Basemap.setDomById('map')
Basemap.setCanvasSize()
window.addEventListener('resize', Basemap.setCanvasSize)
// Basemap.setCenterByWgs84(22.54, 114.06, 18)

@ -1,333 +0,0 @@
let map = document.getElementById('map');
let coord = document.getElementById('coord');
map.width = map.clientWidth;
map.height = map.clientHeight;
let ctx = map.getContext('2d');
const tilesize = 256;
const minlevel = 3;
const maxlevel = 18;
let zoom_level = 3; //默认
let viewport = {};
viewport.width = map.width; //默认已知
viewport.height = map.height; //默认已知
viewport.centerx = Math.pow(2, zoom_level) * 256 / 2; //默认
viewport.centery = Math.pow(2, zoom_level) * 256 / 2; //默认
viewport.xoffset = (viewport.width / 2 - viewport.centerx);
viewport.yoffset = (viewport.height / 2 - viewport.centery);
let needtiles = new Array();
let cache = new Array();
let alltilecount = 0;
let downtilecount = 0;
let drecord = new Array();
function calTileSize() {
let etsize = tilesize;
if (zoom_level != Math.floor(zoom_level)) {
etsize = tilesize * Math.pow(2, zoom_level - Math.floor(zoom_level + 1));
}
return etsize;
}
function calTileNum(coord) {
return Math.floor(coord / calTileSize());
}
function tilesfilter(tails) {
let news = new Array();
let max = Math.pow(2, zoom_level);
if (zoom_level != Math.floor(zoom_level)) {
max = Math.pow(2, Math.floor(zoom_level + 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;
}
function clearcanvas() {
ctx.clearRect(0, 0, map.width, map.height);
}
function drawRect(x, y, width, height) {
ctx.strokeStyle = "white"
ctx.strokeRect(x, y, width, height);
}
function drawtext(text, x, y) {
ctx.fillStyle = "white"
ctx.font = "18px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function mplat2viewport(mx, my) {
const vx = mx + viewport.xoffset;
const vy = my + viewport.yoffset;
return { x: vx, y: vy };
}
function viewport2mplat(vx, vy) {
const mx = vx - viewport.xoffset;
const my = vy - viewport.yoffset;
return { x: mx, y: my };
}
function viewport2wgs84(vx, vy) {
const mlc = viewport2mplat(vx, vy);
//数据超界处理, 这里对应缩放等级的地图的大小是
const limit = calTileSize() * Math.pow(2, zoom_level);
console.log(calTileSize());
if (vx <= 0 || vy <= 0 || vx >= limit || vy >= limit) {
throw "OutMapRange";
}
const lat = tile2lat(mlc.y / tilesize, zoom_level);
const lng = tile2long(mlc.x / tilesize, zoom_level);
return { lat: lat, lng: lng }
}
function wgs84toviewport(lat, lng) {
//数据超界处理 经度 -180 180
//85.0511287798066 (2*Math.atan(Math.pow(Math.E,Math.PI))-Math.PI/2)/Math.PI*180
const latlimit = 85.051128;
const lngp = lng - Math.floor((lng + 180) / 360) * 360;
if (lat < (0 - latlimit) || lat > latlimit) {
throw "OutMapRange";
}
const my = lat2y(lat, zoom_level) * tilesize;
const mx = lon2x(lngp, zoom_level) * tilesize;
const vp = mplat2viewport(mx, my);
return { x: vp.x, y: vp.y };
}
function updateviewporttest(scrollx, scrolly, oldz, newz) {
//计算出缩放后坐标系的偏移值
// xv = xm + offset ; offset = xv - xm
//const newz = zoom_level;
const scrollm = viewport2mplat(scrollx, scrolly);
//const basescale = 2 * Math.pow(2, newz) / Math.pow(2, Math.floor(newz) + 1);
console.log('zoomlevel : ', newz, oldz);
const basescale = Math.pow(2, newz - oldz);
console.log('basescale: ', basescale);
const newscrollm = { x: scrollm.x * basescale, y: scrollm.y * basescale };
console.log('缩放点新坐标 ', newscrollm);
viewport.xoffset = scrollx - newscrollm.x;
viewport.yoffset = scrolly - newscrollm.y;
}
//test
function drawtest() {
ctx.clearRect(0, 0, map.width, map.height);
const viewport_left_top = viewport2mplat(0, 0);
const viewport_right_bottom = viewport2mplat(viewport.width, viewport.height);
const mixnum = calTileNum(viewport_left_top.x);
const miynum = calTileNum(viewport_left_top.y);
const maxnum = calTileNum(viewport_right_bottom.x);
const maynum = calTileNum(viewport_right_bottom.y);
const horicount = maxnum - mixnum + 1;
const verticount = maynum - miynum + 1;
clearcanvas();
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 });
}
}
needtiles = tilesfilter(tails);
alltilecount += needtiles.length;
//console.log(needtiles.length, needtiles);
needtiles.forEach(et => {
const etsize = calTileSize();
//const tilev = mplat2viewport(et.x * etsize, et.y * etsize);
//drawRect(tilev.x, tilev.y, etsize, etsize);
let z = zoom_level;
if (zoom_level != Math.floor(zoom_level)) {
z = Math.floor(zoom_level + 1);
}
const text = `${z}/${et.x}/${et.y}.png`;
console.log(text);
//drawtext(text, tilev.x + etsize / 2, tilev.y + etsize / 2);
drawTileImg(z, et.x, et.y);
});
debug();
}
let moveflag = false;
let downpoing = { x: 0, y: 0 };
map.onmousedown = (e) => {
console.log('鼠标按下');
moveflag = true;
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
console.log(zoom_level, e.offsetX, e.offsetY);
const wgs84 = viewport2wgs84(e.offsetX, e.offsetY);
console.log(wgs84, 'check', wgs84toviewport(wgs84.lat, wgs84.lng));
}
map.onmouseup = (e) => {
console.log('鼠标松开');
moveflag = false;
}
map.onmousemove = (e) => {
//输出latlng
//const mlc = viewport2mplat(e.offsetX, e.offsetY);
//let lat = tile2lat(mlc.y / tilesize, zoom_level);
//let lng = tile2long(mlc.x / tilesize, zoom_level);
//const coordstr = `(${e.offsetX},${e.offsetY}) - (${mlc.x.toFixed(3)},${mlc.y.toFixed(3)}) - (${lat},${lng})`;
//coord.innerText = coordstr;
//console.log(e.offsetX, e.offsetY);
if (moveflag) {
console.log('有效位移');
const movex = downpoing.x - e.offsetX;
const movey = downpoing.y - e.offsetY;
//实现移动逻辑
viewport.xoffset -= movex;
viewport.yoffset -= movey;
drawtest();
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
}
map.onwheel = (e) => {
//console.log(e);
const ds = 0.25;
const zoomleveloffset = (e.deltaY > 0) ? 1 * ds : -1 * ds;
const oldz = zoom_level;
zoom_level = zoom_level - zoomleveloffset;
if (zoom_level < minlevel) {
zoom_level = minlevel;
return;
}
if (zoom_level > maxlevel) {
zoom_level = maxlevel;
return;
}
console.log(zoom_level, e.offsetX, e.offsetY);
updateviewporttest(e.offsetX, e.offsetY, oldz, zoom_level);
drawtest();
}
drawtest();
Array.prototype.remove = function (val) {
const index = this.indexOf(val);
if (index > -1) {
return this.splice(index, 1);
}
return this;
}
//add opeenstreetmap
function downloadimg(z, x, y) {
return new Promise((resolve, reject) => {
//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 };
drecord.push(obj); //下载前加入记录
let img = new Image();
img.onload = () => {
console.log('downloaded' + z + '/' + x + '/' + y + '.png');
cache.push({ img: img, z: z, x: x, y: y });
drecord.remove(obj); //下载完成后从记录中移除
downtilecount += 1;
resolve(img);
}
img.src = url;
});
}
async function getImg(z, x, y) {
//先判断是否加入下载记录
const isdownloading = drecord.includes({ z: z, x: x, y: y });
if (isdownloading) {
//return getImg(z, x, y);
return setTimeout(() => {
getImg(z, x, y);
}, 100);
}
const found = cache.find(e => (e.z == z && e.x == x && e.y == y));
if (found == undefined) {
let p = await downloadimg(z, x, y);
return p;
} else {
console.log('exist' + z + '/' + x + '/' + y + '.png');
return found.img;
}
}
function drawTileImg(z, x, y) {
const img = getImg(z, x, y);
img.then((e) => {
const etsize = calTileSize();
const tilev = mplat2viewport(x * etsize, y * etsize);
let fz = zoom_level;
if (zoom_level != Math.floor(zoom_level)) {
fz = Math.floor(zoom_level + 1);
}
if (needtiles.find(t => z == fz && t.x == x && t.y == y)) {
ctx.drawImage(e, tilev.x, tilev.y, etsize, etsize);
}
});
}
function debug() {
console.log('=============debug=================');
console.log('zoom_level', zoom_level);
console.log('alltilecount', alltilecount);
console.log('downtilecount', downtilecount);
console.log('cachecount', caches.length);
console.log('drecord count', drecord.length);
console.log('=============debug=================');
}
//=====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))));
}
//=========

@ -0,0 +1,17 @@
{
"name": "formaptile",
"version": "1.0.0",
"description": "",
"main": "index.js",
"type": "module",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "http://atsw.top:3000/alimu/formaptile.git"
},
"keywords": [],
"author": "",
"license": "ISC"
}

@ -0,0 +1,220 @@
import {
viewport,
calTileNum,
calTileSize,
setViewportSize,
setViewportCenterByWgs84,
setViewportCenterByMap,
map2viewport,
viewport2map,
updateZoomLevel,
} from './viewport.js'
import * as Tiles from './tiles.js'
let map = undefined
export function setDomById(id) {
map = document.getElementById(id)
console.debug('底图挂载在', map)
//添加响应事件
// let moveflag = false
// let downpoing = { x: 0, y: 0 }
const minlevel = 3
const maxlevel = 18
map.onwheel = (e) => {
const ds = 0.25
const zoomleveloffset = e.deltaY > 0 ? 1 * ds : -1 * ds
let newz = viewport.zoom_level
newz = newz - zoomleveloffset
if (newz < minlevel) {
newz = minlevel
return
}
if (newz > maxlevel) {
newz = maxlevel
return
}
updateZoomLevel(e.offsetX, e.offsetY, newz)
savePosInfo()
drawGrid()
}
let moveflag = false
let downpoing = { x: 0, y: 0 }
map.onmousedown = (e) => {
//console.log('鼠标按下')
moveflag = true
downpoing.x = e.offsetX
downpoing.y = e.offsetY
//console.log(viewport.zoom_level, e.offsetX, e.offsetY)
// try {
// const wgs84 = viewport2wgs84(e.offsetX, e.offsetY)
// console.log(
// 'check',
// { x: e.offsetX, y: e.offsetY },
// wgs84,
// wgs84toviewport(wgs84.lat, wgs84.lon)
// )
// } catch (error) {
// //console.log(error)
// }
}
map.onmouseup = (e) => {
//console.log('鼠标松开')
moveflag = false
}
map.onmousemove = (e) => {
//输出latlng
//const mlc = viewport2map(e.offsetX, e.offsetY)
// const wgs84 = viewport2wgs84(e.offsetX, e.offsetY)
// const coordstr = `(${e.offsetX},${e.offsetY})->(${mlc.x.toFixed(
// 3
// )},${mlc.y.toFixed(3)})->(${wgs84.lat},${wgs84.lon})`
// console.log(coordstr)
if (moveflag) {
//console.log('有效位移')
const movex = downpoing.x - e.offsetX
const movey = downpoing.y - e.offsetY
//实现移动逻辑
viewport.xoffset -= movex
viewport.yoffset -= movey
savePosInfo()
drawGrid()
downpoing.x = e.offsetX
downpoing.y = e.offsetY
}
}
}
export function getContext() {
if (map) {
return map.getContext('2d')
}
}
function savePosInfo() {
const mcenter = viewport2map(viewport.width / 2, viewport.height / 2)
let poseInfo = {
mx: mcenter.x,
my: mcenter.y,
zoom_level: viewport.zoom_level,
}
localStorage.setItem('poseInfo-nas22udfh12', JSON.stringify(poseInfo))
}
export function setCanvasSize() {
if (map) {
map.width = map.clientWidth
map.height = map.clientHeight
//console.debug('重新设置大小', map.width, map.height)
//init
setViewportSize(map.width, map.height)
//随机用一个标识
const poseInfoStr = localStorage.getItem('poseInfo-nas22udfh12')
const poseInfo = JSON.parse(poseInfoStr)
if (poseInfo) {
setViewportCenterByMap(
poseInfo.mx,
poseInfo.my,
poseInfo.zoom_level
)
} else {
setViewportCenterByWgs84(0, 0, 3)
}
//draw
drawGrid()
}
}
export function setCenterByWgs84(lat, lon, z) {
setViewportCenterByWgs84(lat, lon, z)
drawGrid()
}
function drawRect(x, y, width, height) {
const ctx = getContext()
ctx.strokeStyle = 'white'
ctx.strokeRect(x, y, width, height)
}
function clear() {
const ctx = getContext()
ctx.clearRect(0, 0, map.width, map.height)
}
function drawtext(text, x, y) {
const ctx = getContext()
ctx.fillStyle = 'white'
ctx.font = '18px serif'
ctx.textAlign = 'center'
ctx.textBaseline = 'middle'
ctx.fillText(text, x, y)
}
function drawTileImg(z, x, y) {
const img = Tiles.getImg(z, x, y)
img.then((e) => {
const etsize = calTileSize()
const tilev = map2viewport(x * etsize, y * etsize)
let fz = viewport.zoom_level
if (fz != Math.floor(fz)) {
fz = Math.floor(fz + 1)
}
if (needtiles.find((t) => z == fz && t.x == x && t.y == y)) {
const ctx = getContext()
ctx.drawImage(e, tilev.x, tilev.y, etsize, etsize)
}
})
}
function tilesfilter(tails) {
const zoom_level = viewport.zoom_level
let news = new Array()
let max = Math.pow(2, zoom_level)
if (zoom_level != Math.floor(zoom_level)) {
max = Math.pow(2, Math.floor(zoom_level + 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
}
let needtiles = new Array()
export function drawGrid() {
clear()
const viewport_left_top = viewport2map(0, 0)
const viewport_right_bottom = viewport2map(viewport.width, viewport.height)
const mixnum = calTileNum(viewport_left_top.x)
const miynum = calTileNum(viewport_left_top.y)
const maxnum = calTileNum(viewport_right_bottom.x)
const maynum = calTileNum(viewport_right_bottom.y)
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 })
}
}
needtiles = tilesfilter(tails)
needtiles.forEach((et) => {
let z = viewport.zoom_level
if (z != Math.floor(z)) {
z = Math.floor(z + 1)
}
// const etsize = calTileSize()
// const tilev = map2viewport(et.x * etsize, et.y * etsize)
//drawRect(tilev.x, tilev.y, etsize, etsize)
//const text = `${z}/${et.x}/${et.y}.png`
//console.log(text)
//drawtext(text, tilev.x + etsize / 2, tilev.y + etsize / 2)
drawTileImg(z, et.x, et.y)
})
}

@ -0,0 +1,52 @@
Array.prototype.remove = function (val) {
const index = this.indexOf(val)
if (index > -1) {
return this.splice(index, 1)
}
return this
}
const cache = new Array()
const downloadingRecord = new Array()
//const tileserverUrl =
//openstreetmap
//const url = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`;
function downloadimg(z, x, y) {
return new Promise((resolve, reject) => {
const url =
'https://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 }
downloadingRecord.push(obj) //下载前加入记录
let img = new Image()
img.onload = () => {
cache.push({ img: img, z: z, x: x, y: y })
downloadingRecord.remove(obj) //下载完成后从记录中移除
resolve(img)
}
img.referrerPolicy = 'no-referrer'
img.src = url
})
}
export async function getImg(z, x, y) {
//先判断是否加入下载记录
const isdownloading = downloadingRecord.includes({ z: z, x: x, y: y })
if (isdownloading) {
//return getImg(z, x, y);
return setTimeout(() => {
getImg(z, x, y)
}, 100)
}
const found = cache.find((e) => e.z == z && e.x == x && e.y == y)
if (found == undefined) {
let p = await downloadimg(z, x, y)
return p
} else {
return found.img
}
}

@ -0,0 +1,45 @@
//=====coord about=====
export function lon2tile(lon, z) {
return Math.floor(((lon + 180) / 360) * Math.pow(2, z))
}
export 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)
)
}
export function lon2x(lon, z) {
return ((lon + 180) / 360) * Math.pow(2, z)
}
export 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)
)
}
export function tile2long(x, z) {
return (x / Math.pow(2, z)) * 360 - 180
}
export 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)))
}
//=========

@ -0,0 +1,109 @@
import * as Utils from './utils.js'
export const viewport = {
width: 256,
height: 256,
zoom_level: 0,
xoffset: 0,
yoffset: 0,
}
//计算所有情况下的底图大小包括非整数z
export function calTileSize() {
let etsize = 256
const zoom_level = viewport.zoom_level
if (zoom_level != Math.floor(zoom_level)) {
etsize = 256 * Math.pow(2, zoom_level - Math.floor(zoom_level + 1))
}
return etsize
}
export function calTileNum(coord) {
return Math.floor(coord / calTileSize())
}
export function setViewportSize(width, height) {
viewport.width = width
viewport.height = height
}
export function updateOffset() {
const z = viewport.zoom_level
}
export function map2viewport(mx, my) {
const vx = mx + viewport.xoffset
const vy = my + viewport.yoffset
return { x: vx, y: vy }
}
export function viewport2map(vx, vy) {
const mx = vx - viewport.xoffset
const my = vy - viewport.yoffset
return { x: mx, y: my }
}
export function setViewportCenterByWgs84(lat, lon, z) {
viewport.zoom_level = z
const size = calTileSize()
const mx = Utils.lon2x(lon, z) * size
const my = Utils.lat2y(lat, z) * size
console.log('测试', mx, my)
// const { vx, vy } = map2viewport(mx, my)
const cx = viewport.width / 2
const cy = viewport.height / 2
viewport.xoffset = cx - mx
viewport.yoffset = cy - my
}
export function setViewportCenterByMap(mx, my, z) {
viewport.zoom_level = z
const cx = viewport.width / 2
const cy = viewport.height / 2
viewport.xoffset = cx - mx
viewport.yoffset = cy - my
}
export function updateZoomLevel(vx, vy, newz) {
const oldz = viewport.zoom_level
const mxy = viewport2map(vx, vy)
viewport.zoom_level = newz
const scale = Math.pow(2, newz - oldz)
const newmxy = { x: mxy.x * scale, y: mxy.y * scale }
viewport.xoffset = vx - newmxy.x
viewport.yoffset = vy - newmxy.y
}
// //test
// setViewportSize(512, 512)
// setViewportCenter(0, 0, 0)
// console.log(map2viewport(0, 0))
// console.log(map2viewport(128, 128))
export function viewport2wgs84(vx, vy) {
const mlc = viewport2map(vx, vy)
const z = viewport.zoom_level
//数据超界处理, 这里对应缩放等级的地图的大小是
const limit = calTileSize() * Math.pow(2, z)
if (vx <= 0 || vy <= 0 || vx >= limit || vy >= limit) {
throw `OutMapRange limit ${limit}`
}
const lat = Utils.tile2lat(mlc.y / 256, z)
const lon = Utils.tile2long(mlc.x / 256, z)
return { lat: lat, lon: lon }
}
export function wgs84toviewport(lat, lon) {
//数据超界处理 经度自动限制在 -180 180
//85.0511287798066 (2*Math.atan(Math.pow(Math.E,Math.PI))-Math.PI/2)/Math.PI*180
const latlimit = 85.051128
const lonp = lon - Math.floor((lon + 180) / 360) * 360
if (lat < 0 - latlimit || lat > latlimit) {
throw 'OutMapRange'
}
const z = viewport.zoom_level
const my = Utils.lat2y(lat, z) * 256
const mx = Utils.lon2x(lonp, z) * 256
const vp = map2viewport(mx, my)
return { x: vp.x, y: vp.y }
}

@ -1,316 +0,0 @@
//椭球相关参数计算
const a = 6378137; //长半轴 a
const f = 1 / 298.257223563; //扁率 (a-b)/a
const b = a * (1 - f); //短半轴 b
const e = Math.sqrt(a * a - b * b) / a; //椭球第一偏心率
function degToRad(deg) {
return deg / 180 * Math.PI;
}
function radToDeg(rad) {
return rad / Math.PI * 180;
}
function wgs84ToWebMercator(lat, lon) {
let x = a * lon;
let y = a * Math.log(Math.tan(Math.PI / 4 + lat / 2));
return { x: x, y: y };
}
function webMercatorToWgs84(x, y) {
let lon = x / a;
let lat = 2 * Math.atan(Math.pow(Math.E, y / a)) - Math.PI / 2;
return { lon: lon, lat: lat }
}
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))));
}
//https://tile.openstreetmap.org/z/x/y.png
//https://tile.openstreetmap.org/15/26794/14256.png
let cache = new Array();
function downloadimg(z, x, y) {
const src = 'https://tile.openstreetmap.org/' + z + '/' + x + '/' + y + '.png';
let img = new Image();
img.src = src;
img.onload = () => {
console.log('downloaded' + z + '/' + x + '/' + y + '.png');
cache.push({ img: img, z: z, x: x, y: y });
}
}
function getIndex(z, x, y) {
for (let i = 0; i < cache.length; i++) {
const e = cache[i];
if (e.z == z && e.x == x && e.y == y) {
return i;
} else {
continue;
}
}
//last
downloadimg(z, x, y);
return cache.length;
}
//已知条件 lat long zoom_level 显示范围的宽高
let zoom_level = 15;
let lat = 22.7490073;
let long = 114.3781213;
let width = 1600;
let height = 900;
function calBasetile(lat, long, zoom_level) {
let basetile = {};
basetile.x = lon2x(long, zoom_level); //对应zoomlevel下long的x坐标
basetile.y = lat2y(lat, zoom_level); //对应zoomlevel下lat的y坐标
basetile.tilex = lon2tile(long, zoom_level); //对应zoomlevel下long的x tile编号
basetile.tiley = lat2tile(lat, zoom_level); //对应zoomlevel下lat的y tile编号
basetile.offsetx = Math.floor((basetile.x - basetile.tilex) * 256); //相对于左上角其点的x偏移值
basetile.offsety = Math.floor((basetile.y - basetile.tiley) * 256); //相对于左上角其点的y偏移值
basetile.minx = width / 2 - basetile.offsetx;
basetile.maxx = basetile.minx + 256;
basetile.miny = height / 2 - basetile.offsety;
basetile.maxy = basetile.miny + 256;
basetile.p1 = Math.floor(basetile.maxy / 256 + 0.5);
basetile.p2 = Math.floor((height - basetile.miny) / 256 + 0.5);
basetile.p3 = Math.floor(basetile.maxx / 256 + 0.5);
basetile.p4 = Math.floor((width - basetile.minx) / 256 + 0.5);
console.log(basetile);
return basetile;
}
//基于已知条件计算需要的各个tile
function needtiles(basetile, p1, p2, p3, p4) {
let tiles = new Array();
let tilex = basetile.tilex;
let tiley = basetile.tiley;
tiles.push({ x: tilex, y: tiley });
for (let i = 0; i < p4; i++) {
tiles.push({ x: tilex + i + 1, y: tiley });
}
for (let i = 0; i < p3; i++) {
tiles.push({ x: tilex - i - 1, y: tiley });
}
for (let i = 0; i < p1; i++) {
for (let j = 0; j < p3 + p4 + 1; j++) {
tiles.push({ x: tilex - p3 + j, y: tiley - i - 1 });
}
}
for (let i = 0; i < p2; i++) {
for (let j = 0; j < p3 + p4 + 1; j++) {
tiles.push({ x: tilex - p3 + j, y: tiley + i + 1 });
}
}
//console.log(tiles);
//console.log(tiles.length);
return tilesfilter(tiles, zoom_level);
}
//过滤超过限制的tiles
function tilesfilter(tiles, zoom_level) {
const maxrange = Math.pow(2, zoom_level) - 1;
let newtiles = new Array();
for (let i = 0; i < tiles.length; i++) {
if (tiles[i].x < 0 || tiles[i].x > maxrange) {
continue;
}
if (tiles[i].y < 0 || tiles[i].y > maxrange) {
continue;
}
newtiles.push(tiles[i]);
}
return newtiles;
}
let basetile = calBasetile(lat, long, zoom_level);
let tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
//渲染问题
//通过与基准之间的位置关系计算渲染的坐标
//minx miny 即为基准tile当前渲染位置
// for (let i = 0; i < tiles.length; i++) {
// const element = tiles[i];
// let renderx = (element.x - tilex) * 256 + minx;
// let rendery = (element.y - tiley) * 256 + miny;
// console.log({ x: renderx, y: rendery });
// }
let canvas = document.getElementById('map');
canvas.width = map.clientWidth;
canvas.height = map.clientHeight;
let ctx = canvas.getContext('2d');
function clearcanvas() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
function drawPoint(x, y) {
ctx.fillStyle = "white"
ctx.strokeStyle = "#a35312"
ctx.beginPath();
ctx.arc(x, y, 3, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function drawRect(x, y) {
ctx.strokeStyle = "white"
ctx.strokeRect(x, y, 256, 256);
}
function drawImg(z, tilex, tiley, x, y, ) {
const index = getIndex(z, tilex, tiley);
if (cache[index]) {
ctx.drawImage(cache[index].img, x, y);
}
}
function drawtext(text, x, y) {
ctx.fillStyle = "white"
ctx.font = "18px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function drawgrid(basetile, tiles) {
let minx = basetile.minx;
let miny = basetile.miny;
let offsetx = basetile.offsetx;
let offsety = basetile.offsety;
let tilex = basetile.tilex;
let tiley = basetile.tiley;
clearcanvas();
drawPoint(minx + offsetx, miny + offsety);
for (let i = 0; i < tiles.length; i++) {
const element = tiles[i];
let renderx = (element.x - tilex) * 256 + minx;
let rendery = (element.y - tiley) * 256 + miny;
//console.log({ x: renderx, y: rendery });
drawPoint(renderx, rendery);
drawRect(renderx, rendery);
drawImg(zoom_level, tiles[i].x, tiles[i].y, renderx, rendery);
const showtext = 'x:' + element.x + ' ' + 'y:' + element.y;
drawtext(zoom_level, renderx + 256 / 2, rendery + 256 / 2);
drawtext(showtext, renderx + showtext.length / 4 * 18 + 4, rendery + 9);
}
}
drawgrid(basetile, tiles);
// drawPoint(width / 2, height / 2);
// const element = tiles[0];
// let renderx = (element.x - tilex) * 256 + minx;
// let rendery = (element.y - tiley) * 256 + miny;
// drawPoint(renderx, rendery);
// drawPoint(renderx + 256 / 2, rendery + 256 / 2);
// drawRect(renderx, rendery);
// drawtext(showtext, renderx + 256 / 2, rendery + 256 / 2);
// console.log({ x: renderx, y: rendery });
//实现移动缩放方法
//缩放
map.onwheel = (e) => {
//console.log(e);
const zoomleveloffset = (e.deltaY > 0) ? 1 : -1;
zoom_level -= zoomleveloffset;
if (zoom_level < 0) {
zoom_level = 0;
return;
}
if (zoom_level > 19) {
zoom_level = 19;
return;
}
console.log(zoom_level);
//clearcanvas();
//重新计算basetile
basetile = calBasetile(lat, long, zoom_level);
tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
drawgrid(basetile, tiles);
}
let moveflag = false;
let downpoing = { x: 0, y: 0 };
map.onmousedown = (e) => {
console.log('鼠标按下');
moveflag = true;
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
map.onmouseup = (e) => {
console.log('鼠标松开');
moveflag = false;
}
map.onmousemove = (e) => {
if (moveflag) {
//console.log('有效位移');
const movex = downpoing.x - e.offsetX;
const movey = downpoing.y - e.offsetY;
//实现移动逻辑
let xp = basetile.x + movex / 256; //注意像素移动要除以256
let yp = basetile.y + movey / 256;
long = tile2long(xp, zoom_level);
let latp = tile2lat(yp, zoom_level);
//需要在y方向限制移动范围
//(2*Math.atan(Math.pow(Math.E,Math.PI))-Math.PI/2)/Math.PI*180
//85.0511287798066
if (Math.abs(latp) > 89.99999) return;
// if (lat2y(latp, zoom_level) == Infinity || lat2y(latp, zoom_level) == NaN) {
// console.log('outrange');
// return;
// }
lat = latp;
console.log(long + ' ' + lat);
basetile = calBasetile(lat, long, zoom_level);
tiles = needtiles(basetile, basetile.p1, basetile.p2, basetile.p3, basetile.p4);
drawgrid(basetile, tiles);
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
}
//

@ -1,251 +0,0 @@
let map = document.getElementById('map');
map.width = map.clientWidth;
map.height = map.clientHeight;
let ctx = map.getContext('2d');
const tilesize = 256;
const minlevel = 0;
const maxlevel = 19;
let zoom_level = 0; //默认
let viewport = {};
viewport.width = map.width; //默认已知
viewport.height = map.height; //默认已知
viewport.centerx = Math.pow(2, zoom_level) * 256 / 2; //默认
viewport.centery = Math.pow(2, zoom_level) * 256 / 2; //默认
viewport.xoffset = (viewport.width / 2 - viewport.centerx);
viewport.yoffset = (viewport.height / 2 - viewport.centery);
let needtiles = new Array();
let cache = new Array();
let xhrmanager = new Array();
function calTileSize() {
let etsize = tilesize;
if (zoom_level != Math.floor(zoom_level)) {
etsize = tilesize * Math.pow(2, zoom_level - Math.floor(zoom_level + 1));
}
return etsize;
}
function calTileNum(coord) {
return Math.floor(coord / calTileSize());
}
function tilesfilter(tails) {
let news = new Array();
let max = Math.pow(2, zoom_level);
if (zoom_level != Math.floor(zoom_level)) {
max = Math.pow(2, Math.floor(zoom_level + 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;
}
function clearcanvas() {
ctx.clearRect(0, 0, map.width, map.height);
}
function drawRect(x, y, width, height) {
ctx.strokeStyle = "white"
ctx.strokeRect(x, y, width, height);
}
function drawtext(text, x, y) {
ctx.fillStyle = "white"
ctx.font = "18px serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, x, y);
}
function mplat2viewport(mx, my) {
const vx = mx + viewport.xoffset;
const vy = my + viewport.yoffset;
return { x: vx, y: vy };
}
function viewport2mplat(vx, vy) {
const mx = vx - viewport.xoffset;
const my = vy - viewport.yoffset;
return { x: mx, y: my };
}
function updateviewporttest(scrollx, scrolly, oldz, newz) {
//计算出缩放后坐标系的偏移值
// xv = xm + offset ; offset = xv - xm
//const newz = zoom_level;
const scrollm = viewport2mplat(scrollx, scrolly);
//const basescale = 2 * Math.pow(2, newz) / Math.pow(2, Math.floor(newz) + 1);
console.log('zoomlevel : ', newz, oldz);
const basescale = Math.pow(2, newz - oldz);
console.log('basescale: ', basescale);
const newscrollm = { x: scrollm.x * basescale, y: scrollm.y * basescale };
console.log('缩放点新坐标 ', newscrollm);
viewport.xoffset = scrollx - newscrollm.x;
viewport.yoffset = scrolly - newscrollm.y;
}
//test
function drawtest() {
ctx.clearRect(0, 0, map.width, map.height);
const viewport_left_top = viewport2mplat(0, 0);
const viewport_right_bottom = viewport2mplat(viewport.width, viewport.height);
const mixnum = calTileNum(viewport_left_top.x);
const miynum = calTileNum(viewport_left_top.y);
const maxnum = calTileNum(viewport_right_bottom.x);
const maynum = calTileNum(viewport_right_bottom.y);
const horicount = maxnum - mixnum + 1;
const verticount = maynum - miynum + 1;
clearcanvas();
xhrabort();
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 });
}
}
needtiles = tilesfilter(tails);
//console.log(needtiles.length, needtiles);
needtiles.forEach(et => {
const etsize = calTileSize();
const tilev = mplat2viewport(et.x * etsize, et.y * etsize);
//drawRect(tilev.x, tilev.y, etsize, etsize);
let z = zoom_level;
if (zoom_level != Math.floor(zoom_level)) {
z = Math.floor(zoom_level + 1);
}
const text = `${z}/${et.x}/${et.y}.png`;
console.log(text);
drawtext(text, tilev.x + etsize / 2, tilev.y + etsize / 2);
drawTileImg(z, et.x, et.y);
});
}
let moveflag = false;
let downpoing = { x: 0, y: 0 };
map.onmousedown = (e) => {
console.log('鼠标按下');
moveflag = true;
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
//console.log(viewport2mplat(e.offsetX, e.offsetY));
}
map.onmouseup = (e) => {
console.log('鼠标松开');
moveflag = false;
}
map.onmousemove = (e) => {
if (moveflag) {
console.log('有效位移');
const movex = downpoing.x - e.offsetX;
const movey = downpoing.y - e.offsetY;
//实现移动逻辑
viewport.xoffset -= movex;
viewport.yoffset -= movey;
drawtest();
downpoing.x = e.offsetX;
downpoing.y = e.offsetY;
}
}
map.onwheel = (e) => {
//console.log(e);
const ds = 0.25;
const zoomleveloffset = (e.deltaY > 0) ? 1 * ds : -1 * ds;
const oldz = zoom_level;
zoom_level = zoom_level - zoomleveloffset;
if (zoom_level < minlevel) {
zoom_level = minlevel;
return;
}
if (zoom_level > maxlevel) {
zoom_level = maxlevel;
return;
}
console.log(zoom_level, e.offsetX, e.offsetY);
updateviewporttest(e.offsetX, e.offsetY, oldz, zoom_level);
drawtest();
}
drawtest();
//add opeenstreetmap
function downloadimg(z, x, y) {
return new Promise((resolve, reject) => {
const url = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`;
let img = new Image();
img.onload = () => {
console.log('downloaded' + z + '/' + x + '/' + y + '.png');
cache.push({ img: img, z: z, x: x, y: y });
resolve(img);
}
img.src = url;
});
}
//尝试优化图片下载机制
function xhrdownloadimg(z, x, y) {
return new Promise((resolve, reject) => {
const url = `https://tile.openstreetmap.org/${z}/${x}/${y}.png`;
let imgxhr = new XMLHttpRequest();
imgxhr.open('GET', url, true);
//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);
cache.push({ img: img, z: z, x: x, y: y });
resolve(img);
}
};
xhrmanager.push(imgxhr);
imgxhr.send(null);
});
}
function xhrabort() {
xhrmanager.forEach(element => {
element.abort();
});
xhrmanager = new Array();
}
async function getImg(z, x, y) {
const found = cache.find(e => (e.z == z && e.x == x && e.y == y));
if (found == undefined) {
let p = await xhrdownloadimg(z, x, y);
return p;
} else {
console.log('exist' + z + '/' + x + '/' + y + '.png');
return found.img;
}
}
function drawTileImg(z, x, y) {
const img = getImg(z, x, y);
img.then((e) => {
const etsize = calTileSize();
const tilev = mplat2viewport(x * etsize, y * etsize);
let fz = zoom_level;
if (zoom_level != Math.floor(zoom_level)) {
fz = Math.floor(zoom_level + 1);
}
if (needtiles.find(t => z == fz && t.x == x && t.y == y)) {
ctx.drawImage(e, tilev.x, tilev.y, etsize, etsize);
}
});
}

Binary file not shown.

@ -1,437 +0,0 @@
//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=================');
}
Loading…
Cancel
Save