前言:最近写无聊的业务,都没啥值得写的东西。这个业务稍微动了点脑子,值得一写,遂有此文。
业务描述
某单位有一海上雷达设备用于采集信息,两种型号,一种扫描半径在30km内的,一种扫描半径在60km内的,隔一段时间就会传输采集到的信息入库。雷达设备放置的位置处于中心点,采集到的数据表示位置为经纬度。这里的需求是需要将点显示出来,同时点击查看雷达数据的详细内容。
效果
圆(公里的表示)
centerPixel 为当前容器的中心像素
scaleRatio 为缩放比例。
之后不再赘述
drawCircle() {
let ctx = this.ctx
let centerPixel = this.centerPixel
// 以更小的作处理. 确保 是 当前容器的最大正圆.
let min = Math.min(centerPixel[0], centerPixel[1])
let radius = min * this.scaleRatio;
let diff = radius / 6;
const PI2 = Math.PI * 2
ctx.strokeStyle = '#31474c'
ctx.beginPath()
ctx.arc(centerPixel[0], centerPixel[1], radius, 0, PI2)
ctx.stroke()
// 在大圆的基础下 往内收缩.
for (let i = 1; i < 7; i++) {
ctx.beginPath()
if(i == 3 || i == 6){
ctx.strokeStyle = '#fff'
}
ctx.arc(centerPixel[0], centerPixel[1], diff * i, 0, PI2)
ctx.stroke()
if(i == 3 || i == 6){
ctx.strokeStyle = '#31474c'
}
}
}
经纬度坐标的处理
在这里,需要重点解决的是:获取从经纬度坐标 相对于中心点的 距离,通过此距离 处理成 该显示的屏幕坐标。应该表述已经足够清晰,可配合代码理解
calculateCurMeterPixel(currentLonLat) {
// 绘制 以 originlonlatCenter 作为原点的 坐标系下的 点
let originCenter = this.originCenter
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
// 总厂
let allLen = 60
// 将当前中心点 偏移回原点. 得到偏移量
let offset = [-originCenter[0], -originCenter[1]]
let xy = this.vector2Add(offset, currentLonLat)
// degree = meter / (Math.PI * 6371004) * 180;
//在经线上,纬度每差1度,实地距离大约为111千米;
// 在纬线上,经度每差1度,实际距离为111×cosθ千米。(其中θ表示该纬线的纬度.在不同纬线上,经度每差1度的实际距离是不相等的)。
// 但是这些我都不管QAQ 我就用111 ,甲方要是发现了 我再改
// to meter
// const degree = (Math.PI * 6371004)
const lonK = 111
// 求得比例.
// 实际意义:xRatio 为表示 当前经纬度的点 距离中心点 的偏移比例
// 公式的含义: x * lonK / allLen 表示 当前偏移量所代表的实地距离 在 总长距离 下的比例.
let xRatio = xy[0] * lonK / allLen * this.scaleRatio
let yRatio = xy[1] * lonK / allLen * this.scaleRatio
let xValue = min * xRatio + centerPixel[0]
console.log(xRatio, yRatio);
// 取反的原因:
// canvas 坐标系 向下增加. 经纬度表示 y 轴向上增加.
let yValue = centerPixel[1] - min * yRatio
return {
x: xValue,
y: yValue
}
}
vector2Add(v1, v2) {
return [v1[0] + v2[0], v1[1] + v2[1]]
}
接着绘制点,在这个地方我们维护一个雷达对象的表示数组,里面包含:xy 屏幕像素,经纬度,与其他数据。
/**
* 绘制当前坐标系下的 点 .
*/
drawCurrentAxisPoint() {
let pointsArr = this.pointsArr
this.ctx.fillStyle = '#d3cfa1'
for (let i = 0; i < pointsArr.length; i++) {
let cur = pointsArr[i]
let pointStruct = this.calculateCurMeterPixel(cur)
let info = this.calculateDirectionAndDistance(cur)
let obj = {
x: pointStruct.x,
y: pointStruct.y,
lon: cur[0],
lat: cur[1],
radarBatchNum: this.radarData[i].radarBatchNum,
direction: info.direction,
distance: info.distance,
collectTime: this.radarData[i].collectTime,
course: this.radarData[i].course,
speed: this.radarData[i].speed
}
this.pointStructs.push(obj)
this.drawPointByPixel(obj)
}
this.ctx.font = "14px Arial"
this.ctx.textBaseline = "bottom"
this.ctx.textAlign = "left";
this.ctx.fillStyle = '#d3cfa1'
for (let i = 0; i < this.pointStructs.length; i++) {
let cur = this.pointStructs[i]
this.ctx.fillText(this.radarData[i].radarBatchNum, cur.x, cur.y)
}
}
drawPointByPixel(point) {
let ctx = this.ctx
ctx.beginPath()
ctx.arc(point.x, point.y, 6, 0, Math.PI * 2)
ctx.fill()
}
鼠标事件处理
hover改个鼠标指针,不值一提,直接click,以此触类旁通
/**
* 为点生成点击事件.
*/
clickHandler(e) {
let x = (e.clientX - this.canvas.getBoundingClientRect().left)
let y = (e.clientY - this.canvas.getBoundingClientRect().top)
let info = this.checkPixelIsPoint({ x, y })
if (info.isPoint) {
console.log(info.pointStruct);
this.tooltipDom.style.top = y + 'px'
this.tooltipDom.style.left = x + 'px'
this.tooltipDom.innerHTML = `
<span>经纬度:${info.pointStruct.lon},${info.pointStruct.lat}</span><br />
<span>时间:${info.pointStruct.collectTime}</span><br />
<span>方位:${info.pointStruct.direction}°</span><br />
<span>距离:${info.pointStruct.distance}km</span><br />
<span>速度:${info.pointStruct.speed}节</span><br />
<span>航向:${info.pointStruct.course}°</span><br />
`
this.tooltipDom.style.display = 'block'
// console.log(info.pointStruct);
}
}
// 检测当前坐标是否为处理点
checkPixelIsPoint(pointStruct) {
// 点本身 具有大小, 会影响结果. 因此 应该是计算 该pixel 落入的范围.
// 这里做的检测机制为 包裹住圆点 的正方形盒子. 不完全精确 但应该够用.
// 目前为写死的 半径为6的小圆点.
let radius = 6
let pointStructs = this.pointStructs
let checkObj = {
isPoint: false,
pointStruct: null
}
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
for (let i = 0; i < pointStructs.length; i++) {
let element = pointStructs[i]
let leftBottom = [element.x - radius, element.y - radius];
let rightTop = [element.x + radius, element.y + radius];
// 在范围内. 说明 该点 命中 检测.
if ((leftBottom[0] < pointStruct.x && pointStruct.x < rightTop[0]) && (leftBottom[1] < pointStruct.y && pointStruct.y < rightTop[1])) {
checkObj.pointStruct = element
checkObj.isPoint = true
return checkObj
}
}
return checkObj
}
tooltipDom 的创建 这里fatherDom 表示将这个雷达塞入到哪个DOM下去。
/**
* 创建 图例.. 即点击弹窗后的 div 内容
*/
createTooltip() {
// 这里仅做创建. 因为无论是 输出的文本内容或者是 位置. 都需要配合点击事件 做处理.
let dom = document.createElement('div')
dom.style.display = 'none'
dom.style.color = 'red'
dom.style.fontSize = '20px'
dom.style.position = 'absolute'
dom.style.width = '500px'
dom.style.pointerEvents = 'none'
let fatherDom = document.getElementById(this.radarId)
fatherDom.style.position = 'relative'
fatherDom.appendChild(dom)
return dom
}
完整代码
使用示例
let radarInstance = new Radar({
pointsArr: [[120.61, 24], [100.61, -9]],
originCenter: [114.61, 14],
radarData: data
});
/**
* canvas 绘制雷达图\
* 需求一个雷达图 同时 需要 填充点、点击事件
* @written by liuqingQAQ on 2022/08/15.
*/
const defaultOptions = {
radarId: 'radar',
originCenter: [114.61, 14],
pointsArr: [[120.61, 24], [100.61, -9]]
}
export default class Radar {
constructor(options) {
let __options = Object.assign(defaultOptions, options);
this.radarId = __options.radarId
this.originCenter = __options.originCenter
this.pointsArr = __options.pointsArr
this.radarData = __options.radarData
console.log(this.originCenter, this.pointsArr);
this.pointStructs = []
this.scaleRatio = 0.9
this.createCanvas()
this.tooltipDom = this.createTooltip()
this.render()
// console.log(this);
}
destroy() {
let dom = document.getElementById(this.radarId)
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
dom.removeChild(this.canvas)
// tooltipDom 也要清除.
dom.removeChild(this.tooltipDom)
this.canvas = null;
}
/**
* 创建 图例.. 即点击弹窗后的 div 内容
*/
createTooltip() {
// 这里仅做创建. 因为无论是 输出的文本内容或者是 位置. 都需要配合点击事件 做处理.
let dom = document.createElement('div')
dom.style.display = 'none'
dom.style.color = 'red'
dom.style.fontSize = '20px'
dom.style.position = 'absolute'
dom.style.width = '500px'
dom.style.pointerEvents = 'none'
let fatherDom = document.getElementById(this.radarId)
fatherDom.style.position = 'relative'
fatherDom.appendChild(dom)
return dom
}
createCanvas() {
let dom = document.getElementById(this.radarId)
let canvas = document.createElement('canvas')
let ctx = canvas.getContext('2d')
canvas.height = dom.clientHeight
canvas.width = dom.clientWidth
canvas.onmousemove = this.hoverHandler.bind(this)
canvas.onclick = this.clickHandler.bind(this)
dom.appendChild(canvas)
this.centerPixel = this.calculateCenterPixel(dom.clientHeight, dom.clientWidth)
this.canvas = canvas
this.ctx = ctx
// 作为离屏 使用. 保存住 不会变的 点线 的图像状态
let screenCanvas = document.createElement('canvas')
let screenCtx = screenCanvas.getContext('2d')
screenCanvas.height = canvas.height
screenCanvas.width = canvas.width
this.screenCanvas = screenCanvas
this.screenCtx = screenCtx
}
/**
* 计算中心像素
* @param {*} height clientHeight
* @param {*} width clientWidth
* @returns [x,y]
*/
calculateCenterPixel(height, width) {
return [Math.round(width / 2), Math.round(height / 2)]
}
render() {
this.renderBase()
this.renderPoint()
let imageData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
this.screenCtx.putImageData(imageData, 0, 0)
this.startRadarSaomiao()
}
// haha .. lande fanyi
startRadarSaomiao() {
let deg = 0;
let _this = this
function animate() {
deg++;
_this.ctx.drawImage(_this.screenCanvas, 0, 0)
_this.cover()
_this.drawRadarPie(deg)
requestAnimationFrame(animate)
}
animate()
}
/**
* 创建底图
*/
renderBase() {
this.drawCircle()
this.drawText()
this.drawLine()
}
cover() {
let centerPixel = this.centerPixel
let ctx = this.ctx
let min = Math.min(centerPixel[0], centerPixel[1])
ctx.fillStyle = 'rgba(5,21,49,0.07)';
ctx.arc(centerPixel[0], centerPixel[1], min * this.scaleRatio, 0, 2 * Math.PI);
ctx.fill();
ctx.restore();
}
// 动画绘制.. 从最基础的 绘制扇形开始
drawRadarPie(iDeg) {
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
let ctx = this.ctx
ctx.fillStyle = 'rgba(117,253,159,.2)';
ctx.beginPath();
ctx.moveTo(centerPixel[0], centerPixel[1]);
ctx.arc(centerPixel[0], centerPixel[1], min * this.scaleRatio, (-2 * 20 + iDeg) / 180 * Math.PI, (0 + iDeg) / 180 * Math.PI);
ctx.closePath();
ctx.fill();
}
drawCircle() {
let ctx = this.ctx
let centerPixel = this.centerPixel
// 以更小的作处理. 确保 是 当前容器的最大正圆.
let min = Math.min(centerPixel[0], centerPixel[1])
let radius = min * this.scaleRatio;
let diff = radius / 6;
const PI2 = Math.PI * 2
ctx.strokeStyle = '#31474c'
ctx.beginPath()
ctx.arc(centerPixel[0], centerPixel[1], radius, 0, PI2)
ctx.stroke()
// 在大圆的基础下 往内收缩.
for (let i = 1; i < 7; i++) {
ctx.beginPath()
if(i == 3 || i == 6){
ctx.strokeStyle = '#fff'
}
ctx.arc(centerPixel[0], centerPixel[1], diff * i, 0, PI2)
ctx.stroke()
if(i == 3 || i == 6){
ctx.strokeStyle = '#31474c'
}
}
}
drawText() {
let ctx = this.ctx
let centerPixel = this.centerPixel
// 以更小的作处理. 确保 是 当前容器的最大正圆.
let min = Math.min(centerPixel[0], centerPixel[1])
let radius = min * this.scaleRatio;
let diff = radius / 6;
const PI2 = Math.PI * 2
let textArr = [0, 10, 20, 30, 40, 50, 60]
ctx.font = "20px Arial";
ctx.fillStyle = '#fff'
// 在大圆的基础下 往内收缩.
for (let i = 1; i < 7; i++) {
ctx.arc(centerPixel[0], centerPixel[1], diff * i, 0, PI2)
ctx.fillText(textArr[i], centerPixel[0] + diff * i, centerPixel[1] - diff / 10);
}
ctx.textAlign = "right";
for (let i = 1; i < 7; i++) {
ctx.arc(centerPixel[0], centerPixel[1], diff * i, 0, PI2)
ctx.fillText(textArr[i], centerPixel[0] - diff * i, centerPixel[1] - diff / 10);
}
}
drawLine() {
let ctx = this.ctx
let centerPixel = this.centerPixel
// 以更小的作处理. 确保 是 当前容器的最大正圆.
let min = Math.min(centerPixel[0], centerPixel[1])
let radius = min * this.scaleRatio;
let diff = radius / 4;
const PI2 = Math.PI * 2
// 绘制 十字线 先横后树.
ctx.strokeStyle = '#e8dfe1'
ctx.beginPath()
ctx.moveTo(centerPixel[0] - radius, centerPixel[1])
ctx.lineTo(centerPixel[0] + radius, centerPixel[1])
ctx.stroke();
ctx.closePath()
ctx.beginPath()
ctx.moveTo(centerPixel[0], centerPixel[1] - radius)
ctx.lineTo(centerPixel[0], centerPixel[1] + radius)
ctx.stroke();
ctx.closePath()
}
/**
* 渲染点.. 同时记录
*/
renderPoint() {
this.drawCenterPoint()
this.drawCurrentAxisPoint()
}
/**
* Draw the center point
*/
drawCenterPoint() {
let ctx = this.ctx
let centerPixel = this.centerPixel
// 以更小的作处理. 确保 是 当前容器的最大正圆.
let min = Math.min(centerPixel[0], centerPixel[1])
let radius = min * this.scaleRatio;
let diff = radius / 5;
const PI2 = Math.PI * 2
let pointSize = min / 100;
ctx.beginPath()
ctx.fillStyle = '#fff'
ctx.arc(centerPixel[0], centerPixel[1], pointSize, 0, PI2)
ctx.fill()
}
/**
* 绘制当前坐标系下的 点 .
*/
drawCurrentAxisPoint() {
let pointsArr = this.pointsArr
this.ctx.fillStyle = '#d3cfa1'
for (let i = 0; i < pointsArr.length; i++) {
let cur = pointsArr[i]
let pointStruct = this.calculateCurMeterPixel(cur)
let info = this.calculateDirectionAndDistance(cur)
let obj = {
x: pointStruct.x,
y: pointStruct.y,
lon: cur[0],
lat: cur[1],
radarBatchNum: this.radarData[i].radarBatchNum,
direction: info.direction,
distance: info.distance,
collectTime: this.radarData[i].collectTime,
course: this.radarData[i].course,
speed: this.radarData[i].speed
}
this.pointStructs.push(obj)
this.drawPointByPixel(obj)
}
this.ctx.font = "14px Arial"
this.ctx.textBaseline = "bottom"
this.ctx.textAlign = "left";
this.ctx.fillStyle = '#d3cfa1'
for (let i = 0; i < this.pointStructs.length; i++) {
let cur = this.pointStructs[i]
this.ctx.fillText(this.radarData[i].radarBatchNum, cur.x, cur.y)
}
}
// 正切tan转换角度
getTanDeg(tan) {
var result = Math.atan(tan) / (Math.PI / 180);
result = Math.round(result);
return result;
}
// 计算角度
ComputingAngle(x1, y1, x2, y2) {
let computedNum
let numDeg = this.getTanDeg(Math.abs((y1 - y2) / (x1 - x2)))
if (x1 - x2 < 0 && y1 - y2 < 0) {
computedNum = 90 - numDeg
}
else if (x1 - x2 > 0 && y1 - y2 < 0) {
computedNum = 270 + numDeg
}
else if (x1 - x2 > 0 && y1 - y2 > 0) {
computedNum = 270 - numDeg
}
else if (x1 - x2 < 0 && y1 - y2 > 0) {
computedNum = 90 + numDeg
}
else if (x1 - x2 == 0) {
if (y1 - y2 < 0) {
computedNum = 0
} else if (y1 - y2 > 0) {
computedNum = 180
}
}
else if (y1 - y2 == 0) {
if (x1 - x2 < 0) {
computedNum = 90
} else if (x1 - x2 > 0) {
computedNum = 270
}
}
return computedNum;
}
// 计算方位 跟距离.
calculateDirectionAndDistance(cur) {
let originCenter = this.originCenter
// 将当前中心点 偏移回原点. 得到偏移量
let offset = [-originCenter[0], -originCenter[1]]
let xy = this.vector2Add(offset, cur)
let direction = this.ComputingAngle(originCenter[0], originCenter[1], cur[0], cur[1])
let distance = Math.pow(Math.pow(xy[0] * 111, 2) + Math.pow(xy[1] * 111, 2), 0.5).toFixed(2)
return {
direction,
distance
}
}
/**
* 根据原始点计算 该渲染的canvas像素坐标
* @param {*} currentLonLat 经纬度数组
*/
calculateCurLonlatPixel(currentLonLat) {
// 绘制 以 originlonlatCenter 作为原点的 坐标系下的 点
let originCenter = this.originCenter
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
// 总厂
let allLen = 2
// 将当前中心点 偏移回原点. 得到偏移量
let offset = [-originCenter[0], -originCenter[1]]
let xy = this.vector2Add(offset, currentLonLat)
let xRatio = xy[0] / allLen * this.scaleRatio
let yRatio = xy[1] / allLen * this.scaleRatio
let xValue = min * xRatio + centerPixel[0]
// 取反的原因:
// canvas 坐标系 向下增加. 经纬度表示 y 轴向上增加.
let yValue = centerPixel[1] - min * yRatio
return {
x: xValue,
y: yValue
}
}
calculateCurMeterPixel(currentLonLat) {
// 绘制 以 originlonlatCenter 作为原点的 坐标系下的 点
let originCenter = this.originCenter
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
// 总厂
let allLen = 60
// 将当前中心点 偏移回原点. 得到偏移量
let offset = [-originCenter[0], -originCenter[1]]
let xy = this.vector2Add(offset, currentLonLat)
// degree = meter / (Math.PI * 6371004) * 180;
//在经线上,纬度每差1度,实地距离大约为111千米;
// 在纬线上,经度每差1度,实际距离为111×cosθ千米。(其中θ表示该纬线的纬度.在不同纬线上,经度每差1度的实际距离是不相等的)。
// to meter
// const degree = (Math.PI * 6371004)
const lonK = 111
// 求得比例.
let xRatio = xy[0] * lonK / allLen * this.scaleRatio
let yRatio = xy[1] * lonK / allLen * this.scaleRatio
let xValue = min * xRatio + centerPixel[0]
console.log(xRatio, yRatio);
// 取反的原因:
// canvas 坐标系 向下增加. 经纬度表示 y 轴向上增加.
let yValue = centerPixel[1] - min * yRatio
return {
x: xValue,
y: yValue
}
}
drawPointByPixel(point) {
let ctx = this.ctx
ctx.beginPath()
ctx.arc(point.x, point.y, 6, 0, Math.PI * 2)
ctx.fill()
}
vector2Add(v1, v2) {
return [v1[0] + v2[0], v1[1] + v2[1]]
}
/**
* 为点生成点击事件.
*/
clickHandler(e) {
let x = (e.clientX - this.canvas.getBoundingClientRect().left) / __XRATIO
let y = (e.clientY - this.canvas.getBoundingClientRect().top) / __YRATIO
let info = this.checkPixelIsPoint({ x, y })
if (info.isPoint) {
console.log(info.pointStruct);
this.tooltipDom.style.top = y + 'px'
this.tooltipDom.style.left = x + 'px'
this.tooltipDom.innerHTML = `
<span>经纬度:${info.pointStruct.lon},${info.pointStruct.lat}</span><br />
<span>时间:${info.pointStruct.collectTime}</span><br />
<span>方位:${info.pointStruct.direction}°</span><br />
<span>距离:${info.pointStruct.distance}km</span><br />
<span>速度:${info.pointStruct.speed}节</span><br />
<span>航向:${info.pointStruct.course}°</span><br />
`
this.tooltipDom.style.display = 'block'
// console.log(info.pointStruct);
}
}
hoverHandler(e) {
let x = (e.clientX - this.canvas.getBoundingClientRect().left) / __XRATIO
let y = (e.clientY - this.canvas.getBoundingClientRect().top) / __YRATIO
let info = this.checkPixelIsPoint({ x, y })
var body = document.querySelector("body")
if (info.isPoint) {
body.style.cursor = "pointer"
} else {
body.style.cursor = ""
}
}
// 检测当前坐标是否为处理点
checkPixelIsPoint(pointStruct) {
// 点本身 具有大小, 会影响结果. 因此 应该是计算 该pixel 落入的范围.
// 这里做的检测机制为 包裹住圆点 的正方形盒子. 不完全精确 但应该够用.
// 目前为写死的 半径为4的小圆点.
let radius = 6
let pointStructs = this.pointStructs
let checkObj = {
isPoint: false,
pointStruct: null
}
let centerPixel = this.centerPixel
let min = Math.min(centerPixel[0], centerPixel[1])
let pointSize = min / 100;
for (let i = 0; i < pointStructs.length; i++) {
let element = pointStructs[i]
let leftBottom = [element.x - radius, element.y - radius];
let rightTop = [element.x + radius, element.y + radius];
// 在范围内. 说明 该点 命中 检测.
if ((leftBottom[0] < pointStruct.x && pointStruct.x < rightTop[0]) && (leftBottom[1] < pointStruct.y && pointStruct.y < rightTop[1])) {
checkObj.pointStruct = element
checkObj.isPoint = true
return checkObj
}
}
return checkObj
}
//
containsExtent(pixel) {
console.log();
}
}