How to make an interactive map on OpenLayers. Part 1

Anton Efremenkov, Senior web developer ITentika and HolyJS speaker, shares his experience of creating an interactive map based on the OpenLayers library.

At the end of last year, our company was approached by a customer who needed to develop a system for managing the city’s transport network (buses, trolleybuses, trams, more exotic transport – all together). This is quite an enterprise project, with a development team of more than 30 people, within the framework of which a really large number of tasks had to be implemented. Of course, I won’t list them all now (it’s too long and dull). I will focus on one group of tasks – the one related to displaying a city map and different modes of working with it.

What we needed to implement:

  • display a map of the city (I can’t name the city itself – NDA).
  • add vehicle stops to the map (and display stops at specified coordinates)
  • display the vehicle in real time
  • plot on the map the route the vehicle will take
  • draw geometric shapes on the map and look for objects that fall inside the shape
  • highlight an object on the map and see related information

Description of the problem

At that time, we had more than good expertise related to using the Leaflet library to draw various maps and interact with them. And everything would be fine, but…

Our customer is one of the state. companies, and it is quite important for them which third-party software products we use. It so happens that the leaflet NPM package can be easily installed now (you take it and install it, there are no problems), but if you want to read the documentation for this library, you will see that this site is not available to residents of our country.

The devil is in the details: interactive map on OpenLayers.  Part 1. 1

During our research, we selected only 2 candidates – Mapbox and OpenLayers.

The first candidate has great functionality, but, unfortunately, it’s paid – and for some reason our customer really didn’t like it. The second candidate was, sort of, a dark horse – no one in the company had experience using this library, and besides, the statistics on the use of this library raise one big question: “what’s wrong with them?..”

Mapbox и OpenLayers

We studied the examples posted on the OpenLayers website, and they seemed quite convincing to us. In general, we believed in the capabilities of this library and that it really can work with the card, so we decided to make a PoC with the main features using OpenLayers.

PoC

A few words about the stack that we chose. On the backend we had .NET and PostgreSQL along with PostGIS and pgRouting (for storing information about objects on the map). The city map was provided to us by MapServer, the map itself was imported from OpenStreetMap. Well, for the browser part we chose Angular and the OpenLayers library (unfortunately, there is no Angular wrapper for it).

1. Creating and setting up a map

If we compare OpenLayers with Leaflet, I can immediately say that when working with OpenLayers, you have to write a little more code, but not criminally.

As you can see in the code below, we can set the starting coordinate for centering the map and the starting zoom level. We can also create any number of available controls and transfer them to the map being created (for example, we created a scale with a scale for all maps using just one line of code).

To display a city map as a kind of “background” we use TileLayer, where we enter the url of our MapServer.

We can also use the default interactivity elements (for example, moving around the map and zooming), supplementing them with some others, if necessary.

const view = new View({
center: options.mapCenter,
zoom: options.initialZoomLevel,
});

useGeographic();
const scaleControl = new ScaleLine({ units: ‘metric’ });

const tileLayer = new TileLayer({
source: new TileWMS({
url: environment.mapServerUrl,
params: {LAYERS: ‘default’, FORMAT: ‘image/png’}
}),
});

return new Map({
interactions: defaultInteractions()
.extend(options.interactions || ()),
view: view,
layers: ((tileLayer) as Array)
.concat(options.vectorLayers || ()),
controls: (scaleControl),
target: ‘ol-map’,
});

2. Working with vector layers

We need to be able to display different types of objects on the map, and for this we decided to use vector layers displayed on top of our background map.

For each type of object, we use our own vector layer so that we can quite easily hide/display all objects of the type we need. If necessary, we can specify a generic type in order to indicate as clearly as possible what exactly the layer is intended for.

this.labelVectorSource =
new VectorSource({ wrapX: false });
const labelLayer =
new VectorLayer>({
source: this.labelVectorSource,
style: this.labelStyle,
});

this.roadVectorSource =
new VectorSource({ wrapX: false });
const roadLayer =
new VectorLayer>({
source: this.roadVectorSource,
});

this.drawVectorSource = new VectorSource({ wrapX: false });
const drawLayer = new VectorLayer({
source: this.drawVectorSource,
});

3. Styling markers

A marker is a point on the map that looks like a geometric figure or a specific icon. In OpenLayers we have two basic options for styling these markers. To create markers of the first type, we use Style with such primitives as Fill and Stroke, and to display an icon we use Style with an Icon object.

new Style({
image: new Icon({
height: isBig ? 32 : 20,
src: `/assets/markers_stop.svg`,
}),
zIndex: 5,
});

new Style({
fill: new Fill({
color: (64, 169, 255, 0.35),
}),

stroke: new Stroke({
color: ‘green’,
width: 2,
lineDash: (8, 12),
}),
});

4. Drawing shapes

Knowing the coordinates, we can create a shape object and place it on the desired vector layer. But there is a more interesting option: using Draw – an object from the interactions family – we can switch the map to the mode of drawing geometric shapes, specifying the type of shape we need. In our arsenal there are both geometric primitives and more complex figures (point, straight line, broken line, circle, …). We can also draw regular polygons by passing the appropriate geometric function.

this.draw = new Draw({
source: this.drawVectorSource,
type: ‘LineString’,
});

this.map.addInteraction(this.draw);

this.draw = new Draw({
source: this.drawVectorSource,
type: ‘Polygon’,
geometryFunction: createRegularPolygon(6),
});

5. Changing the shapes

Separately, I would like to dwell on one interactivity of OpenLayers, which really pleasantly surprised me. According to the technical specifications, we must be able to not only draw geometric shapes, but also change them (and save new coordinates, of course). The Leaflet library does not have such functionality out of the box; for this you have to either use additional plugins or write your own implementation. In OpenLayers, this behavior is configured in literally three lines of code – using the Modify object.

this.modifyShapeInteraction = new Modify({
source: this.drawVectorSource,
pixelTolerance: 10,
});

return new Map({
interactions:
defaultInteractions()
.extend((this.modifyShapeInteraction)),
view: view,
layers: ((…)),
controls: (scaleControl),
target: ‘ol-map’,
});

After some time, our PoC was ready, and the result more than convinced us that OpenLayers was suitable for our purposes, and we could achieve all our goals.

Let me make a reservation in advance that both the designer and most of the developers of our team are from St. Petersburg, so our layouts and test coordinates are presented on the map of this particular glorious city =)

Well, we decided to move on to the next stage. And, of course, we encountered the first problems.

Label problems

According to the specification, we should be able to display vehicle markers, and they should look like the one shown in the picture on the left. But after the first iteration we got a completely different picture. The one on the right.

OpenLayers map

We were unable to round the frame, apply a shadow, or set normal indents… Why? Because OpenLayers draws all objects on canvas, and there our capabilities are very, very limited, since most CSS properties are simply missing.

What can we ultimately ask? The list is short:

  • Fill and Stroke (primitives for filling with color and displaying a frame)
  • Image (directly, the marker icon on the map)
  • Text (marker signature, which, among other things, can be used to add your own Fill and Stroke)

From the very beginning, we planned to build our markers around the Text object. Well, it didn’t work out =)

The design of this part of the system had already been agreed upon with the customer, so we had to look for a solution to the problem, without any hopes of getting by with a “light” version of the markers.

OpenLayers has the ability to “assemble” the visual design of a point on the map from several parts – simply by specifying an array of styles applied to this point. This is what we got hooked on. Now we have several styles, and together they form a vehicle marker:

const textStyle = new Style({
text: new Text({
text: `${coordinates.boardNumber}`,
textAlign: ‘left’,
fill: new Fill({ color: ‘#40A9FF’ }),
font: labelsFont,
padding: (6, 20, 0, 24),
offsetX: 24,
offsetY: 1,
}),
zIndex: coordinates.vehicleId,
});
const vehicleMarkerStyle = new Style({
image: new Icon({
height: 46,
width: 46,
src: ‘/assets/marker.svg’,
anchor: (0.5, 0.5),
rotation: coordinates.direction * Math.PI / 180,
}),
zIndex: coordinates.vehicleId,
});
const backWidth = textWidth + shadowGap + radiusGap + leftPadding;
const backStyle = new Style({
image: new Icon({
height: 56,
width: backWidth,
src: ‘/assets/vehicle-label-back.svg’,
anchor: (0.1, 0.35),
}),
zIndex: coordinates.vehicleId,
});

Of course, I can’t say that this is the most elegant solution, and based on the way we get the backWidth (the width of our matte, with background and shadow), the solution looks quite fragile. But it has two significant advantages:

  1. It works! (which is no longer unimportant)
  2. We understand the order of the layers, and we can manipulate it if necessary.

Also, the disadvantages include some brakes when there is a fairly large number of vehicles on the map. But more on that later…)

One way or another, we solved the problem and achieved the intended goal.

And now, dear reader, a question for you: what do you think could have gone wrong? Yes, yes, that’s right. We have had His Majesty Redesign. Yes, even at this stage of the project. Having gotten a taste for it, our customer wanted to make “a few” changes to the layout. As you can see in the picture below, the markers themselves are now multi-colored (depending on the type of vehicle), and related information is also displayed inside such a marker (about the status of the vehicle, for example):

OpenLayers map

This resulted in an even more complex and fragile way to collect all the styles. In the picture below you can see that we have added logic, and the number of layers has increased::

const shadowGap = 28;
const radiusGap = 8;
const leftPadding = 24;
const bnsoGap = coords.hasEquipFault ? 22 : 0;
const emergencyGap =
coords.vehicleStatus === VehicleStatus.Emergency ? 20 : 0;
const extraGap = bnsoGap && emergencyGap ? 6 : 0;

const backStyle = new Style({
image: new Icon({
height: 56,
width: textWidth + shadowGap + radiusGap + leftPadding
+ bnsoGap + emergencyGap + extraGap,
src: ‘/assets/vehicle-label-back.svg’,
anchor: (0.1, 0.35),
}),
zIndex: coords.vehicleId,
});

const styles = (backStyle, textStyle, vehicleMarkerStyle,
vehicleIconStyle, bnsoStyle, emergencyStyle, statusStyle);

However, the problem was solved, and we reached a very important stage in the life of our project: we were finally convinced that the chosen library was suitable for us, and all the assigned tasks could be implemented with its help. Well, the first results can be seen in this video:

We showed this demo to the customer and it was truly a success! In high spirits, we set about implementing the next module, and… of course, we encountered new difficulties caused by the use of OpenLayers.

But more on this in the second part.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *