话不多说直接看效果
为啥要整这个呢,就是项目有个需求,要求底图改成带有山脉纹理的,但是有不能有除中国以外的地方出现背景(这里只能排除使用各种底图API了),然后我就查阅Echart文档,没有现成的可实现的方法,网上有Three.js + react实现的,可是我自己又不会,咋整呢?当然这个年头了问AI呀,问了,说是使用Ehcart的graphic添加背景图,然后我就网上各种找,要这种除了区划边界之外都是透明的底图,还真被我在GitHub上找到了,是个项目,直接本地运行,根据选择的区划直接下载底图。背景图片找到了,那就是写代码呗
话不多说直接上源码
源码在下边,先听我说完,按照ai的方式给代码写出来了,发现边界数据和图片是两个层级,且对不上,差十万八千里,难受,继续叫他修改。。。。。此时改了很久,终于改好了。
主要解决了,缩放拖拽时底图和区划边界不重合的问题,其实就是每次缩放和拖拽都重新计算一下底图的位置。及有了底图,有兼容了echarts的各种配置,打点,飞行图...不过不支持下钻,想要下钻还需要找到对应地区的底图,然后下钻之后再对应一下底图和区划边界,想想就麻烦,这里就不弄了。
需要自己来取源码,使用的geojson数据来源DataV。
<template>
<div class="china-map-page">
<div ref="mapRef" class="china-map"></div>
</div>
</template>
<script setup lang="ts">
import {
ref,
reactive,
onMounted,
onBeforeUnmount,
nextTick,
computed
} from 'vue'
import * as echarts from 'echarts'
import { geoMercator } from 'd3-geo'
import chinaBg from '@/assets/china.png'
const mapRef = ref<HTMLElement | null>(null)
let chart: echarts.ECharts | null = null
/**
* =========================================================
* 你现在已经调好的参数
* =========================================================
*
* 这几个值会作为「初始状态」保存。
*
* 初始状态:
* scaleX = 1
* scaleY = 1.3
* offsetX = 3
* offsetY = 93
*
* 只要初始状态已经对齐,后面的缩放、拖动都会基于
* 这个状态进行同步,不会重新把纹理强行 fit 到地图。
* (根据自身情况可调整)
*/
const textureConfig = reactive({
scaleX: 1,
scaleY: 1.3,
offsetX: 3,
offsetY: 93
})
/**
* =========================================================
* 中国大致经纬度范围(根据自身情况可调整)
* =========================================================
*/
const CHINA_BOUNDS = {
left: 73.2,
top: 54.1,
right: 134.8,
bottom: 17.8
}
// 根据CHINA_BOUNDS计算出横纵坐标的参考点
const x = computed(() => {
return (CHINA_BOUNDS.left + CHINA_BOUNDS.right) / 2
})
const y = computed(() => {
return (CHINA_BOUNDS.top + CHINA_BOUNDS.bottom) / 2
})
// 这个点主要是为了测量横向缩放。也就是和x ,y 相差一些数值,就可以近似表示地图当前的横向比例。
const bl = [134,20]
/**
* =========================================================
* 固定参考点
* =========================================================
*
* 不再使用:
*
* 当前地图矩形中心
*
* 而是使用固定的地理坐标。
*
* 这样地图发生 roam 后:
*
* 经纬度 -> 当前像素
*
* 可以准确知道地图到底发生了多少缩放和位移。
*/
const GEO_ANCHORS = {
/**
* 中心参考点
*/
center: [x.value, y.value] as [number, number],
/**
* X 方向参考点
*
* 和 center 保持相同纬度。
*/
x: [bl[0], y.value] as [number, number],
/**
* Y 方向参考点
*
* 和 center 保持相同经度。
*/
y: [x.value, bl[1]] as [number, number]
}
/**
* =========================================================
* 初始状态
* =========================================================
*
* 地图第一次渲染完成后,把:
*
* 1. 参考点像素位置
* 2. PNG 的位置
* 3. PNG 的大小
*
* 全部记录下来。
*
* 后面的 roam 都基于这个状态计算。
*/
let initialState: {
anchorCenter: [number, number]
anchorX: [number, number]
anchorY: [number, number]
textureCenter: [number, number]
textureWidth: number
textureHeight: number
} | null = null
/**
* =========================================================
* 初始化地图
* =========================================================
*/
async function initMap() {
if (!mapRef.value) return
chart = echarts.init(mapRef.value)
/**
* 加载中国 GeoJSON
*/
const chinaGeoJson = await fetch(
'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json'
).then(res => {
if (!res.ok) {
throw new Error(`加载中国地图失败:${res.status}`)
}
return res.json()
})
/**
* 注册地图
*/
echarts.registerMap('china', chinaGeoJson)
/**
* =======================================================
* d3 Mercator(使用第三方的投影坐标可以更好的让底图和区域边界重合)
* =======================================================
*/
const projection = geoMercator()
/**
* 先给一个中心
*/
projection.center([104, 35])
/**
* fitSize 只负责初始化地图。
*
* 注意:
*
* 后面不会再使用这个东西去重新计算 PNG。
*/
projection.fitSize([1000, 700], chinaGeoJson)
/**
* =======================================================
* ECharts option
* =======================================================
*/
const option: echarts.EChartsOption = {
backgroundColor: '#000',
geo: {
map: 'china',
/**
* 开启拖动 + 缩放
*/
roam: true,
/**
* 初始缩放如果初始不是1,那就需要重新调整textureConfig和CHINA_BOUNDS
*/
zoom: 1,
/**
* 地图层级
*/
z: 2,
/**
* 使用 d3 的 Mercator 投影
*/
projection: {
project: (point) => {
return projection(point as [number, number])
},
unproject: (point) => {
return projection.invert(
point as [number, number]
)!
}
},
/**
* 地图区域透明
*/
itemStyle: {
areaColor: 'rgba(0, 0, 0, 0)',
borderColor: '#00ffff',
borderWidth: 2
},
/**
* 鼠标移入
*/
emphasis: {
itemStyle: {
areaColor: 'rgba(0, 150, 255, 0.2)'
},
label:{
color:'#fff'
}
}
},
/**
* 初始不放 graphic
*
* 后面 updateChinaTexture 再添加。
*/
graphic: []
}
chart.setOption(option)
await nextTick()
/**
* 等 ECharts 真正完成第一次布局
*/
requestAnimationFrame(() => {
requestAnimationFrame(() => {
/**
* 第一次创建纹理。
*
* 这里会记录「初始状态」。
*/
initTextureState()
})
})
/**
* =======================================================
* 地图缩放 / 拖动
* =======================================================
*/
chart.on('georoam', () => {
updateChinaTexture()
})
}
/**
* =========================================================
* 经纬度 -> 当前 ECharts 像素坐标
* =========================================================
*/
function geoToPixel(
point: [number, number]
): [number, number] | null {
if (!chart) return null
const result = chart.convertToPixel(
{
geoIndex: 0
},
point
) as number[]
if (!result || result.length < 2) {
return null
}
if (
!Number.isFinite(result[0]) ||
!Number.isFinite(result[1])
) {
return null
}
return [
result[0],
result[1]
]
}
/**
* =========================================================
* 计算初始纹理
* =========================================================
*
* 注意:
*
* 这里仍然使用你之前的算法。
*
* 所以:
*
* scaleX = 1
* scaleY = 1.3
* offsetX = 3
* offsetY = 93
*
* 得到的初始效果不会被改变。
*/
function initTextureState() {
if (!chart) return
/**
* 获取三个固定参考点
*/
const anchorCenter = geoToPixel(GEO_ANCHORS.center)
const anchorX = geoToPixel(GEO_ANCHORS.x)
const anchorY = geoToPixel(GEO_ANCHORS.y)
if (
!anchorCenter ||
!anchorX ||
!anchorY
) {
return
}
/**
* =======================================================
* 获取当前中国地图矩形
* =======================================================
*/
const leftTop = geoToPixel([
CHINA_BOUNDS.left,
CHINA_BOUNDS.top
])
const rightBottom = geoToPixel([
CHINA_BOUNDS.right,
CHINA_BOUNDS.bottom
])
if (
!leftTop ||
!rightBottom
) {
return
}
const baseLeft = leftTop[0]
const baseTop = leftTop[1]
const baseWidth =
rightBottom[0] - leftTop[0]
const baseHeight =
rightBottom[1] - leftTop[1]
if (
baseWidth <= 0 ||
baseHeight <= 0
) {
return
}
/**
* =======================================================
* 根据你原来的参数计算 PNG 大小
* =======================================================
*/
const textureWidth =
baseWidth * textureConfig.scaleX
const textureHeight =
baseHeight * textureConfig.scaleY
/**
* 原来的中心算法
*/
const centerX =
baseLeft +
baseWidth / 2
const centerY =
baseTop +
baseHeight / 2
/**
* 原来的 offset
*/
const textureCenterX =
centerX +
textureConfig.offsetX
const textureCenterY =
centerY +
textureConfig.offsetY
/**
* =======================================================
* 保存初始状态
* =======================================================
*/
initialState = {
anchorCenter: [
anchorCenter[0],
anchorCenter[1]
],
anchorX: [
anchorX[0],
anchorX[1]
],
anchorY: [
anchorY[0],
anchorY[1]
],
textureCenter: [
textureCenterX,
textureCenterY
],
textureWidth,
textureHeight
}
/**
* 第一次直接绘制
*/
renderTexture(
textureCenterX,
textureCenterY,
textureWidth,
textureHeight
)
}
/**
* =========================================================
* 根据地图当前状态更新 PNG
* =========================================================
*
* 这是整个方案最关键的地方。
*
* 不再:
*
* 当前地图矩形 -> 重新计算 PNG
*
* 而是:
*
* 初始地图
* ↓
* 当前地图
* ↓
* 计算实际 X/Y 缩放
* ↓
* 计算实际位移
* ↓
* 同样的 transform 应用到 PNG
*/
function updateChinaTexture() {
if (!chart) return
if (!initialState) {
return
}
/**
* =======================================================
* 获取当前三个参考点
* =======================================================
*/
const currentCenter = geoToPixel(
GEO_ANCHORS.center
)
const currentX = geoToPixel(
GEO_ANCHORS.x
)
const currentY = geoToPixel(
GEO_ANCHORS.y
)
if (
!currentCenter ||
!currentX ||
!currentY
) {
return
}
/**
* =======================================================
* 计算初始参考距离
* =======================================================
*/
/**
* X 方向初始距离
*/
const initialDistanceX =
initialState.anchorX[0] -
initialState.anchorCenter[0]
/**
* Y 方向初始距离
*/
const initialDistanceY =
initialState.anchorY[1] -
initialState.anchorCenter[1]
if (
Math.abs(initialDistanceX) < 0.0001 ||
Math.abs(initialDistanceY) < 0.0001
) {
return
}
/**
* =======================================================
* 当前参考距离
* =======================================================
*/
const currentDistanceX =
currentX[0] -
currentCenter[0]
const currentDistanceY =
currentY[1] -
currentCenter[1]
/**
* =======================================================
* 得到地图真正的 X / Y 缩放比例
* =======================================================
*/
const scaleX =
currentDistanceX /
initialDistanceX
const scaleY =
currentDistanceY /
initialDistanceY
/**
* 防止异常值
*/
if (
!Number.isFinite(scaleX) ||
!Number.isFinite(scaleY) ||
scaleX <= 0 ||
scaleY <= 0
) {
return
}
/**
* =======================================================
* 计算纹理相对于初始锚点的位置
* =======================================================
*/
const textureOffsetX =
initialState.textureCenter[0] -
initialState.anchorCenter[0]
const textureOffsetY =
initialState.textureCenter[1] -
initialState.anchorCenter[1]
/**
* =======================================================
* 地图缩放后:
*
* 初始:
*
* Anchor -------- Texture
*
* 缩放:
*
* Anchor ---------------- Texture
*
* 所以纹理相对于 Anchor 的距离也需要一起缩放。
* =======================================================
*/
const newTextureCenterX =
currentCenter[0] +
textureOffsetX * scaleX
const newTextureCenterY =
currentCenter[1] +
textureOffsetY * scaleY
/**
* =======================================================
* PNG 本身也跟着缩放
* =======================================================
*/
const newTextureWidth =
initialState.textureWidth *
scaleX
const newTextureHeight =
initialState.textureHeight *
scaleY
/**
* 绘制
*/
renderTexture(
newTextureCenterX,
newTextureCenterY,
newTextureWidth,
newTextureHeight
)
}
/**
* =========================================================
* 真正绘制 PNG
* =========================================================
*/
function renderTexture(
centerX: number,
centerY: number,
width: number,
height: number
) {
if (!chart) return
/**
* 根据中心点算左上角
*/
const left =
centerX -
width / 2
const top =
centerY -
height / 2
chart.setOption({
graphic: {
type: 'image',
id: 'china-texture',
left,
top,
/**
* PNG 在下面
*
* geo 在上面。
*
* 这样青色边界可以覆盖 PNG。
*/
z: 1,
silent: true,
style: {
image: chinaBg,
width,
height,
opacity: 1
}
}
})
}
/**
* =========================================================
* 浏览器尺寸变化
* =========================================================
*/
function handleResize() {
if (!chart) return
chart.resize()
/**
* resize 后:
*
* 地图像素位置变了,
* 但当前地图的 roam 状态仍然存在。
*
* 所以不能重新初始化 initialState。
*
* 直接按照当前地图状态重新计算纹理。
*/
requestAnimationFrame(() => {
updateChinaTexture()
})
}
/**
* =========================================================
* mounted
* =========================================================
*/
onMounted(async () => {
await nextTick()
await initMap()
window.addEventListener(
'resize',
handleResize
)
})
/**
* =========================================================
* beforeUnmount
* =========================================================
*/
onBeforeUnmount(() => {
window.removeEventListener(
'resize',
handleResize
)
if (chart) {
chart.off('georoam')
chart.dispose()
chart = null
}
initialState = null
})
</script>
<style scoped>
.china-map-page {
width: 100%;
height: 100%;
min-height: 600px;
overflow: hidden;
background: #000;
position: relative;
}
.china-map {
width: 100%;
height: 100%;
}
</style>