DEV Community

Cover image for Building a 3D Map Application Using Mapterhorn Terrain Data
Yasunori Kirimoto for MIERUNE

Posted on

Building a 3D Map Application Using Mapterhorn Terrain Data

About Mapterhorn

img

Have you heard of the geospatial project Mapterhorn?

Mapterhorn is an open data project that publishes terrain data. It creates terrain tiles from various open data sources, such as ESA’s Copernicus DEM and Switzerland’s swissALTI3D, and distributes them in the PMTiles format. The project is led by Oliver (formerly at MapLibre).

I also introduced Mapterhorn as a project to watch in my presentations at FOSS4G Hokkaido 2025 and FOSS4G Japan 2025.

Geospatialの世界最前線を探る [2025年版] - Speaker Deck

FOSS4G 2025 Japan 発表資料 https://www.osgeo.jp/foss4g-2025-japan/

favicon speakerdeck.com

Dataset Overview

Mapterhorn creates terrain tiles by combining multiple open data sources.

Global Data

Data Source Resolution Zoom Level Notes
Copernicus GLO-30 30m z0-z12 ESA Global DEM

The global data is based on ESA’s Copernicus GLO-30 model, covering the entire world up to z12.

High-Resolution Data

img

In addition to global data, Mapterhorn also provides high-resolution data primarily using open DEM/LiDAR from European countries. For Switzerland specifically, swisstopo's swissALTI3D is used, which offers terrain data at a resolution of 0.5m.

Japan Data

Update (Dec 2025)): High-resolution data for Japan has been added.
✅ Japan, country-wide, 1 m, 5 m, 10 m

LinkedIn

Add sources jp*: Japan, 1m, 5m, and 10m

Advance Preparation

Execution environment

  • node v24.4.1
  • npm v11.4.2

MapLibre GL JS Starter

Fork or download the MapLibre GL JS starter to your local environment and run it.

https://github.com/mug-jp/maplibregljs-starter

maplibregljs-starter
├── dist
│   └── index.html
├── img
├── src
│   ├── main.ts
│   ├── style.css
│   └── vite-env.d.ts
├── README.md
├── LICENSE
├── index.html
├── package-lock.json
├── package.json
├── tsconfig.json
└── vite.config.ts
Enter fullscreen mode Exit fullscreen mode

Install the package

npm install
Enter fullscreen mode Exit fullscreen mode

Install PMTiles as well

npm install pmtiles
Enter fullscreen mode Exit fullscreen mode

package.json

{
  "name": "maplibregljs-starter",
  "version": "4.5.0",
  "description": "",
  "scripts": {
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
  },
  "keywords": [],
  "author": "MapLibre User Group Japan",
  "license": "ISC",
  "devDependencies": {
    "typescript": "^5.5.2",
    "vite": "^5.3.2"
  },
  "dependencies": {
    "maplibre-gl": "^4.5.0",
    "pmtiles": "^4.3.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Creating the Map Application

Display the Mapterhorn terrain data. Modify src/main.ts.

import './style.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';

const protocol = new Protocol({ metadata: true });

maplibregl.addProtocol('mapterhorn', async (params, abortController) => {
    const [z, x, y] = params.url.replace('mapterhorn://', '').split('/').map(Number);
    const name = z <= 12 ? 'planet' : `6-${x >> (z - 6)}-${y >> (z - 6)}`;
    const url = `pmtiles://https://download.mapterhorn.com/${name}.pmtiles/${z}/${x}/${y}.webp`;
    const response = await protocol.tile({ ...params, url }, abortController);
    if (response['data'] === null) throw new Error(`Tile z=${z} x=${x} y=${y} not found.`);
    return response;
});

const map = new maplibregl.Map({
    container: 'map',
    hash: 'map',
    style: {
        version: 8,
        sources: {
            hillshadeSource: {
                type: 'raster-dem',
                tiles: ['mapterhorn://{z}/{x}/{y}'],
                encoding: 'terrarium',
                tileSize: 512,
                attribution: '<a href="https://mapterhorn.com/attribution">© Mapterhorn</a>'
            }
        },
        layers: [
            {
                id: 'hillshade',
                type: 'hillshade',
                source: 'hillshadeSource'
            }
        ]
    },
    center: [138.7782, 35.3019],
    zoom: 10
});

map.addControl(
    new maplibregl.NavigationControl({
        visualizePitch: true
    })
);
Enter fullscreen mode Exit fullscreen mode

Start the local server

npm run dev
Enter fullscreen mode Exit fullscreen mode

img

Finally, add 3D terrain rendering. Using MapLibre GL JS’s terrain feature enables 3D terrain visualization.

import './style.css';
import 'maplibre-gl/dist/maplibre-gl.css';
import maplibregl from 'maplibre-gl';
import { Protocol } from 'pmtiles';

const protocol = new Protocol({ metadata: true });

maplibregl.addProtocol('mapterhorn', async (params, abortController) => {
    const [z, x, y] = params.url.replace('mapterhorn://', '').split('/').map(Number);
    const name = z <= 12 ? 'planet' : `6-${x >> (z - 6)}-${y >> (z - 6)}`;
    const url = `pmtiles://https://download.mapterhorn.com/${name}.pmtiles/${z}/${x}/${y}.webp`;
    const response = await protocol.tile({ ...params, url }, abortController);
    if (response['data'] === null) throw new Error(`Tile z=${z} x=${x} y=${y} not found.`);
    return response;
});

const map = new maplibregl.Map({
    container: 'map',
    hash: 'map',
    style: {
        version: 8,
        sources: {
            MIERUNEMAP: {
                type: 'raster',
                tiles: ['https://tile.mierune.co.jp/mierune/{z}/{x}/{y}.png'],
                tileSize: 256,
                attribution:
                    "Maptiles by <a href='http://mierune.co.jp/' target='_blank'>MIERUNE</a>, under CC BY. Data by <a href='http://osm.org/copyright' target='_blank'>OpenStreetMap</a> contributors, under ODbL.",
            },
            terrainSource: {
                type: 'raster-dem',
                tiles: ['mapterhorn://{z}/{x}/{y}'],
                encoding: 'terrarium',
                tileSize: 512,
                attribution: '<a href="https://mapterhorn.com/attribution">© Mapterhorn</a>'
            }
        },
        layers: [
            {
                id: 'MIERUNEMAP',
                type: 'raster',
                source: 'MIERUNEMAP'
            },
            {
                id: 'hillshade',
                type: 'hillshade',
                source: 'terrainSource'
            }
        ],
        terrain: {
            source: 'terrainSource',
            exaggeration: 1.5
        }
    },
    center: [138.8016, 35.2395],
    zoom: 11,
    pitch: 60,
    bearing: -20
});

map.addControl(
    new maplibregl.NavigationControl({
        visualizePitch: true
    })
);
Enter fullscreen mode Exit fullscreen mode

img

Top comments (0)