@wework/floormap-sdk
TypeScript icon, indicating that this package has built-in type declarations

1.12.1 • Public • Published

FloorMap SDK

CircleCI Coverage Status

Installation

NPM

You might need npm login

You will need to be logged in to install a dependency. Execute npm login and use the username, password, and e-mail found in 1Password under npmjs (dev-team).

npm install @wework/floormap-sdk

# Yarn
yarn add @wework/floormap-sdk

And import the SDK with:

import * as FloorMap from '@wework/floormap-sdk'

// you also can cherry-pick import a module from sdk by
// import { Manager } from '@wework/floormap-sdk'

UMD

By using the UMD format, You can access the SDK via FloorMap variable.

<script type="text/javascript" src="floormap-sdk.min.js"></script>

In JavaScript

const manager = new FloorMap.Manager({
  /* ... */
})
const floorMap = manager.createFloorMap(target, {
  /* ... */
})

Please see example/sample for UMD usage

Getting started

Authentication

Before create and render a floor map, you need to authenticate with MilkyWay service by creating Manager object and providing a credential.

To request for an appId/appSecret, kindly email tech-sg@wework.com with the subject Request for FloorMap SDK credentials and a brief explanation of its intended purpose.

const manager = new FloorMap.Manager({
  appId: /* App ID */,
  appSecret: /* App Secret */,
  baseUrl: 'https://occupancy-api.phoenix.dev.wwrk.co/v2',
})

Then, use .authenticate function to start authenticating

manager
  .authenticate()
  .then(mwAccessToken => {
    // Authenticated
  })
  .catch(e => {
    // Authenticating Error
  })

Spaceman JWT Token

Spaceman JWT Token is supported directly by the SDK. You can pass Spaceman JWT while constructing manager instance.

const manager = new FloorMap.Manager({
  appId: /* App ID */,
  appSecret: /* App Secret */,
  baseUrl: 'https://occupancy-api.phoenix.dev.wwrk.co/v2',
  spacemanToken: /* Spaceman JWT */
})

Store result mwAccessToken for futher usage

authenticate function resolves mwAccessToken object. You can store the access token object for further use and provide the token object next time you're creating a manager.

manager.authenticate().then(mwAccessToken => {
  // Authenticated
  localStorage.setItem('MW_TOKEN_STORAGE_KEY', JSON.stringify(mwAccessToken))
})

// Next time
const mwAccessToken = JSON.parse(localStorage.getItem('MW_TOKEN_STORAGE_KEY'))
const manager = new FloorMap.Manager({
  /* ... */
  mwAccessToken: mwAccessToken,
})

After this point, the manager instance is ready to create and render the floormap.

Create a floor map

First, create an empty HTML element for the map to render itself.

<body>
  <section id="container"></section>
</body>

Next, Use manager instance to create and render a floormap and provide target element.

const target = document.getElementById('container')
const floorMap = manager.createFloorMap(target, options)

FloorMap options

  • options.backgroundColor - Background color of the map
  • options.deskLayout - Show desk layout
  • options.deskInteractable - Allow desk/chair to be interactable

After creating a floor map, Call render function with buildingId and floorId to render a floor map into the screen (if floorId is omitted, the lowest floor of the building will be rendered)

// Render Map
floorMap
  .render({
    buildingId: 'b308b94c-bca6-4318-b906-1c148f7ca183',
    /* floorId: '' */
  })
  .then(result => {
    console.log(result)
    // { building, floor, spaces, objects, options}
  })
  .catch(e => {
    console.log(e.data)
    // { building, floor? }
    console.log(e.options)
  })

