Geotagging is easy. OpenLayers
12 min read January 14, 2025 #openlayers #javascript #mapsI've always found it tedious and difficult to display stuff on maps. To understand the API, you need examples that aren't always accessible because they require a token to display and interact with the map. The token has to be obtained and hidden from other people's eyes.
But then I thought, why do I need proprietary Google Maps when there is OpenStreetMaps? I searched for APIs, came across OpenLayers, opened examples and was surprised by their number and simplicity.

In this series of articles, we will explore OpenLayers, display photo thumbnails on a map, add clustering, and finally create a Rust application that collects geodata and thumbnails from photos and generates a map.
Layer 1. OpenStreetMap#
Let's start with the first layer, the map. We'll use OpenStreetMap since it doesn't require any tokens.
Let's add the OpenLayers library and its styles to HTML:
<html>
<head>
<link rel"stylesheet" href"https://cdn.jsdelivr.net/npm/[email protected]/ol.css">
<style>html body .map {
padding: 0;
margin: 0;
width: 100%;
height: 100%;
}</style>
</head>
<body>
<div id"map" class"map"></div>
<script src"https://cdn.jsdelivr.net/npm/[email protected]/dist/ol.js"></script>
<script src"map.js"></script>
</body>
</html>
Then, create an OpenStreetMap layer and a map object in JS:
const layerOSM = new ol.layer.Tile
new ol.source.OSM
const map = new ol.Map
'map'
layerOSM
new ol.View
ol.proj.transform20 47 'EPSG:4326' 'EPSG:3857'
4
In the map parameters, in target we need to specify the id of the HTML element, list the layers in layers, and customize the view.
The code is very simple, except for this line:
center: ol.proj.transform20 47 'EPSG:4326', 'EPSG:3857'
Here I'm centering the map on Europe, namely 47Β° North latitude and 20Β° East longitude from EPSG:4326 projection to the web map standard - EPSG:385 projection. Every time you work with the GPS coordinate system, you'll have to do this conversion.
That's enough to display the map:
Layer 2. Photo thumbnails#
Let's add a container for the marker objects. This will be an ol.source.Vector:
const photosSource = new ol.source.Vector
Let's create a layer with thumbnails:
const layerThumbs = new ol.layer.Vector
photosSource
thumbStyle
// -- icon styles --
const cache;
function photoStyle(feature, scale) {
const url = feature.get'url'
const key = `scaleurl`;
if!cachekey
cachekey = new ol.style.Style
image: new ol.style.Icon url, scale
}
return cachekey
}
function thumbStyle(feature, resolution) {
returnphotoStylefeature, clamp0.2, 0.1 / resolution, 1
}
function clamp(min, value, max) {
ifvalue < min return min;
ifvalue > max return max;
return value;
}
This layer will take thumbnails from the photosSource vector and display them in the style defined in the thumbStyle function.
Styling is needed to specify what exactly we'll draw and with what size. In this case, we will draw icons with a certain URL taken from feature.
const url = feature.get'url'
// ..
cachekey = new ol.style.Style
image: new ol.style.Icon url, scale
What isfeature? ol.Feature is a geographic object displayed on the map (position, area, territory). It can be linked to a layer and contains attributes, styles, and geometry.
To display a photo, we need the following data:
- Marker coordinates.
- Image URL (optional, you can just draw circles).
- Details (optional) that will be displayed when the marker is selected.
We'll handle data collection in the next articles, but for now, let's look at an example:
[{
"preview": "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png",
"lat": 47.765957,
"lon": 37.255646
}]
This is a JSON array with one element. We'll load it directly from the code:
const handleMapDataLoaded = items
const transform = ol.proj.getTransform'EPSG:4326' 'EPSG:3857'
items.forEachitem
const feature = new ol.Featureitem
feature.set'url' item.preview
const coordinate = transformparseFloatitem.lon parseFloatitem.lat
feature.setGeometrynew ol.geom.Pointcoordinate
photosSource.addFeaturefeature
;
handleMapDataLoaded
"preview" "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png"
"lat" 47.765957
"lon" 37.255646
In handleMapDataLoaded we iterate through the loaded features, create an ol.Feature object, transform the coordinates into the map projection and set the geometry to a single point ol.geom.Point. Then add the object to photosSource.
All that's left is to add a layer to the map:
const map = new ol.Map
'map'
layerOSM layerThumbs
...
Demo
Details on click#
Small thumbnails aren't very convenient to look at, so let's collect a bit more data. For example, the camera model, original photo URL, and creation date:
handleMapDataLoaded
"name" "dsc_1337_ps_5996.jpg"
"path" "https://annimon.com/albums/files/dsc_1337_ps_5996.jpg"
"preview" "https://annimon.com/albums/screens/dsc_1337_ps_5996.jpg.250.png"
"lat" 47.765957
"lon" 37.255646
"make" "Sony Ericsson"
"model" "MK16i"
"date" "2017-06-02 19:30:08"
When clicking on a photo on the map, we'll show the details in a separate panel:
<div id"map" class"map"></div>
<div id"photo-details"></div>
<script type"text/html" id"photo-template">
<div class"photo-details-container">
<a href"{path}" target"_blank" title"Click to open photo in new tab">
<img src"{preview}" class"photo-thumbnail">
</a>
<div class"photo-details-content">
<h3>{name}</h3>
<p><b>Camera:</b> {make} {model}</p>
<p><b>Date:</b> {date}</p>
</div>
</div>
</script>
Using ol.interaction.Select we can add selection handlers for the map objects and define their styles:
const thumbSelector = new ol.interaction.Select
layerThumbs
selectedStyle
map.addInteractionthumbSelector
function selectedStyle(feature, resolution) {
returnphotoStylefeature, clamp0.4, 0.14 / resolution, 1.2
}
The selection will be configured for layerThumbs layer, and the scale of the selected photo will be slightly increased.
Now, let's add selection and deselection handlers:
const selectedFeatures = thumbSelector.getFeatures
selectedFeatures.on'add', event => {
const feature = event.target.item0
const details = photoDetailsfeature
document.getElementById'photo-details'.innerHTML = details;
selectedFeatures.on'remove', () => {
document.getElementById'photo-details'.innerHTML = '';
function photoDetails(feature) {
let content = document.getElementById'photo-template'.innerHTML;
const keys ='name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'
keys.forEachkey => {
const value = feature.getkey
content = content.replace{key}`, value
return content;
}
When a marker is selected, we insert HTML to #photo-details with the replaced parameters listed in the array const keys = ['name', 'preview', 'date', 'lat', 'lon', 'make', 'model', 'path'];.
Demo
As you can see, if there are a lot of photos, they overlap, so we'll add clustering in the next article.