Render MVF v2 with deck.gl
Mappedin SDK version 6 is currently in a beta state while Mappedin perfects new features and APIs. Open the v6 release notes to view the latest changes.
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
- 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
- Install deck.gl and related packages.
yarn add @deck.gl/core @deck.gl/layers @types/geojson
- 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"); const mapData = await loadData(MVF, "map.geojson"); // Get the current map for the set elevation const mapId = mapData.find((m) => m.elevation === ELEVATION).id; // Get geometry data by map ID const spaceData = await loadData(MVF, `space/${mapId}.geojson`); const obstructionData = await loadData(MVF, `obstruction/${mapId}.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 map 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 map ID. Using the desired elevation, the map ID can be pulled from the Map GeoJSON file. Then, in the fetch specify which map ID file to load.
const MVF = "Dev Office Demo"; // MVF directory nameconst 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 mapData = await loadData(MVF, "map.geojson");// Get the current map for the set elevationconst mapId = mapData.find((m) => m.elevation === ELEVATION).id;// Get geometry data by map IDconst spaceData = await loadData(MVF, `space/${mapId}.geojson`);const obstructionData = await loadData(MVF, `obstruction/${mapId}.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.