import { onUnmounted, inject, watch } from 'vue' import { isFunction } from '@vue/shared' import { getRealPath } from '@dcloudio/uni-platform' import { defineSystemComponent, useCustomEvent } from '@dcloudio/uni-components' import { Maps, Map, LatLng, Callout, CalloutOptions } from './maps' import { MapType, getMapInfo, getIsAMap } from '../../../helpers/location' import { LatLng as GLatLng, Marker as GMarker, Label as GLabel, Icon, GoogleMaps, } from './maps/google/types' import { Map as QMap, LatLng as QLatLng, Marker as QMarker, Label as QLabel, MarkerImage, QQMaps, } from './maps/qq/types' const props = { id: { type: [Number, String], default: '', }, latitude: { type: [Number, String], require: true, }, longitude: { type: [Number, String], require: true, }, title: { type: String, default: '', }, iconPath: { type: String, require: true, }, rotate: { type: [Number, String], default: 0, }, alpha: { type: [Number, String], default: 1, }, width: { type: [Number, String], default: '', }, height: { type: [Number, String], default: '', }, callout: { type: Object, default: null, }, label: { type: Object, default: null, }, anchor: { type: Object, default: null, }, clusterId: { type: [Number, String], default: '', }, customCallout: { type: Object, default: null, }, ariaLabel: { type: String, default: '', }, } interface Point { latitude: number longitude: number } export type Props = Partial> type CustomEventTrigger = ReturnType type OnMapReadyCallback = ( map: Map, maps: Maps, trigger: CustomEventTrigger ) => void type OnMapReady = (callback: OnMapReadyCallback) => void export interface TranslateMarkerOptions { destination: Point autoRotate?: Boolean rotate?: number duration?: number animationEnd?: Function } export interface Context { id: string translate: (options: TranslateMarkerOptions) => void } type AddMapChidlContext = (context: Context) => void type RemoveMapChidlContext = (context: Context) => void type Label = GLabel | QLabel interface MarkerExt { callout?: InstanceType label?: Label lastPosition?: LatLng } interface GMarkerExt extends GMarker, MarkerExt {} interface QMarkerExt extends QMarker, MarkerExt {} interface AMarkerExt extends AMap.Marker, MarkerExt {} type Marker = GMarkerExt | QMarkerExt | AMarkerExt type MarkerLabelStyle = Partial< Pick< CSSStyleDeclaration, | 'position' | 'top' | 'borderStyle' | 'borderColor' | 'borderWidth' | 'padding' | 'borderRadius' | 'backgroundColor' | 'color' | 'fontSize' | 'lineHeight' | 'marginLeft' | 'marginTop' > > function useMarkerLabelStyle(id: string) { const className = 'uni-map-marker-label-' + id const styleEl = document.createElement('style') styleEl.id = className document.head.appendChild(styleEl) onUnmounted(() => { styleEl.remove() }) return function updateMarkerLabelStyle(style: MarkerLabelStyle) { const newStyle: MarkerLabelStyle = Object.assign({}, style, { position: 'absolute', top: '70px', borderStyle: 'solid', }) const div = document.createElement('div') Object.keys(newStyle).forEach((key) => { div.style[key as keyof MarkerLabelStyle] = newStyle[key as keyof MarkerLabelStyle] || '' }) styleEl.innerText = `.${className}{${div.getAttribute('style')}}` return className } } export default /*#__PURE__*/ defineSystemComponent({ name: 'MapMarker', props, setup(props) { const id = String(!isNaN(Number(props.id)) ? props.id : '') const onMapReady: OnMapReady = inject('onMapReady') as OnMapReady const updateMarkerLabelStyle = useMarkerLabelStyle(id) let marker: Marker function removeMarker() { if (marker) { if (marker.label && 'setMap' in marker.label) { ;(marker.label as QLabel).setMap(null) } if (marker.callout) { removeMarkerCallout(marker.callout) } marker.setMap(null) } } function removeMarkerCallout(callout: Marker['callout']) { if (getIsAMap()) { callout!.removeAMapText() } else { callout!.setMap(null) } } onMapReady((map, maps, trigger) => { function updateMarker(option: Props) { const title = option.title const position = getIsAMap() ? new (maps as AMap.NameSpace).LngLat( option.longitude, option.latitude ) : new (maps as QQMaps | GoogleMaps).LatLng( option.latitude, option.longitude ) const img = new Image() img.onload = () => { const anchor = option.anchor || {} let icon: MarkerImage | Icon let w let h let top let x = typeof anchor.x === 'number' ? anchor.x : 0.5 let y = typeof anchor.y === 'number' ? anchor.y : 1 if (option.iconPath && (option.width || option.height)) { w = option.width || (img.width / img.height) * option.height h = option.height || (img.height / img.width) * option.width } else { w = img.width / 2 h = img.height / 2 } top = h - (h - y * h) if ('MarkerImage' in maps) { icon = new maps.MarkerImage( img.src, null, null, new (maps as QQMaps).Point(x * w, y * h), new (maps as QQMaps).Size(w, h) ) } else if ('Icon' in maps) { // 高德 icon = new (maps as AMap.NameSpace).Icon({ image: img.src, size: new (maps as AMap.NameSpace).Size(w, h), imageSize: new (maps as AMap.NameSpace).Size(w, h), imageOffset: new (maps as AMap.NameSpace).Pixel(x * w, y * h), }) } else { icon = { url: img.src, anchor: new (maps as GoogleMaps).Point(x, y), size: new (maps as GoogleMaps).Size(w, h), } } marker.setPosition(position as any) marker.setIcon(icon as any) if ('setRotation' in marker) { marker.setRotation(option.rotate || 0) } const labelOpt = option.label || {} if ('label' in marker) { ;(marker.label as QLabel).setMap(null) delete marker.label } let label if (labelOpt.content) { const labelStyle = { borderColor: labelOpt.borderColor, borderWidth: (Number(labelOpt.borderWidth) || 0) + 'px', padding: (Number(labelOpt.padding) || 0) + 'px', borderRadius: (Number(labelOpt.borderRadius) || 0) + 'px', backgroundColor: labelOpt.bgColor, color: labelOpt.color, fontSize: (labelOpt.fontSize || 14) + 'px', lineHeight: (labelOpt.fontSize || 14) + 'px', marginLeft: (Number(labelOpt.anchorX || labelOpt.x) || 0) + 'px', marginTop: (Number(labelOpt.anchorY || labelOpt.y) || 0) + 'px', } if ('Label' in maps) { label = new maps.Label({ position: position as QLatLng, map: map as QMap, clickable: false, content: labelOpt.content, style: labelStyle, }) marker.label = label } else if ('setLabel' in marker) { if (getIsAMap()) { const content = `
${labelOpt.content}
` marker.setLabel({ content, direction: 'bottom-right', } as any) } else { const className = updateMarkerLabelStyle(labelStyle) ;(marker as GMarker).setLabel({ text: labelOpt.content, color: labelStyle.color, fontSize: labelStyle.fontSize, className, }) } } } const calloutOpt = option.callout || {} let callout = marker.callout let calloutStyle: CalloutOptions if (calloutOpt.content || title) { const boxShadow = '0px 0px 3px 1px rgba(0,0,0,0.5)' calloutStyle = calloutOpt.content ? { position, map, top, // handle AMap callout offset offsetY: -option.height / 2, content: calloutOpt.content, color: calloutOpt.color, fontSize: calloutOpt.fontSize, borderRadius: calloutOpt.borderRadius, bgColor: calloutOpt.bgColor, padding: calloutOpt.padding, boxShadow: calloutOpt.boxShadow || boxShadow, display: calloutOpt.display, } : { position, map, top, // handle AMap callout offset offsetY: -option.height / 2, content: title, boxShadow, } if (callout) { callout.setOption(calloutStyle) } else { if (getIsAMap()) { const callback = (id: number | string) => { if (id !== '') { trigger('callouttap', {} as Event, { markerId: Number(id), }) } } callout = marker.callout = new maps.Callout( calloutStyle, callback ) } else { callout = marker.callout = new maps.Callout(calloutStyle) callout.div!.onclick = function ($event: Event) { if (id !== '') { trigger('callouttap', $event, { markerId: Number(id), }) } $event.stopPropagation() $event.preventDefault() } // The mobile terminal prevent google map callout click trigger map click if (getMapInfo().type === MapType.GOOGLE) { callout.div!.ontouchstart = function ($event: Event) { $event.stopPropagation() } callout.div!.onpointerdown = function ($event: Event) { $event.stopPropagation() } } } } } else { if (callout) { removeMarkerCallout(callout) delete marker.callout } } } if (option.iconPath) { img.src = getRealPath(option.iconPath) } else { console.error('Marker.iconPath is required.') } } function addMarker(props: Props) { marker = new maps.Marker({ map: map as any, flat: true, autoRotation: false, }) updateMarker(props) const MapsEvent = (maps as QQMaps | GoogleMaps).event || (maps as AMap.NameSpace).Event MapsEvent.addListener(marker, 'click', () => { const callout = marker.callout if (callout && !callout.alwaysVisible) { if (getIsAMap()) { callout.visible = !callout.visible if (callout.visible) { marker.callout!.createAMapText() } else { marker.callout!.removeAMapText() } } else { callout.set('visible', !callout.visible) if (callout.visible) { const div = callout.div! const parent = div.parentNode! parent.removeChild(div) parent.appendChild(div) } } } if (id) { trigger('markertap', {} as Event, { markerId: Number(id), latitude: props.latitude, longitude: props.longitude, }) } }) } addMarker(props as Props) watch(props, updateMarker) }) if (id) { const addMapChidlContext: AddMapChidlContext = inject( 'addMapChidlContext' ) as AddMapChidlContext const removeMapChidlContext: RemoveMapChidlContext = inject( 'removeMapChidlContext' ) as RemoveMapChidlContext const context = { id: id, translate(data: TranslateMarkerOptions) { onMapReady((map, maps, trigger) => { const destination = data.destination const duration = data.duration const autoRotate = !!data.autoRotate let rotate: number = Number(data.rotate) || 0 let rotation = 0 if ('getRotation' in marker) { rotation = marker.getRotation() } const a = marker.getPosition() const b = new (maps as QQMaps | GoogleMaps).LatLng( destination.latitude, destination.longitude ) const distance = ( maps as QQMaps | GoogleMaps ).geometry.spherical.computeDistanceBetween(a as any, b as any) / 1000 const time = (typeof duration === 'number' ? duration : 1000) / (1000 * 60 * 60) const speed = distance / time const MapsEvent = (maps as QQMaps | GoogleMaps).event || (maps as AMap.NameSpace).Event const movingEvent = MapsEvent.addListener( marker, 'moving', (e: any) => { const latLng = e.latLng const label = marker.label if (label) { ;(label as QLabel).setPosition(latLng) } const callout = marker.callout if (callout) { callout.setPosition(latLng) } } ) const event = MapsEvent.addListener(marker, 'moveend', () => { event.remove() movingEvent.remove() marker.lastPosition = a as QLatLng marker.setPosition(b as any) const label = marker.label if (label) { ;(label as QLabel).setPosition(b as QLatLng) } const callout = marker.callout if (callout) { callout.setPosition(b) } const cb = data.animationEnd if (isFunction(cb)) { cb() } }) let lastRtate = 0 if (autoRotate) { if (marker.lastPosition) { lastRtate = ( maps as QQMaps | GoogleMaps ).geometry.spherical.computeHeading( marker.lastPosition as any, a as any ) } rotate = (maps as QQMaps | GoogleMaps).geometry.spherical.computeHeading( a as any, b as any ) - lastRtate } if ('setRotation' in marker) { marker.setRotation(rotation + rotate) } if ('moveTo' in marker) { marker.moveTo(b as QLatLng, speed) } else { marker.setPosition(b as GLatLng) MapsEvent.trigger(marker, 'moveend', {}) } }) }, } addMapChidlContext(context) onUnmounted(() => removeMapChidlContext(context)) } onUnmounted(removeMarker) return () => { return null } }, })