Skip to content

Library to create and compose animated map transitions in the browser, and to preload the required map tiles.

License

Notifications You must be signed in to change notification settings

UniStuttgart-VISUS/map-transition-helper

Repository files navigation

DOI

This library provides functionalities to define and compose animated transitions on maps, which are then rendered in a web page. The WebMercator map tiles required to show the transition are pre-loaded to ensure a smooth animation. npm package link: https://www.npmjs.com/package/map-transition-helper.

Table of Contents

Installation

The library is built using Rollup. A demo with some event tests and transitions is available in demo/. The demo requires the library to be compiled. To do so:

$ npm install
$ npx rollup -c

To use the library in another project, either build it and use the generated ES module1 (lib/map-transition-helper.esm.js) or the CommonJS build (lib/map-transition-helper.umd.js, global name MapTransitionHelper) directly. Alternatively, it can be installed as an npm package, either from the command line:

$ npm install [email protected]

or by adding the following to the dependencies property in the package.json of your project (with the appropriate version):

{
    ...
    "dependencies": {
        "map-transition-helper": "0.5.0",
        ...
    },
    ...
}

API Description

Point Types

Different types of points exist throughout the API: Two- and three-dimensional cartesian points, two-dimensional geographical coordinates, and three-dimensional geographical view points that include a zoom level.

interface Point2D {
  x: number;
  y: number;
}

interface Point3D {
  x: number;
  y: number;
  z: number;
}

interface Coordinate {
  lat: number;
  lng: number;
}

interface ViewPoint {
  lat: number;
  lng: number;
  zoom: number;
}

Transition Functions

Transitions can be defined by a function that, for each point in time, returns a ViewPoint; that is, a tuple of latitude, longitude, and zoom level. Transitions can be chained, and the time (in [0,1]) they receive can be pre-affected by an easing function.

Types

EasingFunction
type EasingFunction = (t: number) => number;

An easing function takes a value t between 0 and 1, and returns a value (ideally between) 0 and 1. Easing functions are used to define how a transition should behave regarding speed.

PanningFunction
type PanningFunction = (t: number) => Coordinate;

A panning function is a function that returns a coordinate for each point in time between the start time t=0 and the end time t=1.

ZoomFunction
type ZoomFunction = (t: number) => number;

A zoom function is a function that returns a zoom level for each point in time between the start time t=0 and the end time t=1.

TransitionFunction
type TransitionFunction = (t: number) => ViewPoint;

A transition function returns an entire ViewPoint (Coordinate plus zoom level) for each point in time between the start time t=0 and the end time t=1.

Factory Functions

The factory functions can be used to create basic building blocks for more complex transition functions. Very custom functions can be written instead, of course.

createPanning(p0: Coordinate, p1: Coordinate): PanningFunction

Create a linear panning function between two coordinates. The transition will be linear in the (Mercator) image space.

Parameter Type Description
p0 Coordinate Start coordinate
p1 Coordinate End coordinate
createPanningGreatCircle(p0: Coordinate, p1: Coordinate): PanningFunction

Create a great circle panning function between two points. The resulting panning will happen along the geodesic line between p0 and p1.

Parameter Type Description
p0 Coordinate Start coordinate
p1 Coordinate End coordinate
createZoom(z0: number, z1: number): ZoomFunction

Create a linear zoom function between two zoom levels. This is just a thin wrapper around d3.interpolateNumber.

Parameter Type Description
z0 number Start zoom
z1 number End zoom

Composition Functions

Basic building blocks, but also whole transition functions, can be composed for more complex behavior. This can happen multiple times.

addPanningEase(pan: PanningFunction, ease: EasingFunction): PanningFunction

Add an easing function to a panning function. The result will be t -> pan(ease(t)).

Parameter Type Description
pan PanningFunction Initial panning function
ease EasingFunction Easing function to apply
addZoomingEase(zoom: ZoomFunction, ease: EasingFunction): ZoomFunction

Add an easing function to a zoom function. The result will be t -> zoom(ease(t)).

