blog.itcode.devblog.itcode.dev

A Guide for Developers Traveling Through OpenLayers - 12. Displaying Useful Map Information

When working with maps, there are various situations where you need to obtain information such as the coordinates of the area you're currently viewing, the coordinates where the mouse is positioned on the map, the zoom level, and more. To satisfy these needs, creating a status panel on the map that outputs related information would be useful whenever needed.

A Guide for Developers Traveling Through OpenLayers - 12. Displaying Useful Map Information

When working with maps, there are various situations where you need to obtain information such as the coordinates of the area you're currently viewing, the coordinates where the mouse is positioned on the map, the zoom level, and more. To satisfy these needs, creating a status panel on the map that outputs related information would be useful whenever needed.
RWB0104
@RWBwritten at 2022-03-22 13:37:21
A Guide for Developers Traveling Through OpenLayers

시리즈 모아보기

A Guide for Developers Traveling Through OpenLayers

12 / 23

When working with maps, there are various situations where you need to obtain information such as the coordinates of the area you're currently viewing, the coordinates where the mouse is positioned on the map, the zoom level, and more.

To satisfy these needs, creating a status panel on the map that outputs related information would be useful whenever needed.


This chapter covers how to extract potentially useful information from a Map.

The information to extract is as follows.

  • Coordinate system EPSG code
  • Zoom level
  • Coordinates of the current area
  • Coordinates of the mouse

Additionally, this chapter also covers how to implement the following feature.

  • Changing map layers

Through this process, you'll be able to intuitively show information about the map and let users immediately switch to the type of map they want (e.g. satellite map).




Let's extract some potentially useful information from the Map object.

  • Feature: Elements such as points, lines, and polygons (vector layer only)
  • Source: The data source of a layer. Similar to a collection of Features. (SHP, GeoJSON, etc.)
  • Layer: A dataset defined based on a data source (vector, image)
  • View: Information about how the user currently views the map
  • Interaction: Interactive elements of the map (zoom in/out buttons, etc.)
  • Overlay: Elements to display on the map

Let's think about this in connection with the OpenLayers structure.

The Map object is denoted as map in the code.



The EPSG code is the coordinate system code currently used when we view the map. Every base map and object is rendered and represented on the map according to the coordinate system we've declared.

In other words, since it is determined by how we view the map, the View object should hold information related to the EPSG code.

TYPESCRIPT

const epsg: string = map.getView().getProjection().getCode();
  • getView(): A method that returns the map's View object
  • getProjection(): A method that returns the Projection object declared in the View
  • getCode(): A method that returns the EPSG code used by the Projection as a string

The EPSG code we want can finally be obtained from getCode().



The zoom level is the height at which we're currently viewing the map. The higher the zoom level, the more the map is zoomed in; the lower it is, the more the map is zoomed out.

In other words, since the zoom level is also determined by how we view the map, the View object should hold related information.

TYPESCRIPT

const zoom: number | undefined = map.getView().getZoom();
  • getView(): A method that returns the map's View object
  • getZoom(): A method that returns the map's zoom level as a number (may possibly be undefined)

The zoom level we want can be obtained from getZoom(). Note that getZoom() returns the zoom level as a number, but there's a possibility it returns undefined, so exception handling of the value is needed.


For the zoom level, the value changes every time the map is scrolled. Therefore, it's important to appropriately reflect the value every time the zoom level changes via an event.

The moveend event, which fires when the map's movement ends, is most appropriate. This is because measuring the zoom level after the map's movement has completely finished is accurate. Conversely, if you use movestart, an event that fires when the map begins moving, the value is measured before the zoom movement has finished, so an inaccurate value would be displayed.

TYPESCRIPT

map.on('moveend', () =>
{
	const zoom: number | undefined = map.getView().getZoom();
});

Detect the zoom level change in that event, and use it by rendering the value appropriately.



The current area coordinates are the coordinates of the area we're currently viewing on the map. The area's coordinates change every time the map is moved or zoomed in/out.

Likewise, since the area coordinates are also determined by how we view the map, the View object should hold related information.

TYPESCRIPT

