Tutorial

How To Build Maps in Angular with Leaflet, Part 2: The Marker Service

Angular

Introduction

Leaflet supports markers. These are indicators placed on the map that can contain information. This provides a way to highlight landmarks and destinations on a map.

Note: This is Part 2 of a 4-part series on using Angular and Leaflet.

In this tutorial, you will learn how to add markers to your map using a service to manage the marker logic.

Prerequisites

To complete this tutorial, you will need:

  • This tutorial builds directly upon the installation and steps in previous parts.

Step 1 — Downloading the GeoJSON Data

This tutorial will plot GeoJSON data for the state capitals of the United States of America. It will also include some additional metadata for state names, capital names, and population.

Note: The usa-capitals.geojson file is available in the accompanying project repo.

Create a new data subdirectory under the assets directory:

  • mkdir src/assets/data

Then, save the usa-capitals.geojson file in this directory.

Step 2 — Creating the Marker Service

At this point, you should have a working implementation of Leaflet in an Angular application.

Use your terminal window to navigate to the project directory. Then, run the following command to generate a new service:

  • npx @angular/cli generate service marker --skip-tests

This will create a new file: marker.service.ts.

Next, you will add this new service as a provider in your app.module.ts. You will also be loading the data from your assets folder so you will need to include the HttpClientModule.

Open app.module.ts in your code editor and make the following changes:

src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { HttpClientModule } from '@angular/common/http';
import { MarkerService } from './marker.service';

import { AppComponent } from './app.component';
import { MapComponent } from './map/map.component';

@NgModule({
  declarations: [
    AppComponent,
    MapComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    MarkerService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

Your application now supports your new MarkerService.

Step 3 — Loading and Plotting Markers

Next, open your newly created marker.service.ts in your code editor and add HttpClient to the constructor:

src/app/marker.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class MarkerService {
  capitals: string = '/assets/data/usa-capitals.geojson';

  constructor(private http: HttpClient) { }
}

Create a new function that will load the GeoJSON data and create the markers. This function will take in a Leaflet map as a parameter.

Modify marker.service.ts to import Leaflet and declare a makeCapitalMarkers function:

src/app/marker.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class MarkerService {
  capitals: string = '/assets/data/usa-capitals.geojson';

  constructor(private http: HttpClient) { }

  makeCapitalMarkers(map: L.map): void { }
}

Using HttpClient, get the data and subscribe to the result.

Once you have the data, you will then loop through each feature, construct a marker, and add it to the map.

src/app/marker.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class MarkerService {
  capitals: string = '/assets/data/usa-capitals.geojson';

  constructor(private http: HttpClient) {
  }

  makeCapitalMarkers(map: L.map): void {
    this.http.get(this.capitals).subscribe((res: any) => {
      for (const c of res.features) {
        const lon = c.geometry.coordinates[0];
        const lat = c.geometry.coordinates[1];
        const marker = L.marker([lat, lon]);

        marker.addTo(map);
      }
    });
  }
}

This code handles the logic for loading and adding markers to the map.

Now, you will have to call this method from MapComponent:

src/app/map/map.component.ts
import { Component, AfterViewInit } from '@angular/core';
import * as L from 'leaflet';
import { MarkerService } from '../marker.service';

@Component({
  selector: 'app-map',
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.css']
})
export class MapComponent implements AfterViewInit {
  private map;

  private initMap(): void {
    this.map = L.map('map', {
      center: [ 39.8282, -98.5795 ],
      zoom: 3
    });

    const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 18,
      minZoom: 3,
      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    });

    tiles.addTo(this.map);
  }

  constructor(private markerService: MarkerService) { }

  ngAfterViewInit(): void {
    this.initMap();
    this.markerService.makeCapitalMarkers(this.map);
  }
}

If you were to run your application at this point, you would encounter two errors in your console:

Output
marker-icon-2x.png:1 GET http://localhost:4200/marker-icon-2x.png 404 (Not Found) marker-shadow.png:1 GET http://localhost:4200/marker-shadow.png 404 (Not Found)

You will need to import Leaflet’s assets to your project to reference the marker-icon-2x.png and marker-shadow.png image files.

Open the angular.json file and add the Leaflet images directory:

angular.json
{
  // ...
  "projects": {
    "angular-leaflet-example": {
      // ...
      "architect": {
        "build": {
          // ...
          "options": {
            // ...
            "assets": [
              "src/favicon.ico",
              "src/assets",
              {
                "glob": "**/*",
                "input": "node_modules/leaflet/dist/images/",
                "output": "./assets"
              }
            ],
            // ..
          },
          // ...
        },
        // ...
      }
    }},
  "defaultProject": "angular-leaflet-example"
}

This code will copy Leaflet’s marker images locally.

Then, revisit the map.component.ts and define the icon:

src/app/map/map.component.ts
import { Component, AfterViewInit } from '@angular/core';
import * as L from 'leaflet';
import { MarkerService } from '../marker.service';

const iconRetinaUrl = 'assets/marker-icon-2x.png';
const iconUrl = 'assets/marker-icon.png';
const shadowUrl = 'assets/marker-shadow.png';
const iconDefault = L.icon({
  iconRetinaUrl,
  iconUrl,
  shadowUrl,
  iconSize: [25, 41],
  iconAnchor: [12, 41],
  popupAnchor: [1, -34],
  tooltipAnchor: [16, -28],
  shadowSize: [41, 41]
});
L.Marker.prototype.options.icon = iconDefault;

@Component({
  selector: 'app-map',
  templateUrl: './map.component.html',
  styleUrls: ['./map.component.css']
})
export class MapComponent implements AfterViewInit {
  private map;

  constructor(private markerService: MarkerService) { }

  private initMap(): void {
    this.map = L.map('map', {
      center: [ 39.8282, -98.5795 ],
      zoom: 3
    });

    const tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
      maxZoom: 18,
      minZoom: 3,
      attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
    });

    tiles.addTo(this.map);
  }

  ngAfterViewInit(): void {
    this.initMap();
    this.markerService.makeCapitalMarkers(this.map);
  }
}

