跪拜 Guibai
← Back to the summary

A TMap Utility for AMap in Vue 3: Markers, Areas, Loca Pulse Lines, and Vue-Powered InfoWindows

Vue 3 + AMap Wrapper in Practice: Points, Regions, Loca Pulse Lines, and Vue InfoWindows

Tech stack: Vue 3 + TypeScript + AMap JS API 2.0 + Loca 2.0

This article provides a unified wrapper around AMap's capabilities, and will later cover common map-backend page features and examples using the wrapped API, along with the underlying implementation details of the wrapper.

1. Wrapper Capability Overview

The TMap wrapper in the project provides the following methods:

TMap
├── initMap()
├── setMapCenter()
├── setMapStyle()
├── addMarker()
├── addMarkers()
├── setMarkerIcon()
├── markerDraggable()
├── addPolygons()
├── drawPolygonTool()
├── updatePolygon()
├── addPolylines()
├── setPolylineDisplay()
├── addPulseLayer()
├── removePulseLayer()
├── addCluster()
├── setClusterData()
├── createdInfoWindow()
├── openInfoWindow()
└── closeInfoWindow()

Usage on a page:

import TMap from '@/utils/tmap'

const tmap = new TMap()

onMounted(() => {
  tmap.initMap('map-container', () => {
    initMapData()
  })
})

2. Map Initialization

const tmap = new TMap()

tmap.initMap('map-container', () => {
  tmap.setMapStyle('amap://styles/your-style-id')
  tmap.setMapCenter([116.397428, 39.90923], 14)
})

Under the hood it uses @amap/amap-jsapi-loader to load AMap 2.0:

AMapLoader.load({
  key,
  version: '2.0',
  plugins: [
    'AMap.MouseTool',
    'AMap.Geocoder',
    'AMap.PolygonEditor',
    'AMap.DistrictSearch',
    'AMap.PolylineEditor',
    'AMap.MarkerCluster'
  ],
  Loca: {
    version: '2.0.0'
  }
})

3. Point Rendering

1. Adding Points Using the Wrapper

const pointList = [
  {
    id: 1,
    name: 'Point One',
    shape: [116.397428, 39.90923],
    icon: 'point-icon',
    color: '#2484f4'
  },
  {
    id: 2,
    name: 'Point Two',
    shape: [116.407428, 39.91923],
    icon: 'point-icon',
    color: '#2484f4'
  }
]

tmap.addMarkers(
  pointList,
  (item) => {
    console.log('Clicked point', item)
  },
  true
)

Internally, addMarkers() converts the config into AMap.Marker:

const marker = new AMap.Marker({
  position: item.shape,
  offset: new AMap.Pixel(-14, -32),
  content: this.dealMarkerIcon(item.icon, item.color, []),
  extData: item
})

marker.on('click', () => {
  onClick?.(item)
})

map.add(marker)

2. Changing Point Icon and Position

tmap.setMarkerPosition([116.4, 39.91])
tmap.setMarkerIcon('point-active', '#00ffff', [36, 44])

3. Point Dragging

tmap.markerSetDraggable(true)

tmap.markerDraggable((event) => {
  const lnglat = [event.lnglat.lng, event.lnglat.lat]

  tmap.regeoCode(lnglat, (addressInfo) => {
    console.log('Address after drag', addressInfo.address)
  })
})

4. Region Drawing and Rendering

1. Rendering Regions

const areaList = [
  {
    id: 1,
    name: 'Example Region',
    shape: [
      [116.39, 39.9],
      [116.41, 39.9],
      [116.41, 39.92],
      [116.39, 39.92]
    ],
    color: '#2484f4'
  }
]

tmap.addPolygons(
  areaList,
  (area) => {
    console.log('Clicked region', area)
  },
  {
    show: true
  }
)

Under the hood it creates:

const polygon = new AMap.Polygon({
  path: item.shape,
  fillColor: item.color,
  fillOpacity: 0.3,
  strokeColor: item.color,
  strokeWeight: 4,
  extData: item,
  zIndex: 50
})

2. Drawing Regions

tmap.drawPolygonTool('#2484f4', (path) => {
  console.log('Drawing complete', path)
})

3. Editing Regions

tmap.updatePolygon((path) => {
  const normalizedPath = path.map((point) => [
    Number(point.lng),
    Number(point.lat)
  ])
  console.log('Edited path', normalizedPath)
})

5. Administrative District Mask

tmap.maskCoverMapOtherArea({
  searchVal: 'Example District',
  center: [116.397428, 39.90923],
  zoom: 14,
  fillColor: '#040b31',
  fillOpacity: 0.8,
  strokeColor: '#2484f4'
})

Under the hood it uses AMap.DistrictSearch to get the administrative district boundaries, then covers the outside area with a polygon that has a hole.

const district = new AMap.DistrictSearch({
  subdistrict: 1,
  extensions: 'all',
  level: 'district'
})