const [ minX, minY, maxX, maxY ]: number[] = map.getView().calculateExtent();
  • getView(): A method that returns the map's View object
  • calculateExtent(): A method that returns the map's current extent coordinates

You can obtain the current coordinate area through calculateExtent(). You can also input a desired pixel area as a parameter, in which case it returns the coordinates corresponding to that pixel area.


For the current area, the value changes frequently as the map moves. Therefore, you need to design it to catch the event when the map moves and calculate the map's area.

You can insert a desired event into the Map object via the on() method. In this article, we insert this logic into the pointermove event, since mouse movement is always required to move the map.

TYPESCRIPT

map.on('pointermove', e =>
{
	// You can call the Map object from the event
	const [ minX, minY, maxX, maxY ]: number[] = e.map.getView().calculateExtent();
});

Since the cycle in which the value changes is quite fast, managing it as a state value isn't great. Because repeated state value changes happen in a short amount of time, rendering issues can occur.



The mouse position coordinates are the coordinates where the mouse is currently positioned on the map. The coordinates change every time the mouse moves on the map.

Strictly speaking, the mouse position isn't an object subordinate to Map. Since mouse-related events return the mouse position when they occur, it can inevitably only be obtained within a mouse event.

TYPESCRIPT

map.on('pointermove', e =>
{
	const [ x, y ]: number[] = e.coordinate;
});

As with the current area, since the value transformation happens repeatedly in a short amount of time, managing it as a state value can cause problems.




Already-declared Map objects and their sub-elements can also be changed using appropriate methods. This lets you change the Source object, Layer object, and so on. In other words, you can transform the map's properties in response to user interaction.

This chapter only covers how to change layers.



Through the feature of dynamically adding a layer, you can implement a map layer that changes according to user interaction.

This allows an implementation where the initial screen only shows the base map, but additional maps are then shown depending on the user's subsequent choice.

TYPESCRIPT

// Satellite map
const vworldSatelliteLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/Satellite/{z}/{y}/{x}.jpeg' }),
	properties: { name: 'base-vworld-satellite' },
	minZoom: 5,
	maxZoom: 19,
	zIndex: 2,
	preload: Infinity
});

map.addLayer(vworldSatelliteLayer);
  • addLayer(): A method that adds a Layer object to the map

It takes a Layer object implemented by extending BaseLayer as a parameter and adds a layer to the Map object.



Through the feature of dynamically removing a layer, you can implement a map layer that changes according to user interaction.

TYPESCRIPT

// Satellite map
map.getAllLayers().forEach(layer =>
{
	if (layer.get('name') === 'target')
	{
		map.removeLayer(layer);
	}
});
  • getAllLayers(): A method that returns an array of Layer objects declared in the map
  • addLayer(): A method that removes a Layer object from the map

It takes a Layer object implemented by extending BaseLayer as a parameter and removes that layer from Map.

You can get the Map's array of layers through getAllLayers(), and use this to extract and remove the layer you want.

Here's why we've always specified a properties object every time we've declared a Layer so far — if you assign a value based on a layer-naming convention to properties, it becomes easy to find the layer you want. Without such a unique value, it would be hard to find the layer you're looking for.



Beyond this, the Map object also provides various methods for dynamic CUD of its sub-objects. Not just Layer, but sub-objects such as Control, Overlay, and Interaction can also be dynamically managed. The principle is the same as with Layer.

The approach is to create the object you want to add, use the appropriate method among the Map's methods to add it, and find the element you want to remove and remove it with a method.




If you create a map info component that uses the Map object to provide needed information, it will be convenient to reuse across multiple maps.

Let's implement the component using the methods mentioned above.



Let's create a component that changes the base map. The base map can be one of 4 options excluding the hybrid map among OSM and VWorld maps, so you can choose one out of a total of 5 maps.

In this case, select would be appropriate.

The layer objects to use are as follows.

TYPESCRIPT

import TileLayer from 'ol/layer/Tile';
import { OSM, XYZ } from 'ol/source';

