Skip to main content

Render MVF v2 with deck.gl

Mappedin Venue Format (MVF) v2 is currently in an alpha state while Mappedin perfects a new design. Breaking changes may occur and this page will be updated to reflect this.

A GeoJSON renderer is required to display MVF data. For a full breakdown of the MVF bundle, read the MVF Data Model. This guide will demonstrate how to get started using deck.gl, a popular and highly performant WebGL2 renderer.

Project Setup

  1. Start a new vanilla Vite project using TypeScript.
yarn create vite mappedin-mvf-guide

From the setup menu, select "Vanilla", then "TypeScript".

cd mappedin-mvf-guide
  1. Install deck.gl and related packages.
yarn add @deck.gl/core @deck.gl/layers @types/geojson
  1. Download the MVF and extract the contents to the public directory. If uncertain how to download an MVF, see Getting Started with MVF v2.

Update main.ts, style.css

Open the project in your editor. In the src directory, create or update the main.ts file and replace the contents with the code block below. The rest of this guide will break down and explain the code.

import { Color, Deck } from '@deck.gl/core/typed';
import { GeoJsonLayer } from '@deck.gl/layers/typed';
import { Feature } from 'geojson';
import './style.css';

async function init() {
const MVF = 'Dev Office Demo'; // MVF directory name (replace with your own MVF directory name)
const ELEVATION = 0; // The floor elevation desired

async function loadData(directory: string, file: string) {
// The unzipped MVF bundle should be placed in the public directory
return (await fetch(`/public/${directory}/${file}`)).json();
}

// Load the essential MVF data files
const manifestData = await loadData(MVF, 'manifest.geojson');
const styles = await loadData(MVF, 'styles.json');
// Extract the features array from the FeatureCollection
const mapData = floorData.features.map((feature) => feature.properties);
// Get the current map for the set elevation
const floorId = mapData.find((f) => f.elevation === ELEVATION).id;
// Get geometry data by floor ID
const spaceData = await loadData(MVF, `space/${floorId}.geojson`);
const obstructionData = await loadData(MVF, `obstruction/${floorId}.geojson`);

function hexToRGB(hex: string): Color {
// Converts a lowercase hex string such as #ffffff to an RGB array
return hex.match(/[0-9a-f]{2}/g)!.map((x) => parseInt(x, 16)) as Color;
}

// Space data contains traversable areas such as rooms and hallways
const roomStyles = styles['Rooms'];
const roomLayer = new GeoJsonLayer({
id: 'room-layer',
// Filter by Space polygons that are defined in the Rooms style
data: spaceData.features.filter((f: Feature) => roomStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(roomStyles.color), // Apply Walls polygon color
stroked: false, // No outline
});

const hallwayStyles = styles['Hallways'];
const hallwayLayer = new GeoJsonLayer({
id: 'hallway-layer',
// Filter by Space polygons that are defined in the Hallways style
data: spaceData.features.filter((f: Feature) => hallwayStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(hallwayStyles.color), // Apply Hallways polygon color
stroked: false, // No outline
});

// Obstruction data contains non-traversable areas such as walls and desks
const wallStyles = styles['Walls'];
const wallLayer = new GeoJsonLayer({
id: 'wall-layer',
// Filter by Obstruction line strings that are defined in the Walls style
data: obstructionData.features.filter((f: Feature) => wallStyles.lineStrings.includes(f.properties!.id)),
getLineColor: hexToRGB(wallStyles.color), // Apply Walls line string color
getLineWidth: wallStyles.width, // Apply Walls line string width
stroked: false, // No outline
});

const exteriorWallStyles = styles['ExteriorWalls'];
const exteriorWallLayer = new GeoJsonLayer({
id: 'exterior-wall-layer',
// Filter by Obstruction line strings that are defined in the ExteriorWalls style
data: obstructionData.features.filter((f: Feature) =>
exteriorWallStyles.lineStrings.includes(f.properties!.id),
),
getLineColor: hexToRGB(exteriorWallStyles.color), // Apply ExteriorWalls line string color
getLineWidth: exteriorWallStyles.width, // Apply ExteriorWalls line string width
stroked: false, // No outline
});

const deskStyles = styles['Desks'];
const deskLayer = new GeoJsonLayer({
id: 'desk-layer',
// Filter by Obstruction polygons that are defined in the Desks style
data: obstructionData.features.filter((f: Feature) => deskStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(deskStyles.color), // Apply Desks polygon color
stroked: false, // No outline
});

new Deck({
initialViewState: {
// Set the initial view state to the maps center point
longitude: manifestData.features[0].geometry.coordinates[0],
latitude: manifestData.features[0].geometry.coordinates[1],
zoom: 18,
},
controller: true, // Enable map controls
// Order layers in vertical stacking order
layers: [roomLayer, hallwayLayer, deskLayer, wallLayer, exteriorWallLayer],
});
}
init();

Replace the contents of style.css with a simple background color. This will help the white and grey shades to stand out.

body {
background-color: darkseagreen;
}

Loading the Data

The GeoJSON data for an MVF is split into multiple parts, often separated by floor ID. The structure of this bundle is explained in the MVF v2 Data Model.

Load all the datasets from the public directory using a fetch function. For a basic 2D or 3D rendering, the following datasets are needed:

Some data - like Space and Obstruction - is divided by floor ID. Using the desired elevation, the floor ID can be pulled from the Floor GeoJSON file. Then, in the fetch specify which floor ID file to load.

const MVF = 'Dev Office Demo'; // MVF directory name
const ELEVATION = 0; // The floor elevation desired

async function loadData(directory: string, file: string) {
return (await fetch(`/public/${directory}/${file}`)).json();
}

const manifestData = await loadData(MVF, 'manifest.geojson');
const styles = await loadData(MVF, 'styles.json');
const floorData = await loadData(MVF, 'floor.geojson');
// Extract the features array from the FeatureCollection
const mapData = floorData.features.map((feature) => feature.properties);
// Get the current map for the set elevation
const floorId = mapData.find((f) => f.elevation === ELEVATION).id;
// Get geometry data by floor ID
const spaceData = await loadData(MVF, `space/${floorId}.geojson`);
const obstructionData = await loadData(MVF, `obstruction/${floorId}.geojson`);

Rendering the Geometry

The Space and Obstruction datasets contain the geometry of the map, but they don't contain any properties which specify how they should be rendered. Instead, the MVF comes with a Styles JSON file for this purpose.

Open styles.json in an editor to preview some of the style sets available. The key names are not guaranteed, but are generally English descriptors such as "Rooms" or "Walls". Each style has a list of either polygons or lineStrings which belong to it.

Create a new DeckGL GeoJsonLayer for rooms. To select only the rooms from the Space GeoJSON, filter the dataset by polygons included in the "Rooms" Styles. Since these are polygons, apply the color to getFillColor in DeckGL.

const roomStyles = styles['Rooms'];
const roomLayer = new GeoJsonLayer({
id: 'room-layer',
data: spaceData.features.filter((f: Feature) => roomStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(roomStyles.color),
stroked: false,
});

Similarly, the same process is done for Hallways.

const hallwayStyles = styles['Hallways'];
const hallwayLayer = new GeoJsonLayer({
id: 'hallway-layer',
data: spaceData.features.filter((f: Feature) => hallwayStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(hallwayStyles.color),
stroked: false,
});

There are 3 main Styles for Obstruction GeoJSON data, "Walls", "ExteriorWalls", and "Desks". Follow the same process, however, Walls and ExteriorWalls are lineStrings. As such, set getLineColor and getLineWidth in the GeoJsonLayer.

const wallStyles = styles['Walls'];
const wallLayer = new GeoJsonLayer({
id: 'wall-layer',
data: obstructionData.features.filter((f: Feature) => wallStyles.lineStrings.includes(f.properties!.id)),
getLineColor: hexToRGB(wallStyles.color),
getLineWidth: wallStyles.width,
stroked: false,
});

const exteriorWallStyles = styles['ExteriorWalls'];
const exteriorWallLayer = new GeoJsonLayer({
id: 'exterior-wall-layer',
data: obstructionData.features.filter((f: Feature) => exteriorWallStyles.lineStrings.includes(f.properties!.id)),
getLineColor: hexToRGB(exteriorWallStyles.color),
getLineWidth: exteriorWallStyles.width,
stroked: false,
});

const deskStyles = styles['Desks'];
const deskLayer = new GeoJsonLayer({
id: 'desk-layer',
data: obstructionData.features.filter((f: Feature) => deskStyles.polygons.includes(f.properties!.id)),
getFillColor: hexToRGB(deskStyles.color),
stroked: false,
});

Finally, create the Deck object and attach the layers. The Manifest GeoJSON contains the center coordinate of the map, making it a good candidate for the initialViewState. List the geometry layers in the order that they stack vertically. For example, the Desks and Walls should be a layer above the Rooms and Hallways.

new Deck({
initialViewState: {
longitude: manifestData.features[0].geometry.coordinates[0],
latitude: manifestData.features[0].geometry.coordinates[1],
zoom: 18,
},
controller: true,
layers: [roomLayer, hallwayLayer, deskLayer, wallLayer, exteriorWallLayer],
});

End Result

The end result should be a 2D render of the MVF v2 in DeckGL. The following CodeSandbox implements this guide with the Dev Office Demo.