Parameter Type Description
zoom ZoomFunction Initial zoom function
ease EasingFunction Easing function to apply
addTransitionEase(trans: TransitionFunction, ease: EasingFunction): TransitionFunction

Add an easing function to a transition function. The result will be t -> trans(ease(t)).

Parameter Type Description
trans TransitionFunction Initial transition function
ease EasingFunction Easing function to apply
createTransitionFunction(pan: PanningFunction, zoom: ZoomFunction): TransitionFunction

Join a panning and a zoom function to create a transition function.

Parameter Type Description
pan PanningFunction Panning function
zoom ZoomFunction Zoom function
joinTransitions(transs: Array<TransitionFunction>, weights?: Array<number>): TransitionFunction

Join multiple transition functions together. Returns a single transition function from t=0 to t=1. The portions of the new time domain the passed transition functions occupy is given by the weights array. If that is not passed, all functions will have equal weight.

Parameter Type Description
transs Array<TransitionFunction> Array of transition functions
weights Array<number> (optional) Array of time portion weights

Predefined Transition Functions

Some basic transition functions come predefined.

createLinearPanTransition(p0: ViewPoint, p1: ViewPoint): TransitionFunction

Create a linear panning transition without zooming. The zoom level of the first view point will be used throughout.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
createLinearZoomAndPanTransition(p0: ViewPoint, p1: ViewPoint): TransitionFunction

Create a linear pan and zoom. Can also be used to just zoom.

NOTE: In some cases, this is probably not what you want: Zooming is linear in the zoom level, but panning speed then needs to scale with the current zoom level so that it is perceived as linear. For zoom+pan transitions, use createPerceivedLinearZoomAndPanTransition instead.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
createPerceivedLinearZoomAndPanTransition(p0: ViewPoint, p1: ViewPoint): TransitionFunction

Create a pan and zoom transition that is perceived as being linear. The panning will depend on the zoom level.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
createBoxTransition(p0: ViewPoint, p1: ViewPoint, canvasSize: Point2D, minZoom?: number): TransitionFunction

Create a box transition: zoom out from p0 until p1 is visible. Then pan to p1. Then zoom into p1.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
canvasSize Point2D Size of the canvas the transition will run on
minZoom number (optional) Zoom level for panning phase. If not given, will be calculated to fit.
createTriangularTransition(p0: ViewPoint, p1: ViewPoint, canvasSize: Point2D, minZoom?: number): TransitionFunction

Create a (perceived) triangular transition: Zoom out and pan from p0 to the midpoint between p0 and p1 in the image space, at which time both p0 and p1 will be visible. Then, continue panning and zooming in again until centered on p1.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
canvasSize Point2D Size of the canvas the transition will run on
minZoom number (optional) Zoom level for panning phase. If not given, will be calculated to fit.
createVanWijkAndNuijTransition(p0: ViewPoint, p1: ViewPoint, canvasSize: Point2D, rho: number = 1.4): TransitionFunction

Create a transition following the hyperbolic path recommended by van Wijk and Nuij: "Smooth and efficient zooming and panning" (Proc. InfoVis, 2003).

Note: As of now, this library only supports Mercator projection. This transition should be done in image space, even if the TransitionFunction outputs geographical space ViewPoints. For now, this function will assume Mercator projection is used for its internal calculations. This should be amended once different projections are supported.

Create a (perceived) triangular transition: Zoom out and pan from p0 to the midpoint between p0 and p1 in the image space, at which time both p0 and p1 will be visible. Then, continue panning and zooming in again until centered on p1.

Parameter Type Description
p0 ViewPoint Start point
p1 ViewPoint End point
canvasSize Point2D Size of the canvas the transition will run on
rho number (optional) Parameter ρ, which determines the shape of the hyperbolic path in (u, w) space (see the paper). If not passed, the recommended value of 1.4 is used.

preloadTransition Function

