Vintage 1960 world map with MapLibre GL JS

For a lot of us, an old desk globe or a world atlas is where a love of maps began, and part of the fascination was finding countries on an older map that no longer existed.

This map shows the world from around 1960, with bygone countries like the USSR, East and West Germany, and cities like Leningrad, Léopoldville, and Peking.

It's styled like a globe from that era too, with pastel countries, red dashed borders, and shaded relief.

The borders come from CShapes 2.0, a dataset of the world's borders on every day since 1886 made by researchers at ETH Zürich, as they stood on the last day of 1960, trimmed to Natural Earth's coastline. CShapes leaves out territories that never became countries, such as Greenland, Kuwait, and the Falklands, and many small islands, so OpenHistoricalMap, a free map of history that anyone can edit, names those from its borders on the same day. Each place gets the name a globe of the time would print, such as Algeria (Fr.) and Upper Volta. The borders are simplified for a view of the whole world, so the map stops at zoom 5, where a view takes in a region the size of Western Europe.

Cities have their names of 1960 where Wikidata or GeoNames dates them, such as Stalingrad, Bombay, and Djakarta, and Chinese cities have the spellings English maps used until 1979, such as Peking and Nanking. Elsewhere they have today's names. Mapricot's tiles don't have elevation, so the contours are drawn in the browser from the free AWS Terrain Tiles.

Build it yourself

The countries come ready to use in the map above's own countries.json, and a page draws them on a globe with the AWS Terrain Tiles for relief shading and contour lines that the maplibre-contour plugin draws from the same terrain tiles. Mapricot's tiles add the cities. It's the map above without the grid and the cities' 1960 names. It was tested with MapLibre GL JS 6.15 and maplibre-contour 0.1.1 in Chromium.

What you need

  • A web server to serve the page, and a browser key with the Map tiles product, made as API keys shows. Add http://localhost:* to its allowed origins, so it works from your computer while you build. Map tiles are free, and signing up takes a minute, with no credit card. The terrain tiles and the countries need no key.

Get the countries

Make a folder for the map, and in it download the countries:

Terminal
curl -O https://mapricot.com/gallery/countries.json

It's one GeoJSON file with everything the globe needs, all made with mapshaper from CShapes 2.0's borders on 31 December 1960, OpenHistoricalMap's for the places CShapes leaves out, and Natural Earth's land. Since it's made from CShapes, it's licensed CC BY-NC-SA 4.0 like CShapes is: credit CShapes, don't use it commercially, and share what you make from it the same way.

  • Each country and colony, as polygons with a color from 1 to 6 that none of its neighbors has, the way old globes colored them. Land that no country covers, such as Antarctica, has color 0.
  • The borders countries share, as lines, so the coasts don't get a border too.
  • A point for each name, with the country's area in square kilometers in area.
  • The sea, in pieces with sea set, and the land as one simplified shape with coast set. The page below doesn't use them.

Make the page

Next to it, make index.html, with your browser key in place of mapricot_pk_…:

