DEV Community

Cover image for Integrating Maps into your Angular Application with Leaflet[in-process]
ng-conf
ng-conf

Posted on

Integrating Maps into your Angular Application with Leaflet[in-process]

Jim Armstrong | ng-conf | Oct 2019

This article is oriented to beginning Angular developers wishing to integrate Leaflet into their applications. The integration of third-party dependencies into Angular can be anywhere from trivial to problematic. While Leaflet tends more towards the trivial side, maps that fluidly adjust with browser size can present some setup problems relative to the Angular lifecycle.

Before starting the deconstruction, point your friendly, neighborhood browser to the following Github.

theAlgorithmist/Angular8-Leaflet](https://github.com/theAlgorithmist/Angular8-Leaflet) on github.com

Now, you can follow along or fork the project and use it as a base for your own applications.

Background

This tutorial presumes a small amount of prior Leaflet knowledge, but no more than is expressed in the Leaflet Quickstart Guide.

ArcGis is used for tiling in the application covered in this article (the esri-leaflet package is installed from npm).

Angular Setup

Very little needs to be done after the ubiquitous npm install. Necessary Leaflet styles are made available through the angular.json file,

"styles": [
  "src/styles.scss",
  "./node_modules/leaflet/dist/leaflet.css"
],
Enter fullscreen mode Exit fullscreen mode

I’ve had mixed results with @types/leaflet, so this application does not exploit typings for sake of simplicity. Leaflet and ESRI tiles are imported as shown below,

import * as esri from 'esri-leaflet';
import * as L from 'leaflet';
Enter fullscreen mode Exit fullscreen mode

A Leaflet Map, for example, is temporarily typed as any; feel free to firm up typings as an exercise after you have deconstructed the tutorial.

Coordinates of a map’s center are important for map initialization. Many demos hardcode this information. As an alternative, this tutorial illustrates how to inject map-center coordinates using Angular’s Injection Tokens.

A coordinate is a two-dimensional point on a map, represented by latitude and longitude. An InjectionToken for a coordinate may be created as illustrated in the /src/app/tokens.ts file,

import { InjectionToken } from '@angular/core';

export const INIT_COORDS = new InjectionToken<{lat: number, long: number}>('INIT_COORDS');
Enter fullscreen mode Exit fullscreen mode

INIT_COORDS is used in the /src/app/app.module.ts file to perform the actual (lat/long) provision (or definition),

providers: [
  { provide: INIT_COORDS, useValue: {lat: 32.9756, long: -96.89} }
],
Enter fullscreen mode Exit fullscreen mode

This moves the definition of the map center to the highest level in the Angular application (where it is easily seen and almost self-documenting).

In order to inject this coordinate into the application, import INIT_COORDS from the tokens file and inject from a class constructor. This is illustrated in the /src/app/map.component.ts file (the primary map component in this tutorial),

constructor( @Inject(INIT_COORDS) protected _initCoords: {lat: number, long: number} )
Enter fullscreen mode Exit fullscreen mode

Map Component

The application covered in this article displays a fixed-height map with a width that extends across the entire browser window. The map redraws as the window resizes. Some markers are also rendered on top of the map tiles.

The map is integrated into the application through the main app component’s template, as shown below

/src/app/app.component.html

<app-map [markers]="markers"></app-map>
Enter fullscreen mode Exit fullscreen mode

and the map component’s source code is found in /src/app/maps/map.component.ts.

The map is rendered into a container (DIV) that is currently in the map component’s template,

/src/app/maps/map.component.html

<div>

  <div id="map-container">
    <div #primaryMap [style.height.px]="currentHeight" [style.width.px]="currentWidth"></div>

    <map-control class="map-control"></map-control>

    <div class="mouse-coords">
      <span id="mouse-coords-label"></span>
      <span [innerHTML]="mcText"></span>
    </div>
  </div>

</div>
Enter fullscreen mode Exit fullscreen mode

If the width and height of the containing DIV are fixed, then map tiles can be rendered at an early point in the component lifecycle. Two approaches are possible for passing variable width and height, outside-in and inside-out. The outside-in method defines Angular Inputs for width and height. The parent component is responsible for communicating width and height to the map component. The inside-out approach defines width and height internal to the map component and ‘passes’ that information to its template. Most applications use the outside-in approach. This tutorial employs the inside-out method for purposes of illustration. Modify the code to change the passing of width and height as an exercise.

Variable width and height introduce some subtle issues relative to map rendering. It is tempting to perform everything related to Leaflet setup in the on-init handler, and this typically works with fixed width and height. Since map width varies with browser size in this tutorial, it is better to separate the map initialization process between the on-init and after-view-init lifecycle handlers. This is illustrated in the following code segments from /src/app/maps/map.component.ts.

First, the identification of a containing DIV for map initialization is offloaded to a ViewChild,

@ViewChild('primaryMap', {static: true}) protected mapDivRef: ElementRef;
protected mapDiv: HTMLDivElement;
Enter fullscreen mode Exit fullscreen mode

Most of the map setup is performed in ngOnInit,

public ngOnInit(): void
{
  // Reference to DIV containing map is used in Leaflet initialization
  this.mapDiv = this.mapDivRef.nativeElement;

  this.__initializeMap();
  this.__renderMap();
  this.__showMarkers();
}
Enter fullscreen mode Exit fullscreen mode

Invalidating the map size is deferred to ngAfterViewInit, when the full width and height necessary for the map tiles to work is available. The map variable refers to the Leaflet Map created in the ngOnInit handler.

public ngAfterViewInit(): void
{
  this.map.invalidateSize();

  this.__initMapHandlers();
}
Enter fullscreen mode Exit fullscreen mode

Note that the update map size handler recomputes the class currentWidth and currentHeight variables, which are bound to the DIV styles that control the width and height of the map DIV. Leaflet uses that width/height information to re-tile and re-render the map.

Since map width/height definition and resizing the map with window changes are handled internal to the map component, a simple window-resize handler is added.

@HostListener('window:resize', ['$event'])
protected __onResize(event: any): void
{
  this.__updateMapSize();

  this.map.invalidateSize();
}
Enter fullscreen mode Exit fullscreen mode

This could also be handled by an outside service, run outside Angular, and debounced. If there is sufficient interest, I will provide a tutorial that illustrates the technique.

Map Controls

Leaflet does allow custom controls(this will be a link), but you may also use Angular components as map controls. Create the Angular component and use CSS for positioning. I’ve used this in several projects for highly customized zoom controls. See the /src/app/maps/map-control folder for a skeleton that may be used as a starting point.

Refer to /src/app/maps/map.component.scss for relevant styling.

Beware the z-index

If tiling or some other aspect of map display does not appear to work, it may be an issue with z-index. In the past, I’ve had to re-map z-indices simply from changing tile providers or even migrating from Angular 7 to Angular 8!

Map Markers

Markers with known coordinates may be added to a map. Define the coordinates and then create a Leaflet Icon for each one. Marker coordinates for this tutorial are defined in the main app component,

/src/app/app.component.ts

public markers: {lat: number, long: number}[];   // Map markers (relevance depends on map center)

constructor()
{
  // some map markers
  this.markers = [
    { lat: 32.9756, long: -96.89   },
    { lat: 33.1543, long: -96.8352 },
    { lat: 32.93  , long: -96.8195 },
    { lat: 32.8998, long: -97.0403 },
    { lat: 33.0737, long: -96.3697 },
    { lat: 33.1014, long: -96.6744 }
  ];
}
Enter fullscreen mode Exit fullscreen mode

and are passed as an Angular Input to the Map Component,

/src/app/app.component.html

<app-map [markers]="markers"></app-map>
Enter fullscreen mode Exit fullscreen mode

Markers are rendered in the map component,

/src/app/maps/map.component.ts

protected __showMarkers(): void
{
  if (this.markers !== undefined && this.markers != null && this.markers.length > 0)
  {
    // Add markers
    const icon = L.icon({
      iconUrl: MapIconOptions.mapIcon,
      iconSize: MapIconOptions.iconSize,
      iconAnchor: MapIconOptions.iconAnchor,
      shadowUrl: MapIconOptions.mapShadowIcon,
      shadowSize: MapIconOptions.shadowSize,
      shadowAnchor: MapIconOptions.shadowAnchor,
    });

    const n: number = this.markers.length;
    let i: number;
    let m: L.marker;

    let x: number;
    let y: number;

    for (i = 0; i < n; ++i) {

      x = this.markers[i].lat;
      y = this.markers[i].long;

      if (x !== undefined && !isNaN(x) && y !== undefined && !isNaN(y))
      {
        // okay to add the icon
        m = L.marker([x, y], {icon: icon}).addTo(this.map);
      }
      else
      {
        // implement your own error handling
        console.log('MARKER ERROR, Marker number: ', (i+1), 'x: ', x, ' y: ', y);
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Interactivity

A couple handlers for basic map interactivity have been added for you, namely mouse-click and mouse-move. The latter updates the current map position (lat/long) based on the mouse position.

/src/app/maps/map.component.ts

protected __onMapMouseMove(evt: any): void
{
  const lat: string  = evt.latlng.lat.toFixed(3);
  const long: string = evt.latlng.lng.toFixed(3);
  this.mcText        = `Latitude: ${lat} &nbsp; &nbsp; Longitude: ${long}`;
}
Enter fullscreen mode Exit fullscreen mode

The string reflecting the current position is displayed in the map component’s template,

/src/app/maps/map.component.html

<div class="mouse-coords">
  <span id="mouse-coords-label"></span>
  <span [innerHTML]="mcText"></span>
</div>
Enter fullscreen mode Exit fullscreen mode

The Result

Build the application and you should observe something similar to this screenshot,

Image for post

Angular version 8 and Leaflet Map

Move the mouse over the map to observe updates of the mouse position in map coordinates.

I hope this gets you started on working with Angular and Leaflet. These two applications work very well together and there is near-limitless potential for highly interactive, engaging maps.

Good luck with your Angular efforts!

For more Angular goodness, be sure to check out the latest episode of The Angular Show podcast.


ng-conf: Join us for the Reliable Web Summit

Come learn from community members and leaders the best ways to build reliable web applications, write quality code, choose scalable architectures, and create effective automated tests. Powered by ng-conf, join us for the Reliable Web Summit this August 26th & 27th, 2021.
https://reliablewebsummit.com/

Top comments (1)

Collapse
 
nguepi profile image
Idriss Nguepi

How to integrate the interactive map? So that the user can be able to move the marker directly on the map???