async function preloadTransition(
  transitionFunction: TransitionFunction,
  canvasSize: Point2D,
  numberOfFrames: number,
  tileMap?: TileMap
): Promise<Transition>

This is the central functionality of the library. It preloads all map tiles and pre-calculates all Frames needed for a transition, and returns a Transition object asynchronously.

Parameter Type Description
transitionFunction TransitionFunction Transition function which describes the position of the viewing window and its zoom for every point during the transition.
canvasSize Point2D size of the canvas.
numberOfFrames number number of frames of the transition (directly determines the length of the animation). On most devices, 60 frames are displayed per second.
tileMap TileMap (optional) If set by the user, this tilemap object is used to store the bitmaps of the needed Mercator tiles. Can be useful if more than one transition should be stored in the tilemap. This can lead to less memory consumption since a tile may participate in more than one transition.

Utility Functions

calcMinZoomCenteredOn(center: Coordinate, outer: Coordinate, canvasSize: Point2D, margin?: number): number

Calculate the minimal zoom level, centered on center on a canvas of size canvasSize, such that outer is visible margin pixels from the canvas edge. Returns the minimal (strictly: maximal) zoom level that satisfies the condition.

Parameter Type Description
center Coordinate Center of the map
outer Coordinate Other point that should be visible
canvasSize Point2D Size of the canvas
margin number (optional) Distance of outer from the edge in pixels, default 20

calcZoomFitPoints(coordinates: Array<Coordinate>, canvasSize: Point2D, margin: number = fitMargin,): number

Calculate the minimal (maximal) zoom level such that all coordinates are still visible in a map with size canvasSize, with all points at least margin pixels from the edge of the canvas.

Parameter Type Description
coordinates Array<Coordinate> Array of coordinates that should be visible
canvasSize Point2D Size of canvas
margin number (optional) Minimum distance of coordinates from the edge of the canvas in pixels, default 20

calculateFrame(canvasSize: Point2D, currentPoint: ViewPoint): Frame

Calculate the Frame object that describes a particular ViewPoint for a given canvas size.

Class TileMap

An object of this class preloads and stores the map tiles required for transitions. TileMap objects can be reused for multiple transitions. TileMap extends EventTarget and fires events when tiles are loaded, fail to load, and loading is complete.

constructor

constructor(
    readonly _tileServerUrl: string = 'https://services.arcgisonline.com/arcgis/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
    readonly _maxZoom: number = 19,
)

Properties

numTiles: number

The number of tiles stored in the tile map.

Methods

hasTile(tile: Tile): boolean

Returns true if the tile described by tile is loaded.

getProgress(): { loading: number, finished: number, failed: number, total: number }

Get the current loading progress, for example for a progress bar. total is the sum of loading, failed, and finished.

async addToTileMap(tile: Tile): Promise<void>

Preload tile, if it has not been loaded yet.

getTileImage(tile: Tile): ImageBitmap

Get the image data for the passed tile coordinates. If the coordinates are outside the world boundaries, returns a white tile instead.

async fetchFailedTiles(): Promise<void>

Try again to load all tiles that failed loading.

Events

tileadded

Fired each time a new (unique) tile is added. The CustomEvent's detail property contains the Tile instance.

tileloaded

Fired each time a tile is successfully loaded. The CustomEvent's detail property contains the Tile instance.

tilefailed

Fired each time a tile load fails. The CustomEvent's detail property contains the Tile instance.

loaded

Fired each time all queued tiles are loaded.

Class Tile

Class which represents a map tile with x, y an zoom value.

constructor

constructor(
  readonly x: number,
  readonly y: number,
  readonly zoom: number
)

Does not usually need to be called directly. This is handled by preloadTransition.

Properties

The properties are the same as the parameters of the constructor.

clone(): Tile

Create a new Tile instance with the same x, y and zoom values.

Class Frame

Frame class where all necessary information about a frame are stored. Especially the required tiles for a frame are saved here.

constructor