export const osmLayer = new TileLayer({
	source: new OSM({ attributions: '<p>Developed by <a href="https://itcode.dev" target="_blank">RWB</a></p>', cacheSize: 0 }),
	properties: { name: 'base-osm' },
	zIndex: 1,
	preload: Infinity
});

export const vworldBaseLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/Base/{z}/{y}/{x}.png' }),
	properties: { name: 'base-vworld-base' },
	minZoom: 5,
	maxZoom: 19,
	zIndex: 2,
	preload: Infinity
});

export const vworldGrayLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/gray/{z}/{y}/{x}.png' }),
	properties: { name: 'base-vworld-gray' },
	minZoom: 5,
	maxZoom: 18,
	zIndex: 2,
	preload: Infinity
});

export const vworldMidnightLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/midnight/{z}/{y}/{x}.png' }),
	properties: { name: 'base-vworld-midnight' },
	minZoom: 5,
	maxZoom: 18,
	zIndex: 2,
	preload: Infinity
});

export const vworldSatelliteLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/Satellite/{z}/{y}/{x}.jpeg' }),
	properties: { name: 'base-vworld-satellite' },
	minZoom: 5,
	maxZoom: 19,
	zIndex: 2,
	preload: Infinity
});

Manage a unique value per map type through the name in the properties object. This value will become the key when extracting a layer later.

For base maps, the name prefix is unified to always start with base.

TSX

<select value={layerState} onChange={(e) => setLayerState(e.target.value)}>
	<option value='base-osm'>OSM</option>
	<option value='base-vworld-base'>VWorld Base</option>
	<option value='base-vworld-gray'>VWorld Grayscale</option>
	<option value='base-vworld-midnight'>VWorld Night</option>
	<option value='base-vworld-satellite'>VWorld Satellite</option>
</select>

layerState, setLayerState() are state management objects for the layer's unique value.

Whenever this component's value changes, it's configured so that the option's value is designated as the state value, and the value is automatically reflected in the select.

TYPESCRIPT

useEffect(() =>
{
	// Remove all base map layers
	map.getAllLayers().filter(layer => (layer.get('name') as string).startsWith('base')).forEach(layer => map.removeLayer(layer));

	// Add a layer based on the selected value
	switch (layerState)
	{
		case 'base-vworld-base':
			map.addLayer(vworldBaseLayer);
			break;

		case 'base-vworld-gray':
			map.addLayer(vworldGrayLayer);
			break;

		case 'base-vworld-midnight':
			map.addLayer(vworldMidnightLayer);
			break;

		case 'base-vworld-satellite':
			map.addLayer(vworldSatelliteLayer);
			break;

		default:
			map.addLayer(osmLayer);
			setExtState(false);
			break;
	}
}, [ layerState ]);

Configure it so that logic runs every time layerState changes through useEffect().

Remove all base map layers, add the pre-designated layer according to the changed value, and default to OSM if an unexpected value comes in.

If OSM is selected, VWorld's extension maps will not be usable.



Let's create a component that changes whether the extension map is displayed. VWorld's hybrid map corresponds to this, and it's appropriate to toggle it On/Off.

Given this structure, input[type=checkbox] would be appropriate.

The layer objects to use are as follows.

TYPESCRIPT

import TileLayer from 'ol/layer/Tile';
import { XYZ } from 'ol/source';

export const vworldHybridLayer = new TileLayer({
	source: new XYZ({ url: 'https://api.vworld.kr/req/wmts/1.0.0/2AAC4DD9-4F6F-3844-A740-E2DB6BDC8CEF/Hybrid/{z}/{y}/{x}.png' }),
	properties: { name: 'ext-vworld-hybrid' },
	minZoom: 5,
	maxZoom: 19,
	zIndex: 3,
	preload: Infinity
});

Manage a unique value per map type through the name in the properties object. This value will become the key when extracting a layer later.

The base map layers are unified to always start with the base prefix in name. When changing base maps, all layers whose name starts with base are removed — the extension map is exempt from this.

TSX

<input type='checkbox' name='ext' checked={extState} disabled={layerState === 'base-osm'} onChange={(e) => setExtState(e.target.checked)} />

extState, setExtState() are state management objects for whether the extension map is displayed.