Final source code

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>FloorMap</title>

    <style>
      body {
        margin: 0;
        padding: 0;
      }
      #main {
        position: fixed;
        top: 0;
        right: 0;
        bottom: 0;
        left: 0;
      }
    </style>
  </head>
  <body>
    <div id="container"></div>

    <script src="../../dist/floormap-sdk.js"></script>
    <script>
      async function renderMap() {
        const target = document.getElementById('container')
        let currentId = null

        // Credential
        const manager = new FloorMap.Manager({
          appId: /* APP_ID */,
          appSecret: /* APP_SECRET */,
          baseUrl: 'https://occupancy-api.phoenix.dev.wwrk.co/v2',
        })

        // Authenticating
        await manager.authenticate()

        // Create Floor Map
        const floorMap = manager.createFloorMap(target, {
          backgroundColor: '#303030',
        })

        // Render Floor
        floorMap.render({
            buildingId: 'b308b94c-bca6-4318-b906-1c148f7ca183',
        })
      }

      renderMap()
    </script>
  </body>
</html>

Map Event

You can subscribe to user interaction and data event on the map by using addEventlistener and removeEventListener to remove a listener on the floor map instance.

// Mouse moveover a space
floorMap.addEventListener('onmouseover', event => {})

// Mouse moveout a space
floorMap.addEventListener('onmouseout', event => {})

// On click a space
floorMap.addEventListener('onclick', event => {})

// When user moving mouse cursor on the map
floorMap.addEventListener('onmousemove', event => {})

// When physical data change
floorMap.addEventListener('datachange', event => {})

// Renderer event (mostly use for debugging purpose)
floorMap.addEventListener('onerror', event => {})
floorMap.addEventListener('onrender', event => {})

An event object will contain:

  • type - Event type
  • payload - An informations of the interaction
  • data - Space data, If an interaction is on space/object

Example payload

{
  "type": "onclick",
  "payload": {
    "id": "cdc9c84e-d092-11e7-9d13-0642b0acf810",
    "point": { "x": 14.237798863250575, "y": 74.47726859122804, "z": -3 },
    "mousePos": { "x": 530, "y": 203 }
  },
  "data": {
    /* Space Data */
  }
}

Styling

You can manipulate the style of space, table, and chair by using functions on floorMap instance:

Apply style

Apply style to a given space uuid

applyStyle({ id: string, style: object, key: string }, completion?: function)

example
floorMap.applyStyle({ id: 'space-uuid-1', style: { color: 'red' }, key: 'highlight' })

// Bulk apply
floorMap.applyStyle([
  { id: 'space-uuid-1', style: { color: 'red' }, key: 'highlight' },
  { id: 'space-uuid-2', style: { color: 'red' }, key: 'highlight' },
])

Revert style

revertStyle({ id: string, key: string }, completion?: function)

Revert style with matched key to a given space uuid

example
floorMap.revertStyle({ id: 'space-uuid-1', key: 'highlight' })

// Bulk revert
floorMap.revertStyle([
  { id: 'space-uuid-1', key: 'highlight' },
  { id: 'space-uuid-2', key: 'highlight' },
])

Reset style

Reset all styles to a given space uuid

resetStyle({ id: string }, completion?: function)

example
flooMap.resetStyle({ id: 'space-uuid-1' })

// Bulk reset
flooMap.resetStyle([{ id: 'space-uuid-1' }, { id: 'space-uuid-2' }])

Style is Stack

Calling applyStyle will push style object into the stack. If we call applyStyle with a different key, the later style will be placed on top of the stack of overwrite property in previous items in the stack.

Also, we call applyStyle with a key that already exists in the stack, that style with the same key will be replaced with new style instead of merging and stay in the current position in the stack instead of bumping to the top

For example:

// Stack
//
// - somekey:{ color: 'red', opacity: 0.8 }
//
// result style: { color: 'red', opacity: 0.8  }
floorMap.applyStyle({ id: spaceUUID, style: { color: 'red', opacity: 0.8 }, key: 'somekey' })

// Apply style with new key `somekey`
//
// Stack
//
// - otherkey: { opacity: 0.5 }
// - somekey: { color: 'red', opacity: 0.8 }
//
// result style: { color: 'red', opacity: 0.5 }
floorMap.applyStyle({ id: spaceUUID, style: { opacity: 0.5 }, key: 'otherkey' })