constructor(
  tiles: Array<[number, number, number]>,
  readonly offset: Point2D,
  readonly translation: Point2D,
  readonly canvasSize: Point2D,
  readonly scale: number,
  readonly zoom: number
)
Parameter Type Description
tiles Array<[number, number, number]> tile coordinates that are required for a frame, as produced by d3-tile
offset Point2D offset of the canvas center from the WebMercator origin
translation Point2D d3-tile translation, used to align the tile quadtree to the projection
canvasSize Point2D the size of the corresponding canvas (x=width, y=height)
scale number scale value for the tiles in this frame. Only needed for drawing the tiles into the canvas
zoom number the current zoom level

Does not usually need to be called directly. This is handled by preloadTransition.

Properties

The properties are the same as the parameters of the constructor.

Methods

project(coord: Coordinate | ViewPoint): Point2D

Calculate the pixel coordinates of real-world coordinates. This is the inverse of unproject.

unproject(coord: Point2D): ViewPoint

Calculate the latitude, longitude and zoom level shown at the pixel coordinate coord. This is the inverse of project.

isCoordinateVisible(c: Coordinate, offset: number = 0): boolean

Checks if the coordinate is within the drawable area of the canvas in the frame. Optionally, an offset can be passed, similar to the one specified in borderPosition. The offset is perpendicular to the border and positive inwards of the frame border.

borderPosition(c: Coordinate | ViewPoint, offset: number = 0): { x, y, border, direction }

Calculate the position c at the border of the frame that is in the direction of a coordinate. This can be used for positions that are visible in the current frame as well, but should not be. An optional parameter offset specifies the perpendicular distance of the point to the border it should lie on, with positive values going into the frame. This can be used to place objects of known size at the frame border in different alignments. Returns an object with the x and y coordinates relative to the frame, the border (top, left, bottom, or right) at which the shortest path to the coordinate leaves the frame, and the direction (in radians) towards the point from the center of the frame.

Class Transition

Main class to interact with. This extends EventTarget, so addEventListener is supported.

constructor

constructor(frames: Frame[], tileMap: TileMap)

Takes two parameters, an array of Frames and a TileMap. The constructor need never be called directly. Use the preloadTransition function instead.

Properties

frameArray: Frame[]

Property (read-only) containing all Frames.

length: number

Property (read-only) containing the number of Frames in the transition.

tileMap: TileMap

Read-write property containing the transition's TileMap.

currentFrame: Frame

Read-only property containing the current Frame.

currentFrameIndex: number

Read-write property containing the current Frame's index.

state: TransitionState

Read-only property containing the state of the transition (uninitialized, paused, running, finished).

reversed: boolean

Read-write property stating the direction of the transition. If reversed === true, then the transition will run from the last frame to the first.

Methods

getFrame(index: number): Frame

Get the Frame at index.

initialize(context: CanvasRenderingContext2D): void

Set up the transition for the canvas rendering context. This must be called before the transition so that the transition can draw to the canvas. This will also render the first (or last) frame to the canvas.

Will fire frame and render events.

play(): void

Play the transition from the current frame. This will draw a new frame each browser frame.

Will fire a play event immediately. Will fire frame and render events each frame until the last frame is rendered. Then, a finished and a pause event are fired as well.

pause(): void

Stop the animation immediately.

Will fire a pause event.

pause(): void

Stop the animation immediately, reset to the first (or last) frame, and render that to the canvas.

Will fire cancel, pause, frame and render events. The cancel event is fired immediately, but the first (or last) frame is only rendered in the next browser frame, and the other events are fired then.

Events

frame

Fired when a new frame will be rendered; i.e., when the transformations on the canvas might have changed. This is the event to listen for to determine if overlays should be re-rendered.

render

Fired when the frame is actually rendered.

play

Fired when transition starts or resumes.

pause

Fired when transition pauses.

cancel

Fired when transition is cancelled.

finish

Fired when transition completes.

Enum TransitionState

The state of a transition, as stored in Transition.state.