Configure it so the state value changes and is reflected in input[type=checkbox] each time it's checked/unchecked.

Additionally, to prevent use of the extension map when the base map is OSM, it is disabled when layerState === 'base-osm'.

TYPESCRIPT

useEffect(() =>
{
	// When adding the extension layer
	if (extState)
	{
		map.addLayer(vworldHybridLayer);
	}

	// When removing the extension layer
	else
	{
		map.getAllLayers().filter(layer => (layer.get('name') as string).startsWith('ext')).forEach(layer => map.removeLayer(layer));
	}
}, [ extState ]);

Configure it so that logic runs every time extState changes through useEffect().

When the extension map is used, add the extension map layer to the Map; when not used, remove the extension map layer.



Displays the map's EPSG code. Simply displayed as input[type=text].

Also, none of the maps in this project have a feature to dynamically change the coordinate system. Therefore, it's enough to check once after the map is rendered and display it.

TSX

<input name='proj' value={epsg} readOnly />

epsg, setEpsg() are state management objects for the EPSG code.

You just need to perform this once when the Map finishes loading. Therefore, register an event, but have it run only once.

TYPESCRIPT

map.once('postrender', () =>
{
	setEpsg(map.getView().getProjection().getCode());
});
  • once(): A method that registers an event to run exactly once
    • postrender: Runs after the map is rendered
  • getView(): A method that returns the View object of the Map
  • getProjection(): A method that returns the coordinate system object of the View
  • getCode(): A method that returns the coordinate system code

If you call the above methods before the Map has finished rendering, an error occurs. This is why the action must be performed after Map rendering.

Since we registered an event to run only once via once(), it only runs once after the first map rendering completes. In contrast, using the on() method registers a general event of the kind we commonly use.

If postrender were registered with on(), the action would run every time the map is rendered, such as on map movement or zooming in/out. Given that the EPSG code doesn't change, this would be a waste of computation.



Displays the zoom level. Simply displayed as input[type=text].

Since the zoom level changes when the map is zoomed in/out, it's best to register the event most appropriate for this action. Unlike the EPSG code, this needs to be an event that keeps firing continuously.

TSX

<input name='zoom' readOnly />

From the zoom level onward, we won't manage the value through state management. This is because too much state transformation happening can cause rendering problems. Of course, since the degree of change in the zoom level isn't that great, using state management wouldn't cause major issues either.

TYPESCRIPT

/**
 * Method to set the zoom level
 *
 * @param {number} level: zoom level
 */
function setZoom(level: number)
{
	const tag = meta.querySelector('input[name=zoom]');;

	// If the tag is valid
	if (tag)
	{
		tag.value = level.toString();
	}
}

Create a method to display the zoom level. It takes level as a parameter and assigns it to the input.

TYPESCRIPT

map.once('postrender', () =>
{
	const zoom = map.getView().getZoom() || 0;

	setZoom(zoom);
});

map.on('moveend', () =>
{
	const zoom = map.getView().getZoom() || 0;

	setZoom(zoom);
});
  • on(): A method that registers an event that keeps running continuously
  • once(): A method that registers an event to run exactly once
    • postrender: Runs after the map is rendered
    • moveend: Runs after the map has moved
  • getView(): A method that returns the View object of the Map
  • getZoom(): A method that returns the current View's zoom level. It may return undefined, so appropriate exception handling is needed.

Two events are registered. One is the postrender event that runs only once. The other is the moveend event that runs continuously.

For the zoom level, extracting it after the map has fully finished moving gives an accurate value. If you extract the zoom level at an event like movestart, which fires right as the map starts moving, you may get an intermediate value while moving from the current level to the next level. For example, if you register the event when the map starts moving, when zooming from 14 to 15, you might get an intermediate value like 14.2235.

The reason postrender is also registered is for the need to set an initial value. If only the moveend event were registered, the zoom level couldn't be displayed until the map is moved.



Displays the current area. Simply displayed as input[type=text].

Since the current area changes when the map moves, it's best to register the event most appropriate for this action.

TSX

<input name='minX' readOnly />
<input name='minY' readOnly />
<input name='maxX' readOnly />
<input name='maxY' readOnly />

