Tutorial

How To Build Maps in Angular with Leaflet, Part 4: The Shape Service

Angular

Introduction

Leaflet supports shapes. By providing a GeoJSON file that contains data for boundaries, you can indicate counties, states, and countries on your map.

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

In this tutorial, you will learn how to render shapes for the continental states of the United States of America.

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 outlines of the states of the United States of America.

Visit Eric Celeste’s GeoJSON and KML data for the United States and download the 5m GeoJSON file (gz_2010_us_040_00_5m.json).

Save this file in your /assets/data directory.

Step 2 — Creating the Shape 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 shape --skip-tests

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

Next, you will add this new service as a provider in your app.module.ts.

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 { PopupService } from './popup.service';
import { ShapeService } from './shape.service';

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

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

Your application now supports your new ShapeService.

Step 3 — Loading the Shapes

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

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

@Injectable({
  providedIn: 'root'
})
export class ShapeService {
  constructor(private http: HttpClient) { }

  getStateShapes() {
    return this.http.get('/assets/data/gz_2010_us_040_00_5m.json');
  }
}

The function getStateShapes() will return an observable of the serialized GeoJSON object. To use this, you will need to subscribe to the observable in your MapComponent.

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

// ...

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

  constructor(
    private markerService: MarkerService,
    private shapeService: ShapeService
  ) { }

  // ...

  ngAfterViewInit(): void {
    this.initMap();
    this.markerService.makeCapitalCircleMarkers(this.map);
    this.shapeService.getStateShapes().subscribe(states => {
      this.states = states;
    });
  }
}

This code injects the ShapeService in the constructor, creates a local variable to store the data, and calls the getStateShapes() function to pull the data and subscribe to the result.

Note: An even better approach would be to pre-load the data in a resolver.

Once the data is loaded, you will need to add the shapes to the map as a layer. Leaflet provides a factory just for GeoJSON layers that you can leverage. Let’s put this logic in its own function and then call it after the data has been resolved.

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

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

  // ...

  private initStatesLayer() {
    const stateLayer = L.geoJSON(this.states, {
      style: (feature) => ({
        weight: 3,
        opacity: 0.5,
        color: '#008f68',
        fillOpacity: 0.8,
        fillColor: '#6DB65B'
      })
    });

    this.map.addLayer(stateLayer);
  }

  ngAfterViewInit(): void {
    this.initMap();
    this.markerService.makeCapitalCircleMarkers(this.map);
    this.shapeService.getStateShapes().subscribe(states => {
      this.states = states;
      this.initStatesLayer();
    });
  }
}

The initStatesLayer() function creates a new GeoJSON layer and adds it to the map.

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

Screenshot of a map with shapes for the states.

Next, you will will attach mouseover and mouseout events to interact with each of the shapes with onEachFeature:

src/app/map/map.component.ts
private highlightFeature(e) {
  const layer = e.target;

  layer.setStyle({
    weight: 10,
    opacity: 1.0,
    color: '#DFA612',
    fillOpacity: 1.0,
    fillColor: '#FAE042'
  });
}

private resetFeature(e) {
  const layer = e.target;

  layer.setStyle({
    weight: 3,
    opacity: 0.5,
    color: '#008f68',
    fillOpacity: 0.8,
    fillColor: '#6DB65B'
  });
}

private initStatesLayer() {
  const stateLayer = L.geoJSON(this.states, {
    style: (feature) => ({
      weight: 3,
      opacity: 0.5,
      color: '#008f68',
      fillOpacity: 0.8,
      fillColor: '#6DB65B'
    }),
    onEachFeature: (feature, layer) => (
      layer.on({
        mouseover: (e) => (this.highlightFeature(e)),
        mouseout: (e) => (this.resetFeature(e)),
      })
    )
  });

  this.map.addLayer(stateLayer);
}

Save your changes. Then, open the application in your web browser (localhost:4200) and move your mouse over the shapes:

Screenshot of a map with the shape for the state of Texas indicated.

However, the markers appear faint because the shape layer is above the marker layer.

There are two approaches to addressing this. The first approach would be to move the makeCapitalCircleMarkers() call directly after initStatesLayer(). The second approach would be to call bringToBack() on the shape layer after it is added to the map.

Here is the complete map.component.ts file with the bringToBack() approach:

src/app/map/map.component.ts
import { Component, AfterViewInit } from '@angular/core';
import * as L from 'leaflet';
import { MarkerService } from '../marker.service';
import { ShapeService } from '../shape.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;
  private states;

  constructor(
    private markerService: MarkerService,
    private shapeService: ShapeService
  ) { }

  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);
  }

  private highlightFeature(e) {
    const layer = e.target;

    layer.setStyle({
      weight: 10,
      opacity: 1.0,
      color: '#DFA612',
      fillOpacity: 1.0,
      fillColor: '#FAE042'
    });
  }

  private resetFeature(e) {
    const layer = e.target;

    layer.setStyle({
      weight: 3,
      opacity: 0.5,
      color: '#008f68',
      fillOpacity: 0.8,
      fillColor: '#6DB65B'
    });
  }

  private initStatesLayer() {
    const stateLayer = L.geoJSON(this.states, {
      style: (feature) => ({
        weight: 3,
        opacity: 0.5,
        color: '#008f68',
        fillOpacity: 0.8,
        fillColor: '#6DB65B'
      }),
      onEachFeature: (feature, layer) => (
        layer.on({
          mouseover: (e) => (this.highlightFeature(e)),
          mouseout: (e) => (this.resetFeature(e)),
        })
      )
    });

    this.map.addLayer(stateLayer);
    stateLayer.bringToBack();
  }

  ngAfterViewInit(): void {
    this.initMap();
    // this.markerService.makeCapitalMarkers(this.map);
    this.markerService.makeCapitalCircleMarkers(this.map);
    this.shapeService.getStateShapes().subscribe(states => {
      this.states = states;
      this.initStatesLayer();
    });
  }
}

Save your changes. Then, open the application in your web browser (localhost:4200) and observe the scaled circle markers for state capitals and the shapes for state borders:

Screenshot of a map with shapes and markers.

You now have a map that supports shapes.

Conclusion

In this post, you created a shape service that loads data and constructs shapes. You added interactivity with L.GeoJSON’s onEachFeature() and L.DomEvent.On.

This concludes the 4-part series on using Angular and Leaflet.

If you’d like to learn more about Angular, check out our Angular topic page for exercises and programming projects.

Creative Commons License