Skip to content

Texture Maps

Overview

The Texture Map and Globe prefabs provide a quick way to add geographic maps to your scene by using map layers and mesh textures. This prefab implements the OpenLayers API to provide support for map layers and geographic utilities. Additionally, these prefabs provide methods and functions to assist in mapping to lat/lon positions and map interactions. These can be created using createTextureMap() and createTextureGlobe().

Usage

js
//name (required), options (optional), Babylon scene (optional)

//Returns instance of textureMap object
let textureMap = anu.createTextureMap(name: String, options?: {}, scene?: BABYLON.Scene);

//Returns instance of textureGlobe object
let textureGlobe = anu.createTextureGlobe(name: String, options?: {}, scene?: BABYLON.Scene);

Setting Custom XYZ Tile Providers

You can change the XYZ Tile Provider used by TextureMap and TextureGlobe inside of the options argument. This is determined by one or more URLs in the XYZ format. You can find XYZ Tile Providers online with varying license agreements (OpenStreetMaps is permissive).

js
import { XYZ } from 'ol/source';
import TileLayer from 'ol/layer/Tile';

let textureMap = anu.createTextureMap('map', 
  {
    layers: [new TileLayer({
      source: new XYZ({
        crossOrigin: 'anonymous', //Required
        urls: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"]  //Default URL, replace or add to array
      })
    })]
  });

Options

Texture Map

PropertyValueDefault
layers(Tilelayers[ ]) list of map layer objects from OpenLayers[ new TileLayer({ source: new OSM() }) ]
mapHeight(number) the height of the texture in pixels1000
mapWidth(number) the width of the texture in pixels2000
meshSize(number) the scale of the ground mesh50
view(View) map view object from OpenLayersnew View({ center: [0, 0], zoom: 1 })

Texture Globe

PropertyValueDefault
layers(Tilelayers[ ]) list of map layer objects from OpenLayers[ new TileLayer({ source: new OSM(), extent: [-180, -90, 180, 90] }) ]
resolution(Vector2) the resolutions of the texture in pixels 2:1 ratio recommendednew Vector2(1000, 500)
diameter(number) the diameter of the sphere mesh1
view(View) map view object from OpenLayersnew View({projection: 'EPSG:4326', extent[-180, -90, 180, 90], center: [0, 0], zoom: 0})

Methods and Properties

Texture Map

Property / MethodDescription
containerthe DOM element holding the map texture canvas
mapthe open layers Map object
texturethe AdvancedDynamicTexture object used for the map
contextthe DOM canvas object context
meshthe ground mesh
scaleLon([lon,lat])a function that take a lat lon coordinate as a list [lon, lat] and returns the x positions
scaleLat([lon,lat])a function that take a lat lon coordinate as a list [lon, lat] and returns the y positions

Texture Globe

Property / MethodDescription
containerthe DOM element holding the map texture canvas
mapthe open layers Map object
texturethe AdvancedDynamicTexture object used for the map
contextthe DOM canvas object context
meshthe ground mesh
lonLatToVector3([lon,lat])function that take lon lat array [lon, lat] and returns a Vector3(x,y,z) position

Examples

js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import * as anu from '@jpmorganchase/anu';
import * as BABYLON from '@babylonjs/core';
import * as d3 from 'd3';
import data from './data/airports.csv';

