|
|
|
|
@ -1,7 +1,10 @@
|
|
|
|
|
declare global {
|
|
|
|
|
interface Window {
|
|
|
|
|
T?: {
|
|
|
|
|
Map: new (container: HTMLElement | string) => TiandituMap
|
|
|
|
|
Map: new (
|
|
|
|
|
container: HTMLElement | string,
|
|
|
|
|
options?: { layers?: TiandituTileLayer[] },
|
|
|
|
|
) => TiandituMap
|
|
|
|
|
LngLat: new (lng: number, lat: number) => TiandituLngLat
|
|
|
|
|
Marker: new (lnglat: TiandituLngLat, options?: { icon?: TiandituIcon }) => TiandituMarker
|
|
|
|
|
Icon: new (options: {
|
|
|
|
|
@ -10,6 +13,10 @@ declare global {
|
|
|
|
|
iconAnchor?: TiandituPoint
|
|
|
|
|
}) => TiandituIcon
|
|
|
|
|
Point: new (x: number, y: number) => TiandituPoint
|
|
|
|
|
TileLayer?: new (
|
|
|
|
|
url: string,
|
|
|
|
|
options?: { minZoom?: number; maxZoom?: number },
|
|
|
|
|
) => TiandituTileLayer
|
|
|
|
|
Label: new (options: {
|
|
|
|
|
text: string
|
|
|
|
|
position: TiandituLngLat
|
|
|
|
|
@ -81,6 +88,10 @@ export interface TiandituIcon {
|
|
|
|
|
// marker icon handle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TiandituTileLayer {
|
|
|
|
|
// tile layer handle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TiandituPoint {
|
|
|
|
|
x: number
|
|
|
|
|
y: number
|
|
|
|
|
@ -88,28 +99,254 @@ export interface TiandituPoint {
|
|
|
|
|
|
|
|
|
|
export interface TiandituMapClickEvent {
|
|
|
|
|
lnglat?: TiandituLngLat
|
|
|
|
|
containerPoint?: TiandituPoint
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface TiandituMap {
|
|
|
|
|
centerAndZoom(lnglat: TiandituLngLat, zoom: number): void
|
|
|
|
|
enableScrollWheelZoom(): void
|
|
|
|
|
addOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
|
|
|
|
|
removeOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
|
|
|
|
|
enableDrag?(): void
|
|
|
|
|
disableDrag?(): void
|
|
|
|
|
enableAutoResize?(): void
|
|
|
|
|
panBy?(position: TiandituPoint): void
|
|
|
|
|
addOverLay(overlay: TiandituMarker | TiandituLabel | TiandituSchoolOverlay): void
|
|
|
|
|
removeOverLay(overlay: TiandituMarker | TiandituLabel | TiandituSchoolOverlay): void
|
|
|
|
|
setViewport?(points: TiandituLngLat[]): void
|
|
|
|
|
clearOverLays?(): void
|
|
|
|
|
addLayer?(layer: TiandituTileLayer): void
|
|
|
|
|
addEventListener?(type: string, handler: (e?: TiandituMapClickEvent) => void): void
|
|
|
|
|
removeEventListener?(type: string, handler: (e?: TiandituMapClickEvent) => void): void
|
|
|
|
|
lngLatToLayerPoint?(lnglat: TiandituLngLat): TiandituPoint
|
|
|
|
|
getPanes?(): { overlayPane: HTMLElement }
|
|
|
|
|
lngLatToContainerPoint?(lnglat: TiandituLngLat): TiandituPoint
|
|
|
|
|
getPanes?(): Record<string, HTMLElement>
|
|
|
|
|
getContainer?(): HTMLElement
|
|
|
|
|
checkResize?(): void
|
|
|
|
|
destroy?(): void
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const MAP_INTERACTIVE_PANE_KEYS = new Set(['mapPane', 'tilePane', 'floatPane'])
|
|
|
|
|
const MAP_PASSTHROUGH_PANE_CLASS_RE =
|
|
|
|
|
/tdt-overlay-pane|tdt-marker-pane|tdt-tooltip-pane|tdt-popup-pane|tdt-shadow-pane/i
|
|
|
|
|
|
|
|
|
|
function invokeMapMethod(map: TiandituMap, method: string) {
|
|
|
|
|
const fn = (map as unknown as Record<string, unknown>)[method]
|
|
|
|
|
if (typeof fn === 'function') {
|
|
|
|
|
;(fn as () => void).call(map)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 创建天地图实例。使用 SDK 默认底图,避免自定义瓦片图层影响拖拽/初始化。 */
|
|
|
|
|
export function createTiandituMap(
|
|
|
|
|
T: NonNullable<typeof window.T>,
|
|
|
|
|
container: HTMLElement | string,
|
|
|
|
|
): TiandituMap {
|
|
|
|
|
return new T.Map(container)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 弹窗内地图在容器尺寸变化后需重算布局,避免瓦片错位到页面顶部 */
|
|
|
|
|
export function invalidateMapSize(map: TiandituMap) {
|
|
|
|
|
map.checkResize?.()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 等待地图容器具备有效宽高(弹窗动画期间常为 0) */
|
|
|
|
|
export async function waitForMapContainerReady(
|
|
|
|
|
container: HTMLElement,
|
|
|
|
|
timeoutMs = 5000,
|
|
|
|
|
): Promise<boolean> {
|
|
|
|
|
const deadline = Date.now() + timeoutMs
|
|
|
|
|
while (Date.now() < deadline) {
|
|
|
|
|
const rect = container.getBoundingClientRect()
|
|
|
|
|
if (rect.width > 0 && rect.height > 0) return true
|
|
|
|
|
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()))
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 重新计算尺寸并刷新视野,修复弹窗内瓦片不渲染 */
|
|
|
|
|
export function refreshMapView(
|
|
|
|
|
map: TiandituMap,
|
|
|
|
|
T: NonNullable<typeof window.T>,
|
|
|
|
|
center: { lng: number; lat: number },
|
|
|
|
|
zoom: number,
|
|
|
|
|
) {
|
|
|
|
|
invalidateMapSize(map)
|
|
|
|
|
map.centerAndZoom(new T.LngLat(center.lng, center.lat), zoom)
|
|
|
|
|
scheduleMapResize(map, [50, 200, 500])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 容器尺寸变化时自动触发 checkResize(弹窗、侧栏展开等场景) */
|
|
|
|
|
export function bindMapResizeObserver(
|
|
|
|
|
container: HTMLElement,
|
|
|
|
|
getMap: () => TiandituMap | null,
|
|
|
|
|
): () => void {
|
|
|
|
|
if (typeof ResizeObserver === 'undefined') {
|
|
|
|
|
return () => {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const observer = new ResizeObserver(() => {
|
|
|
|
|
const map = getMap()
|
|
|
|
|
if (map) invalidateMapSize(map)
|
|
|
|
|
})
|
|
|
|
|
observer.observe(container)
|
|
|
|
|
return () => observer.disconnect()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 多次延迟触发 checkResize,覆盖弹窗动画结束后的布局 */
|
|
|
|
|
export function scheduleMapResize(map: TiandituMap, delays = [0, 80, 240, 480, 800]) {
|
|
|
|
|
for (const delay of delays) {
|
|
|
|
|
window.setTimeout(() => invalidateMapSize(map), delay)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 覆盖物层不拦截鼠标,仅可交互标记响应点击。
|
|
|
|
|
* SDK 的 getPanes 与 DOM class 双路径设置,避免类名不一致导致拖拽失效。
|
|
|
|
|
*/
|
|
|
|
|
export function applyOverlayPassthrough(map: TiandituMap) {
|
|
|
|
|
const panes = map.getPanes?.()
|
|
|
|
|
if (panes) {
|
|
|
|
|
for (const [key, pane] of Object.entries(panes)) {
|
|
|
|
|
if (!pane || MAP_INTERACTIVE_PANE_KEYS.has(key)) continue
|
|
|
|
|
pane.style.pointerEvents = 'none'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const root = map.getContainer?.()
|
|
|
|
|
if (root) {
|
|
|
|
|
root.querySelectorAll<HTMLElement>('[class*="tdt-"][class*="pane"]').forEach((pane) => {
|
|
|
|
|
if (MAP_PASSTHROUGH_PANE_CLASS_RE.test(pane.className)) {
|
|
|
|
|
pane.style.pointerEvents = 'none'
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
root.querySelectorAll<HTMLElement>('.slake-map-school-marker').forEach((marker) => {
|
|
|
|
|
marker.style.pointerEvents = 'auto'
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 启用滚轮缩放与拖拽,并让覆盖物层不挡住地图 */
|
|
|
|
|
export function configureMapInteraction(map: TiandituMap) {
|
|
|
|
|
invokeMapMethod(map, 'enableScrollWheelZoom')
|
|
|
|
|
invokeMapMethod(map, 'enableDrag')
|
|
|
|
|
invokeMapMethod(map, 'enableInertia')
|
|
|
|
|
invokeMapMethod(map, 'enableAutoResize')
|
|
|
|
|
|
|
|
|
|
const setOptions = (map as TiandituMap & { setOptions?: (opts: { drag?: boolean }) => void })
|
|
|
|
|
.setOptions
|
|
|
|
|
setOptions?.({ drag: true })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 自定义 HTML 覆盖物会挡住 SDK 内置拖拽时,改用手动 panBy 平移。
|
|
|
|
|
* 会关闭 SDK 拖拽,避免与手动平移冲突。
|
|
|
|
|
*/
|
|
|
|
|
export function bindMapDragPan(
|
|
|
|
|
map: TiandituMap,
|
|
|
|
|
T: NonNullable<typeof window.T>,
|
|
|
|
|
options: { ignoreSelector?: string } = {},
|
|
|
|
|
): () => void {
|
|
|
|
|
const container = map.getContainer?.()
|
|
|
|
|
if (!container || !map.panBy) return () => {}
|
|
|
|
|
const root: HTMLElement = container
|
|
|
|
|
|
|
|
|
|
invokeMapMethod(map, 'disableDrag')
|
|
|
|
|
|
|
|
|
|
const ignoreSelector =
|
|
|
|
|
options.ignoreSelector ?? '.slake-map-school-marker, .slake-map-school-marker *'
|
|
|
|
|
|
|
|
|
|
let panning = false
|
|
|
|
|
let lastX = 0
|
|
|
|
|
let lastY = 0
|
|
|
|
|
let pendingDx = 0
|
|
|
|
|
let pendingDy = 0
|
|
|
|
|
let panRaf = 0
|
|
|
|
|
|
|
|
|
|
function shouldIgnore(target: EventTarget | null) {
|
|
|
|
|
return target instanceof Element && Boolean(target.closest(ignoreSelector))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function startPan(clientX: number, clientY: number) {
|
|
|
|
|
panning = true
|
|
|
|
|
lastX = clientX
|
|
|
|
|
lastY = clientY
|
|
|
|
|
root.style.cursor = 'grabbing'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function movePan(clientX: number, clientY: number) {
|
|
|
|
|
if (!panning) return
|
|
|
|
|
const dx = clientX - lastX
|
|
|
|
|
const dy = clientY - lastY
|
|
|
|
|
if (!dx && !dy) return
|
|
|
|
|
lastX = clientX
|
|
|
|
|
lastY = clientY
|
|
|
|
|
pendingDx += dx
|
|
|
|
|
pendingDy += dy
|
|
|
|
|
if (panRaf) return
|
|
|
|
|
panRaf = requestAnimationFrame(() => {
|
|
|
|
|
panRaf = 0
|
|
|
|
|
if (!pendingDx && !pendingDy) return
|
|
|
|
|
map.panBy?.(new T.Point(-pendingDx, -pendingDy))
|
|
|
|
|
pendingDx = 0
|
|
|
|
|
pendingDy = 0
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function endPan() {
|
|
|
|
|
if (!panning) return
|
|
|
|
|
panning = false
|
|
|
|
|
root.style.cursor = 'grab'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMouseDown = (e: MouseEvent) => {
|
|
|
|
|
if (e.button !== 0 || shouldIgnore(e.target)) return
|
|
|
|
|
startPan(e.clientX, e.clientY)
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMouseMove = (e: MouseEvent) => {
|
|
|
|
|
movePan(e.clientX, e.clientY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onMouseUp = () => endPan()
|
|
|
|
|
|
|
|
|
|
const onTouchStart = (e: TouchEvent) => {
|
|
|
|
|
if (shouldIgnore(e.target) || e.touches.length !== 1) return
|
|
|
|
|
startPan(e.touches[0].clientX, e.touches[0].clientY)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onTouchMove = (e: TouchEvent) => {
|
|
|
|
|
if (!panning || e.touches.length !== 1) return
|
|
|
|
|
movePan(e.touches[0].clientX, e.touches[0].clientY)
|
|
|
|
|
e.preventDefault()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const onTouchEnd = () => endPan()
|
|
|
|
|
|
|
|
|
|
root.style.cursor = 'grab'
|
|
|
|
|
root.addEventListener('mousedown', onMouseDown)
|
|
|
|
|
window.addEventListener('mousemove', onMouseMove)
|
|
|
|
|
window.addEventListener('mouseup', onMouseUp)
|
|
|
|
|
root.addEventListener('touchstart', onTouchStart, { passive: false })
|
|
|
|
|
root.addEventListener('touchmove', onTouchMove, { passive: false })
|
|
|
|
|
root.addEventListener('touchend', onTouchEnd)
|
|
|
|
|
root.addEventListener('touchcancel', onTouchEnd)
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
if (panRaf) {
|
|
|
|
|
cancelAnimationFrame(panRaf)
|
|
|
|
|
panRaf = 0
|
|
|
|
|
}
|
|
|
|
|
root.removeEventListener('mousedown', onMouseDown)
|
|
|
|
|
window.removeEventListener('mousemove', onMouseMove)
|
|
|
|
|
window.removeEventListener('mouseup', onMouseUp)
|
|
|
|
|
root.removeEventListener('touchstart', onTouchStart)
|
|
|
|
|
root.removeEventListener('touchmove', onTouchMove)
|
|
|
|
|
root.removeEventListener('touchend', onTouchEnd)
|
|
|
|
|
root.removeEventListener('touchcancel', onTouchEnd)
|
|
|
|
|
root.style.cursor = ''
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface SchoolMapOverlayInstance {
|
|
|
|
|
lnglat: TiandituLngLat
|
|
|
|
|
options: SchoolMapOverlayOptions
|
|
|
|
|
@ -204,13 +441,23 @@ function getSchoolMapOverlayClass(T: NonNullable<typeof window.T>) {
|
|
|
|
|
if (this.options.active) div.classList.add('is-active')
|
|
|
|
|
div.setAttribute('role', 'button')
|
|
|
|
|
div.setAttribute('tabindex', '0')
|
|
|
|
|
div.style.pointerEvents = 'auto'
|
|
|
|
|
div.innerHTML =
|
|
|
|
|
'<span class="slake-map-school-dot" aria-hidden="true"></span>' +
|
|
|
|
|
`<span class="slake-map-school-label">${escapeMapLabelHtml(this.options.name.trim() || '—')}</span>`
|
|
|
|
|
this._div = div
|
|
|
|
|
map.getPanes?.().overlayPane.appendChild(div)
|
|
|
|
|
|
|
|
|
|
this._onMapChange = () => this.update()
|
|
|
|
|
const panes = map.getPanes?.()
|
|
|
|
|
const overlayPane = panes?.overlayPane ?? panes?.markerPane
|
|
|
|
|
if (overlayPane) overlayPane.appendChild(div)
|
|
|
|
|
|
|
|
|
|
let moveRaf = 0
|
|
|
|
|
this._onMapChange = () => {
|
|
|
|
|
if (moveRaf) return
|
|
|
|
|
moveRaf = requestAnimationFrame(() => {
|
|
|
|
|
moveRaf = 0
|
|
|
|
|
this.update()
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
map.addEventListener?.('move', this._onMapChange)
|
|
|
|
|
map.addEventListener?.('zoomend', this._onMapChange)
|
|
|
|
|
|
|
|
|
|
|