// Apply style to existing key `somekey`
//
// Stack
//
// - otherkey: { opacity: 0.5 }
// - somekey: { color: 'red', opacity: 1.0 }
//
// result style: { color: 'red', opacity: 0.5 }
floorMap.applyStyle({ id: spaceUUID, style: { color: 'red', opacity: 1.0 }, key: 'somekey' })

// We revert 'somekey' style
// Stack
//
// - otherkey: { opacity: 0.5 }
//
// result style: { opacity: 0.5 }
floorMap.revertStyle({ id: spaceUUID, key: 'somekey' })

// Remove all style in stack
floorMap.resetStyle({ id: spaceUUID })

Combine event and style to create a user interaction feedback

// - Event Handling
floorMap.addEventListener('onmouseover', event => {
  const { payload, data } = event

  // Highlight Space
  floorMap.applyStyle({ id: payload.id, style: { color: 'aqua' }, key: 'HOVER' })
})

floorMap.addEventListener('onmouseout', event => {
  const { payload, data } = event

  // unhighlight Space
  floorMap.applyStyle({ id: payload.id, style: {}, key: 'HOVER' })

  // You also can use revertStyle
  // floorMap.revertStyle({ id: payload.id, key: 'HOVER' })
})

Map lifecycle hooks

Map lifecycle gives an opportunity to you to start loading data along with when the map starts loading data, modify physical data or apply a style to an object before the map start to render an object into the screen.

onLoad -> didLoad -> onRender -> didRender

Lifecycle function will get called with the following parameters:

  • building A building object
  • floor A floor object
  • options Passing options from .render and .load
  • errors If any errors occurred

onLoad

onLoad hook allows you to prepare your data while map starts loading their data. You can make the map to be waiting for your data to be loaded before starting rendering into the screen by returning promise from the function.

Example
const unsubscribe = floorMap.onLoad(({ building, floor, spaces, objects, options, errors }) => {
  // Do something
})

If a promise gets returned from the function, the map will wait until the promise gets resolved before start rendering.

const unsubscribe = floorMap.onLoad(({ building, floor, spaces, objects, options, errors }) => {
  return fetch(/* ... */)
})

didLoad

didLoad will get called when the map finished loading the data (including returned promises from onLoad function), and the Promise from onLoad(s) has been resolved. You can use this function to modify the physical data in the map.

Example
const unsubscribe = floorMap.didLoad(({ building, floor, spaces, objects, options, errors }) => {
  // Change room type
  floorMap.updateData(spaceeUUID, { roomType: 'Private Large Office' })
})

onRender

onRender will get called during the map is preparing to render object for rendering into the screen, but not yet rendered into the screen. This function gives you an opportunity to apply a style to spaces.

Example
const unsubscribe = floorMap.onRender(({ building, floor, spaces, objects, options, errors }) => {
  floorMap.applyStyle({
    id: spaceUUID,
    style: { color: 'aqua' },
    key: 'occupancy-style',
  })
})

didRender

didRender will get called when the map finished rendering objects into the screen. You can add custom overlay into the map on this lifecycle (We will talk about map overlay in next section)

Example
const unsubscribe = floorMap.didRender(({ building, floor, spaces, objects, options, errors }) => {
  const imageOverlay = new ImageOverlay(noteIcon, {
    style: {
      width: 3,
      height: 3,
    },
  })

  imageOverlay.spaceUUID = spaceUUID
  floorMap.addObject(imageOverlay)
})

onError

Get a call when an error occurred during loading or rending

Example
const unsubscribe = floorMap.onError(({ options, errors }) => {
  // Change room type
  floorMap.updateData(spaceeUUID, { roomType: 'Private Large Office' })
})

Unsubscribe

Call returned function to remove a listener.

