Angular17+leaflet集成天地图组件

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是通过逗号“,”分隔的字符串

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值