原生2d web地图引擎
下面这段核心代码,不依赖任何2d地图引擎如leaflet,open layers,基于原生的JavaScript实现了地图瓦片的加载渲染,以及地图平移和缩放等功能
<template>
<div id="mapWrapper">
<div id="mapPanel">
<canvas
@mousedown="startDrag"
@mousemove="dragging"
@mouseup="endDrag"
@wheel="zoomMap"
></canvas>
</div>
</div>
</template>
<script setup>
import { onMounted } from "vue";
// 地球参考椭球半径
const EARTHRADIUS = 6378137;
// 瓦片大小
const TILESIZE = 256;
// 是否是第一次加载
let initFlag = false;
// 初始化层级
let zoom = 16;
// 初始化比例尺
let mapSize = 0;
let scale = 0;
// 计算比例尺
// 根据地图层级计算整个地图瓦片阵列的行列数以及地图比例尺
const getMapScale = () => {
mapSize = TILESIZE * Math.pow(2, zoom);
scale = (2 * Math.PI * EARTHRADIUS) / mapSize;
};
getMapScale();
// 地图中心点坐标
let mapCenter = {
lon: 0,
lat: 0,
};
let pixelBound = {
minPixelX: 0,
maxPixelX: 0,
minPixelY: 0,
maxPixelY: 0,
};
let canvas;
// 基于中心点坐标计算地图的四至像素范围
const getPixelBound = () => {
const [x, y] = lonlatToxy(mapCenter.lon, mapCenter.lat);
const [pixelX, pixelY] = xyTopixel(x, y);
pixelBound.minPixelX = pixelX - canvas.width / 2;
pixelBound.maxPixelX = pixelX + canvas.width / 2;
pixelBound.minPixelY = pixelY - canvas.height / 2;
pixelBound.maxPixelY = pixelY + canvas.height / 2;
};
// 经纬度坐标转web墨卡托坐标
const lonlatToxy = (lon, lat) => {
const x = (lon / 180) * Math.PI * EARTHRADIUS;
const y =
Math.log(Math.tan(Math.PI / 4 + ((lat / 180) * Math.PI) / 2)) *
EARTHRADIUS;
return [x, y];
};
// web墨卡托坐标转经纬度坐标
const xyTolonlat = (x, y) => {
const lon = x / EARTHRADIUS / Math.PI * 180;
const lat = (2 * Math.atan(Math.exp(y / EARTHRADIUS)) - Math.PI / 2) / Math.PI * 180;
return [lon, lat];
};
// web墨卡托坐标转像素坐标
const xyTopixel = (x, y) => {
//计算比例尺
const pixelX = x / scale + mapSize / 2;
const pixelY = mapSize / 2 - y / scale;
return [pixelX, pixelY];
};
// 像素坐标转web墨卡托坐标
const pixelToxy = (pixelX, pixelY) => {
const x = (pixelX - mapSize / 2) * scale;
const y = (mapSize / 2 - pixelY) * scale;
return [x, y];
};
//基于像素坐标计算瓦片编号
const pixelToTileNo = (pixelX, pixelY) => {
return [
Number.parseInt(pixelY / TILESIZE),
Number.parseInt(pixelX / TILESIZE),
];
};
//影像地图瓦片
const drawImage = (ctx, row, col, zoom) => {
const url = `https://khms3.google.com/kh/v=947?x=${col}&y=${row}&z=${zoom}`;
const tileImg = new Image();
tileImg.setAttribute("crossOrigin", "anonymous");
tileImg.onload = () => {
const leftTopX = col * TILESIZE;
const leftTopY = row * TILESIZE;
const sx = leftTopX - pixelBound.minPixelX;
const sy = leftTopY - pixelBound.minPixelY;
ctx.drawImage(tileImg, sx, sy);
};
tileImg.src = url;
};
const drawMap = () => {
//计算行列号的范围
const [minRow, minCol] = pixelToTileNo(
pixelBound.minPixelX,
pixelBound.minPixelY
);
const [maxRow, maxCol] = pixelToTileNo(
pixelBound.maxPixelX,
pixelBound.maxPixelY
);
//绘制视口范围内的地图瓦片
const ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let i = minRow; i <= maxRow; i++) {
for (let j = minCol; j <= maxCol; j++) {
drawImage(ctx, i, j, Number.parseInt(zoom));
}
}
};
onMounted(() => {
canvas = document.getElementsByTagName("canvas")[0];
canvas.style.letf = "0px";
canvas.style.top = "0px";
canvas.height = canvas.clientHeight;
canvas.width = canvas.clientWidth;
const getPosition = (position) => {
const lat = position.coords.latitude;
const lon = position.coords.longitude;
mapCenter.lon = 116.411455;
mapCenter.lat = 39.918255;
getPixelBound();
drawMap();
initFlag = true;
};
navigator.geolocation.getCurrentPosition(getPosition, (error) => {
console.log(error.code);
});
});
//鼠标拖动地图事件
let startX, startY;
const startDrag = (e) => {
if (!initFlag || e.button != 0) {
return;
}
startX = e.x;
startY = e.y;
};
const dragging = (e) => {
if (!initFlag || e.buttons != 1) {
return;
}
const currentX = e.x;
const currentY = e.y;
const dx = currentX - startX;
const dy = currentY - startY;
canvas.style.left = `${dx}px`;
canvas.style.top = `${dy}px`;
};
const endDrag = (e) => {
if (!initFlag || e.button != 0) {
return;
}
canvas.style.left = "0px";
canvas.style.top = "0px";
let endX = e.x;
let endY = e.y;
const dx = endX - startX;
const dy = endY - startY;
// 更新四至像素坐标
pixelBound.minPixelX -= dx;
pixelBound.minPixelY -= dy;
pixelBound.maxPixelX -= dx;
pixelBound.maxPixelY -= dy;
// 更新地图中心点坐标
const mapCenterXY = pixelToxy(
pixelBound.minPixelX + canvas.width / 2,
pixelBound.minPixelY + canvas.height / 2
);
const mapCenterLonLat = xyTolonlat(mapCenterXY[0], mapCenterXY[1]);
mapCenter.lon = mapCenterLonLat[0];
mapCenter.lat = mapCenterLonLat[1];
drawMap();
};
const zoomMap = (e) => {
// 向下滚动鼠标缩小,拉远镜头
// 向上滚动鼠标放大,拉近镜头
let zoomFlag = e.deltaY > 0 ? "out" : "in";
if (zoomFlag == "out") {
if (zoom <= 0) {
return;
}
zoom--;
} else {
if (zoom >= 19) {
return;
}
zoom++;
}
// 缩放时保持鼠标所在位置不变
const canvasX = e.offsetX;
const canvasY = e.offsetY;
const mousePixelX = pixelBound.minPixelX + canvasX;
const mousePixelY = pixelBound.minPixelY + canvasY;
const mousexy = pixelToxy(mousePixelX, mousePixelY);
// 更新比例尺
getMapScale();
// 更新中心点坐标
const dx = canvasX * scale;
const dy = canvasY * scale;
const leftTopX = mousexy[0] - dx;
const leftTopY = mousexy[1] + dy;
const mapCenterX = leftTopX + canvas.width / 2 * scale;
const mapCenterY = leftTopY - canvas.height / 2 * scale;
const _mapCenter = xyTolonlat(mapCenterX, mapCenterY);
mapCenter.lon = _mapCenter[0];
mapCenter.lat = _mapCenter[1];
//计算像素坐标的四至
getPixelBound();
drawMap();
};
</script>
<style scoped>
#mapWrapper {
width: 100%;
height: 100%;
position: relative;
}
#mapPanel {
width: 1200px;
height: 800px;
position: absolute;
left: calc(50% - 600px);
top: calc(50% - 400px);
overflow: hidden;
background-color: #bbffaa;
}
canvas {
width: 100%;
height: 100%;
position: absolute;
}
</style>
效果展示
Video