├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── api ├── README.md ├── barycentre.js └── max.js ├── collect ├── .vscode │ └── launch.json ├── collect-window.js ├── collect.js ├── devices.json └── package.json ├── collector.js ├── devices.json ├── mapwize ├── clanz-local.html ├── clanz.html ├── demo │ ├── actions_events.html │ ├── calendar.html │ ├── click_place.html │ ├── markers.html │ ├── measurement.html │ ├── quick_place.html │ ├── simplemap.html │ ├── tooltip.html │ ├── urls.html │ └── xy2latlng.html ├── dist │ ├── mapwize.css │ └── mapwize.js ├── doc │ └── doc.md ├── messukeskus.html └── scripts │ ├── jquery-3.3.1.min.js │ └── lodash-4.17.4.min.js ├── mock.js ├── package.json ├── server.js ├── tests └── schedule-barycentre.js └── util ├── barycentre.js ├── max.js └── tests ├── from-after-last-counter.js ├── from-and-to-extend-series.js ├── from-and-to-in-series.js ├── from-before-first-counter.js └── to-after-last-counter.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.debug 3 | 4 | work/ 5 | 6 | *-lock.json 7 | *.lock 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "API", 11 | "program": "${workspaceFolder}/server.js", 12 | "env" : { 13 | "WINDOW" : "900", 14 | "DEBUG" : "api*,collector*,barycentre*,max*", 15 | "PORT" : "8080" 16 | } 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Mock", 22 | "program": "${workspaceFolder}/mock.js", 23 | "env" : { 24 | "PORT" : "9090" 25 | } 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Cisco Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PeopleCount Collector for Room Devices [![published](https://static.production.devnetcloud.com/codeexchange/assets/images/devnet-published.svg)](https://developer.cisco.com/codeexchange/github/repo/ObjectIsAdvantag/roomkit-collector) 2 | 3 | Collects PeopleCount events from Webex Room Devices, stores them in an in-memory Time Series database to compute weighted averages over flexible time windows, and returns these data through a RESTful API. 4 | 5 | Background: 6 | - Webex Room Devices fire events every time they notice a change. As participants in meeting roms happen to move their head, it is frequent to see updates to the PeopleCount counter even though no participant entered or left the room. Thus, when queried several times, even in close intervals, the PeopleCount value returned by a Room Kit would be expected to differ. 7 | - The collector batch utility and 'barycentre' computation help create a stable counter for each device part of a RoomKit deployment. Concretely, PeopleCounter events for each device are collected and stored in an in-memory timeseries database. Moreover, a REST API lets you retreive computed averaged values for a custom period of time. 8 | 9 | This repo contains 3 components: 10 | 1. [a collector batch](collector/collector.js): collects PeopleCount events for a pre-configured list of devices, and stores them as TimeSeries (also recycles all elapsed TimeSeries, aka, out of the observation window), 11 | 2. [a 'barycentre' utility](util/barycentre.js): computes an average value from a Time Series, by weighting each value based on its duration (before next event happens, 12 | 3. [a REST API](server.js): exposes the latest and average weighted value from PeopleCount events fired by a [pre-configured list of devices](devices.json). 13 | 14 | 15 | ## Quickstart 16 | 17 | To install and configure the collector, run the instructions below: 18 | 19 | ```shell 20 | git clone https://github.com/ObjectIsAdvantag/roomkit-collector 21 | cd roomkit-collector 22 | npm install 23 | ``` 24 | 25 | Let's now configure the collector for your Room Devices deployment: 26 | Edit the [devices.json file](devices.json) with your Room Devices deployment. 27 | 28 | Here is an example of a deployment of RoomKits in the DevNet Zone: 29 | 30 | ```json 31 | [ 32 | { 33 | "id": "Workbench1", 34 | "location": "Workshop 1", 35 | "ipAddress": "192.68.1.32", 36 | "username" : "integrator", 37 | "password" : "integrator" 38 | }, 39 | { 40 | "id": "Workbench2", 41 | "location": "Workshop 2", 42 | "ipAddress": "192.68.1.33", 43 | "username" : "integrator", 44 | "password" : "integrator" 45 | }, 46 | { 47 | "id": "Workbench3", 48 | "location": "Workshop 3", 49 | "ipAddress": "192.68.1.34", 50 | "username" : "integrator", 51 | "password" : "integrator" 52 | } 53 | ] 54 | ``` 55 | 56 | Now, we will run the collector in DEBUG mode, and with an observation window of 60 seconds (time series older than 1 minute are erased): 57 | 58 | ```shell 59 | # Starts the collector collecting PeopleCount for devices listed in devices.json, and computes averages over 60s periods 60 | DEBUG=collector*,api* WINDOW=60 node server.js 61 | ... 62 | collector:fine connecting to device: Workbench1 +0ms 63 | collector:fine connecting to device: Workbench2 +16ms 64 | collector:fine connecting to device: Workbench3 +18ms 65 | collector collecting window: 60 seconds +0ms 66 | 67 | Collector API started at http://localhost:8080/ 68 | GET / for healthcheck 69 | GET /devices for the list of devices 70 | GET /devices/{device} to get the details for the specified device 71 | GET /devices/{device}/last for latest PeopleCount value received 72 | GET /devices/{device}/average?period=30 for a computed average 73 | 74 | collector:fine connexion successful for device: Workbench1 +334ms 75 | collector connected to device: Workbench1 +336ms 76 | collector:fine fetched PeopleCount for device: Workbench1 +17ms 77 | collector:fine adding count: 0, for device: Workbench1 +1ms 78 | collector:fine adding feedback listener to device: Workbench1 +1ms 79 | ... 80 | ``` 81 | 82 | All set! 83 | 84 | You can now query the Collector's API (make sure to replace `Workbench1` below by one of the devices identifier configured in your devices.json): 85 | 86 | - GET / => healthcheck 87 | - GET /devices => returns the list of devices for which data is collected 88 | - GET /devices/Workbench1 => returns the details for the specified device 89 | - GET /devices/Workbench1/last => returns the latest PeopleCount value fired by the 'Workbench1' device 90 | - GET /devices/Workbench1/max => returns the max value on the default period (15 seconds) 91 | - GET /devices/Workbench1/average?period=60 => returns an averaged PeopleCount value computed from the PeopleCount events fired by the 'Workbench1' device, over the last 60 seconds 92 | 93 | Example: 94 | 95 | `GET http://localhost:8080/devices/Workbench1/average?period=60` 96 | 97 | ```json 98 | { 99 | "device": "Workbench1", 100 | "peopleCount": 8.508, 101 | "period": "60", 102 | "unit": "seconds" 103 | } 104 | ``` 105 | 106 | _Note that the average weighted value is not rounded by default, in order to maximize your options to use these averages._ 107 | 108 | 109 | ## Mock service 110 | 111 | For tests purpose, a mock mimics the collector API and returns random data for the same list of devices. 112 | 113 | ```shell 114 | DEBUG=collector*,api* WINDOW=60 node mock.js 115 | ... 116 | Collector API started at http://localhost:8080/ 117 | GET / for healthcheck 118 | GET /devices for the list of devices 119 | GET /devices/{device} to get the details for the specified device 120 | GET /devices/{device}/last for latest PeopleCount value received 121 | GET /devices/{device}/average?period=30 for a computed average 122 | 123 | api:fine returned mock latest: 7, for device: Workbench1 +0ms 124 | api:fine returned mock latest: 4, for device: Workbench1 +2s 125 | api:fine returned mock latest: 6, for device: Workbench1 +879ms 126 | api:fine returned mock average: -1, for device: Workbench1 +9s 127 | api:fine returned mock average: 1, for device: Workbench1 +3s 128 | ... 129 | ``` 130 | 131 | 132 | ## History 133 | 134 | v1.0: release for DevNet Automation Exchange 135 | 136 | v0.5: updates for Cisco Live US 2018 137 | 138 | v0.4: updates for [DevNet Create](https://devnetcreate.io/) with a [React Map](https://github.com/ObjectIsAdvantag/roomkit-react-map) companion 139 | 140 | v0.3: updates for [Cisco Connect Finland](https://www.cisco.com/c/m/fi_fi/training-events/2018/cisco-connect/index.html#~stickynav=2) (Messukeskus) 141 | 142 | v0.2: updates for [Cisco Live Melbourne](https://www.ciscolive.com/anz/) 143 | 144 | v0.1: created at [BCX18 - Bosch IoT Hackathon Berlin](https://github.com/ObjectIsAdvantag/hackathon-resources/tree/master/bcx18-berlin) 145 | -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # API to expose RoomKits collected analytics 2 | 3 | ## Quick start 4 | 5 | Fill in your RoomKits info into [devices.json](devices.json) 6 | 7 | To run the api in debug mode, type 8 | ```shell 9 | DEBUG=api*,collector* node server.js 10 | ``` 11 | 12 | Invoke the REST Resources 13 | 14 | - GET / => healthcheck 15 | - GET /devices/Workbench1/last => returns the latest PeopleCount value fired by the 'Theater' device 16 | - GET /devices/Workbench1/average?period=30 => returns an averaged PeopleCount value computed from the PeopleCount events fired by the 'Theater' device, over the last 30 seconds 17 | 18 | ```json 19 | { 20 | "device": "Theater", 21 | "peopleCount": 8.508, 22 | "period": "30", 23 | "unit": "seconds" 24 | } 25 | ``` 26 | 27 | -------------------------------------------------------------------------------- /api/barycentre.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | const debug = require("debug")("barycentre") 8 | const fine = require("debug")("barycentre:fine") 9 | 10 | 11 | // A series is an ordered collection of ticks and values 12 | // tick are formatted as dates 13 | 14 | // Computes the weighted average on the period (begin/end) 15 | // for the time-series values (as a map of time / values) 16 | // The algorithm considers the values are substains till next serie is registered 17 | module.exports.computeBarycentre = function (series, from, to) { 18 | let computed = 0; 19 | let previousSerie = undefined; 20 | let begun = false; 21 | let skippedMilliseconds = 0; // Store periods where the device was not counting (typically went to standby mode) 22 | 23 | fine(`requested to compute barycentre from: ${from}, to: ${to}, with ${series.length} values in serie`) 24 | 25 | for (var i = 0; i < series.length; i++) { 26 | var serie = series[i]; 27 | if (serie[0] <= from) { 28 | // We've not started yet, let's skip to next serie 29 | previousSerie = serie; 30 | continue; 31 | } 32 | 33 | if (!begun) { 34 | if (!previousSerie) { 35 | // throw an error if from is before the series begins 36 | throw new Error("from is before serie begins") 37 | } 38 | 39 | previousSerie = [ from, previousSerie[1]]; 40 | begun = true; 41 | } 42 | 43 | if (serie[0] >= to) { 44 | // we are done, add serie's value till 'to' 45 | computed += previousSerie[1] * (new Date(to).getTime() - new Date(previousSerie[0]).getTime()); 46 | previousSerie = serie; 47 | break; 48 | } 49 | 50 | // if the value to add is negative, we were not counting on this period, simply skip it 51 | if (previousSerie[1] < 0) { 52 | skippedMilliseconds = new Date(serie[0]).getTime() - new Date(previousSerie[0]).getTime(); 53 | } 54 | else { 55 | // let's add the value for the period 56 | computed += previousSerie[1] * (new Date(serie[0]).getTime() - new Date(previousSerie[0]).getTime()); 57 | } 58 | 59 | previousSerie = serie; 60 | } 61 | 62 | // Compute average for standard cases between to and from bounts 63 | // 2 exceptions though, when from or to are after the last series 64 | // Exception 1: from is after last serie 65 | let barycentre; 66 | if (!begun) { 67 | barycentre = previousSerie[1]; 68 | } 69 | // Exception 2: to is after last serie 70 | else if (to >= previousSerie[0]) { 71 | // add serie's value till 'to' 72 | computed += previousSerie[1] * (new Date(to).getTime() - new Date(previousSerie[0]).getTime()); 73 | } 74 | if (!barycentre) { 75 | barycentre = computed / (new Date(to).getTime() - new Date(from).getTime() - skippedMilliseconds); 76 | } 77 | 78 | fine(`computed barycentre: ${barycentre}`) 79 | return barycentre; 80 | } 81 | -------------------------------------------------------------------------------- /api/max.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | const debug = require("debug")("max"); 7 | const fine = require("debug")("max:fine"); 8 | 9 | 10 | // A series is an ordered collection of ticks and values 11 | // tick are formatted as dates 12 | 13 | // Returns the max value on the period (begin/end) 14 | // for the time-series values (as a map of time / values) 15 | module.exports.max = function (series, from, to) { 16 | fine(`requested to compute max from: ${from}, to: ${to}, with ${series.length} values in serie`) 17 | 18 | let max = -1; 19 | for (var i = 0; i < series.length; i++) { 20 | var serie = series[i]; 21 | if (serie[0] < from) { 22 | // We've not started yet, let's skip to next serie 23 | continue; 24 | } 25 | 26 | if (serie[0] > to) { 27 | // we are done, add serie's value till 'to' 28 | break; 29 | } 30 | 31 | // if the value to add is negative, we were not counting on this period, simply skip it 32 | if (serie[1] > max) { 33 | max = serie[1] 34 | } 35 | } 36 | 37 | fine(`found max value: ${max}`) 38 | return max; 39 | } 40 | -------------------------------------------------------------------------------- /collect/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "API", 11 | "cwd": "${workspaceFolder}/api", 12 | "program": "${workspaceFolder}/api/server.js", 13 | "env" : { 14 | "DEBUG" : "collector*, api*", 15 | "WINDOW" : 30 16 | } 17 | }, 18 | { 19 | "type": "node", 20 | "request": "launch", 21 | "name": "Collect", 22 | "cwd": "${workspaceFolder}/collector", 23 | "program": "${workspaceFolder}/collector/collect.js", 24 | "env" : { 25 | "DEBUG" : "collector*" 26 | } 27 | }, 28 | { 29 | "type": "node", 30 | "request": "launch", 31 | "name": "Collect (Window)", 32 | "cwd": "${workspaceFolder}/collector", 33 | "program": "${workspaceFolder}/collector/collect-window.js", 34 | "env" : { 35 | "DEBUG" : "collector*", 36 | "WINDOW" : 30 37 | } 38 | }, 39 | { 40 | "type": "node", 41 | "request": "launch", 42 | "name": "Collector (Barycentre)", 43 | "cwd": "${workspaceFolder}/collector/tests", 44 | "program": "${workspaceFolder}/collector/tests/schedule-barycentre.js", 45 | "env" : { 46 | "DEBUG" : "*", 47 | "WINDOW" : 900 48 | } 49 | }, 50 | { 51 | "type": "node", 52 | "request": "launch", 53 | "name": "Barycentre", 54 | "cwd": "${workspaceFolder}/util/tests", 55 | "program": "${workspaceFolder}/util/tests/to-after-last-counter.js", 56 | "env" : { 57 | "DEBUG" : "*", 58 | "WINDOW" : 900 59 | } 60 | } 61 | ] 62 | } -------------------------------------------------------------------------------- /collect/collect-window.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | /** 8 | * Collects PeopleCount analystics from a collection of RoomKits 9 | * with a moving window interval 10 | */ 11 | 12 | 13 | const debug = require("debug")("collector"); 14 | const fine = require("debug")("collector:fine"); 15 | 16 | 17 | // Connects to a Room Device 18 | const jsxapi = require("jsxapi"); 19 | function connect(device) { 20 | return new Promise(function (resolve, reject) { 21 | 22 | // Connect via SSH 23 | const xapi = jsxapi.connect(`ssh://${device.ip}`, { 24 | username: device.username, 25 | password: device.password 26 | }); 27 | xapi.on('error', (err) => { 28 | debug(`connexion failed: ${err}`) 29 | reject(err) 30 | }); 31 | xapi.on('ready', () => { 32 | fine("connexion successful"); 33 | resolve(xapi); 34 | }); 35 | }); 36 | } 37 | 38 | // Time series storage 39 | const stores = {}; 40 | function createStore(device) { 41 | stores[device.name] = []; 42 | } 43 | function addCounter(device, date, count) { 44 | fine(`adding count: ${count}, for device: ${device.name}`) 45 | 46 | const store = stores[device.name]; 47 | store.push([date.toISOString(), count]); 48 | } 49 | 50 | 51 | // Initialize listeners for each device 52 | const devices = require("./devices.json"); 53 | devices.forEach(device => { 54 | 55 | fine(`connecting to device: ${device.name}`) 56 | connect(device) 57 | 58 | .then(xapi => { 59 | debug(`connected to device: ${device.name}`); 60 | 61 | // Check devices can count 62 | xapi.status 63 | .get('RoomAnalytics PeopleCount') 64 | .then((counter) => { 65 | fine(`fetched PeopleCount for device: ${device.name}`); 66 | 67 | // Abort if device does not count 68 | var count = counter.Current; 69 | if (count == -1) { 70 | debug(`device is not counting: ${device.name}`); 71 | return; 72 | } 73 | 74 | // Store first TimeSeries 75 | createStore(device); 76 | addCounter(device, new Date(Date.now()), count); 77 | 78 | // Listen to events 79 | fine(`adding feedback listener to device: ${device.name}`); 80 | xapi.feedback.on('/Status/RoomAnalytics/PeopleCount', (counter) => { 81 | fine(`new PeopleCount for device: ${device.name}`); 82 | 83 | if (count == -1) { 84 | debug(`WARNING: device has stopped counting: ${device.name}`); 85 | return; 86 | } 87 | 88 | // register new TimeSeries 89 | var count = counter.Current; 90 | addCounter(device, new Date(Date.now()), count); 91 | }); 92 | 93 | }) 94 | .catch((err) => { 95 | console.log(`Failed to fetch PeopleCount, err: ${err.message}`); 96 | console.log(`Are you interacting with a RoomKit? exiting...`); 97 | xapi.close(); 98 | }); 99 | }) 100 | .catch(err => { 101 | debug(`could not connect device: ${device.name}`) 102 | }) 103 | }); 104 | 105 | 106 | // 107 | // Clean TimeSeries 108 | // 109 | 110 | // Collect interval (moving window of collected time series) 111 | var window = process.env.WINDOW ? process.env.WINDOW : 15 * 60; // in seconds 112 | debug(`collecting window: ${window} second(s)`); 113 | 114 | // Individual store cleaner 115 | function cleanStore(store) { 116 | const oldest = Date.now() - window*1000; 117 | 118 | const lowestDate = new Date(oldest).toISOString(); 119 | store.forEach(serie => { 120 | if (serie[0] < lowestDate) { 121 | store.shift() 122 | } 123 | }); 124 | } 125 | 126 | // Dump time series in a store 127 | function dumpSeries(store) { 128 | store.forEach(serie => { 129 | fine(`time: ${serie[0]}, count: ${serie[1]}`); 130 | }); 131 | } 132 | 133 | // Run cleaner 134 | setInterval(function () { 135 | Object.keys(stores).forEach((key) => { 136 | fine(`cleaning TimeSeries for device: ${key}`); 137 | 138 | const store = stores[key]; 139 | cleanStore(store); 140 | 141 | if ("production" !== process.env.NODE_ENV) { 142 | fine(`dumping stored series for device: ${key}`); 143 | dumpSeries(store); 144 | } 145 | }) 146 | }, window * 1000); // in milliseconds 147 | -------------------------------------------------------------------------------- /collect/collect.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Collects PeopleCount analystics from a collection of RoomKits 8 | */ 9 | 10 | const debug = require("debug")("collector"); 11 | const fine = require("debug")("collector:fine"); 12 | 13 | 14 | // Check args 15 | // if (!process.env.JSXAPI_DEVICE_URL || !process.env.JSXAPI_USERNAME) { 16 | // console.info("Please specify info to connect to your device as JSXAPI_DEVICE_URL, JSXAPI_USERNAME, JSXAPI_PASSWORD env variables"); 17 | // console.info("Bash example: JSXAPI_DEVICE_URL='ssh://10.10.1.52' JSXAPI_USERNAME='integrator' JSXAPI_PASSWORD='integrator' node example.js"); 18 | // process.exit(1); 19 | // } 20 | 21 | // Connects to a Room Device 22 | const jsxapi = require("jsxapi"); 23 | function connect(device) { 24 | return new Promise(function (resolve, reject) { 25 | 26 | // Connect via SSH 27 | const xapi = jsxapi.connect(`ssh://${device.ip}`, { 28 | username: device.username, 29 | password: device.password 30 | }); 31 | xapi.on('error', (err) => { 32 | debug(`connexion failed: ${err}`) 33 | reject(err) 34 | }); 35 | xapi.on('ready', () => { 36 | fine("connexion successful"); 37 | resolve(xapi); 38 | }); 39 | }); 40 | } 41 | 42 | // Time series storage 43 | const stores = {}; 44 | function createStore(device) { 45 | stores[device.name] = []; 46 | } 47 | function addCounter(device, date, count) { 48 | fine(`adding count: ${count}, for device: ${device.name}`) 49 | 50 | const store = stores[device.name]; 51 | store.push([date.toISOString(), count]); 52 | } 53 | 54 | 55 | const devices = require("./devices.json"); 56 | 57 | devices.forEach(device => { 58 | 59 | fine(`connecting to device: ${device.name}`) 60 | connect(device) 61 | 62 | .then(xapi => { 63 | debug(`connected to device: ${device.name}`); 64 | 65 | // Check devices can count 66 | xapi.status 67 | .get('RoomAnalytics PeopleCount') 68 | .then((counter) => { 69 | fine(`fetched PeopleCount for device: ${device.name}`); 70 | 71 | // Abort if device does not count 72 | var count = counter.Current; 73 | if (count == -1) { 74 | debug(`device is not counting: ${device.name}`); 75 | return; 76 | } 77 | 78 | // Store first TimeSeries 79 | createStore(device); 80 | addCounter(device, new Date(Date.now()), count); 81 | 82 | // Listen to events 83 | fine(`adding feedback listener to device: ${device.name}`); 84 | xapi.feedback.on('/Status/RoomAnalytics/PeopleCount', (counter) => { 85 | fine(`new PeopleCount for device: ${device.name}`); 86 | 87 | if (count == -1) { 88 | debug(`WARNING: device has stopped counting: ${device.name}`); 89 | return; 90 | } 91 | 92 | // register new TimeSeries 93 | var count = counter.Current; 94 | addCounter(device, new Date(Date.now()), count); 95 | }); 96 | 97 | }) 98 | .catch((err) => { 99 | console.log(`Failed to fetch PeopleCount, err: ${err.message}`); 100 | console.log(`Are you interacting with a RoomKit? exiting...`); 101 | xapi.close(); 102 | }); 103 | }) 104 | .catch(err => { 105 | debug(`could not connect device: ${device.name}`) 106 | }) 107 | }); 108 | -------------------------------------------------------------------------------- /collect/devices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "Theater", 4 | "location": "CLANZ DevNetZone Theater", 5 | "ip" : "192.168.1.35", 6 | "username" : "integrator", 7 | "password" : "integrator" 8 | }, 9 | { 10 | "name": "Workshop1", 11 | "location": "CLANZ DevNetZone Workshop1", 12 | "ip" : "100.103.1.11", 13 | "username" : "integrator", 14 | "password" : "integrator" 15 | } 16 | ] -------------------------------------------------------------------------------- /collect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "batch", 3 | "version": "0.1.0", 4 | "description": "Aggregates PeopleCount metrics from RoomKits", 5 | "main": "collect-window.js", 6 | "dependencies": { 7 | "debug": "^3.1.0", 8 | "jsxapi": "^4.1.2" 9 | }, 10 | "devDependencies": {}, 11 | "scripts": { 12 | "test": "echo \"Error: no test specified\" && exit 1" 13 | }, 14 | "author": "Stève Sfartz ", 15 | "license": "MIT" 16 | } 17 | -------------------------------------------------------------------------------- /collector.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | /** 8 | * Collects PeopleCount analystics from a collection of RoomKits 9 | * with a moving window interval 10 | */ 11 | 12 | 13 | const debug = require("debug")("collector"); 14 | const fine = require("debug")("collector:fine"); 15 | 16 | 17 | // Connects to a Room Device 18 | const jsxapi = require("jsxapi"); 19 | function connect(device) { 20 | return new Promise(function (resolve, reject) { 21 | 22 | // Connect via SSH 23 | const xapi = jsxapi.connect(`ssh://${device.ipAddress}`, { 24 | username: device.username, 25 | password: device.password 26 | }); 27 | xapi.on('error', (err) => { 28 | debug(`connexion failed for device: ${device.id}, ip address: ${device.ipAddress}, with err: ${err}`) 29 | reject(err) 30 | }); 31 | xapi.on('ready', () => { 32 | fine(`connexion successful for device: ${device.id}`); 33 | resolve(xapi); 34 | }); 35 | }); 36 | } 37 | 38 | // Time series storage 39 | const stores = {}; 40 | function createStore(device) { 41 | stores[device.id] = []; 42 | } 43 | function addCounter(device, date, count) { 44 | fine(`adding count: ${count}, for device: ${device.id}`) 45 | 46 | const store = stores[device.id]; 47 | store.push([date.toISOString(), count]); 48 | } 49 | 50 | 51 | // Initialize listeners for each device 52 | const devices = require("./devices.json"); 53 | devices.forEach(device => { 54 | 55 | fine(`connecting to device: ${device.id}`) 56 | connect(device) 57 | 58 | .then(xapi => { 59 | debug(`connected to device: ${device.id}`); 60 | 61 | // Check devices can count 62 | xapi.status 63 | .get('RoomAnalytics PeopleCount') 64 | .then((counter) => { 65 | fine(`fetched PeopleCount for device: ${device.id}`); 66 | 67 | // Abort if device does not count 68 | var count = counter.Current; 69 | if (count == -1) { 70 | debug(`device is not counting: ${device.id}`); 71 | return; 72 | } 73 | 74 | // Store first TimeSeries 75 | createStore(device); 76 | addCounter(device, new Date(Date.now()), count); 77 | 78 | // Listen to events 79 | fine(`adding feedback listener to device: ${device.id}`); 80 | xapi.feedback.on('/Status/RoomAnalytics/PeopleCount', (counter) => { 81 | fine(`new PeopleCount: ${counter.Current}, for device: ${device.id}`); 82 | 83 | // fetch PeopleCount value 84 | var count = parseInt(counter.Current); // turn from string to integer 85 | if (count == -1) { 86 | debug(`WARNING: device '${device.id}' has stopped counting`); 87 | return; 88 | } 89 | 90 | // register new TimeSeries 91 | addCounter(device, new Date(Date.now()), count); 92 | }); 93 | 94 | }) 95 | .catch((err) => { 96 | debug(`Failed to retrieve PeopleCount status for device: ${device.id}, err: ${err.message}`); 97 | console.log(`Please check your configuration: seems that '${device.id}' is NOT a Room device.`); 98 | xapi.close(); 99 | }); 100 | }) 101 | .catch(err => { 102 | debug(`Could not connect device: ${device.id}`) 103 | }) 104 | }); 105 | 106 | 107 | // 108 | // Clean TimeSeries 109 | // 110 | 111 | // Collect interval (moving window of collected time series) 112 | var window = process.env.WINDOW ? process.env.WINDOW : 15 * 60; // in seconds 113 | debug(`collecting window: ${window} seconds`); 114 | 115 | // Individual store cleaner 116 | function cleanStore(store) { 117 | const oldest = Date.now() - window*1000; 118 | 119 | const lowestDate = new Date(oldest).toISOString(); 120 | store.forEach(serie => { 121 | if (serie[0] < lowestDate) { 122 | store.shift() 123 | } 124 | }); 125 | } 126 | 127 | // Dump time series in a store 128 | function dumpSeries(store) { 129 | store.forEach(serie => { 130 | fine(`time: ${serie[0]}, count: ${serie[1]}`); 131 | }); 132 | } 133 | 134 | // Run cleaner 135 | setInterval(function () { 136 | Object.keys(stores).forEach((key) => { 137 | fine(`cleaning TimeSeries for device: ${key}`); 138 | 139 | const store = stores[key]; 140 | cleanStore(store); 141 | 142 | if ("production" !== process.env.NODE_ENV) { 143 | fine(`dumping stored series for device: ${key}`); 144 | dumpSeries(store); 145 | } 146 | }) 147 | }, window * 1000); // in milliseconds 148 | 149 | 150 | // 151 | // Return people count for the device and averaged on the period (in seconds) 152 | // 153 | 154 | const { computeBarycentre } = require("./util/barycentre"); 155 | 156 | module.exports.averageOnPeriod = function (device, period) { 157 | fine(`searching store for device: ${device}`); 158 | 159 | const store = stores[device]; 160 | if (!store) { 161 | fine(`could not find store for device: ${device}`); 162 | return undefined; 163 | } 164 | 165 | fine(`found store for device: ${device}`); 166 | 167 | // Compute average 168 | const to = new Date(Date.now()).toISOString(); 169 | const from = new Date(Date.now() - period*1000).toISOString(); 170 | const avg = computeBarycentre(store, from, to); 171 | fine(`computed avg: ${avg}, over last: ${period} seconds, for device: ${device}`); 172 | 173 | return avg; 174 | } 175 | 176 | module.exports.latest = function (device) { 177 | fine(`searching store for device: ${device}`); 178 | const store = stores[device]; 179 | if (!store) { 180 | fine(`could not find store for device: ${device}`); 181 | return undefined; 182 | } 183 | fine(`found store for device: ${device}`); 184 | 185 | // Looking for last serie 186 | const lastSerie = store[store.length - 1]; 187 | fine(`found last serie with value: ${lastSerie[1]}, date: ${lastSerie[0]}, for device: ${device}`); 188 | 189 | return lastSerie[1]; 190 | } 191 | 192 | const { max } = require("./util/max"); 193 | 194 | module.exports.max = function (device) { 195 | fine(`searching store for device: ${device}`); 196 | const store = stores[device]; 197 | if (!store) { 198 | fine(`could not find store for device: ${device}`); 199 | return undefined; 200 | } 201 | fine(`found store for device: ${device}`); 202 | 203 | // Looking for max value in series 204 | const to = new Date(Date.now()).toISOString(); 205 | const from = new Date(Date.now() - period*1000).toISOString(); 206 | const maxSerie = computeMax(store, from, to); 207 | 208 | fine(`found max value: ${maxSerie[1]}, date: ${maxSerie[0]}, for device: ${device}`); 209 | 210 | return maxSerie; 211 | } 212 | -------------------------------------------------------------------------------- /devices.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "Workbench1", 4 | "location": "Workshop 1", 5 | "ipAddress" : "192.168.1.32", 6 | "username" : "integrator", 7 | "password" : "integrator" 8 | }, 9 | { 10 | "id": "Workbench2", 11 | "location": "Workshop 2", 12 | "ipAddress" : "192.168.1.33", 13 | "username" : "integrator", 14 | "password" : "integrator" 15 | }, 16 | { 17 | "id": "Workbench3", 18 | "location": "Workshop 3", 19 | "ipAddress" : "192.168.1.34", 20 | "username" : "integrator", 21 | "password" : "integrator" 22 | }, 23 | { 24 | "id": "Workbench4", 25 | "location": "Workshop 4", 26 | "ipAddress" : "192.168.1.35", 27 | "username" : "integrator", 28 | "password" : "integrator" 29 | } 30 | ] -------------------------------------------------------------------------------- /mapwize/clanz-local.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mapwize.js Demo - simple map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /mapwize/clanz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mapwize.js Demo - simple map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /mapwize/demo/actions_events.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - Actions & Events 6 | 7 | 8 | 9 | 10 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 35 | 36 | 37 | 38 | 39 | 40 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /mapwize/demo/calendar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - Calendar 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 28 | 29 | 30 | 31 | 32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | 44 |
45 | 46 | 47 | 268 | 269 | 270 | -------------------------------------------------------------------------------- /mapwize/demo/click_place.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - Click on place 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |

