Web-based 3D terrain cartography with Cartolina
Ondřej Procházka (Twitter/X: @ondrej1974)
A personal project:
to create a global web-based topographic-scale 3D terrain map
based on open-source software and public-domain data
I have released the code I wrote to create these demos as
An experimental software for web-based 3D terrain cartography
Why cartolina might not be for you
... but bear with me ...
... because it does have some compelling features:
cartolina is a narrowly-focused fork of vts-geospatial, originally by Melown Technologies / Leica Geosystems (2015-2023).
That was a massive system, involving 10+ components, over 20 supporting libraries and 70 software repositories.
Before cartolina could be released, I had to
The two components
cartolina-tileserver: a C++ Unix server daemon, manages data sources.
cartolina-js: a JavaScript / TypeScript library, renders data as interactive maps.
Hello, cartolina!
<div id="map"></div>
<script type="module">
import { map as createMap } from
'https://cdn.tspl.re/libs/cartolina/dist/current/cartolina.min.esm.js';
let map = createMap({
container: 'map',
style: '/style.json',
position: ["obj", 13.302, 47.038, "fix", 2479, -169, -90, 0, 14763, 30],
});
</script>
Hello, cartolina: style.json
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
}
}
Adding layers: lightened satellite imagery
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"eoxit-s2c": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/eoxit-sentinel2-cloudless-2018/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"layers": [
{ "source": "eoxit-s2c", "whitewash": 0.3 }
]
}
More interesting: natural colors
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"ne1v6plcw": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/natural-earth1-v6p-lcw/"
},
"esa-worldcover2021": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover2021-cr4/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"layers": [
{ "source": "ne1v6plcw" },
{ "source": "esa-worldcover2021" }
]
}
Adding a bump-map layer
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"ne1v6plcw": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/natural-earth1-v6p-lcw/"
},
"esa-worldcover2021": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover2021-cr4/"
},
"eoxit-s2c-normalmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/eoxit-s2c-2020-normalmap/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"layers": [
{
"type": "bump-map",
"source": "eoxit-s2c-normalmap",
"alpha": 0.2
},
{
"source": "ne1v6plcw",
"blendMode": "overlay",
"alpha": { "mode": "constant", "value": 1.0 }
},
{
"type": "diffuse-map",
"source": "esa-worldcover2021"
},
{
"type": "constant",
"source": [255,255,255],
"blendMode": "overlay",
"alpha": 0.15
}
]
}
Adding sun glints (specular reflection layers)
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"ne1v6plcw": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/natural-earth1-v6p-lcw/"
},
"esa-worldcover2021": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover2021-cr4/"
},
"eoxit-s2c-normalmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/eoxit-s2c-2020-normalmap/"
},
"ne1lc-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/ne1-lc-500m-specularmap/"
},
"esa-worldcover-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover-2021-specularmap/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"layers": [
{
"type": "bump-map",
"source": "eoxit-s2c-normalmap",
"alpha": 0.2,
"necessity": "optional"
},
{
"source": "ne1v6plcw",
"blendMode": "overlay",
"alpha": { "mode": "constant", "value": 1.0 }
},
{
"type": "diffuse-map",
"source": "esa-worldcover2021"
},
{
"type": "constant",
"source": [255,255,255],
"blendMode": "overlay",
"alpha": 0.15
},
{
"type": "specular-map",
"terrain": ["topoearth-copernicus-dem-glo30"],
"source": "ne1lc-specularmap"
},
{
"type": "specular-map",
"source": "esa-worldcover-specularmap"
}
]
}
Adding haze and shadows
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"ne1v6plcw": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/natural-earth1-v6p-lcw/"
},
"esa-worldcover2021": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover2021-cr4/"
},
"eoxit-s2c-normalmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/eoxit-s2c-2020-normalmap/"
},
"ne1lc-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/ne1-lc-500m-specularmap/"
},
"esa-worldcover-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover-2021-specularmap/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"atmosphere": {
"visibilityToEyeDistance": 3.0,
"edgeDistanceToEyeDistance": 1.0,
"maxVisibility": 1e6
},
"shadows": {},
"layers": [
{
"type": "bump-map",
"source": "eoxit-s2c-normalmap",
"alpha": 0.2,
"necessity": "optional"
},
{
"source": "ne1v6plcw",
"blendMode": "overlay",
"alpha": { "mode": "constant", "value": 1.0 }
},
{
"type": "diffuse-map",
"source": "esa-worldcover2021"
},
{
"type": "constant",
"source": [255,255,255],
"blendMode": "overlay",
"alpha": 0.15
},
{
"type": "specular-map",
"terrain": ["topoearth-copernicus-dem-glo30"],
"source": "ne1lc-specularmap",
"necessity": "essential"
},
{
"type": "specular-map",
"source": "esa-worldcover-specularmap",
"necessity": "essential"
}
]
}
Lettering
cartolina provides rudimentary support for lettering.
Point labels are solid with a well-defined visual hierarchy.
Line labels are weak.
Area labels have not been implemented.
Lettering with AST syntax
{
"version": 2,
"sources": {
"topoearth-copernicus-dem-glo30": {
"type": "cartolina-surface",
"url": "https://cdn.tspl.re/mapproxy/melown2015/surface/topoearth/copernicus-dem-glo30/"
},
"ne1v6plcw": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/natural-earth1-v6p-lcw/"
},
"esa-worldcover2021": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover2021-cr4/"
},
"eoxit-s2c-normalmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/eoxit-s2c-2020-normalmap/"
},
"ne1lc-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/ne1-lc-500m-specularmap/"
},
"esa-worldcover-specularmap": {
"type": "cartolina-tms",
"url": "https://cdn.tspl.re/mapproxy/melown2015/tms/topoearth/esa-worldcover-2021-specularmap/"
},
"osm-openfreemap": {
"type": "cartolina-freelayer",
"url": "https://cdn.tspl.re/mapproxy/melown2015/geodata/topoearth/osm-openfreemap/"
}
},
"terrain": {
"sources": ["topoearth-copernicus-dem-glo30"]
},
"illumination": {
"light": ["tracking", 315, 45]
},
"vertical-exaggeration" : {
"heightRamp": [[0,4000], [1.5,1.3]],
"viewExtentProgression": [12.3, 13e6, 1.38, 1, 13.5]
},
"atmosphere": {
"visibilityToEyeDistance": 3.0,
"edgeDistanceToEyeDistance": 1.0,
"maxVisibility": 1e6
},
"shadows": {},
"constants": {
"@osmid": {"if":[["has","$id_"],"$id_",""]},
"@name": {"if":[["has","$name"],{"if":[["any",["!has","$name:en"],["==",{"has-latin":"$name"},true]],"{$name}","{$name}\n{$name:en}"]},""]},
"@serif-font": ["noto-mix","noto-cjk"],
"@italic-font": ["noto-italic","noto-mix","noto-cjk"]
},
"fonts": {
"noto-italic": "https://cdn.tspl.re/fonts/noto-italic/1.0.0/noto-i.fnt",
"noto-mix": "https://cdn.tspl.re/fonts/noto-extended/1.0.0/noto.fnt",
"noto-cjk": "https://cdn.tspl.re/fonts/noto-cjk/1.0.0/noto.fnt",
"noto-serif": "https://cdn.tspl.re/fonts/noto-serif/1.0.0/noto-serif.fnt",
"#default": "https://cdn.tspl.re/libs/vtsjs/fonts/noto-extended/1.0.0/noto.fnt"
},
"layers": [
{
"type": "bump-map",
"source": "eoxit-s2c-normalmap",
"alpha": 0.2,
"necessity": "optional"
},
{ "source": "ne1v6plcw" },
{ "source": "esa-worldcover2021" },
{
"type": "constant",
"source": [255,255,255],
"blendMode": "overlay",
"alpha": 0.15
},
{
"type": "specular-map",
"terrain": ["topoearth-copernicus-dem-glo30"],
"source": "ne1lc-specularmap",
"necessity": "essential"
},
{
"type": "specular-map",
"source": "esa-worldcover-specularmap",
"necessity": "essential"
},
{
"id": "places",
"type": "labels",
"source": "osm-openfreemap",
"filter": ["all",["==","#group","place"],["in","$class","city","town","village","suburb","hamlet"]],
"&population": {"str2num":{"if":[["has","$population"],"$population",1]}},
"&mt-rank": {"str2num":{"if":[["has", "$rank"],"$rank",11]}},
"&importance": {"linear2":["&mt-rank",[[3,90],[11,10]]]},
"&rank": {"linear2":["&mt-rank",[[1,1],[11,6]]]},
"importance-source": "&importance",
"label": true,
"label-source": "@name",
"label-size": {"linear2":["&rank",[[0,25],[6,14]]]},
"label-font": "@italic-font",
"label-color": [0,0,0,192],
"label-color2": [255,255,255,192],
"label-outline": [0.5,0.7,2.2,2.2],
"zbuffer-offset": [-0.35,0,0],
"culling": 90,
"&id": "{@osmid} {@name}",
"hysteresis": [1500,1500,"&id",true]
},
{
"id": "peaks",
"type": "labels",
"source": "osm-openfreemap",
"filter": ["all",["==","#group","mountain_peak"],["in","$class","peak","volcano","saddle"],["has","$name"],["!=","$name",""]],
"&prominence": {"add":[
{"if":[["has","$ele"],{"mul":[0.0001,{"str2num":"$ele"}]},0]},
{"if":[["has","$prominence"],{"mul":[0.3048,{"str2num":"$prominence"}]},0]}]},
"&importance": {"logScale":["&prominence",8848.8848]},
"&rank": {"discrete2":["&prominence",[[0,6],[30,5],[70,4],[150,3],[300,2],[700,1],[1500,0]]]},
"importance-source": "&importance",
"&feet": {"round":{"mul":[3.2808399,{"str2num":"$ele"}]}},
"&elevation-name": {"if":[["==","#metric",true],"{{'round':{'str2num':'$ele'}}} m","{&feet} ft"]},
"&prominence-name": "{{'round':'&prominence'}} m",
"&peak-name": {"if":[["==","&rank",0],{"uppercase":"{@name}"},"{@name}"]},
"&peak-name2": {"if":[["has","$ele"],"{&peak-name}\n{&elevation-name}","{@peak-name}"]},
"label": true,
"label-source": "&peak-name2",
"label-size": {"linear2":["&rank",[[0,20],[6,14]]]},
"label-font": "@serif-font",
"label-color": {"linear2":["&rank",[[1,[0,0,0,255]],[6,[0,0,0,160]]]]},
"label-color2": {"linear2":["&rank",[[1,[255,255,255,96]],[6,[255,255,255,60]]]]},
"label-outline": [0.2,0.65,2.2,10],
"label-origin": "top-center",
"label-offset": [0,30],
"label-align": "left",
"zbuffer-offset": [-0.45,0,0],
"culling": 100,
"&id": "{@osmid} {&peak-name}",
"hysteresis": [1500,1500,"&id",true]
},
{
"id": "country-boundaries",
"type": "lines",
"source": "osm-openfreemap",
"filter": ["all",["==","#group","boundary"],["==","$admin_level","2"],["!=","$maritime","1"]],
"line": true,
"line-flat": false,
"line-width": 4,
"line-color": [143,117,82,128],
"zbuffer-offset": [-0.01,0,0]
},
{
"id": "state-boundaries",
"type": "lines",
"source": "osm-openfreemap",
"filter": ["all",["==","#group","boundary"],["==","$admin_level","4"],["!=","$maritime","1"]],
"line": true,
"line-flat": false,
"line-width": 2,
"line-color": [143,117,82,128],
"zbuffer-offset": [-0.01,0,0]
}
]
}
https://cartolina.dev/
Your feedback is appreciated
E-mail: ondrej@tspl.re, Twitter/X: @ondrej1974