Save your changes. Then, stop your application and relaunch it. Open the application in your web browser (localhost:4200) and observe the markers for the state capitals:

Screenshot of a map of the United States with marker pins indicating the location of state capitals.

At this point, you have a map that supports default markers.

Step 4 — Displaying Circle Markers

In this next step, you will change the markers from icons to circles. Then scale the size of the circles to reflect the population of the state capitol.

Open MarkerService and create a makeCapitalCircleMarkers() function. It will be very similar to the makrCapitalMarkers() function. Instad of Leaflet’s marker method, you will use the circleMarker method:

src/app/marker.service.ts
makeCapitalCircleMarkers(map: L.map): void {
  this.http.get(this.capitals).subscribe((res: any) => {
    for (const c of res.features) {
      const lon = c.geometry.coordinates[0];
      const lat = c.geometry.coordinates[1];
      const circle = L.circleMarker([lat, lon]);

      circle.addTo(map);
    }
  });
}

Then, call this function in MapComponent:

src/app/map/map.component.ts
ngAfterViewInit(): void {
  this.initMap();
  // this.markerService.makeCapitalMarkers(this.map);
  this.markerService.makeCapitalCircleMarkers(this.map);
}

Save these changes and open the application in your web browser (localhost:4200):

Screenshot of a map of the United States with circles indicating the location of state capitals.

The icons have now been replaced with circles.

circleMarker accepts a third optional parameter. This object can contain a radius property. In your MarkerService, modify the makeCapitalCircleMarkers function to use a radius of 20:

const circle = L.circleMarker([lat, lon], { radius: 20 }).addTo(map);

This code sizes all radii to be the same value (20).

Next, you will change the radius to reflect the population of the state capital:

static scaledRadius(val: number, maxVal: number): number {
  return 20 * (val / maxVal);
}

This function takes in a value (population), a max value (maximum population), and returns a radius in the range [0 - 20].

You will use the spread-operator and map to find the capital with the largest population:

const maxPop = Math.max(...res.features.map(x => x.properties.population), 0);

From the GeoJSON data, the largest population will be: “Phoenix, Arizona” (1626078).

Finally, you will put it all together by using ScaledRadius as the radius function.

Open MarkerService in your code editor and make the following changes:

src/app/marker.service.ts
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import * as L from 'leaflet';

@Injectable({
  providedIn: 'root'
})
export class MarkerService {
  capitals: string = '/assets/data/usa-capitals.geojson';

  constructor(private http: HttpClient) { }

  static scaledRadius(val: number, maxVal: number): number {
    return 20 * (val / maxVal);
  }

  makeCapitalMarkers(map: L.map): void {
    this.http.get(this.capitals).subscribe((res: any) => {
      for (const c of res.features) {
        const lon = c.geometry.coordinates[0];
        const lat = c.geometry.coordinates[1];
        const marker = L.marker([lat, lon]);

        marker.addTo(map);
      }
    });
  }

  makeCapitalCircleMarkers(map: L.map): void {
    this.http.get(this.capitals).subscribe((res: any) => {

      const maxPop = Math.max(...res.features.map(x => x.properties.population), 0);

      for (const c of res.features) {
        const lon = c.geometry.coordinates[0];
        const lat = c.geometry.coordinates[1];
        const circle = L.circleMarker([lat, lon], {
          radius: MarkerService.scaledRadius(c.properties.population, maxPop)
        });

        circle.addTo(map);
      }
    });
  }
}

Save your changes. Then, stop your application and relaunch it. Open the application in your web browser (localhost:4200) and observe the new scaled circle markers for state capitals:

Screenshot of a map of the United States with circles scaled to indicate the population sizes.

You now have a map that supports markers.

Conclusion

In this post, you created a marker service that loads data and constructs markers. You learned how to create two types of markers: L.marker and L.circleMarker. Finally, you learned how to define the size of each circle marker by passing a function for the radius.

Continue to Part 3 of this series on using Angular and Leaflet.

Creative Commons License