//Create and export a function that takes a Babylon engine and returns a Babylon Scene
export function textureMap(engine){

  //Create an empty Scene
  const scene = new BABYLON.Scene(engine);
  //Add some lighting
  new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, 0), scene);
  //Add a camera that rotates around the origin and adjust its properties
  const camera = new BABYLON.ArcRotateCamera('Camera', 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
  camera.position = new BABYLON.Vector3(0, 3, -0.05);
  camera.wheelPrecision = 20;
  camera.minZ = 0;
  camera.attachControl(true);

  //Use the Texture Map prefab to create a plane with an OpenLayers map canvas as the texture
  let textureMap = anu.createTextureMap('map', { meshSize: 2 });

  //Get the OpenLayers map object from the prefab which we will need to customize its settings
  let map = textureMap.map;
  //Change the view parameters of the map to focus on the US
  map.getView().setCenter([-100, 40]);
  map.getView().setZoom(5);

  //Turn on keyboard controls on the TextureMap prefab (uses WASD and -+)
  //Due to a technical quirk, this function must be called *after* setting the center and zoom of the view
  textureMap.keyboardControls(scene);


  //To help create our dots, the Texture Map prefab generates scale functions for us to convert lon/lat to positions in Babylon's coordinate space
  let scaleLon = textureMap.scaleLon;
  let scaleLat = textureMap.scaleLat;
  //Create a D3 scale for color, using Anu helper functions map scale outputs to Color4 objects based on the 'schemecategory10' palette from D3
  let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toColor4(52));

  //We use Mesh instancing here for better performance, first we create a Mesh that serves as the root Node
  let rootSphere = anu.create('sphere', 'sphere', { diameter: 0.02 });
  rootSphere.isVisible = false;
  rootSphere.registerInstancedBuffer('color', 4);   //We need an InstancedBuffer to set the color of instances

  //Select our map object as a Selection object which will serve as our CoT
  let chart = anu.selectName('map', scene);

  //Create instanced sphere meshes from our rootSphere as children of our CoT for each row of our data and set their visual encodings using method chaining
  let spheres = chart.bindInstance(rootSphere, data)
    .setInstancedBuffer('color', new BABYLON.Color4(0, 0, 0, 1));

  //We want to position our spheres whenever the map is loaded and updated (i.e., panned or zoomed), use OpenLayers' callback functions for this
  //N.B: Texture Map's scale functions are only created after the map is fully rendered, so we need to use this callback regardless
  map.on('postrender', () => {
    //Create instanced sphere meshes from our rootSphere as children of our CoT for each row of our data and set their visual encodings using method chaining
    spheres.positionX((d) => scaleLon([d.longitude, d.latitude]))  //These scale functions need as arguments both the longitude and latitude in an array
           .positionZ((d) => scaleLat([d.longitude, d.latitude]))  //This is a requirement from the OpenLayers API
           .setInstancedBuffer('color', (d) => scaleC(d.state))
           .prop('isVisible', (d,n,i) => {  //Custom function to determine if a sphere is inside or outside of the Texture Map which will show/hide the sphere as needed
             let parentBoundingBox = textureMap.mesh.getBoundingInfo().boundingBox;
             return !(n.position.x > parentBoundingBox.maximum.x ||
                      n.position.x < parentBoundingBox.minimum.x ||
                      n.position.z > parentBoundingBox.maximum.z ||
                      n.position.z < parentBoundingBox.minimum.z);
           });
  });

  return scene;
}
js
// SPDX-License-Identifier: Apache-2.0
// Copyright : J.P. Morgan Chase & Co.

import * as anu from '@jpmorganchase/anu';
import * as BABYLON from '@babylonjs/core';
import * as d3 from 'd3';
import data from './data/airports.csv';

//Create and export a function that takes a Babylon engine and returns a Babylon Scene
export function textureGlobe(engine) {

  //Create an empty Scene
  const scene = new BABYLON.Scene(engine);
  //Add some lighting
  new BABYLON.HemisphericLight('light1', new BABYLON.Vector3(0, 10, 0), scene);
  //Add a camera that rotates around the origin and adjust its properties
  const camera = new BABYLON.ArcRotateCamera('Camera', 0, 0, 0, new BABYLON.Vector3(0, 0, 0), scene);
  camera.position = new BABYLON.Vector3(0, 2, -2.5)
  camera.wheelPrecision = 20;
  camera.minZ = 0;
  camera.attachControl(true);

  //Use the Texture Globe prefab to create a sphere with an OpenLayers map canvas as the texture
  let textureGlobe = anu.createTextureGlobe('globe', { resolution: new BABYLON.Vector2(5000, 2500), diameter: 2 });


  //Create a D3 scale for color, using Anu helper functions map scale outputs to Color4 objects based on the 'schemecategory10' palette from D3
  let scaleC = d3.scaleOrdinal(anu.ordinalChromatic('d310').toColor4(52));

  //We use Mesh instancing here for better performance, first we create a Mesh that serves as the root Node
  let rootSphere = anu.create('sphere', 'sphere', { diameter: 0.01 });
  rootSphere.isVisible = false;
  rootSphere.registerInstancedBuffer('color', 4);   //We need an InstancedBuffer to set the color of instances
  
  //Select our globe object as a Selection object which will serve as our CoT
  let chart = anu.selectName('globe', scene);

  //Create instanced sphere meshes from our rootSphere as children of our CoT for each row of our data and set their visual encodings using method chaining
  let spheres = chart.bindInstance(rootSphere, data)
                     .position((d) => textureGlobe.lonLatToVector3([d.longitude, d.latitude]))  //Texture Globe prefab has a scale function for us to convert lon/lat to positions in Babylon's coordinate space
                     .setInstancedBuffer('color', (d) => scaleC(d.state))

  return scene;
}