## v3 ### Getting Started with MVF v3 # Getting Started with MVF v3 **Mappedin Venue Format (MVF)** is a GeoJSON based format containing geometry data associated with an indoor map. Mappedin's exports take highly accurate indoor map data and format it as per GeoJSON specs, for a variety of use cases. These GeoJSON based exports provide complete flexibility to build any indoor mapping experience. Read the MVF v3 Specification for a full breakdown of the MVF bundle. ## Downloading an MVF Using Mappedin Maker MVF export is a feature of Mappedin Pro and Solution Tiers. To export an MVF using Mappedin Maker, follow the steps below. 1. Open Mappedin Maker and navigate to the desired map. 2. From the Mappedin Maker screen, select the download button. 3. Select "Download as MVF (GeoJSON)". !Download as MVF This will download a `zip` file titled the name of the map. Within the directory are a number of GeoJSON and JSON files describing the geometry and styling extensions of the map. These files are explained in further detail in the MVF v3 Specification. ## Downloading an MVF Using the Mappedin REST API MVF export is a feature of Mappedin Pro and Solution Tiers. To export an MVF using the Mappedin REST API, follow the steps below. Refer to the Mappedin REST API documentation for more information. ### 1. Get an API Key Follow the steps in the Create a Key & Secret Guide to generate an API key and secret. ### 2. Exchange API Key and secret for JWT Using the API key and secreted generated in the previous step, make a `POST` call to `https://app.mappedin.com/api/v1/api-key/token`, passing the `key` and `secret` within the body of the call in JSON format. Use the API key as the username and the secret as the password. The body of the call should be formatted as follows: ```json { "key": "mik_yeBk0Vf0nNJtpesfu560e07e5", "secret": "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022" } ``` The API responds with JSON that contains a JSON Web Token (JWT) token that can be used to authenticate to other Mappedin REST API endpoints. The response will be formatted as follows: ```json { "access_token": "YOUR_TOKEN_HERE", "expires_in": 172800 } ``` Refer to the Exchange API Key and secret for JWT REST API documentation for more information and examples. ### Get an MVF v3 Download Link Using the JWT token generated in the previous step, make a `GET` call to `https://app.mappedin.com/api/v1/venue//mvf?version=3.0.0`. Replace `` with the ID of the map you wish to export. Use the `Authorization` header with the value `Bearer ` where `` is the JWT token. The API responds with JSON that contains a link to download a zip file containing the MVF. This download link expires after 48 hours. Refer to the Get venue MVF REST API documentation for more information and examples. ### Get an MVF v3 Locale Package Maps that include multiple languages will have locale packages available for download. The translated assets are packaged within locale packages, with each language contained within its own zip file. Links to these zip files are included in the response of the `GET /venue/{venueId}/mvf` endpoint described above. Locale packages do not contain building geometry data, only the translated assets. Below is an example response that includes links to locale packages: ```json { "url": "https://path/to/mvf.zip", "updated_at": "2024-09-07T08:10:07.858Z", "locale_packs": { "ar": "https://path/to/ar.zip", "es": "https://path/to/es.zip", "th": "https://path/to/th.zip" } } ``` ## Example MVF v3 Packages The following are examples of MVF v3 packages: - Single Language: Demo Office MVF v3 - Multi-Language: Demo Airport MVF v3 + Demo Airport MVF v3 French Locale Packages ### MVF v3 Package API Docs # MVF v3 Package API Docs ## Latest Version Mappedin Venue Format v3 (MVF v3) v3.0.0-beta.10 ## Previous Versions v3.0.0-beta.9v3.0.0-beta.8v3.0.0-beta.2v3.0.0-beta ## examples ### Render MVF v3 with deck.gl # Render MVF v3 with deck.gl A GeoJSON renderer is required to display MVF data. For a full breakdown of the Mappedin Venue Format v3 (MVFv3) bundle, read the MVF v3 Specification. This guide will demonstrate how to get started using deck.gl, a popular and highly performant WebGL2 renderer. !deck.gl Image ## Project Setup 1. Start a new vanilla Vite project using TypeScript. ```sh yarn create vite mappedin-mvf-guide ``` From the setup menu, select `Vanilla`, then `TypeScript`. ```sh cd mappedin-mvf-guide ``` 2. Install deck.gl and related packages. ```sh yarn add @deck.gl/core @deck.gl/layers @types/geojson jszip @turf/buffer ``` ## 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. Create a `utils.ts` and `mapkit.d.ts` file in the `src` directory copy the code below into these files. The rest of this guide will break down and explain the code. ```ts title="src/main.ts" import type { Color } from '@deck.gl/core'; import { Deck } from '@deck.gl/core'; import { GeoJsonLayer } from '@deck.gl/layers'; import type { Feature } from 'geojson'; import JSZip from 'jszip'; import './style.css'; import { downloadAndProcessVenue, getAccessToken, loadFileFromZip } from './utils'; //The Mappedin Map ID to load. const MAP_ID = '64ef49e662fd90fe020bee61'; // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const MAPPEDIN_KEY = 'mik_yeBk0Vf0nNJtpesfu560e07e5'; const MAPPEDIN_SECRET = 'mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022'; const ELEVATION = 0; // The floor elevation to be displayed. // Initialize the visualization with the GoeJSON files in the MVFv3 zip. async function initVisualization(zip: JSZip) { // Load the essential MVF data files from zip const manifestData = await loadFileFromZip(zip, 'manifest.geojson'); const styles = await loadFileFromZip(zip, 'default-style.json'); const floorData = await loadFileFromZip(zip, 'floors.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 geometryData = await loadFileFromZip(zip, `geometry/${floorId}.geojson`); function hexToRGB(hex: string): Color { return hex.match(/[0-9a-f]{2}/gi)!.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', data: geometryData.features.filter((f: Feature) => roomStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), getFillColor: hexToRGB(roomStyles.color), stroked: false, }); const hallwayStyles = styles['Hallways']; const hallwayLayer = new GeoJsonLayer({ id: 'hallway-layer', data: geometryData.features.filter((f: Feature) => hallwayStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), getFillColor: hexToRGB(hallwayStyles.color), stroked: false, }); const wallStyles = styles['Walls']; const wallLayer = new GeoJsonLayer({ id: 'wall-layer', data: geometryData.features.filter((f: Feature) => wallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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: geometryData.features.filter((f: Feature) => exteriorWallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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: geometryData.features.filter((f: Feature) => deskStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), getFillColor: hexToRGB(deskStyles.color), stroked: false, }); new Deck({ initialViewState: { longitude: manifestData.features[0].geometry.coordinates[0], latitude: manifestData.features[0].geometry.coordinates[1], zoom: 18.5, }, controller: true, layers: [roomLayer, hallwayLayer, deskLayer, wallLayer, exteriorWallLayer], }); } // Execute everything getAccessToken(MAPPEDIN_KEY, MAPPEDIN_SECRET) .then(async token => { const zip = await downloadAndProcessVenue(token, MAP_ID); // Initialize visualization with the zip contents. await initVisualization(zip); }) .catch(error => console.error('Error:', error)); ``` ```ts title="src/utils.ts" import JSZip from 'jszip'; interface TokenResponse { access_token: string; expires_in: number; } interface VenueResponse { url: string; updated_at: string; } // Use the MappedIn API key & secret to get an access token. export async function getAccessToken(key: String, secret: String): Promise { // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const response = await fetch('https://app.mappedin.com/api/v1/api-key/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: key, secret: secret, }), }); const data: TokenResponse = await response.json(); return data.access_token; } // Load a file from a zip archive. export async function loadFileFromZip(zip: JSZip, path: string) { const file = zip.file(path); if (!file) throw new Error(`File not found in zip: ${path}`); const content = await file.async('text'); return JSON.parse(content); } // Download the MVFv3 zip file from the MappedIn API and process it. export async function downloadAndProcessVenue(accessToken: string, mapId: string): Promise { const venueResponse = await fetch( `https://app.mappedin.com/api/venue/${mapId}/mvf?version=3.0.0`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const venueData: VenueResponse = await venueResponse.json(); // Download the zip file const zipResponse = await fetch(venueData.url); const zipBuffer = await zipResponse.arrayBuffer(); // Process zip contents in memory const zip = new JSZip(); await zip.loadAsync(zipBuffer); return zip; } export function hexToRGB(hex: string): string { const rgb = hex.match(/[0-9a-f]{2}/gi)!.map(x => parseInt(x, 16)); return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.95)`; } ``` ```css title="src/style.css" body { background-color: darkseagreen; } ``` ## Get an Access Token The Mappedin API requires an access token to download the MVF bundle. This guide will use the Demo API key to get an access token from the API Key REST endpoint. ```ts // Use the MappedIn API key & secret to get an access token. export async function getAccessToken(key: String, secret: String): Promise { // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const response = await fetch('https://app.mappedin.com/api/v1/api-key/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: key, secret: secret, }), }); const data: TokenResponse = await response.json(); return data.access_token; } ``` ## Download the MVF Bundle The Get Venue MVF endpoint is called with the venue/map ID. The response contains a URL to the MVF v3 bundle, which is downloaded, loaded into memory, and passed to the `initVisualization` function. ```ts // Download the MVF v3 zip file from the MappedIn API and process it. export async function downloadAndProcessVenue(accessToken: string, mapId: string): Promise { const venueResponse = await fetch( `https://app.mappedin.com/api/venue/${mapId}/mvf?version=3.0.0`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const venueData: VenueResponse = await venueResponse.json(); // Download the zip file const zipResponse = await fetch(venueData.url); const zipBuffer = await zipResponse.arrayBuffer(); // Process zip contents in memory const zip = new JSZip(); await zip.loadAsync(zipBuffer); return zip; } ``` --- ## 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 v3 Specification. the `loadFileFromZip` function is used to load the GeoJSON files from the zip archive and return them as JSON objects. ```ts // Load a file from a zip archive. export async function loadFileFromZip(zip: JSZip, path: string) { const file = zip.file(path); if (!file) throw new Error(`File not found in zip: ${path}`); const content = await file.async('text'); return JSON.parse(content); } ``` For a basic 2D or 3D rendering, the following datasets are needed: - Manifest GeoJSON - Floors GeoJSON - Default Style JSON Some data - like geometry - 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. ```ts // Load the essential MVF data files from zip const manifestData = await loadFileFromZip(zip, 'manifest.geojson'); const styles = await loadFileFromZip(zip, 'default-style.json'); const floorData = await loadFileFromZip(zip, 'floors.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 geometryData = await loadFileFromZip(zip, `geometry/${floorId}.geojson`); ``` ## Rendering the Geometry The Floor dataset contains the geometry of the map, but it doesn't contain any properties which specify how they should be rendered. The MVF comes with a Default Style JSON file for this purpose. Open `default-style.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. ```ts const roomStyles = styles['Rooms']; const roomLayer = new GeoJsonLayer({ id: 'room-layer', data: geometryData.features.filter((f: Feature) => roomStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), getFillColor: hexToRGB(roomStyles.color), stroked: false, }); ``` Similarly, the same process is done for Hallways. ```ts const hallwayStyles = styles['Hallways']; const hallwayLayer = new GeoJsonLayer({ id: 'hallway-layer', data: geometryData.features.filter((f: Feature) => hallwayStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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. ```ts const wallStyles = styles['Walls']; const wallLayer = new GeoJsonLayer({ id: 'wall-layer', data: geometryData.features.filter((f: Feature) => wallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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: geometryData.features.filter((f: Feature) => exteriorWallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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: geometryData.features.filter((f: Feature) => deskStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .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, which is used to set 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. ```ts new Deck({ initialViewState: { longitude: manifestData.features[0].geometry.coordinates[0], latitude: manifestData.features[0].geometry.coordinates[1], zoom: 18.5, }, controller: true, layers: [roomLayer, hallwayLayer, deskLayer, wallLayer, exteriorWallLayer], }); ``` ## End Result The end result should be a 2D render of the MVF v3 in DeckGL. The following example implements this guide with the Dev Office Demo Map. ### Render MVF v3 with MapKit # Render MVF v3 with MapKit A GeoJSON renderer is required to display Mappedin Venue Format (MVF) data. For a full breakdown of the MVF v3 bundle, read the MVF v3 Specification. This guide demonstrates how to render MVF v3 data with MapKit, Apple's native mapping framework for iOS. Unlike MVF v2, which separates geometry into `space/` and `obstruction/` directories, MVF v3 combines all geometry into a single `geometry/{floorId}.geojson` file and uses a `default-style.json` file to define styles. *Mappedin indoor map rendered in MapKit on iOS* ## Project Setup This example uses a SwiftUI project with the ZIPFoundation package for extracting the MVF v3 zip bundle. The project consists of four files: - **RenderMFVv3MapKitApp.swift** — App entry point. - **ContentView.swift** — SwiftUI view that embeds the MKMapView. - **MapViewModel.swift** — Core logic for downloading, parsing, and rendering the MVF v3 bundle. - **UIColor+Hex.swift** — Extension to convert hex color strings to `UIColor`. 1. Create a new SwiftUI project in Xcode. 2. Add the ZIPFoundation package dependency via **File > Add Package Dependencies**. ## Get an Access Token The Mappedin API requires an access token to download the MVF bundle. This guide uses the Demo API key to get an access token from the API Key REST endpoint. ```swift struct TokenResponse: Codable { let accessToken: String let expiresIn: Int enum CodingKeys: String, CodingKey { case accessToken = "access_token" case expiresIn = "expires_in" } } // See Demo API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps private let apiKey = "mik_yeBk0Vf0nNJtpesfu560e07e5" private let apiSecret = "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022" private func getAccessToken() async throws -> String { let url = URL(string: "https://app.mappedin.com/api/v1/api-key/token")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: String] = [ "key": apiKey, "secret": apiSecret ] request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(TokenResponse.self, from: data) return response.accessToken } ``` ## Download the MVF v3 Bundle The Get Venue MVF endpoint is called with the map ID and the `version=3.0.0` query parameter to request the MVF v3 format. The response contains a URL to the MVF v3 bundle, which is downloaded and saved to a temporary file for extraction. ```swift struct VenueResponse: Codable { let url: String let updatedAt: String enum CodingKeys: String, CodingKey { case url case updatedAt = "updated_at" } } private let mapId = "64ef49e662fd90fe020bee61" private func downloadMVFBundle(accessToken: String) async throws -> URL { let url = URL(string: "https://app.mappedin.com/api/venue/\(mapId)/mvf?version=3.0.0")! var request = URLRequest(url: url) request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let venueResponse = try JSONDecoder().decode(VenueResponse.self, from: data) let zipUrl = URL(string: venueResponse.url)! let (zipData, _) = try await URLSession.shared.data(from: zipUrl) let tempDir = FileManager.default.temporaryDirectory let zipPath = tempDir.appendingPathComponent("venue_mvfv3.zip") try zipData.write(to: zipPath) return zipPath } ``` ## 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 v3 Specification. The `loadFileFromZip` function extracts individual files from the zip archive and returns them as raw `Data`. ```swift private func loadFileFromZip(archive: Archive, path: String) throws -> Data { guard let entry = archive[path] else { throw NSError( domain: "ZipError", code: 2, userInfo: [NSLocalizedDescriptionKey: "File not found in zip: \(path)"]) } var data = Data() _ = try archive.extract(entry) { chunk in data.append(chunk) } return data } ``` For a basic 2D rendering, the following datasets are needed: - Manifest GeoJSON — Contains the center coordinate of the venue. - Floors GeoJSON — Lists all floors with their IDs and elevations. - Default Style JSON — Named style groups that map geometry IDs to visual properties. Some data, like geometry, is divided by floor ID. Using the desired elevation, the floor ID can be pulled from `floors.geojson`. Then, the corresponding geometry file `geometry/{floorId}.geojson` is loaded. ```swift private func processZipFile(at path: URL, mapView: MKMapView) async throws { let archive: Archive do { archive = try Archive(url: path, accessMode: .read) } catch { throw NSError( domain: "VenueError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to read ZIP archive: \(error.localizedDescription)"]) } let manifestData = try loadFileFromZip(archive: archive, path: "manifest.geojson") let stylesData = try loadFileFromZip(archive: archive, path: "default-style.json") let floorData = try loadFileFromZip(archive: archive, path: "floors.geojson") let floorJson = try JSONSerialization.jsonObject(with: floorData) as! [String: Any] let floorFeatures = floorJson["features"] as! [[String: Any]] var floorId: String = "" for feature in floorFeatures { let properties = feature["properties"] as! [String: Any] if let floorElevation = properties["elevation"] as? Int, floorElevation == elevation { floorId = properties["id"] as! String break } } guard !floorId.isEmpty else { throw NSError( domain: "VenueError", code: 3, userInfo: [NSLocalizedDescriptionKey: "No floor found for elevation \(elevation)"]) } let geometryData = try loadFileFromZip(archive: archive, path: "geometry/\(floorId).geojson") try await initVisualization( manifest: manifestData, styles: stylesData, geometryData: geometryData, mapView: mapView ) } ``` ## Parse Styles The MVF v3 bundle contains a `default-style.json` file that defines named style groups. Each group has a `color` property for polygon fills, a `buffer` property for line widths, and a `geometryAnchors` array that links geometry IDs to the style. Style group names are descriptors such as "Rooms", "Walls", "Hallways", "ExteriorWalls", and "Desks". An `OverlayStyle` struct holds the resolved visual properties for each geometry. ```swift struct OverlayStyle { let fillColor: UIColor let strokeColor: UIColor let lineWidth: CGFloat } ``` The `buildStyleLookup` function iterates over all style groups and creates a reverse lookup table mapping each geometry ID to its `OverlayStyle`. Groups with a `color` property represent polygon fills (rooms, hallways, desks), while groups with a `buffer` property represent line geometry (walls, exterior walls). ```swift var styleLookup: [String: OverlayStyle] = [:] private func buildStyleLookup(from stylesDict: [String: Any]) { for (groupName, value) in stylesDict { guard let group = value as? [String: Any], let anchors = group["geometryAnchors"] as? [[String: Any]] else { continue } let color = group["color"] as? String let buffer = group["buffer"] as? Double let style: OverlayStyle if let color = color, buffer != nil { style = OverlayStyle( fillColor: UIColor(hex: color).withAlphaComponent(0.95), strokeColor: .clear, lineWidth: CGFloat(buffer ?? 1.0) ) } else if let buffer = buffer { let strokeHex: String let widthMultiplier: Double if groupName.lowercased().contains("exterior") { strokeHex = "#acacac" widthMultiplier = 2.0 } else { strokeHex = "#b2b2b2" widthMultiplier = 1.5 } style = OverlayStyle( fillColor: .clear, strokeColor: UIColor(hex: strokeHex), lineWidth: CGFloat(buffer * widthMultiplier) ) } else if let color = color { style = OverlayStyle( fillColor: UIColor(hex: color).withAlphaComponent(0.95), strokeColor: .clear, lineWidth: 0 ) } else { style = OverlayStyle( fillColor: UIColor(hex: "#f5f5f5"), strokeColor: .clear, lineWidth: 0 ) } for anchor in anchors { if let geometryId = anchor["geometryId"] as? String { styleLookup[geometryId] = style } } } } ``` ## Create the Geometry The `manifest.geojson` file contains the center coordinate of the venue. This is used to center the MKMapView on the indoor map. GeoJSON geometry is decoded using `MKGeoJSONDecoder`, which converts features into the appropriate MapKit types: `MKPolygon` and `MKMultiPolygon` for filled shapes, `MKPolyline` and `MKMultiPolyline` for line shapes. Each overlay's `title` property stores the geometry ID for style lookup during rendering. ```swift private func initVisualization( manifest: Data, styles: Data, geometryData: Data, mapView: MKMapView ) async throws { let manifestJson = try JSONSerialization.jsonObject(with: manifest) as! [String: Any] let features = (manifestJson["features"] as! [[String: Any]])[0] let geometry = features["geometry"] as! [String: Any] let coordinates = geometry["coordinates"] as! [Double] let center = CLLocationCoordinate2D(latitude: coordinates[1], longitude: coordinates[0]) await MainActor.run { let region = MKCoordinateRegion( center: center, latitudinalMeters: 100, longitudinalMeters: 100 ) mapView.setRegion(region, animated: false) } guard let stylesDict = try JSONSerialization.jsonObject(with: styles) as? [String: Any] else { return } buildStyleLookup(from: stylesDict) let decoder = MKGeoJSONDecoder() guard let geoFeatures = try decoder.decode(geometryData) as? [MKGeoJSONFeature] else { return } await MainActor.run { for feature in geoFeatures { guard let featureData = feature.properties, let properties = try? JSONSerialization.jsonObject(with: featureData) as? [String: Any], let geometryId = properties["id"] as? String else { continue } for geo in feature.geometry { if let polygon = geo as? MKPolygon { polygon.title = geometryId mapView.addOverlay(polygon) } else if let multiPolygon = geo as? MKMultiPolygon { multiPolygon.title = geometryId mapView.addOverlay(multiPolygon) } else if let polyline = geo as? MKPolyline { polyline.title = geometryId mapView.addOverlay(polyline) } else if let multiPolyline = geo as? MKMultiPolyline { multiPolyline.title = geometryId mapView.addOverlay(multiPolyline) } } } } } ``` ## Render the Map The geometry is rendered using the `MKMapViewDelegate` method `rendererFor overlay:`. Each overlay's `title` property contains the geometry ID, which is used to look up the `OverlayStyle` from the `styleLookup` dictionary. The style determines the fill color, stroke color, and line width for each overlay. ```swift func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { let style = (overlay.title ?? nil).flatMap { viewModel.styleLookup[$0] } if let polygon = overlay as? MKPolygon { let renderer = MKPolygonRenderer(polygon: polygon) if let style = style { renderer.fillColor = style.fillColor renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.fillColor = UIColor(hex: "#f5f5f5") } return renderer } else if let multiPolygon = overlay as? MKMultiPolygon { let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygon) if let style = style { renderer.fillColor = style.fillColor renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.fillColor = UIColor(hex: "#f5f5f5") } return renderer } else if let polyline = overlay as? MKPolyline { let renderer = MKPolylineRenderer(polyline: polyline) if let style = style { renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.strokeColor = UIColor(hex: "#dddddd") renderer.lineWidth = 2.0 } return renderer } else if let multiPolyline = overlay as? MKMultiPolyline { let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolyline) if let style = style { renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.strokeColor = UIColor(hex: "#dddddd") renderer.lineWidth = 2.0 } return renderer } return MKOverlayRenderer(overlay: overlay) } ``` ## Color Conversion The `UIColor` extension converts hex color strings from `default-style.json` to `UIColor` objects for use with MapKit overlay renderers. ```swift extension UIColor { convenience init(hex: String) { var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() if hexString.hasPrefix("#") { hexString.remove(at: hexString.startIndex) } else if hexString.hasPrefix("0X") { hexString.removeSubrange( hexString.startIndex..> 16) / 255.0 let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 let blue = CGFloat(rgb & 0x0000FF) / 255.0 self.init(red: red, green: green, blue: blue, alpha: 1.0) } } ``` ## SwiftUI Integration Since this example uses SwiftUI, MKMapView is wrapped in a `UIViewRepresentable`. The `Coordinator` class acts as the `MKMapViewDelegate` and handles the `rendererFor overlay:` callback described above. ```swift struct MapViewRepresentable: UIViewRepresentable { @ObservedObject var viewModel: MapViewModel func makeCoordinator() -> Coordinator { Coordinator(viewModel: viewModel) } func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator Task { await viewModel.loadVenue(mapView: mapView) } return mapView } func updateUIView(_ uiView: MKMapView, context: Context) {} class Coordinator: NSObject, MKMapViewDelegate { let viewModel: MapViewModel init(viewModel: MapViewModel) { self.viewModel = viewModel } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { // Style lookup implementation as described in "Render the Map" above } } } ``` The `ContentView` embeds the `MapViewRepresentable` and displays loading and error states. ```swift struct ContentView: View { @StateObject private var viewModel = MapViewModel() var body: some View { ZStack { MapViewRepresentable(viewModel: viewModel) .ignoresSafeArea() if viewModel.isLoading { ProgressView("Loading venue...") .padding() .background(.ultraThinMaterial) .cornerRadius(10) } if let error = viewModel.errorMessage { VStack { Spacer() Text("Error: \(error)") .foregroundColor(.white) .padding() .background(Color.red.opacity(0.8)) .cornerRadius(8) .padding() } } } } } ``` ## Complete Example The complete source code for this example is available on GitHub. The files below contain all of the code described in this guide. ### MapViewModel.swift ```swift import Foundation import MapKit import ZIPFoundation struct TokenResponse: Codable { let accessToken: String let expiresIn: Int enum CodingKeys: String, CodingKey { case accessToken = "access_token" case expiresIn = "expires_in" } } struct VenueResponse: Codable { let url: String let updatedAt: String enum CodingKeys: String, CodingKey { case url case updatedAt = "updated_at" } } struct OverlayStyle { let fillColor: UIColor let strokeColor: UIColor let lineWidth: CGFloat } class MapViewModel: ObservableObject { // See Demo API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps private let apiKey = "mik_yeBk0Vf0nNJtpesfu560e07e5" private let apiSecret = "mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022" private let mapId = "64ef49e662fd90fe020bee61" private let elevation: Int = 0 var styleLookup: [String: OverlayStyle] = [:] @Published var isLoading = true @Published var errorMessage: String? func loadVenue(mapView: MKMapView) async { do { let token = try await getAccessToken() let archiveURL = try await downloadMVFBundle(accessToken: token) try await processZipFile(at: archiveURL, mapView: mapView) await MainActor.run { self.isLoading = false } } catch { await MainActor.run { self.errorMessage = error.localizedDescription self.isLoading = false } print("Error loading venue: \(error)") } } private func getAccessToken() async throws -> String { let url = URL(string: "https://app.mappedin.com/api/v1/api-key/token")! var request = URLRequest(url: url) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: String] = [ "key": apiKey, "secret": apiSecret ] request.httpBody = try JSONSerialization.data(withJSONObject: body) let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(TokenResponse.self, from: data) return response.accessToken } private func downloadMVFBundle(accessToken: String) async throws -> URL { let url = URL(string: "https://app.mappedin.com/api/venue/\(mapId)/mvf?version=3.0.0")! var request = URLRequest(url: url) request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let venueResponse = try JSONDecoder().decode(VenueResponse.self, from: data) let zipUrl = URL(string: venueResponse.url)! let (zipData, _) = try await URLSession.shared.data(from: zipUrl) let tempDir = FileManager.default.temporaryDirectory let zipPath = tempDir.appendingPathComponent("venue_mvfv3.zip") try zipData.write(to: zipPath) return zipPath } private func processZipFile(at path: URL, mapView: MKMapView) async throws { let archive: Archive do { archive = try Archive(url: path, accessMode: .read) } catch { throw NSError( domain: "VenueError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Unable to read ZIP archive: \(error.localizedDescription)"]) } let manifestData = try loadFileFromZip(archive: archive, path: "manifest.geojson") let stylesData = try loadFileFromZip(archive: archive, path: "default-style.json") let floorData = try loadFileFromZip(archive: archive, path: "floors.geojson") let floorJson = try JSONSerialization.jsonObject(with: floorData) as! [String: Any] let floorFeatures = floorJson["features"] as! [[String: Any]] var floorId: String = "" for feature in floorFeatures { let properties = feature["properties"] as! [String: Any] if let floorElevation = properties["elevation"] as? Int, floorElevation == elevation { floorId = properties["id"] as! String break } } guard !floorId.isEmpty else { throw NSError( domain: "VenueError", code: 3, userInfo: [NSLocalizedDescriptionKey: "No floor found for elevation \(elevation)"]) } let geometryData = try loadFileFromZip(archive: archive, path: "geometry/\(floorId).geojson") try await initVisualization( manifest: manifestData, styles: stylesData, geometryData: geometryData, mapView: mapView ) } private func loadFileFromZip(archive: Archive, path: String) throws -> Data { guard let entry = archive[path] else { throw NSError( domain: "ZipError", code: 2, userInfo: [NSLocalizedDescriptionKey: "File not found in zip: \(path)"]) } var data = Data() _ = try archive.extract(entry) { chunk in data.append(chunk) } return data } private func initVisualization( manifest: Data, styles: Data, geometryData: Data, mapView: MKMapView ) async throws { let manifestJson = try JSONSerialization.jsonObject(with: manifest) as! [String: Any] let features = (manifestJson["features"] as! [[String: Any]])[0] let geometry = features["geometry"] as! [String: Any] let coordinates = geometry["coordinates"] as! [Double] let center = CLLocationCoordinate2D(latitude: coordinates[1], longitude: coordinates[0]) await MainActor.run { let region = MKCoordinateRegion( center: center, latitudinalMeters: 100, longitudinalMeters: 100 ) mapView.setRegion(region, animated: false) } guard let stylesDict = try JSONSerialization.jsonObject(with: styles) as? [String: Any] else { return } buildStyleLookup(from: stylesDict) let decoder = MKGeoJSONDecoder() guard let geoFeatures = try decoder.decode(geometryData) as? [MKGeoJSONFeature] else { return } await MainActor.run { for feature in geoFeatures { guard let featureData = feature.properties, let properties = try? JSONSerialization.jsonObject(with: featureData) as? [String: Any], let geometryId = properties["id"] as? String else { continue } for geo in feature.geometry { if let polygon = geo as? MKPolygon { polygon.title = geometryId mapView.addOverlay(polygon) } else if let multiPolygon = geo as? MKMultiPolygon { multiPolygon.title = geometryId mapView.addOverlay(multiPolygon) } else if let polyline = geo as? MKPolyline { polyline.title = geometryId mapView.addOverlay(polyline) } else if let multiPolyline = geo as? MKMultiPolyline { multiPolyline.title = geometryId mapView.addOverlay(multiPolyline) } } } } } private func buildStyleLookup(from stylesDict: [String: Any]) { for (groupName, value) in stylesDict { guard let group = value as? [String: Any], let anchors = group["geometryAnchors"] as? [[String: Any]] else { continue } let color = group["color"] as? String let buffer = group["buffer"] as? Double let style: OverlayStyle if let color = color, buffer != nil { style = OverlayStyle( fillColor: UIColor(hex: color).withAlphaComponent(0.95), strokeColor: .clear, lineWidth: CGFloat(buffer ?? 1.0) ) } else if let buffer = buffer { let strokeHex: String let widthMultiplier: Double if groupName.lowercased().contains("exterior") { strokeHex = "#acacac" widthMultiplier = 2.0 } else { strokeHex = "#b2b2b2" widthMultiplier = 1.5 } style = OverlayStyle( fillColor: .clear, strokeColor: UIColor(hex: strokeHex), lineWidth: CGFloat(buffer * widthMultiplier) ) } else if let color = color { style = OverlayStyle( fillColor: UIColor(hex: color).withAlphaComponent(0.95), strokeColor: .clear, lineWidth: 0 ) } else { style = OverlayStyle( fillColor: UIColor(hex: "#f5f5f5"), strokeColor: .clear, lineWidth: 0 ) } for anchor in anchors { if let geometryId = anchor["geometryId"] as? String { styleLookup[geometryId] = style } } } } } ``` ### ContentView.swift ```swift import MapKit import SwiftUI struct ContentView: View { @StateObject private var viewModel = MapViewModel() var body: some View { ZStack { MapViewRepresentable(viewModel: viewModel) .ignoresSafeArea() if viewModel.isLoading { ProgressView("Loading venue...") .padding() .background(.ultraThinMaterial) .cornerRadius(10) } if let error = viewModel.errorMessage { VStack { Spacer() Text("Error: \(error)") .foregroundColor(.white) .padding() .background(Color.red.opacity(0.8)) .cornerRadius(8) .padding() } } } } } struct MapViewRepresentable: UIViewRepresentable { @ObservedObject var viewModel: MapViewModel func makeCoordinator() -> Coordinator { Coordinator(viewModel: viewModel) } func makeUIView(context: Context) -> MKMapView { let mapView = MKMapView() mapView.delegate = context.coordinator Task { await viewModel.loadVenue(mapView: mapView) } return mapView } func updateUIView(_ uiView: MKMapView, context: Context) {} class Coordinator: NSObject, MKMapViewDelegate { let viewModel: MapViewModel init(viewModel: MapViewModel) { self.viewModel = viewModel } func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { let style = (overlay.title ?? nil).flatMap { viewModel.styleLookup[$0] } if let polygon = overlay as? MKPolygon { let renderer = MKPolygonRenderer(polygon: polygon) if let style = style { renderer.fillColor = style.fillColor renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.fillColor = UIColor(hex: "#f5f5f5") } return renderer } else if let multiPolygon = overlay as? MKMultiPolygon { let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygon) if let style = style { renderer.fillColor = style.fillColor renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.fillColor = UIColor(hex: "#f5f5f5") } return renderer } else if let polyline = overlay as? MKPolyline { let renderer = MKPolylineRenderer(polyline: polyline) if let style = style { renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.strokeColor = UIColor(hex: "#dddddd") renderer.lineWidth = 2.0 } return renderer } else if let multiPolyline = overlay as? MKMultiPolyline { let renderer = MKMultiPolylineRenderer(multiPolyline: multiPolyline) if let style = style { renderer.strokeColor = style.strokeColor renderer.lineWidth = style.lineWidth } else { renderer.strokeColor = UIColor(hex: "#dddddd") renderer.lineWidth = 2.0 } return renderer } return MKOverlayRenderer(overlay: overlay) } } } ``` ### UIColor+Hex.swift ```swift import UIKit extension UIColor { convenience init(hex: String) { var hexString = hex.trimmingCharacters(in: .whitespacesAndNewlines).uppercased() if hexString.hasPrefix("#") { hexString.remove(at: hexString.startIndex) } else if hexString.hasPrefix("0X") { hexString.removeSubrange( hexString.startIndex..> 16) / 255.0 let green = CGFloat((rgb & 0x00FF00) >> 8) / 255.0 let blue = CGFloat(rgb & 0x0000FF) / 255.0 self.init(red: red, green: green, blue: blue, alpha: 1.0) } } ``` ### RenderMFVv3MapKitApp.swift ```swift import SwiftUI @main struct RenderMFVv3MapKitApp: App { var body: some Scene { WindowGroup { ContentView() } } } ``` ### Render MVF v3 with Mapkit JS # Render MVF v3 with Mapkit JS A GeoJSON renderer is required to display MVF data. For a full breakdown of the Mappedin Venue Format v3 (MVFv3) bundle, read the MVF v3 Specification. This guide will demonstrate how to get started using MapKit JS, Apple's JavaScript library for rendering maps. A screenshot of the result is shown below. Jump straight to the End Result to try out the live interactive map. !Mapkit JS Image ## Project Setup 1. Start a new vanilla Vite project using TypeScript. ```sh yarn create vite mappedin-mvf-guide ``` From the setup menu, select `Vanilla`, then `TypeScript`. ```sh cd mappedin-mvf-guide ``` 2. Install deck.gl and related packages. ```sh yarn add @types/geojson jszip geojson @turf/buffer @types/adm-zip ``` ## 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. Create a `utils.ts` and `mapkit.d.ts` file in the `src` directory copy the code below into these files. The rest of this guide will break down and explain the code. ```ts title="src/main.ts" import { Feature } from 'geojson'; import JSZip from 'jszip'; import './styles.css'; import { downloadAndProcessVenue, getAccessToken, hexToRGB, loadFileFromZip } from './utils'; //The Mappedin Map ID to load. const MAP_ID = '64ef49e662fd90fe020bee61'; // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const MAPPEDIN_KEY = 'mik_yeBk0Vf0nNJtpesfu560e07e5'; const MAPPEDIN_SECRET = 'mis_2g9ST8ZcSFb5R9fPnsvYhrX3RyRwPtDGbMGweCYKEq385431022'; // Replace this with your Mapkit JWT token. This token is whitelisted for this domain only. const MAPKIT_TOKEN = 'eyJraWQiOiJDUTNLUTNLNURGIiwidHlwIjoiSldUIiwiYWxnIjoiRVMyNTYifQ.eyJpc3MiOiJINkMyOTI0UUE1IiwiaWF0IjoxNzQ4NTYyNTM4LCJvcmlnaW4iOiI1MnE5OWYuY3NiLmFwcCJ9.zWH1DI_sM8HhMETQkASSRxr77Uv80d6ND8LGilhdP69oI-l-0RA7AlUfBknlV0YOj8aTpb1BYgdztDpSchclMw'; // Initialize MapKit JS with your JWT token. mapkit.init({ authorizationCallback: function (done) { done(MAPKIT_TOKEN); }, }); const ELEVATION = 0; // The floor elevation to be displayed. // Helper function to create MapKit overlays from GeoJSON features function createGeoJSONOverlay( features: Feature[], style: { strokeColor: string; fillColor: string; lineWidth: number; } ): mapkit.Overlay[] { return features .map(feature => { if (feature.geometry.type === 'Polygon') { const coordinates = feature.geometry.coordinates[0].map( coord => new mapkit.Coordinate(coord[1], coord[0]) ); const polygon = new mapkit.PolygonOverlay(coordinates); polygon.style = new mapkit.Style({ strokeColor: style.strokeColor, fillColor: style.fillColor, lineWidth: style.lineWidth, strokeOpacity: 1.0, fillOpacity: 0.95, }); return polygon; } else if (feature.geometry.type === 'LineString') { const coordinates = feature.geometry.coordinates.map( coord => new mapkit.Coordinate(coord[1], coord[0]) ); const polyline = new mapkit.PolylineOverlay(coordinates); polyline.style = new mapkit.Style({ strokeColor: style.strokeColor, lineWidth: style.lineWidth * 2, strokeOpacity: 1.0, }); return polyline; } // Return null for unsupported geometry types console.warn('Unsupported geometry type:', feature.geometry.type); return null; }) .filter(overlay => overlay !== null) as mapkit.Overlay[]; } // Initialize the visualization with the GoeJSON files in the MVFv3 zip. async function initVisualization(zip: JSZip) { // Load the essential MVF data files from zip const manifestData = await loadFileFromZip(zip, 'manifest.geojson'); const styles = await loadFileFromZip(zip, 'default-style.json'); const floorData = await loadFileFromZip(zip, 'floors.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 geometryData = await loadFileFromZip(zip, `geometry/${floorId}.geojson`); // Initialize MapKit JS map const map = new mapkit.Map('map', { center: new mapkit.Coordinate( manifestData.features[0].geometry.coordinates[1], manifestData.features[0].geometry.coordinates[0] ), cameraZoomRange: new mapkit.CameraZoomRange(15, 500), showsMapTypeControl: true, showsZoomControl: true, }); // Set map features visibility map.showsScale = mapkit.FeatureVisibility.Visible; // Set initial zoom level using camera distance map.setCameraDistanceAnimated(150); // Create overlay styles for each layer const roomStyles = styles['Rooms']; const roomOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => roomStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(roomStyles.color), lineWidth: 0, } ); const hallwayStyles = styles['Hallways']; const hallwayOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => hallwayStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(hallwayStyles.color), lineWidth: 0, } ); // Modify the wall styles to be more visible. const wallStyles = styles['Walls']; const wallOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => wallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: '#b2b2b2', lineWidth: wallStyles.buffer * 1.5, fillColor: 'transparent', } ); // Modify the exterior wall styles to be more visible. const exteriorWallStyles = styles['ExteriorWalls']; const exteriorWallOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => exteriorWallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: '#acacac', lineWidth: exteriorWallStyles.buffer * 2, fillColor: 'transparent', } ); const deskStyles = styles['Desks']; const deskOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => deskStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(deskStyles.color), lineWidth: 0, } ); // Add all overlays to the map roomOverlays.forEach(overlay => map.addOverlay(overlay)); hallwayOverlays.forEach(overlay => map.addOverlay(overlay)); wallOverlays.forEach(overlay => map.addOverlay(overlay)); exteriorWallOverlays.forEach(overlay => map.addOverlay(overlay)); deskOverlays.forEach(overlay => map.addOverlay(overlay)); } // Execute everything getAccessToken(MAPPEDIN_KEY, MAPPEDIN_SECRET) .then(async token => { //Download the MVF Bundle. const zip = await downloadAndProcessVenue(token, MAP_ID); // Initialize visualization with the zip contents. await initVisualization(zip); }) .catch(error => console.error('Error:', error)); ``` ```ts title="src/utils.ts" import JSZip from 'jszip'; interface TokenResponse { access_token: string; expires_in: number; } interface VenueResponse { url: string; updated_at: string; } // Use the MappedIn API key & secret to get an access token. export async function getAccessToken(key: String, secret: String): Promise { // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const response = await fetch('https://app.mappedin.com/api/v1/api-key/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: key, secret: secret, }), }); const data: TokenResponse = await response.json(); return data.access_token; } // Load a file from a zip archive. export async function loadFileFromZip(zip: JSZip, path: string) { const file = zip.file(path); if (!file) throw new Error(`File not found in zip: ${path}`); const content = await file.async('text'); return JSON.parse(content); } // Download the MVFv3 zip file from the MappedIn API and process it. export async function downloadAndProcessVenue(accessToken: string, mapId: string): Promise { const venueResponse = await fetch( `https://app.mappedin.com/api/venue/${mapId}/mvf?version=3.0.0`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const venueData: VenueResponse = await venueResponse.json(); // Download the zip file const zipResponse = await fetch(venueData.url); const zipBuffer = await zipResponse.arrayBuffer(); // Process zip contents in memory const zip = new JSZip(); await zip.loadAsync(zipBuffer); return zip; } export function hexToRGB(hex: string): string { const rgb = hex.match(/[0-9a-f]{2}/gi)!.map(x => parseInt(x, 16)); return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.95)`; } ``` ```ts title="src/mapkit.d.ts" declare var mapkit: any; ``` ```css title="src/style.css" :root { font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; line-height: 1.5; font-weight: 400; color-scheme: light dark; color: rgba(255, 255, 255, 0.87); background-color: #242424; font-synthesis: none; text-rendering: optimizeLegibility; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } a { font-weight: 500; color: #646cff; text-decoration: inherit; } a:hover { color: #535bf2; } body { margin: 0; display: flex; place-items: center; min-width: 320px; min-height: 100vh; } h1 { font-size: 3.2em; line-height: 1.1; } #app { max-width: 1280px; margin: 0 auto; padding: 2rem; text-align: center; } .logo { height: 6em; padding: 1.5em; will-change: filter; transition: filter 300ms; } .logo:hover { filter: drop-shadow(0 0 2em #646cffaa); } .logo.vanilla:hover { filter: drop-shadow(0 0 2em #3178c6aa); } .card { padding: 2em; } .read-the-docs { color: #888; } button { border-radius: 8px; border: 1px solid transparent; padding: 0.6em 1.2em; font-size: 1em; font-weight: 500; font-family: inherit; background-color: #1a1a1a; cursor: pointer; transition: border-color 0.25s; } button:hover { border-color: #646cff; } button:focus, button:focus-visible { outline: 4px auto -webkit-focus-ring-color; } @media (prefers-color-scheme: light) { :root { color: #213547; background-color: #ffffff; } a:hover { color: #747bff; } button { background-color: #f9f9f9; } } ``` ## Get an Access Token The Mappedin API requires an access token to download the MVF bundle. This guide will use the Demo API key to get an access token from the API Key REST endpoint. ```ts // Use the MappedIn API key & secret to get an access token. export async function getAccessToken(key: String, secret: String): Promise { // See Trial API key Terms and Conditions // https://developer.mappedin.com/docs/demo-keys-and-maps const response = await fetch('https://app.mappedin.com/api/v1/api-key/token', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ key: key, secret: secret, }), }); const data: TokenResponse = await response.json(); return data.access_token; } ``` ## Download the MVF Bundle The Get Venue MVF endpoint is called with the venue/map ID. The response contains a URL to the MVF v3 bundle, which is downloaded, loaded into memory, and passed to the `initVisualization` function. ```ts // Download the MVF v3 zip file from the MappedIn API and process it. export async function downloadAndProcessVenue(accessToken: string, mapId: string): Promise { const venueResponse = await fetch( `https://app.mappedin.com/api/venue/${mapId}/mvf?version=3.0.0`, { headers: { Authorization: `Bearer ${accessToken}`, }, } ); const venueData: VenueResponse = await venueResponse.json(); // Download the zip file const zipResponse = await fetch(venueData.url); const zipBuffer = await zipResponse.arrayBuffer(); // Process zip contents in memory const zip = new JSZip(); await zip.loadAsync(zipBuffer); return zip; } ``` --- ## 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 v3 Specification. the `loadFileFromZip` function is used to load the GeoJSON files from the zip archive and return them as JSON objects. ```ts // Load a file from a zip archive. export async function loadFileFromZip(zip: JSZip, path: string) { const file = zip.file(path); if (!file) throw new Error(`File not found in zip: ${path}`); const content = await file.async('text'); return JSON.parse(content); } ``` For a basic 2D or 3D rendering, the following datasets are needed: - Manifest GeoJSON - Floors GeoJSON - Default Style JSON Some data - like geometry - 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. ```ts // Load the essential MVF data files from zip const manifestData = await loadFileFromZip(zip, 'manifest.geojson'); const styles = await loadFileFromZip(zip, 'default-style.json'); const floorData = await loadFileFromZip(zip, 'floors.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 geometryData = await loadFileFromZip(zip, `geometry/${floorId}.geojson`); ``` ## Initialize the Mapkit JS Map The `initVisualization` function is called with the zip contents. The first step is to initialize the Mapkit JS map, which provides the outdoor map view and will be used to render the GeoJSON data from the MVF bundle. The Manifest GeoJSON contains the center coordinate of the indoor map, which is used to set the MapKit JS `center` property. ```ts // Initialize MapKit JS map const map = new mapkit.Map('map', { center: new mapkit.Coordinate( manifestData.features[0].geometry.coordinates[1], manifestData.features[0].geometry.coordinates[0] ), cameraZoomRange: new mapkit.CameraZoomRange(15, 500), showsMapTypeControl: true, showsZoomControl: true, }); // Set map features visibility map.showsScale = mapkit.FeatureVisibility.Visible; // Set initial zoom level using camera distance map.setCameraDistanceAnimated(75); ``` Mapkit JS uses a `Style` object to define the appearance of the map. The `hexToRGB` function is used to convert the hex color code from the Styles JSON to an RGBA value. ```ts export function hexToRGB(hex: string): string { const rgb = hex.match(/[0-9a-f]{2}/gi)!.map(x => parseInt(x, 16)); return `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.95)`; } ``` ## Rendering the Geometry The `createGeoJSONOverlay` function is used to create Mapkit overlays from the GeoJSON features. This function is used for both the Space and Obstruction datasets. It accepts an array of GeoJSON features and a style object, and returns an array of Mapkit overlays. ```ts // Helper function to create MapKit overlays from GeoJSON features function createGeoJSONOverlay( features: Feature[], style: { strokeColor: string; fillColor: string; lineWidth: number; } ): mapkit.Overlay[] { return features .map(feature => { if (feature.geometry.type === 'Polygon') { const coordinates = feature.geometry.coordinates[0].map( coord => new mapkit.Coordinate(coord[1], coord[0]) ); const polygon = new mapkit.PolygonOverlay(coordinates); polygon.style = new mapkit.Style({ strokeColor: style.strokeColor, fillColor: style.fillColor, lineWidth: style.lineWidth, strokeOpacity: 1.0, fillOpacity: 0.95, }); return polygon; } else if (feature.geometry.type === 'LineString') { const coordinates = feature.geometry.coordinates.map( coord => new mapkit.Coordinate(coord[1], coord[0]) ); const polyline = new mapkit.PolylineOverlay(coordinates); polyline.style = new mapkit.Style({ strokeColor: style.strokeColor, lineWidth: style.lineWidth * 2, strokeOpacity: 1.0, }); return polyline; } // Return null for unsupported geometry types console.warn('Unsupported geometry type:', feature.geometry.type); return null; }) .filter(overlay => overlay !== null) as mapkit.Overlay[]; } ``` The Floor dataset contains the geometry of the map, but it doesn't contain any properties which specify how they should be rendered. The MVF comes with a Default Style JSON file for this purpose. Open `default-style.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 Mapkit Overlays for rooms using the `createGeoJSONOverlay` function described above. 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 `fillColor` in Mapkit JS. ```ts const roomStyles = styles['Rooms']; const roomOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => roomStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(roomStyles.color), lineWidth: 0, } ); ``` Similarly, the same process is done for Hallways. ```ts const hallwayStyles = styles['Hallways']; const hallwayOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => hallwayStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(hallwayStyles.color), lineWidth: 0, } ); ``` There are 3 main Styles for Obstruction GeoJSON data, `Walls`, `ExteriorWalls`, and `Desks`. Note that Walls and ExteriorWalls are `lineStrings` and Desks are `polygons`. The style of walls is modified to make them more visible. In this example the `strokeColor` is set to a gray shade and the `lineWidth` thickness is increased. ```ts // Modify the wall styles to be more visible. const wallStyles = styles['Walls']; const wallOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => wallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: '#b2b2b2', lineWidth: wallStyles.buffer * 1.5, fillColor: 'transparent', } ); // Modify the exterior wall styles to be more visible. const exteriorWallStyles = styles['ExteriorWalls']; const exteriorWallOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => exteriorWallStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: '#acacac', lineWidth: exteriorWallStyles.buffer * 2, fillColor: 'transparent', } ); const deskStyles = styles['Desks']; const deskOverlays = createGeoJSONOverlay( geometryData.features.filter((f: Feature) => deskStyles.geometryAnchors .map((anchor: { floorId: string; geometryId: string }) => anchor.geometryId) .includes(f.properties!.id) ), { strokeColor: 'transparent', fillColor: hexToRGB(deskStyles.color), lineWidth: 0, } ); ``` Finally, add all of the overlays to the map. 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. ```ts // Add all overlays to the map roomOverlays.forEach(overlay => map.addOverlay(overlay)); hallwayOverlays.forEach(overlay => map.addOverlay(overlay)); wallOverlays.forEach(overlay => map.addOverlay(overlay)); exteriorWallOverlays.forEach(overlay => map.addOverlay(overlay)); deskOverlays.forEach(overlay => map.addOverlay(overlay)); ``` ## End Result The end result should be a 2D render of the MVF v3 in DeckGL. The following example implements this guide with the Dev Office Demo Map. ## mvf-v3-specification ### @mappedin/mvf-address # @mappedin/mvf-address The address extension for MVF. Allows specifying an address for the entire bundle, as well as addresses for individual floor stacks. An `Address` object is an object with keys representing different `type`s of address data, and values that contain the data in that format. As of right now, only one schema is supported: the `display` type. Addresses of the `display` type have a single `displayAddress` property, which is a formatted string that can be displayed directly to a user. Future versions of this extension may add new types of structured address data, on new properties of the Address type. Definition: ```ts export type Address = { display?: { displayAddress?: string } } export type AddressExtension = { primary?: Address, floorStack?: Record } ``` Example: ```json { address: { primary: { display: { displayAddress: "5788 Wunsch Cliffs, Iristown, IA 75553" } }, floorStack: { fs_0000001: { display: { displayAddress: "5788 Wunsch Cliffs, Iristown, IA 75553" } } } } } ``` ### MVF Annotations # MVF Annotations Annotations are very simple objects that point to where some item of interest exists at. For example, an MVF might have an Annotation where each Fire Extinguisher is. There are a wide variety of types of Annotation, including those for building safety, (fire extinguishers, alarm panels, AEDs), security (cameras, smart locks, motion detectors), and parking (EV chargers, bike racks, visitor parking), etc. Typically an application would have an idea the types of annotations it wants to feature in some view, rather than attempting to just show them all at once. Annotations are useful to place as markers on the map. More complex objects with more properties and more differences between instances are better modeled with something like Locations. ## Spec The Annotations extension adds an `annotationSymbols` property, along with an `annotations` object, with a `` key and array of AnnotationProperties objects per floor. ### AnnotationSymbols The `annotationSymbols` property contains an AnnotationSymbol definitions for all the different types of annotations in this MVF. This is in the form of a single object containing a record of of symbol definitions, like this: ```ts annotationSymbols: { Record } ``` The `url` will be a link to an icon for the symbol, suitable for display in a marker. The `name` is something like "Electrical Panel" or "First Aid" suitable for display to the user. AnnotationProperties are also very simple objects: ```ts { id: AnnotationId, externalId: string, symbolKey: string } & WithGeometryId ``` Since they are WithGeometryId, they reference a single geometry on the floor their collection is for (which should be a Point). The `externalId` is a string that may be used to link the annotation to some other system external to Mappedin, and the `symbolKey` should be a key on the `annotationSymbols` object, to find the URL and name. A developer can use the icon at the URL to show a marker anchored at the geometry, or can use the symbolKey to determine their own experience. ### MVF CMS Extension # MVF CMS Extension The CMS Extension adds a number of properties to the MVF, all under the base `cms` property This extra data is only available to certain CMS clients, and should not be used directly without discussing the implications with your Mappedin representative. In particular, everything in this extension is OPTIONAL, because much of it will eventually be promoted to better, more standard MVF extensions, and deprecated out of this one. Others are only for use by the SDK. If there is a piece of information available in the CMS Extension and another extension, USE THE OTHER EXTENSION. ## Common Sub-Schemas Most likely, if the CMS Extension is used, it will be for one of the following sub-schemas: - EnterpriseLocations - EnterpriseLocationInstance - EnterpriseVenue - EnterpriseLayers ### Enterprise Venue An Enterprise Venue adds extra enterprise data to the Enterprise Venue the MVF describes. The `slug` is the unique identifier for the venue. ```ts type cms: { venue: { id: EnterpriseVenueId; slug: string; defaultLanguage: Language; languages: Language[]; countrycode: string; logo: string; mappedinWebUrl: string; topLocations: EnterpriseLocationId[]; operationHours: OpeningHoursSpecification[]; coverImage: string; }; & WithDetails(['name']); & WithExtra; } ``` ### Enterprise Categories Enterprise Categories add extra data to an existing LocationCategory. They MUST link to a LocationCategory by `categoryId`, they do not replace them. ```ts type cms: { categories: { id: EnterpriseCategoryId; categoryId: LocationCategoryId; color: string; sortOrder: number; iconFromDefaultList: string; picture: string; }[]; }; ``` ### Enterprise Locations Enterprise Locations add extra enterprise data to an existing Location. They MUST link to a Location by `locationId`, they do not replace them. ```ts type cms: { locations: { id: EnterpriseLocationId; locationId: LocationId; type: string; sortOrder: number; tags: string[]; picture: string; states: { start: string; end: string; type: LocationStateType; }[]; siblingGroups: { label: string; siblings: LocationId[]; // Note this links to the LocationId, not the EnterpriseLocationId. type?: SiblingGroupType; subtype?: string; }[]; gallery: { caption: string; image: string; embeddedUrl: string }[]; showFloatingLabelWhenImagePresent: boolean; amenity: string; showLogo: boolean; }[]; }; ``` ### Enterprise Layers Enterprise Layers describe the logical/stlyistic "layer" that a geometry is on. It is a simple per floor mapping of GeometryId to layer name. Typically, all geometries on the same layer will have the same overall visual style, though individual properties like altitude can be different. The layer name can also be used to identify groups of geometries that are related in some way, for example to hide all geometries on the "Pillars" layer. There may or may not be a standard layer convention for a given venue or organization, but it is set and implemented by person making the map and not enforced by the system. Enterprise Layers are similar in concept to "layers" in image editing software, except there is no visual order or hierarchy. Ie: The "Room" layer is neither above nor below the "Wall" layer. What will be visible is determined by the altitude and heights of the styles applied to the individual geometries. ```ts type cms: { layers: { f_1: { g_1: 'Floor'; g_2: 'Floor'; g_3: 'Wall'; }; }; }; ``` ### MVF Connections # MVF Connections ## Introduction Connections are things like elevators, escalators, doors, and ramps. They are a way to represent special links a person can use to traverse between specific geometries, on the same floor or between floors. This is separate from the Nodes extension, which describes how to walk through the walkable space on each floor. Connections act more like a vortex: They have one or more entry points a person can enter, and they can exit at any one of the exit points, with the normal cost being a static entry cost and an additional cost per floor transitioned. This implies that Connections work best when they connect two points on the same floor, or any number of points stacked on different floors. A connection with several exits all over a single floor, or where exits on other floors are spread out, will be difficult to cost correctly. ## Specification ### Connection There will be a single `connections.json` file in the root of the bundle, containing an array of all connections objects for the MVF. A Connection MUST have the following properties: - `id`, a unique identifier for the connection, matching the pattern `^c_[A-Za-z0-9_-]+$`. **Important**: While the suffix can be any length, it is strongly recommended to use suffixes of at least 8 characters to ensure uniqueness and avoid collisions. - `entrances`, an array of MVF Navigation Flags's FlaggedGeometryAnchors that are entrances to the connection. - `exits`, an array of FlaggedGeometryAnchors that are exits from the connection. - `entryCost`, the static cost to enter the connection. It MUST be greater than or equal to 0. - `floorCostMultiplier`, the additional cost for each floor transitioned. It MUST be greater than 1. - `type`, a safe string enum connection type for the connection, such as `elevator`, `escalator`, `door`, `ramp`, or `unknown`. It MAY also have the following optional properties: - `details`, a Details object to describe basic metadata. - `extra`, an Extra object of arbitrary key-value pairs. ### Entrances and Exits Entrances and Exits are arrays of FlaggedGeometryAnchors. Each item describes a specific place on the map that a person can enter (or exit) a Connection, along with the navigation flags that have been set for it's use. They may anchor to any type of geometry. A connection MUST have at least one unique entrance and one unique exit, though for many connections the entrance and exit arrays will be the same set of two or more anchors. If the arrays are NOT the same, it implies some sort of directionality. Eg, an Escalator will have a separate entrance and exit, as the direction of travel is fixed: ```json { "id": "c_1", "type": "escalator", "entryCost": 0, "floorCostMultiplier": 10, "entrances": [ { "geometryId": "g_1", "floorId": "f_1", "flags": [0] } ], "exits": [ { "geometryId": "g_1", "floorId": "f_2", "flags": [0] } ] } ``` A door may have a separate entrance and exit, to indicate a one way path, but it's more likely to have the same pair of anchors in both arrays: ```json { "id": "c_1", "type": "door", "entrances": [ { "geometryId": "g_1", "floorId": "f_1", "flags": [1] }, { "geometryId": "g_2", "floorId": "f_1", "flags": [1] } ], "exits": [ { "geometryId": "g_1", "floorId": "f_1", "flags": [1] }, { "geometryId": "g_2", "floorId": "f_1", "flags": [1] } ] } ``` The list of entrances and exits MAY have anchors that only differ by Navigation Flag. For example, consider a `public` Navigation flag: ```json { "key": "public", "index": 0, "bit": 1, } ``` along with the Well Known Accessible flag at bit 0. There may be an Elevator that can be Entered from every floor, but the second floor is not public and only accessible to certain users. ```json { "id": "c_1", "type": "elevator", "entrances": [ { "geometryId": "g_1", "floorId": "f_1", "flags": [3] // 1 * 2^0 + 1 * 2^1 = 3, ie it is 'accessible' and 'public' }, { "geometryId": "g_2", "floorId": "f_2", "flags": [3] }, { "geometryId": "g_3", "floorId": "f_3", "flags": [3] } ], "exits": [ { "geometryId": "g_1", "floorId": "f_1", "flags": [3] }, { "geometryId": "g_2", "floorId": "f_2", "flags": [1] // Accessible, but not public }, { "geometryId": "g_3", "floorId": "f_2", "flags": [3] } ] } ``` This means that when wayfinding with the "Public" flag set, a person can use the elevator from any floor, but can only exit on the first and third floors. The same is true with BOTH accessible and public flags set. A user with private access may wayfind with the "Accessible" flag set, and could enter and exit from any floor. This extension does not strictly depend on the Nodes extension, but if they are being used together each anchor SHOULD also be referenced by a Node, if the intent is to connect **to** or **from** the rest of the node graph through this Connection. However it is valid to use this extension on it's own, or to have Connections connected directly to other Connections, or to an anchor without a Node that should then be used as part of some other wayfinding process defined by another extension. ### Connection Type A connection type is a string that describes the type of connection. It is a SafeStringEnum that SHOULD be one of the following: `elevator`, `escalator`, `door`, `ramp`, `stairs`, `travelator`, `ladder`, or `unknown`. `unknown` is a special type that is not normally used directly. It is NOT considered a breaking change for an MVF to contain new types that are not present in this list. Libraries that parse or create an MVF bundle MUST downgrade any types not present in the version of the specification they are targeting to `unknown`. The Mappedin MVF library will perform this downgrade automatically via the SafeStringEnum parser. Any other parser must handle this downgrade itself. Applications that consume MVFs MUST continue to provide wayfinding with unknown connection types present. For example, some future version of the spec add a `slide` connection type. Applications producing MVFs may then create a Slide tool. Users may create new Slides on existing maps, or convert incorrectly modeled "Ramps" into Slides. At that point, new MVFs will be produced for that venue which will contain `slide` type Connections. An SDK consuming that MVF MUST ensure `slide` connections are downgraded to `unknown`. A existing in market application using that SDK may then start seeing `unknown` type connections. A user of that app must not have broken experience, even if the app does not, eg, have a Slide icon in their UI. ### Line String Doors with Nodes If a Door connection has a line string representing it's physical geometry, and is connected to Nodes, it will be modeled in the following way: * The Door's `entrance` array will reference the Line String geometry. * The Door's `exit` array will reference one or two Point geometries. They MAY overlap with the line string. To enter the door from one direction, there will be a Node referencing the Line String geometry, with neighboors connecting to the side of the door it's coming from. To enter the door from the OTHER direction, there will be a SECOND NODE, connecting to it's side of the node graph. The two entrance nodes WILL NOT BE NEIGHBORS. To be an exit for the door, the node should also reference the Point geometry on their side. Here is a simplified example: ```json { geometry: { f_1: { // a geometry feature collection with g_line, g_north_point, and g_south_point features } }, connections: [ { id: "c_1", type: "door" entrances: ["g_line"], exits: ["g_north_point", "g_south_point"] } ], nodes: { f_1: { features: [ { id: "n_north_1", geometries: ["g_line", "g_north_point"], neighbors: ["n_north_2"], // NOT n_south_anything }, { id: "n_south_1", geometries: ["g_line", "g_south_point"], neighbors: ["n_north_2"], // NOT n_north_anything }, ] } } } ``` That is, the node graph going through a door is NOT DIRECTLY CONNECTED, and a door MUST be used to cross it. ## Navigation Cost When wayfinding through a connection, the cost is the sum of the `entryCost` and the `floorCostMultiplier` times the absolute difference in elevation of the entry and exit floors. This value is then considered as the cost in meters to traverse the connection, and can be thought of as how far out of their way a person should walk to avoid that particular connection in favour of a cheaper option. ### A\* Considerations The `entryCost` of a connection that enters and exits on the same floor is the ONLY cost paid to use it. It must be greater than or equal to 0, but it MAY be less than the straight line distance between the entrance and exit points. This matters because when wayfinding using the A\* algorithm, one typically uses the straight line distance from a node to the goal node as the heuristic. However, to be `admissable` according to A\*, the heuristic cannot underestimate the actual cost. Any connection who's `entryCost` is less than the distance between the entrance and exit points will break this assumption. `Travelators` are a good example of this, but poorly costed `doors` or any `unknown` type connections can also break this assumption. For this reason, when wayfinding using same-floor Connections, a different approach must be used. Failure to do so will still produce a valid path, but it may not be optimal. ### Examples #### Stairs and Elevator Consider a user at point A on floor 1. They would like to get to point B on floor 4. There exists a set of stairs that connects the two floors, with an entrance 1 meter away from A, and an exit 1 meter away from B. The stairs have an `entryCost` of 0, since a person can just walk on without waiting and a `floorCostMultiplier` of 10. The cost to traverse the stairs is then 0 + 10 \* (4 - 1) = 30 meters. Therefore the total path cost to get from A to B via the stairs is 32 meters. Consider that there is also an elevator that connects the two floors. It has an `entryCost` of 10, since a person may have to wait for it to arrive. The elevator has a `floorCostMultiplier` of 1, since once a person is on the elevator it's very easy to traverse between floors. The cost to traverse the elevator is then 10 + 1 \* (4 - 1) = 13 meters. The difference between the stairs path (32) and the cost of the elevator (13) is 19 meters. This means that it would be worth it for the person to walk up to 19 additional meters out of their way to get to and from the elevator, to avoid taking the stairs. #### Travelator Consider a user at point A on floor 1. They would like to get to point B on floor 1. The path cost through the node graph from A to B is 10 (meaning the distance walked is 10 meters, if there are no weights in the node graph). There exists a travelator (ie, people mover, moving sidewalk, etc) that starts at point C and exits and point D. It is 1 meter from A to C, and 1 meter from B to D. The distance the travelator covers, B to D, is 10 meters. Since the travelator is faster than walking, it's `entryCost` should be set with regard to how much faster it is. If it's twice as fast the `entryCost` should be 5 meters. This means the cost to get from A to B via the travelator is 10 + 5 = 15 meters, less than the 20 meters of walking. NOTE: The distance between the entrance and exit point (C and D in this case) is NOT a factor in the cost, though it may have been used to calculate the entry cost. Also note, this breaks admissibility the typical A\* heuristic. See A\* Considerations for more details. ### MVF Core # MVF Core The `mvf-core` package contains the fundamental "extensions" of the MVF format. They are technically optional, but practically every MVF will contain: * The Manifest extension * The Floors extension * The Geometry extension. This document contains an overall summary of these extensions, but see the pages linked above for more details. There are also a number of utility types used by other extensions, as well as a set of RFC-7946 compliant GeoJSON types ## Structure An MVF is a package of files and folders that can be parsed with one or more MVF Extension parsers. Typically, the package is shipped as a compressed file, but it would also be correct to host the files on a webserver with paths relative to the Manifest. The MVF package will parse a compressed MVF file with the extensions specified, and, if it valid according to the extensions, return a JavaScript object of the contents, organized with objects for folders and files (without extension) as properties. For example, a basic MVF containing the three core extensions may look like this: ``` geometry/ abcde1234.geojson abcde1235.geojson floors.geojson manifest.geojson ``` When parsed with a parser configured for the core extensions, it will produce an object like this: ``` { geometry: { abcde1234: { // GeometryFeatureCollection }, abcde1235: { // GeometryFeatureCollection } }, floors: { // FloorsFeatureCollection } manifest: { // ManifestFeatureCollection } } ``` If there were additional extensions present, they would NOT be included in the resulting object. A parser will only get the extensions the application is prepared to use. Of special note is the `geometry` extension. The property IDs/file names are `floorIds`, which should exist in `floors.geojson`. This means that the geometry in those files are on that floor. This is a common pattern used by many extensions. If it is safe to break data up by floor, it should be done. This will allow partial parsing and faster load times in future versions. ## Manifest The Manifest extension includes a single file, `manifest.geojson`. It contains the file structure of the MVF, as well as some data that describes the place the bundle as a whole represents. For example, a name, a default map, a language the data is in. ## Floors The Floors extension includes a single file, `floors.geojson`, that is a FeatureCollection of Floor features. These are all of the floors/levels in the MVF. Many extensions will break data into a separate file per floor. The floor features themselves will contain data like the name and elevation of the floor, but grouping the floors into logical units like Buildings are handled by other extensions, like Floor Stacks ## Geometry The Geometry extension includes a `geometry` folder with a single geojson file per floorId. This will be a `FeatureCollection` containing ALL geometry for that floor. That includes lines describing the shape of rooms and doors, polygons describing the shape of desks and areas, and points marking where safety annotations are. The Geometry itself does not contain a reference to those higher level concepts however, they only represent geometry where something interesting, typically referenced by at least one other extension, is. Geometry features implement the WithDetails utility type on their properties, meaning they MAY have some very basic metadata available about them. Most common is an `externalId` that may represent something like a room / unit number. For common extensions that reference geometries, please see: Locations, Connections, Default Style, and Kinds. ### MVF Default Style # MVF Default Style The default style extension for an MVF. A DefaultStyle has things like color, height and opacity and the geometry it should be applied to. It is geared towards rendering the MVF in a 3D mapping engine, but specific properties like color may be useful in other contexts. If a geometry is NOT referenced by a style, or some other style-like extension, it should generally NOT be rendered by default. In particular, it is expected that Geometry of kind area will not get a default style, but there may be other cases. ### MVF Entrance Aesthetic # MVF Entrance Aesthetic The Entrance Aesthetic extension describes aesthetic elements of doors, such as their swing direction and whether they are single or double doors. This is purely for informational / rendering purposes, and does NOT impact wayfinding. For the direction doors can be walked through, see the Connections and Nodes extensions. The line string geometry an entrance aesthetic is associated with should be the entrance line string of the door Connection, if that extension is present. ### MVF Facade # MVF Facade The Facade extension describes the Geometry of a FloorStack that live on Floors not part of the stack. For example (and typically), the Outdoors floor may have one or more polygons that represent the facade of a building, which is grouped into a single FloorStack with it's inner floors. This is separate from the geometry of the `Floors`, which will usually be the exterior walls only. The `facade` may include extra geometry, such as a representation of the roof. As they are normal `Geometry` objects, the `facade` may get things like styles, floor images, or even a Location describing the building as a whole attached to it. ## Schema The facade extension adds a `facade` property to the MVF, which is a record of FloorStackId to FacadeProperties. ### FacadeProperties FacadeProperties are WithGeometryAnchors objects, with at least one anchor, plus a `id` property that is a FacadeId. A facade may include anchors on multiple floors, if, for some reason, it has representation on them, but typically they will be on the Outdoors floor. ### FacadeIntegrityError FacadeIntegrityError is a simple object with a single property, `id`, which is the FloorStackId of the facade. ### MVF Floor Images # MVF Floor Images The floor images extension for an MVF allows for the addition of images to a floor, either anchored to a geometry or floating in space. It adds a floorImages property, with a feature collection of points with FloorImageProperties per floor. The Mappedin SDK handles this, an SDK user is unlikely to need to use this extension directly. ### MVF Floor Stacks # MVF Floor Stacks The Floor Stacks extension describes groups, or stacks, of floors. The are typically used to represent buildings, but this is not a requirement. A floor can appear in multiple floor stacks, or none. It may NOT appear in the same floor stack twice. If there is an `outdoors` floor, that information can be found in the Outdoors extension, not here. ## Schema The Floor Stacks extension adds a `floorStacks` property to the MVF, which is an array of FloorStack objects. ### FloorStack FloorStack are an array of FloorIds. They may optionally have a `defaultFloor`, and a Details object. If the `FloorStack` does not have a `defaultFloor`, it is up to the consumer of the MVF to decide which floor is used as the default. Generally, a safe choice is the floor with elevation ordinal closest to 0. ### MVF Kinds # MVF Kinds The Kinds extension provides a high level categorization of geometry in an MVF. A geometry can have only ONE kind, from the KIND enum. Examples include `room`, `hallway`, `wall`, and `area`. Note that this is a SafeStringEnum, so `unknown` is a valid value. New kinds will likely be added in the future, and if the version of the MVF package being used does not support it, it will be downgraded to `unknown` automatically. When consuming an MVF directly without this package, the downgrade will need to be handled by the application. An application may ignore the `unknown` kind, or have some generic behavior, but it must not crash or throw errors. ## Schema The Kinds extension adds a `kinds` property to the MVF, which is a record of FloorId to GeometryKinds objects. GeometryKinds are records of GeometryId to Kind objects. ## Example ```json { "kinds": { "f_000001": { "g_000001": "room", "g_000002": "hallway" } } } ``` ### MVF Locations # MVF Locations The Locations extension adds a way to create Location/PoI metadata associated with one or more geometries. So where a specific Polygon might be `unit 123`, a Location might be attached to that Polygon, another an the floor above, and called "Macy's", with a description, photos and links to it's social media accounts. A geometry can have multiple locations (including 0), and a location can have multiple geometries. The locations extension adds three properties to the root of the MVF, corresponding to the three files: - `locations`, for the `locations.json` file - `locationCategories`, for the `location-categories.json` file - `locationInstances`, for the `location-instances.json` file ## Schema ### Location A Location is a place of interest in the MVF that end users may want to know about. It will always have a `details` property with at least a `name`. It will also have `geometryAnchors`, which will have 0 or more Geometries this location is attached to. Many locations will only have one anchor, but it is common to have things like Washrooms be a single location with many different anchors. Also if a location is spread across floors it may have multiple anchors. Locations may also have `locationCategories`, which are used to group locations together. In cases where locations are attached to multiple geometries, there may also be `locationInstances` for that location, where the properties specific to that instance are noted. For example, a location instance may have different hours. ### LocationCategory A LocationCategory is a category Locations may belong to. It may have a parent category, and will otherwise have a standard `details` property with at least a `name`. ### LocationInstance A LocationInstance is a particular instance of an location (usually one that is attached to multiple geometries) that has some properties different from the parent. Eg, it may have different hours, or a slightly different name. A LocationInstance: - MUST have a parentId, referring to a Location. - MUST have a unique ID, for localization to work. - SHOULD have AT LEAST ONE polygon or node to anchor it in space. - SHOULD have AT LEAST ONE other Location property set. It is otherwise a `Partial` ### Example ```json { "locations": [ { "id": "loc_00001", "geometryAnchors": ["g_00001"], "locationCategories": ["lcat_00001"], "details": { "name": "Mappedin", "description": "Your new way to map", }, "logo": "https://www.mappedin.com/logo.png", "website": { "label": "Mappedin", "url": "https://mappedin.com" }, "links": [ { "label": "Get started free", "url": "https://app.mappedin.com/" }, { "label": "Work for us", "url": "https://mappedin.com/careers" } ], "social": [ { "name": "LinkedIn", "url": "https://www.linkedin.com/company/mappedin" }, ], "geometryAnchors": [ { "geometryId": "g_00001", "floorId": "f_00001" } ], } ], "locationCategories": [ { "id": "lcat_00001", "details": { "name": "Technology" } } ] } ``` ### MVF Navigation Flags # MVF Navigation Flags ## 1. Introduction Navigation flags provide a mechanism to describe the properties of navigation paths within an MVF. They enable consumers of MVF data to make intelligent routing decisions based on the qualities of different paths. For example, an application might use flags to: - Find routes that are wheelchair accessible - Avoid outdoor areas when it's raining - Only show paths that are open to the public - Include VIP-only areas for authorized users These flags can be applied to any wayfinding component in other extensions, including but not limited to Connections and Nodes # 2. Types ### 2.1 Navigation Flags Declarations At the root of the MVF will be a `navigationFlagsDelcarations` property. The NavigationFlagsDeclarations file contains a map of all navigation flag declarations for an MVF. For an MVF to be considered valid, all navigation flags used in any extensions MUST be declared in this file. The key of the map is a unique, durable string identifier for the flag that developers use when interacting with the flag through an SDK. This will typically be something meaningful like `accessible` or `indoors`. The value will be a NavigationFlagDeclaration object that describes the flag (in particular, the `index` and `bit` values used to check for the flag in a `flags` array). ### 2.2 Navigation Flag Declaration A navigation flag declaration defines a single navigation property. Each declaration MUST include: - `index`: The non-durable index of the flag the 32-bit integer in the `flags` array, provided by the `Flags` type. `index` MUST be sequential, starting at 0, and increasing by 1 for every 32 flag definitions. - `bit`: The non-durable bit position (0-31) that will be set in the `flags` integer at the index specified by `index` used by other extensions to indicate the presence of this flag. `bit` MUST be sequential, starting at 0, and increasing by 1 for each new flag in the same `index`. The combination of `index` and `bit` MUST be unique across all navigation flag declarations in the same MVF. A declaration MAY also include: - `details`: A Details object that can provide additional information. This is usually a name, but may include other information such as description, images, and links. Like all details, it is targeted towards users, not developers. ### 2.3 Flags Type This extension defines a reusable Flags type that can be incorporated into any other type to make it navigation flag aware: ```typescript type Flags = { /** * An array of 32-bit integers representing the navigation flags. Each bit on an integer corresponds to a flag * in the NavigationFlagDeclarations, as determined by its index and bit position. */ flags: number[]; }; ``` For convenience, this extension also defines a FlaggedGeometryAnchor type that combines the core `GeometryAnchor` type with the `Flags` type: ```typescript type FlaggedGeometryAnchor = GeometryAnchor & Flags; ``` There are also the WithFlags, WithFlaggedGeometryAnchor, and WithFlaggedGeometryAnchors types that can be added to objects by other extensions to make them navigation flag aware. ### 2.4 Using Navigation Flags Extensions that support navigation flags SHOULD either include the `Flags` type in their own types, or use the `FlaggedGeometryAnchor` type for their anchors, which include the `flags` property. `flags` is an array of 32-bit integers, where each bit of each integer corresponds to a specific navigation flag. The presence of a flag is indicated by setting the corresponding bit to 1. For example, if the NavigationFlagsDeclarations file contains: ```json { "accessible": { "index": 0, "bit": 0 }, "public": { "index": 0, "bit": 1 }, "indoors": { "index": 0, "bit": 2 } } ``` Then a `flags` value of `[6]` can be broken down as follows: - `accessible` flag (index 0, bit 0): 2^0 = 1 (not set) - `public` flag (index 0, bit 1): 2^1 = 2 - `indoors` flag (index 0, bit 2): 2^2 = 4 The total value is 0 + 2 + 4 = 6, indicating that both `indoors` and `public` flags are enabled, while `accessible` is disabled. When a consumer of MVF data wants to find a path with specific flags, they can check that each component along the path has the required flags set or not, and use it to change the weight or block that part of the path entirely. ## 3. Well Known Flags Certain flags have well known meanings and should be used where possible in preference of custom flags. Applications SHOULD provide first class wayfinding support for Well Known Flags. They MAY provide support for custom flags. A given MVF DOES NOT need to include all well known flags, only the ones it is using. For well known flags, only the `key` (and meaning) is durable. The `index` and `bit` values are not guaranteed to be the same across different MVFs. Consumers of MVFs MUST NOT crash if a Well Known Flag is not present in the NavigationFlagsDeclarations. ```typescript /** * All well known navigation flags. */ export const WELL_KNOWN_FLAGS = { accessible: 'accessible', outdoors: 'outdoors', public: 'public' } as const; ``` ### 3.1 Accessible The `accessible` flag indicates that a navigation component is designed to be accessible to people using mobility aids such as wheelchairs. This includes appropriate width for doorways, paths taking ramps instead of stairs, and elevators instead of escalators. ### 3.2 Outdoors The `outdoors` flag indicates that a navigation component is or goes outside. ### 3.3 Public The `public` flag indicates that something is navigable by the public users of the building or space. By default, consumers of MVF should not route through non-public things when the public flag is present. ## 4. File Structure The navigation flags extension adds a single file to the MVF bundle: ``` map-bundle/ └── navigationFlags.json ``` The navigationFlags.json file contains an array of NavigationFlagDeclaration objects. ## 5. Implementation Guidelines ### 5.1 Checking for Flags To check if a specific flag is set on a WithFlags object, use the hasFlag function. It performs the appropriate bitwise AND and looks something like this: ```typescript // Assuming flagKey is the key of the flag to check, // flagBit is its bit position from NavigationFlagsDeclarations, // and entityFlags is the flags value from an entity function hasFlag(entityFlags: number[], flagIndex: number, flagBit: number): boolean { return (entityFlags[flagIndex] & (1 << flagBit)) !== 0; } ``` ### 5.2 Setting Flags When constructing an MVF, a developer can set flags on a flaggable object using a bitwise OR operation: ```typescript // To set a flag with bit position flagBit on an entity with existing flags function setFlag(entityFlagBitfield: number, flagBit: number): number { return entityFlagBitfield | (1 << flagBit); } ``` ### 5.3 Making Types Navigation Flag Aware Extensions that have made their objects navigation flag aware will have either added the WithFlags type: ```typescript import { Flags } from "./navigationFlags"; // An example type in another extension export type Node = { id: string; position: [number, number]; // Other properties... } & WithFlags; // This adds the flags property ``` Or used the `FlaggedGeometryAnchor` type for their geometryAnchor(s). ```typescript import { FlaggedGeometryAnchor } from "./navigationFlags"; // An example type in another extension export type Connection = { id: string; entrances: FlaggedGeometryAnchor[]; exits: FlaggedGeometryAnchor[]; // Other properties... } ``` This pattern allows any extension to incorporate navigation flags in a consistent way. ### MVF Nodes # MVF Nodes ## 1. Introduction Nodes describe paths a person can walk around on in an MVF. There is a single file of nodes per floor, and those files will describe how to walk around on that floor. Going from one floor to another, or through things like travelators or doors, are done by Connections. Nodes also make use of the NavigationFlags extension to indicate under what situations a node's edge may be used. For example, if a user is in a wheelchair, they can cross edges that have the Well Known `accessible` flag set. Nodes link back to other extensions through shared geometry. For example, to navigate to a specific Location, that location must have a `GeometryAnchor` that is also referenced as one of the `geometryIds` of a node on that floor. For developers using the MappedIn SDKs, nodes are typically not interacted with directly. ## 2. Specification ### 2.1 Node Nodes are a GeoJSON Point geometry, with the following properties: - `id`: a unique identifier for the node within the MVF, Should match the pattern `^n_[A-Za-z0-9-]+$`. **Important**: While the suffix can be any length, it is strongly recommended to use suffixes of at least 8 characters to ensure uniqueness and avoid collisions. - `neighbors`: an array of nodes that this node can connect to, of the form: - `id`: the identifier of the neighbor node - `extraCost`: the additional cost of navigation to the neighbor node, above the straight line distance between the nodes. Must be >= 0. - `flags`: an array of navigation flags that control the behaviour of this edge further. - `geometryIds`: an optional array of geometry that is linked to this node. This may be useful for a variety of reasons: - if navigating to a specific piece of geometry, this can be used to signal the end of navigation - used to bridge between nodes and connections to facilitate floor transitions - discovering landmarks or areas a path traverses - etc. Nodes MUST only connect to other nodes on the same floor, and MUST NOT reference themselves in their neighbor list. Nodes MUST only reference geometry on the same floor. Navigation is permitted to traverse floors through information provided by other extensions -- Connections as a primary example, though future extensions may provide additional functionality. ### 2.2 Nodes Collection Data will be organized by floor ID, and will be a FeatureCollection of nodes. ## 3. File Structure Node data will be stored as follows: ``` nodes/ ├── f_abcd1234.geojson ├── f_defg5678.geojson └── f_hijk9012.geojson ``` Where `f_abcd1234`, `f_defg5678` and `f_hijk9012` are valid floor IDs. ## Example This example demonstrates a two node network, on a single floor. If a user were at `n_000001`, they could walk to `n_000002` to reach the destination geometry `g_000001`. You cannot walk back. ```json { "nodes": { "f_000001": { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [10.0, 10.0] }, "properties": { "id": "n_000001", "neighbors": [ { "id": "n_000002", "extraCost": 10, "flags": [0] } ], "geometryIds": [] }, }, { "type": "Feature", "geometry": { "type": "Point", "coordinates": [10.0, 10.0] }, "properties": { "id": "n_000002", "neighbors": [], "geometryIds": ["g_000001"] }, } ] } } } ``` ### MVF Outdoors # MVF Outdoors The MVF Outdoors extension is used to describe which floors are outdoors in an MVF. It adds a single property to the root of the bundle, `outdoors`, which contains a unique array of FloorIds. These floors are considered "outdoors" and may be treated specially by applications, such as always showing them on the map. Typically, there will only be one floor in an MVF that is outdoors, but there may be more at complex venues like airpots. A floorId CANNOT be part of both `outdoors` and a FloorStack. ### MVF Overview # MVF Overview The Mappedin Venue Format (MVF) is a GeoJSON based format to describe indoor maps and their surroundings. It has a simple, flexible core of GeoJSON broken up by floor, with a powerful extension framework to add many different kinds of data. TypeScript/JavaScript developers will primarily interact with MVFs though the main MVF package. It contains the parser to unzip an MVF bundle and convert it into a typed JavaScript object of the MVFv3 type. It also re-exports the Mappedin extensions, so developers only need to install the `@mappedin/mvf` extension for most common cases. Developers looking at the MVF directly can find specifics on each extension in their specific docs. The most common ones are: * Core - The main extension almost every MVF will implement. Includes the manifest, floors, and geometry (one file per floor). * Locations - Rich data for places of interest, linked to geometry. Others may be used, but many, such as Connections, Nodes, and Navigation Flags, are mostly used by the Mappedin SDK. ## Core Almost every MVF will contain the Core extension. In particular, most extensions will contain references to Geometry and Floors. Here is a very simple example, as an `MVFv3` object: ```ts import type { MVFv3 } from '@mappedin/mvf'; export const CORE_EXAMPLE: MVFv3 = { manifest: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Point', coordinates: [0, 0], }, properties: { name: 'My Map', version: '3.0.0', time: '2025-01-01T00:00:00.000Z', contents: [ { name: 'manifest.geojson', type: 'file', }, { name: 'floors.geojson', type: 'file', }, { name: 'geometry', type: 'folder', children: [ { name: 'f_00000001.geojson', type: 'file', }, { name: 'f_00000002.geojson', type: 'file', }, ], }, ], }, }, ], }, floors: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Polygon', coordinates: [ [ [0, 0], [1, 0], [1, 1], [0, 1], [0, 0], ], ], }, properties: { id: 'f_00000001', elevation: 0, details: { name: 'Floor 1', }, }, }, { type: 'Feature', geometry: { type: 'Polygon', coordinates: [ [ [0, 0], [1, 0], [1, 1], [0, 1], [0, 0], ], ], }, properties: { id: 'f_00000002', elevation: 1, details: { name: 'Floor 2', }, }, }, ], }, geometry: { f_00000001: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Polygon', coordinates: [ [ [0, 0], [1, 0], [1, 1], [0, 1], [0, 0], ], ], }, properties: { id: 'g_00000001', }, }, ], }, f_00000002: { type: 'FeatureCollection', features: [ { type: 'Feature', geometry: { type: 'Polygon', coordinates: [ [ [0, 0], [1, 0], [1, 1], [0, 1], [0, 0], ], ], }, properties: { id: 'g_00000002', }, }, ], }, }, }; ``` That would have come from a zip file with the following structure: ``` manifest.geojson floors.geojson geometry/ f_00000001.geojson f_00000002.geojson ``` This describes a map, called "My Map", with two floors. Each has one geometry feature, which is a polygon. All of the files exposed by Core are GeoJSON feature collections. The manifest has a single point that is roughly the center of the map as a whole. Floors contains a outline of each floor, and geometry contains all of the actual geometry (walls, rooms, doors, desks, etc.) for each floor. There may be a few extra properties on these objects (see their types for details), but most other, more complex relationships, will be described in other extensions. ### "Core" Concepts There are a few concepts and patterns that Core provides that are important to understand. #### Files vs Objects MVF is a bundle format, but most developers will interact with it as a parsed JavaScript object. This means that when describing the structure of an MVF, this documentation will tend to prefer the object model over the file model. As can be seen from the example above, an MVF bundle is parsed into a single object, with top level properties for every file and folder in the root of the zip. For any file, they will be parsed as JSON and their extension removed. For any folder, their children (files and folders) will be parsed the same way and added to the object as a child property. So in the example above, the contents of the "manifest.geojson" file is what is now in the manifest property of the `MVFv3` object. The contents of the "floors.geojson" file is what is now in the floors property of the `MVFv3` object. The contents of the "geometry" folder would be two files, one for each floor, with the name of the file being the floor id + ".geojson", and the contents of the file being what is now in the geometry property of the `MVFv3` object, on the keys of the floor ids. #### One File Per Floor If at all possible, an extension will provide a top level property for itself, an object per floor, keyed by the floor id. Geometry in Core is a good example of this, but many extensions will do this as well. Floors are a fundamental concept that differential MVF from other geo-spatial formats, in particular outdoor oriented ones. Each floor can contain a lot of data, and a client may wish to only parse or consider data on a certain floor in many situations. #### Utility Types Core exports a number of utility types that are used by many extensions to have a common way of handling certain ideas. #### WithGeometryId and WithGeometryAnchor Nearly all extensions will involve relating some information to some Geometry from the Core extension. The Geometry object is kept deliberately minimal, with the idea being that one or more extensions will have references to geometries to add more data and context. For example, for a given piece of Geometry, a Location such as a Restroom may reference it, along with several other geometries, to indicate where the restrooms are. A node may reference it to say a user can reach this place through the node graph by getting to that node. The Default Style extension may reference the geometry to say it should rendered blue. All of this is done in a standard way, though two main utility types, exported from Core: WithGeometryId and WithGeometryAnchor, and their array variants (WithGeometryIds and WithGeometryAnchors). `WithGeometryId` adds a `geometryId` property to a type, and `WithGeometryIds` adds an array of `geometryIds` properties. ```ts export type WithGeometryId = { geometryId: GeometryId; } export type WithGeometryIds = { geometryIds: GeometryId[]; } ``` `WithGeometryAnchor` adds a `geometryAnchor` property to a type, containing a GeometryAnchor, and `WithGeometryAnchors` adds `geometryAnchors`, which is an array of GeometryAnchors. A `GeometryAnchor` is an object with a geometryId and floorId. ```ts export type GeometryAnchor = { geometryId: GeometryId; floorId: FloorId; } export type WithGeometryAnchor = { geometryAnchor: GeometryAnchor; } export type WithGeometryAnchors = { geometryAnchors: GeometryAnchor[]; } ``` An extension will use `withGeometryId` if it is already known what floor it belongs to (ie, if it is following One File Per Floor practice and is inside of a object keyed by the floor id). If it is not known what floor it belongs to, it will use `withGeometryAnchor` instead. ### WithDetails Core also exports a WithDetails utility type, which is used to add a standard `details` property to an object. This is the way to add a small amount of additional (typically optional) user facing metadata to an object. Extensions will tend to add a `details` object rather than any of the properties inside of it directly. Developers can build default components to display information about objects that are `WithDetails` if they don't have a more specific display. ```ts export type Details = { description?: string; externalId?: string; icon?: string; name?: string; shortName?: string; }; export type WithDetails = { details?: Details; } ``` `externalId` is worth noting specifically. This is a string that should be used to link the object to some other system external to Mappedin. For example, the core Geometry extension marks Geometry as `withDetails` and a particular geometry might get an `externalId` of `UNIT-21` to represent the unit number in some leasing system. ### Extra Core also includes a WithExtras utility type, which is used to add the `extra` property to an object, which is a grab back of string keys to any data. This should be used sparingly, preferring instead a more specific extension, since there is no validation or integrity checking on the data. Enterprise customers with custom data syncs and custom SDK applications may have some of their own data included in the extra property on certain objects, as per their agreements with Mappedin. ```ts export type Extra = Record; export type WithExtras = { extra?: Extra; } ``` ### Example The Locations extension uses all three of these utility types. Here is a simple example of a Location object: ```ts const location: Location = { id: 'loc_00000001', geometryAnchors: [ { geometryId: 'g_00000001', floorId: 'f_00000001', }, ], details: { name: 'Main Conference Room', }, extra: { customData: 'some data', }, categories: [], social: [], images: [], links: [], }; ``` This location can be found on floor one. It is the Main Conference Room, and it has some custom data. ### MVF Tileset # MVF Tileset The Tileset extension provides a way to define the outdoor tileset to use when rendering an MVF. It is largely use by the SDK, and will not typically be needed by a developer. ## Schema The Tileset extension adds a single property to the root of the bundle, `tileset`, which is a TilesetCollection object. ### MVF Traversability # MVF Traversability The Traversability extension provides a way to define which geometries can be walked on/through, and which cannot. It is primarily used to influence the automatic node generation process. ## Schema The Traversability extension adds two properties to the root of the bundle, `walkable` and `nonwalkable`, which are Records of FloorId to WalkableGeometries and NonWalkableGeometries objects. If a geometry id is present in a walkable record, it can be walked over. If it is present in a nonwalkable record, it will block walking. If it is both, nodes may be generated to walk up to it, but not through it. If it is in neither, it will be ignored for the purposes of node generation.