微信小程序手绘地图实现之《Canvas》
环境:微信SDK2.9+ + uniapp (可切换直接使用.map.js不限制环境)
正题:
先创建一个地图组件
<template>
<view class="customCanvasComponent">
<!-- 建立画布坐标系 -->
<canvas
:style="{
width: `${options.style.width}rpx`,
height: `${options.style.height}rpx`,
border: options.style.border,
background: options.style.background
}"
type="2d"
:id="customMapId"
:canvas-id="customMapId"
@click="clickToCanvas"
@touchstart="touchStartToCanvas"
@touchmove="touchMoveToCanvas"
@touchend="touchEndToCanvas">
<!-- 由于微信限制 暂时只支持这种写法 请不要秀其他方式 否则凉凉 -->
<!-- Marker点集合 -->
<!-- <blank v-for="poi in handlerMarkerList" :key="poi.id">
<cover-view
class="point"
@click="pointChange(poi)"
:style="{
position: 'absolute',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
left: poi.x + 'px',
top: poi.y + 'px',
transform: `translate(-50%, -100%)`
}">
<cover-image :style="poi.stringStyle" :src="poi.icon"></cover-image>
<cover-view class="labelView" :style="poi.stringLabelStyle">
<cover-view class="labelTitle">{{poi.label}}</cover-view>
</cover-view>
</cover-view>
</blank> -->
<!-- WindowInfo窗体设置 -->
<blank v-if="checkPointMarker">
<cover-view class="windowInfoGroupBox" :style="{
position: 'absolute',
left: checkPointMarker.x + 'px',
top: checkPointMarker.y + 'px',
transform: `translate(-50%, calc(-100% - 90rpx))`
}">
<cover-view class="infoTitle">
<cover-view class="infoVoiceBtn">
<cover-image class="infoImage" :src="checkPointMarker.image"></cover-image>
<cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_play@2x.png"></cover-image>
<cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_pause@2x.png"></cover-image>
</cover-view>
<cover-view class="infoContent">
<cover-view class="title otext2"></cover-view>
<cover-view class="distance"></cover-view>
</cover-view>
</cover-view>
<cover-view class="btnTools">
<cover-view class="btn">
<cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_voice@2x.png"></cover-image>
<cover-view class="btnText">解说</cover-view>
</cover-view>
<cover-view class="btn">
<cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_info@2x.png"></cover-image>
<cover-view class="btnText">详情</cover-view>
</cover-view>
</cover-view>
</cover-view>
</blank>
<!-- 预留控件 由于小程序限制机制 请使用时仅可使用顶级标签<cover-view><cover-image> -->
<!-- 默认返回处理后的Marker点集合 -->
<!-- ControlFirmware Left -->
<slot name="control-l"/>
<!-- ControlFirmware Right -->
<slot name="control-r"/>
<!-- ControlFirmware Top -->
<slot name="control-t"/>
<!-- ControlFirmware Bottom -->
<slot name="control-b"/>
<!-- 其他控件预留 -->
<slot name="other"/>
<!-- <cover-view class="toolsBox">
<cover-view class="pointGroupBox">
<blank>
<cover-view v-for="poi in handlerMarkerList" :key="poi.id" class="point" :style="{position: 'absolute', left: poi.x + 'px', top: poi.y + 'px'}">
<cover-image :style="{...poi.style}" :src="poi.icon"></cover-image>
<cover-view class="labelView" :style="{...poi.labelStyle}">
<cover-view class="labelTitle">{{poi.label}}</cover-view>
</cover-view>
</cover-view>
</blank>
</cover-view>
<cover-view class="windowInfoGroupBox">
测试
<cover-image style="" src="/static/images/scenic/tour_voice_poi_01@2x.png"></cover-image>
</cover-view>
</cover-view> -->
</canvas>
<!-- 建立与画布对应的平面坐标系 -->
</view>
</template> <script>
import CustomCavnasMap from './map'
let CustomMapInital = null
export default {
// 组件配置说明 必须基于某个地图提供商进行的适配 高德 百度 腾讯 谷歌
// 这里使用高德
props: {
// 部分配置参数
options: {
type: Object,
default: () => {
return {
// 样式层
style: {
// 宽高单位均为rpx
width: 750,
height: 1334,
// 背景支持色值或者网络图片背景图
background: 'pink',
border: 'none'
},
// 坐标中心点 LngLat对象
center: [113.9120864868165, 22.545537650869],
// 地图范围 [LngLat, LngLat] 取点应为对角两个坐标 !!!注意坐标点位置 [右上<RT>, 左下<LB>]
limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],
// 初始化地图层级
initalZoom: 16,
// 地图层级范围
zooms: [16, 18],
// 图层
layers: [
{
// 图片覆盖物 坐标范围 !!!注意坐标点位置 [右上<RT>, 左下<LB>]
limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],
// 覆盖物地址
image: 'https://xxx/static/map-bg.jpeg',
// 透明度
opacity: 1,
// 缩放范围
zooms: [16, 19]
}
],
// 路线
lineStyle: {
lineWidth: 5,
lineColor: 'red',
lineArray: []
},
// 自定义Marker
markers: [
{
icon: '/static/images/scenic/tour_voice_poi_01@2x.png',
position: [113.9128,22.544674],
style: {
width: '93rpx',
height: '105rpx',
position: 'relative',
top: '60rpx'
},
label: '(内测)城管大楼',
labelStyle: {
position: 'relative',
top: '-90rpx',
left: '50%',
transform: 'translateX(-50%)',
background: '#FFF',
padding: '5rpx 10rpx',
fontSize: '28rpx'
}
},
{
icon: '/static/images/scenic/tour_voice_poi_01@2x.png',
position: [113.911765,22.545397],
style: {
width: '93rpx',
height: '105rpx',
position: 'relative',
top: '60rpx'
},
label: '(内测)凉亭',
labelStyle: {
position: 'relative',
top: '-90rpx',
left: '50%',
transform: 'translateX(-50%)',
background: '#FFF',
padding: '5rpx 10rpx',
fontSize: '28rpx'
}
}
]
}
}
},
// canvasId
customMapId: {
type: String,
default: 'customMap'
}
},
data () {
return {
// initalZoom: null,
// CustomMapInital: null, // 不要定义到data中 容易引发内存互换
handlerMarkerList: [],
checkPointMarker: null
}
},
watch: {
'options.lineStyle.lineArray': {
handler (_new, _old) {
if (_new !== _old) {
this.drawLine(_new)
}
},
deep: true
}
},
methods: {
initalCanvasMap () {
// console
CustomMapInital = new CustomCavnasMap({
customMapId: this.customMapId,
_component: this
}, Object.assign({}, this.options, {
markerCallBack: (list) => {
console.log(list)
this.handlerMarkerList = list
},
cilckPointChange: (info) => {
if (info) {
console.log(info)
console.log('得到点击成功后的触发')
this.pointChange(info)
} else {
console.log('得到点击空白的回调')
}
}
}))
},
fetchCustomBoxSize () {
nui.getImageInfo({
src: '',
success: (rect) => {
console.log(rect.fillPath[0])
}
})
},
/**
* @Function
* @public 公共类方法
* @return Object
*/
// 设置缩放比例
setZoom (zoom, callback) {
// 最低限制为初始化的缩放比例
if (zoom > this.options.initalZoom) {
// 逻辑处理
CustomMapInital.setZoom(this.initalZoom, callback)
} else {
CustomMapInital.setZoom(zoom, callback)
}
},
// 获取缩放比例
getZoom (callback) {
if (callback) {
callback && callback(CustomMapInital.getZoom())
} else {
return CustomMapInital.getZoom()
}
},
/**
*
* @touch 事件向this.CustomMapInital触发
*/
touchStartToCanvas (e) {
CustomMapInital.touchStartToCanvas(e)
},
touchMoveToCanvas (e) {
CustomMapInital.touchMoveToCanvas(e)
},
touchEndToCanvas (e) {
CustomMapInital.touchEndToCanvas(e)
},
/**
* @click 事件向下触发
*/
clickToCanvas (e) {
CustomMapInital.clickToCanvas(e)
// 点击其他地方进行清空WindowInfo窗体
this.checkPointMarker = null
},
/**
* @param {info<Object>} 类型为Marker数据对象
*/
pointChange (info) {
this.checkPointMarker = info
},
/**
* @param {lineArray<Array|Object>} 传入的线路数据
* @param {Object} {longitude, latitude} 必须
*/
drawLine (lineArray) {
CustomMapInital.drawLine(CustomMapInital.LngLatConversionToPixel(lineArray))
}
},
onReady () {
this.initalCanvasMap()
},
onUnload () {
CustomMapInital = null
}
}
</script> <style lang="sass" scoped>
$defaultBg: #FFF
$bgF4: #F4F4F4
$color3: #333
$color6: #666
$color9: #999
// $defaultBg: pink
// 取消默认样式
cover-view
overflow: initial !important
.customCanvasComponent
// .toolsBox
// position: absolute
.point
position: absolute
z-index: -1
display: flex
flex-direction: column
align-items: center
.labelView
border-radius: 10rpx
background-color: $defaultBg
.labelTitle
font-size: 28rpx
.windowInfoGroupBox
background-color: $defaultBg
border-radius: 10rpx
width: 320rpx
height: 228rpx
box-shadow: 10rpx 10rpx 20rpx -10rpx $color6
display: flex
flex-direction: column
z-index: 99
.infoTitle
display: flex
align-items: center
padding: 20rpx
.infoVoiceBtn
width: 120rpx
height: 120rpx
flex: 0 0 120rpx
border: 1px solid $bgF4
border-radius: 50%
overflow: hidden
position: relative
cover-image
width: 100%
height: 100%
object-fit: contain
.playControl
position: absolute
width: 68rpx
height: 68rpx
top: 50%
left: 50%
transform: translate(-50%, -50%)
.infoContent
flex: 1
margin-left: 20rpx
.title
font-size: 28rpx
line-height: 28rpx
min-height: 56rpx
color: $color3
font-weight: bold
// margin-right: 58rpx
overflow: inherit
.distance
font-size: 22rpx
color: $color9
// margin-right: 0.58rem
margin-top: 10rpx
.btnTools
display: flex
flex: 1
.btn
flex: 0 0 calc(50% - 40rpx)
display: flex
margin: 0 20rpx 15rpx 20rpx
align-items: center
justify-content: center
border-radius: 30rpx
cover-image
width: 30rpx
height: 30rpx
.btnText
color: $defaultBg
font-size: 28rpx
.btn:nth-child(1)
background: #80D2FC
background: linear-gradient(#80D2FC, #188EE9)
background: linear-gradient(to right, #80D2FC, #188EE9)
.btn:nth-child(2)
background: #FBA326
background: linear-gradient(#FBA326, #FBA326)
background: linear-gradient(to right, #FBA326, #FBA326)
</style>
.map.js
module.exports = class CustomCavnasMap {
canvasContext = null
// 定义背景装载图
layersImages = []
// 初始化Lock锁超出最大值停止初始化
initLock = 0
maxLockValue = 1000
// 记录手指按下时的坐标 以及位置
startingCoordinate = null
// 旋转时中心点或者缩放时中心点 默认为画布起点
rotateCenter = {
x: 0,
y: 0
}
// 背景图的偏移量
offsetConfig = {
mapX: 0,
mapY: 0
}
// 捏合缩放倍数或者滚轮缩放倍数
mapScale = 1
// 捏合缩放状态
mapZoom = false
// 双指旋转角度地图旋转角度
mapRotate = 0
// 两指距离
mapDistance = 0
// 地图层级限制 最大值 默认两倍
mapMaxZoom = 2
// 地图层级限制 最小值 默认一倍
mapMinZoom = 1
// 惯性的运动距离 带方向的距离单位
inertialMotion = {
x: 0,
y: 0
}
// 新增拖拽惯性支持 摩擦系数μs 范围应该在0-1之间
us = 0.9
// 惯性定时器
inertialMotionTimer = null
COMPUT_TIME = null
// 图片预加载对象
pictureExtractionObject = {}
// 点击Canvas后的点位
clickPoint = {
x: 0,
y: 0
}
// 点击触发后的状态 0未点击 1点击了 2点击了但是点击错了
clickStatus = 0
/**
* @methods
* @param {Object<customMapId,_component>} canvasOtions 画布对象
* @param {Object<style,center,limitBounds,initalZoom,layers>} options 地图参数管控
*/
constructor(canvasOtions, options) {
// super(this)
console.log('进入构造函数-->')
// Object.keys(options)
// 获取设备属性
this.asyncFetchSystemInfo()
// this.systemInfo = wx.getSystemInfoSync()
// 属性继承
Object.assign(this, canvasOtions, options)
// 手动处理范围值
this.zooms && (this.mapMaxZoom = this.zooms[1] - (this.initalZoom || this.zooms[0])) && (this.mapMinZoom = (this.zooms[0] - this.initalZoom) || 1)
console.log('当前限制范围为:' + this.mapMinZoom + '-' + this.mapMaxZoom)
// if (canvasOtions instanceof Object) {
// this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId, canvasOtions._component)
// } else {
// this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId)
// }
// 设置分辨率
// this.dpr = 1
// 设置画布实际大小
// this.canvasOptions = {
// width: parseInt(this.rpxToPx(options.style.width) * this.dpr),
// height: parseInt(this.rpxToPx(options.style.height) * this.dpr)
// }
// 获取Canvas节点元素
this.wxCreateSelectorQuery().select(`#${canvasOtions.customMapId}`).fields({
node: true,
rect: true
}, res => {
// console.log(res)
this.customCanvas = res.node
// this.computedConversionData()
// this.createMapBGImage(rect.node)
this.dpr = this.systemInfo.pixelRatio // this.dpr = 1
// 设置大小
this.customCanvas.width = parseInt(this.rpxToPx(options.style.width) * this.dpr)
this.customCanvas.height = parseInt(this.rpxToPx(options.style.height) * this.dpr)
// 获取画布context上下文 2d
this.ctxCanvas = this.customCanvas.getContext('2d')
// 获取画布context上下文 webgl
// this.glCanvas = this.customCanvas.getContext('webgl')
// console.log(this.customCanvas)
}).exec()
// 开始初始化自定义地图
this.initalCanvasChange()
}
// 初始化Canvas画布对象
initalCanvasChange() {
if (this.customCanvas) {
this.computedConversionData()
} else {
setTimeout(() => {
console.log('设置延迟100ms进行渲染Canvas画布')
this.initLock++
this.initLock < this.maxLockValue && this.initalCanvasChange()
}, 100)
}
}
// 提供选择节点的公共方法
wxCreateSelectorQuery() {
if (this._component) {
return wx.createSelectorQuery().in(this._component)
} else {
return wx.createSelectorQuery()
}
}
// 计算两点坐标实际距离公式
GetDistance(LngLat1, LngLat2) {
var radLat1 = LngLat1[1] * Math.PI / 180.0
var radLat2 = LngLat2[1] * Math.PI / 180.0
var a = radLat1 - radLat2
var b = LngLat1[0] * Math.PI / 180.0 - LngLat2[0] * Math.PI / 180.0
var s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)))
s = s * 6378.137 // EARTH_RADIUS
s = Math.round(s * 10000) / 10000
return s
}
// 顺序构建map图库
createMapBGImage() {
// 清空页面绘制 2d
this.ctxCanvas.clearRect(0, 0, this.customCanvas.width, this.customCanvas.height) // 绘制canvas背景颜色
// this.ctxCanvas.fillStyle = this.style.background
// this.ctxCanvas.fillRect(0, 0, this.customCanvas.width, this.customCanvas.height)
// this.canvasContext.clearRect(0, 0, this.canvasOptions.width, this.canvasOptions.height)
// this.glCanvas.clear(this.glCanvas.COLOR_BUFFER_BIT)
// console.log(this.rotateCenter)
// 设置旋转中心点
this.ctxCanvas.translate(this.rotateCenter.x, this.rotateCenter.y)
// 对画布进行旋转 暂时关闭旋转
// this.ctxCanvas.rotate(this.mapRotate * Math.PI / 180)
// 当绘制结束后 还原旋转中心点
this.ctxCanvas.translate(-this.rotateCenter.x, -this.rotateCenter.y)
this.ctxCanvas.save()
// 循环进行处理图片 缩放 平移控制
this.layersImages.map(img => {
// console.log(img)
// 设置图片透明度
this.ctxCanvas.globalAlpha = img.opacity
this.ctxCanvas.drawImage(img, 0, 0, img.width, img.height, this.canvasLimitConfig.offsetLeft + this.offsetConfig.mapX * this.dpr, this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr, this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr, this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr)
this.ctxCanvas.restore()
})
// 清除旋转角度
// this.ctxCanvas.rotate(this.mapRotate)
this.mapRotate = 0
// console.log('绘画完成')
// this.ctxCanvas.restore()
this.ctxCanvas.save()
this.COMPUT_TIME = new Date().getTime()
console.log('开始计算坐标点:' + this.COMPUT_TIME)
// 计算点
this.drawMarker(this.markers)
}
// 绘制Marker景点 传入参数MarkerList对象
drawMarker(infoList = []) {
// console.log(infoList)
if (infoList instanceof Array && infoList.length > 0) {
// 计算之前 先得到图标
if (Object.keys(this.pictureExtractionObject).length > 0) {
// 开始绘制
// 使用定位解决方案 避免canvas数据量过大造成卡顿 [定位方案更卡。。。]
// this.LngLatToPixel()
this.handlerMarkerList = infoList.map((item, index) => {
item.stringStyle = ''
Object.keys(item.style).map(key => {
item.stringStyle += `${key}: ${item.style[key]};`
})
item.stringLabelStyle = ''
Object.keys(item.labelStyle).map(key => {
item.stringLabelStyle += `${key}: ${item.labelStyle[key]};`
})
return Object.assign(item, this.LngLatToPixel(item.position), {id: index})
})
// 创建ICON图标
this.handlerMarkerList.map(item => {
this.ctxCanvas.beginPath()
this.ctxCanvas.arc(item.canvasX, item.canvasY, 5, 0, 2 * Math.PI)
this.ctxCanvas.strokeStyle = 'red'
this.ctxCanvas.fillStyle = 'pink'
this.ctxCanvas.fill()
this.ctxCanvas.stroke()
this.ctxCanvas.restore()
const w = this.rpxToPx(parseInt(item.style.width)) * this.dpr
const h = this.rpxToPx(parseInt(item.style.height)) * this.dpr
this.ctxCanvas.drawImage(this.pictureExtractionObject[item.icon], item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h)
this.ctxCanvas.restore()
this.ctxCanvas.rect(item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h)
const clickPointX = this.clickPoint.x * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr + this.canvasLimitConfig.offsetLeft
const clickPointY = this.clickPoint.y * this.mapScale * this.dpr + this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop if (this.clickStatus !== 0) {
if (this.ctxCanvas.isPointInPath(clickPointX, clickPointY)) {
this.cilckPointChange(item)
this.clickStatus = 1
console.log('成功触发画布点击回调')
} else {
console.log('点位错误')
}
}
})
if (this.clickStatus === 2) {
// 触发未点中的回调
this.cilckPointChange()
}
// console.log(this.handlerMarkerList)
const END_TIME = new Date().getTime() console.log('计算结束:' + (END_TIME - this.COMPUT_TIME))
this.markerCallBack(this.handlerMarkerList)
} else {
setTimeout(() => {
this.drawMarker(infoList)
}, 100)
}
}
}
LngLatConversionToPixel (LngLatArray = []) {
if (LngLatArray instanceof Array && LngLatArray.length > 0) {
return LngLatArray.map((item, index) => {
return Object.assign(item, this.LngLatToPixel([item.longitude, item.latitude]), {id: index})
})
}
}
// 绘制线路
drawLine(LinePathArray = []) {
if (LinePathArray instanceof Array && LinePathArray.length > 0) {
// 设置绘制样式
this.ctxCanvas.strokeStyle = this.lineStyle.lineColor || '#000000'
this.ctxCanvas.lineWidth = this.lineStyle.lineWidth || 5
// 开始绘制
LinePathArray.map((line, index) => {
if (index === 1) {
this.ctxCanvas.moveTo(line.x, line.y)
} else {
this.ctxCanvas.lineTo(line.x, line.y)
}
})
this.ctxCanvas.stroke()
// 绘制结束
// 保存一次
this.ctxCanvas.save()
}
}
// 取中心点方法
Vector(vector1, vector2) {
this.x = vector2.x - vector1.x
this.y = vector2.y - vector1.y
}
// 计算点乘 => 公式:a↑ * b↑ = |a↑||b↑|cosθ
// 其中:a↑ * b↑ = x1*x2 + y1*y2
// 模计算:|a↑| = Math.sqrt(x1 ** 2 + y1 ** 2)
calculateVM(vector1, vector2) {
return (vector1.x * vector2.x + vector1.y * vector2.y) / (Math.sqrt(vector1.x ** 2 + vector1.y ** 2) * Math.sqrt(vector2.x ** 2 + vector2.y ** 2))
}
// 计算叉乘
calculateVC(vector1, vector2) {
return (vector1.x * vector2.y - vector2.x * vector1.y) > 0 ? 1 : -1
}
// 获取系统信息
asyncFetchSystemInfo() {
this.systemInfo = wx.getSystemInfoSync()
}
// rpx转px
rpxToPx(v) {
return v / 750 * this.systemInfo.windowWidth
}
// 初始化需要计算的所有数据
computedConversionData() {
// 排序提取背景覆盖物的值
this.handlerImages = this.layers.map(item => {
!item.zIndex && (item.zIndex = 100)
return item
}).sort((a, b) => {
return a.zIndex - b.zIndex
}).filter(fs => fs) // 对角坐标计算 => 转成4个 [LT, RT, RB, LB] 顺时针顺序
if (this.limitBounds.length === 2) {
this.mapCanvasBoxLngLats = [
[this.limitBounds[1][0], this.limitBounds[0][1]],
this.limitBounds[0],
[this.limitBounds[0][0], this.limitBounds[1][1]],
this.limitBounds[1]
]
// 得到转化后的坐标进行计算实际距离
const width = this.GetDistance(this.mapCanvasBoxLngLats[0], this.mapCanvasBoxLngLats[1])
const height = this.GetDistance(this.mapCanvasBoxLngLats[1], this.mapCanvasBoxLngLats[2])
const viewWidth = this.rpxToPx(this.style.width || 750)
const viewHeight = parseInt(height * viewWidth / width) this.canvasLimitConfig = {
proportionX: viewWidth / width,
proportionY: viewHeight / height,
width,
height,
viewWidth,
viewHeight,
offsetTop: parseInt(Math.abs((this.customCanvas.height / this.dpr - viewHeight) / 2)),
offsetLeft: parseInt(Math.abs((this.customCanvas.width / this.dpr - viewWidth) / 2))
}
} // 图片加载处理
this.handlerImages.map(item => {
const img = this.customCanvas.createImage()
img.onload = (e) => {
// console.log('已成功加载图片---->')
// 设置附件值
Object.assign(img, item)
this.layersImages.push(img)
this.createMapBGImage()
// console.log('设置图片完成')
}
img.onerror = (e) => {
console.log(e)
img.src = item.image
}
img.src = item.image
})
// ICON预加载
this.pictureExtraction(this.markers, 'icon').map(item => {
const image = this.customCanvas.createImage()
image.onload = (e) => {
this.pictureExtractionObject[item] = image
}
image.onerror = (e) => {
image.src = item
}
image.src = item
})
}
/**
* 其他辅助类函数
* @method deepClone 深度克隆
* @param {Any} Any 任意类型
*
* 对一个object进行深度拷贝
*
* 使用递归来实现一个深度克隆,可以复制一个目标对象,返回一个完整拷贝
* 被复制的对象类型会被限制为数字、字符串、布尔、日期、数组、Object对象。不会包含函数、正则对象等
*
* @param {Object} ObjectSource 需要进行拷贝的对象
*/
deepClone(ObjectSource) {
if (Array.isArray(ObjectSource)) {
return Object.assign([], ObjectSource)
}
return Object.assign({}, ObjectSource)
}
/**
*
* @param {Array<Object>} imageArray 传入数组遍历对象
* @param {String} name 需要指定去重的数据名称
* @return {Array} 返回的是去重后的Image数组
*/
pictureExtraction (imageArray, name) {
let cloneImageObject = {}
imageArray.map(item => {
cloneImageObject[item[name]] = item[name]
})
return Object.keys(cloneImageObject)
}
/**
* @touch 事件处理
* @param {Event} e Event对象
*/
touchStartToCanvas(e) {
// 操作开始时 清空处理
this.inertialMotionTimer && clearInterval(this.inertialMotionTimer)
// 多指处理
if (e.touches.length > 1) {
// 属于多指操作类型
console.log('当前属于多指操作')
// console.log(e)
// 计算并存储数据
const xMove = e.touches[1].x - e.touches[0].x
const yMove = e.touches[1].y - e.touches[0].y
// 计算两指距离
this.mapDistance = Math.sqrt(xMove ** 2 + yMove ** 2)
this.thisCoordinate = e.touches
this.startingCoordinate = e.touches
this.mapZoom = true
} else {
this.startingCoordinate = e.touches[0]
// 初始化惯性速度
this.inertialMotion = {
x: 0,
y: 0
}
}
}
touchMoveToCanvas(e) {
if (e.touches.length > 1) {
// 属于多指操作类型
console.log('当前属于多指操作')
this.mapZoom = true
// 计算旋转
const preCoordinate = this.deepClone(this.startingCoordinate)
this.startingCoordinate = e.touches
const vector1 = new this.Vector(preCoordinate[0], preCoordinate[1])
const vector2 = new this.Vector(this.startingCoordinate[0], this.startingCoordinate[1])
const resultCosVal = this.calculateVM(vector1, vector2)
// 弧度换算成角度
const angle = Math.acos(resultCosVal) * 180 / Math.PI const direction = this.calculateVC(vector1, vector2)
// 得到最后的旋转度数
const _allDeg = direction * angle // 双指缩放
const xMove = e.touches[1].x - e.touches[0].x
const yMove = e.touches[1].y - e.touches[0].y // 取中心点
const posCenter = this.rotateCenter = {
x: (e.touches[0].x + e.touches[1].x) / 2,
y: (e.touches[0].y + e.touches[1].y) / 2
} const distance = Math.sqrt(xMove ** 2 + yMove ** 2)
const distanceDiff = distance - this.mapDistance
const scalingIndex = 0.005 * distanceDiff
const newScale = this.mapScale + scalingIndex let mapX = this.offsetConfig.mapX
let mapY = this.offsetConfig.mapY const scaleSizeX = scalingIndex * this.canvasLimitConfig.viewWidth * this.mapScale
const scaleSizeY = scalingIndex * this.canvasLimitConfig.viewHeight * this.mapScale mapX -= scaleSizeX / 2
mapY -= scaleSizeY / 2
console.log('多指') if (Math.abs(_allDeg) > 1) {
this.mapRotate = this.mapRotate + _allDeg
// 重绘
this.createMapBGImage()
}
// 限制范围 不存在mapX mapY时出现计算错误时退出当前缩放
if (newScale < this.mapMinZoom || newScale > this.mapMaxZoom || isNaN(mapX) || isNaN(mapY)) {
return
}
this.mapDistance = distance
this.mapScale = newScale
this.offsetConfig.mapX = mapX
this.offsetConfig.mapY = mapY
// 重绘
this.createMapBGImage()
} else {
// slidingDistanceX
// const offsetX =
// 不处理在双指或者多指情况下的剩余操作
if (this.mapZoom) {
return
}
// 判断是否为数组
if (this.startingCoordinate instanceof Array) {
this.startingCoordinate = this.startingCoordinate[0]
}
const thisCoordinate = e.touches[0]
const slidingDistanceX = thisCoordinate.x - this.startingCoordinate.x
const slidingDistanceY = thisCoordinate.y - this.startingCoordinate.y this.offsetConfig.mapX += slidingDistanceX
this.offsetConfig.mapY += slidingDistanceY
// 处理速度
this.inertialMotion = {
x: slidingDistanceX || 0,
y: slidingDistanceY || 0
}
// console.log(this.inertialMotion)
console.log('单指')
// console.log(this.inertialMotion.x, this.inertialMotion.y)
// 处理边界
this.touchMoveLimitBounds() // 重新设置初始点
this.startingCoordinate = thisCoordinate
// 重绘
this.createMapBGImage()
}
}
touchEndToCanvas(e) {
// console.log(e)
if (e.touches.length === 0) {
// 处理惯性
!this.mapZoom && this.inertialMotionToCanvas(this.inertialMotion.x, this.inertialMotion.y)
this.mapZoom = false
// 如果初始大小 则复位
if (this.mapScale === 1) {
// this.offsetConfig = {
// mapX: 0,
// mapY: 0
// }
// 重绘
// this.createMapBGImage()
}
// 处理用户多指操作 抬起某一手指 应进行删除控制
// e.touches.map(item => { // })
} else {
// console.log(e)
this.mapZoom = false
}
}
touchMoveLimitBounds() {
// 处理边界问题
// X 轴
if ((this.offsetConfig.mapX + this.canvasLimitConfig.offsetLeft + this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) > this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) {
this.offsetConfig.mapX = this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr - this.canvasLimitConfig.offsetLeft - this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr
} else if ((this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr) < this.customCanvas.width) {
this.offsetConfig.mapX = (this.customCanvas.width - this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) / this.dpr
}
// Y 轴
if (this.customCanvas.height > this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) {
if ((this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) < (this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop)) {
this.offsetConfig.mapY = (this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr - this.canvasLimitConfig.offsetTop) / this.dpr
} else if ((this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr) < 0) {
this.offsetConfig.mapY = (0 - this.canvasLimitConfig.offsetTop) / this.dpr
}
} else {
if ((this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) > (this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop)) {
this.offsetConfig.mapY = (this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr - this.canvasLimitConfig.offsetTop) / this.dpr
} else if ((this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr) > 0) {
this.offsetConfig.mapY = (0 - this.canvasLimitConfig.offsetTop) / this.dpr
}
}
}
/**
* 处理拖动惯性运动
* @param {Number} speedX X轴的速度
* @param {Number} speedY Y轴的速度
* @handler Canvas 处理函数
*/
inertialMotionToCanvas(speedX, speedY) {
if (isNaN(speedX) || isNaN(speedY)) return
this.inertialMotionTimer && clearInterval(this.inertialMotionTimer)
this.inertialMotionTimer = setInterval(() => {
speedX *= this.us
speedY *= this.us
this.offsetConfig.mapX += speedX
this.offsetConfig.mapY += speedY
// 处理边界
this.touchMoveLimitBounds()
if (Math.abs(speedX) < 1) speedX = 0
if (Math.abs(speedY) < 1) speedY = 0
if (speedX == 0 && speedY == 0) {
this.inertialMotion = {
x: 0,
y: 0
}
clearInterval(this.inertialMotionTimer)
}
// 重绘
this.createMapBGImage()
}, 30)
} /**
* @click 事件处理
* @param {Event} e Event对象
*/
clickToCanvas(e) {
// 假设没点中
this.clickStatus = 2
this.clickPoint = {
x: e.target.x - e.target.offsetLeft,
y: e.target.y - e.target.offsetTop
}
this.createMapBGImage()
}
/**
* 坐标换算
* P (a)
* D ┍━━━━━━━┳━━━━━━━━┒ A
* ╲ ┃ ╱
* ╲ ┃ ╱
* ╲ ┃h ╱
* c ╲ ┃ ╱ b
* ╲ ┃ ╱
* ╲ ┃ ╱
* ╲┃ ╱
* ┻ O
* 从地图坐标系到物理坐标戏
* @methods LngLatToPixel {LngLat<Array|Number>} []
* @return {Object<x, y>} {x, y}
*/
LngLatToPixel (LngLat) {
const DO = this.GetDistance(this.mapCanvasBoxLngLats[0], LngLat)
const DA = this.canvasLimitConfig.width
const AO = this.GetDistance(this.mapCanvasBoxLngLats[1], LngLat)
const PixelPoint = this.TargetTriangleAreaToXY_Heiht(DA, AO, DO, DA)
return PixelPoint
}
/**
* 目标三角形面积计算
* @methods TargetTriangleArea {a, b, c} 三角形三边长
* 原理 海伦定理 S = Math.sqrt(p(p-a)(p-b)(p-c)) 其中 p = (a + b + c) / 2
*/
TargetTriangleArea(a, b, c) {
const p = (a + b + c) / 2
return Math.sqrt(p * (p - a) * (p - b) * (p - c))
}
/**
* 目标三角形的高
* @methods TargetTriangleAreaToHeiht {a, b, c, xh} 三角形三边长 加对应需求解的底边xh
* 原理 S = 1/2 AH 其中A代表底边 H代表底边对应的高
* @return {Number} 对应底边的高
*/
TargetTriangleAreaToHeiht(a, b, c, xh) {
return 2 * this.TargetTriangleArea(a, b, c) / xh
}
/**
* 计算XY值 即底边垂线 DP PA值
* @param {a, b, c, xh} 注意区分大a边为DA 大b边为AO 大c边为DO
* P (a)
* D ┍━━━━━━━┳━━━━━━━━┒ A
* ╲ ┃ ╱
* ╲ ┃ ╱
* ╲ ┃h ╱
* c ╲ ┃ ╱ b
* ╲ ┃ ╱
* ╲ ┃ ╱
* ╲┃ ╱
* ┻ O
* @return {x, y} 返回值以原点为坐标的坐标点
*/
TargetTriangleAreaToXY_Heiht(a, b, c, xh) {
const H = this.TargetTriangleAreaToHeiht(a, b, c, xh)
const hcReg = Math.acos(H / c)
const DP = c * Math.sin(hcReg)
return {
canvasX: this.canvasLimitConfig.proportionX * DP * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr + this.canvasLimitConfig.offsetLeft,
canvasY: this.canvasLimitConfig.proportionY * H * this.mapScale * this.dpr + this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop,
x: this.canvasLimitConfig.proportionX * DP * this.mapScale + this.offsetConfig.mapX + this.canvasLimitConfig.offsetLeft / this.dpr,
y: this.canvasLimitConfig.proportionY * H * this.mapScale + this.offsetConfig.mapY + this.canvasLimitConfig.offsetTop / this.dpr,
zx: DP,
zy: H
}
}
/**
* 计算实际值与像素值的动态倍率
* @method ActualScalingIndex
* @return {scale<Number>} 返回真实的缩放数值 单位:米/像素 m/pixel
*/
ActualScalingIndex() {
// 获取实长
const ActualWidth = this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr
return this.canvasLimitConfig.width * 1000 / ActualWidth
}
}
至此结束
数据格式解析
{
// 样式层
style: {
// 宽高单位均为rpx
width: 750,
height: 1334,
// 背景支持色值或者网络图片背景图
background: 'pink',
border: 'none'
},
// 坐标中心点 LngLat对象
center: [113.9120864868165, 22.545537650869],
// 地图范围 [LngLat, LngLat] 取点应为对角两个坐标 !!!注意坐标点位置 [右上<RT>, 左下<LB>]
limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],
// 初始化地图层级
initalZoom: 16,
// 地图层级范围
zooms: [16, 18],
// 图层
layers: [
{
// 图片覆盖物 坐标范围 !!!注意坐标点位置 [右上<RT>, 左下<LB>]
limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]],
// 覆盖物地址
image: 'https://weixin.xmzt.cn/static/map-bg.jpeg',
// 透明度
opacity: 1,
// 缩放范围
zooms: [16, 19]
}
],
// 路线
lineStyle: {
lineWidth: 5,
lineColor: 'red',
lineArray: [{longitude: 112.111, latitude: 12.333}]
},
// 自定义Marker
markers: [
{
icon: '/static/images/scenic/tour_voice_poi_01@2x.png',
position: [113.9128,22.544674],
style: {
width: '93rpx',
height: '105rpx',
position: 'relative',
top: '60rpx'
},
label: '(内测)城管大楼',
labelStyle: {
position: 'relative',
top: '-90rpx',
left: '50%',
transform: 'translateX(-50%)',
background: '#FFF',
padding: '5rpx 10rpx',
fontSize: '28rpx'
}
},
{
icon: '/static/images/scenic/tour_voice_poi_01@2x.png',
position: [113.911765,22.545397],
style: {
width: '93rpx',
height: '105rpx',
position: 'relative',
top: '60rpx'
},
label: '(内测)凉亭',
labelStyle: {
position: 'relative',
top: '-90rpx',
left: '50%',
transform: 'translateX(-50%)',
background: '#FFF',
padding: '5rpx 10rpx',
fontSize: '28rpx'
}
}
]
}
整个代码其实很简单。当然也有瑕疵的地方,双指缩放时,缩放中心点问题(解决方案可以是缩放开始时便锁定当前缩放中心点,可解决。提供的代码中未解决。)
整个代码计算量都是很大的。所以性能会有所丢失。主要思路:火星坐标=>物理坐标=>画布坐标=>绘制点或者线
至于精准度问题:基本和高德地图提供的对比图是一致的,画质方面会更加清晰。
其他的便是性能问题了主要性能问题包括两个:一个是cover-view渲染较慢 造成部分东西渲染延迟 拓展性严重下降
另一个是手绘图的图片大小不宜过大,一般手机带不动。当然测试的晓龙855手机和IPhoneXR以上的就没这个问题的啦 需要适当的调节dpr即绘画质量
该方案完全适配高德地图坐标,即火星坐标系。其他坐标正常来说都是通用的。因为绘制的并不涉及投影点问题。距离的计算公式都是统一的。
暂不提供GitHub示例。没时间,有空再说。
微信小程序手绘地图实现之《Canvas》的更多相关文章
- 微信小程序-基于高德地图API实现天气组件(动态效果)
微信小程序-基于高德地图API实现天气组件(动态效果) 在社区翻腾了许久,没有找到合适的天气插件.迫不得已,只好借鉴互联网上的web项目,手动迁移到小程序中使用.现在分享到互联网社区中,帮助后续有 ...
- 微信小程序把玩(四十一)canvas API
原文:微信小程序把玩(四十一)canvas API 绘图是每个移动应用必备的技术,基本上和Android,IOS,等移动开发都是相同的,创建个上下文,给你个画布再上画,官网给的小例子都比较全了自己去看 ...
- [微信小程序]在应用地图时,如何设置满屏(高度)
微信小程序在做地图功能时 用常规的办法height:100%:来设置高度来占满屏幕是不行的 它不会生效 应该改用单位vh 例如 height:100vh 这样就可以是地图占满整个屏幕高度
- 微信小程序腾讯地图SDK使用方法
一.本篇文章主要知识点有以下几种: 1.授权当前位置 2.map组件的使用 3.腾讯地图逆地址解析 4.坐标系的转化 二.效果如下: 三.WXML代码 <map id="map&quo ...
- 微信小程序添加外部地图服务数据
先上效果: 缘起 使用微信小程序做地图相关功能的时候,有个需求是需要接入自己发布的地图服务.查看微信小程序地图组件文档,发现它对地图相关的支持很少,只有一些基础功能,比如添加点.线.面.气泡和一些常规 ...
- 微信小程序之 map 地图使用
1.在app.json中与pages平级的位置处,加上: "permission": { "scope.userLocation": { "desc& ...
- Taro开发微信小程序之初始化地图到当前位置
在componentDidMount中,初始化mapCtx. let _this = this this.mapCtx = Taro.createMapContext('container') //c ...
- 微信小程序分享朋友圈 长海报 canvas 动态高度计算
业务场景 在微信中 小程序无法分享到朋友圈,目前大部分的解决方案都是,canvas动态绘制 生成图片后,保存到用户相册,用户进行分享照片到朋友圈,朋友圈打开图片后识别二维码进入小程序,达到分享目的 g ...
- 微信小程序调用高德地图
index.wxml: longitude:经度 latitude:维度 地图所定位的区域 index.js 地图所定位的点
随机推荐
- [FBCTF2019]Products Manager
基于约束的SQL攻击 一.知识点: 1.数据库字符串比较: 在数据库对字符串进行比较时,如果两个字符串的长度不一样,则会将较短的字符串末尾填充空格,使两个字符串的长度一致,比如,字符串A:[Strin ...
- 软件环境常识 --dev sit uat
DEV环境:DEV顾名思义就是develop,即代码开发的环境. SIT环境:System Integration Test系统集成测试,开发人员自己测试流程是否走通. UAT环境:User Acce ...
- POJ - 1061 青蛙的约会 (扩展欧几里得求同余式)
题意:两只青蛙在网上相识了,它们聊得很开心,于是觉得很有必要见一面.它们很高兴地发现它们住在同一条纬度线上,于是它们约定各自朝西跳,直到碰面为止.可是它们出发之前忘记了一件很重要的事情,既没有问清楚对 ...
- Job 失败了怎么办?【转】
上一节讨论了 Job 执行成功的情况,如果失败了会怎么样呢? 修改 myjob.yml,故意引入一个错误: 先删除之前的 Job: 如果将 restartPolicy 设置为 OnFailure 会怎 ...
- PHP-WebShell-Bypass-WAF
PHP-WebShell-Bypass-WAF PHP WebShell 一句话的结构是:输入和执行,这是经典的PHP 一句话代码: <?php eval($_GET['test']); ?&g ...
- Manjaro Linux 添加源及输入法
生成可用的中国镜像站列表 sudo pacman-mirrors -i -c China -m rank 勾选相应的镜像站 ,看自己的喜好 如中科大:http://mirrors.ustc.edu.c ...
- Selenium -- ActionChains().move_by_offset() 卡顿的解决方法
测试运行时间 运行时间 发现每次0.5秒,此时需要修改默认的时间 打开Python安装目录下的Lib\site-packages\selenium\webdriver\common\actions\p ...
- 002、创建第一个Java程序HelloWord
代码如下: package TIANPAN; public class TestDemo { public static void main(String args[]) { System.out.p ...
- 7.1 Varnish VCL
根据以上的配置增加集群,修改default.vcl # This ) # man page for details on VCL syntax and semantics. # # Default b ...
- (九)微信小程序---for指令
对于数据是列表 wxml <view wx:for="{{dataList}}">{{index}}-{{item}}</view> 我们可以看到上面的代码 ...