For the current area, since the value changes every time the map moves, using state management is inappropriate.

This is because the state value changes wildly with the slightest movement of the map. If you use state management, you'll see stuttering every time you move the map.

TYPESCRIPT

/**
 * Method to set the area
 *
 * @param {number[]} pos: area
 */
function setBoundary(pos: number[])
{
	const tag1 = boundary.querySelector('input[name=minX]');
	const tag2 = boundary.querySelector('input[name=minY]');
	const tag3 = boundary.querySelector('input[name=maxX]');
	const tag4 = boundary.querySelector('input[name=maxY]');

	// If the tags are valid
	if (tag1 && tag2 && tag3 && tag4)
	{
		const [ minX, minY, maxX, maxY ] = pos;

		tag1.value = minX.toString();
		tag2.value = minY.toString();
		tag3.value = maxX.toString();
		tag4.value = maxY.toString();
	}
}

Create a method to display the current area. It takes pos as a parameter and assigns it to each input.

TYPESCRIPT

map.once('postrender', () =>
{
	const [ minX, minY, maxX, maxY ] = map.getView().calculateExtent();

	setBoundary([ minX, minY, maxX, maxY ]);
});

map.on('postrender', () => setBoundary(map.getView().calculateExtent()));
  • on(): A method that registers an event that keeps running continuously
  • once(): A method that registers an event to run exactly once
    • postrender: Runs after the map is rendered
  • getView(): A method that returns the map's View object
  • calculateExtent(): A method that returns the map's current extent coordinates

Two events are registered. Both are of the postrender type. once() is used once to display the initial area, and on() is used thereafter, so that the area is calculated and displayed every time the map moves.

For the same reason as with the zoom level, it's most accurate to calculate the area after the map's rendering has completed.



Displays the mouse cursor position. Simply displayed as input[type=text].

Since the mouse cursor position changes when the mouse moves, it's best to register the event most appropriate for this action.

TSX

<input name='x' readOnly />
<input name='y' readOnly />

The mouse cursor position is also unsuitable for state management.

TYPESCRIPT

/**
 * Method to set the mouse position
 *
 * @param {number[]} pos: mouse position
 */
function setBoundary(pos: number[])
{
	const tag1 = boundary.querySelector('input[name=x]');
	const tag2 = boundary.querySelector('input[name=y]');

	// If the tags are valid
	if (tag1 && tag2)
	{
		const [ x, y ] = pos;

		tag1.value = x.toString();
		tag2.value = y.toString();
	}
}

Create a method to display the mouse cursor position. It takes pos as a parameter and assigns it to each input.

TYPESCRIPT

map.once('postrender', () =>
{
	const [ minX, minY, maxX, maxY ] = map.getView().calculateExtent();
	const [ x, y ] = [ (minX + maxX) / 2, (minY + maxY) / 2 ];

	setPosition([ x, y ]);
});

map.on('pointermove', (e) => setPosition(e.coordinate));
  • on(): A method that registers an event that keeps running continuously
  • once(): A method that registers an event to run exactly once
    • postrender: Runs after the map is rendered
    • pointermove: Runs on mouse movement
  • getView(): A method that returns the map's View object
  • calculateExtent(): A method that returns the map's current extent coordinates
  • e.coordinate: The coordinate object of the mouse event object

Since this isn't a position tied to the map but rather the mouse's position, unlike other elements, a mouse-related event must be registered.

With the pointermove event, when the mouse moves, you can display this using the mouse position from the event object.

In the postrender event, the center of the map is displayed for the initial value. The center of the map can be obtained by appropriately calculating the current area, or by using map.getView().getCenter().


By gathering the elements mentioned above into a single component, it can be reused across multiple maps. In this project, a component called MapBoard is implemented to display the map's information.




You can check an example implementing this at OpenLayers6 Sandbox - MapInfo.

From the panel in the bottom-right, you can view map-related information and manage layers.

# GIS# OpenLayers
ship
blog.itcode.dev

Notes from the π-th Alpaca

7.0.1
Developed by RWB since 2021.057th upgraded at 2026.08