// Subscribe to onLoad
const unsubscribe = floorMap.onLoad(() => {})

// Unsubscribe
unsubscribe()

Map Data

Preload Building / Floor Data

Pre-load and cache building/floor data in local. This function won't render floormap into the screen

floorMap.load(options: Object): Promise<Result>

Options:

  • buildingId - Physical / System Building UUID
  • floorId - Physical / System Floor UUID
  • autoLoadFloor - Auto load lowerest floor in building if floorId is omitted
  • skipCache - Skip local building/floor cache in the SDK

Result:

  • building - Building object
  • floor - Floor object
  • spaces - Space objects inside floor
  • objects - Objects (Chair, Table) inside floor

Render Floormap

Load and render floormap into the screen

floorMap.render(options: Object): Promise<Result>

Options:

  • buildingId - Physical / System Building UUID
  • floorId - Physical / System Floor UUID
  • autoLoadFloor - Auto load lowerest floor in building if floorId is omitted
  • skipCache - Skip local building/floor cache in the SDK

Result:

  • building - Building object
  • floor - Floor object
  • spaces - Space objects inside floor
  • objects - Objects (Chair, Table) inside floor

Reload Map

Reload and re-render current floor

floorMap.reload(options: Object): Promise

Options:

  • skipCache: Skip local building/floor cache in the SDK

Get Spaces

Returns all spaces and objects in current floor (if floorId is omitted)

floorMap.getSpaces(floorId: string?): Space|SpaceObject[]

Update Data

Update Space/SpaceObject data by its uuid. This will trigger on('datachange') event.

floorMap.updateData(uuid: string, data: object?)

Map State

Current Building

Returns current building

floorMap.getCurrentBuilding(): Building

Current Floor

Returns current floor

floorMap.getCurrentBuilding(): Floor

Current floor map state

Returns current status of the floor map

floorMap.getCurrentState(): string

State values:

  • IDLE - Floor map is ready to render
  • LOADING - Loading data
  • RENDERING - Rendering map
  • RENDERED - Floor map is displayed on the screen
  • TERMINATED - Floor map is terminated from teardown call
  • ERROR - Have an error during loading/rendering

Camera control

Fit Content

Fit camera to the map content

floorMap.fitContent({ padding: number })

Example
floorMap.fitContent({ padding: 20 })

setZoomLevel

Set the zoom level

floorMap.setZoomLevel(zoomLevel: number)

getZoomLevel

Get current zoom level of the map.

floorMap.getZoomLevel(): number

setCenter

Set the camera rotation view angle

floorMap.setCenter({ x: number, y:number, z: number })

Example
floorMap.setCenter({ x: 2, y: 0, z: 5 })

setRotation

Rotate the map

floorMap.setRotation(veticalDegree: number, horozontalDegree: number)

  • rotationDegree Vetical rotation degree
  • polarAngle Horizontal rotation degree
Example
floorMap.setRotation(rotationDegree, polarAngle)

Object Overlay

ImageOverlay

const imageOverlay = new FloorMap.ImageOverlay(imageUrl, {
  style: {
    width: 3,
    height: 3,
  },
})

// Add image into space
// This will automatically calculate position of the image
imageOverlay.spaceUUID = '[PHYSICAL_SPACE_UUUD]'

floorMap.addObject(imageOverlay)

TextOverlay

const textOverlay = new FloorMap.TextOverlay('FloorMapSDK!!')

textOverlay.position = { x: 0, y: 0, z: 0 }
textOverlay.scalar = 2
textOverlay.style = {
  color: '#000000',
  fontFamily: 'Arial, Helvetica, sans-serif',
  textAlign: 'center',
  fontWeight: 'normal', // normal, bold, bolder, lighter
  fontStyle: 'normal', // normal, italic, oblique
}

floorMap.addObject(overlay)

LineOverlay

const lineOverlay = new FloorMap.LineOverlay()
lineOverlay.style = {
  color: '#303030',
}

