When a map has to do more than show a few markers (projections, WMS layers, editing shapes, real GIS work), OpenLayers is usually the library people reach for. In this post we'll plug TMaps raster tiles into it, sidestep the classic projection trap, and add a small feature users really like: a URL that remembers where you are on the map.
Live demo: examples.tmaps.tn/02-raster-openlayers
Or play with it right here, and fork it on CodePen to change the code:
Before you start
You'll need a TMaps API key from the TMaps console, with your domain (and localhost for development) in its Authorized Domains. Wildcards don't work there, so list each subdomain. The first post of this series walks through this step if you haven't done it yet.
1. The tile layer
TMaps raster tiles follow the XYZ scheme, so the source is a plain ol.source.XYZ:
const source = new ol.source.XYZ({
url: 'https://tiles.tmaps.tn/r-tile/{z}/{x}/{y}?api_key=YOUR_API_KEY',
maxZoom: 18,
attributions: '© <a href="https://www.tmaps.tn">TMaps</a>',
});
Each tile is a 256 × 256 PNG, and zoom goes from 0 to 18.
2. Get the projection right
This is the one that sends markers into the sea. By default OpenLayers works in Web Mercator (EPSG:3857), while GPS coordinates are longitude and latitude in EPSG:4326. Convert them with ol.proj.fromLonLat, and watch the order, because longitude comes first:
const map = new ol.Map({
target: 'map',
layers: [new ol.layer.Tile({ source })],
view: new ol.View({
center: ol.proj.fromLonLat([10.637, 35.828]), // Sousse, as [lng, lat]
zoom: 14,
maxZoom: 18,
}),
});
3. Keep the view in the URL
Sooner or later someone wants to send "this exact spot" to a colleague. The simplest way is to keep the view in the URL hash as #zoom,lat,lng: update it whenever the map moves, and read it back when the page loads.
Reading it on load, with Tunis as the fallback:
const [zoom = 12, lat = 36.8002, lng = 10.1815] = location.hash
.slice(1)
.split(',')
.filter(Boolean)
.map(Number);
Writing it after every move. history.replaceState changes the address bar without adding one history entry per drag, so the back button still behaves:
map.on('moveend', () => {
const view = map.getView();
const [lon, la] = ol.proj.toLonLat(view.getCenter());
history.replaceState(null, '', `#${view.getZoom().toFixed(2)},${la.toFixed(6)},${lon.toFixed(6)}`);
});
With that in place, a link like this one opens straight on Djerba:
https://examples.tmaps.tn/02-raster-openlayers/#11,33.80,10.88
4. The complete example
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Shareable map of Tunisia</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/ol@10.4.0/ol.css">
<script src="https://cdn.jsdelivr.net/npm/ol@10.4.0/dist/ol.js"></script>
<style>
html, body, #map { height: 100%; margin: 0; }
</style>
</head>
<body>
<div id="map"></div>
<script>
const API_KEY = 'YOUR_API_KEY';
// Initial view from the URL: #zoom,lat,lng
const [zoom = 12, lat = 36.8002, lng = 10.1815] = location.hash
.slice(1).split(',').filter(Boolean).map(Number);
const source = new ol.source.XYZ({
url: `https://tiles.tmaps.tn/r-tile/{z}/{x}/{y}?api_key=${API_KEY}`,
maxZoom: 18,
attributions: '© <a href="https://www.tmaps.tn">TMaps</a>',
});
source.once('tileloaderror', () =>
console.error('TMaps tiles could not be loaded: check your API key and its authorized domains.'));
const map = new ol.Map({
target: 'map',
layers: [new ol.layer.Tile({ source })],
view: new ol.View({ center: ol.proj.fromLonLat([lng, lat]), zoom, maxZoom: 18 }),
});
// Share the current view through the URL
map.on('moveend', () => {
const view = map.getView();
const [lon, la] = ol.proj.toLonLat(view.getCenter());
history.replaceState(null, '', `#${view.getZoom().toFixed(2)},${la.toFixed(6)},${lon.toFixed(6)}`);
});
</script>
</body>
</html>
A few tips
If the map stays empty, open the network tab. A 401 means the key is missing or revoked. A 403 means the page's domain isn't in the key's authorized domains, or the request went out over plain HTTP.
Tiles are sent with long cache headers, so coming back to the map is fast.
Using a bundler? The same code works with import { Map, View } from 'ol', TileLayer from ol/layer/Tile, XYZ from ol/source/XYZ and fromLonLat from ol/proj.
Going further
- TMaps, a Google Maps alternative for Tunisia: www.tmaps.tn
- Live demos: examples.tmaps.tn
- Source code, MIT licensed: github.com/tmaps-tn/tmaps-snippets
- Postman collection: postman.com/tmaps-tn/tmaps-api
- Raster tiles documentation: tmaps.tn/documentation
Next up in the series: vector tiles with MapLibre GL, with four styles, dark mode and a 3D tilt.



Top comments (0)