Place data

33 |
34 |
35 |

Id:

36 |

Name:

37 |

Alias:

38 | 39 |

translations:

40 |
41 | 42 |

Data:

43 |
44 |
45 |
46 |
47 |
48 |
49 | 50 | 51 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /mapwize/demo/markers.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - markers 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /mapwize/demo/measurement.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - simple map 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /mapwize/demo/quick_place.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - Quick places 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 46 |
47 |
48 | 49 | 50 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /mapwize/demo/simplemap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - simple map 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /mapwize/demo/tooltip.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - simple map 6 | 7 | 8 | 9 | 10 | 11 | 12 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 93 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /mapwize/demo/urls.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - Urls 6 | 7 | 8 | 9 | 10 | 11 | 12 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 |

28 | 29 | 30 |

31 |
32 | 33 | 34 | 35 |

Template URLs: (click to select)

36 | 48 | 49 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /mapwize/demo/xy2latlng.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Mapwize.js Demo - XY 2 LatLng 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 34 | 35 | 36 | 37 | 38 |
39 | 40 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /mapwize/dist/mapwize.css: -------------------------------------------------------------------------------- 1 | .leaflet-image-layer,.leaflet-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane,.leaflet-pane>canvas,.leaflet-pane>svg,.leaflet-tile,.leaflet-tile-container,.leaflet-zoom-box{position:absolute;left:0;top:0}.leaflet-container{overflow:hidden}.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-tile{-webkit-user-select:none;-moz-user-select:none;user-select:none;-webkit-user-drag:none}.leaflet-safari .leaflet-tile{image-rendering:-webkit-optimize-contrast}.leaflet-safari .leaflet-tile-container{width:1600px;height:1600px;-webkit-transform-origin:0 0}.leaflet-marker-icon,.leaflet-marker-shadow{display:block}.leaflet-container .leaflet-marker-pane img,.leaflet-container .leaflet-overlay-pane svg,.leaflet-container .leaflet-shadow-pane img,.leaflet-container .leaflet-tile-pane img,.leaflet-container img.leaflet-image-layer{max-width:none!important}.leaflet-container.leaflet-touch-zoom{-ms-touch-action:pan-x pan-y;touch-action:pan-x pan-y}.leaflet-container.leaflet-touch-drag{-ms-touch-action:pinch-zoom}.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom{-ms-touch-action:none;touch-action:none}.leaflet-tile{filter:inherit;visibility:hidden}.leaflet-tile-loaded{visibility:inherit}.leaflet-zoom-box{width:0;height:0;-moz-box-sizing:border-box;box-sizing:border-box;z-index:800}.leaflet-overlay-pane svg{-moz-user-select:none}.leaflet-pane{z-index:400}.leaflet-tile-pane{z-index:200}.leaflet-overlay-pane{z-index:400}.leaflet-shadow-pane{z-index:500}.leaflet-marker-pane{z-index:600}.leaflet-tooltip-pane{z-index:650}.leaflet-popup-pane{z-index:700}.leaflet-map-pane canvas{z-index:100}.leaflet-map-pane svg{z-index:200}.leaflet-vml-shape{width:1px;height:1px}.lvml{behavior:url(#default#VML);display:inline-block;position:absolute}.leaflet-control{position:relative;z-index:800;pointer-events:visiblePainted;pointer-events:auto}.leaflet-bottom,.leaflet-top{position:absolute;z-index:1000;pointer-events:none}.leaflet-top{top:0}.leaflet-right{right:0}.leaflet-bottom{bottom:0}.leaflet-left{left:0}.leaflet-control{float:left;clear:both}.leaflet-right .leaflet-control{float:right}.leaflet-top .leaflet-control{margin-top:10px}.leaflet-bottom .leaflet-control{margin-bottom:10px}.leaflet-left .leaflet-control{margin-left:10px}.leaflet-right .leaflet-control{margin-right:10px}.leaflet-fade-anim .leaflet-tile{will-change:opacity}.leaflet-fade-anim .leaflet-popup{opacity:0;-webkit-transition:opacity .2s linear;-moz-transition:opacity .2s linear;-o-transition:opacity .2s linear;transition:opacity .2s linear}.leaflet-fade-anim .leaflet-map-pane .leaflet-popup{opacity:1}.leaflet-zoom-animated{-webkit-transform-origin:0 0;-ms-transform-origin:0 0;transform-origin:0 0}.leaflet-zoom-anim .leaflet-zoom-animated{will-change:transform}.leaflet-zoom-anim .leaflet-zoom-animated{-webkit-transition:-webkit-transform .25s cubic-bezier(0,0,.25,1);-moz-transition:-moz-transform .25s cubic-bezier(0,0,.25,1);-o-transition:-o-transform .25s cubic-bezier(0,0,.25,1);transition:transform .25s cubic-bezier(0,0,.25,1)}.leaflet-pan-anim .leaflet-tile,.leaflet-zoom-anim .leaflet-tile{-webkit-transition:none;-moz-transition:none;-o-transition:none;transition:none}.leaflet-zoom-anim .leaflet-zoom-hide{visibility:hidden}.leaflet-interactive{cursor:pointer}.leaflet-grab{cursor:-webkit-grab;cursor:-moz-grab}.leaflet-crosshair,.leaflet-crosshair .leaflet-interactive{cursor:crosshair}.leaflet-control,.leaflet-popup-pane{cursor:auto}.leaflet-dragging .leaflet-grab,.leaflet-dragging .leaflet-grab .leaflet-interactive,.leaflet-dragging .leaflet-marker-draggable{cursor:move;cursor:-webkit-grabbing;cursor:-moz-grabbing}.leaflet-image-layer,.leaflet-marker-icon,.leaflet-marker-shadow,.leaflet-pane>svg path,.leaflet-tile-container{pointer-events:none}.leaflet-image-layer.leaflet-interactive,.leaflet-marker-icon.leaflet-interactive,.leaflet-pane>svg path.leaflet-interactive{pointer-events:visiblePainted;pointer-events:auto}.leaflet-container{background:#ddd;outline:0}.leaflet-container a{color:#0078a8}.leaflet-container a.leaflet-active{outline:2px solid orange}.leaflet-zoom-box{border:2px dotted #38f;background:rgba(255,255,255,.5)}.leaflet-container{font:12px/1.5 "Helvetica Neue",Arial,Helvetica,sans-serif}.leaflet-bar{box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.leaflet-bar a,.leaflet-bar a:hover{background-color:#fff;border-bottom:1px solid #ccc;width:26px;height:26px;line-height:26px;display:block;text-align:center;text-decoration:none;color:#000}.leaflet-bar a,.leaflet-control-layers-toggle{background-position:50% 50%;background-repeat:no-repeat;display:block}.leaflet-bar a:hover{background-color:#f4f4f4}.leaflet-bar a:first-child{border-top-left-radius:4px;border-top-right-radius:4px}.leaflet-bar a:last-child{border-bottom-left-radius:4px;border-bottom-right-radius:4px;border-bottom:none}.leaflet-bar a.leaflet-disabled{cursor:default;background-color:#f4f4f4;color:#bbb}.leaflet-touch .leaflet-bar a{width:30px;height:30px;line-height:30px}.leaflet-control-zoom-in,.leaflet-control-zoom-out{font:bold 18px 'Lucida Console',Monaco,monospace;text-indent:1px}.leaflet-control-zoom-out{font-size:20px}.leaflet-touch .leaflet-control-zoom-in{font-size:22px}.leaflet-touch .leaflet-control-zoom-out{font-size:24px}.leaflet-control-layers{box-shadow:0 1px 5px rgba(0,0,0,.4);background:#fff;border-radius:5px}.leaflet-control-layers-toggle{background-image:url(images/layers.png);width:36px;height:36px}.leaflet-retina .leaflet-control-layers-toggle{background-image:url(images/layers-2x.png);background-size:26px 26px}.leaflet-touch .leaflet-control-layers-toggle{width:44px;height:44px}.leaflet-control-layers .leaflet-control-layers-list,.leaflet-control-layers-expanded .leaflet-control-layers-toggle{display:none}.leaflet-control-layers-expanded .leaflet-control-layers-list{display:block;position:relative}.leaflet-control-layers-expanded{padding:6px 10px 6px 6px;color:#333;background:#fff}.leaflet-control-layers-scrollbar{overflow-y:scroll;padding-right:5px}.leaflet-control-layers-selector{margin-top:2px;position:relative;top:1px}.leaflet-control-layers label{display:block}.leaflet-control-layers-separator{height:0;border-top:1px solid #ddd;margin:5px -10px 5px -6px}.leaflet-default-icon-path{background-image:url(images/marker-icon.png)}.leaflet-container .leaflet-control-attribution{background:#fff;background:rgba(255,255,255,.7);margin:0}.leaflet-control-attribution,.leaflet-control-scale-line{padding:0 5px;color:#333}.leaflet-control-attribution a{text-decoration:none}.leaflet-control-attribution a:hover{text-decoration:underline}.leaflet-container .leaflet-control-attribution,.leaflet-container .leaflet-control-scale{font-size:11px}.leaflet-left .leaflet-control-scale{margin-left:5px}.leaflet-bottom .leaflet-control-scale{margin-bottom:5px}.leaflet-control-scale-line{border:2px solid #777;border-top:none;line-height:1.1;padding:2px 5px 1px;font-size:11px;white-space:nowrap;overflow:hidden;-moz-box-sizing:border-box;box-sizing:border-box;background:#fff;background:rgba(255,255,255,.5)}.leaflet-control-scale-line:not(:first-child){border-top:2px solid #777;border-bottom:none;margin-top:-2px}.leaflet-control-scale-line:not(:first-child):not(:last-child){border-bottom:2px solid #777}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-attribution,.leaflet-touch .leaflet-control-layers{box-shadow:none}.leaflet-touch .leaflet-bar,.leaflet-touch .leaflet-control-layers{border:2px solid rgba(0,0,0,.2);background-clip:padding-box}.leaflet-popup{position:absolute;text-align:center;margin-bottom:20px}.leaflet-popup-content-wrapper{padding:1px;text-align:left;border-radius:12px}.leaflet-popup-content{margin:13px 19px;line-height:1.4}.leaflet-popup-content p{margin:18px 0}.leaflet-popup-tip-container{width:40px;height:20px;position:absolute;left:50%;margin-left:-20px;overflow:hidden;pointer-events:none}.leaflet-popup-tip{width:17px;height:17px;padding:1px;margin:-10px auto 0;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.leaflet-popup-content-wrapper,.leaflet-popup-tip{background:#fff;color:#333;box-shadow:0 3px 14px rgba(0,0,0,.4)}.leaflet-container a.leaflet-popup-close-button{position:absolute;top:0;right:0;padding:4px 4px 0 0;border:none;text-align:center;width:18px;height:14px;font:16px/14px Tahoma,Verdana,sans-serif;color:#c3c3c3;text-decoration:none;font-weight:700;background:0 0}.leaflet-container a.leaflet-popup-close-button:hover{color:#999}.leaflet-popup-scrolled{overflow:auto;border-bottom:1px solid #ddd;border-top:1px solid #ddd}.leaflet-oldie .leaflet-popup-content-wrapper{zoom:1}.leaflet-oldie .leaflet-popup-tip{width:24px;margin:0 auto}.leaflet-oldie .leaflet-popup-tip-container{margin-top:-1px}.leaflet-oldie .leaflet-control-layers,.leaflet-oldie .leaflet-control-zoom,.leaflet-oldie .leaflet-popup-content-wrapper,.leaflet-oldie .leaflet-popup-tip{border:1px solid #999}.leaflet-div-icon{background:#fff;border:1px solid #666}.leaflet-tooltip{position:absolute;padding:6px;background-color:#fff;border:1px solid #fff;border-radius:3px;color:#222;white-space:nowrap;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;pointer-events:none;box-shadow:0 1px 3px rgba(0,0,0,.4)}.leaflet-tooltip.leaflet-clickable{cursor:pointer;pointer-events:auto}.leaflet-tooltip-bottom:before,.leaflet-tooltip-left:before,.leaflet-tooltip-right:before,.leaflet-tooltip-top:before{position:absolute;pointer-events:none;border:6px solid transparent;background:0 0;content:""}.leaflet-tooltip-bottom{margin-top:6px}.leaflet-tooltip-top{margin-top:-6px}.leaflet-tooltip-bottom:before,.leaflet-tooltip-top:before{left:50%;margin-left:-6px}.leaflet-tooltip-top:before{bottom:0;margin-bottom:-12px;border-top-color:#fff}.leaflet-tooltip-bottom:before{top:0;margin-top:-12px;margin-left:-6px;border-bottom-color:#fff}.leaflet-tooltip-left{margin-left:-6px}.leaflet-tooltip-right{margin-left:6px}.leaflet-tooltip-left:before,.leaflet-tooltip-right:before{top:50%;margin-top:-6px}.leaflet-tooltip-left:before{right:0;margin-right:-12px;border-left-color:#fff}.leaflet-tooltip-right:before{left:0;margin-left:-12px;border-right-color:#fff}.noClickable{pointer-events:none!important}.leaflet-container a.leaflet-active{outline:0}.leaflet-bottom{-webkit-transition:bottom .25s ease;-moz-transition:bottom .25s ease;-ms-transition:bottom .25s ease;-o-transition:bottom .25s ease;transition:bottom .25s ease}.leaflet-top{-webkit-transition:top .25s ease;-moz-transition:top .25s ease;-ms-transition:top .25s ease;-o-transition:top .25s ease;transition:top .25s ease}.leaflet-touch .mapwize-control a,.leaflet-touch .mapwize-control a.hasDirection,.mapwize-control a,.mapwize-control a.hasDirection,.mapwize-control a.selectedFloor,.mapwize-control a:hover{width:40px;height:40px;line-height:40px;border-radius:0}.mapwize-control a.selectedFloor{background-color:#ddd}.mapwize-control a.hasDirection{border-left:4px solid #c51586;box-sizing:border-box}.mapwize-control .mapwize-control-image{background-repeat:no-repeat;background-size:cover}.mapwize-control .mapwize-position-control-button{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+pJREFUeNrsmUtIG0EYx40UGrxUBWnMwQq+ctGKoPUBNl5MwEMr6kEvEV8ENNSDETwoAcGD5mDxoiJiQJCiokWExIOmB5MUQVIETVDBCr4uur2E9GT3X9gyLhtw9mFLOnuRHTez85vv9f9mdff3966UJLhSU5LkYiAMhIEwEAbCQJRcz7Sa+Pz8XO/1eg3kWHFxMVdVVcX98yBra2sGt9tdeXR0lHt3d/dC6hm9Xv+zrKwsYrPZwj09PWdqvVunhtYCgNPptJ6enr7KyMj4UV5eHmlqaopYrdbrnJycuPDc7Oxs7t7enmFzc7P06urqZXZ29o3L5fKqAaQYpKWlxbyysvKWdlGAGh8fNwO+vr7+q8/n8/41kJqamveBQOB1c3Pzl+XlZb+cObq7uysXFxfNvCW5UCi0QFrwSUAEiIGBgc8TExPhRMF+fHycznGcnne360TBDtdsa2trB8zl5eX0k4FgF+fm5ixSEKTLSP0WLuhwOPxDQ0MRKZja2tqwHDejBgkGg+nV1dUfpNzJYrFYt7a23jxmnpKSkujGxsY66UrCBs3MzHhoEwA1CFwK6fX29nZSytVo5oJ1xHGRn5/fHovF9LQulkprDSy2s7PTL85ctBC4kIJbW1ut5Njg4KAf43BRzUDm5+dNKGhkXAAO6Vdu5sMGkIuGS8FSHo+nVDOQnZ0dE6oyOcYHvFlpMUNyIO8bGhrC+/v7Js1AkIn4QH8QhLQvTDQv0rVwj1Qdj8efw9qqgwgvKigo4Ei3wgvV0EqkwIS0wd+DgwP1QcRKlvZFNJec6v7/NVZSBUpwAS16Gc0tAhlOugBkuxqLJzdKcGOajaICycvL+354ePggVtB7KIWAXCHvV1dXTdggmlihAqmrq4sg3ZKm53uQEIqkEpDe3t6QyOom2g2iAhkeHg4j3U5NTf2pHZDlfX19spsiNFWkW6HKo0222+1hzUUjrBKNRidJ08sRjXxx/ba7u7tOjhmNRntaWlr85ORkQdNgX1pa+r37YrGHBaE/eayboQ0QQ0B8QjDyWs6redaCFUZGRtax+06n84Gwg5jc3t6exk5LAWEM/+N/+1Hcy2AuiE8ANjY2Uqd12a2u0EQlanUFCSNU/6ysrHiiBQLC7Xa/k3K1Jzl8EGCwALicHGlBziEXQrFEQW89Njb2CcFfVFTUj1b1sVUZVsjMzOwHRFdXl08JhGoHdFg8gh9xgzgoLCw8q6ioOIMcJ5/DiQr/TC7AkcZhBd6l/Goco+rU/KoLoNHR0VI0YBcXFwYpiQ91gMLa0dERUfMcWKf152mhjRUfn6p96dh3dgbCQBgIA2EgDCSJQH4JMADNyFajRlz3ZQAAAABJRU5ErkJggg==)}.mapwize-control .mapwize-position-control-button.active{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA/NJREFUeNrsmF1IU2EYx1NTV06dzZwTyxmaYzX8oHIE2S4EF1hKhEGIiETroqjwXsILb8u7jBATiTIQJaEFEsugZoVONDll6TRxmx84TXNmtvwLR94dZuyc7Z0i59zMve685/yez//zhnk8nrt7dsEVvmeXXCKICCKCiCAiiAgSyLWX1sbuCZdkrms4mVxLKMx0SFJlbhrPCwumRJl5MZQ81dKbs9hvV68trMT7+k1EXPT8vgy5LfGS1qqsPGnbUSAAGK97bVgZc6XhRaXZSibhXBYTo1G44k4ddpEemu8eVS28G8sBaHSabExpzDcHAyhgEKaqVT9n+noWACnGfFPqnQLGn/vsTR9V9oYePeDjz6h6NM/KTdsGMlDcWLrYO5mdYDj6Rt1YZhayx/DtDt1M60ARvKN5Xv5UaA4JBmEhkq+e6EivNVgDSXaE5vebHZWRylhH3vsbTSEDYa3oCwIh42zu1S0z01m+7t0qLwAzbGwzCg0z3iALH8Zln0ubb/kKp6HLLYb5t7Z8f/aR5qX0Zz64aCI9xBpIVVf0mG8B4N0Qx2q79EhsLgRCzV8IXAjLweKmSnIt836JBR5DEaDa2eENvMDBMq2ZW7mwzvfhq1OLChiAXEPYoZIh1KiBOJ/0qcOjIlbIvAAcyq/QygcDIK82QdZDCh63N1h01EB+9vxQ7z+uYLihFmgzQ3Egv8edTrMuf5tVUQOBy9eT1CsJfw061YGCcCtcfEG6DZ0fJTzoIOymkiNyFxlWf3+vRQdDK5HhhX6DT24fCgoIu2mkPGazXC4NOWU0lKyQ7s67/EYppFRkeMgGK7ZBkV6AuqXxUghZ6h5xj8xuPgQSHaUyGC9PdnLWWHy6Oy8QdN31uu9VFjF7BAoBueKVjy+/qPkaiBdIbP4hBuWWLIup1QUWNMmAvGHUWbya5PqEyddAvEAUV3IZlFv7Q4uaDK8DpRqzUAio3cTzGgf7feJe98aYnFSeZ6UGgpdGGEw19xlIr0DsccPD35DiSvbJhh4DQpiEo5LskN4bkvt6m4Fc13ZWtSeWaV/5uw/GANzDFZ/wRkb9hXbqVQvNKqki1wSxN1pjyuHK8GPtFfWwtK+8wRr+h99wxwDsBfEJY7AHFiEZddkhaqtRl536VmeXJKwi2CpcAOF49KkEkFwvheTwgYXxNe35q99Gqjv17B5CIYJyHARrIvnxN0JOeU3H+AOE+6ZbB/Qe9x8Jqh7CctsP6GBZJD/yBnmAmUWSIXdIc1IcXFWAhopehDIOL6TVFJqF5AQVEFIjYYrEAIbZZSt1gMaKnhQMACog/5szaB5ghwRkx8l4EUQEEUFEEBFEBNnNIP8EGACE20ZI52WxFgAAAABJRU5ErkJggg==)}.mapwize-control .mapwize-qrcode-control-button{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABDlJREFUeNrsWUFIG1EQNaWhwUMbQ7RqQANG6aG2SqqiF4NWkqug0lt6EL0Lem168KLgwZvioQEhBQWviaKkFwVtMcUeI6QBNdESt1AkVcHuCxkZtonJrmwU+QPr3/3/7+a/PzNvZr6Gq6srX9kDkEdlD0QEEAFEABFABBABRAB5SEAeq33B7XZ79vf3q2+aE41GP6F1OBzv0TY0NCRCoVCQxqk/n/T390emp6cjumoEIA4ODnICSaVSZnm8HveDg4Oui4uLzEadnJw8xTOu7DfqMTfXN/DtWCxm1l0jEJvNltjY2PgcDAavATU3N0szMzMty8vL3XhGOzc35x8ZGYkBAPXLEsaf3t7eyNjYWGRvb+960R6PJ9HT0/OuJKZFAhCjo6Neeh4YGPjCx+Xy4CPdLy0thQkAFw4cAuAl85FixWAwfFD2cXB37uzFCmlofX29xWQypX0+X/BesRb3CW5OfX19sbW1NTs97+7u2mXmCaLFc2VlZRpshT7+jvKbJQUCxoF98z6AoEUTM8lsZaJn3PM+Ppf7TD420wXI6enpM+6k+WRnZ6eaKBj3ChqvJ6q+E9OiYFdI4NhweDK/hYUFt55OrxqIIib859yFNJWlbP/U1FQmOLa2tsbwDgeepWt1LKn2OGhra8uMIAZTwS4PDw+H2traEpOTk29hRsROPMbU1NQk0U/vFMN4asGo1khnZ6eEK2suZQCB6C3v8OXx8bGVdlrhU2b0n52dZRy9q6vre21t7TVDHR4emjc3N19n87KfSjbTNfsFneJHA4HAS2gJiSFSFxrHmBw//vJ3ysvL02i9Xm8EO65cMDSHhBEbo7uPrKysZNjH6XRKExMTYZiQbGo/kN2S2fG8CcwEgEQS8AOYGGIGKJv7FMwPG4TvkNZ1AzI+Pu5BSyDy5U6yKQWweGXKTk4tSZIpV/qC+SAAtT6i2tlJI7IJJArlWkgCscOzs7NvwuGwkxY7Pz9vBznE43Ebfwfz4UtagGjSSKFAxuMEn0+JJBZsNBovlWwGDbtcrm9I70sS2SsqKn6jnlD2I+2gRYNC5byqW2anFEVyZRW4urr6grMZxGq1/lHrH5pZy2KxSMpdAwPBJHjf+fm5saOjI45Fg8Vwod/v97c0NjZKTU1NCQA8Ojp6TuN2u11T4qg5+wU73RTFMYZ4gQWDTqkGhzMnk0nz4uLiKzl+WEDRVVVVv2gTMP9e1SMQBDkEPlAt0TJK5Lq6ukxKD21AC0oG1BJHSnIcBFrGQnHxOh8geBIKEtCSZ91KIwh4vMamw4dcJsYXSkkjEQA0A43curRWG0fwwziy4ekIL7hQq1DQo5yK2AwaQAvaRf6VTqefkI+0t7dHt7e3HUNDQ1/Vnmlp0ggYqJhzJ9AvmA1UigBIZbCS2fhJi1wiaHZ2g/ivrgAigAggAogAIoAIIAKIbvJPgAEA/6KlHbNivY4AAAAASUVORK5CYII=)}.leaflet-control-floor::-webkit-scrollbar{width:0;background-color:inherit;border-top:none;border-bottom:none}@media only screen and (-webkit-min-device-pixel-ratio:1.5),only screen and (-o-min-device-pixel-ratio:3/2),only screen and (min--moz-device-pixel-ratio:1.5),only screen and (min-device-pixel-ratio:1.5){.mapwize-control .mapwize-position-control-button{background-image:url(data:image/png;base64,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)}.mapwize-control .mapwize-position-control-button.active{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAACApJREFUeNrsnHtsU1UcxwdsrCtj62CPDosrY0CBaafEDZBQFjFFDCyiZhgki4mIRIkxxviH0RAf8Q9D0ChBxMRMQuJi1AyUUB8ZMwgMA1IcUmSMDibrHrBujNHxEPctO+Skae+9fd17D/x+/6ykt+X2fM7veX6/O+rmzZsbUkh0I6NpCQgICQEhICQEhICQEBACQkJACAgJASEhIASEhIAQEBICQkBICAgBISEgJGElVcSbvtrZb+g7dMaM15c9HeYbl4cM7L2sOVYv/mYU5/kzZ0/yi/bbRonQdTJw/LypZ/cx2+DJjqKhzn7zjUsBkyL1H5saSMsf7zOWFLRNXDzLY1owzUdA4tCCru8O2y78+nfFtZ4BcyK+c8x4g39Yg9wFTz90VK/aozsgAHFuS8Pcvv0tFf9dvW7g30u35Hix243T8n0ZRbn+SDseGnWltdvUf9hrHWq/WDDY0mULvSaz1HLUsq6yUW9gdAXE++GeuRd/Pu7gQQBCzsPT3Pkr5njGFmQFYv3ujh0HbL0NHnsonJxFtsbJ6yoPxvPddxwQ7OgzH/xQNdTea+V3sHlleVOi7T7+r/YtDY6B5vYy3pRZXlhUn7fM7r3rgQwvTlnX90ecTCugEebqisZkLw7AnN3kcvIaA22ZuqFq710L5PSG+kW9ez0O9u/cx+0u6+tLDqp5DzBlHbX7q9iGgGYWv7XMpZUJ0wyI5+UdVcxswGRMeWNpnVZhKQKJEy9tr2HRXFpupm/m5tW1WkDRBAgPQ8sfr8f7Uh0IIqmeH93OeH40y9QR1oZ7H2Hx+LIiXywhLQ8F5sv26ar6OxYI7PW/236rjgUGIJz/an9ZX9Npu9JEEaYwc2ahp7BmQVM0cHgoajt61YBgQZtXb3sFzhMljdLtaz5WAkMqUYxGok0E3U9tXsvAF722pFatkFg1IH89+3kNyzNK3luxVYkD797ltp775JfqeECE1rYmPDq7UUkkx28gaNrsL57bqoY/Ga2WqWIwYAKUwEBI3LZxT02iYEDwXfBfMElYcKlrsfiFNfOD/gPFTGipGmulChDf14eczKYrscdYMD4/SXhSOOwfEObKQSlcNc9jLMn34DXuB8mk8ECQibNyOcoT0TjUZAr8A6DIXXfvq04X91scwgPp3n3MwUoico4RIbEaMHgo2ACSwcBwEICAgGlWsrUkqUDglJl25D52X5PUtf59p8wsP1FTsMjQYqlrEJ2x153f/FEmLJALrmY78x2wx1LXtn30U5VWGTqKm1L+BFrCfMlwMmoXFsjgP75gJRXJmZyfSdSpYKzRV+u7uyS1M6fS5mYRF7RZOCAwVyxkzVv+gFuJn9FSYLqktITXcP+B01bhgLA6E5IxqbwDu01p00KyBaUZqfeZ2brkPjtDOCCDLZ1F+Gu4d4JkZNW98097ik4EdTKp99MtEzqD0VnXJfFM1vW+Kyb+R0QE19pt1QsQOT/Ger5giuWSSt0BYWYIpfB4FkFtge+L9B6a725r00ijnhBA+OQpNcsYkPIfKTqTofaLJqnwV8hMHT1RikxE76BBd0A6+xXdO1pYhSydZJdP0X37ZkwmmesnFgrI1Z4BQwqJfoAoNV+iyZhx6QFhgCg97kzLMQb0ttByUSGTcbZCn5AaIhW16HE8QOuoMGlA0FWiJGph1+lFpLT7SluPKVoroB8gJmMwZmcllIixfanlpG7M1UitKmJV4VRXUENwnCCchhhnFLbdMlm9kqURDM/oBUjWHKvk5hhobg8WFdMLsnzCATHNm3pbpdF1IpX94nhX83BzbGrAsq4y4uZA7YqVeeTA6RPIsMNmqt13oFWyXI3xA62BZM8vkTxixnjd7d+2cIZXOCDB3T9yUoiTQ6nqKByknP1Oak6hoD0Js44sCElmTSupQNhJIcrV/A4LJyXvP1kPs6EFELn2JFSAmbma+MisJmEzdZgt5h/kjmnRKTh5/eI6tWFgSEguhO2uP1LB/AxmHYUunTD/gPMR9F3J5QDm6nLV2v/RbyXX5wvtYGNv8DPJ7u9NOhAsMtMSTNjKnbQh0lEDitLZj/O1+5xMOzCtK2ymzsvkFytdzJe0vPmtbP8VoGAEIFk+BcCVwIBGM9+R/8SDrjum+x2+JLt8ysFbmXuXTa5TkGnW9E3PbE1kjoIICaMQUvkGE76TEveg5DOJEFUHdvjBSqUzIsyOw3TEev6OsDZv6f2NShcV93r8+S/Xwu9BS7Ex1Hrig6ojbdh1re/srGFTVMVvL6+NpuKLz6NtaOBEh02ulwsQMopyvdnzik/KtbFKbZx71iysi+bzQgFhZRQ2ZxgLFH7hInV+4Ng4FnsfCkOLBwloMhYNH+KrO1QVL5RkmlQtJnA1AxIKhUU+ajnOcD6Kn2XUCoamQJj54h9rgXoWSihqDeuzCV+tH++hGyDMUWM2hJmKaCZl490MmH1kwUEw8Vu/uE7rJwLp4vFM2KltG/c4hp303NBQNd7nZIUzlb2/n7LzB2dqa6bugfDacu6zBie/WNi5xulmT7Thq5JwGdDNK8tdaoa1QgHhnayvrskR7vgXWbNhksmXlpfVF6kVB50uaK7AeT5GB0Jn3aNNFO96ILEkgrI1ohFNm+gsdevhyXFCAgmFg1EyPCr2mn/QJFdGgRakZmf48dBMzHXoGYKQQKTMWyKydAJCEt600hIQEBICQkBICAgBISEgBISEgBAQEgJCQkAICAkBISAkBISAkBAQAkJCQEjCyv8CDABHe93+/OM+xAAAAABJRU5ErkJggg==)}.mapwize-control .mapwize-qrcode-control-button{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABVpJREFUeNrsXD1IK1kUTpaFDVpsBHFF0AiipU4jCBa7hfDstNHtfI1oL2q7a63Ya/cstdFKH1i4hWCZZ6mNPyAhCGYRJZ07n7yzXO+7ubmTTCaTyfdByHBnchPmm3POd885N+m3t7e/U0Rs8BNvAQkhSAgJIUgICSFICAkhSAgJIUgIQUJICEFCSAhBQkgIQUJICEFCCBJCQggS0ur4udFfcHd3lzk5OemtZ46enp7y7OxsQR07PDzsLRaLGXVsenq6MDAwUDbNYbo+KJaWlm4afb/SjW6U293dHVxeXv5czxzDw8M3V1dXX9SxkZGRz9fX14Pq2M7OzpdKN810fVD492qDLosuq3HAk+553q3r9QcHB7+bnnS8d3R0lOfm5v5Rz21sbHza2tr64LJ0ywL0z9mQz+dz9VpWbAkBGfv7+2fO/jSd/oEQuTm4qfpcputNCPIb5ufn/0gsIWpcOT09HazlhsHK8J7L5UqVzgW52ZXOTU1N3UQRxGNBCMgwuSMFFQkxuSCXc64uMWpVxaDOoB6yTk+n/6pFJnOlTrSHheiB++XlJfPw8PCeCZicnMz39fX9awr4JKRB0F2RmglYWFj41oxA3NaEgAC8j46OliYmJoyWcHFxkb28vMzi2JbfIiEf1xlntXxWrMG0MBRsb297Imlt+a0oclMM6gzqwRZirumNZktnWggRjYWguBQ0x6RjaGiooMteX239hlyUL3l/lbG9vb0xpGWQobXNEeuFb6v+k4NacJLgbHJFtqBOl0XEw0KCBlHbE1/r9SKT4eZ0SWyyNhmLOi9GC2lHC9ELQWpZVPJPcg5BulgsZr8LgpJ6TqSzPr8Edf16zHV+fu7huKurq9Td3V16fHzMPj09vc/vX1vo7Ows39/f95bL5Yw6l4z5KPf39xdMaZvEBHWT21BTI7IaNwVkm1uyzRUGoljZxzqXJTK21uthIVyHhAhxN2Fcb8t9VbOoKHNeDOq0kFRqZWUlj64OHOOJ9F//P8FIlyMWuLgn3SIWFxe/jo+Pf1iR22KILR5BbKC+0haEoI4htQyQoQK1C5eVtSm2gAz9s/r8roBaS2QbEIpFqE/gWHqdqvVlCdbW1rzb29usbl0gE2VaSFS1hGuCnvvCcZC+L/W3Bmmwiy0hqNypawcQ4tCX9Y6jo6MxvWsQpIKQzc3NPF7VpK2sHbDyVr7zrNr1qiW6fI5BnSv12qDuD7HVwZ1/8PdALCJAnV8sxZR/kv0hJjHgmvtKxMLQNUiHMb8tgMuGnyALzUTKXjyZ6+vrn+qZA8Wl4+Pjr67zIw8l2xYEMzMz3xBzdGlbKfflEzfmz5ETa0kMIXATjWznN82PpKA+5qu1H/al6H1cEAhCCBKQeMH9RaGuWiJ1YpOleJrh35+fn3+RsaDExzH3FSkhLvmkaitpCdae5/3pB1tPDbYuBS21JyyO3SctK3tfX18zqQSiKS5LlZIm2OSlPNUIyKurqx+krSkHJlJYesKqlWT1OdA1k3hCwoAp12SS10FzWc3uUGlZQlRZanvidckKEaBLYtkdjKCun7NJbhKiQGSpYxBPqdLWt5oJdUx2B4dd8m0rQkyQrQoquB2hiTA93bbORdu5oBK97WVvUtEUC5HybVjQAzF6sPzveC+/yt9tmHJZtnNtRQjcRJjyUk+ZoCFO3I3sRzHlsqTSaDrXFoSgY9H2dxZhAd2Jpm5J2b5Q6XepWxuk5ItSbpTW03L/l6UXqEyr+HrAZmsiWgshaCEkhCAhJIQgISSEICEECSEhBAkhIQQJISEECSEhBAkhSAgJIUgICSFICAkhSAgJIZqJ/wQYAAuzg0mSKObKAAAAAElFTkSuQmCC)}.mapwize-marker-icon{background-image:url(data:image/png;base64,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)}}.mapwize-div-icon{white-space:nowrap}.mapwize-div-icon .placeIcon{width:30px;height:30px;padding:5px 3px 5px 5px;margin-top:-5px;margin-left:-5px;box-sizing:border-box;border-top-left-radius:5px;border-bottom-left-radius:5px}.mapwize-div-icon .placeIcon .iconImage:hover{cursor:pointer}.mapwize-div-icon .placeIcon .iconLabel{position:absolute;display:inline-block;height:30px;margin-left:5px;padding-right:10px;margin-top:-5px;box-sizing:border-box;color:#333;text-shadow:1px 0 0 #fff,-1px 0 0 #fff,0 1px 0 #fff,0 -1px 0 #fff;white-space:nowrap;border-top-right-radius:5px;border-bottom-right-radius:5px}.mapwize-div-icon .placeIcon .iconLabel .title{margin-top:8px;font-size:1.1em;line-height:1.1em}.mapwize-div-icon .placeIcon .iconLabel.hasSubtitle .title{margin-top:3px}.mapwize-div-icon .placeIcon .iconLabel .subtitle{font-size:.9em;line-height:.9em}.mapwize-marker-icon{display:block;background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAABQCAYAAABFyhZTAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAABP9JREFUeNrknE1IVFEUx++IkBCktGjCSE2ooNJqUYugUihoEzlBtRFqFrYdpZZBvmhZZGsDFdq4qAwXudMKWtQi+1qkCycjsaB4RoGCi84Z79RznJl3z7n3vvsmD1zeuHgfv3vOPf/7zr1PIdaZJaK60fOtN5vgEGxBm4TmQ8senb+WrUhgAGyDA7bj8qhqvuyAp9BGoAMmYwssvZiBdglanaHLoseHoA2a8H7CoDevEz3JsUFong54woBH70DriDj39ElwPzJggO2WXq0Tbgxh0wA9YhUYQBFwwIFXS3oboHusAMsQfgTtQMzkdQJaSiXEEwRYhBx3GMJhhvLVHgad+E9glaETimN2pgJglaCrFC4wXkGwQuaXOywPg3fxxG4TT7Fxb1LUHmkUNdtrc7+D9vvD11xbePFJLH5eMAXeA17uUwaWs6dxnTsi3JYL+0XyfKvYAL9VbAmAv/S/FN+G34rln4u6On2wcFZWDnimyFuNklVvqhH1lw+LhitH2U+LsHP9r8TsrWdacgXA7aHAANsrZ1Fkw7Dd1Xda2aNhhqE+lRnNHZmGCWyiZNKSWTnDuXLyQqtoedBpDDY/9lseduY6kmkDYVm6m5OVEXYneNaG4RDBjsR7MKwJnNhRDvhinGCDtuPGyTUZXtEyRYFlTzRRQw4fJApDT+8ZOJc7Eq1Nvges8fAZ6pWaAZbxAGzD/NDM6+COYsAd1FDWSCZs2wL3ZYT2mVXA8gWBlKwarhxzNn/c1nWYHNaFHm6j6q1J+eF4uYZ4f5w9BoFJsYnTRde2+dRu8otFVcFbBsnDro3xDI1BYGU5wqzsMpz/ATdoeVgZeOO+pIiDceSwSlS4UeWp4oGra2vWF/DywuL6Aqa+JweBlZclsfYUB1ti1L+CwL7NnrVhv+jP8JTl4bh4+cfYR+op2SDwG8qZX4ffOgf+/mSKespkEHiCGtIuw5pRxvVx+8RfYFm/zVKuoFlC1bLZ2+R7TxSTJdLi8vexKSdjea7/JWeF4nEx4LvUq0x3j+quEJB1d/bWc+ppft6Zq4BlWJPGMvb0dGY0mlkVdCwW5RkdPJJfTSw20xoiZ0sIbfS0bdh3Z+9zE6VXcmoJPTFITV55mbIV3pqwq/Z3lZpL93CujNAaD1ZygjN54p7ONb3gH+VWD3GptI17l4arx0R91yF2zRrnySg9mhOcNTt8ygFjjeu1bkUCq4tJQh0ZPYmyY2Amh0lqR+HWh7AdAL2CuWxaaFhSXSnt1uXgqzdtkONzaWXW9n4ewnfWZA5IFdu0prKpRSu0HRkmqjS3AJCmvjo6tmy5pBsKLFN6uoKAy+7IUyrxyLEwWAGwXtiGckpNq4czIYnQcANLL6XEE+ZlDJNUTGF91WFHqlrKcPFiCJxW3SXP2iAeM6kqKUHaHo6hVGWp834WcIykKkX97oG98hADqfI43zTpLrW4kiolCTIO7EiqfJ3hpL2Y5kCq0s4+1HIgVSQJsuLhCKWKLEFWgSOQqhTn0zubHrYpVZ6pz2pt7AAwLVVsCYoE2LBU+aaHiZU9HgalKm36E3mr/wMApArLvNwPM7UlKDIPBzMrU6qMSFDkwDIcOQ9uRIJceDi/OEdZaPdM/2eHSIEDszCV5GNUgpwBy/AMS0DGJcilh4X8HM6LUoIilyWCVFmRIKceLiNV1iQoFsBFpMqaBMXKILQfyfXnSO2PAAMAQo4HxBrUnjcAAAAASUVORK5CYII=);background-size:contain;height:40px;width:30px}.mwzDirectionChangeFloorIcon{color:#fff;border-radius:3px;text-align:center}.mwzDirectionChangeFloorIconColor{background-color:#c51586}.mwzDirectionChangeFloorIcon .part{display:inline-block;vertical-align:middle}.mwzDirectionChangeFloorIcon .up{-ms-transform:rotate(-90deg);-webkit-transform:rotate(-90deg);transform:rotate(-90deg)}*{-webkit-tap-highlight-color:transparent}.mwzDirectionChangeFloorIcon .down{-ms-transform:rotate(90deg);-webkit-transform:rotate(90deg);transform:rotate(90deg)}.mwzDirectionChangeFloorIcon .toFloor{font-weight:700}@-webkit-keyframes leaflet-ant-path-animation{from{stroke-dashoffset:100%}to{stroke-dashoffset:0}}@-moz-keyframes leaflet-ant-path-animation{from{stroke-dashoffset:100%}to{stroke-dashoffset:0}}@-ms-keyframes leaflet-ant-path-animation{from{stroke-dashoffset:100%}to{stroke-dashoffset:0}}@-o-keyframes leaflet-ant-path-animation{from{stroke-dashoffset:100%}to{stroke-dashoffset:0}}@keyframes leaflet-ant-path-animation{from{stroke-dashoffset:100%}to{stroke-dashoffset:0}}path.leaflet-ant-path{fill:none;stroke-linecap:square;-webkit-animation:linear infinite leaflet-ant-path-animation;-moz-animation:linear infinite leaflet-ant-path-animation;-ms-animation:linear infinite leaflet-ant-path-animation;-o-animation:linear infinite leaflet-ant-path-animation;animation:linear infinite leaflet-ant-path-animation} -------------------------------------------------------------------------------- /mapwize/doc/doc.md: -------------------------------------------------------------------------------- 1 | # Mapwize.js 2 | 3 | The SDK is built as a [Leaflet 1.x](http://leafletjs.com/) plugin. Leaflet is included in the package so you don't have to worry about importing it. Mapwize.js is all you need. 4 | 5 | The package is exposed as `Mapwize`. 6 | 7 | Here are the specific instructions for using Mapwize. Please refer to the [Leaflet 1.x Doc](http://leafletjs.com/reference-1.0.3.html) for all the Leaflet related options. 8 | 9 | ---------- 10 | 11 | ## Summary 12 | * Install Mapwize 13 | * Enter api key 14 | * Display a Mapwize map 15 | * Map constructor 16 | * Limit the visible area 17 | * Center the map 18 | * Center on coordinates 19 | * Center on venue 20 | * Center on place 21 | * Fit given area 22 | * Floors 23 | * Promoting and ignoring places 24 | * Using places that are not managed on the Mapwize plaform 25 | * Display directions 26 | * User position 27 | * Display user position 28 | * Use browser location 29 | * Follow user mode 30 | * Center map on user position 31 | * Get user position 32 | * Setting the user position 33 | * Using multiple measurement sources 34 | * Setting the user heading 35 | * Setting the map based on URL 36 | * Parse a mapwize URL 37 | * Adding markers 38 | * Listen for events 39 | * `click` 40 | * `contextmenu` 41 | * `directionsStart` 42 | * `directionsStop` 43 | * `floorChange` 44 | * `floorsChange` 45 | * `followUserModeChange` 46 | * `marginsChange` 47 | * `markerClick` 48 | * `moveend` 49 | * `placeClick` 50 | * `preferredLanguageChange` 51 | * `userPositionChange` 52 | * `venueEnter` 53 | * `venueExit` 54 | * QR-code 55 | * Access Key 56 | * Cache 57 | * Margins 58 | * Modify Place Style 59 | * Multilingual venues 60 | * Outdoor map provider 61 | * Working with universes 62 | * Adding custom data to objects 63 | * Api 64 | * Venues 65 | * Places 66 | * Layers 67 | * Connector places 68 | * Beacons 69 | * Place types 70 | * Place lists 71 | * Search 72 | 73 | ---------- 74 | 75 | ## Install Mapwize 76 | 77 | ### Bower 78 | 79 | Run `bower install mapwize.js-dist --save` 80 | 81 | Import Javascript and css files in your html: 82 | ```html 83 | 84 | 85 | ``` 86 | ## Setup your api key 87 | If you have to use a Mapwize service without loading the map, you can setup your api key as follow: 88 | 89 | ```javascript 90 | Mapwize.setApiKey(); 91 | ``` 92 | 93 | ## Display the Mapwize map 94 | The simplest way to display a Mapwize map: 95 | ```html 96 | 97 | 98 | 99 | 100 | 105 | 106 | 107 | 108 |
109 | 110 | 124 | 125 | 126 | 127 | ``` 128 | 129 | There has to be a div for where the map is going to go. This div can have a css style like for example 130 | 131 | ```css 132 | #map { 133 | position:absolute; top:0; bottom:0; width:100%; height: 100%; 134 | } 135 | ``` 136 | 137 | for a full screen display. Pay attention to have `
` and **not** `
` as the second case will NOT work. 138 | 139 | On the full map, everything is displayed: 140 | 141 | - Layers with floor selector 142 | - Places with shapes and markers 143 | - Button to position yourself and enable the tracking 144 | 145 | ---------- 146 | 147 | ## Map constructor 148 | 149 | ```javascript 150 | Mapwize.map(mapId, options, callback); 151 | 152 | /* 153 | * mapId: The id of the html div where the map should be created 154 | * options: The options for the map (see further for all possible options) 155 | * callback: function(err, mapInstance) The callback function called when the map is initialized, with errors if any, and the map instance. 156 | */ 157 | ``` 158 | Possible map options (in addition to all leaflet options): 159 | 160 | - `apiKey` *(String, **required**)* Your Mapwize api key, find it in the Admin portal under the **Developers/Applications** menu 161 | - `accessKey` *(String, optionnal)* A Mapwize access key to access private venues. If necessary, it can be generated in the Admin portal. 162 | - `displayLayers` *(Boolean, optionnal, default: true).* If `true`, Mapwize **layers** are displayed on the map 163 | - `displayVenues` *(Boolean, optionnal, default: true).* If `true`, Mapwize **venues** are displayed on the map 164 | - `displayPlaces` *(Boolean, optionnal, default: true).* If `true`, Mapwize **places** are displayed on the map 165 | - `displayPlacesOptions` *(Object, optionnal, default: {})* 166 | - `displayConnectors` *(Boolean, optionnal, default: true)* If `true`, Mapwize **connectors** are displayed on the map 167 | - `displayFloorControl` *(Boolean, optionnal, default: true)* If `true`, the Floor Control is added on the map 168 | - `displayMarkerOptions`*(Object, optionnal, default: null)* See marker part for details. 169 | - `floorControlOptions` *(Object, optionnal, default: {})* 170 | - `cacheParams` *(Object, optionnal, default: {})* Sent with each Mapwize api request. See Mapwize api doc 171 | - `floor` *(Integer, optionnal, default: 0)* Sets the initial floor 172 | - `bounds` *(L.LatLngBounds, optionnal, default: null)* Sets the initial bounds 173 | - `showUserPosition` *(Boolean, optionnal, default: true)* See the User Position section 174 | - `showUserPositionControl` *(Boolean, optionnal, default: true)* See the User Position section 175 | - `useBrowserLocation` *(Boolean, optionnal, default: true)* See the User Position section 176 | - `marginTop` *(Integer, optionnal, default: 0)* See the Margins section 177 | - `marginBottom` *(Integer, optionnal, default: 0)* See the Margins section 178 | - `language` *(String, optionnal, default: null)* See the Multilingual section 179 | - `outdoorMapProvider` *(String, optional, default:null) See the OutdoorMapProvider section 180 | - `mainColor` *(String, optional, default: #C51586)* changes the main color for the user position marker, direction path and floor control. (The marker can be customised using displayMarkerOptions. The user position control cannot be customised but it can be hidden using `showUserPositionControl` option) 181 | 182 | ---------- 183 | 184 | ## Limit the visible area 185 | The area the user can browse can be limited to a given bound. To do so, use the Leaflet map options `maxBounds` and `minZoom` at initialization of the map. Example: 186 | 187 | ```javascript 188 | var map = Mapwize.map('map', { 189 | maxBounds: [ [40.712, -74.227], [40.774, -74.125] ], 190 | minZoom: 18 191 | }); 192 | ``` 193 | ---------- 194 | 195 | ## Center the map 196 | 197 | ### Center on coordinates 198 | You can center the map on given coordinates defined by 199 | 200 | - latitude 201 | - longitude 202 | - floor: if not set or if does not exist at that location, the floor is set to 'null' 203 | - zoom: between 0 and 21, default 19 204 | 205 | or specify the bounds and the floor to be displayed 206 | 207 | You can give the options at creation time: 208 | ```javascript 209 | var map = Mapwize.map('map', { 210 | center: [40.712, -74.227], 211 | zoom: 19, 212 | floor: 2 213 | }); 214 | ``` 215 | Or dynamically: 216 | ```javascript 217 | map.centerOnCoordinates(latitude, longitude, [floor], [zoom]); 218 | map.setFloor(2); 219 | map.setZoom(19); 220 | ``` 221 | ### Center on venue 222 | You can center the map on a venue by calling `centerOnVenue` with either a venueId or a venue object. Venue objects can be retrieved using the `api.getVenue` method. 223 | 224 | map.centerOnVenue('venueId'); 225 | map.centerOnVenue(venue); 226 | 227 | ### Center on place 228 | You can center the map on a given place by calling `centerOnPlace` with either a placeId or a place object. The floor is automatically selected based on the place. Place objects can be retrieved using the `api.getPlace` method. 229 | ```javascript 230 | map.centerOnPlace('placeId'); 231 | map.centerOnPlace(place); 232 | ``` 233 | ### Fit a given area 234 | You can set the map so that a given bound is completely displayed. This takes the margins into account. 235 | ```javascript 236 | map.fitBounds([ 237 | [40.712, -74.227], 238 | [40.774, -74.125] 239 | ], [options]); 240 | ``` 241 | Besides the default options supported by leaflet, `options` can also accept `minZoom` as an integer. Note that if `minZoom` is used, the entire bounds might not be displayed. 242 | 243 | ---------- 244 | 245 | ## Floors 246 | 247 | Venues usually have multiple floors. The floor controller let the user change floor. But the floors can also be controlled programatically. 248 | 249 | #### Get 250 | Get the displayed floor 251 | ```javascript 252 | var floor = map.getFloor(); // Returns an int or null 253 | ``` 254 | 255 | Get the list of available floors on the current view 256 | ```javascript 257 | var floors = map.getFloors(); // Returns an array of int 258 | ``` 259 | 260 | #### Set 261 | Set the floor to be displayed 262 | ```javascript 263 | map.setFloor(floor); // floor must be an int or null 264 | ``` 265 | 266 | ---------- 267 | 268 | ## Promoting and ignoring places 269 | 270 | By default, places are displayed (or not) based on their `isVisible` attributes. Their display order is based on their `order` property. And if they are clickable or not is based on the `isClickable` attribute. It is possible to modify the default behavior using the following methods. 271 | 272 | #### Promoting place 273 | You can promote places so they will be displayed on top of any other. The order in which promoted places are displayed is set by the order in the promotePlaces list. Please note that: 274 | 275 | - The first place in the promotePlace list is the first to be displayed 276 | - The display of the next promoted places depends of potential collisions with other places earlier in the list. 277 | - Promoted places are always considered visible, disregarding the `isVisible` attribute. 278 | 279 | To set the list of promoted places 280 | ```javascript 281 | map.setPromotePlaces(listOfplaceIds); 282 | ``` 283 | 284 | To add a single place in promoted list 285 | ```javascript 286 | map.addPromotePlace(placeId); // Add the place at the end of the list 287 | ``` 288 | 289 | To add a multiple places in promoted list 290 | ```javascript 291 | map.addPromotePlaces(listOfplaceIds); // Add places at the end of the list 292 | ``` 293 | 294 | To remove place from promoted list 295 | ```javascript 296 | map.removePromotePlace(placeId); // Remove place from the list 297 | ``` 298 | 299 | #### Ignoring place 300 | You can ignore places and therefore make sure they will never be displayed. 301 | 302 | To ignore a place 303 | ```javascript 304 | map.addIgnorePlace(placeId); 305 | ``` 306 | 307 | To remove a place from the ignored list 308 | ```javascript 309 | map.removeIgnorePlace(placeId); 310 | ``` 311 | 312 | To set the list of ignored places 313 | ```javascript 314 | map.setIgnorePlaces(listOfplaceIds); 315 | ``` 316 | 317 | #### Ignoring the `isVisible` attribute 318 | Setting `setIgnoreIsVisible` to true you can enforce that all places will be displayed disregarding their initial settings 319 | ```javascript 320 | map.setIgnoreIsVisible(boolean) 321 | ``` 322 | 323 | #### Ignoring the `isClickable` attribute 324 | By default places that are specified as non-clickable will have their click event disabled. Setting `setIgnoreIsClickable` to true you can enforce that all places will be clickable disregarding their initial settings 325 | ```javascript 326 | map.setIgnoreIsClickable(boolean) 327 | ``` 328 | 329 | ---------- 330 | 331 | ## Using places that are not managed on the Mapwize plaform 332 | You can add places on the map coming from your own data set, that are not hosted on the Mapwize platform. 333 | To do so, set the list of external places using the `setExternalPlaces` method on the map object. 334 | 335 | ```javascript 336 | map.setExternalPlaces(externalPlaces) 337 | ``` 338 | 339 | The place object need to have at least the 4 following attributes: 340 | 341 | - venueId: string with the id of the venue the place belongs to 342 | - floor: integer with the floor number of the place 343 | - translations: array of {title, subTitle, details, language} strings that define what text need to be displayed on the map 344 | - geometry: geojson geometry object with the position of the place 345 | 346 | Example: 347 | 348 | ``` 349 | { 350 | venueId: '56c2ea3402275a0b00fb00ac', 351 | floor: 0, 352 | translations:[{title: 'External place', details: '', language: 'en'}], 353 | geometry: { 354 | type: 'Polygon', 355 | coordinates: [[[4.5993915013968945,49.742717037274765],[4.599440451711416,49.74270013759909],[4.599408432841301,49.74266146331921],[4.599359482526779,49.742678363008345],[4.5993915013968945,49.742717037274765]]] 356 | } 357 | } 358 | ``` 359 | 360 | You can also add an _id field to the place. The _id is only useful if you want to refer to methods that uses place ids like promote. 361 | 362 | The folowing attributes are also available: 363 | 364 | - placeTypeId: id of a Mapwize placeType that defines a default style 365 | - style: a style object for the place. See the style section for details 366 | - data: a free json object that is returned in any callback like placeClick 367 | - isClickable: set to false to disable click 368 | - isVisible: set to false to make it invisible 369 | - marker: {latitude, longitude} object to specify the position of the place marker 370 | - order: integer to define the order of display. Places are displayed in ascending order. Default 0 371 | 372 | Please note that in the current implementation, external places do not support universes. 373 | 374 | To remove the external places, simply pass an empty table to setExternalPlaces: `setExternalPlaces([])` 375 | 376 | ---------- 377 | 378 | ## Display directions 379 | 380 | You can display directions on the map, either between 2 points or through a list of waypoints, as long as all the points are in the same venue. 381 | 382 | The show direction method is the easyest to use and will take car of the API request and the display. 383 | 384 | ```javascript 385 | map.showDirections(from, to, waypoints, options, callback); 386 | ``` 387 | 388 | You can also get the directions first using the API and then display all or portion of the directions using: 389 | 390 | ```javascript 391 | Mapwize.Api.getDirections(from, to, waypoints, options, callback); 392 | map.startDirections(directions, options); 393 | ``` 394 | 395 | The from parameter is required and is an objects which can have the following properties: 396 | 397 | { 398 | placeId: string (the Id of a place. If used, latitude/longitude/floor/venueId are ignored) 399 | latitude: number (if used, all latitude/longitude/floor are required) 400 | longitude: number (if used, all latitude/longitude/floor are required) 401 | floor: number (if used, all latitude/longitude/floor are required) 402 | venueId: string (the Id of the venue the latitude/longitude is in. Only required if the venue cannot be inferred from the to and the waypoints) 403 | } 404 | 405 | The to parameter is required. It can be an object, with the same structure as the From, so the destination is fixed. It can also be an array, and in that case, the closest destination is used. It can also be an object refering to a placeList, and in that case the closest place of the list if used: 406 | 407 | { 408 | placeListId: string (the Id of a placeList) 409 | } 410 | 411 | For the waypoints, you can specify an array with objects with the same structure as the From. 412 | 413 | The options parameter is an object supporting the following properties: 414 | 415 | { 416 | isAccessible: boolean (if set to true, only routes and connectors accessible to people with disabilities are used in the computation. Default is false) 417 | waypointOptimize: boolean (if set to true, the order of the waypoints is optimized to minimize the total time of the directions. Default is false) 418 | } 419 | 420 | The callback is a function returning an error object or directions object 421 | 422 | callback = function(err, directions) 423 | 424 | Err is null if there was no error and a direction is displayed. 425 | 426 | Directions is an object containing the folloing properties: 427 | 428 | { 429 | from: from object as above, 430 | to: to object as above, 431 | distance: total distance in meters, 432 | traveltime: total time in seconds, 433 | bounds: bounds containing the total route, 434 | route: [ a list of paths on different floors 435 | floor: the floor number of the route 436 | path: the list of coordinates on the way points 437 | distance: the distance of the route in meter 438 | bounds: the bounds of the route 439 | isStart: if the route of the first one 440 | isEnd: if the route is the last one 441 | fromFloor: the floor of the route before 442 | toFloor: the floor of the route after 443 | connectorTypeTo: the type of connector used to go to the next floor 444 | connectorTypeFrom: the type of connector used to come from the previous floor 445 | ], 446 | waypoints: list of waypoints, ordered, 447 | subdirections: directions object with the same structure as the parent for each step between waypoints. 448 | } 449 | 450 | To remove the directions from the map, use the method `stopDirections` 451 | 452 | ```javascript 453 | map.stopDirections(); 454 | ``` 455 | ---------- 456 | 457 | ## User position 458 | 459 | ### Display user position 460 | 461 | By default, if the user position is available, it is displayed. You can prevent the user location from being displayed by setting the `showUserPosition` map option to false: 462 | 463 | ```javascript 464 | var map = Mapwize.map('map', { 465 | showUserPosition: false 466 | }); 467 | ``` 468 | 469 | If `showUserPosition` is not false, the control proposing the user to go to its position is displayed by default. You can disable it by setting the `showUserPositionControl` map option to false: 470 | 471 | ```javascript 472 | var map = Mapwize.map('map', { 473 | showUserPositionControl: false 474 | }); 475 | ``` 476 | 477 | ### Use browser location 478 | 479 | By default, the user position is acquired from the web browser if available. You can disable it by setting the `useBrowserLocation` map option to false: 480 | 481 | ```javascript 482 | var map = Mapwize.map('map', { 483 | useBrowserLocation: false 484 | }); 485 | ``` 486 | 487 | ### Follow user mode 488 | 489 | The follow user mode allows to have the map moving as the position is updated. 490 | 491 | You can manage the followUserMode using the following commands: 492 | 493 | ```javascript 494 | map.getFollowUserMode(); 495 | map.setFollowUserMode(true or false); 496 | ``` 497 | 498 | ### Center map on user position 499 | 500 | To center the map on the current user position, you can use the `centerOnUser` method. The method takes a minZoom parameter. If the current zoom of the map is lower than minZoom, then the map is zoomed to minZoom. 501 | 502 | ```javascript 503 | map.centerOnUser(19); 504 | ``` 505 | 506 | ### Get user position 507 | 508 | You can get the current user position using the method 509 | 510 | ```javascript 511 | map.getUserPosition(); 512 | ``` 513 | The returned object has the following properties: 514 | 515 | { 516 | latitude: the latitude 517 | longitude: the longitude 518 | floor: the floor (null if ouside a building or unknown) 519 | accuracy: the radius in meter in which the user has a high probability to actually be, on the current floor. There is no vertical accuracy. 520 | validUntil: timestamp until when the position is valid. Used for example for sources like QR-code which do not necesseraly get continuous measurements. 521 | source: a string describing the source of the measurement. 522 | } 523 | 524 | ### Setting the user position 525 | 526 | You can manually set the user position by using the `setUserPosition` method: 527 | 528 | ```javascript 529 | map.setUserPosition({ 530 | latitude: 40.712, 531 | longitude: -74.227, 532 | floor: 2, 533 | accuracy: 5 534 | }); 535 | ``` 536 | 537 | The accuracy is specified in meters. At this point the accuracy is only on horizontal position. There is no accuracy on floor. 538 | 539 | If the followUserMode is enabled, then this methods will have as consequence to move the map. Otherwize, only the user position dot will be moved. 540 | 541 | When the setUserPosition method is used, the position of the user is locked on the provided position until setUserPosition is called again. If you then want to unlock the positon and use the default position given by the browser for example, then you need to call 542 | 543 | ```javascript 544 | map.unlockUserPosition(); 545 | ``` 546 | 547 | ### Using multiple measurement sources 548 | 549 | In many situations, many positioning methods can be combined in order to define the most probable position of the user, for example GPS, iBeacons, QR-Code, Wifi, Lifi, ... 550 | 551 | To handle this situation, Mapwize allows you to send different measurements from different sources to the SDk. 552 | 553 | To do so, use the method 554 | 555 | ```javascript 556 | map.newUserPositionMeasurement(measurement); 557 | ``` 558 | 559 | where measurement is an object with the following properties 560 | 561 | { 562 | latitude 563 | longitude 564 | floor 565 | accuracy 566 | validity: the time in milliseconds the measurement is valid. This allows the use of single measurement methods like the scan of a code, for which no update is provided as the user moves. Set validity to null for infinite. 567 | source: a string describing the source of the measurement. Each time a new measurement arrives, it discards automatically the previous measurement from the same source. 568 | } 569 | 570 | You can lock the user position to the current position using the method lockUserPosition. When the position is locked, all measurements are ignored until the method unlockUserPosition is called. 571 | 572 | The method setUserPosition overrules all the previous rules and sets the user position until unlockUserPosition is called. 573 | 574 | ### Setting the user heading 575 | When a compass is available, it can be interesting to display the direction the user is looking. To do so, the method setUserHeading can be used, giving it an angle in degree. Example if the user is looking south: 576 | 577 | ```javascript 578 | map.setUserHeading(180); 579 | ``` 580 | 581 | To remove the display of the compass, simply set the angle to null. 582 | 583 | ---------- 584 | 585 | ## Setting the map based on URL 586 | 587 | The Mapwize URL allows to refer positions, venues, places and directions so they can be displayed uniformly on every device, web and mobile. 588 | 589 | The same URLs can be typed in a web browser or scanned as QR-code, using the Mapwize apps or the SDK. 590 | 591 | The URLs always start with http://mwz.io/ 592 | 593 | To load a URL, use the following method: 594 | 595 | ```javascript 596 | map.loadURL(url, callback); 597 | ``` 598 | Same callback as Mapwize.Url.parse function (see below) 599 | 600 | The complete documentation regarding the URL format can be found [in the mapwize-url-scheme repository on github](https://github.com/Mapwize/mapwize-url-scheme). 601 | 602 | ---------- 603 | 604 | ## Parse a mapwize URL 605 | 606 | ```javascript 607 | Mapwize.Url.parse(url, callback); 608 | ``` 609 | 610 | The callback function returns an error (if any) and the parsed object in the following format 611 | 612 | { 613 | - venue: the venue object to which the url relates. 614 | - universe: the universe object if the ?u parameter is specified in the url 615 | - language: the language code if the ?l parameter is specified in the url 616 | - outdoorMapProvider: the outdoor map provider if the ?outdoorMapProvider is specified in the url 617 | - accessKey: the accessKey if the ?k parameter is specified in the url. 618 | - floor: the value of the floor if specified in the url, or the floor of the place, or the start floor of the direction 619 | - zoom: the zoom if ?z parameter is specified 620 | - userPosition: a user position object with lat lon floor if the url is a beacon, or if the direction starts from the beacon 621 | - from: object with the origin of the direction if direction url starting from the place. 622 | - to: object with the venue, place or placelist for related urls or the destination of the direction. 623 | - direction: direction object if it's a direction url, or null 624 | - bounds: the bounds that should be set to the map to properly display the url 625 | } 626 | 627 | #### Example 628 | ````javascript 629 | Mapwize.Url.parse(url, function (err, parsedUrl) { 630 | // In case of error, parsedUrl might still contain useful information. 631 | 632 | if (parsedUrl) { 633 | if (err) { 634 | console.error(err); 635 | // Show user a warning 636 | } 637 | // Do something with parsedUrl 638 | } 639 | else { 640 | console.error(err); 641 | } 642 | }); 643 | 644 | ```` 645 | 646 | ## Markers 647 | 648 | ### Styling 649 | 650 | You can use your own marker by passing the following options on the map creation. 651 | Please refer to Leaflet for iconSize and iconAnchor options. 652 | ```javascript 653 | displayMarkerOptions: { 654 | iconUrl: 'your-url?png', 655 | iconSize: [x, y], 656 | iconAnchor: [x, y] 657 | } 658 | ``` 659 | 660 | ### Adding marker 661 | 662 | You can add markers on the map to show a position of interest. At this points, marker are static elements and users cannot interact with them. 663 | 664 | To add a marker, use the function 665 | 666 | ```javascript 667 | map.addMarker(position, callback); 668 | ``` 669 | 670 | where `position` is an **object** with the following properties (with same priority order): 671 | 672 | *A full place object* 673 | 674 | OR 675 | 676 | { 677 | latitude: number (if used, longitude is required) 678 | longitude: number (if used, latitude is required) 679 | floor: number (can be null) 680 | } 681 | 682 | OR 683 | 684 | *Same params as api get place* 685 | 686 | The `callback` function take 2 params: 687 | (*Object*) `err` if an error has occurred 688 | (*String*) `markerId` an uniq id for this marker 689 | 690 | ### Remove marker 691 | #### Remove one marker 692 | 693 | To remove only one marker, use the function 694 | 695 | ```javascript 696 | map.removeMarker(markerId); 697 | ``` 698 | 699 | `markerId` is returned by addMarker callback 700 | 701 | #### Remove all markers 702 | 703 | To remove all the markers at once, use the function 704 | 705 | ```javascript 706 | map.removeMarkers(); 707 | ``` 708 | 709 | ---------- 710 | 711 | ## Listen for events 712 | 713 | The map will emit various events you can listen to. 714 | 715 | ### click 716 | Fired when the user clicks (or taps) the map. 717 | 718 | ```javascript 719 | map.on('click', function (e) { 720 | console.log(e); 721 | }); 722 | ``` 723 | 724 | ### contextmenu 725 | Fired when the user pushes the right mouse button on the map. Also fired on mobile when the user holds a single touch for a second (also called long press). 726 | 727 | ```javascript 728 | map.on('contextmenu', function (e) { 729 | console.log(e); 730 | }); 731 | ``` 732 | 733 | ### directions start 734 | Fired when directions are displayed on the map. 735 | 736 | ```javascript 737 | map.on('directionsStart', function (e) { 738 | console.log('Directions have been loaded'); 739 | }); 740 | ``` 741 | 742 | ### directions stop 743 | Fired when directions are stopped and not displayed on the map anymore. 744 | 745 | ```javascript 746 | map.on('directionsStop', function (e) { 747 | console.log('Directions have stopped'); 748 | }); 749 | ``` 750 | 751 | ### floor change 752 | Fired when the currently viewed floor is changed. 753 | 754 | ```javascript 755 | map.on('floorChange', function (e) { 756 | console.log('Floor changed to ' + e.floor); 757 | }); 758 | ``` 759 | 760 | ### floors change 761 | Fired when the list of available floors at the currently viewed location is changed. 762 | 763 | ```javascript 764 | map.on('floorsChange', function(e) { 765 | console.log('Available floors at currently viewed location changed to ' + e.floors); 766 | }); 767 | ``` 768 | 769 | ### followUserMode change 770 | Fired when FollowUserMode changed. 771 | 772 | ```javascript 773 | map.on('followUserModeChange', function (e) { 774 | console.log('followUserMode new value: ' + e.followUserMode); 775 | }); 776 | ``` 777 | 778 | ### margins change 779 | Fired when the map margins have changed 780 | 781 | ```javascript 782 | map.on('marginsChange', function (e) { 783 | console.log('margins: ' + e.margins); 784 | }); 785 | ``` 786 | 787 | ### marker click 788 | Fired when a marker is clicked 789 | 790 | ```javascript 791 | map.on('markerClick', function (e) { 792 | console.log('marker id: ' + e.markerId); 793 | }); 794 | ``` 795 | 796 | ### moveend 797 | Fired when the view of the map stops changing (e.g. user stopped dragging the map). 798 | 799 | ```javascript 800 | map.on('moveend', function (e) { 801 | console.log(e); 802 | }); 803 | ``` 804 | 805 | ### place click 806 | Fired when a place is clicked. 807 | 808 | ```javascript 809 | map.on('placeClick', function (e) { 810 | console.log(e.place); 811 | }); 812 | ``` 813 | 814 | ### preferred language change 815 | Fired when the map preferred language has changed 816 | 817 | ```javascript 818 | map.on('preferredLanguageChange', function (e) { 819 | console.log('Language: ' + e.language); 820 | }); 821 | ``` 822 | 823 | ### user position change 824 | Fired when the user position has changed. 825 | 826 | ```javascript 827 | map.on('userPositionChange', function (e) { 828 | console.log('User position changed to ' + e.userPosition); 829 | }); 830 | ``` 831 | 832 | ### venue enter 833 | Fired when a venue is displayed (zoom level >= 16 and center of the map inside the venue) 834 | 835 | ```javascript 836 | map.on('venueEnter', function (e) { 837 | console.log('Venue entered: ' + e.venue); 838 | }); 839 | ``` 840 | 841 | ### venue exit 842 | Fired when leaving the venue that was previously entered 843 | 844 | ```javascript 845 | map.on('venueExit', function (e) { 846 | console.log('Venue exited: ' + e.venue); 847 | }); 848 | ``` 849 | 850 | ---------- 851 | 852 | ## QR-code 853 | If you have retrieved a QR-code and want to pass it to the SDK, you can use the following method and pass the QR-code payload: 854 | 855 | ```javascript 856 | map.loadURL('payload'); 857 | ``` 858 | 859 | To add the QR-code scan button to the map (by default at bottom left): 860 | 861 | ```javascript 862 | var qrcodeControl = Mapwize.qrcodeControl({ 863 | onClick: function () { 864 | /* scan the QR code and retrieve payload */ 865 | map.loadURL(payload); 866 | } 867 | }); 868 | qrcodeControl.addTo(map); 869 | ``` 870 | 871 | ---------- 872 | 873 | ## Access Key 874 | If you want to access private buildings, you need to specify the related access key. 875 | 876 | You can pass the access key directly when initializing the map: 877 | 878 | ```javascript 879 | var map = Mapwize.map('map', { 880 | accessKey: 'YOUR KEY' 881 | }); 882 | ``` 883 | 884 | Or you can use the access method 885 | 886 | ```javascript 887 | map.access('key', function (result) { 888 | if (result) { 889 | // NO error, key is valid 890 | } 891 | else { 892 | // ERROR, key is not valid 893 | } 894 | }); 895 | ``` 896 | 897 | ---------- 898 | 899 | ## Cache 900 | To prevent too many network requests while browsing the map, the SDK keeps a cache of some data it already downloaded. 901 | 902 | The Time To Live of the cache is 5 minutes. 903 | 904 | If you want to force the map to refresh the cache and update itself, you can call the refresh method anytime. 905 | 906 | ```javascript 907 | map.refresh(); 908 | ``` 909 | 910 | ---------- 911 | 912 | ## Margins 913 | It often happens that part of the map is hidden by banners or controls on the top or on the bottom. For example, if you display a banner to show the details of the place you just clicked on, it's better to display the banner on top of the map than having to resize the map. 914 | 915 | However, you want to make sure that the Mapwize controls are always visible, like the followUserMode button and the floor selector. Also, that if you make a fitBounds, the area will be completely in the visible part of the map. 916 | 917 | For this purpose, you can set a top and a bottom margin on the map. We garantee that nothing important will be displayed in those margin areas. 918 | 919 | To set the margins, you can pass them in pixels when you intialize the map: 920 | 921 | ```javascript 922 | var map = Mapwize.map('map', { 923 | marginTop: 50, 924 | marginBottom: 50 925 | }); 926 | ``` 927 | 928 | Or you can change them at runtime 929 | 930 | ```javascript 931 | map.setTopMargin(50); 932 | map.setBottomMargin(50); 933 | ``` 934 | 935 | ---------- 936 | 937 | ## Modify Place Style 938 | The style of a place can be modifyed directly within the SDK and can then override the style sent by the server. This is the best way to make changes in real-time on the map as it does not require to contact the Mapwize servers. For example, this can be used to display the availability of a meeting room. 939 | 940 | ```javascript 941 | map.setPlaceStyle(placeId, style); 942 | ``` 943 | 944 | where style is an object with the format: 945 | 946 | { 947 | markerUrl: string (An url to the icon of the marker. Must be an image, ideally png, square, 100*100 pixels), 948 | strokeColor: string (The color of the shape border as #hex), 949 | strokeOpacity: number (The opacity of the border, between 0 and 1), 950 | strokeWidth: number (The width of the border), 951 | fillColor: string (The color of the inside of the shape as #hex), 952 | fillOpacity: number (The opacity of the inside, between 0 and 1), 953 | labelBackgroundColor: string (The color of the backgroud of the label as #hex), 954 | labelBackgroundOpacity: number (The opacity of the background of the label, between 0 and 1) 955 | } 956 | 957 | example: 958 | 959 | ```javascript 960 | { 961 | markerUrl: 'http://myserver.com/image.png', 962 | strokeColor: '#C51586', 963 | strokeOpacity: 1, 964 | strokeWidth: 2, 965 | fillColor: '#FFFFFF', 966 | fillOpacity: 0.3, 967 | labelBackgroundColor: null, 968 | labelBackgroundOpacity: null 969 | } 970 | ``` 971 | 972 | Note that if a parameter is null, the value defined on the server will be used. 973 | 974 | ---------- 975 | 976 | ## Multilingual venues 977 | Venues can support multiple languages. 978 | By default, venues are displayed in their default language configured on the backend-side. 979 | Using the function 980 | 981 | ```javascript 982 | map.setPreferredLanguage(language); 983 | ``` 984 | 985 | it is possible to set the preferred language of the user. If a venue supports the preferred language, it will be displayed in that language. 986 | Otherwise, it will be displayed in the default language. 987 | 988 | Languages are defined using 2 letter codes like 'en', 'fr', ... 989 | 990 | Setting the preferred language to null displays all venues in their default language. 991 | 992 | ---------- 993 | 994 | ## Outdoor map provider 995 | Available map provider : 'mapbox-street', 'mapbox', 'mapbox-satellite', 'tomtom-street', 'tomtom', 'none' 996 | 997 | If you don't set a specific provider, our default outdoor will be displayed 998 | 999 | ---------- 1000 | 1001 | ## Working with universes 1002 | 1003 | Defining multiple universes for a venue let you show different views with different permission levels. By default, the first universe which the user has access to is displayed. 1004 | 1005 | ### Set universe for venue 1006 | Tu display a specific universe for a venue, set it with the `setUniverseForVenue` method 1007 | 1008 | ```javascript 1009 | map.setUniverseForVenue(universeId, venueId); 1010 | ``` 1011 | 1012 | This automaticaly refreshes the map if needed. 1013 | 1014 | ### Get universe for venue 1015 | To know wich universe is set for a venue use `getUniverseForVenue` method 1016 | 1017 | ```javascript 1018 | var universeId = map.getUniverseForVenue(venueId); 1019 | ``` 1020 | 1021 | returns the universeId or null if no universe was previously set. 1022 | 1023 | ---------- 1024 | 1025 | 1026 | ## Adding custom data to objects 1027 | To define specific behavior in your app for venues, places, placeLists or beacons, it is handy to attach custom data to those objects. 1028 | Data can be added using the API or the backend interface. 1029 | Data are retrieved in the objects under the property "data". 1030 | 1031 | ---------- 1032 | 1033 | ## Api 1034 | 1035 | You can access to the Mapwize api with the SDK by using `Mapwize.Api`. 1036 | 1037 | ### Venues 1038 | 1039 | #### get 1040 | 1041 | ```javascript 1042 | Mapwize.Api.venues.get(options, callback); 1043 | ``` 1044 | 1045 | ##### Arguments 1046 | 1047 | 1. `options`: *object|String* 1048 | 2. `callback`: *function(err, venue)* `venue` is an **venue** object. 1049 | 1050 | `options` can be a string as venueId or an object as: 1051 | 1052 | { 1053 | [id|venueId]: (String) an id of venue 1054 | [alias|venueAlias]: (String) an alias of venue 1055 | [name|venueName]: (String) a name of venue 1056 | } 1057 | 1058 | Priority rules: 1059 | - if you provide id, alias and name are ignored 1060 | - if you provide alias, name is ignored 1061 | 1062 | ##### Example 1063 | 1064 | ```javascript 1065 | // Using venue id 1066 | Mapwize.Api.venues.get('aValidVenueId', function (err, venue) { 1067 | console.log(venue); 1068 | }); 1069 | 1070 | // Using venue alias 1071 | Mapwize.Api.venues.get({alias: 'aValidVenueAlias'}, function (err, venue) { 1072 | console.log(venue); 1073 | }); 1074 | ``` 1075 | 1076 | #### list 1077 | 1078 | Get a list of venues 1079 | 1080 | ```javascript 1081 | Mapwize.Api.venues.list(options, callback); 1082 | ``` 1083 | 1084 | ##### Arguments 1085 | 1086 | 1. `options`: *object* 1087 | 2. `callback`: *function(err, venues)* `venues` is an array of **venue** object. 1088 | 1089 | `options` is an object as: 1090 | 1091 | { 1092 | // you should use both together 1093 | latitudeMin: (number) 1094 | latitudeMax: (number) 1095 | 1096 | // you should use both together 1097 | longitudeMin: (number) 1098 | longitudeMax: (number) 1099 | } 1100 | 1101 | ##### Example 1102 | 1103 | ```javascript 1104 | // with empty options 1105 | Mapwize.Api.venues.list({}, function (err, venues) { 1106 | // venues contain all venues you can access 1107 | console.log(venues); 1108 | }); 1109 | 1110 | // In defined area 1111 | Mapwize.Api.venues.list({latitudeMin: 0, latitudeMax: 1, longitudeMin: 0, longitudeMax: 1}, function (err, venues) { 1112 | // venues contain all venues in the defined area 1113 | console.log(venues); 1114 | }); 1115 | ``` 1116 | 1117 | #### By organization 1118 | 1119 | Get a list of venues 1120 | 1121 | ```javascript 1122 | Mapwize.Api.getVenuesForOrganization(organizationId, callback); 1123 | ``` 1124 | 1125 | ##### Arguments 1126 | 1127 | 1. `organizationId`: *string* 1128 | 2. `callback`: *function(err, venues)* `venues` is an array of **venue** object. 1129 | 1130 | ##### Example 1131 | 1132 | ```javascript 1133 | Mapwize.Api.getVenuesForOrganization('aValidOrganizationId', function (err, venues) { 1134 | console.log(venues); 1135 | }); 1136 | ``` 1137 | 1138 | ### Places 1139 | 1140 | #### get 1141 | 1142 | ```javascript 1143 | Mapwize.Api.places.get(options, callback); 1144 | ``` 1145 | 1146 | ##### Arguments 1147 | 1148 | 1. `options`: *object|String* 1149 | 2. `callback`: *function(err, place)* `place` is an **place** object. 1150 | 1151 | `options` can be a string as placeId or an object as: 1152 | 1153 | { 1154 | [id|placeId]: (String) an id of place 1155 | [alias|placeAlias]: (String) an alias of place 1156 | [name|placeName]: (String) a name of place 1157 | 1158 | venueId: (String) required if you use name or alias 1159 | } 1160 | 1161 | Priority rules: 1162 | - if you provide id, alias and name are ignored 1163 | - if you provide alias, name is ignored 1164 | 1165 | ##### Example 1166 | 1167 | ```javascript 1168 | // Using venue id 1169 | Mapwize.Api.places.get('aValidPlaceId', function (err, place) { 1170 | console.log(place); 1171 | }); 1172 | 1173 | // Using venue alias 1174 | Mapwize.Api.places.get({alias: 'aValidPlaceAlias', venueId: 'aValideVenueId'}, function (err, place) { 1175 | console.log(place); 1176 | }); 1177 | ``` 1178 | 1179 | #### list 1180 | 1181 | Get a list of places 1182 | 1183 | ```javascript 1184 | Mapwize.Api.places.list(options, callback); 1185 | ``` 1186 | 1187 | ##### Arguments 1188 | 1189 | 1. `options`: *object* 1190 | 2. `callback`: *function(err, places)* `places` is an array of **place** object. 1191 | 1192 | `options` is an object as: 1193 | 1194 | { 1195 | // get only places in placeList 1196 | // if used, other params are ignored 1197 | placeListId: (String) 1198 | 1199 | // you should use both together 1200 | latitudeMin: (number) 1201 | latitudeMax: (number) 1202 | 1203 | // you should use both together 1204 | longitudeMin: (number) 1205 | longitudeMax: (number) 1206 | 1207 | floor: (Integer) 1208 | } 1209 | 1210 | ##### Example 1211 | 1212 | ```javascript 1213 | // with empty options 1214 | Mapwize.Api.places.list({}, function (err, places) { 1215 | // places contain all places you can access 1216 | console.log(places); 1217 | }); 1218 | 1219 | // In defined area 1220 | Mapwize.Api.places.list({latitudeMin: 0, latitudeMax: 1, longitudeMin: 0, longitudeMax: 1, floor: 1}, function (err, places) { 1221 | // places contain all places in the defined area on floor 1 (and floor outdoor (null)) 1222 | console.log(places); 1223 | }); 1224 | ``` 1225 | 1226 | ### Layers 1227 | 1228 | #### list 1229 | 1230 | Get a list of layers 1231 | 1232 | ```javascript 1233 | Mapwize.Api.layers.list(options, callback); 1234 | ``` 1235 | 1236 | ##### Arguments 1237 | 1238 | 1. `options`: *object* 1239 | 2. `callback`: *function(err, layers)* `layers` is an array of **layer** object. 1240 | 1241 | `options` is an object as: 1242 | 1243 | { 1244 | // you should use both together 1245 | latitudeMin: (number) 1246 | latitudeMax: (number) 1247 | 1248 | // you should use both together 1249 | longitudeMin: (number) 1250 | longitudeMax: (number) 1251 | 1252 | floor: (Integer) 1253 | } 1254 | 1255 | ##### Example 1256 | 1257 | ```javascript 1258 | // with empty options 1259 | Mapwize.Api.layers.list({}, function (err, layers) { 1260 | // layers contain all layers you can access 1261 | console.log(layers); 1262 | }); 1263 | 1264 | // In defined area 1265 | Mapwize.Api.layers.list({latitudeMin: 0, latitudeMax: 1, longitudeMin: 0, longitudeMax: 1, floor: 1}, function (err, layers) { 1266 | // layers contain all layers in the defined area on floor 1 (and floor outdoor (null)) 1267 | console.log(layers); 1268 | }); 1269 | ``` 1270 | 1271 | ### Connector places 1272 | 1273 | #### list 1274 | 1275 | Get a list of connectorPlace 1276 | 1277 | ```javascript 1278 | Mapwize.Api.connectorPlaces.list(options, callback); 1279 | ``` 1280 | 1281 | ##### Arguments 1282 | 1283 | 1. `options`: *object* 1284 | 2. `callback`: *function(err, connectorPlaces)* `connectorPlaces` is an array of **connectorPlace** object. 1285 | 1286 | `options` is an object as: 1287 | 1288 | { 1289 | // you should use both together 1290 | latitudeMin: (number) 1291 | latitudeMax: (number) 1292 | 1293 | // you should use both together 1294 | longitudeMin: (number) 1295 | longitudeMax: (number) 1296 | 1297 | floor: (Integer) 1298 | } 1299 | 1300 | ##### Example 1301 | 1302 | ```javascript 1303 | // with empty options 1304 | Mapwize.Api.connectorPlaces.list({}, function (err, connectorPlaces) { 1305 | // connectorPlaces contain all connectorPlaces you can access 1306 | console.log(connectorPlaces); 1307 | }); 1308 | 1309 | // In defined area 1310 | Mapwize.Api.connectorPlaces.list({latitudeMin: 0, latitudeMax: 1, longitudeMin: 0, longitudeMax: 1, floor: 1}, function (err, connectorPlaces) { 1311 | // connectorPlaces contain all connectorPlaces in the defined area on floor 1 (and floor outdoor (null)) 1312 | console.log(connectorPlaces); 1313 | }); 1314 | ``` 1315 | 1316 | ### Beacons 1317 | 1318 | #### get 1319 | 1320 | ```javascript 1321 | Mapwize.Api.beacons.get(options, callback); 1322 | ``` 1323 | 1324 | ##### Arguments 1325 | 1326 | 1. `options`: *object|String* 1327 | 2. `callback`: *function(err, beacons)* `beacons` is an **beacon** object. 1328 | 1329 | `options` can be a string as beaconId or an object as: 1330 | 1331 | { 1332 | [id|beaconId]: (String) an id of beacon 1333 | payload: (String) a payload of beacon 1334 | [alias|beaconAlias]: (String) a alias of beacon 1335 | [name|beaconName]: (String) a name of beacon 1336 | venueId: (String) required if you use name or alias 1337 | } 1338 | 1339 | Priority rules: 1340 | - if you provide id: payload, alias and name are ignored 1341 | - if you provide payload: alias and name are ignored 1342 | - if you provide alias: name is ignored 1343 | 1344 | ##### Example 1345 | 1346 | ```javascript 1347 | // Using venue id 1348 | Mapwize.Api.beacons.get('aValidBeaconId', function (err, beacon) { 1349 | console.log(beacon); 1350 | }); 1351 | 1352 | // Using venue alias 1353 | Mapwize.Api.beacons.get({alias: 'aValidBeaconAlias', venueId: 'aValidVenueId'}, function (err, beacon) { 1354 | console.log(beacon); 1355 | }); 1356 | ``` 1357 | 1358 | #### list 1359 | 1360 | Get a list of beacons 1361 | 1362 | ```javascript 1363 | Mapwize.Api.beacons.list(options, callback); 1364 | ``` 1365 | 1366 | ##### Arguments 1367 | 1368 | 1. `options`: *object* 1369 | 2. `callback`: *function(err, beacons)* `beacons` is an array of **beacon** object. 1370 | 1371 | `options` is an object as: 1372 | 1373 | { 1374 | venueId: (String, required) a venue id 1375 | } 1376 | 1377 | ##### Example 1378 | 1379 | ```javascript 1380 | // with empty options 1381 | Mapwize.Api.beacons.list({venueId: 'aValidVenueId'}, function (err, beacons) { 1382 | // beacons contain all beacons you can access in the venue 1383 | console.log(beacons); 1384 | }); 1385 | ``` 1386 | 1387 | ### Place types 1388 | 1389 | #### get 1390 | 1391 | ```javascript 1392 | Mapwize.Api.placeTypes.get(options, callback); 1393 | ``` 1394 | 1395 | ##### Arguments 1396 | 1397 | 1. `options`: *String* a placeType id 1398 | 2. `callback`: *function(err, placeType)* `placeType` is an **placeType** object. 1399 | 1400 | ##### Example 1401 | 1402 | ```javascript 1403 | Mapwize.Api.placeTypes.get('aValidePlaceTypeId', function (err, placeType) { 1404 | console.log(placeType); 1405 | }); 1406 | ``` 1407 | 1408 | ### Place Lists 1409 | 1410 | #### get 1411 | 1412 | ```javascript 1413 | Mapwize.Api.placeLists.get(options, callback); 1414 | ``` 1415 | 1416 | ##### Arguments 1417 | 1418 | 1. `options`: *object|String* 1419 | 2. `callback`: *function(err, placeList)* `placeList` is an **placeList** object. 1420 | 1421 | `options` can be a string as placeListId or an object as: 1422 | 1423 | { 1424 | [id|listId]: (String) an id of venue 1425 | [alias|listAlias]: (String) an alias of venue 1426 | [name|listName]: (String) a name of venue 1427 | venueId: (String) required if you use name or alias 1428 | } 1429 | 1430 | Priority rules: 1431 | - if you provide id: alias and name are ignored 1432 | - if you provide alias: name is ignored 1433 | 1434 | ##### Example 1435 | 1436 | ```javascript 1437 | // Using placeList id 1438 | Mapwize.Api.placeLists.get('aValidePlaceListId', function (err, placeList) { 1439 | console.log(placeList); 1440 | }); 1441 | 1442 | // Using placeList alias 1443 | Mapwize.Api.placeLists.get({alias: 'aValidePlaceListAlias', venueId: 'aValidVenueId'}, function (err, placeList) { 1444 | console.log(placeList); 1445 | }); 1446 | ``` 1447 | 1448 | #### list 1449 | 1450 | Get a list of placeLists 1451 | 1452 | ```javascript 1453 | Mapwize.Api.placeLists.list(options, callback); 1454 | ``` 1455 | 1456 | ##### Arguments 1457 | 1458 | 1. `options`: *object* 1459 | 2. `callback`: *function(err, placeLists)* `placeLists` is an array of **placeList** object. 1460 | 1461 | `options` is an object as: 1462 | 1463 | { 1464 | venueId: (String, required) 1465 | } 1466 | 1467 | ##### Example 1468 | 1469 | ```javascript 1470 | // with empty options 1471 | Mapwize.Api.placeLists.list({venueId: 'aValidVenueId'}, function (err, placeLists) { 1472 | // placeLists contain all placeLists you can access in the venue 1473 | console.log(placeLists); 1474 | }); 1475 | ``` 1476 | 1477 | ### Search 1478 | 1479 | Search in places, placeLists and venues 1480 | 1481 | ```javascript 1482 | Mapwize.Api.search(query, options, callback); 1483 | ``` 1484 | 1485 | #### Arguments 1486 | 1487 | 1. `query`: *string* 1488 | 2. `options`: *object* 1489 | 3. `callback`: *function(err, results)* `results` is an array of **place**, **placeList** or **venue** objects. 1490 | 1491 | `options` is an object as: 1492 | 1493 | { 1494 | venueId: (string), 1495 | universeId: (string), 1496 | objectClass: (array of string) can contain ['place', 'placeList', 'venue'] 1497 | } 1498 | 1499 | #### Example 1500 | 1501 | ```javascript 1502 | // with empty options 1503 | Mapwize.Api.search('what you want to find', {venueId: 'aValidVenueId'}, function (err, results) { 1504 | // If objectClass is empty, all type can be returned 1505 | console.log(results); 1506 | }); 1507 | ``` 1508 | -------------------------------------------------------------------------------- /mapwize/messukeskus.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Mapwize.js Demo - simple map 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 139 | 140 | 141 | 142 | -------------------------------------------------------------------------------- /mock.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | /** 8 | * Mock version of a REST API that exposes RoomAnalytics 9 | */ 10 | 11 | const debug = require("debug")("api"); 12 | const fine = require("debug")("api:fine"); 13 | 14 | var devices; 15 | try { 16 | devices = require("./devices.json"); 17 | } 18 | catch (err) { 19 | console.log("Please specify a list of devices.") 20 | process.exit(-1) 21 | } 22 | 23 | // 24 | // Web API 25 | // 26 | 27 | var express = require("express"); 28 | var app = express(); 29 | 30 | // Enable CORS 31 | app.use(function (req, res, next) { 32 | res.header("Access-Control-Allow-Origin", "*"); 33 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 34 | next(); 35 | }); 36 | 37 | // Add JSON parsing 38 | var bodyParser = require("body-parser"); 39 | app.use(bodyParser.urlencoded({ extended: true })); 40 | app.use(bodyParser.json()); 41 | 42 | var started = Date.now(); 43 | app.route("/") 44 | // healthcheck 45 | .get(function (req, res) { 46 | res.json({ 47 | message: "Congrats, your RoomAnalytics Aggregator Mock is up and running", 48 | since: new Date(started).toISOString() 49 | }); 50 | }) 51 | 52 | app.get("/devices", function (req, res) { 53 | const mapped = devices.map((device) => { 54 | return { 55 | id: device.id, 56 | location: device.location, 57 | ipAddress: device.ipAddress 58 | }; 59 | }); 60 | 61 | res.json(mapped); 62 | }) 63 | 64 | app.get("/devices/:device", function (req, res) { 65 | const id = req.params.device; 66 | 67 | let found = devices.find(function (device) { 68 | return (device.id === id) 69 | }) 70 | if (!found) { 71 | res.status(404).json({ 72 | message: "device not found" 73 | }) 74 | return 75 | } 76 | 77 | res.json({ 78 | id: found.id, 79 | location: found.location, 80 | ipAddress: found.ipAddress 81 | }) 82 | }) 83 | 84 | app.get("/devices/:device/last", function (req, res) { 85 | const id = req.params.device; 86 | 87 | const count = Math.round(Math.random() * 5 + 2); 88 | fine(`returned mock latest: ${count}, for device: ${id}`) 89 | res.json({ 90 | id: id, 91 | peopleCount: count 92 | }); 93 | }) 94 | 95 | app.get("/devices/:device/average", function (req, res) { 96 | const id = req.params.device; 97 | 98 | // Get period (in seconds) 99 | let period = req.query.period; 100 | if (!period) { 101 | period = 15; // default to 15s 102 | } 103 | 104 | // Mock'ed data 105 | const count = Math.round(Math.random() * 7 - 1); 106 | fine(`returned mock average: ${count}, for device: ${id}`) 107 | res.json({ 108 | id: id, 109 | peopleCount: count, 110 | period: period, 111 | unit: "seconds" 112 | }); 113 | }) 114 | 115 | 116 | // Starts the service 117 | // 118 | var port = process.env.OVERRIDE_PORT || process.env.PORT || 8080; 119 | app.listen(port, function () { 120 | console.log("Collector API started at http://localhost:" + port + "/"); 121 | console.log(" GET / for healthcheck"); 122 | console.log(" GET /devices for the list of devices"); 123 | console.log(" GET /devices/{device} to get the details for the specified device"); 124 | console.log(" GET /devices/{device}/last for latest PeopleCount value received"); 125 | console.log(" GET /devices/{device}/average?period=30 for a computed average"); 126 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "roomkit-collector", 3 | "version": "1.0.0", 4 | "description": "Collects PeopleCount metrics from Webex Room devices and computes weighted averages", 5 | "main": "server.js", 6 | "dependencies": { 7 | "debug": "^4.1.1", 8 | "express": "^4.17.1", 9 | "jsxapi": "^5.0.1" 10 | }, 11 | "devDependencies": {}, 12 | "scripts": { 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "author": "Stève Sfartz ", 16 | "license": "MIT" 17 | } 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | 7 | /** 8 | * REST API that exposes RoomAnalytics 9 | */ 10 | 11 | var debug = require("debug")("api"); 12 | var fine = require("debug")("api:fine"); 13 | 14 | var devices; 15 | try { 16 | devices = require("./devices.json"); 17 | } 18 | catch (err) { 19 | console.log("Please specify a list of devices.") 20 | process.exit(-1) 21 | } 22 | 23 | // 24 | // Web API 25 | // 26 | 27 | var express = require("express"); 28 | var app = express(); 29 | 30 | // Enable CORS 31 | app.use(function (req, res, next) { 32 | res.header("Access-Control-Allow-Origin", "*"); 33 | res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept"); 34 | next(); 35 | }); 36 | 37 | // Add JSON parsing 38 | var bodyParser = require("body-parser"); 39 | app.use(bodyParser.urlencoded({ extended: true })); 40 | app.use(bodyParser.json()); 41 | 42 | var started = Date.now(); 43 | app.route("/") 44 | // healthcheck 45 | .get(function (req, res) { 46 | res.json({ 47 | message: "Congrats, the PeopleCount Collector API is up and running", 48 | help: "Try: GET /devices to fetch a list of devices", 49 | since: new Date(started).toISOString() 50 | }); 51 | }) 52 | 53 | const { latest, averageOnPeriod } = require("./collector.js"); 54 | 55 | app.get("/devices", function (req, res) { 56 | const mapped = devices.map((device) => { 57 | return { 58 | id: device.id, 59 | location: device.location, 60 | ipAddress: device.ipAddress 61 | }; 62 | }); 63 | 64 | res.json(mapped); 65 | }) 66 | 67 | app.get("/devices/:device", function (req, res) { 68 | const id = req.params.device; 69 | 70 | let found = devices.find(function (device) { 71 | return (device.id === id) 72 | }) 73 | if (!found) { 74 | res.status(404).json({ 75 | message: "device not found" 76 | }) 77 | return 78 | } 79 | 80 | res.json({ 81 | id: found.id, 82 | location: found.location, 83 | ipAddress: found.ipAddress 84 | }) 85 | }) 86 | 87 | app.get("/devices/:device/last", function (req, res) { 88 | const id = req.params.device; 89 | 90 | // Retreive count data for device 91 | const count = latest(id); 92 | 93 | if (!count) { 94 | res.status(404).json({ 95 | message: `not collecting data for device: ${id}` 96 | }); 97 | return; 98 | } 99 | 100 | res.json({ 101 | device: id, 102 | peopleCount: count 103 | }); 104 | }) 105 | 106 | app.get("/devices/:device/average", function (req, res) { 107 | const id = req.params.device; 108 | 109 | // Get period (in seconds) 110 | let period = req.query.period; 111 | if (!period) { 112 | period = 15; // default to 15s 113 | } 114 | 115 | // Retreive count data for device 116 | try { 117 | const count = averageOnPeriod(id, period); 118 | 119 | res.json({ 120 | device: id, 121 | peopleCount: count, 122 | period: period, 123 | unit: "seconds" 124 | }); 125 | } 126 | catch (err) { 127 | fine(`could not compute average, err: ${err.message}`); 128 | res.status(400).json({ 129 | message: `period starts before we started collecting data for the device` 130 | }); 131 | } 132 | }) 133 | 134 | 135 | // Starts the service 136 | // 137 | var port = process.env.OVERRIDE_PORT || process.env.PORT || 8080; 138 | app.listen(port, function () { 139 | console.log("Collector API started at http://localhost:" + port + "/"); 140 | console.log(" GET / for healthcheck"); 141 | console.log(" GET /devices for the list of devices"); 142 | console.log(" GET /devices/{device} to get the details for the specified device"); 143 | console.log(" GET /devices/{device}/last for latest PeopleCount value received"); 144 | console.log(" GET /devices/{device}/average?period=30 for a computed average"); 145 | }); -------------------------------------------------------------------------------- /tests/schedule-barycentre.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | const collector = require("../collector"); 7 | 8 | // Wait 30s to make sure we had time to record a few values, 9 | // then schedule a computation every 15 seconds 10 | console.log(`waiting 30 seconds`); 11 | setTimeout(function () { 12 | 13 | var device = "Workbench1"; 14 | setInterval(function () { 15 | 16 | console.log(`computing barycentre for device: ${device}`); 17 | 18 | const result = collector.averageOnPeriod(device, 15) 19 | console.log(`computed barycentre: ${result}, for device: ${device}`); 20 | 21 | }, 15 * 1000); // in milliseconds 22 | 23 | }, 30 * 1000); // in milliseconds 24 | 25 | 26 | -------------------------------------------------------------------------------- /util/barycentre.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | /** 7 | * Collects PeopleCount analystics from a collection of RoomKits 8 | */ 9 | 10 | const debug = require("debug")("barycentre"); 11 | const fine = require("debug")("barycentre:fine"); 12 | 13 | 14 | // A series is an ordered collection of ticks and values 15 | // tick are formatted as dates 16 | 17 | // Computes the weighted average on the period (begin/end) 18 | // for the time-series values (as a map of time / values) 19 | // The algorithm considers the values are substains till next serie is registered 20 | module.exports.computeBarycentre = function (series, from, to) { 21 | let computed = 0; 22 | let previousSerie = undefined; 23 | let begun = false; 24 | let skippedMilliseconds = 0; // Store periods where the device was not counting (typically went to standby mode) 25 | 26 | fine(`requested to compute barycentre from: ${from}, to: ${to}, with ${series.length} values in serie`) 27 | 28 | for (var i = 0; i < series.length; i++) { 29 | var serie = series[i]; 30 | if (serie[0] <= from) { 31 | // We've not started yet, let's skip to next serie 32 | previousSerie = serie; 33 | continue; 34 | } 35 | 36 | if (!begun) { 37 | if (!previousSerie) { 38 | // throw an error if from is before the series begins 39 | throw new Error("from is before serie begins") 40 | } 41 | 42 | previousSerie = [ from, previousSerie[1]]; 43 | begun = true; 44 | } 45 | 46 | if (serie[0] >= to) { 47 | // we are done, add serie's value till 'to' 48 | computed += previousSerie[1] * (new Date(to).getTime() - new Date(previousSerie[0]).getTime()); 49 | previousSerie = serie; 50 | break; 51 | } 52 | 53 | // if the value to add is negative, we were not counting on this period, simply skip it 54 | if (previousSerie[1] < 0) { 55 | skippedMilliseconds = new Date(serie[0]).getTime() - new Date(previousSerie[0]).getTime(); 56 | } 57 | else { 58 | // let's add the value for the period 59 | computed += previousSerie[1] * (new Date(serie[0]).getTime() - new Date(previousSerie[0]).getTime()); 60 | } 61 | 62 | previousSerie = serie; 63 | } 64 | 65 | // Compute average for standard cases between to and from bounts 66 | // 2 exceptions though, when from or to are after the last series 67 | // Exception 1: from is after last serie 68 | let barycentre; 69 | if (!begun) { 70 | barycentre = previousSerie[1]; 71 | } 72 | // Exception 2: to is after last serie 73 | else if (to >= previousSerie[0]) { 74 | // add serie's value till 'to' 75 | computed += previousSerie[1] * (new Date(to).getTime() - new Date(previousSerie[0]).getTime()); 76 | } 77 | if (!barycentre) { 78 | barycentre = computed / (new Date(to).getTime() - new Date(from).getTime() - skippedMilliseconds); 79 | } 80 | 81 | fine(`computed barycentre: ${barycentre}`) 82 | return barycentre; 83 | } 84 | -------------------------------------------------------------------------------- /util/max.js: -------------------------------------------------------------------------------- 1 | // 2 | // Copyright (c) 2017 Cisco Systems 3 | // Licensed under the MIT License 4 | // 5 | 6 | const debug = require("debug")("max"); 7 | const fine = require("debug")("max:fine"); 8 | 9 | 10 | // A series is an ordered collection of ticks and values 11 | // tick are formatted as dates 12 | 13 | // Returns the max value on the period (begin/end) 14 | // for the time-series values (as a map of time / values) 15 | module.exports.max = function (series, from, to) { 16 | fine(`requested to compute max from: ${from}, to: ${to}, with ${series.length} values in serie`) 17 | 18 | let max = -1; 19 | for (var i = 0; i < series.length; i++) { 20 | var serie = series[i]; 21 | if (serie[0] < from) { 22 | // We've not started yet, let's skip to next serie 23 | continue; 24 | } 25 | 26 | if (serie[0] > to) { 27 | // we are done, add serie's value till 'to' 28 | break; 29 | } 30 | 31 | // if the value to add is negative, we were not counting on this period, simply skip it 32 | if (serie[1] > max) { 33 | max = serie[1] 34 | } 35 | } 36 | 37 | fine(`found max value: ${max}`) 38 | return max; 39 | } 40 | -------------------------------------------------------------------------------- /util/tests/from-after-last-counter.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | ["2018-02-21T20:24:05.000Z", 4], 3 | ["2018-02-21T20:24:11.000Z", 1], 4 | ["2018-02-21T20:24:12.000Z", 2], 5 | ["2018-02-21T20:24:13.000Z", 3], 6 | ["2018-02-21T20:24:16.000Z", 10] 7 | ]; 8 | 9 | const { max } = require("../max"); 10 | var res = max(data, "2018-02-21T20:24:17.000Z", "2018-02-21T20:24:20.000Z"); 11 | console.log(`computed max: ${res}, expecting: -1`); 12 | 13 | const { computeBarycentre } = require("../barycentre"); 14 | var res = computeBarycentre(data, "2018-02-21T20:24:17.000Z", "2018-02-21T20:24:20.000Z"); 15 | console.log(`computed weighted: ${res}, expecting: 10`); -------------------------------------------------------------------------------- /util/tests/from-and-to-extend-series.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | ["2018-02-21T20:24:05.000Z", 4], 3 | ["2018-02-21T20:24:11.000Z", 1], 4 | ["2018-02-21T20:24:12.000Z", 2], 5 | ["2018-02-21T20:24:13.000Z", 3], 6 | ["2018-02-21T20:24:16.000Z", 10] 7 | ]; 8 | 9 | const { max } = require("../max"); 10 | var res = max(data, "2018-02-21T20:24:10.000Z", "2018-02-21T20:24:15.000Z"); 11 | console.log(`computed max: 3, expecting: 2.6`); 12 | 13 | const { computeBarycentre } = require("../barycentre"); 14 | var res = computeBarycentre(data, "2018-02-21T20:24:10.000Z", "2018-02-21T20:24:15.000Z"); 15 | console.log(`computed weighted: ${res}, expecting: 2.6`); -------------------------------------------------------------------------------- /util/tests/from-and-to-in-series.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | ["2018-02-21T20:24:05.000Z", 4], 3 | ["2018-02-21T20:24:11.000Z", 1], 4 | ["2018-02-21T20:24:12.000Z", 2], 5 | ["2018-02-21T20:24:13.000Z", 3], 6 | ["2018-02-21T20:24:16.000Z", 10] 7 | ]; 8 | 9 | const { max } = require("../max"); 10 | var res = max(data, "2018-02-21T20:24:11.000Z", "2018-02-21T20:24:13.000Z"); 11 | console.log(`computed max: ${res}, expecting: 3`); 12 | 13 | const { computeBarycentre } = require("../barycentre"); 14 | var res = computeBarycentre(data, "2018-02-21T20:24:11.000Z", "2018-02-21T20:24:13.000Z"); 15 | console.log(`computed weighted: ${res}, expecting: 1.5`); -------------------------------------------------------------------------------- /util/tests/from-before-first-counter.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | ["2018-02-21T20:24:05.000Z", 4], 3 | ["2018-02-21T20:24:11.000Z", 1], 4 | ["2018-02-21T20:24:12.000Z", 2], 5 | ["2018-02-21T20:24:13.000Z", 3], 6 | ["2018-02-21T20:24:16.000Z", 10] 7 | ]; 8 | 9 | const { max } = require("../max"); 10 | try { 11 | const res = max(data, "2018-02-21T20:24:04.000Z", "2018-02-21T20:24:20.000Z"); 12 | console.log(`computed max: ${res}, BUT WAS EXPECTING error`); 13 | } 14 | catch (err) { 15 | console.log(`raised err: ${err.message}, as expected`); 16 | } 17 | 18 | 19 | const { computeBarycentre } = require("../barycentre"); 20 | try { 21 | const res = computeBarycentre(data, "2018-02-21T20:24:04.000Z", "2018-02-21T20:24:20.000Z"); 22 | console.log(`computed weighted: ${res}, BUT WAS EXPECTING error`); 23 | } 24 | catch (err) { 25 | console.log(`raised err: ${err.message}, as expected`); 26 | } -------------------------------------------------------------------------------- /util/tests/to-after-last-counter.js: -------------------------------------------------------------------------------- 1 | var data = [ 2 | ["2018-02-21T20:24:05.000Z", 4], 3 | ["2018-02-21T20:24:11.000Z", 1], 4 | ["2018-02-21T20:24:12.000Z", 2], 5 | ["2018-02-21T20:24:13.000Z", 3], 6 | ["2018-02-21T20:24:16.000Z", 10] 7 | ]; 8 | 9 | const { max } = require("../max"); 10 | var res = max(data, "2018-02-21T20:24:14.000Z", "2018-02-21T20:24:20.000Z"); 11 | console.log(`computed max: ${res}, expecting: 10`); 12 | 13 | const { computeBarycentre } = require("../barycentre"); 14 | var res = computeBarycentre(data, "2018-02-21T20:24:14.000Z", "2018-02-21T20:24:20.000Z"); 15 | console.log(`computed weighted: ${res}, expecting: 7.66...67`); --------------------------------------------------------------------------------