lineOverlay.addPoint({ x: 100, y: 100 })
lineOverlay.addPoint({ x: 100, y: 200 })

// Add Overlay to floormap
floorMap.addObject(lineOverlay)

// Update point at index
// updatePoint(index, point)
lineOverlay.updatePoint(1, { x: 100, y: 300 })

// Remove point at index
// removePoint(index, point)
lineOverlay.removePoint(1, { x: 100, y: 300 })

// Update current overlay
floorMap.updateObject(lineOverlay)

PolygonOverlay

const polygon = new FloorMap.PolygonOverlay(
  [
    { x: x - 10, y: y - 10 },
    { x: x + 10, y: y - 10 },
    { x: x + 10, y: y + 10 },
    { x: x - 10, y: y + 10 },
  ],
  { style: { color: 'aqua', opacity: 0.6 } }
)
polygon.style = { color = '#000000', outline, opacity = 1 }
polygon.interactable = true

floorMap.addObject(polygon)

FloorMapGL Render Object

ImageOverlay, TextOverlay, LineOverlay, and PloygonOverlay are the abstractions of floormap.gl object. That means we can add floormap.gl's object using .addObject function as well.

floorMap.addObject({
  id: '001', // required
  tags: ['level 3'],
  type: 'MESH',
  style: {
    color: 'rgb(155, 255, 55)',
    side: 'FRONT',
    texture: {
      img: '',
      repeat: 0.5
    }
    outline: {
      color: 'rgb(255, 0, 0)',
      width: 0.2,
      only: false
    }
  },
  points: [
    { x: -1, y: -1 },
    { x: 1, y: -1 },
    { x: 1, y: 1 },
    { x: -1, y: 1 }
  ],
  geometryTranslate: {x: -1, y: -1, z: 0},
  interactable: true,
  visible: true,
  extrude: 2,
  position: { x: 0, y: 0, z: 0 },
  rotation: { x: 0, y: 0, z: 0 },
  scale: { x: 1, y: 1, z: 1 }
})

The documentation on floormap.gl render object can be found on Floormap.GL repository

Add Object

floorMap.addObject(overlay)

// You can add multiple objects as once by passing array of objects
floorMap.addObject([overlay, overlay2])

Remove Object

FloorMap overlay/object can be removed by calling .removeObject with object or id

floorMap.removeObject(overlay)
floorMap.removeObject(id)

// Remove multiple objects as once
floorMap.removeObject([overlay1, overlay2])
floorMap.removeObject([id1, id2])

Overlay Positioning

To position an overlay on the map, we can set position property.

const imageOverlay = new ImageOverlay(imageUrl)
imageOverlay.position = { x: 100, y: 100 }

floorMap.addObject(imageOverlay)

In case you want to add an overlay into the specific space/room. Instead of manually calculating the position of a room, we can omit position value and assign spaceUUID to an overlay. The floor map will calculate the center position of the room and assign it to the overlay.

const imageOverlay = new ImageOverlay(imageUrl)
imageOverlay.spaceUUID = 'SPACE_UUID'

floorMap.addObject(imageOverlay)

Session

getAccessToken

Returns an access token of the current session

Example
const mwAccessTokenObj = manager.session.getAccessToken()

setAccessToken

Set new access token to the current session

Example
manager.session.setAccessToken(mwAccessTokenObj)

setSpacemanToken

Set new spaceman JWT token and re-authenticate

Example
const mwAccessTokenObj = await manager.session.setSpacemanToken(jwt)

Separate of concern with extension

Create a extension

We can use lifecycle hooks like onLoad pre-loading our data and apply a style with applyStyle base on data we have. In some case, we want to separate to logic of data loading and styling from main application logic, make it reusable, plug-n-play, or even distribute our logic as a standalone package, and This is where the extension come to play.

