UNPKG

tg-map-vue3

Version:

封装 百度地图, Google地图, Here地图(未完成), Maptalks 的Vue3组件库

1 lines 111 kB
{"version":3,"file":"tg-map.cjs","sources":["../src/utils/vue-utils.ts","../src/components/map-mixin.ts","../src/components/TgMap.vue","../src/components/TgMapWidget.vue","../src/components/controls/TgCustomControl.vue","../src/components/controls/TgMapTypeControl.vue","../src/components/controls/TgScaleControl.vue","../src/utils/hooks.ts","../src/components/map-hooks.ts","../src/components/controls/TgStreetViewControl.vue","../src/components/controls/TgZoomControl.vue","../src/components/extra/TgHeatmap.vue","../src/components/extra/TgMarkerClusterer.vue","../src/components/layers/TgTrafficLayer.vue","../src/components/overlays/TgCircle.vue","../src/components/overlays/TgElementOverlay.vue","../src/components/overlays/TgMarker.vue","../src/components/overlays/TgInfoBox.vue","../src/components/overlays/TgInfoWindow.vue","../src/components/overlays/TgLabel.vue","../src/components/overlays/TgPolygon.vue","../src/components/overlays/TgPolyline.vue","../src/components/overlays/TgRectangle.vue","../src/utils/lifecycle-log.ts","../src/components/index.ts"],"sourcesContent":["import { noop } from 'tg-commons'\nimport { Strings, type AbstractConstructor, type EventCallback, type KeysMatching, type StringEnumLike, type StringEnumValue, type Thing } from 'tg-map-core'\nimport {\n computed,\n createCommentVNode,\n type Component,\n type ComponentOptions,\n type ComponentPublicInstance,\n type ComputedRef,\n type Prop,\n type PropType,\n type Ref,\n type RendererElement,\n type RendererNode,\n type Slots,\n type VNode,\n type WritableComputedOptions,\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n type reactive,\n} from 'vue'\n/** 组件选项中的hook名 */\nexport type VueHookName = ExcludeSubtype<\n KeysMatching<OmitStartsWith<RemoveIndex<ComponentOptions>, '_'>, { (): void } | undefined>,\n 'serverPrefetch' | 'emits' | 'computed' | 'extends' | 'methods' | 'call' | undefined\n>\n\nexport type VueHookFunction = (this: ComponentPublicInstance) => void\n\n/**\n * 模仿Vue的计算属性的写法\n * @see ComputedOptions\n */\nexport type Property<T> = T | { (): T } | { get(): T, set?(value: T): void }\n\n/**\n * ## Vue2\n * 和typed()联合使用, 写成: `props: typed<Props<Options>>({...})`, 可以将`Options`作为属性的类型声明,\n * ### 注意\n * 1. 对于`Options`中可选的属性, 漏了VSCode不会报错...要小心\n * 2. `...`中理论上来说只要写Vue的纯js式的类型验证信息(Boolean/Number/String/Object/...)就行了, 并且还有ts的类型检查\n * 3. 对于`Options`中可选的`boolean`属性, 若将类型验证信息写为`Boolean`时, 该属性的默认值并不是`undefined`而是`false`, 这和它的类型声明(`boolean | undefined`)不匹配, 推荐写成`optionalProp(Boolean)`\n * 4. 因为存在上述特殊情况, 推荐使用{@link requiredProp},{@link optionalProp}等快捷方法写类型验证信息, 方法的传入参数直接写Vue的纯js式的类型验证信息, 就行了\n *\n * ## Vue3\n * 需要用`props: {...} satisfies Props<Options>`替代`typed()`, 防止{@link NotNullProp}被Vue看成必选的属性\n * */\nexport type Props<T> = {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n [K in keyof T]-?: T[K] extends {} ? NotNullProp<T[K]> : Prop<Exclude<T[K], undefined>>\n}\n/** 表示一个非空的Prop, `required`为true 或者 设置了`default`的{@link Prop}, 才是非空的Prop */\ntype NotNullProp<T = any, D = T> = { type: PropType<T> } & ({ required: true } | { default: D | (() => D) })\n\nexport function stringEnumProp<E extends StringEnumLike>(enumObject: E, defaultValue: E[keyof E]): { type: PropType<StringEnumValue<E>>, default: E[keyof E] }\nexport function stringEnumProp<E extends StringEnumLike>(enumObject: E): { type: PropType<StringEnumValue<E>> }\n/**\n * 注意: 由于使用了`Object.values(enumObject)`来获取所有枚举值\n * - 若enumObject为数字枚举, 获取出来的值会同时包含枚举名和枚举值, 故不支持\n * - 若enumObject同时是命名空间名, 获取出来的值会包含命名空间上的方法, 应当避免\n *\n * @param enumObject 枚举对象\n * @param defaultValue 默认值\n * @see https://frontendsociety.com/using-a-typescript-interfaces-and-types-as-a-prop-type-in-vuejs-508ab3f83480\n * */\nexport function stringEnumProp<E extends StringEnumLike>(enumObject: E, defaultValue?: E[keyof E]): Prop<StringEnumValue<E>> {\n return {\n type: String as any,\n default: defaultValue,\n validator: (value: E[keyof E]) => Object.values(enumObject).includes(value),\n }\n}\n\nexport function stringUnionPropFromValues<T extends string>(values: readonly T[], defaultValue: T): { type: PropType<T[][number]>, default: T }\nexport function stringUnionPropFromValues<T extends string>(values: readonly T[]): { type: PropType<T[][number]> }\n/** 使用方法重载, 声明`是否提供defaultValue`和`返回值是否可undefined`的关系_(:3」∠)_ */\nexport function stringUnionPropFromValues<T extends string>(values: readonly T[], defaultValue?: T): Prop<T[][number]> {\n return {\n type: String as any,\n default: defaultValue,\n validator: (value: T) => values.includes(value),\n }\n}\n\nexport function requiredProp<T>(type: PropType<T>): { type: PropType<T>, required: true } {\n return {\n type: type,\n required: true,\n }\n}\n\nexport function optionalProp<T>(type: PropType<T>, defaultValue: T | (() => T)): { type: PropType<T>, default: T }\nexport function optionalProp<T>(type: PropType<T>): { type: PropType<T> }\nexport function optionalProp<T>(type: PropType<T>, defaultValue?: T | (() => T)): Prop<T> {\n return {\n type: type,\n default: defaultValue,\n }\n}\n\n/**\n * 保存在this上的属性\n * @param defaultValues 属性的默认值\n *\n * @deprecated Vue3中已经不支持`cache: false`的计算属性, 使用setup中返回普通的值替代\n */\nexport function computedSaveOnThis<T>(defaultValues: { [K in keyof T]: T[K] }): { [K in keyof T]: WritableComputedOptions<T[K]> } {\n const accessors: any = {}\n Object.entries(defaultValues).forEach(([key, defaultValue]) => {\n const name = `__${key}`\n accessors[key] = {\n get() {\n const value = this[name]\n return value === undefined ? defaultValue : value\n },\n set(value: any) {\n this[name] = value\n },\n // Vue3已经不支持该属性\n // cache: false\n }\n })\n return accessors\n}\n\n// 这个对象是可以共享的~~\nconst sharedPropertyDefinition: PropertyDescriptor = {\n enumerable: true,\n configurable: true,\n get: noop,\n set: noop,\n}\n\n/**\n * 模仿Vue的计算属性的写法, 将方法转换成属性\n * @deprecated 在Vue2时用来将provide的方法转换成属性, 方便使用, Vue3中不生效, 故废弃\n */\nexport function createPropertyObject(obj: any): any {\n Object.keys(obj).forEach(key => {\n const value = obj[key]\n let redefined = false\n if (typeof value === 'function') {\n sharedPropertyDefinition.get = value\n sharedPropertyDefinition.set = noop\n redefined = true\n } else if (typeof value === 'object' && typeof value.get === 'function') {\n sharedPropertyDefinition.get = value.get\n sharedPropertyDefinition.set = value.set\n redefined = true\n }\n if (redefined) {\n obj[key] = Object.defineProperty(obj, key, sharedPropertyDefinition)\n }\n })\n return obj\n}\n\n/**\n * 相比{@link createPropertyObject}增加类型信息\n * @see createPropertyObject\n * @deprecated Vue3中没用\n * */\nexport function createPropertyObjectTyped<T>(obj: { [K in keyof T]: Property<T[K]> }): T {\n return createPropertyObject(obj)\n}\n\n/**\n * 创建空节点\n *\n * Vue2中使用`this._e()`创建空节点: https://github.com/vuejs/vue/blob/b6247fc9d7442c50d60ccf366a7cb183a4d02129/src/core/instance/render-helpers/index.js#L28\n *\n * Vue3中等价的实现是{@link createCommentVNode}: https://github.com/vuejs/core/blob/650f5c26f464505d9e865bdb0eafb24350859528/packages/runtime-core/src/compat/instance.ts#L162\n */\nexport function createEmptyVNode(): VNode {\n return createCommentVNode()\n}\n/**\n * @see https://github.com/vuejs/vue/blob/43b98fe25151b0b6bacd36f3ee27c5d61add5fdb/packages/weex-vue-framework/factory.js/#L2906\n * */\nexport function callHook(vm: ComponentPublicInstance, hookName: VueHookName) {\n // $options中的每个hook函数被合并成一个数组了\n const hooks: Array<VueHookFunction> | undefined = (vm.$options as any)[hookName]\n if (!hooks) {\n return\n }\n for (const hook of hooks) {\n hook.call(vm)\n }\n}\n\n/**\n * {@link import('./mapped-types.ts').safeAsComponent}\n * @deprecated Vue3已经不能遍历子组件的实例了, 所以这个方法目前没用\n */\nexport function findByComponentType<T extends Component & AbstractConstructor>(\n arr: ComponentPublicInstance[],\n component: T,\n): InstanceType<T> | undefined {\n for (const item of arr) {\n // TO-DO: 2023/03/16 ipcjs 有没有更好的判断组件实例的方式?\n // => 目前没找到\n if (item.$options.name === component.name) {\n return item as InstanceType<T>\n }\n }\n return undefined\n}\n\n/**\n * 通过构造slots内容的方式, 提取出slots中对应{@link component}的VNode, {@link VNode.props}就是当前给组件设置的属性值\n *\n * 注意, 该方法并无法获取到{@link component}的实例\n *\n * @see https://stackoverflow.com/questions/64154002/vue-3-how-to-get-information-about-children\n */\nexport function extractVNodeFromSlotsByComponent<Props>(slots: Slots, component: ComponentOptions<Props>) {\n if (!slots.default) return undefined\n\n const nodeList = slots.default()\n for (const node of nodeList) {\n if ((node?.type as Component).name === component.name) {\n return node as VNode<RendererNode, RendererElement, Props>\n }\n }\n return undefined\n}\n\n/**\n * Vue3中 class/style/未声明的事件/未声明的属性 都会集合到`this.$attrs`中\n * 默认情况下(`inheritAttrs`为true), 将被透传给组件的根节点\n *\n * 而Vue2, `this.$attrs`值包含 未声明的属性, 所有事件放到了`this.$listeners`中\n *\n * 这里模仿Vue2的行为, 从{@link attrs}中拆分出未声明的事件, 方便将他们透传给地图的对象\n * 同时若传入{@link props}, 也会从它里面拆分出 声明的事件类型的属性\n *\n * ## 如何给事件添加类型信息\n *\n * - 声明在`emits`中的事件, 会给emit方法添加类型信息, 但没办法在运行时判断是否有被设置\n * - 声明在`props`中的事件, 不会.................. , 但可以通过读取`this.$props`判断是否被设置\n *\n * 所有我们最终选择用{@link EventProps}在`props`中声明事件, 同时用{@link EventEmits}在`emits`上声明事件的类型,\n * 达到既有类型信息, 又能判断是否存在的效果, 写法如下:\n * ```\n * {\n * props: {...} satisfies EventProps<TgMapEmits>,\n * emits: undefined as any as EventEmits<TgMapEmits>,\n * }\n * ```\n *\n * ## 参考\n * - https://cn.vuejs.org/guide/components/attrs.html#disabling-attribute-inheritance\n * - https://v3-migration.vuejs.org/zh/breaking-changes/listeners-removed.html\n * - https://eslint.vuejs.org/rules/require-explicit-emits.html#options\n */\nexport function splitAttrs(attrs: Record<string, unknown>, props?: Record<string, unknown>) {\n const listeners: Record<string, unknown> = {}\n const binds: Record<string, unknown> = {}\n const listenerProps: Record<string, unknown> = {}\n Object.keys(attrs).forEach(key => {\n if (key.startsWith('on')) {\n listeners[Strings.pascal2kebab(key.substring(2))] = attrs[key]\n } else {\n binds[key] = attrs[key]\n }\n })\n if (props) {\n Object.keys(props).forEach(key => {\n if (key.startsWith('on')) {\n listenerProps[Strings.pascal2kebab(key.substring(2))] = props[key]\n }\n })\n }\n return {\n /** class/style/未声明的属性 等 */\n binds,\n /** 未声明的事件 */\n listeners,\n /** 声明在`props`中的事件 */\n listenerProps,\n }\n}\n\n/** 响应式的{@link splitAttrs} */\nexport function useSplittedAttrs(attrs: Record<string, unknown>, props?: Record<string, unknown>) {\n return computed(() => splitAttrs(attrs, props))\n}\n\n/**\n * 事件类型的属性\n * @see splitAttrs\n * */\nexport type EventProps<Emits> = Emits extends string\n // 转换 简单的String Union, 类型使用Function, 是为了简便, 省得写详细的类型信息\n // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type\n ? { [K in Emits as `on${Capitalize<K>}`]: Prop<Function> }\n // 转换 事件名=>事件类型的映射\n : {\n [K in keyof Emits as K extends string ? `on${Capitalize<K>}` : never]: Prop<EventCallback<Emits[K]>>\n }\n\n/**\n * 用来声明事件的类型信息\n * @see splitAttrs\n * */\nexport type EventEmits<T> = {\n [K in keyof T]: (event: T[K]) => boolean\n}\n\n/** 给事件类型的属性标注类型 */\nexport function eventProp<E = any>(): Prop<EventCallback<E>> {\n return Function as PropType<EventCallback<E>>\n}\n\nexport const useEventLogMethods = () => {\n let prevType: Thing | undefined\n let prevTime: number | undefined\n /**\n * @event 一般来说传{@link Tg.Event}对象, 但传其他对象也不会报错\n */\n function eventLog(event: Thing | null | undefined) {\n console.log(event)\n prevType = event && (event as any).type\n prevTime = Date.now()\n }\n /** @see eventLog */\n function eventLogLess(this: any, event: Thing | null | undefined) {\n if ((event && (event as any).type) !== prevType || Date.now() - (prevTime ?? 0) > 1000) {\n eventLog(event)\n }\n }\n return { eventLog, eventLogLess }\n}\n\n/**\n * 值可能是Ref的对象\n * 可以用来给{@link reactive}的参数做类型约束\n * */\nexport type MaybeWrapRefs<T> = {\n [P in keyof T]: Ref<T[P]> | T[P] | ComputedRef<T[P]>\n}\n\n/** 判断{@link instance}是否是{@link component}的实例 */\nexport function isComponentByType<C extends Component & AbstractConstructor>(\n instance: ComponentPublicInstance | null,\n component: C,\n): instance is InstanceType<C> {\n // TODO: 2023/03/15 ipcjs 看有没有其他更好的方式判断组件类型\n return instance?.$options.name === component.name\n}\n\n/** 安全转换组件类型, 若不能转换会返回`undefined` */\nexport function safeAsComponent<C extends Component & AbstractConstructor>(\n instance: ComponentPublicInstance | null,\n component: C,\n): InstanceType<C> | undefined {\n return isComponentByType(instance, component) ? instance : undefined\n}\n\n/**\n * 查找[startInstance, endComponent)之间的第一个类型为{@link component}的父组件\n * @param component 查找的组件\n * @param startInstance 查找的开始实例, 包含\n * @param endComponent 查找的终止组件, 不包含\n * @returns\n */\nexport function findAncestorComponentByType<C extends Component & AbstractConstructor>(\n component: C,\n startInstance: ComponentPublicInstance | null,\n endComponent?: Component & AbstractConstructor,\n): InstanceType<C> | undefined {\n let instance = startInstance\n while (instance) {\n if (isComponentByType(instance, component)) {\n return instance\n } else if (endComponent && isComponentByType(instance, endComponent)) {\n return undefined\n } else {\n instance = instance.$parent\n }\n }\n return undefined\n}\n","import type { BaseMap } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { callHook, createEmptyVNode } from '../utils/vue-utils'\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nimport type { useTgMapInner } from './map-hooks'\n\nexport const MIXIN_MAP_NAME = '$map'\nexport const MIXIN_HOOK_CREATE = 'onCreate'\nexport const MIXIN_HOOK_DESTROY = 'onDestroy'\n\ndeclare module 'vue' {\n interface ComponentCustomProperties {\n /** 混合了MapMixin的Vue才会有该属性 */\n [MIXIN_MAP_NAME]: BaseMap,\n /** 混合了MapMixin的Vue才会有该属性, 会依次调用onDestroy + onCreate, 你应该在这两个hook中分别实现 移除 和 创建并添加 覆盖物的逻辑 */\n recreate: () => void,\n }\n\n interface ComponentCustomOptions {\n [MIXIN_HOOK_CREATE]?(): void\n [MIXIN_HOOK_DESTROY]?(): void\n }\n}\n\n/** @deprecated 使用{@link useTgMapInner}替代 */\nconst MapMixin = defineComponent({\n inject: [MIXIN_MAP_NAME],\n mounted() {\n // children先执行, parent后执行\n // parent中可以读取到this.$children, 参见: TgMarkerClusterer\n callHook(this, MIXIN_HOOK_CREATE)\n },\n beforeUnmount() {\n // parent先执行, children后执行\n callHook(this, MIXIN_HOOK_DESTROY)\n },\n methods: {\n recreate() {\n callHook(this, MIXIN_HOOK_DESTROY)\n callHook(this, MIXIN_HOOK_CREATE)\n },\n },\n render() {\n return createEmptyVNode()\n },\n})\n\nexport default MapMixin\n","<template>\n <div class=\"tg-map\" v-bind=\"attrs.binds\">\n <div\n ref=\"map\"\n :class=\"{\n 'tg-map__map': true,\n [`tg-map__map--hide-logo-${type}`]: hideLogo\n }\"\n ></div>\n <div v-if=\"false\" style=\"position:absolute;right:0px;top:0px;\">\n props:{{ propsJson }}\n </div>\n <template v-if=\"map\">\n <slot></slot>\n </template>\n <slot name=\"overlay\"></slot>\n </div>\n</template>\n<script lang=\"ts\">\nimport { typed } from 'tg-commons'\nimport { bindEvents, BuildInMapTypeId, GestureHandlingOptions, InfoWindowModeValues, LatLng, loadCachedMap, MapType, Objects, TgMapFactory, TgMapType, unwrapStringEnumValue, type AbstractMap, type AbstractMapEventMap, type BaseMap, type MapOptions, type MapStyle, type StringEnumValue, type WrapStringEnumValue } from 'tg-map-core'\nimport { computed, defineComponent, markRaw, watch, type PropType } from 'vue'\nimport { eventProp, optionalProp, requiredProp, stringEnumProp, stringUnionPropFromValues, useSplittedAttrs, type EventEmits, type EventProps, type Props } from '../utils/vue-utils'\nimport { MIXIN_MAP_NAME } from './map-mixin'\n\ntype TgMapEmits = {\n load: AbstractMap,\n error: any,\n 'update:center': LatLng,\n 'update:current-center': LatLng,\n 'update:zoom': number,\n 'update:map-type': MapType,\n 'update:map-type-id': string,\n 'update:last-center': LatLng,\n}\n\nexport const tgMapProps = {\n /** type没做响应式, 但外部可以把type作为tg-map的key, 让type修改时完全重建tg-map */\n type: stringEnumProp(TgMapType, TgMapType.google),\n /**\n * 当对该属性使用双向绑定时, 改变center将触发update:center又反过来触发center改变, 最终导致无限循环...\n * 当前通过setCenter()时判断center是否有改变来避免该问题, 但为了避免可能存在的问题, 另外设计了如下机制:\n * - :center-sync-delay=\"300\": 延时同步center, 防止center的值更新过快\n * - :current-center.sync=\"currentCenter\": 实时获取center的值\n * - :last-center.sync=\"center\": 获取tg-map销毁时的最后的center的值, 使用这种方式可以做到type切换时保留中心点位置\n * @see AbstractMapEventMap.center-changed\n */\n center: requiredProp(LatLng),\n /**\n * 同步center的延时\n * @default 300\n * @see center\n */\n centerSyncDelay: optionalProp(Number, 300),\n /**\n * 仅用来获取center的值, 需要设置center的值请使用`center`属性\n * @see center\n * @deprecated 用的太少, 故maptalks未实现该属性, 要实时获取中心点, 请将{@link centerSyncDelay}设为0\n */\n currentCenter: optionalProp(LatLng),\n /**\n * 仅用于获取tg-map销毁时的最后的center的值\n * @see center\n * @deprecated 用的太少, 故maptalks未实现该属性\n */\n lastCenter: optionalProp(LatLng),\n zoom: requiredProp(Number),\n /**\n * 该属性不会响应式更新, 要让它立即生效, 请参考{@link file://./../views/map/InfoDemo.vue#L3}\n * @see MapOptions.infoWindowMode\n */\n infoWindowMode: stringUnionPropFromValues(InfoWindowModeValues),\n gestureHandling: stringEnumProp(GestureHandlingOptions),\n fractionalZoom: optionalProp(Boolean),\n minZoom: optionalProp(Number),\n maxZoom: optionalProp(Number),\n mapStyle: optionalProp(Object as PropType<MapStyle>),\n mapTypeId: stringEnumProp(BuildInMapTypeId),\n /** 地图类型对象, 优先级比mapTypeId高 */\n mapType: optionalProp(Object),\n hideLogo: optionalProp(Boolean),\n onLoad: eventProp(),\n 'onUpdate:center': eventProp(),\n 'onUpdate:current-center': eventProp(),\n 'onUpdate:last-center': eventProp(),\n 'onUpdate:map-type': eventProp(),\n 'onUpdate:map-type-id': eventProp(),\n 'onUpdate:zoom': eventProp(),\n onError: eventProp(),\n} satisfies Props<WrapStringEnumValue<Omit<MapOptions, 'buildInMapTypeId'>, {\n gestureHandling: typeof GestureHandlingOptions,\n}> & {\n type: StringEnumValue<typeof TgMapType>,\n currentCenter?: LatLng,\n lastCenter?: LatLng,\n centerSyncDelay: number,\n mapTypeId?: StringEnumValue<typeof BuildInMapTypeId>,\n mapType?: MapType\n hideLogo?: boolean\n}> & EventProps<TgMapEmits>\n\nexport type TgMapEventEmits = EventEmits<AbstractMapEventMap & TgMapEmits>\n\nexport default defineComponent({\n name: 'tg-map',\n provide() {\n return {\n [MIXIN_MAP_NAME]: computed(() => this.map),\n }\n },\n inheritAttrs: false,\n props: tgMapProps,\n /** 声明事件的类型信息, 详见: {@link splitAttrs} */\n emits: undefined as any as TgMapEventEmits,\n setup(props, { attrs }) {\n return {\n attrs: useSplittedAttrs(attrs, props),\n centerSyncTimeoutId: undefined as TimeoutId | undefined,\n }\n },\n data() {\n return {\n map: typed<BaseMap>(),\n isDestroyed: false,\n }\n },\n computed: {\n propsJson() {\n return Objects.toJsonSafely(this.$props)\n },\n },\n watch: {\n gestureHandling(value?: StringEnumValue<typeof GestureHandlingOptions>) {\n if (value) {\n this.map?.setGestureHandling(unwrapStringEnumValue(value, GestureHandlingOptions))\n }\n },\n minZoom(value) {\n this.map?.setMinZoom(value)\n },\n maxZoom(value) {\n this.map?.setMaxZoom(value)\n },\n mapStyle(value?: MapStyle) {\n this.map?.setMapStyle(value)\n },\n mapTypeId(value?: StringEnumValue<typeof BuildInMapTypeId>) {\n if (value) {\n this.map?.setBuildInMapTypeId(unwrapStringEnumValue(value, BuildInMapTypeId))\n }\n },\n mapType(value?: MapType) {\n this.map?.setMapType(value ?? MapType.NORMAL)\n },\n hideLogo(value?: boolean) {\n this.map?.setHideLogo(value ?? false)\n },\n fractionalZoom(value?: boolean) {\n this.map?.setFractionalZoom(value ?? false)\n },\n },\n async mounted() {\n // type的值需要局部化, 防止外部修改\n const type = unwrapStringEnumValue(this.type, TgMapType)\n try {\n await loadCachedMap(type)\n } catch (e) {\n this.$emit('error', e)\n return\n }\n if (this.isDestroyed) {\n console.warn(`tg-map(type=${type})已销毁, 不需要继续执行`)\n return\n }\n // options不要直接传this, 因为createMap方法内部会修改options...\n const map = TgMapFactory.createMap(type, this.$refs.map as HTMLElement, /* options: */{\n center: this.center,\n zoom: this.zoom,\n infoWindowMode: this.infoWindowMode,\n gestureHandling: unwrapStringEnumValue(this.gestureHandling, GestureHandlingOptions),\n minZoom: this.minZoom,\n maxZoom: this.maxZoom,\n mapStyle: this.mapStyle,\n buildInMapTypeId: unwrapStringEnumValue(this.mapTypeId, BuildInMapTypeId),\n hideLogo: this.hideLogo,\n fractionalZoom: this.fractionalZoom,\n })\n // map是复杂对象, 不需要转换成响应式对象\n this.map = markRaw(map)\n if (this.mapType) {\n map.setMapType(this.mapType)\n }\n\n // 未声明ready事件, 所以需要从attrs.listeners中查找是否存在\n if ('ready' in this.attrs.listeners) {\n console.error('地图载入完成的事件已经从ready改为了load, 请手动修改')\n }\n bindEvents(this.attrs.listeners, map, this.$emit, /* excludes: */['load'])\n // 需要先发送load(载入完成)事件, 保证外部能够获取到map对象\n this.$emit('load', map)\n\n // Baidu地图的setZoom/Center是区分顺序的, 若同时修改它们, 必须先setZoom再setCenter, 否则中心点会出现偏移\n // Google地图没这个问题\n // 为了规避该问题, 同时监听它们两个, 并按顺序执行set方法\n watch([\n () => this.zoom,\n () => this.center, //\n ], ([zoom, center], [oldZoom, oldCenter]) => {\n if (zoom !== oldZoom) {\n import.meta.env.DEV && console.debug('<tg-map> setZoom', center)\n map.setZoom(zoom)\n }\n if (center !== oldCenter) {\n const currentCenter = map.getCenter()\n if (!currentCenter.equals(center)) {\n import.meta.env.DEV && console.debug('<tg-map> setCenter', center)\n map.setCenter(center)\n }\n }\n })\n\n // 处理v-model(双向绑定)\n if (this.attrs.listenerProps['update:center']) {\n const updateCenter = () => this.$emit('update:center', map.getCenter())\n map.addEventListener('center-changed', () => {\n if (this.centerSyncDelay > 0) {\n clearTimeout(this.centerSyncTimeoutId)\n this.centerSyncTimeoutId = setTimeout(updateCenter, this.centerSyncDelay)\n } else {\n updateCenter()\n }\n })\n }\n if (this.attrs.listenerProps['update:current-center']) {\n // 初始值也得emit出去\n this.$emit('update:current-center', this.center)\n map.addEventListener('center-changed', () => {\n this.$emit('update:current-center', map.getCenter())\n })\n }\n if (this.attrs.listenerProps['update:zoom']) {\n map.addEventListener('zoom-changed', () => {\n this.$emit('update:zoom', map.getZoom())\n })\n }\n const isUpdateMapType = this.attrs.listenerProps['update:map-type']\n const isUpdateMapTypeId = this.attrs.listenerProps['update:map-type-id']\n if (isUpdateMapType || isUpdateMapTypeId) {\n map.addEventListener('map-type-changed', () => {\n const mapType = map.getMapType()\n isUpdateMapType && this.$emit('update:map-type', mapType)\n isUpdateMapTypeId && this.$emit('update:map-type-id', mapType.id)\n })\n }\n },\n unmounted() {\n this.isDestroyed = true\n if (this.map) {\n if (this.attrs.listenerProps['update:last-center']) {\n this.$emit('update:last-center', this.map.getCenter())\n }\n if (this.attrs.listenerProps['update:center']) {\n clearTimeout(this.centerSyncTimeoutId)\n // 最终的值一定要更新出去, 保证切换地图时center的位置能够保持\n this.$emit('update:center', this.map.getCenter())\n }\n }\n },\n methods: {\n },\n})\n</script>\n\n<style lang=\"scss\">\n.tg-map {\n height: 100%;\n position: relative;\n\n &__map {\n width: 100%;\n height: 100%;\n\n &--hide-logo-google {\n .gm-style {\n // 隐藏logo\n a[target=\"_blank\"]:has(>div>img) {\n display: none !important;\n }\n\n // 隐藏右下角的Keyboard shortcuts/Terms of Use等\n &>div:last-child>div:last-child>div {\n display: none;\n }\n }\n }\n\n &--hide-logo-baidu {\n // 隐藏logo\n div.anchorBL>a[target=\"_blank\"]>img {\n display: none;\n }\n\n // 隐藏左下角的版权信息\n div.BMap_cpyCtrl {\n display: none;\n }\n }\n }\n}\n</style>\n","<template>\n <div :class=\"$options.name\" :style=\"{ left: dimen(left), top: dimen(topValue), right: dimen(right), bottom: dimen(bottom) }\">\n <slot></slot>\n </div>\n</template>\n\n<script lang=\"ts\">\nimport { dimen } from 'tg-map-core'\nimport { defineComponent } from 'vue'\n/** TgMap上的Widget, 只是对绝对布局进行简单的封装 */\nexport default defineComponent({\n name: 'tg-map-widget',\n props: {\n left: {\n type: [Number, String],\n default: null,\n },\n top: {\n type: [Number, String],\n default: null,\n },\n right: {\n type: [Number, String],\n default: null,\n },\n bottom: {\n type: [Number, String],\n default: null,\n },\n },\n computed: {\n topValue() {\n // 防止默认看不到组件\n return this.top == null && this.bottom == null ? 0 : this.top\n },\n },\n methods: {\n dimen,\n },\n})\n</script>\n<style lang=\"scss\">\n.tg-map-widget {\n position: absolute;\n}\n</style>\n","<template>\n <div :class=\"$options.name\">\n <slot />\n </div>\n</template>\n<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ControlPosition, CustomControl, type StringEnumValue, unwrapStringEnumValue } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { type Props, safeAsComponent, stringEnumProp } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\nimport TgMap from '../TgMap.vue'\n\nexport default defineComponent({\n name: 'tg-custom-control',\n mixins: [MapMixin],\n props: {\n position: stringEnumProp(ControlPosition, ControlPosition.RIGHT_BOTTOM),\n } satisfies Props<{\n position: StringEnumValue<typeof ControlPosition>\n }>,\n setup() {\n return {\n control: lateinit<CustomControl>(),\n }\n },\n watch: {\n position() {\n this.recreate()\n },\n },\n onCreate() {\n if (!safeAsComponent(this.$parent, TgMap)) {\n console.warn('请将tg-custom-control放到tg-map中')\n }\n const { $el } = this\n const C = class extends CustomControl {\n onCreateElement() {\n return $el as HTMLElement\n }\n }\n this.control = new C(unwrapStringEnumValue(this.position, ControlPosition))\n this.$map.addCustomControl(this.control)\n },\n onDestroy() {\n this.$map.removeCustomControl(this.control)\n },\n})\n</script>\n<style lang=\"scss\">\n// 当作为tg-map的直接child时不显示\n.tg-map > .tg-custom-control {\n display: none;\n}\n</style>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ControlPosition, type MapTypeControl, type MapTypeControlOptions, MapTypeControlType, unwrapStringEnumValue, type WrapStringEnumValue } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { optionalProp, type Props, stringEnumProp } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\n\nexport default defineComponent({\n name: 'tg-map-type-control',\n mixins: [MapMixin],\n props: {\n position: stringEnumProp(ControlPosition, ControlPosition.TOP_LEFT),\n type: stringEnumProp(MapTypeControlType),\n mapTypes: optionalProp(Array as any),\n } satisfies Props<WrapStringEnumValue<MapTypeControlOptions, {\n position: typeof ControlPosition,\n type: typeof MapTypeControlType\n }>>,\n setup() {\n return {\n control: lateinit<MapTypeControl>(),\n }\n },\n watch: {\n position(value) {\n this.control.setPosition(value)\n },\n type() {\n this.recreate()\n },\n mapTypes: {\n handler() {\n this.recreate()\n },\n deep: true,\n },\n },\n onCreate() {\n this.control = this.$map.createMapTypeControl(unwrapStringEnumValue(this))\n this.$map.addControl(this.control)\n },\n onDestroy() {\n this.$map.removeControl(this.control)\n },\n})\n\n</script>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ControlPosition, type ScaleControl, type ScaleControlOptions, type WrapStringEnumValue, unwrapStringEnumValue } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { type Props, stringEnumProp } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\nexport default defineComponent({\n name: 'tg-scale-control',\n mixins: [MapMixin],\n props: {\n position: stringEnumProp(ControlPosition, ControlPosition.BOTTOM_RIGHT),\n } satisfies Props<WrapStringEnumValue<ScaleControlOptions, {\n position: typeof ControlPosition\n }>>,\n setup() {\n return {\n control: lateinit<ScaleControl>(),\n }\n },\n watch: {\n position(value) {\n this.control.setPosition(value)\n },\n },\n onCreate() {\n this.control = this.$map.createScaleControl(unwrapStringEnumValue(this))\n this.$map.addControl(this.control)\n },\n onDestroy() {\n this.$map.removeControl(this.control)\n },\n})\n\n</script>\n","import { path } from 'tg-commons'\nimport { customRef, getCurrentInstance, toRaw, watch } from 'vue'\n\n/** 获取当前组件的名字 */\nexport function useComponentName(): string | undefined {\n const type = getCurrentInstance()?.type\n let name = type?.name ?? type?.__name\n if (!name) {\n const file = type?.__file\n if (file) {\n name = path.basename(file, '.vue')\n }\n }\n return name\n}\n\n/**\n * 将v-model转换成Ref变量, 并防止递归调用\n */\nexport function useLocalModel<P extends object, K extends keyof P & string, Name extends string>(\n props: P,\n key: K,\n emit: (name: Name, ...args: any[]) => void,\n) {\n return customRef<P[K]>((track, trigger) => {\n let local = props[key]\n watch(() => props[key], (v) => {\n if (local === toRaw(v)) {\n // 防止emit出去的值, 被设置回来\n return\n }\n local = v\n trigger()\n })\n\n return {\n get: () => {\n track()\n return local\n },\n set: (v) => {\n local = v\n emit(`update:${key}` as Name, v)\n },\n }\n })\n}\n","import { throwError } from 'tg-commons'\nimport type { AbstractMap } from 'tg-map-core'\nimport { inject, onBeforeUnmount, onMounted, type Ref } from 'vue'\nimport { useComponentName } from '../utils/hooks'\nimport { MIXIN_HOOK_CREATE, MIXIN_HOOK_DESTROY, MIXIN_MAP_NAME } from './map-mixin'\n\n/** 作为Vue3下, `MapMixin`的替代 */\nexport function useTgMapInner<MAP extends AbstractMap = AbstractMap>() {\n const mapRef = inject<Ref<MAP | undefined>>(MIXIN_MAP_NAME)\n let onCreate: VoidFunction | undefined\n let onDestroy: VoidFunction | undefined\n if (mapRef == null) throwError(`<${useComponentName() || 'unknown'}>必须作为<tg-map>的子代组件`)\n\n return {\n mapRef,\n /** 读取map对象, 只要放在<tg-map>里面的组件(除#overlay插槽外)都能够立即读取到map对象 */\n get map() {\n return mapRef.value ?? throwError('map尚未初始化')\n },\n /** 地图元素创建回调 */\n [MIXIN_HOOK_CREATE](hook: VoidFunction) {\n onMounted(hook)\n if (onCreate) throwError(`${MIXIN_HOOK_CREATE}只能调用一次`)\n onCreate = hook\n },\n /** 地图元素销毁回调 */\n [MIXIN_HOOK_DESTROY](hook: VoidFunction) {\n onBeforeUnmount(hook)\n if (onDestroy) throwError(`${MIXIN_HOOK_DESTROY}只能调用一次`)\n onDestroy = hook\n },\n /** 重新创建地图元素 */\n recreate() {\n onDestroy?.()\n onCreate?.()\n },\n }\n}\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ControlPosition, unwrapStringEnumValue, type BaseMap, type StreetViewControl, type StreetViewControlOptions, type WrapStringEnumValue } from 'tg-map-core'\nimport { defineComponent, watch } from 'vue'\nimport { createEmptyVNode, stringEnumProp, type Props } from '../../utils/vue-utils'\nimport { useTgMapInner } from '../map-hooks'\n/**\n * 街景控件, 当前只有google支持\n * */\nexport default defineComponent({\n name: 'tg-street-view-control',\n props: {\n position: stringEnumProp(ControlPosition, ControlPosition.RIGHT_BOTTOM),\n } satisfies Props<WrapStringEnumValue<StreetViewControlOptions, {\n position: typeof ControlPosition,\n }>>,\n setup(props) {\n let control = lateinit<StreetViewControl>()\n\n const { map, onCreate, onDestroy } = useTgMapInner<BaseMap>()\n\n onCreate(() => {\n control = map.createStreetViewControl(unwrapStringEnumValue(props))\n map.addControl(control)\n })\n\n onDestroy(() => {\n map.removeControl(control)\n })\n\n watch(() => props.position, (value) => {\n control.setPosition(unwrapStringEnumValue(value, ControlPosition))\n })\n return createEmptyVNode\n },\n})\n</script>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ControlPosition, unwrapStringEnumValue, type BaseMap, type WrapStringEnumValue, type ZoomControl, type ZoomControlOptions } from 'tg-map-core'\nimport { defineComponent, watch } from 'vue'\nimport { createEmptyVNode, optionalProp, stringEnumProp, type Props } from '../../utils/vue-utils'\nimport { useTgMapInner } from '../map-hooks'\n\nexport default defineComponent({\n name: 'tg-zoom-control',\n props: {\n position: stringEnumProp(ControlPosition, ControlPosition.RIGHT_BOTTOM),\n showZoomLevel: optionalProp(Boolean),\n } satisfies Props<WrapStringEnumValue<ZoomControlOptions, {\n position: typeof ControlPosition,\n }>>,\n setup(props) {\n let control = lateinit<ZoomControl>()\n\n const { map, onCreate, onDestroy } = useTgMapInner<BaseMap>()\n\n onCreate(() => {\n control = map.createZoomControl(unwrapStringEnumValue(props))\n map.addControl(control)\n })\n\n onDestroy(() => {\n map.removeControl(control)\n })\n\n watch(() => props.position, (value) => {\n control.setPosition(unwrapStringEnumValue(value, ControlPosition))\n })\n watch(() => props.showZoomLevel, (value) => {\n control.setShowZoomLevel(value ?? true)\n })\n return createEmptyVNode\n },\n})\n</script>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { BaseMap, Heatmap, type HeatmapOptions } from 'tg-map-core'\nimport { defineComponent, watch } from 'vue'\nimport { createEmptyVNode, optionalProp, requiredProp, type Props } from '../../utils/vue-utils'\nimport { useTgMapInner } from '../map-hooks'\n\nexport default defineComponent({\n name: 'tg-heatmap',\n props: {\n maxIntensity: optionalProp(Number),\n gradient: optionalProp(Object),\n opacity: optionalProp(Number),\n radius: optionalProp(Number),\n data: requiredProp(Array as any),\n } satisfies Props<HeatmapOptions>,\n setup(props) {\n const { map, onCreate, onDestroy } = useTgMapInner<BaseMap>()\n let heatmap = lateinit<Heatmap>()\n\n onCreate(() => {\n heatmap = map.createHeatmap(props)\n })\n onDestroy(() => {\n heatmap.remove()\n })\n\n watch(() => props.data, (v) => heatmap.setData(v))\n watch(\n [() => props.maxIntensity, () => props.gradient, () => props.opacity, () => props.radius],\n () => heatmap.setOptions(props),\n )\n\n return createEmptyVNode\n },\n})\n</script>\n","<template>\n <div :class=\"$options.name\">\n <slot />\n </div>\n</template>\n<script lang=\"ts\">\nimport { Arrays, lateinit, typed } from 'tg-commons'\nimport type { MarkerClusterer, MarkerClustererOptions, MarkerOverlay } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { type Props, optionalProp } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\nconst TgMarkerClusterer = defineComponent({\n name: 'tg-marker-clusterer',\n mixins: [MapMixin],\n // markers从this.$children中读取, 故需要省略(Omit)\n props: {\n gridSize: optionalProp(Number),\n maxZoom: optionalProp(Number),\n minClusterSize: optionalProp(Number),\n averageCenter: optionalProp(Boolean),\n styles: optionalProp(Array as any),\n stylesIndexCalculator: optionalProp(Function as any),\n zIndex: optionalProp(Number),\n } satisfies Props<Omit<MarkerClustererOptions, 'markers'>>,\n setup() {\n return {\n clusterer: lateinit<MarkerClusterer>(),\n markers: typed<MarkerOverlay[]>([]),\n pendingMarkers: typed<MarkerOverlay[]>([]),\n }\n },\n watch: {\n gridSize() {\n this.recreate()\n },\n maxZoom() {\n this.recreate()\n },\n minClusterSize() {\n this.recreate()\n },\n averageCenter() {\n this.recreate()\n },\n styles: {\n handler() {\n this.recreate()\n },\n deep: true,\n },\n stylesIndexCalculator() {\n this.recreate()\n },\n zIndex() {\n this.recreate()\n },\n },\n onCreate() {\n // console.log('TgMarkerClusterer.onCreate', this.markers)\n this.clusterer = this.$map.createMarkerClusterer(this)\n },\n onDestroy() {\n this.clusterer.clearMarkers(true)\n // 清空等待添加的markers\n this.pendingMarkers = []\n },\n methods: {\n /** TgMarker有可能在该组件未初始化之前调用, 需要通过该方法判断 */\n isInitiated(): boolean {\n return this.clusterer != null\n },\n performAddMarkers() {\n this.clusterer.addMarkers(this.pendingMarkers)\n this.pendingMarkers = []\n },\n onAddMarker(marker: MarkerOverlay) {\n this.markers.push(marker)\n if (this.isInitiated()) {\n if (!this.pendingMarkers.length) {\n this.$nextTick(this.performAddMarkers)\n }\n this.pendingMarkers.push(marker)\n }\n },\n onRemoveMarker(marker: MarkerOverlay) {\n if (this.isInitiated()) {\n if (!Arrays.remove(this.pendingMarkers, marker)) {\n this.clusterer.removeMarker(marker)\n }\n }\n Arrays.remove(this.markers, marker)\n },\n },\n})\n/** TgMarkerClusterer作为构造器, 创建的Vue实例类型 */\ntype TgMarkerClusterer = InstanceType<typeof TgMarkerClusterer>\nexport default TgMarkerClusterer\n</script>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { TrafficLayer } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport MapMixin from '../map-mixin'\nexport default defineComponent({\n name: 'tg-traffic-layer',\n mixins: [MapMixin],\n // 请直接使用v-if来控制它的显隐\n // 没有属性时, 写`{}`类型推断会有问题, 故注释掉\n // props: {} satisfies Props<{}>,\n setup() {\n return {\n layer: lateinit<TrafficLayer>(),\n }\n },\n watch: {\n\n },\n onCreate() {\n this.layer = new TrafficLayer()\n this.$map.addLayer(this.layer)\n },\n onDestroy() {\n this.$map.removeLayer(this.layer)\n },\n})\n\n</script>\n","<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { bindEvents, LatLng, type CircleEventMap, type CircleOptions, type CircleOverlay } from 'tg-map-core'\nimport { defineComponent, toRaw } from 'vue'\nimport { eventProp, optionalProp, requiredProp, useSplittedAttrs, type EventEmits, type EventProps, type Props } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\ntype TgCircleEmits = {\n 'update:center': LatLng,\n 'update:radius': number,\n}\nexport default defineComponent({\n name: 'tg-circle',\n mixins: [MapMixin],\n inheritAttrs: false,\n props: {\n center: requiredProp(LatLng),\n radius: requiredProp(Number),\n clickable: optionalProp(Boolean),\n editable: optionalProp(Boolean),\n strokeColor: optionalProp(String),\n strokeOpacity: optionalProp(Number),\n strokeWeight: optionalProp(Number),\n visible: optionalProp(Boolean),\n zIndex: optionalProp(Number),\n fillColor: optionalProp(String),\n fillOpacity: optionalProp(Number),\n 'onUpdate:center': eventProp(),\n 'onUpdate:radius': eventProp(),\n } satisfies Props<CircleOptions> & EventProps<TgCircleEmits>,\n emits: undefined as any as EventEmits<CircleEventMap & TgCircleEmits>,\n setup(props, { attrs }) {\n return {\n attrs: useSplittedAttrs(attrs, props),\n overlay: lateinit<CircleOverlay>(),\n emittedRadius: undefined as number | undefined,\n emittedCenter: undefined as LatLng | undefined,\n }\n },\n watch: {\n center(value) {\n if (this.emittedCenter != null) {\n const center = this.emittedCenter\n this.emittedCenter = undefined\n if (center === toRaw(value)) {\n // 防止emit出的center被设置回来\n return\n }\n }\n this.overlay.setCenter(value)\n },\n radius(value) {\n if (this.emittedRadius != null) {\n const radius = this.emittedRadius\n this.emittedRadius = undefined\n if (radius === value) {\n // 防止emit出的radius被设置回来\n return\n }\n }\n this.overlay.setRadius(value)\n },\n clickable() {\n this.recreate()\n },\n editable(value) {\n this.overlay.setEditable(value)\n },\n strokeColor(value) {\n this.overlay.setStrokeColor(value)\n },\n strokeOpacity(value) {\n this.overlay.setStrokeOpacity(value)\n },\n strokeWeight(value) {\n this.overlay.setStrokeWeight(value)\n },\n fillColor(value) {\n this.overlay.setFillColor(value)\n },\n fillOpacity(value) {\n this.overlay.setFillOpacity(value)\n },\n visible(value) {\n this.overlay.setVisible(value)\n },\n zIndex(value) {\n this.overlay.setZIndex(value)\n },\n },\n onCreate() {\n this.overlay = this.$map.createCircle(this)\n this.$map.addOverlay(this.overlay)\n bindEvents(this.attrs.listeners, this.overlay, this.$emit)\n if (this.attrs.listenerProps['update:center']) {\n this.overlay.addEventListener('center-changed', () => {\n this.$emit('update:center', this.emittedCenter = this.overlay.getCenter())\n })\n }\n if (this.attrs.listenerProps['update:radius']) {\n this.overlay.addEventListener('radius-changed', () => {\n this.$emit('update:radius', this.emittedRadius = this.overlay.getRadius())\n })\n }\n },\n onDestroy() {\n this.$map.removeOverlay(this.overlay)\n },\n})\n</script>\n","<template>\n <div :class=\"$options.name\" v-bind=\"$attrs\">\n <slot />\n </div>\n</template>\n<script lang=\"ts\">\nimport { lateinit } from 'tg-commons'\nimport { ElementOverlay, LatLng, MapPane, unwrapStringEnumValue, type BaseMap, type ElementOverlayOptions, type OverlayProjection, type WrapStringEnumValue } from 'tg-map-core'\nimport { defineComponent } from 'vue'\nimport { requiredProp, stringEnumProp, type Props } from '../../utils/vue-utils'\nimport MapMixin from '../map-mixin'\n\n/** ElementOverlay的简单实现 */\nclass SimpleElementOverlay extends ElementOverlay {\n constructor(\n map: BaseMap,\n options: ElementOverlayOptions,\n public content: HTMLElement,\n private position: LatLng,\n ) {\n super(map, options)\n }\n protected onCreate(): HTMLElement {\n return this.content\n }\n protected onDraw(projection: OverlayProjection): void {\n const point = projection.fromLatLngToOverlayPoint(this.position)\n this.content.style.left = point.x + 'px'\n this.content.style.top = point.y + 'px'\n }\n setPosition(position: LatLng) {\n this.position = position\n this.draw()\n }\n}\n\nconst TgElementOverlay = defineComponent({\n name: 'tg-element-overlay',\n mixins: [MapMixin],\n props: {\n mapPane: stringEnumProp(MapPane, MapPane.overlayMouseTarget),\n position: requiredProp(LatLng),\n } satisfies Props<WrapStringEnumValue<ElementOverlayOptions, {\n mapPane: typeof MapPane,\n }> & { position: LatLng }>,\n setup() {\n return {\n overlay: lateinit<SimpleElementOverlay>(),\n }\n },\n watch: {\n position(value) {\n this.overlay.setPosition(value)\n },\n mapPane() {\n this.recreate()\n },\n },\n methods: {\n content(): HTMLElement {\n return this.$el as HTMLElement\n },\n },\n onCreate() {\n this.overlay = new SimpleElementOverlay(this.$map, unwrapStringEnumValue(this), this.content(), this.position)\n this.$map.addElementOverlay(this.overlay)\n },\n onDestroy() {\n this.$map.removeElementOverlay(this.overlay)\n },\n})\ntype TgElementOverlay = InstanceType<typeof TgElementOverlay>\nexport default TgElementOverlay\n</script>\n<style lang=\"scss\">\n// 只有移到.tg-map__map里面时才显示\n.tg-map .tg-element-overlay {\n display: none;\n}\n.tg-map__map .tg-element-overlay {\n display: block;\n}\n</style>\n","<template>\n <slot></slot>\n</template>\n<script lang=\"ts\">\nimport { deepEqual, lateinit, typed } from 'tg-commons'\nimport { BaiduMarker, bindEvents, LatLng, type MarkerEventMap, type MarkerLabel, type MarkerOptions, type MarkerOverlay, type UnionIcon } from 'tg-map-core'\nimport { defineComponent, toRaw, type PropType } from 'vue'\nimport { callHook, eventProp, optionalProp, requiredProp, safeAsComponent, useSplittedAttrs, type EventEmits, type EventProps, type Props } from '../../utils/vue-utils'\nimport TgMarkerClusterer from '../extra/TgMarkerClusterer.vue'\nimport MapMixin, { MIXIN_HOOK_CREATE, MIXIN_HOOK_DESTROY } from '../map-mixin'\nimport type TgInfoBox from './TgInfoBox.vue'\nimport type TgInfoWindow from './TgInfoWindow.vue'\nimport type TgLabel from './TgLabel.vue'\n\ntype TgMarkerEmits = {\n 'update:position': LatLng,\n}\n\nexport type TgMarkerEventEmits = EventEmits<MarkerEventMap & TgMarkerEmits>\n\nexport const tgMarkerProps = {\n position: requiredProp(LatLng),\n title: optionalProp(String),\n label: optionalProp(Object as PropType<MarkerLabel>),\n icon: optionalProp(Object as PropType<UnionIcon>),\n clickable: optionalProp(Boolean),\n draggable: optionalProp(Boolean),\n crossOnDrag: optionalProp(Boolean),\n cursor: optionalProp(String),\n zIndex: optionalProp(Number),\n visible: optionalProp(Boolean),\n normalizePositionForBaidu: optionalProp(Boolean),\n /**\n * 是否自动添加到TgMarkerClusterer中去\n * @default true\n */\n autoAddToClusterer: optionalProp(Boolean, true),\n 'onUpdate:position': eventProp(),\n} satisfies Props<MarkerOptions & {\n autoAddToClusterer?: boolean\n}> & EventProps<TgMarkerEmits>\n\nconst TgMarker = defineComponent({\n name: 'tg-marker',\n mixins: [MapMixin],\n inheritAttrs: false,\n props: tgMarkerProps,\n emits: undefined as any as TgMarkerEventEmits,\n setup(props, { attrs }) {\n return {\n attrs: useSplittedAttrs(attrs, props),\n marker: lateinit<MarkerOverlay>(),\n labelOverlay: typed<TgLabel>(),\n info: typed<TgInfo>(),\n emittedPosition: undefined as LatLng | undefined,\n autoAddToClustererWhenCreate: props.autoAddToClusterer,\n }\n },\n watch: {\n position(value: LatLng) {\n if (this.emittedPosition != null) {\n const position = this.emittedPosition\n this.emittedPosition = undefined\n if (position === toRaw(value)) {\n // 防止emit出的值被设置回来\n return\n }\n }\n this.marker.setPosition(value)\n },\n title(value: string) {\n this.marker.setTitle(value)\n },\n label(value?: MarkerLabel, oldValue?: MarkerLabel) {\n if (deepEqual(value, oldValue)) {\n // MarkerLabel是对象, 并且大部分时候都是直接在模板里面创建的, 很多时候创建的是内容相同的对象, 此时不需要更新\n return\n }\n if (value) {\n this.marker.setLabel(value)\n } else {\n this.recreate()\n }\n },\n icon(value?: UnionIcon) {\n if (value) {\n // {@macro hot_reload_prop_change_before_on_create}\n this.marker?.setIcon(value)\n } else {\n this.recreate()\n }\n },\n clickable(value: boolean) {\n if (this.marker instanceof BaiduMarker) {\n this.recreate() // baidu不支持setClickable(), 故只能recreate()\n } else {\n this.marker.setClickable(value)\n }\n },\n draggable(value: boolean) {\n this.marker.setDraggable(value)\n },\n crossOnDrag() {\n this.recreate()\n },\n cursor() {\n this.recreate()\n },\n zIndex(value?: number) {\n if (this.marker instanceof BaiduMarker) {\n this.recreate() // 当前版本, baidu调用setZIndex()无效\n } else {\n this.marker.setZIndex(value)\n }\n },\n visible(value: boolean) {\n this.marker.setVisible(value)\n },\n normalizePositionForBaidu() {\n this.recreate()\n },\n autoAddToClusterer() {\n this.recreate()\n },\n },\n onCreate() {\n // 结构化类型, this包含MarkerOptions的所有属性, 故可以直接传进去\n this.marker = this.$map.createMarker(this)\n if (this.labelOverlay) {\n this.marker.attachLabelOverlay(this.labelOverlay.overlay)\n }\n\n if (this.$clusterer() && this.autoAddToClusterer) {\n this.$clusterer()!.onAddMarker(this.marker)\n } else {\n this.$map.addOverlay(this.marker)\n }\n // 保存创建时autoAddToClusterer的值, 销毁时需要使用它\n this.autoAddToClustererWhenCreate = this.autoAddToClusterer\n\n // baidu, 只有marker被添加到地图之后, 才能在它上面打开infoWindow\n // infoBox似乎没有这个限制, 但目前不区分infoWindow和infoBox, 故统一放在这里处理\n if (this.info) {\n this.info.show && this.info.overlay.open(this.marker)\n }\n\n bindEvents(this.attrs.listeners, this.marker, this.$emit)\n\n if (this.attrs.listenerProps['update:position']) {\n this.marker.addEventListener('dragend', (event) => {\n this.$emit('update:position', this.emittedPosition = event.position)\n })\n }\n },\n onDestroy() {\n if (this.$clusterer() && this.autoAddToClustererWhenCreate) {\n this.$clusterer()!.onRemoveMarker(this.marker)\n } else {\n this.$map.removeOverlay(