Openlayers in an Angular application - Basics

Florent Gravin - Mar 22 '21 - - Dev Community

It's always exciting starting the development of a new web mapping application. You already think about the beautiful maps you want to render, the data you want to provide and all the tools which will make your map interactions unique.
Before landing to this perfect picture, you'll have to make important choices in term of architecture and technologies.

For the mapping library, Openlayers would make a great candidate cause it's very flexible and rich in terms of features. Then you need to consider using a framework or not, and if yes, what framework. There is no good or bad choices regarding the pairing with Openlayers, all would work either way.

This article provides a step by step guide walking through the creation of a web mapping application based on Angular and Openlayers. It is the first step of a serie of articles that will cover more and more complex usecases.

We'll see first all the required setup to make both library running together. We'll then add our first map and introduce what would be a correct way to design the architecture of some usefull geospatial Angular components like:

  • Map
  • Mouse position
  • Scale line

Setup

First you need to install Angular-cli



npm install -g @angular/cli


Enter fullscreen mode Exit fullscreen mode

Then generate your Angular application (no strict typechecking, no routing, CSS)



ng new openlayers-angular
cd openlayers-angular


Enter fullscreen mode Exit fullscreen mode

Install Openlayers



npm install --save ol


Enter fullscreen mode Exit fullscreen mode

Add Openlayers CSS to the build process: open angular.json and jump into /projects/openlayers-angular/architect/build/options/styles properties to link the css



"styles": [
  "src/styles.css",
  "node_modules/ol/ol.css"
],


Enter fullscreen mode Exit fullscreen mode

Add a map

The root component of your Angular application is app.component. Let's design the global layout of the application, with a header, a footer, a side bar and a panel to render the map.

Edit first the root styles.css, this CSS file is not attached to any component and there is no style encapsulation, all the rules defined here will be applied in the whole application. It is the right place to declare your CSS variables, import your fonts and add the rules for the root elements like body or html.



@import url('https://fonts.googleapis.com/css?family=Roboto');
body {
    font-family: 'Roboto';
    color: var(--text-color);
    margin: 0;
    --header-color: #D1DFB7;
    --sidebar-color: #FAE6BE;
    --text-color: black;
}


Enter fullscreen mode Exit fullscreen mode

Create the layout in app.component.html



<header>
  <div class="title">Map Viewer - Openlayers & Angular</div>
</header>
<main>
  <div class="left-bar"></div>
  <div id="ol-map" class="map-container"></div>
</main>
<footer>
  Footer
</footer>



Enter fullscreen mode Exit fullscreen mode

And the associated app.component.css



:host {
    display: flex;
    flex-direction: column;
    height: 100vh;
}
header {
    background-color: var(--header-color);
    padding: 2em;
}
header .title {
    font-size: 28px;
}
main {
    display: flex;
    flex-grow: 1;
}
.left-bar {
    width: 20em;
    background-color: var(--sidebar-color);
}
.map-container {
    flex-grow: 1;
}
footer {
    background-color: var(--header-color);
    padding: 1em;
}



Enter fullscreen mode Exit fullscreen mode

Now ceate a simple Openlayers map in the root component and attach it to the map container. Usually, you can define your Openlayers map in the ngOnInit() method, the component will be ready and Openlayers can correctly attach the map to the DOM. See the component lifecycle documentation for more information, ngAfterViewInit() might be a good candidate as well.



import { Component, OnInit } from '@angular/core';
import Map from 'ol/Map';
import View from 'ol/View';
import TileLayer from 'ol/layer/Tile';
import OSM from 'ol/source/OSM';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnInit {

  map: Map;

  ngOnInit(): void {
    this.map = new Map({
      view: new View({
        center: [0, 0],
        zoom: 1,
      }),
      layers: [
        new TileLayer({
          source: new OSM(),
        }),
      ],
      target: 'ol-map'
    });
  }
}


Enter fullscreen mode Exit fullscreen mode

Fair, we now have our Map and a decent layout to build your application uppon to. Let's dig a little bit more to do it the Angular way.

Alt Text

Create a Map component

Note that the map is displayed in the page because of 2 things: the target: 'ol-map' option in the map creation will refer to the element that has the corresponding id: <div id="ol-map" class="map-container"></div>.

Let's see how we could create a map component that manages it for us.

