mapjar
v0.4.2
Published
基于 WebGL2 的高性能地图渲染引擎,支持 EPSG:3857 投影。
Readme
Mapjar - WebGL2 地图引擎
基于 WebGL2 的高性能地图渲染引擎,支持 EPSG:3857 投影。
✨ 功能特性
核心渲染
- ✅ WebGL2 渲染:高性能 GPU 加速渲染
- ✅ EPSG:3857 投影:Web Mercator 标准投影
- ✅ 高 DPR 支持:完美适配 Retina 和高分辨率屏幕,自动 DPI 缩放
- ✅ 跨世界渲染:水平无限循环,无缝拼接
相机系统
- ✅ 平移、缩放、旋转:流畅的交互体验
- ✅ 平滑动画:flyTo、fitBounds 带缓动效果
- ✅ 纬度限制:自动锁定在 ±85.05° 范围内
- ✅ 右键旋转:支持地图旋转(可选)
图层系统
- ✅ 瓦片图层 (TileLayer):支持任意瓦片源,Web Worker 并发加载,LRU 缓存,智能取消机制
- ✅ 矢量图层 (VectorLayer):点、线、面要素渲染,圆形点,Earcut 多边形三角化
- ✅ GeoJSON 图层 (GeoJSONLayer):完整支持 GeoJSON 格式数据加载和渲染
- ✅ 图像图层 (ImageLayer):渲染单张带地理坐标的图像,支持历史地图叠加、卫星影像等
- ✅ 风场图层 (WindLayer):基于 WebGL2 的高性能风场可视化,粒子系统实时动画
- ✅ 热力图层 (HeatmapLayer):温度、降水等连续数值场可视化,支持自定义颜色映射
- ✅ 覆盖层图层 (OverlayLayer):在地图上叠加 HTML 元素,支持自定义样式和交互
- ✅ Canvas 图层 (CanvasLayer):纯 Canvas 2D 渲染,适合自定义绘制
交互与事件
- ✅ 鼠标/触摸交互:拖拽、滚轮、双击放大(带平滑动画)
- ✅ 点击事件:监听地图点击,获取经纬度坐标
- ✅ 鼠标移动事件:实时获取鼠标位置的经纬度(可选启用)
- ✅ 事件系统:统一的事件发射器,类型安全的事件订阅/发布
高级功能
- ✅ 文字渲染:为矢量要素添加文字标注,支持自定义字体、颜色、描边、偏移等
- ✅ 数据驱动样式:根据要素属性动态设置样式
- ✅ 空间查询:点查询、范围查询、最近邻查询
- ✅ 视锥剔除:自动剔除视口外的要素,提升性能
- ✅ 批量渲染:减少 GPU 状态切换,提升渲染效率
- ✅ 资源管理:自动管理 WebGL 资源,防止内存泄漏
🚀 快速开始
安装依赖
bun add mapjar
# 或
npm install mapjar📖 使用指南
基础示例
import { MapEngine, TileLayer, VectorLayer } from 'mapjar';
// 创建地图引擎(自动初始化并开始渲染)
const engine = new MapEngine('#map', {
center: [116.4074, 39.9042], // 北京 [经度, 纬度]
zoom: 10,
rotation: 0,
enableRotation: true // 启用右键旋转(默认 true)
});
// 添加瓦片图层
const tileLayer = new TileLayer(
'osm',
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
tileScale: 1.0, // 瓦片缩放比例(0.5 - 3.0)
wrapX: true, // 启用水平跨世界渲染
fadeInDuration: 200 // 瓦片淡入动画时长(毫秒)
}
);
engine.addLayer(tileLayer);
// 添加矢量图层
const vectorLayer = new VectorLayer('vector', {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0
});
engine.addLayer(vectorLayer);
// 添加点要素
vectorLayer.addFeature({
type: 'point',
coordinates: [116.4074, 39.9042],
properties: { name: '北京' }
});
// 监听点击事件
engine.on('click', (event) => {
console.log('点击位置:', event.lon, event.lat);
});
// 使用 flyTo 飞到上海
setTimeout(() => {
engine.flyTo(121.4737, 31.2304, 10, { duration: 2000 });
}, 2000);
// 可选:手动控制渲染循环
// engine.stop(); // 暂停渲染
// engine.start(); // 恢复渲染核心概念
1. 投影系统
使用 EPSG:3857 (Web Mercator) 投影:
import { WebMercatorProjection } from 'mapjar';
// 经纬度转 Web Mercator 坐标(米)
const pos = WebMercatorProjection.lonLatToMeters(116.4074, 39.9042);
// Web Mercator 坐标转经纬度
const lonLat = WebMercatorProjection.metersToLonLat(pos.x, pos.y);
// 获取瓦片坐标
const tile = WebMercatorProjection.getTileCoord(116.4074, 39.9042, 10);2. 相机控制
const camera = engine.getCamera();
// 设置中心点(经纬度)
camera.setCenterLonLat(116.4074, 39.9042);
// 设置缩放级别(0-22)
camera.setZoom(10);
// 平移(像素)
camera.pan(100, 100);
// 缩放到指定点
camera.zoomTo(1, screenPos);
// 飞行到指定位置(带平滑动画)
engine.flyTo(116.4074, 39.9042, 10, { duration: 1500 });
// 适配到指定边界(带平滑动画)
engine.fitBounds(
{
minLon: 73.5,
minLat: 18.2,
maxLon: 135.0,
maxLat: 53.5
},
{ duration: 2000, padding: 50 }
);3. 瓦片图层 (TileLayer)
const tileLayer = new TileLayer(
'osm',
'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
{
tileScale: 1.0, // 瓦片缩放比例(0.5 - 3.0,默认 1.0)
wrapX: true, // 启用水平跨世界渲染(默认 true)
fadeInDuration: 200 // 瓦片淡入动画时长(毫秒,默认 200)
}
);
engine.addLayer(tileLayer);
// 动态调整瓦片缩放比例
tileLayer.setTileScale(1.5); // 放大 50%,让文字更易读
// 特性:
// - Web Worker 并发加载(自动启用)
// - LRU 缓存(最多 500 个瓦片)
// - 智能取消机制(视口外的瓦片自动取消加载)
// - 淡入动画(平滑加载效果)
// - 跨世界渲染(水平无限循环)4. 图像图层 (ImageLayer)
import { ImageLayer } from 'mapjar';
// 创建图像图层(用于历史地图叠加、卫星影像等)
const imageLayer = new ImageLayer('custom-overlay', {
url: 'https://example.com/historical-map.png',
bounds: {
minLon: 116.2, // 左下角经度
minLat: 39.8, // 左下角纬度
maxLon: 116.6, // 右上角经度
maxLat: 40.1, // 右上角纬度
},
useMipmap: true, // 启用 Mipmap(默认 true)
});
imageLayer.setOpacity(0.7); // 半透明叠加
imageLayer.setZIndex(10); // 在底图之上
engine.addLayer(imageLayer);
// 异步加载图像
await imageLayer.loadFromURL();
// 或直接提供图像
const imageLayer2 = new ImageLayer('overlay2', {
image: imageBitmap, // HTMLImageElement 或 ImageBitmap
bounds: { minLon: 116.2, minLat: 39.8, maxLon: 116.6, maxLat: 40.1 }
});
// 特性:
// - 支持 URL 或直接提供图像
// - Mipmap 优化(多级纹理)
// - 各向异性过滤(提升清晰度)
// - 自动地理配准5. 矢量图层 (VectorLayer)
const vectorLayer = new VectorLayer('vector', {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0,
// 文字样式
textField: 'name', // 显示 properties.name
textFont: '14px Arial',
textColor: [0, 0, 0, 1],
textHaloColor: [1, 1, 1, 1],
textHaloWidth: 2,
textOffset: [0, -15],
textAnchor: 'center'
});
// 添加点(渲染为圆形,带抗锯齿)
vectorLayer.addFeature({
type: 'point',
coordinates: [116.4074, 39.9042],
properties: { name: '北京' }
});
// 添加线
vectorLayer.addFeature({
type: 'line',
coordinates: [[116.4074, 39.9042], [121.4737, 31.2304]],
properties: { name: '路线' }
});
// 添加面(使用 Earcut 三角化,支持凹多边形和带洞多边形)
vectorLayer.addFeature({
type: 'polygon',
coordinates: [[[116, 39], [117, 39], [117, 40], [116, 40], [116, 39]]],
properties: { name: '区域' }
});
// 添加带洞的多边形
vectorLayer.addFeature({
type: 'polygon',
coordinates: [
[[116, 39], [117, 39], [117, 40], [116, 40], [116, 39]], // 外环
[[116.3, 39.3], [116.7, 39.3], [116.7, 39.7], [116.3, 39.7], [116.3, 39.3]] // 洞
],
properties: { name: '带洞区域' }
});
// 空间查询
const nearbyFeatures = vectorLayer.queryNearby([116.4, 39.9], 10000); // 10km 范围内
const featuresInBounds = vectorLayer.queryBBox({ minX: 116, minY: 39, maxX: 117, maxY: 40 });
// 特性:
// - 圆形点渲染(片段着色器实现,带抗锯齿)
// - Earcut 多边形三角化(支持凹多边形和带洞多边形)
// - 文字标注(支持自定义字体、颜色、描边)
// - 空间查询(点查询、范围查询、最近邻查询)
// - 视锥剔除(自动剔除视口外的要素)
// - 批量渲染(减少 GPU 状态切换)6. GeoJSON 图层 (GeoJSONLayer)
import { MapEngine, GeoJSONLayer, StyleFunction } from 'mapjar';
// 创建地图引擎
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
// 从 URL 加载 GeoJSON
const geoJSONLayer = new GeoJSONLayer('geojson', {
url: 'https://example.com/data.geojson',
style: {
fillColor: [0.2, 0.6, 1.0, 0.4],
strokeColor: [0.0, 0.4, 0.8, 1.0],
strokeWidth: 2.0,
pointSize: 10.0,
textField: 'name',
textFont: '14px Arial'
}
});
engine.addLayer(geoJSONLayer);
await geoJSONLayer.loadFromURL();
// 或直接加载数据
const geoJSONLayer2 = new GeoJSONLayer('geojson2', {
data: {
type: 'FeatureCollection',
features: [
{
type: 'Feature',
geometry: {
type: 'Point',
coordinates: [116.4074, 39.9042]
},
properties: { name: '北京', population: 21540000 }
}
]
}
});
// 数据驱动样式
geoJSONLayer.setDataDrivenStyle({
fillColor: StyleFunction.createPropertyColorMap(
'type',
{
'residential': [0.8, 0.8, 0.6, 0.5],
'commercial': [1.0, 0.6, 0.6, 0.5],
'park': [0.4, 0.8, 0.4, 0.5],
},
[0.5, 0.5, 0.5, 0.5] // 默认颜色
)
});
// 支持所有 GeoJSON 几何类型
// Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon, GeometryCollection7. 风场动画图层 (WindLayer)
import { MapEngine, WindLayer } from 'mapjar';
// 创建地图引擎
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
// 创建风场图层
const windLayer = new WindLayer('wind', {
particleCount: 5000, // 粒子数量(默认 5000)
particleAge: 100, // 粒子生命周期(帧数,默认 100)
speedFactor: 0.5, // 速度因子(默认 0.5)
lineWidth: 1.0, // 线宽(默认 1.0)
fadeOpacity: 0.97, // 拖尾透明度(默认 0.97)
wrapX: true, // 启用跨世界渲染(默认 true)
colorRamp: [ // 颜色渐变(可选)
'#3288bd',
'#66c2a5',
'#abdda4',
'#e6f598',
'#fee08b',
'#fdae61',
'#f46d43',
'#d53e4f',
],
});
// 方式1:从图片加载风场数据(推荐)
// 图片格式:R 通道存储归一化的 U,G 通道存储归一化的 V,A 通道存储有效性
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = async () => {
const canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
const ctx = canvas.getContext('2d')!;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const pixels = imageData.data;
const uv = new Float32Array(canvas.width * canvas.height * 2);
const alpha = new Float32Array(canvas.width * canvas.height);
for (let i = 0; i < canvas.width * canvas.height; i++) {
const r = pixels[i * 4] / 255;
const g = pixels[i * 4 + 1] / 255;
const a = pixels[i * 4 + 3] / 255;
alpha[i] = a;
if (a === 0) {
uv[i * 2] = 0;
uv[i * 2 + 1] = 0;
} else {
uv[i * 2] = minU + r * (maxU - minU);
uv[i * 2 + 1] = minV + g * (maxV - minV);
}
}
windLayer.setData({
uv,
alpha,
width: canvas.width,
height: canvas.height,
minU: -12.35,
maxU: 22.81,
minV: -22.71,
maxV: 14.65,
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
};
img.src = 'wind-data.png';
// 方式2:直接使用数值数组
const windData = {
uv: new Float32Array([...]), // UV 数据数组 [u0, v0, u1, v1, ...]
width: 100, // 数据宽度
height: 50, // 数据高度
minU: -10, // U 分量最小值
maxU: 10, // U 分量最大值
minV: -10, // V 分量最小值
maxV: 10, // V 分量最大值
alpha: new Float32Array([...]), // 可选:透明度数组
bounds: { // 地理边界(可选)
minLon: 73.5,
minLat: 18.0,
maxLon: 135.0,
maxLat: 53.5,
},
};
windLayer.setData(windData);
engine.addLayer(windLayer);
// 特性:
// - 粒子系统渲染,GPU 加速
// - 支持从图片加载数据(R/G 通道存储 U/V,A 通道存储有效性)
// - 根据风速自动映射颜色
// - 平滑的拖尾效果
// - 缩放自适应(粒子大小和速度)
// - 跨世界渲染支持
// - 交互时自动暂停渲染8. 热力图层 (HeatmapLayer)
import { MapEngine, HeatmapLayer } from 'mapjar';
// 创建地图引擎
const engine = new MapEngine('#map', {
center: [105, 35],
zoom: 4,
});
// 创建热力图层
const heatmapLayer = new HeatmapLayer('temperature', {
colorRamp: [
{ value: 0.0, color: '#313695' }, // 最冷
{ value: 0.5, color: '#ffffbf' }, // 中间
{ value: 1.0, color: '#a50026' }, // 最热
],
wrapX: true, // 启用跨世界渲染(默认 true)
});
// 方式1:直接使用图片(推荐,更高效)
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = async () => {
const bitmap = await createImageBitmap(img);
heatmapLayer.setData({
image: bitmap,
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
engine.addLayer(heatmapLayer);
};
img.src = 'temperature.png';
// 方式2:使用数值数组(向后兼容)
heatmapLayer.setData({
values: new Float32Array([...]), // 温度值数组
width: 100,
height: 50,
min: -10,
max: 40,
alpha: new Float32Array([...]), // 可选:透明度数组
bounds: {
minLon: 55,
minLat: 1,
maxLon: 155,
maxLat: 57,
},
});
// 动态切换颜色方案
heatmapLayer.setColorRamp([
{ value: 0.0, color: '#0000FF' },
{ value: 0.5, color: '#00FF00' },
{ value: 1.0, color: '#FF0000' },
]);
// 特性:
// - 直接使用图片作为纹理,GPU 加速
// - 支持自定义颜色渐变(ColorStop 格式)
// - 自动 Y 轴翻转以匹配地理坐标系
// - 支持透明度通道(A 通道)
// - 跨世界渲染支持
// - 动态切换颜色方案颜色映射格式:
// 格式1:字符串数组(均匀分布)
colorRamp: ['#0000FF', '#00FF00', '#FF0000']
// 格式2:ColorStop 数组(精确控制,推荐)
colorRamp: [
{ value: 0.0, color: '#0000FF' }, // 0% 位置
{ value: 0.3, color: '#00FF00' }, // 30% 位置
{ value: 1.0, color: '#FF0000' }, // 100% 位置
]应用场景:
- 温度场可视化
- 降水量分布
- 气压场显示
- 污染物浓度
- 海拔高度图
- 任何连续数值场
9. 覆盖层图层 (OverlayLayer)
import { OverlayLayer } from 'mapjar';
// 创建覆盖层图层(一个图层只包含一个覆盖层)
const overlayLayer = new OverlayLayer('overlay');
engine.addLayer(overlayLayer);
// 创建 HTML 元素
const element = document.createElement('div');
element.innerHTML = `
<div style="
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.3);
font-family: Arial;
">
<h3 style="margin: 0 0 5px 0;">北京</h3>
<p style="margin: 0; color: #666;">中国首都</p>
</div>
`;
// 设置覆盖层
overlayLayer.setOverlay({
element: element,
position: {
lon: 116.4074,
lat: 39.9042,
offset: [0, -20], // 向上偏移 20px
anchor: [0.5, 1.0], // 底部中心锚点
},
properties: { name: '北京' },
});
// 更新位置
overlayLayer.updateOverlay({
position: {
lon: 116.5,
lat: 40.0,
}
});
// 更新元素
const newElement = document.createElement('div');
newElement.textContent = '新内容';
overlayLayer.updateOverlay({
element: newElement
});
// 更新多个参数
overlayLayer.updateOverlay({
element: newElement,
position: { lon: 116.5, lat: 40.0 },
visible: false
});
// 清空覆盖层
overlayLayer.clearOverlay();
// 特性:
// - 在地图上叠加单个 HTML 元素
// - 自动跟随地图平移和缩放
// - 支持自定义锚点和偏移
// - 支持完整的 CSS 样式和交互
// - 支持更新所有参数(element、position、visible、properties)
// - 统一的 DPI 管理,在高分辨率屏幕上位置准确
// - 适合复杂的标注、弹窗、自定义控件等位置配置:
interface OverlayPosition {
lon: number; // 经度
lat: number; // 纬度
offset?: [number, number]; // 偏移 [x, y],默认 [0, 0]
anchor?: [number, number]; // 锚点 [x, y],默认 [0.5, 0.5](中心)
}
// 锚点示例:
// [0, 0] - 左上角
// [0.5, 0] - 顶部中心
// [1, 0] - 右上角
// [0, 0.5] - 左侧中心
// [0.5, 0.5] - 中心(默认)
// [1, 0.5] - 右侧中心
// [0, 1] - 左下角
// [0.5, 1] - 底部中心
// [1, 1] - 右下角更新参数:
// 可以更新任意参数组合
overlayLayer.updateOverlay({
element?: HTMLElement, // 更新 HTML 元素
position?: OverlayPosition, // 更新位置
visible?: boolean, // 更新可见性
properties?: Record<string, unknown> // 更新属性
});应用场景:
- 地图标注(Marker)
- 信息弹窗(Popup)
- 自定义控件
- 复杂的交互式标签
- 富文本内容展示
- 图片、视频等媒体内容
- 动态更新的内容展示
10. Canvas 图层 (CanvasLayer)
import { CanvasLayer } from 'mapjar';
// 创建自定义 Canvas 图层
class MyCanvasLayer extends CanvasLayer {
constructor(id: string) {
super(id, 512, 512); // 指定 Canvas 尺寸
}
// 实现自定义渲染逻辑
render(gl: WebGL2RenderingContext, viewMatrix: Float32Array): void {
if (!this.visible) return;
const ctx = this.getContext();
// 清空 Canvas
this.clear();
// 使用 Canvas 2D API 绘制
ctx.fillStyle = 'rgba(255, 0, 0, 0.5)';
ctx.fillRect(100, 100, 200, 200);
ctx.strokeStyle = 'blue';
ctx.lineWidth = 3;
ctx.strokeRect(150, 150, 100, 100);
// Canvas 内容会自动转换为 WebGL 纹理渲染
}
}
const canvasLayer = new MyCanvasLayer('custom');
engine.addLayer(canvasLayer);
// 特性:
// - 纯 Canvas 2D 渲染
// - 自动转换为 WebGL 纹理
// - 适合自定义绘制逻辑
// - 支持所有 Canvas 2D API11. 数据驱动样式
根据要素属性动态设置样式:
import { VectorLayer, StyleFunction } from 'mapjar';
const layer = new VectorLayer('vector');
// 基于属性的颜色映射
layer.setDataDrivenStyle({
fillColor: StyleFunction.createPropertyColorMap(
'type',
{
'residential': [0.8, 0.8, 0.6, 0.5], // 住宅区 - 米黄色
'commercial': [1.0, 0.6, 0.6, 0.5], // 商业区 - 粉红色
'park': [0.4, 0.8, 0.4, 0.5], // 公园 - 绿色
},
[0.5, 0.5, 0.5, 0.5] // 默认颜色
)
});
// 数值范围的颜色插值
layer.setDataDrivenStyle({
fillColor: StyleFunction.createNumericColorScale(
'population',
[
[0, [0.2, 0.4, 1.0, 0.5]], // 低密度 - 蓝色
[5000, [0.4, 0.8, 0.8, 0.5]], // 中密度 - 青色
[10000, [0.8, 0.8, 0.4, 0.5]], // 中高密度 - 黄色
[20000, [1.0, 0.4, 0.2, 0.5]], // 高密度 - 橙色
],
[0.5, 0.5, 0.5, 0.5]
)
});
// 自定义样式函数
layer.setDataDrivenStyle({
fillColor: (properties) => {
const value = properties.temperature as number;
if (value < 0) return [0.2, 0.4, 1.0, 0.6]; // 冷 - 蓝色
if (value < 20) return [0.4, 0.8, 0.4, 0.6]; // 温和 - 绿色
if (value < 30) return [1.0, 0.8, 0.2, 0.6]; // 温暖 - 黄色
return [1.0, 0.2, 0.2, 0.6]; // 热 - 红色
},
pointSize: (properties) => {
const importance = properties.importance as number || 1;
return importance * 2;
}
});样式函数工具:
createPropertyColorMap- 基于分类属性的颜色映射createNumericColorScale- 基于数值范围的颜色插值createNumericSizeScale- 基于数值范围的大小插值createConditionalStyle- 基于条件的样式选择
应用场景:
- 人口密度热力图
- 交通流量可视化
- POI 分类显示
- 建筑高度可视化
12. 图层管理
// 获取图层
const layer = engine.getLayer('osm');
// 设置可见性
layer?.setVisible(false);
// 设置透明度
layer?.setOpacity(0.5);
// 设置层级
layer?.setZIndex(10);
// 移除图层
engine.removeLayer('osm');13. 事件系统
// 新的事件 API(推荐)
engine.on('click', (event) => {
console.log('点击:', event.lon, event.lat);
});
engine.on('mousemove', (event) => {
console.log('鼠标:', event.lon, event.lat);
});
// 一次性事件监听
engine.once('click', (event) => {
console.log('只触发一次');
});
// 移除事件监听
const removeListener = engine.on('click', handler);
removeListener(); // 调用返回的函数移除监听
// 或使用 off 方法
engine.off('click', handler);
// 移除所有监听器
engine.removeAllListeners('click');
engine.removeAllListeners(); // 移除所有事件的监听器
// 事件类型
type MapEventMap = {
click: MapClickEvent;
mousemove: MapMouseMoveEvent;
};交互操作
鼠标操作
- 拖拽平移:按住鼠标左键拖动
- 滚轮缩放:鼠标滚轮上下滚动(每次 1 级,带动画)
- 双击放大:双击鼠标左键(Shift + 双击缩小,带动画)
触摸操作
- 单指拖拽:单指按住拖动
- 双指轻触:快速连续轻触两次放大(带动画)
动画效果
- 平滑过渡:所有缩放操作都带有流畅的动画效果
- 缓动函数:使用 easeOutQuad 缓动,柔和自然
- flyTo 动画:平滑飞行到目标位置和缩放级别
- fitBounds 动画:自动适配边界并平滑过渡
- 可自定义:支持自定义动画时长和缓动函数
🎯 性能优化
Web Worker 瓦片加载
| 指标 | 主线程 | Worker | 提升 | |------|--------|--------|------| | 首屏加载 | 2.5s | 1.8s | 28% ↓ | | 主线程阻塞 | 150ms | 20ms | 87% ↓ | | 帧率下降 | 15 fps | 3 fps | 80% ↓ |
瓦片清晰度优化
| 指标 | 优化前 | 优化后 | 提升 | |------|--------|--------|------| | 纹理上传次数 | 每帧 | 仅一次 | 99% ↓ | | 渲染时间 | 8ms | 3ms | 62% ↓ | | 视觉质量 | ⭐⭐ | ⭐⭐⭐⭐⭐ | 显著提升 |
优化建议
- 瓦片图层:使用 CDN 加速瓦片加载,启用 Worker 并发加载
- 瓦片缩放:根据需求调整
tileScale,平衡清晰度和性能 - 跨世界渲染:全球地图启用
wrapX: true,区域地图禁用 - 矢量图层:避免添加过多要素(建议 < 10000)
- 渲染循环:不需要时调用
engine.stop()停止渲染 - 图层层级:合理设置 zIndex,减少重绘
🌐 浏览器兼容性
WebGL2 支持
- ✅ Chrome 56+
- ✅ Firefox 51+
- ✅ Safari 15+
- ✅ Edge 79+
覆盖 97%+ 的浏览器
📚 API 参考
MapEngine
flyTo(lon, lat, zoom?, options?)
平滑飞行到指定位置和缩放级别。
参数:
lon(number): 目标经度lat(number): 目标纬度zoom(number, 可选): 目标缩放级别,默认保持当前缩放options(object, 可选):duration(number): 动画时长(毫秒),默认自动计算maxDuration(number): 最大动画时长(毫秒),默认 3000
示例:
// 飞到北京,缩放到 10 级
engine.flyTo(116.4074, 39.9042, 10);
// 自定义动画时长
engine.flyTo(121.4737, 31.2304, 12, { duration: 2000 });
// 只改变位置,保持当前缩放
engine.flyTo(113.2644, 23.1291);fitBounds(bounds, options?)
自动适配到指定边界,并平滑过渡。
参数:
bounds(object): 边界对象minLon(number): 最小经度minLat(number): 最小纬度maxLon(number): 最大经度maxLat(number): 最大纬度
options(object, 可选):duration(number): 动画时长(毫秒),默认自动计算maxDuration(number): 最大动画时长(毫秒),默认 3000padding(number): 边界填充(像素),默认 50
示例:
// 适配中国边界
engine.fitBounds({
minLon: 73.5,
minLat: 18.2,
maxLon: 135.0,
maxLat: 53.5
});
// 自定义填充和动画时长
engine.fitBounds(
{
minLon: 110,
minLat: 30,
maxLon: 120,
maxLat: 40
},
{ padding: 100, duration: 1500 }
);Camera
flyTo(lon, lat, zoom?, options?)
相机级别的 flyTo 方法,与 MapEngine.flyTo 相同。
fitBounds(bounds, options?)
相机级别的 fitBounds 方法,与 MapEngine.fitBounds 相同。
📁 项目结构
mapjar/
├── lib/ # 库源代码
│ ├── animation/ # 动画系统
│ │ ├── Easing.ts # 缓动函数
│ │ ├── FlyToAnimation.ts # 飞行动画
│ │ └── ZoomAnimation.ts # 缩放动画
│ ├── core/ # 核心模块
│ │ ├── Camera.ts # 相机系统
│ │ └── WebGL2Renderer.ts # WebGL2 渲染器
│ ├── layer/ # 图层系统
│ │ ├── Layer.ts # 图层基类
│ │ ├── RasterLayer.ts # 栅格图层基类
│ │ ├── TileLayer.ts # 瓦片图层
│ │ ├── ImageLayer.ts # 图像图层
│ │ ├── VectorLayer.ts # 矢量图层
│ │ ├── GeoJSONLayer.ts # GeoJSON 图层
│ │ ├── WindLayer.ts # 风场动画图层
│ │ ├── HeatmapLayer.ts # 热力图层
│ │ ├── OverlayLayer.ts # 覆盖层图层
│ │ └── CanvasLayer.ts # Canvas 图层
│ ├── math/ # 数学工具
│ │ ├── Vec2.ts # 二维向量
│ │ └── Projection.ts # 投影系统
│ ├── spatial/ # 空间查询
│ │ └── SpatialQuery.ts # 空间查询工具
│ ├── style/ # 样式系统
│ │ └── StyleFunction.ts # 数据驱动样式
│ ├── utils/ # 工具类
│ │ ├── BatchRenderer.ts # 批量渲染器
│ │ ├── EventEmitter.ts # 事件发射器
│ │ ├── FrustumCulling.ts # 视锥剔除
│ │ ├── Loader.ts # 资源加载器
│ │ ├── ResourceManager.ts # 资源管理器
│ │ ├── TextRenderer.ts # 文字渲染器
│ │ └── WebGLUtils.ts # WebGL 工具函数
│ ├── workers/ # Web Workers
│ │ ├── TileLoader.worker.ts # 瓦片加载 Worker
│ │ └── TileLoader.worker.factory.ts # Worker 工厂
│ ├── MapEngine.ts # 地图引擎主类
│ └── main.ts # 导出入口
├── examples/ # 示例应用
│ ├── views/ # 示例页面
│ │ ├── Home.vue # 首页
│ │ ├── TileLayerExample.vue # 瓦片图层示例
│ │ ├── VectorLayerExample.vue # 矢量图层示例
│ │ ├── GeoJSONLayerExample.vue # GeoJSON 图层示例
│ │ ├── ImageLayerExample.vue # 图像图层示例
│ │ ├── WindLayerExample.vue # 风场图层示例
│ │ ├── WindLayerExample2.vue # 风场图层示例 2
│ │ ├── HeatmapLayerExample.vue # 热力图层示例
│ │ ├── OverlayLayerExample.vue # 覆盖层图层示例
│ │ └── CombinedExample.vue # 综合示例
│ ├── router/ # 路由配置
│ ├── components/ # 公共组件
│ ├── App.vue # 应用根组件
│ └── main.ts # 应用入口
└── index.d.ts # TypeScript 类型声明❓ 常见问题
Q: 瓦片加载失败?
A: 检查瓦片 URL 是否正确,是否需要 API Key,是否有跨域问题(CORS)。
Q: 矢量要素不显示?
A: 确保坐标在可见范围内,检查图层的 zIndex 是否被其他图层遮挡,检查样式的透明度是否为 0。
Q: 性能问题?
A: 减少矢量要素数量(建议 < 10000),使用瓦片图层代替大量矢量数据,调整 tileScale 平衡清晰度和性能,启用视锥剔除。
Q: 瓦片显示模糊?
A: 瓦片已自动启用 Mipmap 和各向异性过滤。如果文字太小,可以增加 tileScale 参数(1.0 - 3.0)。
Q: 地图边界有缝隙?
A: 确保瓦片服务器支持跨越 180° 经线的瓦片,或禁用 wrapX 选项。
Q: 如何添加自定义瓦片源?
A: 只需提供符合 {z}/{x}/{y} 格式的 URL 模板即可。
Q: 风场/热力图数据如何准备?
A: 推荐使用图片格式:风场数据用 R/G 通道存储 U/V 分量,A 通道存储有效性;热力图直接使用灰度图或彩色图。
Q: 移动端显示太小?
A: 引擎已自动适配高 DPI 屏幕(通过 Camera.getResolution() 的 DPI 缩放),无需额外配置。
Q: 如何自定义图层?
A: 继承 Layer、RasterLayer 或 CanvasLayer 基类,实现 render() 方法即可。
🗺️ 开发路线图
Phase 1: 基础渲染 ✅
- [x] WebGL2 初始化
- [x] 渲染循环
- [x] 响应式 canvas 大小
- [x] 高 DPR 支持
Phase 2: 地图基础 ✅
- [x] EPSG:3857 投影系统
- [x] 相机系统(平移、缩放、旋转)
- [x] 相机动画(flyTo、fitBounds)
- [x] 坐标转换
- [x] 瓦片加载和渲染
- [x] Web Worker 并发加载
- [x] 矢量图层(点、线、面)
- [x] 鼠标/触摸交互
- [x] 事件系统(点击、鼠标移动)
Phase 3: 高级功能 ✅
- [x] GeoJSON 支持(完整实现)
- [x] 多边形三角化(Earcut,支持凹多边形和带洞多边形)
- [x] 圆形点渲染(片段着色器实现,带抗锯齿)
- [x] 数据驱动样式(基于属性的动态样式)
- [x] 空间查询(点查询、范围查询、最近邻查询)
- [x] 图像图层(ImageLayer,支持历史地图叠加等)
- [x] 栅格图层基类(RasterLayer,统一渲染管线)
- [x] 文字渲染(TextRenderer,支持自定义字体、颜色、描边)
- [x] 跨世界渲染(水平无限循环)
- [x] 视锥剔除(FrustumCulling)
- [x] 批量渲染(BatchRenderer)
Phase 4: 科学可视化 ✅
- [x] 风场动画图层(WindLayer,粒子系统)
- [x] 热力图层(HeatmapLayer,温度、降水等)
- [x] Canvas 图层(CanvasLayer,自定义绘制)
- [x] 从图片加载数据(支持 Alpha 通道)
- [x] 颜色映射(ColorStop 格式)
Phase 5: 标注和覆盖层 ✅
- [x] 覆盖层图层(OverlayLayer,HTML 元素叠加)
- [ ] 标注系统(Label/Marker,优化的文字标注)
- [ ] 符号化渲染(Symbol Layer)
- [ ] 要素选择和高亮
- [ ] 矢量瓦片支持(MVT/PBF)
- [ ] 更多栅格图层(WMS、WMTS、Video)
- [ ] 3D 地形渲染
- [ ] 聚类(Clustering)
🛠️ 技术栈
- TypeScript
- WebGL2
- Web Workers
- ImageBitmap API
- earcut - 多边形三角化
- Vue 3
- Vite
📄 License
MIT
🤝 贡献
欢迎提交 Issue 和 Pull Request!