To create an extension, create a class and extend the Extension base class. Then override required functions.

  • getId() - required - ID/Name of the extension, the name has to be unique across all extension
  • getDescription() - Description of the extension
  • onRegister() - This function will get called when register the extension to a map
  • onRemove() - This function will get called when unregister the extension from a map
  • getDataById(spaceUUID: string) - Provide additional data for to space/object based on UUID - Deprecated Use getDataBySpace instead
  • getDataBySpace(space: Space) - Provide additional data for to space/object

Inside extension class, you can access the map that extension has been registered to via getMap(). This allows you to listen and fetch database on the map lifecycle such as onLoad and onRender.

In the example, We're going to create CityWeatherExtension to fetch weather database on the current location of the building.

class CityWeatherExtension extends Extension {
  constructor() {
    super()
    this.data = undefined
    this.currentCity = ''
  }
  getId() {
    return 'cityweather'
  }
  getDescription() {
    return 'Awesome City Weather: Provide Weather information based on building location'
  }
  onRegister() {
    this.unsubscribeOnLoad = this.getMap().onLoad(async ({ building }) => {
      if (this.currentCity === building.city) {
        return
      }

      this.currentCity = building.city

      const res = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${this.currentCity}`)
      const data = await res.json()
      this.data = data
      return
    })
  }

  onRemove() {
    this.data = undefined
    this.unsubscribeOnLoad()
  }

  getDataBySpace(space) {
    // This function will get called when the map ask for data based on spaceUUID
    // such as onclick event

    // For this example
    // When click on space, the payload will contains field
    // {
    //  ...,
    //  cityweather: 'Room ...: ...F'
    // }
    return `Room ${space.number}: ${this.getTemperature()}F`
  }

  // Custom function provided by extension
  getTemperature() {
    // https://samples.openweathermap.org/data/2.5/weather?q=London,uk&appid=b6907d289e10d714a6e88b30761fae22
    return this.data.main.temp
  }
}

Register extension

Register extension by calling registerExtension on FloorMap instance

const weatherExtension = new CityWeatherExtension()

floorMap.registerExtension(weatherExtension)

Now when you switch a building, the extension will fetch the weather data for the user. Also when you click on any space, the event payload will include the data from the extension (which come from getDataById).

floorMap.addEventListener('onclick', event => {
  if (event.data) {
    console.log(event.data.cityweather)
  }
})

Access extension instance through floormap

// extensionId is the value returned from getId()
floorMap.extensions[extensionId]
floorMap.getExtension(extensionId)

// Example
floormap.extensions.cityweather
floormap.getExtension('cityweather')

Remove extension

Remove extension from floormap

floorMap.removeExtension(weatherExtension)
floorMap.removeExtension('cityweather')

Built-in extensions

Please see: https://github.com/WeConnect/floormap-extensions


API References

We provide API references documentation generated via jsdoc

# Clone
git clone git@github.com:WeConnect/floormap-sdk.git
cd floormap-sdk

yarn jsdoc

After that, the API doc will be in the docs folder.


Demo application

React application

# Clone
git clone git@github.com:WeConnect/floormap-sdk.git
cd floormap-sdk

# Install dependencies
yarn install

Open example/occupancy/index.js, then edit appId and appSecret

To request for an appId/appSecret, kindly email tech-sg@wework.com with the subject Request for FloorMap SDK credentials and a brief explanation of its intended purpose.

Then start the demo application

# Start demo and development
yarn start:demo

Open http//localhost:3000/

Plain Javascript application

# Clone
git clone git@github.com:WeConnect/floormap-sdk.git
cd floormap-sdk

# Install dependencies
yarn install

The plain demo version can be found in example/sample. Open index.html file.

Readme

Keywords

none

Package Sidebar

Install

npm i @wework/floormap-sdk

Weekly Downloads

411

Version

1.12.1

License

WeWork

Unpacked Size

934 kB

Total Files

54

Last publish

Collaborators

  • bmanley91
  • techsourcing