district.search(keyword, (status, result) => {
  if (status !== 'complete') return

  const holes = result.districtList[0].boundaries
  const outer = [
    new AMap.LngLat(-360, 90, true),
    new AMap.LngLat(-360, -90, true),
    new AMap.LngLat(360, -90, true),
    new AMap.LngLat(360, 90, true)
  ]

  map.add(new AMap.Polygon({
    path: [outer, ...holes],
    fillColor: '#040b31',
    fillOpacity: 0.8,
    strokeColor: '#2484f4',
    strokeWeight: 4
  }))
})

6. Routes and Loca Pulse Lines

1. Business Route Data Structure

interface FlowLine {
  id: string | number
  name: string
  shape: Array<[number, number]>
  color?: string
  width?: number
}

2. Adding Base Polylines

const lineList: FlowLine[] = [
  {
    id: 1,
    name: 'Example Flow',
    shape: [
      [116.397428, 39.90923],
      [116.405428, 39.91523],
      [116.412428, 39.91923]
    ],
    color: '#00d8ff',
    width: 4
  }
]

tmap.addPolylines(lineList, (line) => {
  console.log('Clicked line', line)
})

3. Converting Routes to GeoJSON

Loca's PulseLineLayer requires standard GeoJSON:

function formatGeoData(list: FlowLine[]) {
  return list.reduce((features: any[], line) => {
    if (line.shape?.length >= 2) {
      features.push({
        type: 'Feature',
        properties: {
          id: line.id,
          name: line.name,
          color: line.color || '#00d8ff',
          width: line.width || 4
        },
        geometry: {
          type: 'LineString',
          coordinates: line.shape
        }
      })
    }

    return features
  }, [])
}

const geo = {
  type: 'FeatureCollection',
  features: formatGeoData(lineList)
}

4. Adding a Pulse Layer Using the Wrapper

tmap.addPulseLayer(geo)

The project's addPulseLayer() creates a Loca pulse layer:

addPulseLayer(geo: GeoJSON.FeatureCollection) {
  if (!this.pulseLayer) {
    this.pulseLayer = new this.Loca.PulseLineLayer({
      zIndex: 1000,
      opacity: 1,
      visible: true,
      zooms: [2, 22]
    })
  }

  this.pulseLayer.setStyle({
    altitude: 0,
    lineWidth: (_, feature) => Number(feature.properties.width),
    headColor: (_, feature) => feature.properties.color,
    trailColor: 'rgba(128,128,128,.5)',
    interval: 0.4,
    duration: 4000
  })

  this.pulseLayer.setSource(
    new this.Loca.GeoJSONSource({ data: geo })
  )

  this.loca.add(this.pulseLayer)
  this.loca.animate.start()
}

5. Adding a New Pulse Line Route

When business data adds a new route, merge it into the route list and regenerate the GeoJSON:

const currentLines: FlowLine[] = []

function addFlowLine(line: FlowLine) {
  currentLines.push(line)

  // This wrapper replaces the whole layer; remove the old pulse layer first
  tmap.removePulseLayer()

  tmap.addPulseLayer({
    type: 'FeatureCollection',
    features: formatGeoData(currentLines)
  })
}

Example:

addFlowLine({
  id: 2,
  name: 'New Flow',
  shape: [
    [116.397428, 39.90923],
    [116.417428, 39.92923]
  ],
  color: '#00e5ff',
  width: 5
})

6. Show, Hide, and Remove

// Controls both normal Polylines and the Loca pulse layer
 tmap.setPolylineDisplay('show')
tmap.setPolylineDisplay('hide')

// Remove all pulse layers and stop the animation
tmap.removePulseLayer()

7. Point Clustering

const markers = pointList.map((point) => {
  return new AMap.Marker({
    position: point.shape,
    extData: point
  })
})

tmap.addCluster(markers)

Updating cluster data:

tmap.setClusterData(newMarkers, 'reset')

Appending data:

tmap.setClusterData(newMarkers, 'add')

8. InfoWindow Mounting Custom Vue Components

1. Rendering Vue Components in the Parent Template

<div ref="infoWindowRef" class="map-info-window">
  <GeneralDetail
    :type="state.displayType"
    :info="state.displayData"
    @on-close="handleCloseInfoWindow"
    @on-check="state.showFullDetail = true"
  />
</div>

The key points here are:

2. Map Service Creating an InfoWindow

createdInfoWindow(content: HTMLElement, closeWhenClickMap = true) {
  this.infoWindow = new this.AMap.InfoWindow({
    isCustom: true,
    content,
    closeWhenClickMap,
    offset: new this.AMap.Pixel(0, -50)
  })
}

openInfoWindow(position: [number, number]) {
  this.infoWindow?.open(this.map, position)
}

3. Clicking a Point to Update and Open

async function openPointInfoWindow(point) {
  state.displayType = 'point'
  state.displayData = point

  // Wait for Vue to render the latest data into the container
  await nextTick()

  if (!tmap.infoWindow) {
    tmap.createdInfoWindow(infoWindowRef.value, false)
  }

  tmap.openInfoWindow(point.shape)
}