index.html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Vintage 1960 globe</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/dist/maplibre-gl.css">
    <script type="importmap">
      {
        "imports": {
          "maplibre-gl": "https://cdn.jsdelivr.net/npm/maplibre-gl@6.15.0/+esm",
          "maplibre-contour": "https://cdn.jsdelivr.net/npm/maplibre-contour@0.1.1/+esm"
        }
      }
    </script>
  </head>
  <body style="margin: 0; background: #f1f1ee">
    <div id="map" style="height: 100vh"></div>
    <script type="module">
      import * as maplibregl from "maplibre-gl";
      import mlcontour from "maplibre-contour";

      const key = "mapricot_pk_…";
      const paper = "#f7f5ef";
      const sea = "#b9d8e6";

      const dem = new mlcontour.DemSource({
        url: "https://elevation-tiles-prod.s3.amazonaws.com/terrarium/{z}/{x}/{y}.png",
        encoding: "terrarium",
        maxzoom: 12,
      });
      dem.setupMaplibre(maplibregl);

      const map = new maplibregl.Map({
        container: "map",
        center: [10, 30],
        zoom: 1.5,
        maxZoom: 5,
        style: {
          version: 8,
          glyphs: "https://api.mapricot.com/tiles/v1/fonts/{fontstack}/{range}.pbf",
          sources: {
            protomaps: { type: "vector", url: `https://api.mapricot.com/tiles/v1/planet.json?key=${key}` },
            countries: {
              type: "geojson",
              data: "countries.json",
              attribution:
                '1960 borders: <a href="https://icr.ethz.ch/data/cshapes/">CShapes 2.0</a> (<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA</a>) by Schvitz et al., <a href="https://www.openhistoricalmap.org/copyright">OpenHistoricalMap</a> · <a href="https://www.naturalearthdata.com/">Coasts: Natural Earth</a>',
            },
            dem: {
              type: "raster-dem",
              encoding: "terrarium",
              tiles: [dem.sharedDemProtocolUrl],
              maxzoom: 12,
              tileSize: 256,
              attribution: '<a href="https://github.com/tilezen/joerd/blob/master/docs/attribution.md">Terrain: Mapzen, USGS, NOAA, and others</a>',
            },
            contours: {
              type: "vector",
              maxzoom: 14,
              tiles: [dem.contourProtocolUrl({ thresholds: { 0: [500, 2000] } })],
            },
          },
          layers: [
            { id: "sea", type: "background", paint: { "background-color": sea } },
            {
              id: "country-colors",
              type: "fill",
              source: "countries",
              filter: ["has", "color"],
              paint: {
                "fill-color": ["match", ["get", "color"], 1, "#f4c7d0", 2, "#f6e6a2", 3, "#cde3b1", 4, "#f7cfa6", 5, "#d8cfec", 6, "#e3d2bd", paper],
              },
            },
            {
              id: "relief",
              type: "hillshade",
              source: "dem",
              paint: { "hillshade-shadow-color": "#4a4a4a", "hillshade-highlight-color": "#ffffff", "hillshade-exaggeration": ["interpolate", ["linear"], ["zoom"], 1, 0.55, 5, 0.35] },
            },
            {
              id: "contours",
              type: "line",
              source: "contours",
              "source-layer": "contours",
              filter: ["any", [">", ["get", "ele"], 0], ["==", ["%", ["get", "ele"], 1000], 0]],
              paint: {
                "line-color": ["case", ["<", ["get", "ele"], 0], "#6f9fbd", "#7d7266"],
                "line-opacity": 0.4,
                "line-width": ["match", ["get", "level"], 1, 0.8, 0.5],
              },
            },
            {
              id: "contour-labels",
              type: "symbol",
              source: "contours",
              "source-layer": "contours",
              filter: ["all", [">", ["get", "level"], 0], [">", ["get", "ele"], 0]],
              layout: {
                "symbol-placement": "line",
                "text-field": ["concat", ["number-format", ["get", "ele"], {}], " m"],
                "text-font": ["Noto Sans Italic"],
                "text-size": 10,
              },
              paint: { "text-color": "#6e655b", "text-halo-color": paper, "text-halo-width": 1.5 },
            },
            {
              id: "borders",
              type: "line",
              source: "countries",
              filter: ["in", ["geometry-type"], ["literal", ["LineString", "MultiLineString"]]],
              paint: { "line-color": "#b2342a", "line-width": 1, "line-dasharray": [4, 1, 1, 1] },
            },
            {
              id: "countries",
              type: "symbol",
              source: "countries",
              filter: ["==", ["geometry-type"], "Point"],
              layout: {
                "text-field": ["get", "name"],
                "text-font": ["Noto Sans Medium"],
                "text-size": 12,
                "text-letter-spacing": 0.3,
                "text-transform": "uppercase",
                "text-max-width": 7,
              },
              paint: { "text-color": "#2f2f2f", "text-halo-color": paper, "text-halo-width": 1.5 },
            },
            {
              id: "cities",
              type: "symbol",
              source: "protomaps",
              "source-layer": "places",
              filter: ["==", ["get", "kind"], "locality"],
              layout: { "text-field": ["coalesce", ["get", "name:en"], ["get", "name"]], "text-font": ["Noto Sans Regular"], "text-size": 12 },
              paint: { "text-color": "#2f2f2f", "text-halo-color": paper, "text-halo-width": 1.5 },
            },
          ],
        },
      });
      map.addControl(new maplibregl.NavigationControl({ showCompass: false }));
      map.on("style.load", () => map.setProjection({ type: "globe" }));
    </script>
  </body>