Create the map component



ng generate component components/Map --changeDetection=OnPush --style=css --inlineTemplate=true --inlineStyle=true


Enter fullscreen mode Exit fullscreen mode

This component is designed to draw the map, not to create it, it is a dumb component, so we pass the map as an Input(). I mostly prefer the imperative approach: you have a component (here the root one) where you create the map by your own, and you pass it as an input to all sub components that needs it. The opposite approach (declarative) would provide a component that accepts the map configuration (extent, zoom, layers) as inputs and which would create the map and return it as an output. I see 2 benefit of the imperative approach:

  1. you entirely control the creation of the map
  2. the map is created and ready before sub components are initialized, in a synchronous way.

To render the map in the component, we inject the ElementRef in the constructor, which is a reference to the root element of the component itself. We can then pass the HTML native element where we want to render the map, with the setTarget(this.elementRef.nativeElement) function.

map.component.ts



import { Component, OnInit, ChangeDetectionStrategy, Input, ElementRef } from '@angular/core';
import Map from 'ol/Map';

@Component({
  selector: 'app-map',
  template: '',
  styles: [':host { width: 100%; height: 100%; display: block; }',
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MapComponent implements OnInit {
  @Input() map: Map;
  constructor(private elementRef: ElementRef) {
  }
  ngOnInit() {
    this.map.setTarget(this.elementRef.nativeElement);
  }
}


Enter fullscreen mode Exit fullscreen mode

Note that the component should have a full width/height so the map can be renderer in the whole container. Angular component are not <div> so we must specify display:block if we want them to be displayed as so.

Now, let's import the map component from the root component:
app.component.ts



  <div class="map-container">
    <app-map [map]="map"></app-map>
  </div>


Enter fullscreen mode Exit fullscreen mode

The result is visually exactly the same as before, but you delegate the map rendering to a dedicated component. You can use this component several times in your application and you'll never get any conflict about the map target element.

Let's go further and create components for other generic Openlayers artefacts, we have a map, now let's add a mouse position and a scale line to see what is the Angular way to deal with Openlayers controls.

Scale line component

The idea is to seggregate the concerns and not to put too much responsabilities in the root component. We don't want to manage everything related to our map view at the same place, but we want to delegate this work to components.

Create the scale line component



ng generate component components/Scaleline --changeDetection=OnPush --style=css --inlineTemplate=true --inlineStyle=true


Enter fullscreen mode Exit fullscreen mode

The approach is globally the same as for the map component, this component will just be the host for an Openlayers artefact. The idea is that the control is created inside the component and nowhere else, so it is added to the map only if the component is present in the application template.



import { Component, OnInit, ChangeDetectionStrategy, Input, ElementRef } from '@angular/core';
import Map from 'ol/Map';
import ControlScaleLine from 'ol/control/ScaleLine';

@Component({
  selector: 'app-scaleline',
  template: ``,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ScalelineComponent implements OnInit {
  @Input() map: Map;
  control: ControlScaleLine;

  constructor(private elementRef: ElementRef) {}

  ngOnInit() {
    this.control = new ControlScaleLine({
      target: this.elementRef.nativeElement,
    });
    this.map.addControl(this.control);
  }
}
```
Note that the component only responsability is to created the control, to tell the control to render its content into the host, and to add the control in the map. You could use this approach to any Openlayers control and respect the responsabilty segregation concerns.

# Mouse position
The mouse position control is a little bit more complex cause it relies on a coordinate format function. It's a perfect opportunity to introduce Angular Services, the logic should not be encapsulated into a component but shared as a service. Let's create this service which responsability is to format the coordinates giving formatting options:

```
ng generate service services/CoordinateFormatter
```
The service will expose a method to format the coordinates, depending on a template and the amout of expected digits.

`coordinate-formatter.service.ts`
```ts
import { Injectable } from '@angular/core';
import { DecimalPipe } from '@angular/common';

@Injectable({
  providedIn: 'root',
})
export class CoordinateFormatterService {
  constructor(private decimalPipe: DecimalPipe) {
  }

  numberCoordinates(
    coordinates: number[],
    fractionDigits: number = 0,
    template?: string,
  ) {
    template = template || '{x} {y}';

    const x = coordinates[0];
    const y = coordinates[1];
    const digitsInfo = `1.${fractionDigits}-${fractionDigits}`;
    const sX = this.decimalPipe.transform(x, digitsInfo);
    const sY = this.decimalPipe.transform(y, digitsInfo);
    return template.replace('{x}', sX).replace('{y}', sY);
  }
}
```
Now create your Angular component for the mouse position control. The logic is the same as `ScaleLineComponent`, the addition here would be the usage of our new service.

Create the component
```
ng generate component components/MousePosition --changeDetection=OnPush --style=css --inlineTemplate=true --inlineStyle=true
```
Add the mouse position control, set its target as before, and bind it to the coordinate map service.

```ts
import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  Input,
  ElementRef,
} from '@angular/core';
import Map from 'ol/Map';
import ControlMousePosition from 'ol/control/MousePosition';
import { CoordinateFormatterService } from '../../services/coordinate-formatter.service';

@Component({
  selector: 'app-mouse-position',
  template: ``,
  styles: [],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class MousePositionComponent implements OnInit {

  @Input() map: Map;
  @Input() positionTemplate: string;
  control: ControlMousePosition;

  constructor(
    private element: ElementRef,
    private coordinateFormatter: CoordinateFormatterService,
  ) {
  }

  ngOnInit() {
    this.control = new ControlMousePosition({
      className: 'mouseposition-control',
      coordinateFormat: (coordinates: number[]) => this.coordinateFormatter
        .numberCoordinates(coordinates, 4, this.positionTemplate),
      target: this.element.nativeElement,
      undefinedHTML: undefined,
    });
    this.map.addControl(this.control);
  }
}


Enter fullscreen mode Exit fullscreen mode

The component logic is very simple, we just pass the coordinates template as an input. In real life, we could extend this component to handle more options like the projection we want the mouse position to be rendered in, a DMS format and more...

Style Openlayers inner HTML

Angular component view encapsulation is a mecanism to attach component CSS only to component HTML. By default, it adds a random attribute to all HTML elements of the component and bind this attribute to the component CSS rules:



<header _ngcontent-cwb-c14="">
   <div _ngcontent-cwb-c14="" class="title">
      Map Viewer - Openlayers Angular
   </div>
</header>


Enter fullscreen mode Exit fullscreen mode


header[_ngcontent-cwb-c14] {
    background-color: var(--header-color);
    padding: 2em;
}


Enter fullscreen mode Exit fullscreen mode

The issue is that when Openlayers renders the HTML for the control, it does not attach this attribute so all the CSS rules you define in your component for the control won't be applied. To be sure you correctly target Openlayers HTML elements, you must add the keyword ng-deep which means that the rules will be applied anywhere in the nested elements of the component.

In mouse-position.component.ts, add the following CSS rules to change the rendering of the scale line:



::ng-deep .ol-scale-line {
      position: relative;
  }
::ng-deep .ol-scale-line, ::ng-deep .ol-scale-line-inner {
      background-color: transparent;
      border-color: var(--text-color);
      color: var(--text-color);
      font-size: inherit;
      bottom: auto;
  }


Enter fullscreen mode Exit fullscreen mode

Final rendering

Include our last 2 components in the footer of our application and align them correctly. Both components take the map as inputs, and the scale line component takes also the coordinates templating format, which indicates we want to call the numberCoordinates method, display no digit, and apply the given template.



<footer>
  <app-scaleline [map]="map"></app-scaleline>
  <app-mouse-position [map]="map" positionTemplate="{x}, {y} m"></app-mouse-position>
</footer>


Enter fullscreen mode Exit fullscreen mode

To have them correctly aligned in the footer, let's update the app.component.css



footer {
    display: flex;
    background-color: var(--header-color);
    padding: 1em;
    justify-content: space-between;
}


Enter fullscreen mode Exit fullscreen mode

And here the final result with the controls in the footer and the custom styled scale bar.
Alt Text

Conclusion

Through this article, we saw how to set up Openlayers in an Angular application and we already cover simple but concrete usecases around web mapping needs. Next articles will help you to oversee deeper integration of the libraries and bring more interactivity to your maps (layers, features, styling, interactions...).

You can find the code of this article on https://github.com/fgravin/angular-openlayers-tutorial/tree/1-basics

. . . . . . . . . . . . . . . . . . . . . . . .