Since GeneralDetail is always managed by Vue, after updating state.displayData, the component updates reactively without needing to recreate the Vue instance.

4. Closing and Destroying

function handleCloseInfoWindow() {
  tmap.closeInfoWindow()
}

onBeforeUnmount(() => {
  tmap.closeInfoWindow()
  tmap.destroyInfoWindow()
})

5. Dynamically Creating a Vue Component as InfoWindow

If the InfoWindow content is not pre-mounted in the template but created at runtime, handle it like this:

import { createApp, h, reactive, nextTick } from 'vue'
import InfoPopup from './InfoPopup.vue'

let popupApp: ReturnType<typeof createApp> | null = null
let popupHost: HTMLElement | null = null
let popupWindow: AMap.InfoWindow | null = null

async function openDynamicInfoWindow(data: any, position: [number, number]) {
  closeDynamicInfoWindow()

  popupHost = document.createElement('div')
  const props = reactive({ data })

  popupApp = createApp({
    render() {
      return h(InfoPopup, {
        data: props.data,
        onClose: closeDynamicInfoWindow
      })
    }
  })

  popupApp.mount(popupHost)
  await nextTick()

  popupWindow = new AMap.InfoWindow({
    isCustom: true,
    content: popupHost,
    offset: new AMap.Pixel(0, -20)
  })

  popupWindow.open(map, position)
}

function closeDynamicInfoWindow() {
  popupWindow?.close()
  popupWindow = null

  popupApp?.unmount()
  popupApp = null
  popupHost = null
}

When mounting dynamically, note:

9. Complete Usage Example

import { nextTick, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import TMap from '@/utils/tmap'
import GeneralDetail from './GeneralDetail.vue'

const mapRef = ref()
const infoWindowRef = ref<HTMLElement>()
const tmap = new TMap()

const state = reactive({
  displayType: 'point',
  displayData: {}
})

onMounted(() => {
  tmap.initMap('map-container', async () => {
    tmap.setMapStyle('amap://styles/your-style-id')

    const points = await getPointList()
    const areas = await getAreaList()
    const lines = await getLineList()

    tmap.addMarkers(points, handlePointClick, true)
    tmap.addPolygons(areas, handleAreaClick)

    tmap.addPolylines(lines)
    tmap.addPulseLayer({
      type: 'FeatureCollection',
      features: formatGeoData(lines)
    })
  })
})

async function handlePointClick(point: any) {
  state.displayType = 'point'
  state.displayData = point

  await nextTick()

  if (!tmap.infoWindow) {
    tmap.createdInfoWindow(infoWindowRef.value!, false)
  }

  tmap.openInfoWindow(point.shape)
}

async function handleAreaClick(area: any) {
  state.displayType = 'area'
  state.displayData = area

  await nextTick()

  if (!tmap.infoWindow) {
    tmap.createdInfoWindow(infoWindowRef.value!, false)
  }

  tmap.openInfoWindow(getPolygonCenter(area.shape))
}

function getPolygonCenter(shape: Array<[number, number]>) {
  const [lng, lat] = shape.reduce(
    (total, point) => [total[0] + point[0], total[1] + point[1]],
    [0, 0]
  )

  return [lng / shape.length, lat / shape.length] as [number, number]
}

onBeforeUnmount(() => {
  tmap.removePulseLayer()
  tmap.closeInfoWindow()
  tmap.destroyInfoWindow()
})

10. Feature-to-Wrapper-Method Mapping

Feature Wrapper Method
Map initialization initMap()
Map center and zoom setMapCenter()
Map style setMapStyle()
Single Marker addMarker()
Batch Markers addMarkers()
Marker dragging markerDraggable()
Reverse geocoding regeoCode()
Region rendering addPolygons()
Region drawing drawPolygonTool()
Region editing updatePolygon()
Route rendering addPolylines()
Route and pulse line visibility setPolylineDisplay()
Add pulse line addPulseLayer()
Remove pulse lines removePulseLayer()
Point clustering addCluster()
Update clustering setClusterData()
Create info window createdInfoWindow()
Open info window openInfoWindow()
Close info window closeInfoWindow()

Summary

The AMap wrapper handles four kinds of capabilities:

  1. Map instance and base map;
  2. Marker, Polygon, Polyline;
  3. Loca pulse lines and clustering;
  4. InfoWindow cooperation with Vue components.

For InfoWindow using custom Vue components, the simplest and most stable approach is to first render a container in the parent template, then pass that container's DOM to AMap.InfoWindow. This way Vue's reactivity, Pinia, Router, and component styles are all reused normally.

Comments

Top 1 of 2 from juejin.cn, machine-translated. The original thread is authoritative.

THEKILL

Is it open source?

啾咪orJIUMI

No, just sharing examples of functionality I've built.