微信小程序手繪地圖實現之《Canvas》


環境:微信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示例。沒時間,有空再說。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM