Add Icons to embedded OpenStreetMaps

The local art gallery was searching for a method to present open-air-art on a website. A word and a blow… Using OpenStreetMap it’s possible and beautiful!

Here is an example of the logo of CODE AHOI projected into Rostock’s city harbour:

This is a Placeholder

To display the map you need to download tiles from OpenStreetMap. Their privacy policy is available here: wiki.osmfoundation.org/wiki/Privacy_Policy

That’s cool, isn’t it? :)

Do it Yourself!

Using the OpenLayers project it is comparatively easy. First we need some HTML skeleton:

  • we need to include OpenLayers as a dependency,
  • we need an HTML container to display the map,
  • we need a bit extra HTML to realise a pop-up.

Here is an example template:

<body>

  <div id="map" style="width:100%; height: 50vh">
    <!-- hier wird die Karte angezeigt -->
  </div>
  
  <div id="popup" class="ol-popup">
    <a href="#" id="popup-closer" class="ol-popup-closer">
      <!-- kleines x um das Pop-Up zu schließen -->
    </a>
    <div id="popup-content">
      <!-- Inhalte im Pop-Up -->
    </div>
  </div>

  <!-- Bibliotheken des OpenLayers' Projekt herunterladen -->
  <link rel="stylesheet" href="/path/to/openlayers/ol.css">
  <script src="/path/to/openlayers/ol.js"></script>
  
</body>

One Icon-Feature per Icon

Now the magic begins. For every icon, that we want to display in our embedded map, we need to create an Icon Feature. For the CODE AHOI map above it looks like the following:

// create a feature, that will be displayed as icon
const iconFeature = new ol.Feature({
  geometry: new ol.geom.Point(ol.proj.fromLonLat([12.1110,54.0966])),
  info: {
    headline: "OpenStreetMap AHOI",
    body: "CODE AHOI's paper boat is swimming in Rostock's city habour. [...]"
  },
});

// style the feature: here we're setting an image as icon
iconFeature.setStyle(new ol.style.Style({
  image: new ol.style.Icon({
    src: "https://codeahoi.de/s/articles/2020/openlayers-showcase.png"
  })
}));

// combine everything into a new layer
var vectorLayer = new ol.layer.Vector({
  source: new ol.source.Vector ({
    features: [iconFeature]
  })
});

The first block sets some properties for the icon: Where should it be located on the map? Which headline and what content should be displayed in the pop-up? The second block defines what the icon should look like. Here we load an image from CODE AHOI’s webserver, which will then be shown on the map.. The third block creates a new vector layer, which will be shown on top of the map tiles – that’s the actual speciality of OpenLayers :-)
In this case it may seem very difficult for a single icon, but for many icons it can easily be automatised.

Load the Map

Now we can load the map. The bottom layer should contain the tiles of OpenStreetMap. On top we want to draw our icon. We want to zoom to level 15 and centre the map in Rostock’s city harbour. Coordinates and zoom level can be obtained from openstreetmap.org. If you choose the same coordinates for the icon and the map, the icon will be centred in the map ;-)

var map = new ol.Map({
  // display the map in the container with the id 'map'
  target: 'map',
  
  // the "open" layers ;-)
  layers: [
    new ol.layer.Tile({
      // use tiles from OpenStreetMap for the bottom layer
      source: new ol.source.OSM()
    }),
    // add out vector layer on top
    vectorLayer
  ],
  
  view: new ol.View({
    // initially center the map at [12.1110,54.0966] (Rostock's city harbour)
    center: ol.proj.fromLonLat([12.1110,54.0966]),
    // zoome to level 15
    zoom: 15
  })
});

Now the map get properly loaded and the icon is displayed at the correct location.

Implement a Pop-Up

Almost done, but we want to display a pop-up if the user clicks the CODE AHOI icon. That requires a bit more code:

var container = document.getElementById('popup');
var content = document.getElementById('popup-content');
var closer = document.getElementById('popup-closer');


// the pop-up will become an extra layer
var popup = new ol.Overlay({
  element: container,
  autoPan: true,
  autoPanAnimation: {
    duration: 250
  },
  positioning: 'bottom-center',
  stopEvent: false,
  offset: [0, -25]
});
map.addOverlay(popup);


// event-listener for clicking the x
closer.onclick = function() {
  popup.setPosition(undefined);
  return false;
};


// event-listener for clicking the map: did the user click our icon -> display the pop-up
map.on('click', function(evt) {
  // was our icon clicked?
  var feature = map.forEachFeatureAtPixel(evt.pixel, function(feature) { return feature; });
  // if so, put the information that we stored along the icon into our pup-up
  if (feature) {
    // where should we display the pop-up?
    var coordinates = feature.getGeometry().getCoordinates();
    // what's the textual content in the pop-up?
    content.innerHTML = '<h2>' + feature["values_"]["info"]["headline"] + '</h2><p>' + feature["values_"]["info"]["body"] + "</p>";
    
    // display the pop-up at the proper location
    popup.setPosition(coordinates);
  } else {
    // the user clicked something else in the map -> close the popupo
    popup.setPosition(undefined);
  }
});

We can realise the pop-up as an extra layer (see second code block) in the map – this way the pop-up is less intrusive and comes with better look and feel. The necessary information for the pop-up are already stored along the icon feature. Here we can read and use the properties, if the user clicks the icon (see last code block).

Styling

The logic is done. However, for the make-up we need some proper finishing ;-)
The style depends a lot on the layout of the whole page and some subjective criteria, but in general you would need to add a closing-symbol to the popup-closer container and style the pop-up-bubble a bit. Here is an example:

/* closing-x */
.ol-popup-closer {
  text-decoration: none;
  position: absolute;
  top: 2px;
  right: 8px;
}
.ol-popup-closer:after {
  content: "✖";
}

/* pop-up-bubble with a little triangle pointing to the icon */
.ol-popup {
  position: absolute;
  background-color: white;
  box-shadow: 0 1px 4px rgba(0,0,0,0.2);
  padding: 15px;
  border-radius: 10px;
  border: 1px solid #cccccc;
  bottom: 12px;
  left: -50px;
  min-width: 280px;
  display: none;
}
.ol-popup:after, .ol-popup:before {
  top: 100%;
  border: solid transparent;
  content: " ";
  height: 0;
  width: 0;
  position: absolute;
  pointer-events: none;
}
.ol-popup:after {
  border-top-color: white;
  border-width: 10px;
  left: 48px;
  margin-left: -10px;
}
.ol-popup:before {
  border-top-color: #cccccc;
  border-width: 11px;
  left: 48px;
  margin-left: -11px;
}

Full Solution

My code for the example above is un-obfuscated available for download

The HTML skeleton of the whole article is of course a bit more complex, but that you can find out on your own!

Let's Go!

Lights off & spot on: CODE AHOI goes live! After some preparations I set sail and give it a try :)

I have been learning and working for years at universities. The urge to advance increased, but I am not sure where to go…
I am professionally trained, I love working interdisciplinary, and I quickly become acquainted with new domains. Furthermore, I am an expert in gathering and integrating data from heterogeneous sources, exploring complex materials to find patterns, and communicating results. I would like to apply earned skills and help organisations and the local economy.
Thus, CODE AHOI is my freelancing-attempt to obtain broad experience in many domains, to eventually learn where to continue my journey.

A first walking advertisement is this website. I decided for a static page with Jekyll: the web server does nothing but serving pre-compiled pages. Such a website is much more performant and eco-friendly, it typically tracks less personal information, and, as no scripts are executed, it can be considered much more secure. In addition, my whole infrastructure is powered by green energy!

The initial setup of such a site may be a bit more complicated, but with a little technical sense you can even make it multilingual (note the flag top right)! :)

I also decided against any kind of tracking: read or skip and click whatever you want — I do not care. If you want my attention just leave a message through the contact form at the bottom of that page.

I am highly motivated and very excited!