效果

简述
项目上有这个需求,在网上找了一下没有找到相关方案,就结合cursor和问小白的相关回答自己实现了一个效果。这个效果呢,我不知其名,强名为点阵地图。理论上可以直接用图片来做的,但是因为需要根据地区打点,使用图片就无法判断国家的位置(返回的国家和地区不是固定的,不好做固定位置),就只能使用地图工具实际的绘制出来才能实现打点功能。
总结
- geoJson地图需要绘制出来,才能提供地图的基准尺寸,同时设置weight=0保持不可见
- 代码里面没有提供geoJson,自己在网上找一下
- 如果启用了拖动功能,图层的位置可能会出现相对于底部绘制的geoJson的偏移
- 通过map.getSize获取了地图尺寸,使用了canvas图层覆盖在上面实现
- canvas图层从左上角开始计算图中的点,根据圆点中心坐标转换为经纬度,判断经纬度是否包含(Turf.js实现)在地图的geoJson区域来过滤需要实际绘制出来的点
- 通过预计算减少点需要遍历的地区geoJSON坐标范围,优化了性能,优化前canvas区域总7000个点,需要5s-8s,优化后50ms
- 通过canvas缓存,避免重复计算
- zoomSnap:0.1,配置leaflet最小缩放步长,能更好的填充满容器区域
- 总结完毕
完整代码
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Leaflet GeoJSON 地图</title>
<!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
<style>
body {
margin: 0;
padding: 0;
font-family: Arial, sans-serif;
}
#map {
height: 447px;
width: 1014px;
background-color: #F2F5FF;
}
.control-panel {
position: absolute;
top: 10px;
right: 10px;
background: white;
padding: 15px;
border-radius: 5px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
z-index: 1000;
min-width: 200px;
}
.control-panel h3 {
margin: 0 0 10px 0;
color: #333;
}
.control-panel button {
width: 100%;
padding: 8px;
margin: 5px 0;
border: none;
border-radius: 3px;
background: #007cba;
color: white;
cursor: pointer;
font-size: 14px;
}
.control-panel button:hover {
background: #005a87;
}
</style>
</head>
<body>
<div id="map"></div>
<div class="control-panel">
<h3>地图控制</h3>
<button onclick="loadGeoJSON()">加载 GeoJSON</button>
<button onclick="clearGeoJSON()">清除 GeoJSON</button>
<button onclick="fitBounds()">适应边界</button>
</div>
<!-- Leaflet JavaScript -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Turf.js for spatial operations -->
<script src="https://unpkg.com/@turf/turf@6.5.0/turf.min.js"></script>
<script>
const dotCanvas = new OffscreenCanvas(6, 6);
const dotCtx = dotCanvas.getContext('2d');
// 在图案画布上绘制格点
dotCtx.beginPath();
dotCtx.arc(3, 3, 3, 0, 2 * Math.PI);
dotCtx.fillStyle = '#D6DFFF';
dotCtx.fill();
class Point {
constructor(x, y, lat, lng) {
this.x = x; // 像素X坐标
this.y = y; // 像素Y坐标
this.lat = lat; // 纬度
this.lng = lng; // 经度
this.image = dotCanvas;
this.turfPoint = null; // turf.js点对象
}
draw(ctx) {
ctx.drawImage(this.image, this.x, this.y);
}
// 获取turf.js点对象
getTurfPoint() {
if (!this.turfPoint) {
this.turfPoint = turf.point([this.lng, this.lat]);
}
return this.turfPoint;
}
// 检查是否包含在GeoJSON要素内
isInsideFeature(feature) {
const point = this.getTurfPoint();
return turf.booleanPointInPolygon(point, feature);
}
// 检查是否包含在GeoJSON要素集合内
isInsideFeatureCollection(featureCollection) {
const point = this.getTurfPoint();
for (const feature of featureCollection.features) {
if (turf.booleanPointInPolygon(point, feature)) {
return { inside: true, feature: feature };
}
}
return { inside: false, feature: null };
}
}
/**
* 记录地图缩放比例对应的格点图
* @type {Record<number, OffscreenCanvas>}
*/
const mapZoomPoints = {}
const createPointCanvas = (w, h, points) => {
const canvas = new OffscreenCanvas(w, h);
const ctx = canvas.getContext('2d');
drawGridPointsOptimized(points, ctx);
return canvas;
}
// ==================== 全局变量和配置 ====================
const map = L.map('map', {
zoomAnimation: false,
keyboard: false,
scrollWheelZoom: false,
zoomSnap: 0.1,
dragging: false,
doubleClickZoom: false,
boxZoom: false,
zoomControl: false,
attributionControl: false,
tap: false,
touchZoom: false,
}).setView([0, 0], 3); // 全球视图
// 图层管理
let geoJSONLayer = null;
let gridMapLayer = null;
let worldMapData = null;
// ==================== 样式配置 ====================
// 轮廓地图样式
function styleFeature(feature) {
return {
fillColor: 'transparent',
weight: 0,
opacity: 1,
color: '#333333',
dashArray: '',
fillOpacity: 0
};
}
// 过滤掉南极洲的函数
function filterOutAntarctica(originalData) {
return {
type: "FeatureCollection",
features: originalData.features.filter(feature => feature.properties['国家名称'] !== '南极洲')
};
}
// 从几何体中提取所有坐标点
function getCoordinatesFromGeometry(geometry) {
const coords = [];
function extractCoords(coordinateArray) {
if (Array.isArray(coordinateArray)) {
if (coordinateArray.length === 2 && typeof coordinateArray[0] === 'number') {
// 这是一个坐标点 [lng, lat]
coords.push(coordinateArray);
} else {
// 这是一个坐标数组,递归处理
coordinateArray.forEach(coord => extractCoords(coord));
}
}
}
if (geometry.coordinates) {
extractCoords(geometry.coordinates);
}
return coords;
}
// ==================== 格点绘制方法 ====================
/**
* 原始绘制方法(性能较慢,用于对比)
* @param {Array} points - 格点数组
* @param {CanvasRenderingContext2D} ctx - Canvas上下文
*/
function drawGridPointsOriginal(points, ctx) {
console.log('使用原始绘制方法(性能较慢)');
const pointsInCountries = points.filter(point => {
// 检查格点是否在某个国家内
if (worldMapData) {
const result = point.isInsideFeatureCollection(worldMapData);
if (result.inside) {
return true;
}
}
return false;
});
pointsInCountries.forEach(point => point.draw(ctx));
console.log(`总共生成了 ${points.length} 个格点,其中 ${pointsInCountries.length} 个格点在国家区域内`);
}
/**
* 优化绘制方法(性能快速)
* @param {Array} points - 格点数组
* @param {CanvasRenderingContext2D} ctx - Canvas上下文
*/
function drawGridPointsOptimized(points, ctx) {
console.log('使用优化绘制方法(性能快速)');
// 性能优化:预计算国家边界框,减少不必要的计算
const countryBounds = worldMapData ? worldMapData.features.map(feature => {
const coords = getCoordinatesFromGeometry(feature.geometry);
if (coords.length === 0) return null;
const lngs = coords.map(coord => coord[0]);
const lats = coords.map(coord => coord[1]);
return {
feature: feature,
minLng: Math.min(...lngs),
maxLng: Math.max(...lngs),
minLat: Math.min(...lats),
maxLat: Math.max(...lats)
};
}).filter(bounds => bounds !== null) : [];
// 性能优化:创建空间哈希表,快速定位可能包含点的国家
const spatialHash = new Map();
const hashSize = 10; // 将地图分为10x10的网格
countryBounds.forEach(bounds => {
const minHashLng = Math.floor((bounds.minLng + 180) / 360 * hashSize);
const maxHashLng = Math.floor((bounds.maxLng + 180) / 360 * hashSize);
const minHashLat = Math.floor((bounds.minLat + 90) / 180 * hashSize);
const maxHashLat = Math.floor((bounds.maxLat + 90) / 180 * hashSize);
for (let lng = minHashLng; lng <= maxHashLng; lng++) {
for (let lat = minHashLat; lat <= maxHashLat; lat++) {
const key = `${lng},${lat}`;
if (!spatialHash.has(key)) {
spatialHash.set(key, []);
}
spatialHash.get(key).push(bounds);
}
}
});
// 直接处理所有格点
const pointsInCountries = points.filter(point => {
if (!worldMapData) return false;
// 使用空间哈希表快速定位可能包含点的国家
const hashLng = Math.floor((point.lng + 180) / 360 * hashSize);
const hashLat = Math.floor((point.lat + 90) / 180 * hashSize);
const key = `${hashLng},${hashLat}`;
const candidateBounds = spatialHash.get(key) || [];
// 只检查空间哈希表中的候选国家
for (const bounds of candidateBounds) {
if (point.lng >= bounds.minLng && point.lng <= bounds.maxLng &&
point.lat >= bounds.minLat && point.lat <= bounds.maxLat) {
// 边界框内,进行精确检查
const result = point.isInsideFeature(bounds.feature);
if (result) {
return true; // 找到一个匹配就返回true
}
}
}
return false;
});
// 绘制所有匹配的格点
pointsInCountries.forEach(point => point.draw(ctx));
console.log(`总共生成了 ${points.length} 个格点,其中 ${pointsInCountries.length} 个格点在国家区域内`);
}
// ==================== 数据加载和管理 ====================
async function loadGeoJSON() {
try {
// 加载数据文件
if (!worldMapData) {
console.log('正在加载世界地图数据...');
const response = await fetch('./world-map.json');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
worldMapData = filterOutAntarctica(await response.json());
console.log('世界地图数据加载完成,包含', worldMapData.features.length, '个国家/地区');
}
// 清除现有图层
clearLayers();
// 始终创建轮廓地图
createOutlineMapLayer();
createGridMapLayer();
// 适应边界
fitBoundsToCurrentLayer();
console.log('世界地图 GeoJSON 加载成功');
} catch (error) {
console.error('加载 GeoJSON 时出错:', error);
alert('加载世界地图数据时出错: ' + error.message);
}
}
function clearLayers() {
if (geoJSONLayer) {
map.removeLayer(geoJSONLayer);
geoJSONLayer = null;
}
if (gridMapLayer) {
// 移除Canvas元素
if (gridMapLayer._canvas) {
const canvas = gridMapLayer._canvas;
if (canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
}
}
// 移除事件监听
map.off('viewreset', gridMapLayer._draw);
map.off('zoom', gridMapLayer._draw);
map.off('move', gridMapLayer._draw);
map.off('resize', gridMapLayer._draw);
map.removeLayer(gridMapLayer);
gridMapLayer = null;
}
}
function createOutlineMapLayer() {
console.log(`轮廓地图过滤后包含 ${worldMapData.features.length} 个国家/地区`);
geoJSONLayer = L.geoJSON(worldMapData, {
style: styleFeature
});
geoJSONLayer.addTo(map);
console.log('轮廓地图加载完成(已排除南极洲)');
}
function createGridMapLayer() {
console.log('正在生成格点地图(Canvas渲染),请稍候...');
// 使用setTimeout让UI有时间更新
setTimeout(() => {
// const gridData = generateGridMapData(worldMapData);
// 创建Canvas图层
gridMapLayer = L.layerGroup();
// 创建Canvas元素
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// 设置Canvas样式
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
canvas.style.pointerEvents = 'none';
canvas.style.zIndex = '1000';
// 绘制函数
function drawGridPoints() {
const bounds = map.getSize();
console.log(bounds);
canvas.width = bounds.x;
canvas.height = bounds.y;
// 清除画布
ctx.clearRect(0, 0, canvas.width, canvas.height);
const zoom = map.getZoom();
if (mapZoomPoints[zoom]) {
ctx.drawImage(mapZoomPoints[zoom], 0, 0);
} else {
const points = [];
const step = 8;
// 创建格点图案
for (let i = 0; i < canvas.width; i += step) {
for (let j = 0; j < canvas.height; j += step) {
// 将像素位置转换为经纬度
const pixelPoint = L.point(i + step / 2, j + step / 2);
const latLng = map.containerPointToLatLng(pixelPoint);
points.push(new Point(i, j, latLng.lat, latLng.lng));
}
}
mapZoomPoints[zoom] = createPointCanvas(canvas.width, canvas.height, points);
ctx.drawImage(mapZoomPoints[zoom], 0, 0);
}
}
// 创建Canvas图层
const canvasLayer = L.layerGroup();
canvasLayer._canvas = canvas;
canvasLayer._draw = drawGridPoints;
// 添加到地图
map.getPanes().overlayPane.appendChild(canvas);
// 监听地图事件
map.on('viewreset', drawGridPoints);
map.on('zoom', drawGridPoints);
map.on('move', drawGridPoints);
map.on('resize', drawGridPoints);
// 初始绘制
drawGridPoints();
gridMapLayer = canvasLayer;
gridMapLayer.addTo(map);
}, 100);
}
function fitBounds() {
fitBoundsToCurrentLayer();
}
function fitBoundsToCurrentLayer() {
// 优先使用轮廓地图的边界,因为它覆盖全球
if (geoJSONLayer && geoJSONLayer.getBounds().isValid()) {
map.fitBounds(geoJSONLayer.getBounds());
}
}
// 页面加载完成后自动加载世界地图数据
window.addEventListener('load', function () {
console.log('页面加载完成,正在加载世界地图...');
loadGeoJSON();
});
</script>
</body>
</html>