Search code examples
javascriptangularazureazure-maps

How to Use Azure Maps with Angular 10?


I've searched high and low for proper documentation on how to configure Azure Maps with Angular and haven't found anything.

How do I do this?


Solution

  • As documentation for configuring Azure Maps with Angular does not exist, this post will accomplish that instead. By the end of this post, you should have a working Angular version of Azure Maps with map markers. Before adding any code, please follow the steps from the Microsoft website to set up your Azure Map keys: https://learn.microsoft.com/en-us/azure/azure-maps/

    The first step to create your Azure Maps component is to create a new Angular component and add the following to your .html file:

    <div id="azure-map"></div>
    

    The id can be used for styling your component in the .scss file.

    Next, we will work on the .ts file. First, let's set up the map. We'll add the following class variables for the map and coordinates:

    map: any;
    defaultLat: number = 47.608013;  // Seattle coordinates
    defaultLng: number = -122.335167;
    

    and this output to emit coordinates to the map's parent component:

    @Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
    

    Now we will make a function called InitMap() and add this code snippet inside to initialize the base map and its properties:

    this.map = new atlas.Map('azure-map', {
      center: [this.defaultLng, this.defaultLat],
      zoom: 12,
      language: 'en-US',
      showLogo: true,
      showFeedbackLink: false,
      dragRotateInteraction: false,
      authOptions: {
        authType: AuthenticationType.subscriptionKey,
        subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
      }
    });
    

    Next, we will add this code snippet inside InitMap() to register the map click hander and zoom controls:

    this.map.events.add('ready', () => {
      // Register the map click handler
      this.map.events.add('click', (e) => {
        this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
      });
    
      //Construct a zoom control and add it to the map.
      this.map.controls.add(new atlas.control.ZoomControl({
        style: ControlStyle.auto,
        zoomDelta: 1
      }), {position: ControlPosition.BottomLeft});
    });
    

    We must also call the InitMap() function inside of ngOnInit().

    The next step is to create the functionality to allow the user to drop and move pins on the map. This function will erase the current marker on the map, set the new marker's coordinates, initialize the marker drag handler, and set the boundaries of the map to track the newly placed pin marker. To handle all these operations, we will add this class variable:

    markersReference: Marker[] = [];
    

    and this function:

    setMarkers(markers: Marker[]) {
      if (markers && markers.length > 0) {
        this.markersReference = markers;
        this.map.markers.clear();
        let boundsPositions: Array<{lng: number, lat:number}> = [];
        for (let marker of markers) {
          if (marker.latitude && marker.longitude) {
          let htmlMarker = new atlas.HtmlMarker({
            draggable: true,
            position: [marker.longitude, marker.latitude]  // longitude first
          });
          // Register the marker drag handler
          this.map.events.add('dragend', htmlMarker, (e) => {
            var pos = htmlMarker.getOptions().position;
            this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
          });
          boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
          this.map.markers.add(htmlMarker);
        }
      }
      this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
        bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
    }
    

    Now we will add a function that allows us to center the map focus onto the dropped pin:

    centerMapWithCoords(lon: number, lat: number) {
      this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
    }
    

    Lastly, in order to pick up changes that the user makes to the map, we will subscribe to the map subject and its markers. Add these inputs alongside your class variables:

    @Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
    @Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
    

    Next, add these subscriptions to your ngOnInit():

    this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
      this.centerMapWithCoords(coords.lng, coords.lat)));
    this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
      this.setMarkers(markers)));
    

    And unsubscribe when the component is closed:

    ngOnDestroy() {
      for (const s of this.subscriptions) {
        s.unsubscribe();
      }
    }
    

    Overall, the class in your .ts file should look similar to the following:

    export class AzureMapComponent implements OnInit {
    
      @Input() markerDataSubject: Subject<Marker[]> = new Subject<Marker[]>();
      @Input() centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
    
      @Output() outputCoordinates: EventEmitter<number[]> = new EventEmitter<number[]>();
    
      subscriptions: Subscription[] = [];
      map: any;
      markersReference: Marker[] = [];
      defaultLat: number = 47.608013;  // Seattle coordinates
      defaultLng: number = -122.335167;
    
      ngOnInit() {
        this.InitMap();
        this.subscriptions.push((this.centerMapSubject).asObservable().subscribe((coords) =>
          this.centerMapWithCoords(coords.lng, coords.lat)));
        this.subscriptions.push((this.markerDataSubject).asObservable().subscribe((markers) =>
          this.setMarkers(markers)));
      }
    
      //Create an instance of the map control and set some options.
      InitMap() {
        this.map = new atlas.Map('azure-map', {
          center: [this.defaultLng, this.defaultLat],
          zoom: 12,
          language: 'en-US',
          showLogo: true,
          showFeedbackLink: false,
          dragRotateInteraction: false,
          authOptions: {
            authType: AuthenticationType.subscriptionKey,
            subscriptionKey: 'YOUR_SUBSCRIPTION_KEY_HERE'
          }
        });
    
        this.map.events.add('ready', () => {
          // Register the map click handler
          this.map.events.add('click', (e) => {
            this.outputCoordinates.emit([e.position[0], e.position[1]]); // 0 = longitude, 1 = latitude
          });
    
          //Construct a zoom control and add it to the map.
          this.map.controls.add(new atlas.control.ZoomControl({
            style: ControlStyle.auto,
            zoomDelta: 1
          }), {position: ControlPosition.BottomLeft});
        });
      }
    
      setMarkers(markers: Marker[]) {
        if (markers && markers.length > 0) {
          this.markersReference = markers;
          this.map.markers.clear();
          let boundsPositions: Array<{lng: number, lat:number}> = [];
          for (let marker of markers) {
            if (marker.latitude && marker.longitude) {
              let htmlMarker = new atlas.HtmlMarker({
                draggable: true,
                position: [marker.longitude, marker.latitude]  // longitude first
              });
              // Register the marker drag handler
              this.map.events.add('dragend', htmlMarker, (e) => {
                var pos = htmlMarker.getOptions().position;
                this.outputCoordinates.emit([pos[0], pos[1]]); // 0 = longitude, 1 = latitude
              });
              boundsPositions.push({lng: marker.longitude, lat: marker.latitude}) // lat, lng
              this.map.markers.add(htmlMarker);
            }
          }
          this.map.setCamera({padding: {top: 20, bottom: 20, left: 20, right: 20}, maxZoom: 16,
            bounds: atlas.data.BoundingBox.fromLatLngs(boundsPositions)});
        }
      }
    
      centerMapWithCoords(lon: number, lat: number) {
        this.map.setCamera({zoom: 12, maxZoom: 16, center: [lon, lat]});
      }
    
      ngOnDestroy() {
        for (const s of this.subscriptions) {
          s.unsubscribe();
        }
      }
    }
    

    Now that your Azure Maps component is complete, all you have to do is call an instance of your component within the .html of whatever view you'd like to place it in and coordinate the required inputs and output:

    <app-azure-map
        [markerDataSubject]="locationMarkerSubject"
        [centerMapSubject]="centerMapSubject"
        (outputCoordinates)="updateCoordinates($event)">
    </app-azure-map>
    

    The input subjects on your parent component should look something like this:

    locationMarkerSubject: Subject<Marker[]> = new Subject<Marker[]>();
    centerMapSubject: Subject<{lng: number, lat: number}> = new Subject<{lng: number, lat: number}>();
    

    And your updateCoordinates() function will handle the marker data sent back from user input upon clicking the map.