You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

242 lines
7.1 KiB

3 months ago
declare global {
interface Window {
T?: {
Map: new (container: HTMLElement | string) => TiandituMap
LngLat: new (lng: number, lat: number) => TiandituLngLat
Marker: new (lnglat: TiandituLngLat, options?: { icon?: TiandituIcon }) => TiandituMarker
Icon: new (options: {
iconUrl: string
iconSize: TiandituPoint
iconAnchor?: TiandituPoint
}) => TiandituIcon
Point: new (x: number, y: number) => TiandituPoint
Label: new (options: {
text: string
position: TiandituLngLat
offset?: TiandituPoint
}) => TiandituLabel
Overlay: {
extend: (proto: Record<string, unknown>) => new (
lnglat: TiandituLngLat,
options: SchoolMapOverlayOptions,
) => TiandituSchoolOverlay
}
}
}
}
export interface SchoolMapOverlayOptions {
name: string
active?: boolean
}
export interface TiandituLngLat {
lng: number
lat: number
}
export interface TiandituMarker {
addEventListener(type: string, handler: () => void): void
}
export interface TiandituLabel {
addEventListener?(type: string, handler: () => void): void
setStyle?(style: Record<string, string>): void
getElement?(): HTMLElement | null
}
export interface TiandituSchoolOverlay {
addEventListener(type: string, handler: () => void): void
setActive?(active: boolean): void
}
export interface TiandituIcon {
// marker icon handle
}
export interface TiandituPoint {
x: number
y: number
}
export interface TiandituMap {
centerAndZoom(lnglat: TiandituLngLat, zoom: number): void
enableScrollWheelZoom(): void
addOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
removeOverLay(overlay: TiandituMarker | TiandituSchoolOverlay): void
setViewport?(points: TiandituLngLat[]): void
clearOverLays?(): void
addEventListener?(type: string, handler: () => void): void
removeEventListener?(type: string, handler: () => void): void
lngLatToLayerPoint?(lnglat: TiandituLngLat): TiandituPoint
getPanes?(): { overlayPane: HTMLElement }
}
let loadPromise: Promise<NonNullable<typeof window.T>> | null = null
let schoolMapOverlayClass: (new (
lnglat: TiandituLngLat,
options: SchoolMapOverlayOptions,
) => TiandituSchoolOverlay) | null = null
/** 天地图浏览器端 Key国家测绘 */
const DEFAULT_TIANDITU_TK = 'cc3f61fa6cafe2f0ad440018d0f07b13'
export function getTiandituKey(): string {
return import.meta.env.VITE_TIANDITU_TK?.trim() || DEFAULT_TIANDITU_TK
}
export function loadTianditu(): Promise<NonNullable<typeof window.T>> {
if (window.T) {
return Promise.resolve(window.T)
}
if (!loadPromise) {
loadPromise = new Promise((resolve, reject) => {
const tk = getTiandituKey()
if (!tk) {
reject(new Error('未配置 VITE_TIANDITU_TK'))
return
}
const script = document.createElement('script')
script.src = `https://api.tianditu.gov.cn/api?v=4.0&tk=${encodeURIComponent(tk)}`
script.async = true
script.onload = () => {
if (window.T) {
resolve(window.T)
} else {
reject(new Error('天地图 SDK 加载失败'))
}
}
script.onerror = () => reject(new Error('天地图脚本加载失败'))
document.head.appendChild(script)
})
}
return loadPromise
}
/** 地图默认中心:苏州市 */
export const SUZHOU_MAP_CENTER = { lng: 120.585316, lat: 31.298886 }
/** 默认缩放:以苏州为中心,首屏仅展示苏州市域 */
export const SUZHOU_MAP_ZOOM = 11
function escapeMapLabelHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
function getSchoolMapOverlayClass(T: NonNullable<typeof window.T>) {
if (schoolMapOverlayClass) return schoolMapOverlayClass
schoolMapOverlayClass = T.Overlay.extend({
initialize(lnglat: TiandituLngLat, options: SchoolMapOverlayOptions) {
this.lnglat = lnglat
this.options = options || { name: '' }
},
onAdd(map: TiandituMap) {
this.map = map
const div = document.createElement('div')
div.className = 'slake-map-school-marker'
if (this.options.active) div.classList.add('is-active')
div.setAttribute('role', 'button')
div.setAttribute('tabindex', '0')
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()
map.addEventListener?.('move', this._onMapChange)
map.addEventListener?.('zoomend', this._onMapChange)
this.update()
},
onRemove() {
const map = this.map as TiandituMap | null
if (map && this._onMapChange) {
map.removeEventListener?.('move', this._onMapChange)
map.removeEventListener?.('zoomend', this._onMapChange)
}
if (this._div?.parentNode) {
this._div.parentNode.removeChild(this._div)
}
this.map = null
this._div = null
},
update() {
const map = this.map as TiandituMap | null
if (!map?.lngLatToLayerPoint || !this._div) return
const pos = map.lngLatToLayerPoint(this.lnglat)
this._div.style.left = `${pos.x}px`
this._div.style.top = `${pos.y}px`
},
setActive(active: boolean) {
this.options.active = active
if (this._div) {
this._div.classList.toggle('is-active', active)
}
},
addEventListener(type: string, handler: () => void) {
if (this._div) {
this._div.addEventListener(type, handler)
}
},
}) as new (lnglat: TiandituLngLat, options: SchoolMapOverlayOptions) => TiandituSchoolOverlay
return schoolMapOverlayClass
}
/** 圆点 + 校名一体覆盖物flex 横排,与原型一致) */
export function createSchoolMapOverlay(
T: NonNullable<typeof window.T>,
school: { name: string; longitude: number; latitude: number },
active = false,
): TiandituSchoolOverlay {
const OverlayClass = getSchoolMapOverlayClass(T)
return new OverlayClass(new T.LngLat(school.longitude, school.latitude), {
name: school.name,
active,
})
}
export function shortenSchoolMapLabel(name: string, max = 12): string {
const t = name.trim()
if (!t) return '—'
return t.length > max ? `${t.slice(0, max)}` : t
}
export function fitMapToSchools(
map: TiandituMap,
T: NonNullable<typeof window.T>,
schools: { latitude: number; longitude: number }[],
) {
if (!schools.length) return
const points = schools.map((s) => new T.LngLat(s.longitude, s.latitude))
if (schools.length === 1) {
map.centerAndZoom(points[0], SUZHOU_MAP_ZOOM)
return
}
if (typeof map.setViewport === 'function') {
map.setViewport(points)
return
}
const lngs = schools.map((s) => s.longitude)
const lats = schools.map((s) => s.latitude)
const center = new T.LngLat(
(Math.min(...lngs) + Math.max(...lngs)) / 2,
(Math.min(...lats) + Math.max(...lats)) / 2,
)
map.centerAndZoom(center, SUZHOU_MAP_ZOOM)
}
export {}