enum TransitionState {
  UNINITIALIZED = 'uninitialized',
  PAUSED = 'paused',
  RUNNING = 'running',
  FINISHED = 'finished',
};

Usage Examples

Here are some simple examples of how to use the API. These expect to be run from an HTML file with a <canvas> element (ideally of larger size, for example 800x600) already added. For more examples, see the demo.

Simple Linear Transition

For a very simple, linear transition, use the predefined transition functions to create a transition from Stuttgart to Esslingen on zoom level 12:

import '../lib/map-transition-helper.umd.js';
const { createLinearPanTransition, preloadTransition } = MapTransitionHelper;

const canvas = document.querySelector('canvas');
const transitionFunction = createLinearPanTransition(
  { lat: 48.7734, lng: 9.1829, zoom: 12 }, // Stuttgart
  { lat: 48.7392, lng: 9.3049, zoom: 12 } // Esslingen
);
const transition = await preloadTransition(
  transitionFunction,
  { x: canvas.width, y: canvas.height },
  120 // number of frames, equals 2s
);

// initialize transition on canvas
transition.initialize(canvas.getContext('2d'));

// notify me when transition is finished
transition.addEventListener('finish', () => console.log('transition finished'));

// run
transition.play();

Add Custom Easing to a Linear Transition

For adding easing, the functions provided by d3-ease might be useful. Here, we define a custom function that transitions slowly in the first and last segment, and pauses in the middle:

import { easeCubic } from 'https://cdn.skypack.dev/d3-ease@3';
import '../lib/map-transition-helper.umd.js';  // provides global MapTransitionHelper
const {
  addTransitionEase,
  createLinearPanTransition,
  preloadTransition,
} = MapTransitionHelper;


const canvas = document.querySelector('canvas');
const linearTransitionFunction = createLinearPanTransition(
  { lat: 48.7734, lng: 9.1829, zoom: 12 }, // Stuttgart
  { lat: 48.7392, lng: 9.3049, zoom: 12 } // Esslingen
);

// easing function
function customEase(t) {
  if (t < 0.4) return 0.5 * easeCubic(2.5 * t);
  if (t < 0.6) return 0.5;
  return 0.5 + 0.5 * easeCubic(2.5 * (t - 0.5));
}

// apply to linear transition
const transitionFunction = addTransitionEase(linearTransitionFunction, customEase);

const transition = await preloadTransition(
  transitionFunction,
  { x: canvas.width, y: canvas.height },
  180 // number of frames, equals 3s
);

transition.initialize(canvas.getContext('2d'));
transition.play();

Compose a Complex Transition

The joinTransitions function can be used to compose multiple transitions to one complex transition. The resulting transition function will again run from t=0 to t=1.

import '../lib/map-transition-helper.umd.js';  // provides global MapTransitionHelper
const {
  calcMinZoomCenteredOn,
  createBoxTransition,
  createLinearPanTransition,
  createLinearZoomAndPanTransition,
  createPerceivedLinearZoomAndPanTransition,
  joinTransitions,
  preloadTransition,
  TileMap,
} = MapTransitionHelper;

const canvas = document.querySelector('canvas');

// use different map tiles
const tileMap = new TileMap('https://tile.openstreetmap.org/{z}/{x}/{y}.png');

const stuttgart = { lat: 48.7734, lng: 9.1829 };
const munich = { lat: 48.1391, lng: 11.569 };
const milano = { lat: 45.4553, lng: 9.2064 };

// part 1: zoom out of Stuttgart until Munich is visible
const stuttgartMunichZoom = calcMinZoomCenteredOn(
  stuttgart,
  munich,
  { x: canvas.width, y: canvas.height },
  50
);
const zoomOut = createLinearZoomAndPanTransition(
  { ...stuttgart, zoom: 15 },
  { ...stuttgart, zoom: stuttgartMunichZoom }
);

// part 2: zoom into Munich while panning to it
const panAndZoomToMunich = createPerceivedLinearZoomAndPanTransition(
  { ...stuttgart, zoom: stuttgartMunichZoom },
  { ...munich, zoom: 12 }
);

