Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

OpenLayers XYZ Tiles with TMaps: Shareable Map of Tunisia

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

OpenLayers XYZ Tiles with TMaps: Shareable Map of Tunisia

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:

OpenLayers demo page showing Sousse with the current view in the URL

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,
  }),
});

Sousse rendered with OpenLayers and TMaps raster tiles

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

Djerba opened from a shared URL

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/[email protected]/ol.css">
  <script src="https://cdn.jsdelivr.net/npm/[email protected]/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

Next up in the series: vector tiles with MapLibre GL, with four styles, dark mode and a 3D tilt.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.