Angular17+leaflet集成天地图组件
例图
需要的包
"@asymmetrik/ngx-leaflet": "^17.0.0","@types/leaflet": "^1.9.12","leaflet": "^1.9.4",
去天地图网站获取一个token
https://www.tianditu.gov.cn/
创建Angular组件component
名称:site-pick-tianditu
html
<div><input #searchInput nz-input placeholder="请输入地址" /><!-- 搜索结果的下拉框 --><ul *ngIf="searchResults.length > 0" class="search-dropdown"><li *ngFor="let result of searchResults" (click)="selectLocation(result)">{{ result.address }}{{ result.name }}</li></ul><div #mapContainer id="mapContainer"></div>
</div>
less 样式
#mapContainer {width: 100%;height: 500px; /* 确保地图显示正确 */
}.search-dropdown {position: absolute;background-color: white;border: 1px solid #ddd;width: 100%;max-height: 200px;overflow-y: auto;list-style: none;padding: 0;margin: 0;z-index: 1000;li {padding: 8px 12px;cursor: pointer;&:hover {background-color: #f0f0f0;}}
}
ts
import {AfterViewInit, ChangeDetectorRef,Component,ElementRef, EventEmitter, HostListener, Input, OnChanges,OnInit, Output, SimpleChanges,ViewChild,ViewEncapsulation
} from '@angular/core';
import * as L from 'leaflet';
import {NzInputDirective, NzInputGroupComponent} from "ng-zorro-antd/input";
import {LeafletModule} from "@asymmetrik/ngx-leaflet";
import {fromEvent, Subject} from "rxjs";
import {debounceTime,map} from "rxjs/operators";
import {NgForOf, NgIf} from "@angular/common";@Component({selector: 'app-site-pick-tianditu',standalone: true,imports: [NzInputDirective,LeafletModule,NzInputGroupComponent,NgForOf,NgIf],templateUrl: './site-pick-tianditu.component.html',styleUrls: ['./site-pick-tianditu.component.less'],encapsulation: ViewEncapsulation.None // 禁用样式封装
})
export class SitePickTiandituComponent implements OnInit, AfterViewInit,OnChanges {@ViewChild('mapContainer', {static: false}) mapContainer!: ElementRef;@ViewChild('searchInput', {static: false}) searchInput!: ElementRef;@Output() inputChange = new EventEmitter<{ lonlat: any, siteName: any, adCode: any }>();@Input() lonlat!: string;@Input() locationName!: string;@Input() boundary!: string;constructor(private cdr: ChangeDetectorRef) {}map!: L.Map;currentMarker: any;drawMapEvent = new Subject();mapLoaded = false;public mapLoadSubject = new Subject<void>();key = "XXXXXXXXXXXXXXXXXXXXXXXX"; // 天地图API KeydefaultCenter = [116.397755, 39.903179]; // 默认中心,北京selectedLocation: L.LatLng | null = null;searchResults: any[] = []; // 保存搜索结果的数组// 天地图瓦片URL 影像底图tiandituImageLayerUrl = 'https://t{s}.tianditu.gov.cn/img_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=img&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + this.key;//影像底图- 影像注记tiandituImageLayerUrlMark = 'https://t{s}.tianditu.gov.cn/cia_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cia&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + this.key;//影像底图- 矢量底图tiandituVecLayerUrl = 'https://t{s}.tianditu.gov.cn/vec_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=vec&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + this.key;//影像底图- 矢量注记tiandituVeceLayerUrlMark = 'https://t{s}.tianditu.gov.cn/cva_w/wmts?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0&LAYER=cva&STYLE=default&TILEMATRIXSET=w&FORMAT=tiles&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&tk=' + this.key;@HostListener('window:resize', [])onWindowResize() {this.map.invalidateSize(); // 在窗口大小调整时强制刷新地图}ngOnInit(): void {this.drawMapEvent.subscribe(() => {this.currentPosition().subscribe(center => {this.addSearchPlugin();});});}ngOnChanges(changes: SimpleChanges): void {if (changes['lonlat'] && this.lonlat) {const [lng, lat] = this.lonlat.split(',').map(Number);if (!this.locationName) {this.reverseGeocode(lat, lng);} else {this.searchInput.nativeElement.value = this.locationName;this.map.setView([lat, lng], 15);this.addMarker(lat, lng);}}}ngAfterViewInit(): void {this.initializeMap();this.addSearchPlugin(); // 检查输入框事件监听// 在地图初始化后立即刷新瓦片setTimeout(() => {this.map.invalidateSize();}, 1);}initializeMap(): void {// 初始化地图,设置默认中心和缩放级别this.map = L.map(this.mapContainer.nativeElement, {center: [39.9042, 116.4074], // 北京市中心坐标zoom: 15,maxZoom: 18, // 天地图最大缩放级别minZoom: 1, // 最小缩放级别,防止缩放太小或太大});L.control.scale({imperial: false}).addTo(this.map);// 设置自定义的 icon 路径L.Icon.Default.mergeOptions({iconRetinaUrl: 'assets/images/marker-icon-2x.png',iconUrl: 'assets/images/marker-icon.png',shadowUrl: 'assets/images/marker-shadow.png'});// 创建图层 - 影像底图const imageLayer = L.tileLayer(this.tiandituImageLayerUrl, {subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],maxZoom: 18,minZoom: 3});// 创建图层 - 影像注记const imageLayerMark = L.tileLayer(this.tiandituImageLayerUrlMark, {subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],maxZoom: 18,minZoom: 3});// 创建图层 - 矢量底图const vecLayer = L.tileLayer(this.tiandituVecLayerUrl, {subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],maxZoom: 18,minZoom: 3});// 创建图层 - 矢量注记const vecLayerMark = L.tileLayer(this.tiandituVeceLayerUrlMark, {subdomains: ['0', '1', '2', '3', '4', '5', '6', '7'],maxZoom: 18,minZoom: 3});// 默认添加矢量底图vecLayer.addTo(this.map);vecLayerMark.addTo(this.map);// 图层控制器const baseLayers = {"影像底图": imageLayer,"矢量底图": vecLayer,// 如果有其他基础图层也可以添加到这里,例如矢量地图};const overlayLayers = {"影像注记": imageLayerMark,"矢量注记": vecLayerMark,};L.control.layers(baseLayers, overlayLayers).addTo(this.map);this.map.on('click', (e: L.LeafletMouseEvent) => {this.onMapClick(e);});this.drawMapEvent.next(null);}// 使用 RxJS 监听搜索框输入,防抖500msaddSearchPlugin(): void {fromEvent(this.searchInput.nativeElement, 'input').pipe(map((event: any) => {return event.target.value;}),debounceTime(500) // 防抖,500ms延迟触发搜索).subscribe((keyword: string) => {if (keyword.length > 2) {this.searchLocation(keyword); // 当输入字符大于2时开始搜索}});}// 调用天地图搜索APIsearchLocation(keyword: string): void {const searchUrl = `https://api.tianditu.gov.cn/v2/search?postStr={"keyWord":"${keyword}","level":"10","mapBound":"-180,-90,180,90","queryType":"7","count":"10","start":"0"}&tk=${this.key}`;fetch(searchUrl).then(response => response.json()).then(data => {// 检查 API 响应是否包含 poisif (data.count > 0 && Array.isArray(data.pois)) {this.searchResults = data.pois.map((poi: any) => ({name: poi.name,lonlat: poi.lonlat,adminname: poi.adminname,address: poi.address}));} else {// 如果返回数据结构不符合预期,或者没有搜索到结果console.warn('未找到匹配的地点');this.searchResults = [];}this.cdr.detectChanges(); // 强制变更检测以更新 UI}).catch(error => {console.error('搜索地址时出错:', error);this.searchResults = [];});}// 选择下拉框中的地点selectLocation(result: any): void {const [lng, lat] = result.lonlat.split(',').map(Number);// 将 name 和 address 组合并设置到输入框中this.searchInput.nativeElement.value = `${result.address}${result.name}`;this.resolveLocation(lat, lng, this.searchInput.nativeElement.value, result.adminname); // 定位地图this.searchResults = []; // 清空搜索结果,关闭下拉框}// 定位地图到指定位置resolveLocation(lat: number, lng: number, siteName: string, adCode: string): void {if (this.currentMarker) {this.map.removeLayer(this.currentMarker);}this.currentMarker = L.marker([lat, lng]).addTo(this.map);this.map.setView([lat, lng], 15);this.inputChange.emit({ lonlat: `${lng},${lat}`, siteName, adCode }); // 传递地点信息}onMapClick(e: L.LeafletMouseEvent): void {const { lat, lng } = e.latlng;const reverseGeocodeUrl = `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${lng},'lat':${lat},'ver':1}&type=geocode&tk=${this.key}`;// 调用天地图逆地址编码接口fetch(reverseGeocodeUrl).then(response => response.json()).then(data => {if (data.status === '0') {const formattedAddress = data.result.formatted_address;// 将获取到的地址回显到输入框中this.searchInput.nativeElement.value = formattedAddress;// 将经纬度和地址传递出去this.inputChange.emit({lonlat: `${lng},${lat}`,siteName: formattedAddress,adCode: '' // 如果需要,可以从返回的数据中解析 adCode});// 在地图上标记选择的位置if (this.currentMarker) {this.map.removeLayer(this.currentMarker);}this.currentMarker = L.marker([lat, lng]).addTo(this.map);this.map.setView([lat, lng], 15);} else {console.error('逆地址编码失败:', data.msg);}}).catch(error => {console.error('调用逆地址编码接口时出错:', error);});}reverseGeocode(lat: number, lng: number): void {const reverseGeocodeUrl = `https://api.tianditu.gov.cn/geocoder?postStr={'lon':${lng},'lat':${lat},'ver':1}&type=geocode&tk=${this.key}`;fetch(reverseGeocodeUrl).then(response => response.json()).then(data => {if (data.status === '0') {const formattedAddress = data.result.formatted_address;// 将逆地址编码获取的地址回显到输入框this.searchInput.nativeElement.value = formattedAddress;// 将经纬度和地址通过事件传递出去this.inputChange.emit({lonlat: `${lng},${lat}`,siteName: formattedAddress,adCode: ''});// 定位并添加地图标记this.map.setView([lat, lng], 15);this.addMarker(lat, lng);} else {console.error('逆地址编码失败:', data.msg);}}).catch(error => console.error('调用逆地址编码接口时出错:', error));}addMarker(lat: number, lng: number): void {if (this.currentMarker) {this.map.removeLayer(this.currentMarker);}this.currentMarker = L.marker([lat, lng]).addTo(this.map);}currentPosition(): Subject<any> {return new Subject();}
}
用法
: [lonlat]=“form.get(‘lonlat’).value” 这里是将form中的经纬度值传入进组件,组件会自动定位到具体地点
(inputChange)=“inputChange($event)” 这里是获取组件传出来的改变值;
/*** 地图input框选中返回lonlat+name* @param $event*/inputChange($event: any) {this.form.get('lonlat').setValue($event.lonlat);this.form.get('address').setValue($event.siteName);}
这里进行将传出来的经纬度和地点名称进行一个赋值
注意:我的经纬度lonlat是通过逗号“,”分隔的字符串