</html>
  • The terrain tiles are PNG images that store each point's height, in meters, in its red, green, and blue values, in a format called Terrarium. They cover the whole world, the sea floor included, from zoom 0 to 15. The map asks for them up to zoom 12 and stretches those when you zoom in further.
  • DemSource loads the terrain tiles once, for both the relief shading and the contours. sharedDemProtocolUrl hands its tiles to MapLibre's hillshade layer, and contourProtocolUrl turns them into vector tiles of contour lines, in a web worker, so the map doesn't stutter while it works.
  • The relief shading is stronger when zoomed out, where the terrain tiles are coarser.
  • thresholds sets the spacing of the lines, in meters, from each zoom up: a minor line, then a major one every few minor lines. Here there's a line every 500 meters and a major one every 2,000 at every zoom. Each line has its height in ele, and level is 1 on major lines, which the style draws thicker and labels.
  • The contour layer colors lines below sea level blue, for the ocean depths, and the rest gray-brown. Its filter keeps only every 1,000 meters of depth, since closer lines run together along the continental shelves. Only land contours get labels.
  • The countries, their borders, and their names all come from the one file, so they always agree with each other. Each layer picks its part of it with a filter on the geometry type, and the fill takes only shapes with a color, so the sea is left to the background.
  • The style draws the sea as its background and the countries over it as the land, so the coasts and borders come from the same data and always meet.
  • The cities come from Mapricot's tiles, with today's names. The map above swaps in their 1960 names, matched by the wikidata id each city has in the tiles.
  • maxZoom stops the map at zoom 5, since the countries are simplified for the whole world and get rough closer in.
  • The globe is set once the style loads, since a style decides its own projection. MapLibre flattens it into a flat map as you zoom in.
  • The terrain tiles' sources ask to be credited, so the dem source adds a line to the map's attribution that links to their list.

Open it

Serve the folder with npx serve or python3 -m http.server, and open the address it prints. Opened straight from the file, the map stays blank, as Open the page explains. You should see a globe of the world in 1960, with the USSR in one color from Europe to the Pacific, contours around the Himalayas and the Andes and blue depth lines along the coasts. Zoom in on the Alps or the Himalayas to see the contours labeled with their heights.

To add a grid of latitude and longitude, add a GeoJSON source of lines every 15° and draw it in a thin gray above the contours.

Data

  • Terrain: Terrain Tiles on AWS Open Data, made by Mapzen from ETOPO1 (NOAA), SRTM, GMTED2010, and 3DEP (USGS), and other national datasets, each credited in their attribution list
  • 1960 borders: CShapes 2.0 (CC BY-NC-SA 4.0), by Schvitz, Rüegger, Girardin, Cederman, Weidmann, and Gleditsch, its borders on 31 December 1960; and OpenHistoricalMap contributors (CC0), its national borders on the same day, for the places CShapes leaves out. Both are trimmed to the coast, with colors, shared borders, and label points made by mapshaper, and labels changed to the names a globe of 1960 would print. The countries.json made from them is CC BY-NC-SA 4.0 too
  • Coasts and lakes: Natural Earth (public domain), its 1:10m land and 1:110m lakes
  • 1960 city names: Wikidata (CC0), official names with start and end dates, Soviet ones written in Latin letters by the BGN/PCGN system; GeoNames (CC BY 4.0), names with dates, for cities Wikidata has none for; and the postal spellings of Chinese cities from Wikipedia's Chinese postal romanization (CC BY-SA 4.0)
  • Cities: OpenStreetMap contributors (ODbL), in Mapricot's map tiles