Tutorial

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

Updated on March 29, 2021
Default avatar

By Chris Engelsma

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

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:

  1. 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.

Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.

Learn more about us


About the authors
Default avatar
Chris Engelsma

author

Still looking for an answer?

Ask a questionSearch for more help

Was this helpful?
 
3 Comments


This textbox defaults to using Markdown to format your answer.

You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!

Thanks so much, this has been my absolutely first introduction to Leaflet for Angular.

Just a note:

  • the get from the shapes file in the service returns an Object (an Observable to);
  • the L.geoJson wants a FeatureCollection, I had to apply a cast to geojson.FeatureCollection to use this (first of all import * as geojson from ‘geojson’).

I have found this tutorial very useful. Thank you Chris Engelsma.

The article was very good, it helped me a lot, it could answer a question, as I add the “bindPopup” in the “Shapes” and if possible how to add the name of the state in the “Shape”

Thank you very much

Try DigitalOcean for free

Click below to sign up and get $200 of credit to try our products over 60 days!

Sign up

Join the Tech Talk
Success! Thank you! Please check your email for further details.

Please complete your information!

Get our biweekly newsletter

Sign up for Infrastructure as a Newsletter.

Hollie's Hub for Good

Working on improving health and education, reducing inequality, and spurring economic growth? We'd like to help.

Become a contributor

Get paid to write technical tutorials and select a tech-focused charity to receive a matching donation.

Welcome to the developer cloud

DigitalOcean makes it simple to launch in the cloud and scale up as you grow — whether you're running one virtual machine or ten thousand.

Learn more
DigitalOcean Cloud Control Panel