環境:微信SDK2.9+ + uniapp (可切換直接使用.map.js不限制環境)
正題:
先創建一個地圖組件
1 <template> 2 <view class="customCanvasComponent"> 3 <!-- 建立畫布坐標系 --> 4 <canvas 5 :style="{ 6 width: `${options.style.width}rpx`, 7 height: `${options.style.height}rpx`, 8 border: options.style.border, 9 background: options.style.background 10 }" 11 type="2d" 12 :id="customMapId" 13 :canvas-id="customMapId" 14 @click="clickToCanvas" 15 @touchstart="touchStartToCanvas" 16 @touchmove="touchMoveToCanvas" 17 @touchend="touchEndToCanvas"> 18 <!-- 由於微信限制 暫時只支持這種寫法 請不要秀其他方式 否則涼涼 --> 19 <!-- Marker點集合 --> 20 <!-- <blank v-for="poi in handlerMarkerList" :key="poi.id"> 21 <cover-view 22 class="point" 23 @click="pointChange(poi)" 24 :style="{ 25 position: 'absolute', 26 display: 'flex', 27 flexDirection: 'column', 28 alignItems: 'center', 29 left: poi.x + 'px', 30 top: poi.y + 'px', 31 transform: `translate(-50%, -100%)` 32 }"> 33 <cover-image :style="poi.stringStyle" :src="poi.icon"></cover-image> 34 <cover-view class="labelView" :style="poi.stringLabelStyle"> 35 <cover-view class="labelTitle">{{poi.label}}</cover-view> 36 </cover-view> 37 </cover-view> 38 </blank> --> 39 <!-- WindowInfo窗體設置 --> 40 <blank v-if="checkPointMarker"> 41 <cover-view class="windowInfoGroupBox" :style="{ 42 position: 'absolute', 43 left: checkPointMarker.x + 'px', 44 top: checkPointMarker.y + 'px', 45 transform: `translate(-50%, calc(-100% - 90rpx))` 46 }"> 47 <cover-view class="infoTitle"> 48 <cover-view class="infoVoiceBtn"> 49 <cover-image class="infoImage" :src="checkPointMarker.image"></cover-image> 50 <cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_play@2x.png"></cover-image> 51 <cover-image class="playControl" src="https://weixin.xmzt.cn/static/scenic/tour_pause@2x.png"></cover-image> 52 </cover-view> 53 <cover-view class="infoContent"> 54 <cover-view class="title otext2"></cover-view> 55 <cover-view class="distance"></cover-view> 56 </cover-view> 57 </cover-view> 58 <cover-view class="btnTools"> 59 <cover-view class="btn"> 60 <cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_voice@2x.png"></cover-image> 61 <cover-view class="btnText">解說</cover-view> 62 </cover-view> 63 <cover-view class="btn"> 64 <cover-image src="https://weixin.xmzt.cn/static/scenic/tour_poi_info@2x.png"></cover-image> 65 <cover-view class="btnText">詳情</cover-view> 66 </cover-view> 67 </cover-view> 68 </cover-view> 69 </blank> 70 <!-- 預留控件 由於小程序限制機制 請使用時僅可使用頂級標簽<cover-view><cover-image> --> 71 <!-- 默認返回處理后的Marker點集合 --> 72 <!-- ControlFirmware Left --> 73 <slot name="control-l"/> 74 <!-- ControlFirmware Right --> 75 <slot name="control-r"/> 76 <!-- ControlFirmware Top --> 77 <slot name="control-t"/> 78 <!-- ControlFirmware Bottom --> 79 <slot name="control-b"/> 80 <!-- 其他控件預留 --> 81 <slot name="other"/> 82 <!-- <cover-view class="toolsBox"> 83 <cover-view class="pointGroupBox"> 84 <blank> 85 <cover-view v-for="poi in handlerMarkerList" :key="poi.id" class="point" :style="{position: 'absolute', left: poi.x + 'px', top: poi.y + 'px'}"> 86 <cover-image :style="{...poi.style}" :src="poi.icon"></cover-image> 87 <cover-view class="labelView" :style="{...poi.labelStyle}"> 88 <cover-view class="labelTitle">{{poi.label}}</cover-view> 89 </cover-view> 90 </cover-view> 91 </blank> 92 </cover-view> 93 <cover-view class="windowInfoGroupBox"> 94 測試 95 <cover-image style="" src="/static/images/scenic/tour_voice_poi_01@2x.png"></cover-image> 96 </cover-view> 97 </cover-view> --> 98 </canvas> 99 <!-- 建立與畫布對應的平面坐標系 --> 100 </view> 101 </template> 102 103 <script> 104 import CustomCavnasMap from './map' 105 let CustomMapInital = null 106 export default { 107 // 組件配置說明 必須基於某個地圖提供商進行的適配 高德 百度 騰訊 谷歌 108 // 這里使用高德 109 props: { 110 // 部分配置參數 111 options: { 112 type: Object, 113 default: () => { 114 return { 115 // 樣式層 116 style: { 117 // 寬高單位均為rpx 118 width: 750, 119 height: 1334, 120 // 背景支持色值或者網絡圖片背景圖 121 background: 'pink', 122 border: 'none' 123 }, 124 // 坐標中心點 LngLat對象 125 center: [113.9120864868165, 22.545537650869], 126 // 地圖范圍 [LngLat, LngLat] 取點應為對角兩個坐標 !!!注意坐標點位置 [右上<RT>, 左下<LB>] 127 limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]], 128 // 初始化地圖層級 129 initalZoom: 16, 130 // 地圖層級范圍 131 zooms: [16, 18], 132 // 圖層 133 layers: [ 134 { 135 // 圖片覆蓋物 坐標范圍 !!!注意坐標點位置 [右上<RT>, 左下<LB>] 136 limitBounds: [[113.914489746094, 22.54744015693], [113.909683227539, 22.543635144808]], 137 // 覆蓋物地址 138 image: 'https://xxx/static/map-bg.jpeg', 139 // 透明度 140 opacity: 1, 141 // 縮放范圍 142 zooms: [16, 19] 143 } 144 ], 145 // 路線 146 lineStyle: { 147 lineWidth: 5, 148 lineColor: 'red', 149 lineArray: [] 150 }, 151 // 自定義Marker 152 markers: [ 153 { 154 icon: '/static/images/scenic/tour_voice_poi_01@2x.png', 155 position: [113.9128,22.544674], 156 style: { 157 width: '93rpx', 158 height: '105rpx', 159 position: 'relative', 160 top: '60rpx' 161 }, 162 label: '(內測)城管大樓', 163 labelStyle: { 164 position: 'relative', 165 top: '-90rpx', 166 left: '50%', 167 transform: 'translateX(-50%)', 168 background: '#FFF', 169 padding: '5rpx 10rpx', 170 fontSize: '28rpx' 171 } 172 }, 173 { 174 icon: '/static/images/scenic/tour_voice_poi_01@2x.png', 175 position: [113.911765,22.545397], 176 style: { 177 width: '93rpx', 178 height: '105rpx', 179 position: 'relative', 180 top: '60rpx' 181 }, 182 label: '(內測)涼亭', 183 labelStyle: { 184 position: 'relative', 185 top: '-90rpx', 186 left: '50%', 187 transform: 'translateX(-50%)', 188 background: '#FFF', 189 padding: '5rpx 10rpx', 190 fontSize: '28rpx' 191 } 192 } 193 ] 194 } 195 } 196 }, 197 // canvasId 198 customMapId: { 199 type: String, 200 default: 'customMap' 201 } 202 }, 203 data () { 204 return { 205 // initalZoom: null, 206 // CustomMapInital: null, // 不要定義到data中 容易引發內存互換 207 handlerMarkerList: [], 208 checkPointMarker: null 209 } 210 }, 211 watch: { 212 'options.lineStyle.lineArray': { 213 handler (_new, _old) { 214 if (_new !== _old) { 215 this.drawLine(_new) 216 } 217 }, 218 deep: true 219 } 220 }, 221 methods: { 222 initalCanvasMap () { 223 // console 224 CustomMapInital = new CustomCavnasMap({ 225 customMapId: this.customMapId, 226 _component: this 227 }, Object.assign({}, this.options, { 228 markerCallBack: (list) => { 229 console.log(list) 230 this.handlerMarkerList = list 231 }, 232 cilckPointChange: (info) => { 233 if (info) { 234 console.log(info) 235 console.log('得到點擊成功后的觸發') 236 this.pointChange(info) 237 } else { 238 console.log('得到點擊空白的回調') 239 } 240 } 241 })) 256 }, 257 fetchCustomBoxSize () { 258 nui.getImageInfo({ 259 src: '', 260 success: (rect) => { 261 console.log(rect.fillPath[0]) 262 } 263 }) 264 }, 265 /** 266 * @Function 267 * @public 公共類方法 268 * @return Object 269 */ 270 // 設置縮放比例 271 setZoom (zoom, callback) { 272 // 最低限制為初始化的縮放比例 273 if (zoom > this.options.initalZoom) { 274 // 邏輯處理 275 CustomMapInital.setZoom(this.initalZoom, callback) 276 } else { 277 CustomMapInital.setZoom(zoom, callback) 278 } 279 }, 280 // 獲取縮放比例 281 getZoom (callback) { 282 if (callback) { 283 callback && callback(CustomMapInital.getZoom()) 284 } else { 285 return CustomMapInital.getZoom() 286 } 287 }, 288 /** 289 * 290 * @touch 事件向this.CustomMapInital觸發 291 */ 292 touchStartToCanvas (e) { 293 CustomMapInital.touchStartToCanvas(e) 294 }, 295 touchMoveToCanvas (e) { 296 CustomMapInital.touchMoveToCanvas(e) 297 }, 298 touchEndToCanvas (e) { 299 CustomMapInital.touchEndToCanvas(e) 300 }, 301 /** 302 * @click 事件向下觸發 303 */ 304 clickToCanvas (e) { 305 CustomMapInital.clickToCanvas(e) 306 // 點擊其他地方進行清空WindowInfo窗體 307 this.checkPointMarker = null 308 }, 309 /** 310 * @param {info<Object>} 類型為Marker數據對象 311 */ 312 pointChange (info) { 313 this.checkPointMarker = info 314 }, 315 /** 316 * @param {lineArray<Array|Object>} 傳入的線路數據 317 * @param {Object} {longitude, latitude} 必須 318 */ 319 drawLine (lineArray) { 320 CustomMapInital.drawLine(CustomMapInital.LngLatConversionToPixel(lineArray)) 321 } 322 }, 323 onReady () { 324 this.initalCanvasMap() 325 }, 326 onUnload () { 327 CustomMapInital = null 328 } 329 } 330 </script> 331 332 <style lang="sass" scoped> 333 $defaultBg: #FFF 334 $bgF4: #F4F4F4 335 $color3: #333 336 $color6: #666 337 $color9: #999 338 // $defaultBg: pink 339 // 取消默認樣式 340 cover-view 341 overflow: initial !important 342 .customCanvasComponent 343 // .toolsBox 344 // position: absolute 345 .point 346 position: absolute 347 z-index: -1 348 display: flex 349 flex-direction: column 350 align-items: center 351 .labelView 352 border-radius: 10rpx 353 background-color: $defaultBg 354 .labelTitle 355 font-size: 28rpx 356 .windowInfoGroupBox 357 background-color: $defaultBg 358 border-radius: 10rpx 359 width: 320rpx 360 height: 228rpx 361 box-shadow: 10rpx 10rpx 20rpx -10rpx $color6 362 display: flex 363 flex-direction: column 364 z-index: 99 365 .infoTitle 366 display: flex 367 align-items: center 368 padding: 20rpx 369 .infoVoiceBtn 370 width: 120rpx 371 height: 120rpx 372 flex: 0 0 120rpx 373 border: 1px solid $bgF4 374 border-radius: 50% 375 overflow: hidden 376 position: relative 377 cover-image 378 width: 100% 379 height: 100% 380 object-fit: contain 381 .playControl 382 position: absolute 383 width: 68rpx 384 height: 68rpx 385 top: 50% 386 left: 50% 387 transform: translate(-50%, -50%) 388 .infoContent 389 flex: 1 390 margin-left: 20rpx 391 .title 392 font-size: 28rpx 393 line-height: 28rpx 394 min-height: 56rpx 395 color: $color3 396 font-weight: bold 397 // margin-right: 58rpx 398 overflow: inherit 399 .distance 400 font-size: 22rpx 401 color: $color9 402 // margin-right: 0.58rem 403 margin-top: 10rpx 404 .btnTools 405 display: flex 406 flex: 1 407 .btn 408 flex: 0 0 calc(50% - 40rpx) 409 display: flex 410 margin: 0 20rpx 15rpx 20rpx 411 align-items: center 412 justify-content: center 413 border-radius: 30rpx 414 cover-image 415 width: 30rpx 416 height: 30rpx 417 .btnText 418 color: $defaultBg 419 font-size: 28rpx 420 .btn:nth-child(1) 421 background: #80D2FC 422 background: linear-gradient(#80D2FC, #188EE9) 423 background: linear-gradient(to right, #80D2FC, #188EE9) 424 .btn:nth-child(2) 425 background: #FBA326 426 background: linear-gradient(#FBA326, #FBA326) 427 background: linear-gradient(to right, #FBA326, #FBA326) 428 </style>
.map.js
1 module.exports = class CustomCavnasMap { 2 canvasContext = null 3 // 定義背景裝載圖 4 layersImages = [] 5 // 初始化Lock鎖超出最大值停止初始化 6 initLock = 0 7 maxLockValue = 1000 8 // 記錄手指按下時的坐標 以及位置 9 startingCoordinate = null 10 // 旋轉時中心點或者縮放時中心點 默認為畫布起點 11 rotateCenter = { 12 x: 0, 13 y: 0 14 } 15 // 背景圖的偏移量 16 offsetConfig = { 17 mapX: 0, 18 mapY: 0 19 } 20 // 捏合縮放倍數或者滾輪縮放倍數 21 mapScale = 1 22 // 捏合縮放狀態 23 mapZoom = false 24 // 雙指旋轉角度地圖旋轉角度 25 mapRotate = 0 26 // 兩指距離 27 mapDistance = 0 28 // 地圖層級限制 最大值 默認兩倍 29 mapMaxZoom = 2 30 // 地圖層級限制 最小值 默認一倍 31 mapMinZoom = 1 32 // 慣性的運動距離 帶方向的距離單位 33 inertialMotion = { 34 x: 0, 35 y: 0 36 } 37 // 新增拖拽慣性支持 摩擦系數μs 范圍應該在0-1之間 38 us = 0.9 39 // 慣性定時器 40 inertialMotionTimer = null 41 COMPUT_TIME = null 42 // 圖片預加載對象 43 pictureExtractionObject = {} 44 // 點擊Canvas后的點位 45 clickPoint = { 46 x: 0, 47 y: 0 48 } 49 // 點擊觸發后的狀態 0未點擊 1點擊了 2點擊了但是點擊錯了 50 clickStatus = 0 51 /** 52 * @methods 53 * @param {Object<customMapId,_component>} canvasOtions 畫布對象 54 * @param {Object<style,center,limitBounds,initalZoom,layers>} options 地圖參數管控 55 */ 56 constructor(canvasOtions, options) { 57 // super(this) 58 console.log('進入構造函數-->') 59 // Object.keys(options) 60 // 獲取設備屬性 61 this.asyncFetchSystemInfo() 62 // this.systemInfo = wx.getSystemInfoSync() 63 // 屬性繼承 64 Object.assign(this, canvasOtions, options) 65 // 手動處理范圍值 66 this.zooms && (this.mapMaxZoom = this.zooms[1] - (this.initalZoom || this.zooms[0])) && (this.mapMinZoom = (this.zooms[0] - this.initalZoom) || 1) 67 console.log('當前限制范圍為:' + this.mapMinZoom + '-' + this.mapMaxZoom) 68 // if (canvasOtions instanceof Object) { 69 // this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId, canvasOtions._component) 70 // } else { 71 // this.canvasContext = wx.createCanvasContext(canvasOtions.customMapId) 72 // } 73 // 設置分辨率 74 // this.dpr = 1 75 // 設置畫布實際大小 76 // this.canvasOptions = { 77 // width: parseInt(this.rpxToPx(options.style.width) * this.dpr), 78 // height: parseInt(this.rpxToPx(options.style.height) * this.dpr) 79 // } 80 // 獲取Canvas節點元素 81 this.wxCreateSelectorQuery().select(`#${canvasOtions.customMapId}`).fields({ 82 node: true, 83 rect: true 84 }, res => { 85 // console.log(res) 86 this.customCanvas = res.node 87 // this.computedConversionData() 88 // this.createMapBGImage(rect.node) 89 this.dpr = this.systemInfo.pixelRatio 90 91 // this.dpr = 1 92 // 設置大小 93 this.customCanvas.width = parseInt(this.rpxToPx(options.style.width) * this.dpr) 94 this.customCanvas.height = parseInt(this.rpxToPx(options.style.height) * this.dpr) 95 // 獲取畫布context上下文 2d 96 this.ctxCanvas = this.customCanvas.getContext('2d') 97 // 獲取畫布context上下文 webgl 98 // this.glCanvas = this.customCanvas.getContext('webgl') 99 // console.log(this.customCanvas) 100 }).exec() 101 // 開始初始化自定義地圖 102 this.initalCanvasChange() 103 } 104 // 初始化Canvas畫布對象 105 initalCanvasChange() { 106 if (this.customCanvas) { 107 this.computedConversionData() 108 } else { 109 setTimeout(() => { 110 console.log('設置延遲100ms進行渲染Canvas畫布') 111 this.initLock++ 112 this.initLock < this.maxLockValue && this.initalCanvasChange() 113 }, 100) 114 } 115 } 116 // 提供選擇節點的公共方法 117 wxCreateSelectorQuery() { 118 if (this._component) { 119 return wx.createSelectorQuery().in(this._component) 120 } else { 121 return wx.createSelectorQuery() 122 } 123 } 124 // 計算兩點坐標實際距離公式 125 GetDistance(LngLat1, LngLat2) { 126 var radLat1 = LngLat1[1] * Math.PI / 180.0 127 var radLat2 = LngLat2[1] * Math.PI / 180.0 128 var a = radLat1 - radLat2 129 var b = LngLat1[0] * Math.PI / 180.0 - LngLat2[0] * Math.PI / 180.0 130 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))) 131 s = s * 6378.137 // EARTH_RADIUS 132 s = Math.round(s * 10000) / 10000 133 return s 134 } 135 // 順序構建map圖庫 136 createMapBGImage() { 144 // 清空頁面繪制 2d 145 this.ctxCanvas.clearRect(0, 0, this.customCanvas.width, this.customCanvas.height) 146 147 // 繪制canvas背景顏色 148 // this.ctxCanvas.fillStyle = this.style.background 149 // this.ctxCanvas.fillRect(0, 0, this.customCanvas.width, this.customCanvas.height) 150 // this.canvasContext.clearRect(0, 0, this.canvasOptions.width, this.canvasOptions.height) 151 // this.glCanvas.clear(this.glCanvas.COLOR_BUFFER_BIT) 152 // console.log(this.rotateCenter) 153 // 設置旋轉中心點 154 this.ctxCanvas.translate(this.rotateCenter.x, this.rotateCenter.y) 155 // 對畫布進行旋轉 暫時關閉旋轉 156 // this.ctxCanvas.rotate(this.mapRotate * Math.PI / 180) 157 // 當繪制結束后 還原旋轉中心點 158 this.ctxCanvas.translate(-this.rotateCenter.x, -this.rotateCenter.y) 159 this.ctxCanvas.save() 160 // 循環進行處理圖片 縮放 平移控制 161 this.layersImages.map(img => { 162 // console.log(img) 163 // 設置圖片透明度 164 this.ctxCanvas.globalAlpha = img.opacity 169 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) 174 this.ctxCanvas.restore() 175 }) 176 // 清除旋轉角度 177 // this.ctxCanvas.rotate(this.mapRotate) 178 this.mapRotate = 0 179 // console.log('繪畫完成') 180 // this.ctxCanvas.restore() 181 this.ctxCanvas.save() 182 this.COMPUT_TIME = new Date().getTime() 183 console.log('開始計算坐標點:' + this.COMPUT_TIME) 184 // 計算點 185 this.drawMarker(this.markers) 186 } 187 // 繪制Marker景點 傳入參數MarkerList對象 188 drawMarker(infoList = []) { 189 // console.log(infoList) 190 if (infoList instanceof Array && infoList.length > 0) { 191 // 計算之前 先得到圖標 192 if (Object.keys(this.pictureExtractionObject).length > 0) { 193 // 開始繪制 194 // 使用定位解決方案 避免canvas數據量過大造成卡頓 [定位方案更卡。。。] 195 // this.LngLatToPixel() 196 this.handlerMarkerList = infoList.map((item, index) => { 197 item.stringStyle = '' 198 Object.keys(item.style).map(key => { 199 item.stringStyle += `${key}: ${item.style[key]};` 200 }) 201 item.stringLabelStyle = '' 202 Object.keys(item.labelStyle).map(key => { 203 item.stringLabelStyle += `${key}: ${item.labelStyle[key]};` 204 }) 207 return Object.assign(item, this.LngLatToPixel(item.position), {id: index}) 208 }) 209 // 創建ICON圖標 211 this.handlerMarkerList.map(item => { 212 this.ctxCanvas.beginPath() 213 this.ctxCanvas.arc(item.canvasX, item.canvasY, 5, 0, 2 * Math.PI) 214 this.ctxCanvas.strokeStyle = 'red' 215 this.ctxCanvas.fillStyle = 'pink' 216 this.ctxCanvas.fill() 217 this.ctxCanvas.stroke() 218 this.ctxCanvas.restore() 220 const w = this.rpxToPx(parseInt(item.style.width)) * this.dpr 221 const h = this.rpxToPx(parseInt(item.style.height)) * this.dpr 222 this.ctxCanvas.drawImage(this.pictureExtractionObject[item.icon], item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h) 223 this.ctxCanvas.restore() 224 this.ctxCanvas.rect(item.canvasX - w / 2, item.canvasY - h / 3 * 2, w, h) 225 const clickPointX = this.clickPoint.x * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr + this.canvasLimitConfig.offsetLeft 226 const clickPointY = this.clickPoint.y * this.mapScale * this.dpr + this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop 229 if (this.clickStatus !== 0) { 230 if (this.ctxCanvas.isPointInPath(clickPointX, clickPointY)) { 231 this.cilckPointChange(item) 232 this.clickStatus = 1 233 console.log('成功觸發畫布點擊回調') 234 } else { 235 console.log('點位錯誤') 236 } 237 } 238 }) 239 if (this.clickStatus === 2) { 240 // 觸發未點中的回調 241 this.cilckPointChange() 242 } 243 // console.log(this.handlerMarkerList) 244 const END_TIME = new Date().getTime() 245 246 console.log('計算結束:' + (END_TIME - this.COMPUT_TIME)) 247 this.markerCallBack(this.handlerMarkerList) 248 } else { 249 setTimeout(() => { 250 this.drawMarker(infoList) 251 }, 100) 252 } 253 } 254 } 255 LngLatConversionToPixel (LngLatArray = []) { 256 if (LngLatArray instanceof Array && LngLatArray.length > 0) { 257 return LngLatArray.map((item, index) => { 258 return Object.assign(item, this.LngLatToPixel([item.longitude, item.latitude]), {id: index}) 259 }) 260 } 261 } 262 // 繪制線路 263 drawLine(LinePathArray = []) { 264 if (LinePathArray instanceof Array && LinePathArray.length > 0) { 265 // 設置繪制樣式 266 this.ctxCanvas.strokeStyle = this.lineStyle.lineColor || '#000000' 267 this.ctxCanvas.lineWidth = this.lineStyle.lineWidth || 5 268 // 開始繪制 269 LinePathArray.map((line, index) => { 270 if (index === 1) { 271 this.ctxCanvas.moveTo(line.x, line.y) 272 } else { 273 this.ctxCanvas.lineTo(line.x, line.y) 274 } 275 }) 276 this.ctxCanvas.stroke() 277 // 繪制結束 278 // 保存一次 279 this.ctxCanvas.save() 280 } 281 } 282 // 取中心點方法 283 Vector(vector1, vector2) { 284 this.x = vector2.x - vector1.x 285 this.y = vector2.y - vector1.y 286 } 287 // 計算點乘 => 公式:a↑ * b↑ = |a↑||b↑|cosθ 288 // 其中:a↑ * b↑ = x1*x2 + y1*y2 289 // 模計算:|a↑| = Math.sqrt(x1 ** 2 + y1 ** 2) 290 calculateVM(vector1, vector2) { 291 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)) 292 } 293 // 計算叉乘 294 calculateVC(vector1, vector2) { 295 return (vector1.x * vector2.y - vector2.x * vector1.y) > 0 ? 1 : -1 296 } 297 // 獲取系統信息 298 asyncFetchSystemInfo() { 299 this.systemInfo = wx.getSystemInfoSync() 300 } 301 // rpx轉px 302 rpxToPx(v) { 304 return v / 750 * this.systemInfo.windowWidth 305 } 306 // 初始化需要計算的所有數據 307 computedConversionData() { 309 // 排序提取背景覆蓋物的值 310 this.handlerImages = this.layers.map(item => { 311 !item.zIndex && (item.zIndex = 100) 312 return item 313 }).sort((a, b) => { 314 return a.zIndex - b.zIndex 315 }).filter(fs => fs) 318 319 // 對角坐標計算 => 轉成4個 [LT, RT, RB, LB] 順時針順序 320 if (this.limitBounds.length === 2) { 321 this.mapCanvasBoxLngLats = [ 322 [this.limitBounds[1][0], this.limitBounds[0][1]], 323 this.limitBounds[0], 324 [this.limitBounds[0][0], this.limitBounds[1][1]], 325 this.limitBounds[1] 326 ] 327 // 得到轉化后的坐標進行計算實際距離 329 const width = this.GetDistance(this.mapCanvasBoxLngLats[0], this.mapCanvasBoxLngLats[1]) 330 const height = this.GetDistance(this.mapCanvasBoxLngLats[1], this.mapCanvasBoxLngLats[2]) 333 const viewWidth = this.rpxToPx(this.style.width || 750) 334 const viewHeight = parseInt(height * viewWidth / width) 335 336 this.canvasLimitConfig = { 337 proportionX: viewWidth / width, 338 proportionY: viewHeight / height, 339 width, 340 height, 341 viewWidth, 342 viewHeight, 345 offsetTop: parseInt(Math.abs((this.customCanvas.height / this.dpr - viewHeight) / 2)), 346 offsetLeft: parseInt(Math.abs((this.customCanvas.width / this.dpr - viewWidth) / 2)) 347 } 350 } 351 352 // 圖片加載處理 353 this.handlerImages.map(item => { 355 const img = this.customCanvas.createImage() 359 img.onload = (e) => { 360 // console.log('已成功加載圖片---->') 362 // 設置附件值 363 Object.assign(img, item) 366 this.layersImages.push(img) 369 this.createMapBGImage() 371 // console.log('設置圖片完成') 372 } 373 img.onerror = (e) => { 374 console.log(e) 375 img.src = item.image 376 } 377 img.src = item.image 389 }) 390 // ICON預加載 394 this.pictureExtraction(this.markers, 'icon').map(item => { 395 const image = this.customCanvas.createImage() 396 image.onload = (e) => { 398 this.pictureExtractionObject[item] = image 399 } 400 image.onerror = (e) => { 401 image.src = item 402 } 403 image.src = item 404 }) 405 } 406 /** 407 * 其他輔助類函數 408 * @method deepClone 深度克隆 409 * @param {Any} Any 任意類型 410 * 411 * 對一個object進行深度拷貝 412 * 413 * 使用遞歸來實現一個深度克隆,可以復制一個目標對象,返回一個完整拷貝 414 * 被復制的對象類型會被限制為數字、字符串、布爾、日期、數組、Object對象。不會包含函數、正則對象等 415 * 416 * @param {Object} ObjectSource 需要進行拷貝的對象 417 */ 418 deepClone(ObjectSource) { 419 if (Array.isArray(ObjectSource)) { 420 return Object.assign([], ObjectSource) 421 } 422 return Object.assign({}, ObjectSource) 423 } 424 /** 425 * 426 * @param {Array<Object>} imageArray 傳入數組遍歷對象 427 * @param {String} name 需要指定去重的數據名稱 428 * @return {Array} 返回的是去重后的Image數組 429 */ 430 pictureExtraction (imageArray, name) { 431 let cloneImageObject = {} 432 imageArray.map(item => { 433 cloneImageObject[item[name]] = item[name] 434 }) 435 return Object.keys(cloneImageObject) 436 } 437 /** 438 * @touch 事件處理 439 * @param {Event} e Event對象 440 */ 441 touchStartToCanvas(e) { 442 // 操作開始時 清空處理 443 this.inertialMotionTimer && clearInterval(this.inertialMotionTimer) 444 // 多指處理 445 if (e.touches.length > 1) { 446 // 屬於多指操作類型 447 console.log('當前屬於多指操作') 448 // console.log(e) 449 // 計算並存儲數據 450 const xMove = e.touches[1].x - e.touches[0].x 451 const yMove = e.touches[1].y - e.touches[0].y 452 // 計算兩指距離 453 this.mapDistance = Math.sqrt(xMove ** 2 + yMove ** 2) 454 this.thisCoordinate = e.touches 455 this.startingCoordinate = e.touches 456 this.mapZoom = true 457 } else { 458 this.startingCoordinate = e.touches[0] 459 // 初始化慣性速度 460 this.inertialMotion = { 461 x: 0, 462 y: 0 463 } 464 } 465 } 466 touchMoveToCanvas(e) { 467 if (e.touches.length > 1) { 468 // 屬於多指操作類型 469 console.log('當前屬於多指操作') 470 this.mapZoom = true 472 // 計算旋轉 473 const preCoordinate = this.deepClone(this.startingCoordinate) 475 this.startingCoordinate = e.touches 476 const vector1 = new this.Vector(preCoordinate[0], preCoordinate[1]) 477 const vector2 = new this.Vector(this.startingCoordinate[0], this.startingCoordinate[1]) 479 const resultCosVal = this.calculateVM(vector1, vector2) 480 // 弧度換算成角度 481 const angle = Math.acos(resultCosVal) * 180 / Math.PI 482 483 const direction = this.calculateVC(vector1, vector2) 484 // 得到最后的旋轉度數 485 const _allDeg = direction * angle 488 489 // 雙指縮放 490 const xMove = e.touches[1].x - e.touches[0].x 491 const yMove = e.touches[1].y - e.touches[0].y 492 493 // 取中心點 494 const posCenter = this.rotateCenter = { 495 x: (e.touches[0].x + e.touches[1].x) / 2, 496 y: (e.touches[0].y + e.touches[1].y) / 2 497 } 498 499 const distance = Math.sqrt(xMove ** 2 + yMove ** 2) 500 const distanceDiff = distance - this.mapDistance 502 const scalingIndex = 0.005 * distanceDiff 503 const newScale = this.mapScale + scalingIndex 509 let mapX = this.offsetConfig.mapX 510 let mapY = this.offsetConfig.mapY 514 515 const scaleSizeX = scalingIndex * this.canvasLimitConfig.viewWidth * this.mapScale 516 const scaleSizeY = scalingIndex * this.canvasLimitConfig.viewHeight * this.mapScale 517 518 mapX -= scaleSizeX / 2 519 mapY -= scaleSizeY / 2 520 console.log('多指') 535 536 if (Math.abs(_allDeg) > 1) { 537 this.mapRotate = this.mapRotate + _allDeg 538 // 重繪 539 this.createMapBGImage() 540 } 541 // 限制范圍 不存在mapX mapY時出現計算錯誤時退出當前縮放 542 if (newScale < this.mapMinZoom || newScale > this.mapMaxZoom || isNaN(mapX) || isNaN(mapY)) { 543 return 544 } 545 this.mapDistance = distance 546 this.mapScale = newScale 547 this.offsetConfig.mapX = mapX 548 this.offsetConfig.mapY = mapY 549 // 重繪 550 this.createMapBGImage() 551 } else { 552 // slidingDistanceX 553 // const offsetX = 554 // 不處理在雙指或者多指情況下的剩余操作 555 if (this.mapZoom) { 556 return 557 } 558 // 判斷是否為數組 559 if (this.startingCoordinate instanceof Array) { 560 this.startingCoordinate = this.startingCoordinate[0] 561 } 562 const thisCoordinate = e.touches[0] 563 const slidingDistanceX = thisCoordinate.x - this.startingCoordinate.x 564 const slidingDistanceY = thisCoordinate.y - this.startingCoordinate.y 565 566 this.offsetConfig.mapX += slidingDistanceX 567 this.offsetConfig.mapY += slidingDistanceY 568 // 處理速度 569 this.inertialMotion = { 570 x: slidingDistanceX || 0, 571 y: slidingDistanceY || 0 572 } 573 // console.log(this.inertialMotion) 574 console.log('單指') 575 // console.log(this.inertialMotion.x, this.inertialMotion.y) 576 // 處理邊界 577 this.touchMoveLimitBounds() 578 579 // 重新設置初始點 580 this.startingCoordinate = thisCoordinate 581 // 重繪 582 this.createMapBGImage() 583 } 584 } 585 touchEndToCanvas(e) { 586 // console.log(e) 587 if (e.touches.length === 0) { 588 // 處理慣性 589 !this.mapZoom && this.inertialMotionToCanvas(this.inertialMotion.x, this.inertialMotion.y) 590 this.mapZoom = false 591 // 如果初始大小 則復位 592 if (this.mapScale === 1) { 593 // this.offsetConfig = { 594 // mapX: 0, 595 // mapY: 0 596 // } 597 // 重繪 598 // this.createMapBGImage() 599 } 600 // 處理用戶多指操作 抬起某一手指 應進行刪除控制 601 // e.touches.map(item => { 602 603 // }) 604 } else { 605 // console.log(e) 606 this.mapZoom = false 607 } 608 } 609 touchMoveLimitBounds() { 610 // 處理邊界問題 611 // X 軸 612 if ((this.offsetConfig.mapX + this.canvasLimitConfig.offsetLeft + this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) > this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) { 613 this.offsetConfig.mapX = this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr - this.canvasLimitConfig.offsetLeft - this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr 614 } else if ((this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr) < this.customCanvas.width) { 615 this.offsetConfig.mapX = (this.customCanvas.width - this.canvasLimitConfig.viewWidth * this.mapScale * this.dpr) / this.dpr 616 } 617 // Y 軸 618 if (this.customCanvas.height > this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) { 619 if ((this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) < (this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop)) { 620 this.offsetConfig.mapY = (this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr - this.canvasLimitConfig.offsetTop) / this.dpr 621 } else if ((this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr) < 0) { 622 this.offsetConfig.mapY = (0 - this.canvasLimitConfig.offsetTop) / this.dpr 623 } 624 } else { 625 if ((this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr) > (this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop)) { 626 this.offsetConfig.mapY = (this.customCanvas.height - this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr - this.canvasLimitConfig.offsetTop) / this.dpr 627 } else if ((this.canvasLimitConfig.offsetTop + this.offsetConfig.mapY * this.dpr) > 0) { 628 this.offsetConfig.mapY = (0 - this.canvasLimitConfig.offsetTop) / this.dpr 629 } 630 } 631 } 632 /** 633 * 處理拖動慣性運動 634 * @param {Number} speedX X軸的速度 635 * @param {Number} speedY Y軸的速度 636 * @handler Canvas 處理函數 637 */ 638 inertialMotionToCanvas(speedX, speedY) { 639 if (isNaN(speedX) || isNaN(speedY)) return 640 this.inertialMotionTimer && clearInterval(this.inertialMotionTimer) 641 this.inertialMotionTimer = setInterval(() => { 642 speedX *= this.us 643 speedY *= this.us 644 this.offsetConfig.mapX += speedX 645 this.offsetConfig.mapY += speedY 646 // 處理邊界 647 this.touchMoveLimitBounds() 648 if (Math.abs(speedX) < 1) speedX = 0 649 if (Math.abs(speedY) < 1) speedY = 0 650 if (speedX == 0 && speedY == 0) { 651 this.inertialMotion = { 652 x: 0, 653 y: 0 654 } 655 clearInterval(this.inertialMotionTimer) 656 } 658 // 重繪 659 this.createMapBGImage() 660 }, 30) 661 } 662 663 /** 664 * @click 事件處理 665 * @param {Event} e Event對象 666 */ 667 clickToCanvas(e) { 669 // 假設沒點中 670 this.clickStatus = 2 671 this.clickPoint = { 672 x: e.target.x - e.target.offsetLeft, 673 y: e.target.y - e.target.offsetTop 674 } 675 this.createMapBGImage() 676 } 677 /** 678 * 坐標換算 679 * P (a) 680 * D ┍━━━━━━━┳━━━━━━━━┒ A 681 * ╲ ┃ ╱ 682 * ╲ ┃ ╱ 683 * ╲ ┃h ╱ 684 * c ╲ ┃ ╱ b 685 * ╲ ┃ ╱ 686 * ╲ ┃ ╱ 687 * ╲┃ ╱ 688 * ┻ O 689 * 從地圖坐標系到物理坐標戲 690 * @methods LngLatToPixel {LngLat<Array|Number>} [] 691 * @return {Object<x, y>} {x, y} 692 */ 693 LngLatToPixel (LngLat) { 694 const DO = this.GetDistance(this.mapCanvasBoxLngLats[0], LngLat) 695 const DA = this.canvasLimitConfig.width 696 const AO = this.GetDistance(this.mapCanvasBoxLngLats[1], LngLat) 701 const PixelPoint = this.TargetTriangleAreaToXY_Heiht(DA, AO, DO, DA) 702 return PixelPoint 703 } 704 /** 705 * 目標三角形面積計算 706 * @methods TargetTriangleArea {a, b, c} 三角形三邊長 707 * 原理 海倫定理 S = Math.sqrt(p(p-a)(p-b)(p-c)) 其中 p = (a + b + c) / 2 708 */ 709 TargetTriangleArea(a, b, c) { 710 const p = (a + b + c) / 2 711 return Math.sqrt(p * (p - a) * (p - b) * (p - c)) 712 } 713 /** 714 * 目標三角形的高 715 * @methods TargetTriangleAreaToHeiht {a, b, c, xh} 三角形三邊長 加對應需求解的底邊xh 716 * 原理 S = 1/2 AH 其中A代表底邊 H代表底邊對應的高 717 * @return {Number} 對應底邊的高 718 */ 719 TargetTriangleAreaToHeiht(a, b, c, xh) { 720 return 2 * this.TargetTriangleArea(a, b, c) / xh 721 } 722 /** 723 * 計算XY值 即底邊垂線 DP PA值 724 * @param {a, b, c, xh} 注意區分大a邊為DA 大b邊為AO 大c邊為DO 725 * P (a) 726 * D ┍━━━━━━━┳━━━━━━━━┒ A 727 * ╲ ┃ ╱ 728 * ╲ ┃ ╱ 729 * ╲ ┃h ╱ 730 * c ╲ ┃ ╱ b 731 * ╲ ┃ ╱ 732 * ╲ ┃ ╱ 733 * ╲┃ ╱ 734 * ┻ O 735 * @return {x, y} 返回值以原點為坐標的坐標點 736 */ 737 TargetTriangleAreaToXY_Heiht(a, b, c, xh) { 739 const H = this.TargetTriangleAreaToHeiht(a, b, c, xh) 740 const hcReg = Math.acos(H / c) 745 const DP = c * Math.sin(hcReg) 748 return { 749 canvasX: this.canvasLimitConfig.proportionX * DP * this.mapScale * this.dpr + this.offsetConfig.mapX * this.dpr + this.canvasLimitConfig.offsetLeft, 750 canvasY: this.canvasLimitConfig.proportionY * H * this.mapScale * this.dpr + this.offsetConfig.mapY * this.dpr + this.canvasLimitConfig.offsetTop, 751 x: this.canvasLimitConfig.proportionX * DP * this.mapScale + this.offsetConfig.mapX + this.canvasLimitConfig.offsetLeft / this.dpr, 752 y: this.canvasLimitConfig.proportionY * H * this.mapScale + this.offsetConfig.mapY + this.canvasLimitConfig.offsetTop / this.dpr, 753 zx: DP, 754 zy: H 755 } 756 } 757 /** 758 * 計算實際值與像素值的動態倍率 759 * @method ActualScalingIndex 760 * @return {scale<Number>} 返回真實的縮放數值 單位:米/像素 m/pixel 761 */ 762 ActualScalingIndex() { 764 // 獲取實長 765 const ActualWidth = this.canvasLimitConfig.viewHeight * this.mapScale * this.dpr 766 return this.canvasLimitConfig.width * 1000 / ActualWidth 767 } 768 }
至此結束
數據格式解析
{ // 樣式層 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示例。沒時間,有空再說。