// part 3: pause in Munich
const pause = createLinearPanTransition({ ...munich, zoom: 12 }, { ...munich, zoom: 12 });

// part 4: box transition to Milano with fixed minZoom = 4
const boxToMilano = createBoxTransition(
  { ...munich, zoom: 12 },
  { ...milano, zoom: 14 },
  { x: canvas.width, y: canvas.height },
  4
);

// join:
//  - 20% of time zooming out of Stuttgart,
//  - 35% zooming into Munich,
//  - pause 10%,
//  - 35% box transition to Milano
const transitionFunction = joinTransitions(
  [zoomOut, panAndZoomToMunich, pause, boxToMilano],
  [20, 35, 10, 35]
);

const transition = await preloadTransition(
  transitionFunction,
  { x: canvas.width, y: canvas.height },
  600, // number of frames, equals 10s
  tileMap
);

transition.initialize(canvas.getContext('2d'));
transition.play();

Custom Panning with an SVG Overlay

This example demonstrates custom panning (here an Archimedean spiral defined in the geospatial domain) and zoom functions. It also demonstrates the API for updating other DOM when the ViewPoint of the canvas changes, and calculating between screen and geographical coordinate systems.

import '../lib/map-transition-helper.umd.js';  // provides global MapTransitionHelper
const {
  createTransitionFunction,
  preloadTransition,
  TileMap,
} = MapTransitionHelper;
const canvas = document.querySelector('canvas');

// create SVG overlay
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const { top, left, width, height } = canvas.getBoundingClientRect();
svg.style.position = 'absolute';
svg.style.top = `${top}px`;
svg.style.left = `${left}px`;
svg.setAttribute('width', width);
svg.setAttribute('height', height);
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
document.body.append(svg);

// spiral inwards towards Stuttgart slowly, going around four times
const stuttgart = { lat: 48.7734, lng: 9.1829, zoom: 12 };
function panningCircle(t) {
  const theta = 4 * 2 * Math.PI * t;
  const r = 0.2 * (1 - t);
  const lat = stuttgart.lat + r * Math.sin(theta);
  const lng = stuttgart.lng + r * Math.cos(theta);
  return { lat, lng };
}

// zoom in slowly
const zoom = t => 10 + 2 * t;

const tileMap = new TileMap('https://tile.openstreetmap.org/{z}/{x}/{y}.png');
const transitionFunction = createTransitionFunction(panningCircle, zoom);
const transition = await preloadTransition(
  transitionFunction,
  { x: canvas.width, y: canvas.height },
  900,
  tileMap
);

// each frame, update the overlay SVG by drawing a dot where Stuttgart is
transition.addEventListener('frame', () => {
  const frame = transition.currentFrame;
  const { x, y } = frame.project(stuttgart);
  svg.innerHTML = `
    <circle r="5"
            cx="${x}"
            cy="${y}"
            stroke="none"
            fill="rebeccapurple" />
  `;
});

transition.initialize(canvas.getContext('2d'));  // fires first 'frame' event
transition.play();  // fires 'frame' event each frame

Attribution

This library was initially designed and developed by Leon Gutknecht, Benjamin Hahn, Alexander Riedlinger, Ingo Schwendinger, and Joel Waimer. This work was done within the scope of a study project (Bachelorforschungsprojekt) at the Institute for Visualization and Interactive Systems at the University of Stuttgart, under the supervision of Max Franke and Samuel Beck, and examined by Prof. Dr. Thomas Ertl. The library has since been further developed and maintained by Max Franke.

Footnotes

  1. The ES module build expects some external dependencies to exist, namely the D3.js modules it uses internally. Unfortunately, I have not yet gotten the ES build to load in the browser without it complaining about the external imports. However, it can be used with a build tool like WebPack or Rollup.

About

Library to create and compose animated map transitions in the browser, and to preload the required map tiles.

Resources

License

Stars

Watchers

Forks

Packages

No packages published