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 Key
defaultCenter = [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 监听搜索框输入,防抖500ms
addSearchPlugin(): 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时开始搜索
}
});
}
// 调用天地图搜索API
searchLocation(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 响应是否包含 pois
if (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是通过逗号“,”分隔的字符串