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
Then generate your Angular application (no strict typechecking, no routing, CSS)
ng new openlayers-angular
cd openlayers-angular
Install Openlayers
npm install --save ol
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"
],
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;
}
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>
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;
}
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'
});
}
}
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.
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
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:
- you entirely control the creation of the map
- 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);
}
}
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>
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
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);
}
}
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>
header[_ngcontent-cwb-c14] {
background-color: var(--header-color);
padding: 2em;
}
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;
}
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>
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;
}
And here the final result with the controls in the footer and the custom styled scale bar.
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
Top comments (2)
Can you give me the link to the next article
Greetings. Welcome to the most awesome dev.to. Feel free to ask comment and have fun. We are a fun loving coding gang. You are in good hands.
Welcome.