diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/BackgroundTemplate.afphoto b/BackgroundTemplate.afphoto new file mode 100644 index 0000000..b8437af Binary files /dev/null and b/BackgroundTemplate.afphoto differ diff --git a/Core.js b/Core.js new file mode 100755 index 0000000..a767883 --- /dev/null +++ b/Core.js @@ -0,0 +1,289 @@ +'use strict' +const UTIL = require('./core/util'); + +// Check Fresh Environment ? +UTIL.checkNewEV(); + +const FS = require('fs'); +const CHALK = require('chalk'); +const SERVER = require('./core/server'); +const ACCESSORY = require('./core/accessory'); +const CONFIG = require(UTIL.ConfigPath); +const IP = require("ip"); +const MQTT = require('./core/mqtt'); +const PATH = require('path'); +const NODECLEANUP = require('node-cleanup'); +const ROUTING = require('./core/routing'); + +// resgister process exit handler +NODECLEANUP(clean); + +// Cleanup our mess +function clean(exitCode, signal) { + + cleanEV().then(() => { + process.kill(process.pid, signal); + }); + + NODECLEANUP.uninstall(); + return false; +} + +function cleanEV() { + + return new Promise((resolve, reject) => { + console.info(' Unpublishing Accessories...'); + Bridge.unpublish(false); + + const AccessoryIDs = Object.keys(Accesories); + for (let i = 0; i < AccessoryIDs.length; i++) { + const Acc = Accesories[AccessoryIDs[i]]; + if (!Acc._Config.bridged) { + Acc.unpublish(false); + } + } + + console.info(' Saving current Characteristics...'); + const CharacteristicCache = {}; + + for (let i = 0; i < AccessoryIDs.length; i++) { + const Acc = Accesories[AccessoryIDs[i]]; + CharacteristicCache[AccessoryIDs[i]] = Acc.getProperties(); + } + + UTIL.saveCharacteristicCache(CharacteristicCache); + + console.info(' Cleaning up Routes...'); + let RouteKeys = Object.keys(Routes); + RouteKeys.forEach((AE) =>{ + Routes[AE].close('appclose'); + }) + + resolve(); + }); +} + +// Check if we are being asked for a Reset. +if (UTIL.checkReset()) { + return; // stop (whilst we check they know what they are doing.) +} + +// Check password reset +if (UTIL.checkPassword()) { + return; // stop +} + +// Set routing module path +ROUTING.setPath(UTIL.RootPath) + +// Check install module +if (UTIL.checkInstallRequest()) { + return; // stop +} + +// Banner +console.log(CHALK.keyword('orange')(" HomeKit")) +console.log(CHALK.keyword('white')(" Device Stack")) +console.log(CHALK.keyword('white')(" For the Smart Home Enthusiast, For the curious.")) +console.log(CHALK.keyword('orange')(" _________________________________________________________________")) +console.log(" ") + +// install modules if needed +ROUTING.installStockModules(); +ROUTING.loadModules(); + +if (!CONFIG.bridgeConfig.hasOwnProperty("pincode")) { + + // Genertae a Bridge + CONFIG.bridgeConfig.pincode = UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999); + CONFIG.bridgeConfig.username = UTIL.genMAC(); + CONFIG.bridgeConfig.setupID = UTIL.makeID(4); + CONFIG.bridgeConfig.serialNumber = UTIL.makeID(12) + UTIL.saveBridgeConfig(CONFIG.bridgeConfig) + + // Create a demo accessory for new configs (accessories will heronin be created via the ui) + const DemoAccessory = { + "type": "SWITCH", + "name": "Switch Accessory Demo", + "route": "Output To Console", + "pincode": UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999), + "username": UTIL.genMAC(), + "setupID": UTIL.makeID(4), + "serialNumber": UTIL.makeID(12), + "bridged": true + } + CONFIG.accessories.push(DemoAccessory) + UTIL.appendAccessoryToConfig(DemoAccessory) +} + +console.log(" Configuring Homekit Bridge") + +// Configure Our Bridge +const Bridge = new ACCESSORY.Bridge(CONFIG.bridgeConfig) +Bridge.on('PAIR_CHANGE', Paired) +Bridge.on('LISTENING', getsetupURI) + +// Routes +const Routes = {} + +function setupRoutes() { + + const Keys = Object.keys(Routes); + + for (let i = 0; i < Keys.length; i++) { + Routes[Keys[i]].close('reconfigure') + delete Routes[Keys[i]]; + } + + const RouteNames = Object.keys(CONFIG.routes); + + for (let i = 0; i < RouteNames.length; i++) { + + let RouteCFG = CONFIG.routes[RouteNames[i]] + console.log(" Configuring Route : " + RouteNames[i] + " (" + RouteCFG.type + ")") + + let RouteClass = new ROUTING.Routes[RouteCFG.type].Class(RouteCFG); + + Routes[RouteNames[i]] = RouteClass; + + } +} + +// This is also called externally (i.e when updating routes via the UI) +setupRoutes(); + +// Load up cache (if available) +var Cache = UTIL.getCharacteristicCache(); + +// Configure Our Accessories +const Accesories = {} +for (let i = 0; i < CONFIG.accessories.length; i++) { + + let AccessoryOBJ = CONFIG.accessories[i] + console.log(" Configuring Accessory : " + AccessoryOBJ.name + " (" + AccessoryOBJ.type + ")") + AccessoryOBJ.accessoryID = AccessoryOBJ.username.replace(/:/g, ""); + let Type = ACCESSORY.Types.filter(C => C.Name == AccessoryOBJ.type)[0] + let Acc = new Type.Class(AccessoryOBJ); + + if (Cache != null) { + if (Cache.hasOwnProperty(AccessoryOBJ.accessoryID)) { + console.log(" Restoring Characteristics...") + Acc.setCharacteristics(Cache[AccessoryOBJ.accessoryID]); + } + } + + Acc.on('STATE_CHANGE', (PL, O) => Change(PL, AccessoryOBJ, O)) + Acc.on('IDENTIFY', (P) => Identify(P, AccessoryOBJ)) + + Accesories[AccessoryOBJ.accessoryID] = Acc; + + if (!AccessoryOBJ.bridged) { + + Acc.on('PAIR_CHANGE', (P) => Pair(P, AccessoryOBJ)) + console.log(" Pin Code " + AccessoryOBJ.pincode) + console.log(" Publishing Accessory (Unbridged)") + Acc.publish(); + } else { + Bridge.addAccessory(Acc.getAccessory()) + } +} + +// Publish Bridge +console.log(" Publishing Bridge") +Bridge.publish(); + +console.log(" Starting Client Services") + +// Web Server (started later) +const UIServer = new SERVER.Server(Accesories, Change, Identify, Bridge, setupRoutes, Pair); + +// MQTT Client (+ Start Server) +const MQTTC = new MQTT.MQTT(Accesories, MQTTDone) + +function MQTTDone() { + UIServer.Start(UIServerDone) +} + +// Server Started +function UIServerDone() { + const BridgeFileName = PATH.join(UTIL.HomeKitPath, "AccessoryInfo." + CONFIG.bridgeConfig.username.replace(/:/g, "") + ".json"); + if (FS.existsSync(BridgeFileName)) { + const IsPaired = Object.keys(require(BridgeFileName).pairedClients) + UIServer.setBridgePaired(IsPaired.length > 0); + } else { + UIServer.setBridgePaired(false); + } + + // All done. + + var IPAddress = IP.address(); + if (CONFIG.webInterfaceAddress != 'ALL') { + IPAddress = CONFIG.webInterfaceAddress + } + + const Address = CHALK.keyword('red')("http://" + IPAddress + ":" + CONFIG.webInterfacePort + "/ui/login") + console.log(" " + CHALK.black.bgWhite("┌─────────────────────────────────────────────────────────────────────────────┐")) + console.log(" " + CHALK.black.bgWhite("| Goto " + Address + " to start managing your installation. |")) + console.log(" " + CHALK.black.bgWhite("| Default username and password is admin |")) + console.log(" " + CHALK.black.bgWhite("└─────────────────────────────────────────────────────────────────────────────┘")) +} + +// Called when bridge is listenting and online +function getsetupURI(port) { + CONFIG.bridgeConfig.QRData = Bridge.getAccessory().setupURI(); +} + +// Bridge Pair Change +function Paired(IsPaired) { + UIServer.setBridgePaired(IsPaired); +} + +// Device Change +function Change(PL, Object, Originator) { + if (Object.hasOwnProperty("route") && Object.route.length > 0) { + const Payload = { + "accessory": Object, + "type": "change", + "change": PL, + "source": Originator + } + + if (Routes.hasOwnProperty(Object.route)) { + const R = Routes[Object.route]; + R.process(Payload); + } + + } +} + +// Device Pair +function Pair(paired, Object) { + if (Object.hasOwnProperty("route") && Object.route.length > 0) { + const Payload = { + "accessory": Object, + "type": "pair", + "isPaired": paired, + } + + if (Routes.hasOwnProperty(Object.route)) { + const R = Routes[Object.route]; + R.process(Payload); + } + } +} + +// Device Identify +function Identify(paired, Object) { + if (Object.hasOwnProperty("route") && Object.route.length > 0) { + const Payload = { + "accessory": Object, + "type": "identify", + "isPaired": paired, + } + + if (Routes.hasOwnProperty(Object.route)) { + const R = Routes[Object.route]; + R.process(Payload); + } + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0c8a977 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Marcus Davies + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8abfce6 --- /dev/null +++ b/README.md @@ -0,0 +1,244 @@ + +![Nodes](./HKDS4.png) +# Homekit Device Stack +A Middleware Server, for bringing HomeKit functionality to your Home Automation. + +Homekit Device Stack is like no other. it's a NodeJS server with a fully web based front end, that allows you to create fully functional, virtual Homekit Smart accessories, then with those accessories, +visually wire the events that occur on them into various other automation platforms using common transport mechanisms. + + - HTTP + - UDP Broadcast + - File + - MQTT + - Websocket + - Custom Route Modules (v4) + + You can for instance, send a command to node-red, openhab & home assistant whenever one of your accessories have been manipulated from your iOS device. + infact, as long as the automation platform supports one of the transports above, it will work with Homekit Device Stack, if not, you can write your own Route module. + + ## Custom Route modules + As of Version 4, routes are now provided in the form of nodejs modules. + Routes are 'plugins', that extend the transport abilities of Homekit Device Stack. + + Whilst Homekit Device Stack has not been written to work with physical devices directly, given enough effort, you could write a route module that does just that. + + Click [here](./RouteModule.md), to learn how to write a module, and install it. + + + The system is extremely intuitive. + + - Create an accessory + - Create a route (the transport method, and endpoint) + - Connect the 2 (drag-connect) + - Done! + + routes can be used by more than 1 accessory, and you can create any number of routes. + +![Nodes](./Screenshot4.png) + +The accessories are pre-confgiured, and you only need to supply the metadata for them, i.e the accessory name, the inputs available (Smart TV), the camera stream source (Smart Camera), and so on. + +currently, 16 different accessory types are supported. - the aim of course is to keep increasing this. + + - CCTV Camera + - Door/Window Contact Sensor + - Electrical Outlet + - Fan + - Garage Door Opener + - Leak Sensor + - Light Sensor + - Lock + - Motion Sensor + - Security System + - Smart Light Bulb + - Smart TV + - Smoke Sensor + - Switch + - Temperature Sensor + - Thermostat + + +The message that is sent using your transport route is below. +```json +{ + "accessory": { + "type": "SWITCH", + "name": "Switch Accessory Demo", + "accessoryID": "CD2947583B71" + }, + "type": "change", + "change": { + "characteristic": "On", + "value": true + }, + "source": "iOS_DEVICE", + "route_type": "FILE", + "route_name": "File Output" +} +``` + +There are 3 possible event types: + +| Event | Description | +| --------------- | --------------------------------------------------------- | +| change | One of the characteristics has changed. | +| identify | The device has been identified (clicking identify in iOS) | +| pair | The pairing state of the non bridged device has changed. | + +The **source** object in the change payload above, identifies where the change occurred **iOS_DEVICE** or **API** +Note : if the event type is **identify** or **pair** then **source** is omitted. + +Events **identify** and **pair** will include **isPaired** - which states whether or not the accessory has currently been enrolled. + +## Is this homebridge? +No, homebridge is targeted towards providing Homekit support for non compatible devices. +The purpose of HomeKit Device Stack, is to provide a homekit virtualisation platform, that allows hot swopping its outgoing communication. + +## So, can I change the status of the accessories, without an iOS device +Yes! +HomeKit Device Stack has 2 network based inputs. + + - A REST based HTTP API + - MQTT Topic Subscription + +Both these inputs allow manipulation of the accessory states. These altered states are then reflected in HomeApp, or any other Homekit based application. + +Changes originating from these inputs may cause routes to trigger, making use of the **source** object can be used to filter these out. + +The URL for the REST API is **http://IP-ADDRESS:7989/{password}/** + +| Method | Address | Description | +| ------ | ----------------------------- | ------------------------------------------------------- | +| GET | /accessories | Lists all accessories and there current characteristics | +| GET | /accessories/accessoryID | Same as above but for the identified accessory | +| PUT | /accessories/accessoryID | Sets characteristics for the identified accessory | + +The body in your HTTP PUT command, should be nothing more than a JSON object representing the characteristics to set + +```json +{ + "On": false, + "OutletInUse": true +} +``` +The same format should be used for MQTT messages. +The topic should end with the accessory ID Ex: **HKDS/IN/HDSH389HJS**. +You can change the leading topic name in the UI. by default its **HKDS/IN/**. + +## Installing +Ensure you have **NodeJS** and **NPM** installed. +Then install Homekit Device Stack + + npm install homekit-device-stack + +## Running +Within the directory that HomeKit Device Stack is installed. + + node App.js + +If creating an auto start script - ensure the script is set to run under the installed directory + +## Command line arguments +**reset** - Completely Resets HomeKit Device Stack (inits a default configuration) +**passwd** {desired password} - set the admin password +**installmodule** {name of module} - install a [custom Route Module](./RouteModule.md) + +## Credits +HomeKit Device Stack is based on the awesome [HAP-NodeJS](https://github.com/homebridge/HAP-NodeJS) +library, without it, projects like this one are not possible. + +## Version History + + - **4.0.0** + **BREAKING CHANGE : V4 is not backwards compatible with V3 configurations - Sorry :(** + + - Added 4 new sensor devices (Smoke, Light, Leak & Temperature) + - Added A Websocket Route + - Optimised Route logic + - Bump HAP-NodeJS to 0.9.1 + - Cleaned hap code to fall in line with hap-nodejs spec. + - Restore option added to setup page, removing the need to re-enroll, if re-installing HKDS. + - Accessory Characteristics are now written to disc and restored, when terminating/starting HKDS. + - Improvements to Read Me. + - Small improvements to UI (new background being one). + - Migrated UI template engine to handlebars + - Added the ability to use either **CIAO** or **BONJOUR-HAP** for the advertiser. + - Added the ability to attach to a specific interface. + - Routes are now provided as modules - allowing enhanced route development + + - **3.0.3** + - Fixed 'Accessory Not Responding' after editing a non bridged device. + - Fixed potential crash where a no longer existing route is to trigger. + - Added pairing events to out going routes for non-bridged devices + - Added pairing pin code to UI for non-bridged devices. + + - **3.0.2** + - House keeping + - Further Camera improvements + + - **3.0.1** + - Added delete option when editing routes + - Fixed crash on attempting to update an unknown Device ID + + - **3.0.0** + - New User Interface (+ Added route configuration to UI) + - Optimisations/Improvements/Bug Fixes to the core code + - Optimisations/Improvements/Bug Fixes to the camera implementation + - **description** property of accessories no longer in use. + - Added Fan Accessory + - Enhanced MQTT Route to allow Accessory ID in the topic name + - Enhanced FILE Route to allow Accessory ID in the folder path + - Enhanced HTTP Route to allow Accessory ID in the URI + - Added ability to publish your accessories attached or detached from a HomeKit 'Bridge' + - Added the ability to backup/restore your configuration. + - Added the (optional) motion sensor and door bell services to camera accessories + (to support rich notifications in iOS 13+) + + + - **2.1.0** + - Bump all dependencies to latest stable releases + - Migrated Camera object to latest **CameraController** API + - Added light bulb device. + + + - **2.0.1** + - Fixed inability to hide TV inputs from view. + - Fixed security issue allowing access without logging in. + - Fixed potential ffmpeg process freeze. + - Fixed disconnected web client exception. + - Added Route type icon to accessory panel + + - **2.0.0** + **BREAKING CHANGES!** + + - Bump all dependencies to latest stable releases + - Relocated HKDS and HomeKit configuration (config now survives updates) + Make a copy of your **config.json** file and any files inside the **HomeKit** dir, then... + **config.json** should be moved to **%Home%/HomeKitDeviceStack** + **HomeKit/*.json** should be moved to **%Home%/HomeKitDeviceStack/HomeKitPersist** + - The **directory** value for File routes should now only contain a name of a folder + that is relative to **%Home%/HomeKitDeviceStack/** + + + - **1.1.3** + - Update Read Me + - **1.1.2** + - Improved layout for acessories UI. + - Outgoing route performance improvements + - Fixed null reference for accessories without a defined route (i.e. camera) + - **1.1.1** + - Removed unused parameter from Server constructor. + - **1.1.0** + - Added ability to manipulate devices via MQTT + - Improved error handling + - Fixed showing loopback address in console. + - Switched to using axios for the HTTP route type + - Switched to internal NodeJS crypto library + - **1.0.1** + - Fixed typo in read me + - **1.0.0** + - Initial Release + + +## To Do + - Continue to add more accessory types \ No newline at end of file diff --git a/RouteModule.md b/RouteModule.md new file mode 100644 index 0000000..d0eb3de --- /dev/null +++ b/RouteModule.md @@ -0,0 +1,132 @@ +# Developing a Route Module +In its basic form, a route module is nothing more than a nodejs module, with an index.js and package.json file. +like any other module in nodejs - your route can require other modules, just add them as a dependency in your package file + +## Lets have a look at the HTTP Post Route +The **package.json** file is needed by all nodejs modules. + +**NOTE:** +Your module name **MUST** begin with **hkds-route-**, if it is not, it will not get loaded. + +```json +{ + "name": "hkds-route-http", + "description": "The stock Homekit Device Stack HTTP route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + }, + "license": "MIT", + "dependencies": { + "axios": "0.21.1" + } +} +``` + +And the all important **index.js** file + +```javascript +'use strict' +const axios = require('axios') + +/* UI Params */ +const Params = [ + { + id: "destinationURI", + label: "HTTP URI" + } +] + +/* Metadata */ +const Name = "HTTP Post Output"; +const Icon = "icon.png"; + +/* Route Class */ +class HTTPRoute { + + /* Constructor */ + constructor(route) { + this.Route = route + } +} + +HTTPRoute.prototype.process = async function (payload) { + + let CFG = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Homekit Device Stack' + }, + method: 'post', + url: this.Route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID), + data: payload + } + + try{ + let Res = await axios.request(CFG); + } + catch(err){ + console.log(" HTTP Route Error: "+err) + } +} + +HTTPRoute.prototype.close = function (reason) { +} + +module.exports = { + "Route": HTTPRoute, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} +``` + +Your module file (it doesn't have to be called **index.js**), must export 4 objects. + +| Property | What it's for | +|----------|-------------------------------------------------------------------------------| +|Route | A pointer to your modules main class. | +|Inputs | An array of input objects | +|Name | The name as displayed in the UI | +|Icon | An icon file, relative to the root of your module. | + +Your class (Exported as **Route**) must have a constructor that accepts an object representing the route settings, as confgiured in the UI. +The class must expose 2 prototype methods: **process** and **close** + +| Method | What it's for | +|-------------------------|------------------------------------------------------------------------------------------------| +|async process(payload) | This is called when an accessory is sending an event, **payload** will contain the event data | +|close(reason) | This is called when the route is being destroyed (iether **reconfgiure** or **appclose**) | + +The **Inputs** object must be an array of input objects, it allows settings to be passed to the route during its constructor. + +```json +[ + { + "id": "some_internal_identifyer", + "label": "A Nice Title For The UI" + } +] +``` + +## Installing your route module. + +**Manual Install** + - Bundle everything up in a folder with a name that matches your module name, and copy this folder to **/%Home%/HKDS/node_modules** + - Go into your folder, now in **/%Home%/HKDS/node_modules**, and run ```npm install``` to install any dependencies your route module may need. + - Restart HomeKit Device Stack. + +**Using NPM** +If your route module has been published to NPM, you can install it with the ```installmodule``` command +this will also allow you to install 3rd party route modules. A Restart of HomeKit Device Stack will be required in any case. + +```node app.js installmodule {name_of_module}``` + +Or use NPM directly (you will need to specify a --prefix that points to the root config directory of HKDS) + +```npm install {name_of_module} --prefix "/%Home%/HKDS"``` + + + + diff --git a/Routes/hkds-route-console/icon.png b/Routes/hkds-route-console/icon.png new file mode 100644 index 0000000..593d6f3 Binary files /dev/null and b/Routes/hkds-route-console/icon.png differ diff --git a/Routes/hkds-route-console/index.js b/Routes/hkds-route-console/index.js new file mode 100644 index 0000000..558c05d --- /dev/null +++ b/Routes/hkds-route-console/index.js @@ -0,0 +1,51 @@ +'use strict' + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ +] + +/* Metadata */ +const Name = "Console Output"; +const Icon = "icon.png"; + +/* Route Class */ +class ConsoleClass { + + /* Constructor */ + constructor(route) { + } +} + +ConsoleClass.prototype.process = async function (payload) { + payload = CleanPayload(payload, "CONSOLE") + console.log(payload) +} + +ConsoleClass.prototype.close = function (reason) { +} + +module.exports = { + "Route": ConsoleClass, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-console/package.json b/Routes/hkds-route-console/package.json new file mode 100644 index 0000000..bc319c3 --- /dev/null +++ b/Routes/hkds-route-console/package.json @@ -0,0 +1,11 @@ +{ + "name": "hkds-route-console", + "description": "The stock Homekit Device Stack CONSOLE route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT" +} \ No newline at end of file diff --git a/Routes/hkds-route-file/icon.png b/Routes/hkds-route-file/icon.png new file mode 100644 index 0000000..8620d00 Binary files /dev/null and b/Routes/hkds-route-file/icon.png differ diff --git a/Routes/hkds-route-file/index.js b/Routes/hkds-route-file/index.js new file mode 100644 index 0000000..ffb25dc --- /dev/null +++ b/Routes/hkds-route-file/index.js @@ -0,0 +1,84 @@ +'use strict' +const path = require("path"); +const fs = require("fs"); + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ + { + id: "directory", + label: "Storage Location/Directoy" + } +] + +/* Metadata */ +const Name = "File Output"; +const Icon = "icon.png"; + +/* Route Class */ +class File { + + /* Constructor */ + constructor(route) { + this.Route = route; + } + +} + +File.prototype.process = async function (payload) { + + payload = CleanPayload(payload, "FILE") + let JSONs = JSON.stringify(payload); + + let Directory = this.Route.directory.replace("{{accessoryID}}", payload.accessory.accessoryID) + + if (!fs.existsSync(Directory)) { + try { + fs.mkdirSync(Directory, { recursive: true }); + } + catch (err) { + console.log(" FILE Route error: "+err) + return; + } + } + + let DT = new Date().getTime(); + let FileName = DT + '_' + payload.accessory.accessoryID + ".json" + + let _Path = path.join(Directory, FileName); + + try { + fs.writeFileSync(_Path, JSONs, 'utf8') + } + catch (err) { + console.log(" FILE Route error: "+err) + } +} + +File.prototype.close = function (reason) { +} + +module.exports = { + "Route": File, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-file/package.json b/Routes/hkds-route-file/package.json new file mode 100644 index 0000000..8a9b4bd --- /dev/null +++ b/Routes/hkds-route-file/package.json @@ -0,0 +1,11 @@ +{ + "name": "hkds-route-file", + "description": "The stock Homekit Device Stack FILE route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT" +} diff --git a/Routes/hkds-route-http/icon.png b/Routes/hkds-route-http/icon.png new file mode 100644 index 0000000..97f08f1 Binary files /dev/null and b/Routes/hkds-route-http/icon.png differ diff --git a/Routes/hkds-route-http/index.js b/Routes/hkds-route-http/index.js new file mode 100644 index 0000000..0f34a8b --- /dev/null +++ b/Routes/hkds-route-http/index.js @@ -0,0 +1,75 @@ +'use strict' +const axios = require('axios') + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ + { + id: "destinationURI", + label: "HTTP URI" + } +] + +/* Metadata */ +const Name = "HTTP POST Output"; +const Icon = "icon.png"; + +/* Route Class */ +class HTTPRoute { + + /* Constructor */ + constructor(route) { + this.Route = route + } +} + +HTTPRoute.prototype.process = async function (payload) { + + payload = CleanPayload(payload, "HTTP") + + let CFG = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Homekit Device Stack' + }, + method: 'post', + url: this.Route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID), + data: payload + } + + try{ + let Res = await axios.request(CFG) + } + catch(err){ + console.log(" HTTP Route error: "+err) + } + +} + +HTTPRoute.prototype.close = function (reason) { +} + +module.exports = { + "Route": HTTPRoute, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-http/package.json b/Routes/hkds-route-http/package.json new file mode 100644 index 0000000..4ac2c23 --- /dev/null +++ b/Routes/hkds-route-http/package.json @@ -0,0 +1,14 @@ +{ + "name": "hkds-route-http", + "description": "The stock Homekit Device Stack HTTP route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT", + "dependencies": { + "axios": "0.21.1" + } +} diff --git a/Routes/hkds-route-mqtt/icon.png b/Routes/hkds-route-mqtt/icon.png new file mode 100644 index 0000000..0269f28 Binary files /dev/null and b/Routes/hkds-route-mqtt/icon.png differ diff --git a/Routes/hkds-route-mqtt/index.js b/Routes/hkds-route-mqtt/index.js new file mode 100644 index 0000000..6dcdeab --- /dev/null +++ b/Routes/hkds-route-mqtt/index.js @@ -0,0 +1,92 @@ +'use strict' +const mqtt = require('mqtt') + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ + { + id: "mqttbroker", + label: "MQTT Broker" + }, + { + id: "mqttusername", + label: "Username" + }, + { + id: "mqttpassword", + label: "Password" + }, + { + id: "mqtttopic", + label: "Topic" + } +] + +/* Metadata */ +const Name = "MQTT Message"; +const Icon = "icon.png"; + +/* Route Class */ +class MQTTRoute { + + /* Constructor */ + constructor(route) { + + this.Route = route; + + let Options = { + username: route.mqttusername, + password: route.mqttpassword + } + + this.MQTTBroker = mqtt.connect(route.mqttbroker, Options) + this.MQTTBroker.on('connect', () => this.mqttConnected()) + this.MQTTBroker.on('error', (e) => this.mqttError(e)) + } +} + +MQTTRoute.prototype.process = async function (payload) { + + payload = CleanPayload(payload, "MQTT") + let JSONs = JSON.stringify(payload); + + let T = this.Route.mqtttopic.replace("{{accessoryID}}", payload.accessory.accessoryID) + this.MQTTBroker.publish(T, JSONs, null, () => { }); +} + +MQTTRoute.prototype.close = function (reason) { + this.MQTTBroker.end(); +} + +MQTTRoute.prototype.mqttConnected = function () { + console.log(" MQTT Route ready."); +} + +MQTTRoute.prototype.mqttError = function (err) { + console.log(" MQTT Route error: " + err); +} + +module.exports = { + "Route": MQTTRoute, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-mqtt/package.json b/Routes/hkds-route-mqtt/package.json new file mode 100644 index 0000000..1867d47 --- /dev/null +++ b/Routes/hkds-route-mqtt/package.json @@ -0,0 +1,14 @@ +{ + "name": "hkds-route-mqtt", + "description": "The stock Homekit Device Stack MQTT route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT", + "dependencies": { + "mqtt": "4.2.6" + } +} diff --git a/Routes/hkds-route-udp/icon.png b/Routes/hkds-route-udp/icon.png new file mode 100644 index 0000000..58107b3 Binary files /dev/null and b/Routes/hkds-route-udp/icon.png differ diff --git a/Routes/hkds-route-udp/index.js b/Routes/hkds-route-udp/index.js new file mode 100644 index 0000000..14d9158 --- /dev/null +++ b/Routes/hkds-route-udp/index.js @@ -0,0 +1,79 @@ +'use strict' +const dgram = require("dgram"); + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ + { + id: "address", + label: "Broadcast Address" + }, + { + id: "port", + label: "Broadcast Port" + } +] + +/* Metadata */ +const Name = "UDP Broadcast"; +const Icon = "icon.png"; + +/* Route Class */ +class UDP { + + /* Constructor */ + constructor(route) { + + this.Route = route; + + this.UDPServer = dgram.createSocket("udp4"); + this.UDPServer.bind(() => this.UDPConnected()) + } +} + +UDP.prototype.process = async function (payload) { + + payload = CleanPayload(payload, "UDP") + let JSONs = JSON.stringify(payload); + this.UDPServer.send(JSONs, 0, JSONs.length, this.Route.port, this.Route.address, this.UDPDone); +} + +UDP.prototype.close = function (reason) { + this.UDPServer.close(); +} + +UDP.prototype.UDPConnected = function () { + this.UDPServer.setBroadcast(true); + console.log(" UDP Route ready."); +} + +UDP.prototype.UDPDone = function (e, n) { + if (e) { + console.log(" UDP Route error: " + e); + } +} + +module.exports = { + "Route": UDP, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-udp/package.json b/Routes/hkds-route-udp/package.json new file mode 100644 index 0000000..cd8ea4d --- /dev/null +++ b/Routes/hkds-route-udp/package.json @@ -0,0 +1,11 @@ +{ + "name": "hkds-route-udp", + "description": "The stock Homekit Device Stack UDP route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT" +} diff --git a/Routes/hkds-route-websocket/icon.png b/Routes/hkds-route-websocket/icon.png new file mode 100644 index 0000000..cb64ce5 Binary files /dev/null and b/Routes/hkds-route-websocket/icon.png differ diff --git a/Routes/hkds-route-websocket/index.js b/Routes/hkds-route-websocket/index.js new file mode 100644 index 0000000..18b48f1 --- /dev/null +++ b/Routes/hkds-route-websocket/index.js @@ -0,0 +1,78 @@ +'use strict' +const WS = require("ws"); + +/* Clean Payload */ +const CleanPayload = function (Payload, Type) { + + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* UI Params */ +const Params = [ + { + id: "uri", + label: "Websocket Address" + } +] + +/* Metadata */ +const Name = "Websocket"; +const Icon = "icon.png"; + + +/* Route Class */ +class WebsocketClass { + + /* Constructor */ + constructor(route) { + + this.Websocket = new WS(route.uri); + + this.Websocket.on('open', () => this.HandleWSOpen()); + this.Websocket.on('error', (e) => this.WSError(e)) + + } +} + + +WebsocketClass.prototype.process = async function (payload) { + + payload = CleanPayload(payload, "WEBSOCKET") + let JSONs = JSON.stringify(payload); + + this.Websocket.send(JSONs); + +} + +WebsocketClass.prototype.close = function () { + this.Websocket.close(); +} + + +WebsocketClass.prototype.HandleWSOpen = function () { + console.log(" WEBSOCKET Route ready."); +} + +WebsocketClass.prototype.WSError = function (err) { + console.log(" WEBSOCKET Route error: " + err); +} + +module.exports = { + "Route": WebsocketClass, + "Inputs": Params, + "Name": Name, + "Icon": Icon +} \ No newline at end of file diff --git a/Routes/hkds-route-websocket/package.json b/Routes/hkds-route-websocket/package.json new file mode 100644 index 0000000..a70abcb --- /dev/null +++ b/Routes/hkds-route-websocket/package.json @@ -0,0 +1,14 @@ +{ + "name": "hkds-route-websocket", + "description": "The stock Homekit Device Stack WEB SOCKET route", + "version": "1.0.0", + "main": "index.js", + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT", + "dependencies": { + "ws":"7.4.3" + } +} diff --git a/core/accessory.js b/core/accessory.js new file mode 100755 index 0000000..4803c2d --- /dev/null +++ b/core/accessory.js @@ -0,0 +1,1287 @@ +'use strict' +const HapNodeJS = require("hap-nodejs"); +const Service = HapNodeJS.Service; +const Accessory = HapNodeJS.Accessory; +const Characteristic = HapNodeJS.Characteristic; +const uuid = HapNodeJS.uuid; +const CharacteristicEventTypes = HapNodeJS.CharacteristicEventTypes; +const AccessoryEventTypes = HapNodeJS.AccessoryEventTypes; +const EventEmitter = require("events"); +const PKG = require('../package.json'); +const CameraSource = require('./cameraSource'); +const Util = require('./util'); +const CameraController = HapNodeJS.CameraController; +const Catagories = HapNodeJS.Categories; +const BridgeCLS = HapNodeJS.Bridge; +const config = require(Util.ConfigPath); + +let Initialised = false; + +/** + * Common Accessory Class + * A prototype class from which all accessories are based. + * It contains the event emitter, the creation of the accessory its self, and attaching some needed events. + */ +class AccessoryCLS extends EventEmitter { + constructor(AccessoryOBJ, Category) { + super(); + + this._Config = AccessoryOBJ; + this._Properties = {}; + this._isBridge = (Category == Catagories.BRIDGE); + + this._Properties = {}; + + if (!Initialised) { + HapNodeJS.HAPStorage.setCustomStoragePath(Util.HomeKitPath); + Initialised = true; + } + + const UUID = uuid.generate('hap-nodejs:accessories:' + AccessoryOBJ.name + ':' + AccessoryOBJ.username); + + if (this._isBridge) { + this._accessory = new BridgeCLS(AccessoryOBJ.name, UUID); + } else { + this._accessory = new Accessory(AccessoryOBJ.name, UUID); + } + + this._accessory.getService(Service.AccessoryInformation) + .setCharacteristic(Characteristic.SerialNumber, AccessoryOBJ.serialNumber) + .setCharacteristic(Characteristic.Manufacturer, "Marcus Davies") + .setCharacteristic(Characteristic.FirmwareRevision, PKG.version) + .setCharacteristic(Characteristic.Name, AccessoryOBJ.name) + + this._accessory.username = AccessoryOBJ.username; + this._accessory.pincode = AccessoryOBJ.pincode; + this._accessory.category = Category; + this._accessory.setupID = AccessoryOBJ.setupID; + + this._accessory.on(AccessoryEventTypes.IDENTIFY, (paired, callback) => { + callback(); + this.emit("IDENTIFY", paired) + }); + + this._accessory.on(AccessoryEventTypes.LISTENING, (port) => { + this.emit("LISTENING", port) + }); + + this._accessory.on(AccessoryEventTypes.PAIRED, () => { + this.emit("PAIR_CHANGE", true); + }); + + this._accessory.on(AccessoryEventTypes.UNPAIRED, () => { + this.emit("PAIR_CHANGE", false); + }); + } +} + +/** + * Helper method to attach get and set routines + */ +AccessoryCLS.prototype._wireUpEvents = function(targetService, EventStruct) { + const GetHooks = EventStruct.Get; + const SetHooks = EventStruct.Set; + + for (let i = 0; i < GetHooks.length; i++) { + targetService.getCharacteristic(Characteristic[GetHooks[i]]) + .on(CharacteristicEventTypes.GET, (cb) => this._get(GetHooks[i], cb)) + } + + for (let i = 0; i < SetHooks.length; i++) { + targetService.getCharacteristic(Characteristic[SetHooks[i]]) + .on(CharacteristicEventTypes.SET, (value, callback, ctx, connection) => this._set(SetHooks[i], value, callback, connection)) + } +} + +/** + * Internal set + */ +AccessoryCLS.prototype._set = function(property, value, callback, connection) { + this._Properties[property] = value; + callback(null); + + const PL = { + "characteristic": property, + "value": value, + } + + this.emit("STATE_CHANGE", PL, connection == null ? "API" : "iOS_DEVICE"); + +} +/** + * Internal get + */ +AccessoryCLS.prototype._get = function(property, callback) { + if (this._Properties[property] != null) { + callback(null, this._Properties[property]); + } else { + callback(null, null); + } +} +/** + * Get Accessory + */ +AccessoryCLS.prototype.getAccessory = function() { + return this._accessory; +} + +/** + * Get Type + */ +AccessoryCLS.prototype.getAccessoryType = function() { + return this._Config.type; +} +/** + * Publish + */ +AccessoryCLS.prototype.publish = function() { + + let CFG = { + username: this._accessory.username, + pincode: this._accessory.pincode, + category: this._accessory.category, + setupID: this._accessory.setupID, + advertiser: config.advertiser + } + + if (config.interface != 'ALL') { + CFG.bind = config.interface + } + + this._accessory.publish(CFG) +} +/** + * unpublish + */ +AccessoryCLS.prototype.unpublish = function(destroy) { + if (destroy) { + this._accessory.destroy(); + } else { + this._accessory.unpublish() + } +} +/** + * get all properties + */ +AccessoryCLS.prototype.getProperties = function() { + return this._Properties; + +} +/** + * add accessory (for bridge) + */ +AccessoryCLS.prototype.addAccessory = function(Accessory) { + if (this._isBridge) { + this._accessory.addBridgedAccessory(Accessory); + + } +} +/** + * remove accessory (for bridge) + */ +AccessoryCLS.prototype.removeAccessory = function(Accessory) { + if (this._isBridge) { + this._accessory.removeBridgedAccessory(Accessory, false) + + } +} +/** + * get accessories (for bridge) + */ +AccessoryCLS.prototype.getAccessories = function() { + if (this._isBridge) { + return this._accessory.bridgedAccessories; + + } +} +/** + * helper method to create a battery service + */ +AccessoryCLS.prototype._createBatteryService = function() { + this._batteryService = new Service.BatteryService('', ''); + this._batteryService.setCharacteristic(Characteristic.BatteryLevel, 100); + this._batteryService.setCharacteristic(Characteristic.StatusLowBattery, 0); + this._batteryService.setCharacteristic(Characteristic.ChargingState, 0); + this._Properties["BatteryLevel"] = 100; + this._Properties["StatusLowBattery"] = 0; + this._Properties["ChargingState"] = 0; + + const EventStruct = { + "Get": ["BatteryLevel", "StatusLowBattery", "ChargingState"], + "Set": [] + } + + this._wireUpEvents(this._batteryService, EventStruct) + this._accessory.addService(this._batteryService); +} + +/** + * Main Bridge + */ +class Bridge extends AccessoryCLS { + constructor(Config) { + Config.name = "HomeKit Device Stack" + super(Config, Catagories.BRIDGE); + this._accessory.getService(Service.AccessoryInformation) + .setCharacteristic(Characteristic.Model, "HKDS4") + + } +} + +/** + * Public Basic Set + */ +const _basicSet = function(payload) { + const Props = Object.keys(payload); + + for (let i = 0; i < Props.length; i++) { + this._Properties[Props[i]] = payload[Props[i]]; + this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + + } +} +/** + * Public Set with a possible battey service + */ +const _setWithBattery = function(payload) { + const Props = Object.keys(payload); + const BatteryTargets = ["BatteryLevel", "StatusLowBattery", "ChargingState"] + + for (let i = 0; i < Props.length; i++) { + this._Properties[Props[i]] = payload[Props[i]]; + + if (BatteryTargets.includes(Props[i])) { + this._batteryService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + } else { + this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + } + + } +} + +/** + * Outlet Accessory + */ +class Outlet extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.OUTLET); + + this._service = new Service.Outlet(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.On, false); + this._service.setCharacteristic(Characteristic.OutletInUse, false); + this._Properties["On"] = false; + this._Properties["OutletInUse"] = false; + + const EventStruct = { + "Get": ["On", "OutletInUse"], + "Set": ["On"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + } +} +Outlet.prototype.setCharacteristics = _basicSet; + +/** + * Fan Accessory + */ +class Fan extends AccessoryCLS { + constructor(Config) { + super(Config, Catagories.FAN); + + this._service = new Service.Fan(Config.name, Config.name) + + this._service.setCharacteristic(Characteristic.On, false); + this._Properties["On"] = false; + this._service.setCharacteristic(Characteristic.RotationSpeed, 100); + this._Properties["RotationSpeed"] = 100; + + const EventStruct = { + "Get": ["On", "RotationSpeed"], + "Set": ["On", "RotationSpeed"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + } +} +Fan.prototype.setCharacteristics = _basicSet; + +/** + * Switch Accessory + */ +class Switch extends AccessoryCLS { + constructor(Config) { + super(Config, Catagories.SWITCH); + + this._service = new Service.Switch(Config.name, Config.name) + + this._service.setCharacteristic(Characteristic.On, false); + this._Properties["On"] = false; + + const EventStruct = { + "Get": ["On"], + "Set": ["On"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + } +} +Switch.prototype.setCharacteristics = _basicSet; + +/** + * Alarm Accessory + */ +class Alarm extends AccessoryCLS { + constructor(Config) { + super(Config, Catagories.SECURITY_SYSTEM); + + this._service = new Service.SecuritySystem(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._service.setCharacteristic(Characteristic.SecuritySystemCurrentState, 3); + this._service.setCharacteristic(Characteristic.SecuritySystemTargetState, 3); + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + this._Properties["SecuritySystemCurrentState"] = 3; + this._Properties["SecuritySystemTargetState"] = 3; + + const EventStruct = { + "Get": ["SecuritySystemTargetState", "StatusFault", "StatusTampered", "SecuritySystemCurrentState"], + "Set": ["SecuritySystemTargetState"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + } +} +Alarm.prototype.setCharacteristics = _basicSet; + +/** + * TV Accessory Speaker Set support + */ +const _TVSet = function(payload) { + const Props = Object.keys(payload); + + for (let i = 0; i < Props.length; i++) { + this._Properties[Props[i]] = payload[Props[i]]; + + this._service.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + if (Props[i] == "Active") { + // speaker and tv are one + this._Speaker.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + } + + } +} +/** + * TV Accessory + */ +class TV extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.TELEVISION); + + this._Inputs = []; + + this._service = new Service.Television(Config.name, Config.Name); + this._service.setCharacteristic(Characteristic.ConfiguredName, Config.name); + this._service.setCharacteristic(Characteristic.SleepDiscoveryMode, Characteristic.SleepDiscoveryMode.ALWAYS_DISCOVERABLE); + this._service.setCharacteristic(Characteristic.ActiveIdentifier, 1); + this._service.setCharacteristic(Characteristic.Active, 0); + this._Properties["Active"] = 0; + this._Properties["ActiveIdentifier"] = 1; + + var EventStruct = { + "Get": ["Active", "ActiveIdentifier"], + "Set": ["Active", "RemoteKey", "ActiveIdentifier", "PowerModeSelection"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + // Speaker + this._Speaker = new Service.TelevisionSpeaker('', '') + this._Speaker.setCharacteristic(Characteristic.Active, 0) + this._Speaker.setCharacteristic(Characteristic.VolumeControlType, Characteristic.VolumeControlType.ABSOLUTE); + + EventStruct = { + "Get": ["Active", "VolumeSelector"], + "Set": ["VolumeSelector"] + } + + this._wireUpEvents(this._Speaker, EventStruct); + this._accessory.addService(this._Speaker); + + // Inputs + for (let i = 0; i < Config.inputs.length; i++) { + if (Config.inputs[i].length < 1) { + continue; + } + const Input = new Service.InputSource(Config.inputs[i], Config.inputs[i]) + Input.setCharacteristic(Characteristic.Identifier, (i + 1)) + Input.setCharacteristic(Characteristic.ConfiguredName, Config.inputs[i]) + Input.setCharacteristic(Characteristic.IsConfigured, Characteristic.IsConfigured.CONFIGURED) + Input.setCharacteristic(Characteristic.InputSourceType, Characteristic.InputSourceType.HDMI) + Input.setCharacteristic(Characteristic.CurrentVisibilityState, 0); + Input.setCharacteristic(Characteristic.TargetVisibilityState, 0); + + Input.getCharacteristic(Characteristic.TargetVisibilityState) + .on(CharacteristicEventTypes.SET, function(value, callback, hap) { + Input.setCharacteristic(Characteristic.CurrentVisibilityState, value); + callback(null); + }) + + this._accessory.addService(Input); + this._service.addLinkedService(Input); + + this._Inputs.push(this.Input); + } + + } +} +TV.prototype.setCharacteristics = _TVSet; + +/** + * CCTV Specific Sets + */ +const _CCTVSet = function(payload) { + const Props = Object.keys(payload); + + const DoorBellTargets = ["ProgrammableSwitchEvent"] + const MotionTargets = ["MotionDetected", "StatusActive", "StatusFault", "StatusTampered"] + + for (let i = 0; i < Props.length; i++) { + this._Properties[Props[i]] = payload[Props[i]]; + + if (DoorBellTargets.includes(Props[i])) { + this._VDBService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + } else if (MotionTargets.includes(Props[i])) { + this._MDService.setCharacteristic(Characteristic[Props[i]], payload[Props[i]]) + } + + } +} +/** + * CCTV Camera + */ +class Camera extends AccessoryCLS { + + constructor(Config) { + // Door Bell? + if (Config.enableDoorbellService == 'true') { + super(Config, Catagories.VIDEO_DOORBELL); + + this._VDBService = new Service.Doorbell('', ''); + this._VDBService.setCharacteristic(Characteristic.ProgrammableSwitchEvent, null); + this._Properties["ProgrammableSwitchEvent"] = null; + + const _VDBService_ES = { + "Get": ["ProgrammableSwitchEvent"], + "Set": [] + } + + this._wireUpEvents(this._VDBService, _VDBService_ES); + this._accessory.addService(this._VDBService); + } else { + super(Config, Catagories.IP_CAMERA); + } + + // Camera + const Options = { + supportedCryptoSuites: [HapNodeJS.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80], + video: { + codec: { + profiles: [HapNodeJS.H264Profile.BASELINE, HapNodeJS.H264Profile.MAIN, HapNodeJS.H264Profile.HIGH], + levels: [HapNodeJS.H264Level.LEVEL3_1, HapNodeJS.H264Level.LEVEL3_2, HapNodeJS.H264Level.LEVEL4_0], + } + } + } + + const videoResolutions = [] + + this.maxFPS = Config.maxFPS > 30 ? 30 : Config.maxFPS + this.maxWidth = Config.maxWidthHeight.split("x")[0]; + this.maxHeight = Config.maxWidthHeight.split("x")[1]; + + if (this.maxWidth >= 320) { + if (this.maxHeight >= 240) { + videoResolutions.push([320, 240, this.maxFPS]) + if (this.maxFPS > 15) { + videoResolutions.push([320, 240, 15]) + } + } + if (this.maxHeight >= 180) { + videoResolutions.push([320, 180, this.maxFPS]) + if (this.maxFPS > 15) { + videoResolutions.push([320, 180, 15]) + } + } + } + if (this.maxWidth >= 480) { + if (this.maxHeight >= 360) { + videoResolutions.push([480, 360, this.maxFPS]) + } + if (this.maxHeight >= 270) { + videoResolutions.push([480, 270, this.maxFPS]) + } + } + if (this.maxWidth >= 640) { + if (this.maxHeight >= 480) { + videoResolutions.push([640, 480, this.maxFPS]) + } + if (this.maxHeight >= 360) { + videoResolutions.push([640, 360, this.maxFPS]) + } + } + if (this.maxWidth >= 1280) { + if (this.maxHeight >= 960) { + videoResolutions.push([1280, 960, this.maxFPS]) + } + if (this.maxHeight >= 720) { + videoResolutions.push([1280, 720, this.maxFPS]) + } + } + if (this.maxWidth >= 1920) { + if (this.maxHeight >= 1080) { + videoResolutions.push([1920, 1080, this.maxFPS]) + } + } + + Options.video.resolutions = videoResolutions; + + if (Config.enableAudio == 'true') { + Options.audio = { + codecs: [{ + type: HapNodeJS.AudioStreamingCodecType.AAC_ELD, + samplerate: HapNodeJS.AudioStreamingSamplerate.KHZ_16, + audioChannels: 1, + bitrate: HapNodeJS.AudioBitrate.VARIABLE + }] + } + } + + this.CameraDelegate = new CameraSource.Camera(Config) + this.Controller = new CameraController({ + cameraStreamCount: Config.maxStreams, + delegate: this.CameraDelegate, + streamingOptions: Options + }); + + this.CameraDelegate.attachController(this.Controller); + this._accessory.configureController(this.Controller); + + // Motion? + if (Config.enableMotionDetectionService == 'true') { + this._MDService = new Service.MotionSensor('', ''); + this._MDService.setCharacteristic(Characteristic.MotionDetected, false); + this._MDService.setCharacteristic(Characteristic.StatusActive, 1); + this._MDService.setCharacteristic(Characteristic.StatusFault, 0); + this._MDService.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["MotionDetected"] = false; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const _MDService_ES = { + "Get": ["MotionDetected", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._MDService, _MDService_ES); + this._accessory.addService(this._MDService); + } + + } +} +Camera.prototype.setCharacteristics = _CCTVSet; + +/** + * Contact Accessory + */ +class Contact extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.ContactSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.ContactSensorState, 0); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._Properties["ContactSensorState"] = 0; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + this._Properties["StatusActive"] = 1; + + const EventStruct = { + "Get": ["ContactSensorState", "StatusFault", "StatusTampered", "StatusActive"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +Contact.prototype.setCharacteristics = _setWithBattery; + +/** + * Motion Sensor Accessory + */ +class Motion extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.MotionSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.MotionDetected, false); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["MotionDetected"] = false; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const EventStruct = { + "Get": ["MotionDetected", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +Motion.prototype.setCharacteristics = _setWithBattery; + +/** + * Lock Accessory + */ +class Lock extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.DOOR_LOCK); + + this._service = new Service.LockMechanism(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.LockTargetState, 0); + this._service.setCharacteristic(Characteristic.LockCurrentState, 0); + this._Properties["LockTargetState"] = 0; + this._Properties["LockCurrentState"] = 0; + + const EventStruct = { + "Get": ["LockTargetState", "LockCurrentState"], + "Set": ["LockTargetState"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + } +} +Lock.prototype.setCharacteristics = _basicSet; + +/** + * Light Accessory + */ +class LightBulb extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.LIGHTBULB); + + this._service = new Service.Lightbulb(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.On, false); + this._Properties["On"] = false; + + const EventStruct = { + "Get": ["On"], + "Set": ["On"] + } + + if (Config.supportsBrightness == 'true') { + this._service.setCharacteristic(Characteristic.Brightness, 100); + this._Properties["Brightness"] = 100; + EventStruct.Get.push("Brightness") + EventStruct.Set.push("Brightness") + } + + switch (Config.colorMode) { + case "hue": + this._service.setCharacteristic(Characteristic.Hue, 0); + this._Properties["Hue"] = 0; + EventStruct.Get.push("Hue") + EventStruct.Set.push("Hue") + + this._service.setCharacteristic(Characteristic.Saturation, 0); + this._Properties["Saturation"] = 0; + EventStruct.Get.push("Saturation") + EventStruct.Set.push("Saturation") + + break; + + case "temperature": + this._service.setCharacteristic(Characteristic.ColorTemperature, 50); + this._Properties["ColorTemperature"] = 50; + EventStruct.Get.push("ColorTemperature") + EventStruct.Set.push("ColorTemperature") + + break; + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + } +} +LightBulb.prototype.setCharacteristics = _basicSet; + +/** + * Garage Door Accessory + */ +class GarageDoor extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.GARAGE_DOOR_OPENER); + + this._service = new Service.GarageDoorOpener(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.CurrentDoorState, 0); + this._service.setCharacteristic(Characteristic.TargetDoorState, 0); + this._service.setCharacteristic(Characteristic.LockCurrentState, 0); + this._service.setCharacteristic(Characteristic.LockTargetState, 0); + this._service.setCharacteristic(Characteristic.ObstructionDetected, false); + this._Properties["CurrentDoorState"] = 0; + this._Properties["TargetDoorState"] = 0; + this._Properties["LockCurrentState"] = 0; + this._Properties["LockTargetState"] = 0; + this._Properties["ObstructionDetected"] = false; + + const EventStruct = { + "Get": ["CurrentDoorState", "TargetDoorState", "LockCurrentState", "LockTargetState", "ObstructionDetected"], + "Set": ["TargetDoorState", "LockTargetState"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + } +} +GarageDoor.prototype.setCharacteristics = _basicSet; + +/** + * Thermotsat Accessory + */ +class Thermostat extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.THERMOSTAT); + + this._service = new Service.Thermostat(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.CurrentHeatingCoolingState, 0); + this._service.setCharacteristic(Characteristic.TargetHeatingCoolingState, 0); + this._service.setCharacteristic(Characteristic.CurrentTemperature, 21); + this._service.setCharacteristic(Characteristic.TargetTemperature, 21); + this._service.setCharacteristic(Characteristic.TemperatureDisplayUnits, 0); + this._service.setCharacteristic(Characteristic.CoolingThresholdTemperature, 26); + this._service.setCharacteristic(Characteristic.HeatingThresholdTemperature, 18); + + this._Properties["CurrentHeatingCoolingState"] = 0; + this._Properties["TargetHeatingCoolingState"] = 0; + this._Properties["CurrentTemperature"] = 21; + this._Properties["TargetTemperature"] = 21; + this._Properties["TemperatureDisplayUnits"] = 0; + this._Properties["CoolingThresholdTemperature"] = 26; + this._Properties["HeatingThresholdTemperature"] = 18; + + const EventStruct = { + "Get": ["TargetHeatingCoolingState", "CurrentHeatingCoolingState", "TemperatureDisplayUnits", "CurrentTemperature", "TargetTemperature", "CoolingThresholdTemperature", "HeatingThresholdTemperature"], + "Set": ["TargetHeatingCoolingState", "TemperatureDisplayUnits", "TargetTemperature", "CoolingThresholdTemperature", "HeatingThresholdTemperature"] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + } +} +Thermostat.prototype.setCharacteristics = _basicSet; + +/** + * Temperature Sensor Accessory + */ +class Temperature extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.TemperatureSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.CurrentTemperature, 21); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["CurrentTemperature"] = 21; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const EventStruct = { + "Get": ["CurrentTemperature", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +Temperature.prototype.setCharacteristics = _setWithBattery; + +/** + * Smoke Sensor Accessory + */ +class Smoke extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.SmokeSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.SmokeDetected, 0); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["SmokeDetected"] = 0; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const EventStruct = { + "Get": ["SmokeDetected", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +Smoke.prototype.setCharacteristics = _setWithBattery; + +/** + * Leak Sensor Accessory + */ +class Leak extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.LeakSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.LeakDetected, 0); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["LeakDetected"] = 0; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const EventStruct = { + "Get": ["LeakDetected", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +Leak.prototype.setCharacteristics = _setWithBattery; + +/** + * Light Sensor Accessory + */ +class LightSensor extends AccessoryCLS { + + constructor(Config) { + super(Config, Catagories.SENSOR); + + this._service = new Service.LightSensor(Config.name, Config.name); + + this._service.setCharacteristic(Characteristic.CurrentAmbientLightLevel, 25); + this._service.setCharacteristic(Characteristic.StatusActive, 1); + this._service.setCharacteristic(Characteristic.StatusFault, 0); + this._service.setCharacteristic(Characteristic.StatusTampered, 0); + this._Properties["CurrentAmbientLightLevel"] = 25; + this._Properties["StatusActive"] = 1; + this._Properties["StatusFault"] = 0; + this._Properties["StatusTampered"] = 0; + + const EventStruct = { + "Get": ["CurrentAmbientLightLevel", "StatusActive", "StatusTampered", "StatusFault"], + "Set": [] + } + + this._wireUpEvents(this._service, EventStruct); + this._accessory.addService(this._service); + + this._createBatteryService(); + } +} +LightSensor.prototype.setCharacteristics = _setWithBattery; + +const AccessoryTypes = [{ + Name: "TEMP", + Label: "Temperature Sensor", + Icon: "Ac_Temp.png", + SupportsRouting: false, + Class: Temperature, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Living Room Temp", + Type: "text" + }] + }, + { + Name: "SMOKE_SENSOR", + Label: "Smoke Alarm", + Icon: "Ac_Smoke.png", + SupportsRouting: false, + Class: Smoke, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Hallway Smoke Alarm", + Type: "text" + }] + }, + { + Name: "LIGHT_SENSOR", + Label: "Light Sensor", + Icon: "Ac_LightSensor.png", + SupportsRouting: false, + Class: LightSensor, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Porch Ambience Sensor", + Type: "text" + }] + }, + { + Name: "LEAK_SENSOR", + Label: "Leak Sensor", + Icon: "Ac_Leak.png", + SupportsRouting: false, + Class: Leak, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Kitchen Leak Sensor", + Type: "text" + }] + }, + { + Name: "FAN", + Label: "Smart Fan", + Icon: "Ac_FAN.png", + SupportsRouting: true, + Class: Fan, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Kitchen Extractor Fan", + Type: "text" + }] + }, + { + Name: "LIGHTBULB", + Label: "Smart Light Bulb", + Icon: "Ac_LIGHTBULB.png", + SupportsRouting: true, + Class: LightBulb, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Hallway Light", + Type: "text" + }, + { + Name: "supportsBrightness", + Label: "Brightness Control", + Default: "true", + Type: "checkbox" + }, + { + Name: "colorMode", + Label: "Color Mode", + Default: "hue", + Choices: ["hue", "temperature", "none"], + Type: "choice" + } + ] + }, + { + Name: "THERMOSTAT", + Label: "Smart Thermostat", + Icon: "Ac_THERMOSTAT.png", + SupportsRouting: true, + Class: Thermostat, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Lounge Thermostat", + Type: "text" + }] + }, + { + Name: "GARAGE_DOOR", + Label: "Garage Door", + Icon: "Ac_GARAGE_DOOR.png", + SupportsRouting: true, + Class: GarageDoor, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Bugatti Veyron Garage", + Type: "text" + }] + }, + { + Name: "LOCK", + Label: "Smart Lock", + Icon: "Ac_LOCK.png", + SupportsRouting: true, + Class: Lock, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Comms Room Lock", + Type: "text" + }] + }, + { + Name: "MOTION_SENSOR", + Label: "Motion Sensor", + Icon: "Ac_MOTION_SENSOR.png", + SupportsRouting: false, + Class: Motion, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Entrance Hall Motion Sensor", + Type: "text" + }] + }, + { + Name: "CONTACT_SENSOR", + Label: "Contact Sensor", + Icon: "Ac_CONTACT_SENSOR.png", + SupportsRouting: false, + Class: Contact, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Loft Hatch", + Type: "text" + }] + }, + { + Name: "ALARM", + Label: "Security Alarm", + Icon: "Ac_ALARM.png", + SupportsRouting: true, + Class: Alarm, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Intruder Alarm", + Type: "text" + }] + }, + { + Name: "SWITCH", + Label: "On/Off Switch", + Icon: "Ac_SWITCH.png", + SupportsRouting: true, + Class: Switch, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Party Switch", + Type: "text" + }] + }, + { + Name: "OUTLET", + Label: "Electrical Outlet", + Icon: "Ac_OUTLET.png", + SupportsRouting: true, + Class: Outlet, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Hallway Socket", + Type: "text" + }] + }, + { + Name: "TV", + Label: "Smart TV", + Icon: "Ac_TV.png", + SupportsRouting: true, + Class: TV, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Cinema Room Plasma", + Type: "text" + }, + { + Name: "inputs", + Label: "Input Sources (1 per line)", + Default: ["HDMI 1", "HDMI 2", "HDMI 3"], + Type: "multi" + } + ] + }, + { + Name: "CAMERA", + Label: "CCTV Camera", + Icon: "Ac_CAMERA.png", + SupportsRouting: false, + Class: Camera, + ConfigProperties: [{ + Name: "name", + Label: "Accessory Name", + Default: "Garage Camera", + Type: "text" + }, + { + Name: "enableMotionDetectionService", + Label: "Enable Motion Detection Feature", + Default: "false", + Type: "checkbox" + }, + { + Name: "enableDoorbellService", + Label: "Enable Door Bell Feature", + Default: "false", + Type: "checkbox" + }, + { + Name: "processor", + Label: "Video Processor", + Default: "ffmpeg", + Type: "text" + }, + { + Name: "liveStreamSource", + Label: "Live Stream Input", + Default: "-rtsp_transport tcp -i rtsp://username:password@ip:port/StreamURI", + Type: "text" + }, + { + Name: "stillImageSource", + Label: "Still Image Input", + Default: "http://username:password@ip:port/SnapshotURI", + Type: "text" + }, + { + Name: "maxWidthHeight", + Label: "Max Width & Height (WxH)", + Default: "1280x720", + Type: "text" + }, + { + Name: "maxFPS", + Label: "Max FPS", + Default: "10", + Type: "text" + }, + { + Name: "maxStreams", + Label: "Max No Of Viewers", + Default: "2", + Type: "text" + }, + { + Name: "encoder", + Label: "Video Encoder", + Default: "libx264", + Type: "text" + }, + { + Name: "maxBitrate", + Label: "Max Bit Rate", + Default: "300", + Type: "text" + }, + { + Name: "packetSize", + Label: "Max Packet Size", + Default: "1316", + Type: "text" + }, + { + Name: "mapVideo", + Label: "Video Map", + Default: "0:0", + Type: "text" + }, + { + Name: "additionalCommandline", + Label: "Additional Processor Args", + Default: "-tune zerolatency -preset ultrafast", + Type: "text" + }, + { + Name: "adhereToRequestedSize", + Label: "Honor Requested Resolution", + Default: "true", + Type: "checkbox" + }, + { + Name: "enableAudio", + Label: "Enable Audio Streaming", + Default: "false", + Type: "checkbox" + }, + { + Name: "encoder_audio", + Label: "Audio Encoder", + Default: "libfdk_aac", + Type: "text" + }, + { + Name: "mapAudio", + Label: "Audio Map", + Default: "0:1", + Type: "text" + } + ] + } +] + +module.exports = { + + Types: AccessoryTypes, + Bridge: Bridge + +} \ No newline at end of file diff --git a/core/cameraSource.js b/core/cameraSource.js new file mode 100644 index 0000000..94672bb --- /dev/null +++ b/core/cameraSource.js @@ -0,0 +1,324 @@ +'use strict' +const HapNodeJS = require('hap-nodejs') +const uuid = HapNodeJS.uuid +const ip = require('ip') +const CameraController = HapNodeJS.CameraController; +const spawn = require('child_process').spawn + +const Camera = function(Config) { + this.config = Config; + this.controller = null; + this.pendingSessions = {} + this.ongoingSessions = {} + this.maxFPS = Config.maxFPS > 30 ? 30 : Config.maxFPS + this.maxWidth = Config.maxWidthHeight.split("x")[0]; + this.maxHeight = Config.maxWidthHeight.split("x")[1]; + this.maxBitrate = Config.maxBitrate; + this.maxPacketSize = Config.packetSize; + this.lastSnapshotTime; + this.imageCache; + +} + +Camera.prototype.attachController = function(Controller) { + this.controller = Controller; +} + +Camera.prototype.handleSnapshotRequest = function(request, callback) { + + if (this.lastSnapshotTime != null) { + const Now = new Date().getTime(); + const Diff = (Now - this.lastSnapshotTime) / 1000; + + // 1 Minute snapshot cache + if (parseInt(Diff) < 60) { + callback(null, this.imageCache) + return; + } + + } + + let imageBuffer = Buffer.alloc(0); + + const CMD = []; + CMD.push('-analyzeduration 1') + CMD.push(this.config.stillImageSource) + CMD.push('-s ' + request.width + 'x' + request.height) + CMD.push('-vframes 1') + CMD.push('-f image2') + CMD.push('-') + + const ffmpeg = spawn(this.config.processor, CMD.join(' ').split(' '), { + env: process.env + }) + + ffmpeg.stdout.on('data', function(data) { + imageBuffer = Buffer.concat([imageBuffer, data]) + }) + + // We need to get access to context here, hence the more recent arrow function approach + ffmpeg.on('close', (c) => { + this.lastSnapshotTime = new Date().getTime(); + this.imageCache = null; + this.imageCache = imageBuffer + + callback(null, this.imageCache) + + }) + +} + +Camera.prototype.prepareStream = function(request, callback) { + const sessionInfo = {} + + const sessionID = request['sessionID'] + sessionInfo['address'] = request['targetAddress'] + + const response = {} + + const videoInfo = request['video'] + if (videoInfo) { + const targetPort = videoInfo['port'] + const srtp_key = videoInfo['srtp_key'] + const srtp_salt = videoInfo['srtp_salt'] + const ssrc = CameraController.generateSynchronisationSource(); + response['video'] = { + port: targetPort, + ssrc: ssrc, + srtp_key: srtp_key, + srtp_salt: srtp_salt + } + sessionInfo['video_port'] = targetPort + sessionInfo['video_srtp'] = Buffer.concat([srtp_key, srtp_salt]) + sessionInfo['video_ssrc'] = ssrc + } + + const audioInfo = request['audio'] + if (audioInfo) { + const targetPort = audioInfo['port'] + const srtp_key = audioInfo['srtp_key'] + const srtp_salt = audioInfo['srtp_salt'] + const ssrc = CameraController.generateSynchronisationSource(); + response['audio'] = { + port: targetPort, + ssrc: ssrc, + srtp_key: srtp_key, + srtp_salt: srtp_salt + } + sessionInfo['audio_port'] = targetPort + sessionInfo['audio_srtp'] = Buffer.concat([srtp_key, srtp_salt]) + sessionInfo['audio_ssrc'] = ssrc + } + + const currentAddress = ip.address() + + const addressResp = { + + address: currentAddress, + } + + if (ip.isV4Format(currentAddress)) { + addressResp['type'] = 'v4' + } else { + addressResp['type'] = 'v6' + } + + response['address'] = addressResp + + this.pendingSessions[uuid.unparse(sessionID)] = sessionInfo + callback(null, response); +} + +Camera.prototype.handleStreamRequest = function(request, callback) { + const sessionID = request['sessionID'] + const requestType = request['type'] + + if (sessionID) { + const sessionIdentifier = uuid.unparse(sessionID) + switch (requestType) { + case "reconfigure": + // to do + callback(null); + break; + + case "stop": + const ffmpegProcess = this.ongoingSessions[sessionIdentifier] + if (ffmpegProcess) { + ffmpegProcess.kill('SIGKILL') + } + delete this.ongoingSessions[sessionIdentifier] + callback(null); + break; + + case "start": + const sessionInfo = this.pendingSessions[sessionIdentifier] + + if (sessionInfo) { + let width = this.maxWidth; + let height = this.maxHeight; + let FPS = this.maxFPS; + let bitRate = this.maxBitrate + let aBitRate = 24 + let aSampleRate = HapNodeJS.AudioStreamingSamplerate.KHZ_16 + let VPT = 0; + let APT = 0; + let MaxPaketSize = this.maxPacketSize + + const videoInfo = request['video'] + if (videoInfo) { + width = videoInfo['width'] + height = videoInfo['height'] + VPT = videoInfo["pt"]; + + const expectedFPS = videoInfo['fps'] + if (expectedFPS < FPS) { + FPS = expectedFPS + } + + if (videoInfo['max_bit_rate'] < bitRate) { + bitRate = videoInfo['max_bit_rate'] + } + + if (videoInfo["mtu"] < MaxPaketSize) { + MaxPaketSize = videoInfo["mtu"]; + } + } + + const audioInfo = request['audio'] + if (audioInfo) { + if (audioInfo['max_bit_rate'] < aBitRate) { + aBitRate = audioInfo['max_bit_rate'] + } + + if (audioInfo['sample_rate'] < aSampleRate) { + aSampleRate = audioInfo['sample_rate'] + } + + APT = audioInfo["pt"]; + } + + const targetAddress = sessionInfo['address'] + const targetVideoPort = sessionInfo['video_port'] + const videoKey = sessionInfo['video_srtp'] + const videoSsrc = sessionInfo['video_ssrc'] + const targetAudioPort = sessionInfo['audio_port'] + const audioKey = sessionInfo['audio_srtp'] + const audioSsrc = sessionInfo['audio_ssrc'] + + const CMD = []; + + // Input + CMD.push(this.config.liveStreamSource) + CMD.push('-map ' + this.config.mapVideo) + CMD.push('-vcodec ' + this.config.encoder) + CMD.push('-pix_fmt yuv420p') + CMD.push('-r ' + FPS) + CMD.push('-f rawvideo') + + if (this.config.additionalCommandline.length > 0) { + CMD.push(this.config.additionalCommandline); + } + + if (this.config.adhereToRequestedSize == 'true' && this.config.encoder != 'copy') { + CMD.push('-vf scale=' + width + ':' + height) + } + + CMD.push('-b:v ' + bitRate + 'k') + CMD.push('-bufsize ' + bitRate + 'k') + CMD.push('-maxrate ' + bitRate + 'k') + CMD.push('-payload_type ' + VPT) + + // Output + CMD.push('-ssrc ' + videoSsrc) + CMD.push('-f rtp') + CMD.push('-srtp_out_suite AES_CM_128_HMAC_SHA1_80') + CMD.push('-srtp_out_params ' + videoKey.toString('base64')) + CMD.push('srtp://' + targetAddress + ':' + targetVideoPort + '?rtcpport=' + targetVideoPort + '&pkt_size=' + MaxPaketSize) + + // Audio ? + if (this.config.enableAudio == 'true') { + // Input + CMD.push('-map ' + this.config.mapAudio) + CMD.push('-acodec ' + this.config.encoder_audio) + CMD.push('-profile:a aac_eld') + CMD.push('-flags +global_header') + CMD.push('-f null'); + CMD.push('-ar ' + aSampleRate + 'k') + CMD.push('-b:a ' + aBitRate + 'k') + CMD.push('-bufsize ' + aBitRate + 'k') + CMD.push('-ac 1') + CMD.push('-payload_type ' + APT) + + // Output + CMD.push('-ssrc ' + audioSsrc) + CMD.push('-f rtp') + CMD.push('-srtp_out_suite AES_CM_128_HMAC_SHA1_80') + CMD.push('-srtp_out_params ' + audioKey.toString('base64')) + CMD.push('srtp://' + targetAddress + ':' + targetAudioPort + '?rtcpport=' + targetAudioPort + '&pkt_size=188') + } + + const ffmpeg = spawn(this.config.processor, CMD.join(' ').split(' '), { + env: process.env, + stdout: 'ignore' + }) + + let live = false; + let CBCalled = false; + + ffmpeg.stderr.on('data', data => { + if (!live) { + if (data.toString().includes('frame=')) { + live = true; + CBCalled = true; + callback(null); + + this.ongoingSessions[sessionIdentifier] = ffmpeg + delete this.pendingSessions[sessionIdentifier] + } + } + }); + + ffmpeg.on('error', error => { + if (!live) { + if (!CBCalled) { + callback(new Error('FFMPEG Error : ' + error.message)); + CBCalled = true; + } + + delete this.pendingSessions[sessionIdentifier] + } else { + this.controller.forceStopStreamingSession(sessionID); + if (this.ongoingSessions.hasOwnProperty(sessionIdentifier)) { + delete this.ongoingSessions[sessionIdentifier] + } + } + + }); + + ffmpeg.on('exit', (c, s) => { + if (c != null && c != 255) { + if (!live) { + if (!CBCalled) { + callback(new Error('FFMPEG Exit : Code - ' + c + ', Signal - ' + s)); + CBCalled = true; + } + + delete this.pendingSessions[sessionIdentifier] + } else { + this.controller.forceStopStreamingSession(sessionID); + if (this.ongoingSessions.hasOwnProperty(sessionIdentifier)) { + delete this.ongoingSessions[sessionIdentifier] + } + } + } + }); + + } + break; + } + } +} + +module.exports = { + Camera: Camera, +} \ No newline at end of file diff --git a/core/mqtt.js b/core/mqtt.js new file mode 100644 index 0000000..9cc1dea --- /dev/null +++ b/core/mqtt.js @@ -0,0 +1,79 @@ +'use strict' +const mqtt = require('mqtt') +const util = require('./util'); +const config = require(util.ConfigPath); + +var MQTTC; +var _Accessories; +var CallBack; + +const MQTTError = function(Error) { + console.log(" Could not connect to MQTT Broker : " + Error); + process.exit(0) +} + +const MQTTConnected = function(Client) { + MQTTC = Client; + MQTTC.subscribe(config.MQTTTopic, MQTTSubscribeDone) +} + +const MQTTSubscribeDone = function(error) { + if (!error) { + MQTTC.on('message', MQTTMessageReceved) + CallBack(); + } else { + console.log(" Could not subscribe to Topic : " + err); + process.exit(0) + } +} + +const MQTTMessageReceved = function(topic, message) { + try { + const sPL = message.toString(); + const PL = JSON.parse(sPL); + const TargetAccessory = topic.split('/').pop() + + const Ac = _Accessories[TargetAccessory] + + if (Ac != null) { + Ac.setCharacteristics(PL) + } + } catch (e) { + console.log(" MQTT input could not be actioned -> MSG: " + sPL + ", Accessory ID: " + TargetAccessory + ""); + } + +} + +const MQTT = function(Accesories, CB) { + _Accessories = Accesories; + CallBack = CB; + + if (config.hasOwnProperty("enableIncomingMQTT") && config.enableIncomingMQTT == 'true') { + if (!config.hasOwnProperty("MQTTOptions")) { + config.MQTTOptions = {}; + } else if (config.MQTTOptions.username.length < 1) { + delete config.MQTTOptions["username"] + delete config.MQTTOptions["password"] + } + + console.log(" Starting MQTT Client") + + try { + const _MQTTC = mqtt.connect(config.MQTTBroker, config.MQTTOptions) + _MQTTC.on('error', MQTTError); + _MQTTC.on('connect', () => MQTTConnected(_MQTTC)) + + } catch (err) { + console.log(" Could not connect to MQTT Broker : " + err); + process.exit(0); + } + + } else { + CallBack(); + } + +} + +module.exports = { + MQTT: MQTT +} \ No newline at end of file diff --git a/core/routes.js b/core/routes.js new file mode 100644 index 0000000..8f6e2be --- /dev/null +++ b/core/routes.js @@ -0,0 +1,167 @@ +'use strict' +const fs = require('fs'); +const dgram = require("dgram"); +const mqtt = require('mqtt') +const axios = require('axios') +const util = require('./util') +const Websocket = require('ws') +const Path = require('path'); + +var UDPServer; +const MQTTCs = {}; +const Websockets = {} + +/* Clean Payload */ +const CleanPayload = function(Payload, Type) { + const Copy = JSON.parse(JSON.stringify(Payload)); + + Copy["route_type"] = Type; + Copy["route_name"] = Payload.accessory.route + + delete Copy.accessory.pincode; + delete Copy.accessory.username; + delete Copy.accessory.setupID; + delete Copy.accessory.route; + delete Copy.accessory.description; + delete Copy.accessory.serialNumber; + + return Copy; + +} + +/* WS */ +const WEBSOCKET = function(route, payload) { + payload = CleanPayload(payload, "WEBSOCKET") + + if (Websockets.hasOwnProperty(route.uri)) { + Websockets[route.uri].send(JSON.stringify(payload)); + } else { + const WS = new Websocket(route.uri); + WS.on('open', () => HandleWSOpen(route, WS, payload)); + WS.on('error', (e) => WSError(e)) + } + +} + +const WSError = function(err) { + console.log(" Could not connect to Websocket : " + err); +} + +const HandleWSOpen = function(route, WS, Payload) { + + Websockets[route.uri] = WS; + Websockets[route.uri].send(JSON.stringify(Payload)); +} + +/* HTTP */ +const HTTP = function(route, payload) { + payload = CleanPayload(payload, "HTTP") + + const CFG = { + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Homekit Device Stack' + }, + url: route.destinationURI.replace('{{accessoryID}}', payload.accessory.accessoryID), + method: 'post', + data: payload + } + + axios.request(CFG) + .then(function(res) {}) + .catch(function(err) { + console.log(" Could not send HTTP request : " + err); + }) +} + +/* UDP */ +const UDP = function(route, payload) { + payload = CleanPayload(payload, "UDP"); + let JSONs = JSON.stringify(payload); + + if (UDPServer == null) { + UDPServer = dgram.createSocket("udp4"); + UDPServer.bind(() => UDPConnected(JSONs, route)) + } else { + UDPServer.send(JSONs, 0, JSONs.length, route.port, route.address, UDPDone) + } +} + +const UDPConnected = function(JSONString, Route) { + UDPServer.setBroadcast(true); + UDPServer.send(JSONString, 0, JSONString.length, Route.port, Route.address, UDPDone); +} + +const UDPDone = function(e, n) { + if (e) { + console.log(" Could not broadcast UDP: " + e); + } +} + +/* MQTT */ +const MQTT = function(route, payload) { + payload = CleanPayload(payload, "MQTT"); + let JSONs = JSON.stringify(payload); + + if (MQTTCs.hasOwnProperty(route.broker)) { + let Topic = route.topic.replace('{{accessoryID}}', payload.accessory.accessoryID); + MQTTCs[route.broker].publish(Topic, JSONs, null, MQTTDone); + } else { + if (!route.hasOwnProperty("MQTTOptions")) { + route.MQTTOptions = {}; + } else if (route.MQTTOptions.hasOwnProperty("username") && route.MQTTOptions.username.length < 1) { + delete route.MQTTOptions["username"] + delete route.MQTTOptions["password"] + } + const MQTTC = mqtt.connect(route.broker, route.MQTTOptions) + let Topic = route.topic.replace('{{accessoryID}}', payload.accessory.accessoryID); + MQTTC.on('error', MQTTError); + MQTTC.on('connect', () => MQTTConnected(JSONs, route, Topic, MQTTC)); + } +} + +const MQTTError = function(err) { + console.log(" Could not connect to MQTT Broker : " + err); +} + +const MQTTConnected = function(JSONString, Route, Topic, Client) { + MQTTCs[Route.broker] = Client; + MQTTCs[Route.broker].publish(Topic, JSONString, null, MQTTDone); +} + +const MQTTDone = function() {} + +/* FILE */ +const FILE = function(route, payload) { + payload = CleanPayload(payload, "FILE"); + + let DirPath = Path.join(util.RootPath, route.directory.replace('{{accessoryID}}', payload.accessory.accessoryID)) + + fs.mkdirSync(DirPath, { + recursive: true + }, function(err) { + if (err) { + console.log(" Could not write output to file."); + } + }); + + const DT = new Date().getTime(); + const FileName = DT + '_' + payload.accessory.accessoryID + ".json" + let _Path = Path.join(DirPath, FileName); + + fs.writeFile(_Path, JSON.stringify(payload), 'utf8', FileDone); +} + +const FileDone = function(err) { + if (err) { + console.log(" Could not write output to file."); + } +} + +module.exports = { + "HTTP": HTTP, + "UDP": UDP, + "FILE": FILE, + "MQTT": MQTT, + "WEBSOCKET": WEBSOCKET +} \ No newline at end of file diff --git a/core/routing.js b/core/routing.js new file mode 100644 index 0000000..92c193f --- /dev/null +++ b/core/routing.js @@ -0,0 +1,82 @@ +'use strict' +const PATH = require('path'); +const FS = require('fs'); +const { spawnSync } = require('child_process'); + +const StockRoutes = ["hkds-route-console", "hkds-route-file", "hkds-route-http", "hkds-route-mqtt", "hkds-route-udp", "hkds-route-websocket"] +var RootPath; + +const Routes = { + +} + +const setPath = function(Path){ + RootPath = Path; +} + +const installStockModules = function () { + + module.paths.push(PATH.join(RootPath, "node_modules")) + + let FoundModules = [] + + if (FS.existsSync(PATH.join(RootPath, "node_modules"))) { + + let Files = FS.readdirSync(PATH.join(RootPath, "node_modules")); + + Files.forEach((D) => { + let FI = FS.lstatSync(PATH.join(RootPath, "node_modules", D)) + if (FI.isDirectory()) { + FoundModules.push(D) + } + }) + } + + StockRoutes.forEach((SR) => { + if (FoundModules.indexOf(SR) < 0) { + install(SR) + } + }) +} + +const loadModules = function (){ + + + let Files = FS.readdirSync(PATH.join(RootPath, "node_modules")); + + Files.forEach((D) => { + + if(!D.startsWith("hkds-route-")){ + return; + } + + let FI = FS.lstatSync(PATH.join(RootPath, "node_modules", D)) + if (FI.isDirectory()) { + + let Mod = require(D); + let RouteOBJ = {} + + RouteOBJ.Type = D + RouteOBJ.Icon = PATH.join(RootPath,"node_modules",D,Mod.Icon); + RouteOBJ.Name = Mod.Name; + RouteOBJ.Class = Mod.Route; + RouteOBJ.Inputs = Mod.Inputs; + + Routes[D] = RouteOBJ; + } + }) + +} + +const install = function(Module) { + console.log(" Installing route module: " + Module); + spawnSync("npm", ["install", "" + Module + "", "--prefix", '"' + RootPath + '"'], { shell: true }); +} + +module.exports = { + "installStockModules": installStockModules, + "Routes": Routes, + "loadModules":loadModules, + "install": install, + "setPath":setPath +} diff --git a/core/server.js b/core/server.js new file mode 100755 index 0000000..e29543e --- /dev/null +++ b/core/server.js @@ -0,0 +1,652 @@ +'use strict' +const EXPRESS = require('express') +const CRYPTO = require('crypto') +const HANDLEBARS = require('handlebars') +const FS = require('fs'); +const BODYPARSER = require('body-parser') +const ACCESSORY = require('./accessory'); +const UTIL = require('./util'); +const CONFIG = require(UTIL.ConfigPath); +const COOKIEPARSER = require('cookie-parser') +const PATH = require('path'); +const OS = require("os"); +const ROUTING = require('./routing'); + +const Server = function(Accesories, ChangeEvent, IdentifyEvent, Bridge, RouteSetup, PairEvent) { + + // Vars + let _Paired = false; + const _Accessories = Accesories + const _ChangeEvent = ChangeEvent; + const _IdentifyEvent = IdentifyEvent + const _Bridge = Bridge; + const _RouteSetup = RouteSetup + const _PairEvent = PairEvent; + + // Template Files + const Templates = { + "Login": process.cwd() + "/ui/login.tpl", + "Setup": process.cwd() + "/ui/setup.tpl", + "Main": process.cwd() + "/ui/main.tpl", + "Create": process.cwd() + "/ui/create.tpl", + "Edit": process.cwd() + "/ui/edit.tpl", + } + + HANDLEBARS.registerHelper('ifvalue', function(conditional, options) { + if (options.hash.equals === conditional) { + return options.fn(this) + } else { + return options.inverse(this); + } + }); + + const CompiledTemplates = {} + + // Start Server + this.Start = function(CB) { + + console.log(" Starting Web Server") + console.log(" ") + + let TemplateKeys = Object.keys(Templates) + + // Compile TPLs + for (let i = 0; i < TemplateKeys.length; i++) { + CompiledTemplates[TemplateKeys[i]] = HANDLEBARS.compile(FS.readFileSync(Templates[TemplateKeys[i]], 'utf8')); + } + + // Express + const app = EXPRESS() + + // Middleware + app.use(BODYPARSER.json()) + app.use(COOKIEPARSER("2jS4khgKVTMaVhwVxYPx8Kjnwwpfyvxa")) + + // UI + app.use('/ui/static', EXPRESS.static(process.cwd() + '/ui/static')) + app.get('/', _Redirect); + app.get('/ui/main', _Main); + app.get('/ui/setup', _Setup); + app.get('/ui/getroutemeta/:module_name', _GetRouteMeta); + app.get('/ui/login', _Login); + app.get('/ui/createaccessory/:type', _CreateAccessory); + app.get('/ui/editaccessory/:type/:id', _EditAccessory); + app.get('/ui/pairstatus', _PairStatus); + app.get('/ui/pairstatus/:id', _PairStatus); + app.get('/ui/backup', _Backup); + app.post('/ui/login', _DoLogin); + app.post('/ui/deleteroute', _DoDeleteRoute); + app.post('/ui/setconfig', _DoSaveConfig); + app.post('/ui/deleteaccessory', _DoDeleteAccessory); + app.post('/ui/restore', _DoRestore); + app.post('/ui/createroute', _DoCreateRoute); + app.post('/ui/connect', _DoConnect); + app.post('/ui/disconnect', _DoDisconnect); + app.post('/ui/createaccessory', _DoCreateAccessory); + app.post('/ui/editaccessory', _DoEditAccessory); + + // API + app.get('/:pwd/accessories/', _processAccessoriesGet); + app.get('/:pwd/accessories/:id', _processAccessoryGet); + app.put('/:pwd/accessories/:id', _processAccessorySet); + + try { + if (CONFIG.webInterfaceAddress == 'ALL') { + app.listen(CONFIG.webInterfacePort) + } else { + app.listen(CONFIG.webInterfacePort, CONFIG.webInterfaceAddress) + } + + } catch (err) { + console.log(" Could not start Web Server : " + err); + process.exit(0); + } + + CB(); + } + + function _GetRouteMeta(req,res){ + + if (!_CheckAuth(req, res)) { + return; + } + + let ModuleName = req.params.module_name; + let RouteType = ROUTING.Routes[ModuleName]; + + let _Buffer = FS.readFileSync(RouteType.Icon) + + let MD = { + Inputs:RouteType.Inputs, + Icon:_Buffer.toString("base64"), + Name:RouteType.Name + } + + res.contentType('application/json'); + res.send(MD); + + } + + function _Redirect(req,res){ + res.redirect('./ui/main') + } + + function _DoEditAccessory(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Data = req.body + + const Acs = _Accessories[Data.username.replace(/:/g, "")]; + const CurrentProps = Acs.getProperties(); + + _DeleteAccessory(Data.username.replace(/:/g, ""), Data.username, Data.bridged, false); + + let URI = _AddAccessory(Data, CurrentProps); + res.contentType('application/json'); + if (!Data.bridged) { + res.send('{"OK":true,"URI":"' + URI + '","Pin":"' + Data.pincode + '","ID":"' + Data.username.replace(/:/g, "") + '"}'); + } else { + res.send('{"OK":true}'); + } + + } + + function _DoCreateAccessory(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Data = req.body + + Data["pincode"] = UTIL.getRndInteger(100, 999) + "-" + UTIL.getRndInteger(10, 99) + "-" + UTIL.getRndInteger(100, 999); + Data["username"] = UTIL.genMAC(); + Data["setupID"] = UTIL.makeID(4); + Data["serialNumber"] = UTIL.makeID(12); + + let URI = _AddAccessory(Data); + res.contentType('application/json'); + if (!Data.bridged) { + res.send('{"OK":true,"URI":"' + URI + '","Pin":"' + Data.pincode + '","ID":"' + Data.username.replace(/:/g, "") + '"}'); + } else { + res.send('{"OK":true}'); + } + } + + function _DoConnect(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Data = req.body + + _ReRoute(Data.SID, Data.TID); + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + function _DoDisconnect(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Data = req.body + + _Unroute(Data.SID); + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + function _DoCreateRoute(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Data = req.body + + _AddRoute(Data); + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + function _DoRestore(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Result = UTIL.restore(req.body); + + if (Result == "Version") { + res.contentType('application/json'); + res.send('{"OK":false,"Reason":"VersionMismatch"}'); + return; + } + if (Result == "Invalid") { + res.contentType('application/json'); + res.send('{"OK":false,"Reason":"InvalidFile"}'); + return; + } + + if (Result == true) { + res.contentType('application/json'); + res.send('{"OK":true}'); + process.exit(0); + } + + } + + function _DoDeleteAccessory(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + const Data = req.body; + + _DeleteAccessory(Data.username.replace(/:/g, ""), Data.username, Data.bridged, true); + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + function _DoSaveConfig(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + const Data = req.body; + + UTIL.updateOptions(Data); + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + function _DoLogin(req, res) { + + const Data = req.body; + + const Username = Data.username; + const Password = CRYPTO.createHash('md5').update(Data.password).digest("hex"); + if (Username == CONFIG.loginUsername && Password == CONFIG.loginPassword) { + res.cookie('Authentication', 'Success', { + 'signed': true + }) + res.contentType('application/json'); + let Response = { + success: true, + destination: '../../../ui/main' + } + res.send(JSON.stringify(Response)) + } else { + res.contentType('application/json'); + let Response = { + success: false + } + res.send(JSON.stringify(Response)) + } + + } + + function _DoDeleteRoute(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + const Data = req.body; + _DeleteRoute(Data.name) + res.contentType('application/json'); + res.send('{"OK":true}'); + + } + + /* Main Page */ + function _Main(req, res) { + + // Auth, Setup Check + if (!_CheckAuth(req, res)) { + return; + } + + // Availbale Route Types + let RouteModules = []; + let RouteTypes = Object.keys(ROUTING.Routes); + RouteTypes.forEach((RT) =>{ + + let RouteType = ROUTING.Routes[RT]; + RouteModules.push({Type:RouteType.Type,Name:RouteType.Name}) + + }); + + // Configured Routes + let ConfiguredRoutesArray = []; + let ConfiguredRoutesKeys = Object.keys(CONFIG.routes); + ConfiguredRoutesKeys.forEach((CR) =>{ + + let ConfiguredRoute = CONFIG.routes[CR]; + + + }) + + + + + + let Interfaces = OS.networkInterfaces(); + let Keys = Object.keys(Interfaces); + let IPs = []; + + for (let i = 0; i < Keys.length; i++) { + + let Net = Interfaces[Keys[i]]; + + Net.forEach((AI) => { + if (AI.family == 'IPv4' && !AI.internal) { + IPs.push(AI.address) + } + }) + + } + + let HTML = CompiledTemplates['Main']({ + "RootPath": UTIL.RootPath, + "Config": CONFIG, + "RouteModules": RouteModules, + "AccessoryTypes": ACCESSORY.Types, + "AccessoryTypesJSON": JSON.stringify(ACCESSORY.Types, null, 2), + "interfaces": IPs + }); + res.contentType('text/html') + res.send(HTML) + + } + + /* Setup Page */ + function _Setup(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + if (_Paired) { + res.redirect("../../../ui/main"); + return; + } + + let HTML = CompiledTemplates['Setup']({ + "Config": CONFIG + }); + res.contentType('text/html') + res.send(HTML) + + } + + /* Login Page */ + function _Login(req, res) { + + let HTML = CompiledTemplates['Login']({}); + + res.contentType('text/html') + res.send(HTML) + + } + + /* Create Page */ + function _CreateAccessory(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Type = req.params.type; + + let HTML = CompiledTemplates['Create']({ + "Config": CONFIG, + "Type": JSON.stringify(ACCESSORY.Types.filter(C => C.Name == Type)[0], null, 2) + }); + + res.contentType('text/html') + res.send(HTML) + + } + + // Edit Pgae */ + function _EditAccessory(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let Type = req.params.type; + let ID = req.params.id; + + const TargetAc = CONFIG.accessories.filter(a => a.accessoryID == ID)[0] + + let HTML = CompiledTemplates['Edit']({ + "Config": CONFIG, + "Object": JSON.stringify(TargetAc, null, 2), + "Type": JSON.stringify(ACCESSORY.Types.filter(C => C.Name == Type)[0], null, 2) + }) + + res.contentType('text/html') + res.send(HTML) + } + + // Pair Status */ + function _PairStatus(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + + let ID = req.params.id; + + let Response = {} + + if (ID != null) { + const AccessoryFileName = PATH.join(UTIL.HomeKitPath, "AccessoryInfo." + ID + ".json"); + if (FS.existsSync(AccessoryFileName)) { + delete require.cache[require.resolve(AccessoryFileName)]; + const IsPaired = Object.keys(require(AccessoryFileName).pairedClients) + Response.paired = IsPaired.length > 0; + } else { + Response.paired = false; + } + } else { + Response.paired = _Paired + } + res.contentType('application/json'); + res.send(JSON.stringify(Response)) + } + + function _Backup(req, res) { + + if (!_CheckAuth(req, res)) { + return; + } + res.contentType('application/octet-stream') + res.header("Content-Disposition", "attachment; filename=\"HKDS-Backup.dat\""); + res.send(UTIL.generateBackup()) + } + + // Add new accessory + function _AddAccessory(Data, Props) { + + UTIL.appendAccessoryToConfig(Data) + Data.accessoryID = Data.username.replace(/:/g, ""); + CONFIG.accessories.push(Data) + + let Type = ACCESSORY.Types.filter(C => C.Name == Data.type)[0] + let Acc = new Type.Class(Data); + + if (Props != null) { + Acc.setCharacteristics(Props) + } + + Acc.on('STATE_CHANGE', (PL, O) => _ChangeEvent(PL, Data, O)) + Acc.on('IDENTIFY', (P) => _IdentifyEvent(P, Data)) + _Accessories[Data.accessoryID] = Acc; + if (Data.bridged) { + _Bridge.addAccessory(Acc.getAccessory()) + } else { + Acc.on('PAIR_CHANGE', (P) => _PairEvent(P, Data)) + Acc.publish(); + } + return Acc.getAccessory().setupURI(); + } + + // Delete Route + function _DeleteRoute(Name) { + let Routes = CONFIG.routes; + delete Routes[Name] + _RouteSetup(); + UTIL.updateRouteConfig(Routes); + } + + // Add Route + function _AddRoute(Route) { + let Routes = CONFIG.routes; + let Name = Route.name; + delete Route.name; + Routes[Name] = Route; + _RouteSetup(); + UTIL.updateRouteConfig(Routes); + } + + // Update Device Route + function _ReRoute(AccessoryID, Route) { + CONFIG.accessories.filter((A) => A.accessoryID == AccessoryID)[0].route = Route.split('_')[1]; + UTIL.routeAccessory(AccessoryID, Route.split('_')[1]); + } + + // Clear Device Route + function _Unroute(AccessoryID) { + CONFIG.accessories.filter((A) => A.accessoryID == AccessoryID)[0].route = ""; + UTIL.routeAccessory(AccessoryID, ""); + } + + // Delete Accessory + function _DeleteAccessory(AccessoryID, Username, Bridged, Permanent) { + + if (Bridged) { + const Acs = _Bridge.getAccessories(); + const TargetAcc = Acs.filter(a => a.username == Username)[0]; + _Bridge.removeAccessory(TargetAcc); + delete _Accessories[AccessoryID] + UTIL.deleteAccessory(AccessoryID) + const NA = CONFIG.accessories.filter(a => a.accessoryID != AccessoryID) + CONFIG.accessories = NA; + } else { + _Accessories[AccessoryID].unpublish(Permanent) + delete _Accessories[AccessoryID] + UTIL.deleteAccessory(AccessoryID) + const NA = CONFIG.accessories.filter(a => a.accessoryID != AccessoryID) + CONFIG.accessories = NA; + } + } + + // Check Auth + function _CheckAuth(req, res) { + if (req.signedCookies.Authentication == null || req.signedCookies.Authentication != 'Success') { + res.redirect("../../../ui/login"); + return false; + } + return true; + } + + // get Accessories + function _processAccessoriesGet(req, res) { + const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex"); + if (PW != CONFIG.loginPassword) { + res.sendStatus(401); + return; + } + const TPL = []; + const Names = Object.keys(_Accessories); + for (let i = 0; i < Names.length; i++) { + const PL = { + "id": Names[i], + "type": _Accessories[Names[i]].getAccessoryType(), + "name": _Accessories[Names[i]].getAccessory().displayName, + "characteristics": _Accessories[Names[i]].getProperties() + } + TPL.push(PL) + } + res.contentType("application/json"); + res.send(JSON.stringify(TPL)); + } + + // get Accessory + function _processAccessoryGet(req, res) { + const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex"); + if (PW != CONFIG.loginPassword) { + res.sendStatus(401); + return; + } + + const Ac = _Accessories[req.params.id] + + if (Ac == null) { + res.contentType("application/json"); + res.send(JSON.stringify({ + "Error": "Device not found" + })); + return; + } + + const PL = { + "id": req.params.id, + "type": Ac.getAccessoryType(), + "name": Ac.getAccessory().displayName, + "characteristics": Ac.getProperties() + } + res.contentType("application/json"); + res.send(JSON.stringify(PL)); + } + + // Set Accessory data + function _processAccessorySet(req, res) { + const PW = CRYPTO.createHash('md5').update(req.params.pwd).digest("hex"); + if (PW != CONFIG.loginPassword) { + res.sendStatus(401); + return; + } + + const Ac = _Accessories[req.params.id] + + if (Ac == null) { + res.contentType("application/json"); + res.send(JSON.stringify({ + "ok": false, + "reason": "Device not found" + })); + return; + } + + Ac.setCharacteristics(req.body) + res.contentType("application/json"); + res.send(JSON.stringify({ + ok: true + })); + } + + // Set Pair Status + this.setBridgePaired = function(IsPaired) { + _Paired = IsPaired; + } +} + +module.exports = { + Server: Server +} \ No newline at end of file diff --git a/core/util.js b/core/util.js new file mode 100755 index 0000000..2a6435c --- /dev/null +++ b/core/util.js @@ -0,0 +1,321 @@ +'use strict' +const FS = require('fs'); +const PATH = require('path'); +const READLINE = require("readline"); +const CHALK = require('chalk'); +const CRYPTO = require('crypto') +const OS = require('os'); +const ROOTPATH = PATH.join(OS.homedir(), "HKDS"); +const CONFIGPATH = PATH.join(ROOTPATH, "hkds_config.json"); +const HOMEKITPATH = PATH.join(ROOTPATH, "HomeKitPersist"); +const PKG = require('../package.json'); +const CACHEPATH = PATH.join(ROOTPATH, "characteristic_cache.json"); +const ROUTING = require("./routing"); + +const RestoreMin = "4.0.0"; +const RestoreMax = "4.0.0"; + + + +const saveCharacteristicCache = function (Cache) { + FS.writeFileSync(CACHEPATH, JSON.stringify(Cache), 'utf8', function (err) { + if (err) { + console.log(" Could not right to the config file."); + } + }) +} + +const getCharacteristicCache = function () { + if (FS.existsSync(CACHEPATH)) { + const C = FS.readFileSync(CACHEPATH, 'utf8'); + return JSON.parse(C); + } + + return null; +} + +const restore = function (data) { + try { + let buff = Buffer.from(data.content, 'base64'); + let text = buff.toString('utf8'); + const JS = JSON.parse(text); + + if (JS.sourceVersion < RestoreMin || JS.sourceVersion > RestoreMax) { + return "Version" + } else { + // Delete, restore config + FS.unlinkSync(CONFIGPATH); + saveConfig(JS.config); + + // Delete Cache Files + FS.readdirSync(HOMEKITPATH).forEach(function (file, index) { + FS.unlinkSync(PATH.join(HOMEKITPATH, file)); + }); + + // Restore Cache Files + for (let i = 0; i < JS.homekitCache.length; i++) { + FS.writeFileSync(PATH.join(HOMEKITPATH, JS.homekitCache[i].file), JSON.stringify(JS.homekitCache[i].content), 'utf8', function (err) { + if (err) { + console.log(" Could not right to cache file."); + process.exit(0); + } + }) + } + + return true; + + } + } catch (err) { + return "Invalid" + } + +} + +const generateBackup = function () { + let BU = {}; + + // Min Version + BU.sourceVersion = PKG.version; + + // Config + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + BU.config = ConfigOBJ + + // Cache + BU.homekitCache = [] + FS.readdirSync(HOMEKITPATH).forEach(function (file, index) { + const FC = FS.readFileSync(PATH.join(HOMEKITPATH, file), 'utf8'); + const FCP = JSON.parse(FC); + BU.homekitCache.push({ + "file": file, + "content": FCP + }); + }); + + const JS = JSON.stringify(BU); + + return Buffer.from(JS).toString('base64'); + +} + +// new install check +const checkNewEV = function () { + + if (!FS.existsSync(CONFIGPATH)) { + reset(); + } +} + +const getRndInteger = function (min, max) { + return Math.floor(Math.random() * (max - min)) + min; +} + +const genMAC = function () { + var hexDigits = "0123456789ABCDEF"; + var macAddress = ""; + for (var i = 0; i < 6; i++) { + macAddress += hexDigits.charAt(Math.round(Math.random() * 15)); + macAddress += hexDigits.charAt(Math.round(Math.random() * 15)); + if (i != 5) macAddress += ":"; + } + return macAddress; +} + +const makeID = function (length) { + var result = ''; + var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + var charactersLength = characters.length; + for (var i = 0; i < length; i++) { + result += characters.charAt(Math.floor(Math.random() * charactersLength)); + } + return result; +} + +// Flash Route COnfig +const updateRouteConfig = function (Config) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + ConfigOBJ.routes = Config; + saveConfig(ConfigOBJ); +} + +// Adjust options +const updateOptions = function (Config) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + + const ConfigOBJ = JSON.parse(CFF); + + ConfigOBJ.enableIncomingMQTT = Config.enableIncomingMQTT; + ConfigOBJ.MQTTBroker = Config.MQTTBroker; + ConfigOBJ.MQTTTopic = Config.MQTTTopic; + ConfigOBJ.advertiser = Config.advertiser; + ConfigOBJ.interface = Config.interface; + ConfigOBJ.webInterfaceAddress = Config.webInterfaceAddress; + ConfigOBJ.webInterfacePort = Config.webInterfacePort; + + if (ConfigOBJ.hasOwnProperty("MQTTOptions")) { + ConfigOBJ.MQTTOptions.username = Config.MQTTOptions.username + ConfigOBJ.MQTTOptions.password = Config.MQTTOptions.password + } else { + ConfigOBJ.MQTTOptions = {} + ConfigOBJ.MQTTOptions.username = Config.MQTTOptions.username + ConfigOBJ.MQTTOptions.password = Config.MQTTOptions.password + } + + saveConfig(ConfigOBJ); +} + +// Add Accessory +const appendAccessoryToConfig = function (Accessory) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + ConfigOBJ.accessories.push(Accessory); + saveConfig(ConfigOBJ); +} + +// Update Device Route +const routeAccessory = function (AccessoryID, RouteName) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + const TargetAc = ConfigOBJ.accessories.filter(a => a.username.replace(/:/g, "") == AccessoryID)[0] + TargetAc.route = RouteName; + saveConfig(ConfigOBJ); +} + +// Save new bridge +const saveBridgeConfig = function (Config) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + ConfigOBJ.bridgeConfig = Config; + saveConfig(ConfigOBJ); +} + +// Global write CFG +const saveConfig = function (Config) { + FS.writeFileSync(CONFIGPATH, JSON.stringify(Config), 'utf8', function (err) { + if (err) { + console.log(" Could not right to the config file."); + process.exit(0); + } + }) +} + +// check password reset request +const checkPassword = function () { + if (process.argv.length > 3) { + if (process.argv[2] == "passwd") { + const NPWD = process.argv[3]; + const PW = CRYPTO.createHash('md5').update(NPWD).digest("hex"); + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + ConfigOBJ.loginPassword = PW; + saveConfig(ConfigOBJ); + console.log(CHALK.keyword('yellow')(" Password has been set.")) + console.log('') + process.exit(0); + } + } +} + +// check password reset request +const checkInstallRequest = function () { + if (process.argv.length > 3) { + if (process.argv[2] == "installmodule") { + const Module = process.argv[3]; + + ROUTING.install(Module) + console.log(CHALK.keyword('green')(" Module ["+Module+"] has been installed.")) + console.log('') + process.exit(0); + + } + } +} + +// Delete Accessory +const deleteAccessory = function (AccessoryID) { + const CFF = FS.readFileSync(CONFIGPATH, 'utf8'); + const ConfigOBJ = JSON.parse(CFF); + const NA = ConfigOBJ.accessories.filter(a => a.username.replace(/:/g, "") != AccessoryID) + ConfigOBJ.accessories = NA; + saveConfig(ConfigOBJ); +} + +// check reset request +const checkReset = function () { + if (process.argv.length > 2) { + if (process.argv[2] == "reset") { + const rl = READLINE.createInterface({ + input: process.stdin, + output: process.stdout + }); + console.log(CHALK.keyword('yellow')(" -- WARNING --")) + console.log('') + console.log(CHALK.keyword('yellow')(" HomeKit Device Stack is about to be RESET!!.")) + console.log(CHALK.keyword('yellow')(" This will.")) + console.log('') + console.log(CHALK.keyword('yellow')(" - Delete all your Accessories (Including any CCTV Cameras).")) + console.log(CHALK.keyword('yellow')(" - Destroy the Bridge hosting those Accessories.")) + console.log(CHALK.keyword('yellow')(" - Delete all HomeKit cache data.")) + console.log(CHALK.keyword('yellow')(" - Delete all HomeKit Device Stack Configuration.")) + console.log(CHALK.keyword('yellow')(" - Discard any Accessory identification.")) + console.log(CHALK.keyword('yellow')(" - Reset the login details for the UI.")) + console.log('') + console.log(CHALK.keyword('yellow')(" Evan if you recreate Accessories, you will need to re-enroll HomeKit Device Stack on your iOS device.")) + console.log('') + rl.question(" Continue? (y/n) :: ", function (value) { + if (value.toUpperCase() == 'Y') { + console.log('') + reset(); + console.log(' Homekit Device Stack has been reset.'); + console.log('') + process.exit(0); + } else { + process.exit(0); + } + }); + return true + } else { + return false; + } + } +} + +// The acrtual reset script +const reset = function () { + + FS.rmdirSync(ROOTPATH, { recursive: true }) + FS.mkdirSync(ROOTPATH, { recursive: true }) + + let DefaultFile = PATH.join(process.cwd(),"hkds_config.json.default") + let SaveTo = PATH.join(ROOTPATH,"hkds_config.json"); + + FS.copyFileSync(DefaultFile,SaveTo) + + +} + +module.exports = { + getRndInteger: getRndInteger, + genMAC: genMAC, + makeID: makeID, + routeAccessory: routeAccessory, + saveConfig: saveConfig, + appendAccessoryToConfig: appendAccessoryToConfig, + checkReset: checkReset, + saveBridgeConfig: saveBridgeConfig, + updateRouteConfig: updateRouteConfig, + checkPassword: checkPassword, + deleteAccessory: deleteAccessory, + updateOptions: updateOptions, + ConfigPath: CONFIGPATH, + HomeKitPath: HOMEKITPATH, + RootPath: ROOTPATH, + checkNewEV: checkNewEV, + generateBackup: generateBackup, + restore: restore, + saveCharacteristicCache: saveCharacteristicCache, + getCharacteristicCache: getCharacteristicCache, + checkInstallRequest:checkInstallRequest +} \ No newline at end of file diff --git a/hkds_config.json.default b/hkds_config.json.default new file mode 100644 index 0000000..a3b530b --- /dev/null +++ b/hkds_config.json.default @@ -0,0 +1,47 @@ +{ + "advertiser": "ciao", + "interface": "ALL", + "loginUsername": "admin", + "loginPassword": "21232f297a57a5a743894a0e4a801fc3", + "webInterfacePort": 7989, + "webInterfaceAddress": "ALL", + "enableIncomingMQTT": "false", + "MQTTBroker": "mqtt://test.mosquitto.org", + "MQTTTopic": "HKDS/IN/+", + "MQTTOptions": { + "username": "", + "password": "" + }, + "bridgeConfig": { + }, + "routes": { + "Node Red": { + "type": "hkds-route-http", + "destinationURI": "http://192.168.0.2:1880/HKDS/{{accessoryID}}" + }, + "Output To Console": { + "type": "hkds-route-console" + }, + "UDP Broadcast": { + "type": "hkds-route-udp", + "address": "255.255.255.255", + "port": 34322 + }, + "File Output": { + "type": "hkds-route-file", + "directory": "/accessory-events/{{accessoryID}}" + }, + "MQTT Broker": { + "type": "hkds-route-mqtt", + "mqttbroker":"mqtt://broker.mqttdashboard.com", + "mqttusername":"", + "mqttpassword":"", + "topic": "HKDS/OUT/{{accessoryID}}" + }, + "Websocket": { + "type": "hkds-route-websocket", + "uri": "ws://server.com:9999/path/to/websocket" + } + }, + "accessories": [] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100755 index 0000000..af6efee --- /dev/null +++ b/package.json @@ -0,0 +1,45 @@ +{ + "name": "homekit-device-stack", + "version": "4.0.0", + "description": "A Middleware Server, for bringing homekit functionality to your Home Automation.", + "main": "App.js", + "keywords": [ + "homekit", + "smart", + "home", + "automation", + "hap", + "apple", + "ios", + "accessory", + "virtual", + "hap-nodejs", + "device" + ], + "author": { + "name": "Marcus Davies", + "email": "marcus.davies83@icloud.com" + }, + "license": "MIT", + "dependencies": { + "chalk":"4.1.0", + "child_process":"1.0.2", + "cookie-parser":"1.4.5", + "events":"3.3.0", + "express":"4.17.1", + "hap-nodejs":"0.9.4", + "ip":"1.1.5", + "mqtt":"4.2.6", + "handlebars": "4.7.7", + "readline":"1.3.0", + "node-cleanup": "2.1.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/marcus-j-davies/Homekit-Device-Stack.git" + }, + "bugs": { + "url": "https://github.com/marcus-j-davies/Homekit-Device-Stack/issues" + }, + "homepage": "https://github.com/marcus-j-davies/Homekit-Device-Stack#readme" +} diff --git a/ui/create.tpl b/ui/create.tpl new file mode 100644 index 0000000..79e0f0c --- /dev/null +++ b/ui/create.tpl @@ -0,0 +1,86 @@ + + + + + Homekit Device Stack + + + + + + + + + + +
Homekit Device Stack
+
+ +
+ + + + \ No newline at end of file diff --git a/ui/edit.tpl b/ui/edit.tpl new file mode 100644 index 0000000..da01990 --- /dev/null +++ b/ui/edit.tpl @@ -0,0 +1,57 @@ + + + + + Homekit Device Stack + + + + + + + + + + +
Homekit Device Stack
+
+ +
+ + + \ No newline at end of file diff --git a/ui/login.tpl b/ui/login.tpl new file mode 100644 index 0000000..0fcd6ac --- /dev/null +++ b/ui/login.tpl @@ -0,0 +1,46 @@ + + + + + + Homekit Device Stack + + + + + + + + + + + + +
+ + + Homekit Device Stack.
+ Version: 4.0.0


+ Homekit Device Stack, is not affiliated with, or endoresed by Apple Inc. +
+ + + + + + + + + + + + + +
Username
Password
 
+ +
+ + + + + \ No newline at end of file diff --git a/ui/main.-OLD.tpl b/ui/main.-OLD.tpl new file mode 100644 index 0000000..23ed76c --- /dev/null +++ b/ui/main.-OLD.tpl @@ -0,0 +1,341 @@ + + + + + Homekit Device Stack + + + + + + + + + + +
Homekit Device Stack + + Settings  + +
+
+
+
+ Devices (Double click to edit) +
+ Create New + Accessory +
+
+ {{#Config.accessories}} +
+ + + + + +
+ {{name}}
+ Device ID : {{accessoryID}}
+ Serial ID : {{serialNumber}} +
+
+ + {{/Config.accessories}} +
+
+
+ Outgoing Routes (Double click to edit) +
+ Create New + Route +
+
+ {{#Routes}} +
+ + + + + +
+ + + {{name}}
+ {{#ifvalue value.type equals="HTTP"}} + {{value.destinationURI}} + {{/ifvalue}} + + {{#ifvalue value.type equals="MQTT"}} + {{value.broker}}
+ {{value.topic}} + {{/ifvalue}} + + {{#ifvalue value.type equals="FILE"}} + {{value.directory}} + {{/ifvalue}} + + {{#ifvalue value.type equals="UDP"}} + {{value.address}}:{{value.port}} + {{/ifvalue}} + + {{#ifvalue value.type equals="WEBSOCKET"}} + {{value.uri}} + {{/ifvalue}} +
+
+ + {{/Routes}} +
+
+ + + + + + + + + + \ No newline at end of file diff --git a/ui/main.tpl b/ui/main.tpl new file mode 100644 index 0000000..a46dc99 --- /dev/null +++ b/ui/main.tpl @@ -0,0 +1,38 @@ + + + + + + Homekit Device Stack + + + + + + + + + + +
+ +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/ui/setup.tpl b/ui/setup.tpl new file mode 100644 index 0000000..3ff6b45 --- /dev/null +++ b/ui/setup.tpl @@ -0,0 +1,64 @@ + + + + + Homekit Device Stack + + + + + + + + +
+
+ + + + + + + +
+
HomeKit Device Stack
Accessory Bridge +
+
+
+ HomeKit Device Stack can act as a HomeKit Bridge. That is, its a kind of hub that has smart + accessories attached. you can publish your devices either attached to, or separate from the bridge. +
+
    +
  1. On your iOS device open the 'Home' app
  2. +
  3. Click the '+' button (top right) and choose 'Add Accessory'
  4. +
  5. Scan this QR Code
  6. +
  7. The Home app will guide you through in setting up a Test Device (Switch + Accessory Demo) which is attached to the bridge. +
  8. +
+ Once paired, your devices will be listed here. If you have problems in using the QR + Code, click 'I Don't have a Code or Cannot Scan' you will see 'HomeKit Device Stack', + click it, and you will be asked for a pin, enter the pin code displayed here. +
+
+
+
+
{{Config.bridgeConfig.pincode}} +
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/ui/static/Images/BG.png b/ui/static/Images/BG.png new file mode 100644 index 0000000..7a6d737 Binary files /dev/null and b/ui/static/Images/BG.png differ diff --git a/ui/static/Images/HKDS.png b/ui/static/Images/HKDS.png new file mode 100644 index 0000000..6b6993d Binary files /dev/null and b/ui/static/Images/HKDS.png differ diff --git a/ui/static/Images/Logo.png b/ui/static/Images/Logo.png new file mode 100644 index 0000000..ad48cea Binary files /dev/null and b/ui/static/Images/Logo.png differ diff --git a/ui/static/Images/device_icons/Ac_ALARM.png b/ui/static/Images/device_icons/Ac_ALARM.png new file mode 100644 index 0000000..95bfbb2 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_ALARM.png differ diff --git a/ui/static/Images/device_icons/Ac_CAMERA.png b/ui/static/Images/device_icons/Ac_CAMERA.png new file mode 100644 index 0000000..909f4f3 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_CAMERA.png differ diff --git a/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png b/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png new file mode 100644 index 0000000..1c5a0d0 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_CONTACT_SENSOR.png differ diff --git a/ui/static/Images/device_icons/Ac_FAN.png b/ui/static/Images/device_icons/Ac_FAN.png new file mode 100644 index 0000000..1459aec Binary files /dev/null and b/ui/static/Images/device_icons/Ac_FAN.png differ diff --git a/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png b/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png new file mode 100644 index 0000000..00da7af Binary files /dev/null and b/ui/static/Images/device_icons/Ac_GARAGE_DOOR.png differ diff --git a/ui/static/Images/device_icons/Ac_LIGHTBULB.png b/ui/static/Images/device_icons/Ac_LIGHTBULB.png new file mode 100644 index 0000000..6814fd5 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LIGHTBULB.png differ diff --git a/ui/static/Images/device_icons/Ac_LOCK.png b/ui/static/Images/device_icons/Ac_LOCK.png new file mode 100644 index 0000000..0cf9ec6 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LOCK.png differ diff --git a/ui/static/Images/device_icons/Ac_Leak.png b/ui/static/Images/device_icons/Ac_Leak.png new file mode 100644 index 0000000..f5f622b Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Leak.png differ diff --git a/ui/static/Images/device_icons/Ac_LightSensor.png b/ui/static/Images/device_icons/Ac_LightSensor.png new file mode 100644 index 0000000..437cb8a Binary files /dev/null and b/ui/static/Images/device_icons/Ac_LightSensor.png differ diff --git a/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png b/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png new file mode 100644 index 0000000..a7eca6d Binary files /dev/null and b/ui/static/Images/device_icons/Ac_MOTION_SENSOR.png differ diff --git a/ui/static/Images/device_icons/Ac_OUTLET.png b/ui/static/Images/device_icons/Ac_OUTLET.png new file mode 100755 index 0000000..81d3599 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_OUTLET.png differ diff --git a/ui/static/Images/device_icons/Ac_SWITCH.png b/ui/static/Images/device_icons/Ac_SWITCH.png new file mode 100755 index 0000000..1248736 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_SWITCH.png differ diff --git a/ui/static/Images/device_icons/Ac_Smoke.png b/ui/static/Images/device_icons/Ac_Smoke.png new file mode 100644 index 0000000..17ec7a7 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Smoke.png differ diff --git a/ui/static/Images/device_icons/Ac_THERMOSTAT.png b/ui/static/Images/device_icons/Ac_THERMOSTAT.png new file mode 100644 index 0000000..4f21508 Binary files /dev/null and b/ui/static/Images/device_icons/Ac_THERMOSTAT.png differ diff --git a/ui/static/Images/device_icons/Ac_TV.png b/ui/static/Images/device_icons/Ac_TV.png new file mode 100755 index 0000000..c24eeba Binary files /dev/null and b/ui/static/Images/device_icons/Ac_TV.png differ diff --git a/ui/static/Images/device_icons/Ac_Temp.png b/ui/static/Images/device_icons/Ac_Temp.png new file mode 100644 index 0000000..c6c02dd Binary files /dev/null and b/ui/static/Images/device_icons/Ac_Temp.png differ diff --git a/ui/static/Images/route_icons/FILE.png b/ui/static/Images/route_icons/FILE.png new file mode 100644 index 0000000..8620d00 Binary files /dev/null and b/ui/static/Images/route_icons/FILE.png differ diff --git a/ui/static/Images/route_icons/HTTP.png b/ui/static/Images/route_icons/HTTP.png new file mode 100644 index 0000000..97f08f1 Binary files /dev/null and b/ui/static/Images/route_icons/HTTP.png differ diff --git a/ui/static/Images/route_icons/MQTT.png b/ui/static/Images/route_icons/MQTT.png new file mode 100644 index 0000000..0269f28 Binary files /dev/null and b/ui/static/Images/route_icons/MQTT.png differ diff --git a/ui/static/Images/route_icons/UDP.png b/ui/static/Images/route_icons/UDP.png new file mode 100644 index 0000000..58107b3 Binary files /dev/null and b/ui/static/Images/route_icons/UDP.png differ diff --git a/ui/static/Images/route_icons/WEBSOCKET.png b/ui/static/Images/route_icons/WEBSOCKET.png new file mode 100644 index 0000000..cb64ce5 Binary files /dev/null and b/ui/static/Images/route_icons/WEBSOCKET.png differ diff --git a/ui/static/JS/functions.js b/ui/static/JS/functions.js new file mode 100755 index 0000000..7fcf685 --- /dev/null +++ b/ui/static/JS/functions.js @@ -0,0 +1,549 @@ +let EndPoints = [] +let Connectors = [] + +// Login +function Login() { + let Data = { + "username": $('#TXT_Username').val(), + "password": $('#TXT_Password').val(), + } + $.ajax({ + type: "POST", + url: "../../../ui/login", + data: JSON.stringify(Data), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: LoginDone + }); + +} + +function LoginDone(data) { + if (data.success) { + document.location = data.destination; + } else { + $('#Message').text('Invalid username and/or password'); + } +} + +// delete route() +function DeleteRoute() { + if (confirm("Are you sure you wish to delete this route?")) { + let Name = $("#RDelete").attr("route_name"); + + $.ajax({ + type: "POST", + data: JSON.stringify({ + "name": "" + Name + "" + }), + contentType: "application/json", + url: "../../../ui/deleteroute", + dataType: "json", + success: ByPass + }); + } +} + +// save config +function SaveConfig() { + let Data = { + "webInterfacePort": parseInt($('#CFG_Port').val()), + "advertiser": $('#CFG_Advertiser').val(), + "MQTTBroker": $('#CFG_Broker').val(), + "MQTTTopic": $('#CFG_Topic').val(), + "MQTTOptions": { + "username": "" + $('#CFG_username').val() + "", + "password": "" + $('#CFG_password').val() + "" + }, + "enableIncomingMQTT": "" + ($('#CFG_MQTT').prop("checked") == true) + "", + "interface": $("#CFG_Interface").val(), + "webInterfaceAddress": $("#CFG_Address").val() + } + + $.ajax({ + type: "POST", + url: "../../../ui/setconfig", + data: JSON.stringify(Data), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: function() { + alert('Please restart the server to apply the new configuration.') + } + }); +} + +// Restore +function StartRestore() { + if (confirm('Are you sure you wish to restore your configuration. This will include all Homekit client mappings?')) { + let IP = document.createElement("input"); + IP.setAttribute("type", "file"); + IP.onchange = ProcessRestore; + IP.click(); + + } +} + +function ProcessRestore(input) { + let FR = new FileReader(); + + FR.onload = function() { + $.ajax({ + type: "POST", + data: JSON.stringify({ + "content": "" + FR.result + "" + }), + contentType: "application/json", + url: "../../../ui/restore", + dataType: "json", + success: RestoreDone + }); + + } + FR.readAsText(input.target.files[0]); + +} + +function RestoreDone(data) { + if (data.OK) { + alert('Configuration Restored! The server has been shutdown - please start it again to apply the Configuration.') + } else { + if (data.Reason == "VersionMismatch") { + alert('This backup is not compatible with the version of HomeKit Devcie Stack Installed.') + } else { + alert('The backup is invalid.') + } + + } +} + +// Device Setup +function StartPairCheck(ID) { + setInterval(() => { + DoCheckDevice(ID) + }, 5000); +} + +function DoCheckDevice(ID) { + $.ajax({ + type: "GET", + url: "../../../ui/pairstatus/" + ID, + dataType: "json", + success: Check + }); +} + +// Bridge Setup +function StartIntervalCheck() { + setInterval(DoCheck, 5000); +} + +function DoCheck() { + $.ajax({ + type: "GET", + url: "../../../ui/pairstatus", + dataType: "json", + success: Check + }); +} + +function Check(data) { + if (data.paired) { + document.location = '../../../ui/main' + } +} + +// Root Type Change +function RouteTypeChanged() { + + let Type = $('#R_Type').val(); + + $.ajax({ + + type: "GET", + url: "../../../ui/getroutemeta/"+Type, + dataType: "json", + success: function(data){ + + let Anchor = $("#Anchor") + + $(".param_row").remove() + + data.Inputs.reverse() + + data.Inputs.forEach((I) =>{ + + + Anchor.after(''+I.label+'') + + + }) + + } + }); + + + +} + +// Populate Route Data +function ProcessRouteData() { + let Prop = JSON.parse($(event.target).attr('properties').replace(/\|/g, '"')); + + $('#R_Type').val(Prop.type) + TypeChanged(); + $("#R_Name").val($(event.target).attr("id").split("_")[1]) + + $("#RDelete").attr("route_name", $("#R_Name").val()); + $("#RDelete").css("visibility", "visible") + + switch (Prop.type) { + case "HTTP": + $("#R_HTTP_URI").val(Prop.destinationURI); + break; + + case "UDP": + $("#R_UDP_ADDRESS").val(Prop.address + ":" + Prop.port) + break; + + case "FILE": + $("#R_FILE_DIRECTORY").val(Prop.directory); + break; + + case "WEBSOCKET": + $("#R_WEBSOCKET_URI").val(Prop.uri); + break; + + case "MQTT": + $("#R_MQTT_BROKER").val(Prop.broker); + $("#R_MQTT_TOPIC").val(Prop.topic); + if (Prop.MQTTOptions != null) { + $("#R_MQTT_USER").val(Prop.MQTTOptions.username) + $("#R_MQTT_PASS").val(Prop.MQTTOptions.password) + } + break; + } + +} + +function ClearRoute() { + $('#R_Type').val("HTTP") + $("#R_Name").val("") + $("#R_FILE_DIRECTORY").val(""); + $("#R_HTTP_URI").val(""); + $("#R_UDP_ADDRESS").val("") + $("#R_MQTT_BROKER").val("") + $("#R_MQTT_TOPIC").val("") + $("#R_MQTT_USER").val("") + $("#R_MQTT_PASS").val("") + $("#R_WEBSOCKET_URI").val(""); + + $("#RDelete").css("visibility", "hidden") + + TypeChanged(); +} + +function Show(ID, CB) { + + $('#' + ID).css('display', 'block') + if (CB) { + CB(); + } +} + +function Hide(ID, CB) { + $('#' + ID).css('display', 'none') + if (CB) { + CB(); + } +} + +function PopulatePrototype() { + $('#TypeIcon').attr('src', '../../../ui/static/Images/device_icons/' + Prototype.Icon); + $('#Label').text(Prototype.Label); + + for (let i = 0; i < Prototype.ConfigProperties.length; i++) { + let Prop = Prototype.ConfigProperties[i]; + + // Table Row + let TR = $('') + + // Label + let LTD = $('') + LTD.css('text-align', 'left') + LTD.html(Prop.Label); + + // Input + let ITD = $('') + ITD.css('text-align', 'right') + + var ITE; + switch (Prop.Type) { + case "text": + ITE = $(''); + ITE.attr('type', 'text'); + ITE.css('width', '90%'); + ITE.attr('config-name', Prop.Name); + if (Prop.hasOwnProperty('Default')) { + ITE.val(Prop.Default); + } + break; + + case "multi": + ITE = $(''); + ITE.css('width', '90%'); + ITE.css('height', '120px'); + ITE.attr('config-name', Prop.Name); + if (Prop.hasOwnProperty('Default')) { + for (let i = 0; i < Prop.Default.length; i++) { + let CV = ITE.val(); + ITE.val(CV + Prop.Default[i] + '\r\n') + } + } + break; + + case "choice": + ITE = $(''); + ITE.css('width', '90%'); + ITE.attr('config-name', Prop.Name); + for (let i = 0; i < Prop.Choices.length; i++) { + let Op = $(''); + Op.attr('value', Prop.Choices[i]) + Op.append(Prop.Choices[i]) + + ITE.append(Op); + } + + if (Prop.hasOwnProperty('Default')) { + ITE.val(Prop.Default) + + } + + break; + + case "checkbox": + ITE = $(''); + ITE.attr('type', 'checkbox'); + ITE.attr('config-name', Prop.Name); + if (Prop.hasOwnProperty('Default')) { + if (Prop.Default == 'true') { + ITE.attr('checked', 'checked'); + } + } + break; + } + + ITD.append(ITE); + + TR.append(LTD); + TR.append(ITD); + + $('#Form').append(TR); + + } +} + +function PopulateData() { + $('#AName').text(ConfiguredObject.name) + + for (let i = 0; i < Prototype.ConfigProperties.length; i++) { + + let Name = Prototype.ConfigProperties[i].Name; + let Type = Prototype.ConfigProperties[i].Type; + + if (Type == 'multi') { + $("[config-name='" + Name + "']").val(''); + for (let i = 0; i < ConfiguredObject[Name].length; i++) { + + let CV = $("[config-name='" + Name + "']").val(); + $("[config-name='" + Name + "']").val(CV + ConfiguredObject[Name][i] + '\r\n'); + } + + } + + if (Type == 'checkbox') { + if (ConfiguredObject[Name] == 'true') { + $("[config-name='" + Name + "']").prop("checked", true); + } else { + $("[config-name='" + Name + "']").prop("checked", false); + } + + } + + if (Type == 'choice') { + $("[config-name='" + Name + "']").val(ConfiguredObject[Name]); + } + + if (Type == 'text') { + $("[config-name='" + Name + "']").val(ConfiguredObject[Name]); + } + + } + +} + +function DeleteAccesssory() { + + if (!confirm('Are you sure you wish to delete \'' + ConfiguredObject['name'] + '\' This action cannot be undone.')) { + return + } + + let AccessoryOBJ = {} + AccessoryOBJ['username'] = ConfiguredObject['username'] + AccessoryOBJ['bridged'] = ConfiguredObject['bridged'] + + $.ajax({ + + type: "POST", + url: "../../../ui/deleteaccessory", + data: JSON.stringify(AccessoryOBJ), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: ByPass + }); +} + +function UpdateAccessory() { + + let Props = $('[config-name]'); + let AccessoryOBJ = {} + + AccessoryOBJ.bridged = ConfiguredObject.bridged; + AccessoryOBJ.type = ConfiguredObject.type; + AccessoryOBJ.route = ConfiguredObject.route; + AccessoryOBJ.pincode = ConfiguredObject.pincode; + AccessoryOBJ.username = ConfiguredObject.username; + AccessoryOBJ.setupID = ConfiguredObject.setupID; + AccessoryOBJ.serialNumber = ConfiguredObject.serialNumber; + + Props.each(function() { + if ($(this).is('textarea')) { + + let lines = $(this).val().replace(/\r\n/g, "\n").split("\n"); + lines = lines.filter(L => L.length > 0); + AccessoryOBJ[$(this).attr('config-name')] = lines; + } + + if ($(this).attr('type') == 'checkbox') { + + AccessoryOBJ[$(this).attr('config-name')] = $(this).is(':checked').toString(); + } else { + AccessoryOBJ[$(this).attr('config-name')] = $(this).val(); + } + + }) + + $.ajax({ + + type: "POST", + url: "../../../ui/editaccessory", + data: JSON.stringify(AccessoryOBJ), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: ByPass + }); + +} + +function SaveAccessory(Bridged) { + let Props = $('[config-name]'); + + let AccessoryOBJ = {} + + AccessoryOBJ.bridged = Bridged; + AccessoryOBJ.type = Prototype.Name; + AccessoryOBJ.route = '' + + Props.each(function() { + if ($(this).is('textarea')) { + + let lines = $(this).val().replace(/\r\n/g, "\n").split("\n"); + lines = lines.filter(L => L.length > 0); + AccessoryOBJ[$(this).attr('config-name')] = lines; + } + + if ($(this).attr('type') == 'checkbox') { + + AccessoryOBJ[$(this).attr('config-name')] = $(this).is(':checked').toString(); + } else { + AccessoryOBJ[$(this).attr('config-name')] = $(this).val(); + } + + }) + + $.ajax({ + + type: "POST", + url: "../../../ui/createaccessory", + data: JSON.stringify(AccessoryOBJ), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: GoToMain + }); +} + +function ByPass() { + document.location = '../../../ui/main'; +} + +function GoToMain(Data) { + if (Data.hasOwnProperty("Pin")) { + + $("#NBCode").text(Data.Pin); + + new QRCode(document.getElementById("qrcode"), { + "text": Data.URI, + width: 125, + height: 125 + }); + + Show('QR') + + StartPairCheck(Data.ID); + } else { + document.location = '../../../ui/main'; + } + +} + +function SaveRoute() { + + let RouteType = $('#R_Type').val(); + + let Data = {}; + + Data["name"] = $('#R_Name').val(); + Data["type"] = RouteType; + + let Params = $(".config_param"); + + Params.each(function(index){ + + Data[$(this).attr('param')] = $(this).val(); + }) + + + $.ajax({ + type: "POST", + url: "../../../ui/createroute", + data: JSON.stringify(Data), + contentType: "application/json; charset=utf-8", + dataType: "json", + success: ByPass + }); + +} + +function RouteCMD(Data, Type) { + $.ajax({ + type: "POST", + url: "../../../ui/" + Type, + data: JSON.stringify({ + SID: Data.sourceId, + TID: Data.targetId + }), + contentType: "application/json; charset=utf-8", + dataType: "json" + + }); +} + diff --git a/ui/static/JS/jquery.connections.js b/ui/static/JS/jquery.connections.js new file mode 100644 index 0000000..4d400b1 --- /dev/null +++ b/ui/static/JS/jquery.connections.js @@ -0,0 +1,250 @@ +(function($) { + $.fn.connections = function(options) { + if (options === "update") { + return processConnections(update, this); + } else if (options === "remove") { + return processConnections(destroy, this); + } else { + options = $.extend( + true, + { + borderClasses: {}, + class: "connection", + css: {}, + from: this, + tag: "connection", + to: this, + within: ":root" + }, + options + ); + connect(options); + return this; + } + }; + + $.event.special.connections = { + teardown: function(namespaces) { + processConnections(destroy, $(this)); + } + }; + + var connect = function(options) { + var borderClasses = options.borderClasses; + var tag = options.tag; + var end1 = $(options.from); + var end2 = $(options.to); + var within = $(options.within); + delete options.borderClasses; + delete options.tag; + delete options.from; + delete options.to; + delete options.within; + within.each(function() { + var container = this; + var done = new Array(); + end1.each(function() { + var node = this; + done.push(this); + end2.not(done).each(function() { + createConnection( + container, + [node, this], + tag, + borderClasses, + options + ); + }); + }); + }); + }; + + var createConnection = function( + container, + nodes, + tag, + borderClasses, + options + ) { + var css = $.extend({ position: "absolute" }, options.css); + var connection = $("<" + tag + "/>", options).css(css); + connection.appendTo(container); + + var border_w = (connection.outerWidth() - connection.innerWidth()) / 2; + var border_h = (connection.outerHeight() - connection.innerHeight()) / 2; + + if (border_w <= 0 && border_h <= 0) { + border_w = border_h = 1; + } + + var data = { + borderClasses: borderClasses, + border_h: border_h, + border_w: border_w, + node_from: $(nodes[0]), + node_to: $(nodes[1]), + nodes_dom: nodes, + css: css + }; + + if ("none" === connection.css("border-top-style")) { + data.css.borderStyle = "solid"; + } + $.data(connection.get(0), "connection", data); + $.data(connection.get(0), "connections", [connection.get(0)]); + for (var i = 0; i < 2; i++) { + var connections = connection.add($.data(nodes[i], "connections")).get(); + $.data(nodes[i], "connections", connections); + if (connections.length == 1) { + $(nodes[i]).on("connections.connections", false); + } + } + update(connection.get(0)); + }; + + var destroy = function(connection) { + var nodes = $.data(connection, "connection").nodes_dom; + for (var i = 0; i < 2; i++) { + var connections = $($.data(nodes[i], "connections")) + .not(connection) + .get(); + $.data(nodes[i], "connections", connections); + } + $(connection).remove(); + }; + + var getState = function(data) { + data.rect_from = data.nodes_dom[0].getBoundingClientRect(); + data.rect_to = data.nodes_dom[1].getBoundingClientRect(); + var cached = data.cache; + data.cache = [ + data.rect_from.top, + data.rect_from.right, + data.rect_from.bottom, + data.rect_from.left, + data.rect_to.top, + data.rect_to.right, + data.rect_to.bottom, + data.rect_to.left + ]; + data.hidden = + 0 === (data.cache[0] | data.cache[1] | data.cache[2] | data.cache[3]) || + 0 === (data.cache[4] | data.cache[5] | data.cache[6] | data.cache[7]); + data.unmodified = true; + if (cached === undefined) { + return (data.unmodified = false); + } + for (var i = 0; i < 8; i++) { + if (cached[i] !== data.cache[i]) { + return (data.unmodified = false); + } + } + }; + + var update = function(connection) { + var data = $.data(connection, "connection"); + getState(data); + if (data.unmodified) { + return; + } + var border_h = data.border_h; + var border_w = data.border_w; + var from = data.rect_from; + var to = data.rect_to; + var b = (from.bottom + from.top) / 2; + var r = (to.left + to.right) / 2; + var t = (to.bottom + to.top) / 2; + var l = (from.left + from.right) / 2; + + var h = ["right", "left"]; + if (l > r) { + h = ["left", "right"]; + var x = Math.max(r - border_w / 2, Math.min(from.right, to.right)); + r = l + border_w / 2; + l = x; + } else { + l -= border_w / 2; + r = Math.min(r + border_w / 2, Math.max(from.left, to.left)); + } + var v = ["bottom", "top"]; + if (t > b) { + v = ["top", "bottom"]; + var x = Math.max(b - border_h / 2, Math.min(from.bottom, to.bottom)); + b = t + border_h / 2; + t = x; + } else { + b = Math.min(b, Math.max(from.top, to.top)); + t -= border_h / 2; + } + var width = r - l; + var height = b - t; + if (width < border_w) { + t = Math.max(t, Math.min(from.bottom, to.bottom)); + b = Math.min(b, Math.max(from.top, to.top)); + l = Math.max(from.left, to.left); + r = Math.min(from.right, to.right); + r = l = (l + r - border_w) / 2; + } + if (height < border_h) { + l = Math.max(l, Math.min(from.right, to.right)); + r = Math.min(r, Math.max(from.left, to.left)); + t = Math.max(from.top, to.top); + b = Math.min(from.bottom, to.bottom); + b = t = (t + b - border_h) / 2; + } + width = r - l; + height = b - t; + width <= 0 && (border_h = 0); + height <= 0 && (border_w = 0); + var style = + "border-" + + v[0] + + "-" + + h[0] + + "-radius: 0;" + + "border-" + + v[0] + + "-" + + h[1] + + "-radius: 0;" + + "border-" + + v[1] + + "-" + + h[0] + + "-radius: 0;"; + (border_h <= 0 || border_w <= 0) && + (style += "border-" + v[1] + "-" + h[1] + "-radius: 0;"); + if (data.hidden) { + style += "display: none;"; + } else { + data.css["border-" + v[0] + "-width"] = 0; + data.css["border-" + h[0] + "-width"] = 0; + data.css["border-" + v[1] + "-width"] = border_h; + data.css["border-" + h[1] + "-width"] = border_w; + var current_rect = connection.getBoundingClientRect(); + data.css.left = connection.offsetLeft + l - current_rect.left; + data.css.top = connection.offsetTop + t - current_rect.top; + data.css.width = width - border_w; + data.css.height = height - border_h; + } + var bc = data.borderClasses; + $(connection) + .removeClass(bc[v[0]]) + .removeClass(bc[h[0]]) + .addClass(bc[v[1]]) + .addClass(bc[h[1]]) + .attr("style", style) + .css(data.css); + }; + + var processConnections = function(method, elements) { + return elements.each(function() { + var connections = $.data(this, "connections"); + if (connections instanceof Array) { + for (var i = 0, len = connections.length; i < len; i++) { + method(connections[i]); + } + } + }); + }; +})(jQuery); diff --git a/ui/static/JS/jsplumb.min.js b/ui/static/JS/jsplumb.min.js new file mode 100644 index 0000000..70d8b38 --- /dev/null +++ b/ui/static/JS/jsplumb.min.js @@ -0,0 +1,7 @@ +(function(){function a(a,b){return[-a[0][b]+3*a[1][b]+-3*a[2][b]+a[3][b],3*a[0][b]-6*a[1][b]+3*a[2][b],-3*a[0][b]+3*a[1][b],a[0][b]]}function b(b){return[a(b,"x"),a(b,"y")]}function c(a){return 0>a?-1:a>0?1:0}function d(a,b,d,e){var f,g,h=b/a,i=d/a,j=e/a,k=(3*i-Math.pow(h,2))/9,l=(9*h*i-27*j-2*Math.pow(h,3))/54,m=Math.pow(k,3)+Math.pow(l,2),n=[];if(m>=0)f=c(l+Math.sqrt(m))*Math.pow(Math.abs(l+Math.sqrt(m)),1/3),g=c(l-Math.sqrt(m))*Math.pow(Math.abs(l-Math.sqrt(m)),1/3),n[0]=-h/3+(f+g),n[1]=-h/3-(f+g)/2,n[2]=-h/3-(f+g)/2,0!==Math.abs(Math.sqrt(3)*(f-g)/2)&&(n[1]=-1,n[2]=-1);else{var o=Math.acos(l/Math.sqrt(-Math.pow(k,3)));n[0]=2*Math.sqrt(-k)*Math.cos(o/3)-h/3,n[1]=2*Math.sqrt(-k)*Math.cos((o+2*Math.PI)/3)-h/3,n[2]=2*Math.sqrt(-k)*Math.cos((o+4*Math.PI)/3)-h/3}for(var p=0;3>p;p++)(n[p]<0||n[p]>1)&&(n[p]=-1);return n}"undefined"==typeof Math.sgn&&(Math.sgn=function(a){return 0==a?0:a>0?1:-1});var e={subtract:function(a,b){return{x:a.x-b.x,y:a.y-b.y}},dotProduct:function(a,b){return a.x*b.x+a.y*b.y},square:function(a){return Math.sqrt(a.x*a.x+a.y*a.y)},scale:function(a,b){return{x:a.x*b,y:a.y*b}}},f=64,g=Math.pow(2,-f-1),h=function(a,b){for(var c=[],d=j(a,b),f=b.length-1,g=2*f-1,h=k(d,g,c,0),i=e.subtract(a,b[0]),l=e.square(i),m=0,n=0;h>n;n++){i=e.subtract(a,o(b,f,c[n],null,null));var p=e.square(i);l>p&&(l=p,m=c[n])}return i=e.subtract(a,b[f]),p=e.square(i),l>p&&(l=p,m=1),{location:m,distance:l}},i=function(a,b){var c=h(a,b);return{point:o(b,b.length-1,c.location,null,null),location:c.location}},j=function(a,b){for(var c=b.length-1,d=2*c-1,f=[],g=[],h=[],i=[],j=[[1,.6,.3,.1],[.4,.6,.6,.4],[.1,.3,.6,1]],k=0;c>=k;k++)f[k]=e.subtract(b[k],a);for(var k=0;c-1>=k;k++)g[k]=e.subtract(b[k+1],b[k]),g[k]=e.scale(g[k],3);for(var l=0;c-1>=l;l++)for(var m=0;c>=m;m++)h[l]||(h[l]=[]),h[l][m]=e.dotProduct(g[l],f[m]);for(k=0;d>=k;k++)i[k]||(i[k]=[]),i[k].y=0,i[k].x=parseFloat(k)/d;for(var n=c,o=c-1,p=0;n+o>=p;p++){var q=Math.max(0,p-o),r=Math.min(p,n);for(k=q;r>=k;k++){var s=p-k;i[k+s].y+=h[s][k]*j[s][k]}}return i},k=function(a,b,c,d){var e,g,h=[],i=[],j=[],p=[];switch(l(a,b)){case 0:return 0;case 1:if(d>=f)return c[0]=(a[0].x+a[b].x)/2,1;if(m(a,b))return c[0]=n(a,b),1}o(a,b,.5,h,i),e=k(h,b,j,d+1),g=k(i,b,p,d+1);for(var q=0;e>q;q++)c[q]=j[q];for(var q=0;g>q;q++)c[q+e]=p[q];return e+g},l=function(a,b){var c,d,e=0;c=d=Math.sgn(a[0].y);for(var f=1;b>=f;f++)c=Math.sgn(a[f].y),c!=d&&e++,d=c;return e},m=function(a,b){var c,d,e,f,h,i,j,k,l,m,n,o,p,q,r,s;i=a[0].y-a[b].y,j=a[b].x-a[0].x,k=a[0].x*a[b].y-a[b].x*a[0].y;var t,u;t=u=0;for(var v=1;b>v;v++){var w=i*a[v].x+j*a[v].y+k;w>t?t=w:u>w&&(u=w)}return n=0,o=1,p=0,q=i,r=j,s=k-t,l=n*r-q*o,m=1/l,d=(o*s-r*p)*m,q=i,r=j,s=k-u,l=n*r-q*o,m=1/l,e=(o*s-r*p)*m,f=Math.min(d,e),h=Math.max(d,e),c=h-f,g>c?1:0},n=function(a,b){var c=1,d=0,e=a[b].x-a[0].x,f=a[b].y-a[0].y,g=a[0].x-0,h=a[0].y-0,i=e*d-f*c,j=1/i,k=(e*h-f*g)*j;return 0+c*k},o=function(a,b,c,d,e){for(var f=[[]],g=0;b>=g;g++)f[0][g]=a[g];for(var h=1;b>=h;h++)for(var g=0;b-h>=g;g++)f[h]||(f[h]=[]),f[h][g]||(f[h][g]={}),f[h][g].x=(1-c)*f[h-1][g].x+c*f[h-1][g+1].x,f[h][g].y=(1-c)*f[h-1][g].y+c*f[h-1][g+1].y;if(null!=d)for(g=0;b>=g;g++)d[g]=f[g][0];if(null!=e)for(g=0;b>=g;g++)e[g]=f[b-g][g];return f[b][0]},p={},q=function(a){var b=p[a];if(!b){b=[];var c=function(){return function(b){return Math.pow(b,a)}},d=function(){return function(b){return Math.pow(1-b,a)}},e=function(a){return function(b){return a}},f=function(){return function(a){return a}},g=function(){return function(a){return 1-a}},h=function(a){return function(b){for(var c=1,d=0;di;i++){for(var j=[new e(a)],k=0;a-i>k;k++)j.push(new f);for(var k=0;i>k;k++)j.push(new g);b.push(new h(j))}b.push(new d),p[a]=b}return b},r=function(a,b){for(var c=q(a.length-1),d=0,e=0,f=0;f0?1:-1,h=null;ee;)e+=.005*f,g=r(a,e),d+=s(g,c),c=g;return console.log("length",(new Date).getTime()-b),d},w=function(a,b,c){return u(a,b,c).point},x=function(a,b,c){return u(a,b,c).location},y=function(a,b){var c=r(a,b),d=r(a.slice(0,a.length-1),b),e=d.y-c.y,f=d.x-c.x;return 0===e?1/0:Math.atan(e/f)},z=function(a,b,c){var d=u(a,b,c);return d.location>1&&(d.location=1),d.location<0&&(d.location=0),y(a,d.location)},A=function(a,b,c,d){d=null==d?0:d;var e=u(a,b,d),f=y(a,e.location),g=Math.atan(-1/f),h=c/2*Math.sin(g),i=c/2*Math.cos(g);return[{x:e.point.x+i,y:e.point.y+h},{x:e.point.x-i,y:e.point.y-h}]},B=function(a,c,e,f,g){var h=f-c,i=a-e,j=a*(c-f)+c*(e-a),k=b(g),l=[h*k[0][0]+i*k[1][0],h*k[0][1]+i*k[1][1],h*k[0][2]+i*k[1][2],h*k[0][3]+i*k[1][3]+j],m=d.apply(null,l),n=[];if(null!=m)for(var o=0;3>o;o++){var p,q=m[o],r=Math.pow(q,2),s=Math.pow(q,3),t=[k[0][0]*s+k[0][1]*r+k[0][2]*q+k[0][3],k[1][0]*s+k[1][1]*r+k[1][2]*q+k[1][3]];p=e-a!==0?(t[0]-a)/(e-a):(t[1]-c)/(f-c),q>=0&&1>=q&&p>=0&&1>=p&&n.push(t)}return n},C=function(a,b,c,d,e){var f=[];return f.push.apply(f,B(a,b,a+c,b,e)),f.push.apply(f,B(a+c,b,a+c,b+d,e)),f.push.apply(f,B(a+c,b+d,a,b+d,e)),f.push.apply(f,B(a,b+d,a,b,e)),f},D=function(a,b){var c=[];return c.push.apply(c,B(a.x,a.y,a.x+a.w,a.y,b)),c.push.apply(c,B(a.x+a.w,a.y,a.x+a.w,a.y+a.h,b)),c.push.apply(c,B(a.x+a.w,a.y+a.h,a.x,a.y+a.h,b)),c.push.apply(c,B(a.x,a.y+a.h,a.x,a.y,b)),c},E=this.jsBezier={distanceFromCurve:h,gradientAtPoint:y,gradientAtPointAlongCurveFrom:z,nearestPointOnCurve:i,pointOnCurve:r,pointAlongCurveFrom:w,perpendicularToCurveAt:A,locationAlongCurveFrom:x,getLength:v,lineIntersection:B,boxIntersection:C,boundingBoxIntersection:D,version:"0.9.0"};"undefined"!=typeof exports&&(exports.jsBezier=E)}).call("undefined"!=typeof window?window:this),function(){"use strict";var a=this,b=a.Biltong={version:"0.4.0"};"undefined"!=typeof exports&&(exports.Biltong=b);var c=function(a){return"[object Array]"===Object.prototype.toString.call(a)},d=function(a,b,d){return a=c(a)?a:[a.x,a.y],b=c(b)?b:[b.x,b.y],d(a,b)},e=b.gradient=function(a,b){return d(a,b,function(a,b){return b[0]==a[0]?b[1]>a[1]?1/0:-(1/0):b[1]==a[1]?b[0]>a[0]?0:-0:(b[1]-a[1])/(b[0]-a[0])})},f=(b.normal=function(a,b){return-1/e(a,b)},b.lineLength=function(a,b){return d(a,b,function(a,b){return Math.sqrt(Math.pow(b[1]-a[1],2)+Math.pow(b[0]-a[0],2))})},b.quadrant=function(a,b){return d(a,b,function(a,b){return b[0]>a[0]?b[1]>a[1]?2:1:b[0]==a[0]?b[1]>a[1]?2:1:b[1]>a[1]?3:4})}),g=(b.theta=function(a,b){return d(a,b,function(a,b){var c=e(a,b),d=Math.atan(c),g=f(a,b);return(4==g||3==g)&&(d+=Math.PI),0>d&&(d+=2*Math.PI),d})},b.intersects=function(a,b){var c=a.x,d=a.x+a.w,e=a.y,f=a.y+a.h,g=b.x,h=b.x+b.w,i=b.y,j=b.y+b.h;return g>=c&&d>=g&&i>=e&&f>=i||h>=c&&d>=h&&i>=e&&f>=i||g>=c&&d>=g&&j>=e&&f>=j||h>=c&&d>=g&&j>=e&&f>=j||c>=g&&h>=c&&e>=i&&j>=e||d>=g&&h>=d&&e>=i&&j>=e||c>=g&&h>=c&&f>=i&&j>=f||d>=g&&h>=c&&f>=i&&j>=f},b.encloses=function(a,b,c){var d=a.x,e=a.x+a.w,f=a.y,g=a.y+a.h,h=b.x,i=b.x+b.w,j=b.y,k=b.y+b.h,l=function(a,b,d,e){return c?b>=a&&d>=e:b>a&&d>e};return l(d,h,e,i)&&l(f,j,g,k)},[null,[1,-1],[1,1],[-1,1],[-1,-1]]),h=[null,[-1,-1],[-1,1],[1,1],[1,-1]];b.pointOnLine=function(a,b,c){var d=e(a,b),i=f(a,b),j=c>0?g[i]:h[i],k=Math.atan(d),l=Math.abs(c*Math.sin(k))*j[1],m=Math.abs(c*Math.cos(k))*j[0];return{x:a.x+m,y:a.y+l}},b.perpendicularLineTo=function(a,b,c){var d=e(a,b),f=Math.atan(-1/d),g=c/2*Math.sin(f),h=c/2*Math.cos(f);return[{x:b.x+h,y:b.y+g},{x:b.x-h,y:b.y-g}]}}.call("undefined"!=typeof window?window:this),function(){"use strict";function a(a,b,c,d,e,f,g,h){return new Touch({target:b,identifier:J(),pageX:c,pageY:d,screenX:e,screenY:f,clientX:g||e,clientY:h||f})}function b(){var a=[];return Array.prototype.push.apply(a,arguments),a.item=function(a){return this[a]},a}function c(c,d,e,f,g,h,i,j){return b(a.apply(null,arguments))}var d=this,e=function(a,b,c){c=c||a.parentNode;for(var d=c.querySelectorAll(b),e=0;ec&&a[c]!=b;c++);c-1&&9>x,z=function(a,b){if(null==a)return[0,0];var c=E(a),d=D(c,0);return[d[b+"X"],d[b+"Y"]]},A=function(a){return null==a?[0,0]:y?[a.clientX+document.documentElement.scrollLeft,a.clientY+document.documentElement.scrollTop]:z(a,"page")},B=function(a){return z(a,"screen")},C=function(a){return z(a,"client")},D=function(a,b){return a.item?a.item(b):a[b]},E=function(a){return a.touches&&a.touches.length>0?a.touches:a.changedTouches&&a.changedTouches.length>0?a.changedTouches:a.targetTouches&&a.targetTouches.length>0?a.targetTouches:[a]},F=function(a){return E(a).length},G=function(a,b,c,d){if(k(a,b,c),d.__tauid=c.__tauid,a.addEventListener)a.addEventListener(b,c,!1);else if(a.attachEvent){var e=b+c.__tauid;a["e"+e]=c,a[e]=function(){a["e"+e]&&a["e"+e](window.event)},a.attachEvent("on"+b,a[e])}},H=function(a,b,c){null!=c&&I(a,function(){var d=f(this);if(l(d,b,c),null!=c.__tauid)if(d.removeEventListener)d.removeEventListener(b,c,!1),u&&w[b]&&d.removeEventListener(w[b],c,!1);else if(this.detachEvent){var e=b+c.__tauid;d[e]&&d.detachEvent("on"+b,d[e]),d[e]=null,d["e"+e]=null}c.__taTouchProxy&&H(a,c.__taTouchProxy[1],c.__taTouchProxy[0])})},I=function(a,b){if(null!=a){a="undefined"!=typeof Window&&"unknown"!=typeof a.top&&a==a.top?[a]:"string"!=typeof a&&null==a.tagName&&null!=a.length?a:"string"==typeof a?document.querySelectorAll(a):[a];for(var c=0;c-1&&9>j,n=9===j,o=function(a){if(m)return[a.clientX+document.documentElement.scrollLeft,a.clientY+document.documentElement.scrollTop];var b=q(a),c=p(b,0);return n?[c.pageX||c.clientX,c.pageY||c.clientY]:[c.pageX,c.pageY]},p=function(a,b){return a.item?a.item(b):a[b]},q=function(a){return a.touches&&a.touches.length>0?a.touches:a.changedTouches&&a.changedTouches.length>0?a.changedTouches:a.targetTouches&&a.targetTouches.length>0?a.targetTouches:[a]},r={delegatedDraggable:"katavorio-delegated-draggable",draggable:"katavorio-draggable",droppable:"katavorio-droppable",drag:"katavorio-drag",selected:"katavorio-drag-selected",active:"katavorio-drag-active",hover:"katavorio-drag-hover",noSelect:"katavorio-drag-no-select",ghostProxy:"katavorio-ghost-proxy",clonedDrag:"katavorio-clone-drag"},s="katavorio-drag-scope",t=["stop","start","drag","drop","over","out","beforeStart"],u=function(){},v=function(){return!0},w=function(a,b,c){for(var d=0;d0){var e=i(O,this.el,a.target||a.srcElement);if(null!=e&&(P=e[0],N=e[1]),null==N)return}else N=this.el;if(G)if(F=N.cloneNode(!0),this.params.addClass(F,r.clonedDrag),F.setAttribute("id",null),F.style.position="absolute",null!=this.params.parent){var g=this.params.getPosition(this.el);F.style.left=g[0]+"px",F.style.top=g[1]+"px",this.params.parent.appendChild(F)}else{var j=f(N);F.style.left=j.left+"px",F.style.top=j.top+"px",document.body.appendChild(F)}else F=N;A&&z(a),q=o(a),F&&F.parentNode&&(y=[F.parentNode.scrollLeft,F.parentNode.scrollTop]),this.params.bind(document,"mousemove",this.moveListener),this.params.bind(document,"mouseup",this.upListener),h.markSelection(this),h.markPosses(this),this.params.addClass(document.body,c.noSelect),ea("beforeStart",{el:this.el,pos:s,e:a,drag:this})}else this.params.consumeFilteredEvents&&z(a)}}}.bind(this),this.moveListener=function(a){if(q){if(!w){var b=ea("start",{el:this.el,pos:s,e:a,drag:this});if(b!==!1){if(!q)return;this.mark(!0),w=!0}else this.abort()}if(q){ca.length=0;var c=o(a),d=c[0]-q[0],e=c[1]-q[1],f=this.params.ignoreZoom?1:h.getZoom();F&&F.parentNode&&(d+=F.parentNode.scrollLeft-y[0],e+=F.parentNode.scrollTop-y[1]),d/=f,e/=f,this.moveBy(d,e,a),h.updateSelection(d,e,this),h.updatePosses(d,e,this)}}}.bind(this),this.upListener=function(a){q&&(q=null,this.params.unbind(document,"mousemove",this.moveListener),this.params.unbind(document,"mouseup",this.upListener),this.params.removeClass(document.body,c.noSelect),this.unmark(a),h.unmarkSelection(this,a),h.unmarkPosses(this,a),this.stop(a),h.notifyPosseDragStop(this,a),w=!1,ca.length=0,G?(F&&F.parentNode&&F.parentNode.removeChild(F),F=null):X&&X(F,this.params.getPosition(F))===!0&&(this.params.setPosition(F,s),ea("revert",F)))}.bind(this),this.getFilters=function(){return Z},this.abort=function(){null!=q&&this.upListener()},this.getDragElement=function(a){return a?N||this.el:F||this.el};var da={start:[],drag:[],stop:[],over:[],out:[],beforeStart:[],revert:[]};b.events.start&&da.start.push(b.events.start),b.events.beforeStart&&da.beforeStart.push(b.events.beforeStart),b.events.stop&&da.stop.push(b.events.stop),b.events.drag&&da.drag.push(b.events.drag),b.events.revert&&da.revert.push(b.events.revert),this.on=function(a,b){da[a]&&da[a].push(b)},this.off=function(a,b){if(da[a]){for(var c=[],d=0;d0)for(var f=0;f0&&fa&&b.setPosition(N,fa),ca.sort(I);for(var e=0;eb.rank?-1:0},J=function(a){return null==a?null:(a="string"==typeof a||a.constructor===String?document.getElementById(a):a,null==a?null:(a._katavorio=a._katavorio||H(),a))};a.Katavorio=function(a){var f=[],g={};this._dragsByScope={},this._dropsByScope={};var h=1,i=function(a,b){y(a,function(a){for(var c=0;c0},k=(this.getMatchingDroppables=function(a){for(var b=[],c={},d=0;d=0&&f.splice(d,1),j(a[b],c)&&y(a[b],function(a){a.destroy()}),delete a[b]}},B=function(a,b,c,d){a=J(a),a[b]&&a[b].off(c,d)};this.elementRemoved=function(a){this.destroyDraggable(a),this.destroyDroppable(a)},this.destroyDraggable=function(a,b,c){1===arguments.length?z(a,"_katavorioDrag",this._dragsByScope):B(a,"_katavorioDrag",b,c)},this.destroyDroppable=function(a,b,c){1===arguments.length?z(a,"_katavorioDrop",this._dropsByScope):B(a,"_katavorioDrop",b,c)},this.reset=function(){this._dragsByScope={},this._dropsByScope={},f=[],g={},C={}};var C={},D=function(a,c,d){var f=e(c)?c:c.id,g=e(c)?!0:c.active!==!1,h=C[f]||function(){var a={name:f,members:[]};return C[f]=a,a}();return y(a,function(a){if(a._katavorioDrag){if(d&&null!=a._katavorioDrag.posseRoles[h.name])return;b(h.members,a._katavorioDrag),b(a._katavorioDrag.posses,h.name),a._katavorioDrag.posseRoles[h.name]=g}}),h};this.addToPosse=function(a,b){for(var c=[],d=1;d0}function j(a){for(var b in a)if(a.hasOwnProperty(b))return!1;return!0}function k(b){if(c(b))return""+b;if(d(b))return!!b;if(g(b))return new Date(b.getTime());if(h(b))return b;if(a(b)){for(var e=[],i=0;i=f.length,i=function(){return e[g[1]]||function(){return e[g[1]]=[],e[g[1]]}()};if(h)g?i()[g[3]]=c:e[a]=c;else if(g){var j=i();e=j[g[3]]||function(){return j[g[3]]={},j[g[3]]}()}else e=e[a]||function(){return e[a]={},e[a]}();return""}),a}}function n(a,b,c){for(var d=0;d-1&&a.splice(c,1),-1!==c}function r(a,b){var c=a.indexOf(b);return c>-1&&a.splice(c,1),-1!==c}function s(a,b,c){-1===p(a,c)&&a.push(b)}function t(a,b,c,d){var e=a[b];return null==e&&(e=[],a[b]=e),e[d?"unshift":"push"](c),e}function u(a,b,c){return-1===a.indexOf(b)?(c?a.unshift(b):a.push(b),!0):!1}function v(b,c,d){var e;c=a(c)?c:[c];var f=function(a){for(var c=a.__proto__;null!=c;)if(null!=c.prototype){for(var d in c.prototype)c.prototype.hasOwnProperty(d)&&!b.prototype.hasOwnProperty(d)&&(b.prototype[d]=c.prototype[d]);c=c.prototype.__proto__}else c=null};for(e=0;e2)for(e=2;e>8&255]+F[a>>16&255]+F[a>>24&255]+"-"+F[255&b]+F[b>>8&255]+"-"+F[b>>16&15|64]+F[b>>24&255]+"-"+F[63&c|128]+F[c>>8&255]+"-"+F[c>>16&255]+F[c>>24&255]+F[255&d]+F[d>>8&255]+F[d>>16&255]+F[d>>24&255]}function x(a){if(null==a)return null;for(var b=a.replace(/^\s\s*/,""),c=/\s/,d=b.length;c.test(b.charAt(--d)););return b.slice(0,d+1)}function y(a,b){a=null==a.length||"string"==typeof a?[a]:a;for(var c=0;cG;G++)F[G]=(16>G?"0":"")+G.toString(16);E.uuid=w,E.fastTrim=x,E.each=y,E.map=z,E.mergeWithParents=A,E.logEnabled=!0,E.log=B,E.wrap=C;var H=function(){function a(){var a=this;this._listeners={},this.eventsSuspended=!1,this.tick=!1,this.eventsToDieOn={ready:!0},this.queue=[],this.bind=function(b,c,d){var e=function(b){t(a._listeners,b,c,d),c.__jsPlumb=c.__jsPlumb||{},c.__jsPlumb[w()]=b};if("string"==typeof b)e(b);else if(null!=b.length)for(var f=0;fe&&g!==!1;){if(this.eventsToDieOn[a])this._listeners[a][e].apply(this,[b,c]);else try{g=this._listeners[a][e].apply(this,[b,c])}catch(h){B("jsPlumb: fire failed for event "+a+" : "+h)}e++,(null==this._listeners||null==this._listeners[a])&&(f=!0)}}this.tick=!1,this._drain()}return this},this._drain=function(){var b=a.queue.pop();b&&a.fire.apply(a,b)},this.unbind=function(a,b){if(0===arguments.length)this._listeners={};else if(1===arguments.length){if("string"==typeof a)delete this._listeners[a];else if(a.__jsPlumb){var c=void 0;for(var d in a.__jsPlumb)c=a.__jsPlumb[d],r(this._listeners[c]||[],a)}}else 2===arguments.length&&r(this._listeners[a]||[],b);return this},this.getListener=function(b){return a._listeners[b]},this.setSuspendEvents=function(b){a.eventsSuspended=b},this.isSuspendEvents=function(){return a.eventsSuspended},this.silently=function(b){a.setSuspendEvents(!0);try{b()}catch(c){B("Cannot execute silent function "+c)}a.setSuspendEvents(!1)},this.cleanupListeners=function(){for(var b in a._listeners)a._listeners[b]=null}}return a}();E.EventGenerator=H}.call("undefined"!=typeof window?window:this),function(){"use strict";var a=this;a.jsPlumbUtil.matchesSelector=function(a,b,c){c=c||a.parentNode;for(var d=c.querySelectorAll(b),e=0;eb.scrollTop+b.offsetHeight)d[i]._jsPlumbProxies||(d[i]._jsPlumbProxies=d[i]._jsPlumbProxies||[],a.select({source:d[i]}).each(function(c){a.proxyConnection(c,0,b,h,function(){return f("bottom",0,c.endpoints[0],c)},function(){return e("bottom",0,c.endpoints[0],c)}),d[i]._jsPlumbProxies.push([c,0])}),a.select({target:d[i]}).each(function(c){a.proxyConnection(c,1,b,h,function(){return f("bottom",1,c.endpoints[1],c)},function(){return e("bottom",1,c.endpoints[1],c)}),d[i]._jsPlumbProxies.push([c,1])}));else if(d[i]._jsPlumbProxies){for(var j=0;jf;f++)d&&d===e[f]||e[f].setHover(b,!0,c)},g=function(a){return null==a?null:a.split(" ")},h=function(a,b,c){for(var d in b)a[d]=c},i=function(a,c,d){if(a.getDefaultType){var e=a.getTypeDescriptor(),f={},g=a.getDefaultType(),i=b.merge({},g);h(f,g,"__default");for(var j=0,k=a._jsPlumb.types.length;k>j;j++){var l=a._jsPlumb.types[j];if("__default"!==l){var m=a._jsPlumb.instance.getType(l,e);if(null!=m){var n=["anchor","anchors","connector","paintStyle","hoverPaintStyle","endpoint","endpoints","connectorOverlays","connectorStyle","connectorHoverStyle","endpointStyle","endpointHoverStyle"],o=[];"override"===m.mergeStrategy?Array.prototype.push.apply(n,["events","overlays","cssClass"]):o.push("cssClass"),i=b.merge(i,m,o,n),h(f,m,l)}}}c&&(i=b.populate(i,c,"_")),a.applyType(i,d,f),d||a.repaint()}},j=a.jsPlumbUIComponent=function(a){b.EventGenerator.apply(this,arguments);var c=this,d=arguments,e=c.idPrefix,f=e+(new Date).getTime();this._jsPlumb={instance:a._jsPlumb,parameters:a.parameters||{},paintStyle:null,hoverPaintStyle:null,paintStyleInUse:null,hover:!1,beforeDetach:a.beforeDetach,beforeDrop:a.beforeDrop,overlayPlacements:[],hoverClass:a.hoverClass||a._jsPlumb.Defaults.HoverClass,types:[],typeCache:{}},this.cacheTypeItem=function(a,b,c){this._jsPlumb.typeCache[c]=this._jsPlumb.typeCache[c]||{},this._jsPlumb.typeCache[c][a]=b},this.getCachedTypeItem=function(a,b){return this._jsPlumb.typeCache[b]?this._jsPlumb.typeCache[b][a]:null},this.getId=function(){return f};var g=a.overlays||[],h={};if(this.defaultOverlayKeys){for(var i=0;if;f++)this.hasType(d[f])||(this._jsPlumb.types.push(d[f]),e=!0);e&&i(this,b,c)}},removeType:function(a,b,c){var d=g(a),e=!1,f=function(a){var b=this._jsPlumb.types.indexOf(a);return-1!==b?(k(this,b),this._jsPlumb.types.splice(b,1),!0):!1}.bind(this);if(null!=d){for(var h=0,j=d.length;j>h;h++)e=f(d[h])||e;e&&i(this,b,c)}},clearTypes:function(a,b){for(var c=this._jsPlumb.types.length,d=0;c>d;d++)k(this,0),this._jsPlumb.types.splice(0,1);i(this,a,b)},toggleType:function(a,b,c){var d=g(a);if(null!=d){for(var e=0,f=d.length;f>e;e++){var h=this._jsPlumb.types.indexOf(d[e]);-1!==h?(k(this,h),this._jsPlumb.types.splice(h,1)):this._jsPlumb.types.push(d[e])}i(this,b,c)}},applyType:function(a,b){if(this.setPaintStyle(a.paintStyle,b),this.setHoverPaintStyle(a.hoverPaintStyle,b),a.parameters)for(var c in a.parameters)this.setParameter(c,a.parameters[c]);this._jsPlumb.paintStyleInUse=this.getPaintStyle()},setPaintStyle:function(a,b){this._jsPlumb.paintStyle=a,this._jsPlumb.paintStyleInUse=this._jsPlumb.paintStyle,d(this),b||this.repaint()},getPaintStyle:function(){return this._jsPlumb.paintStyle},setHoverPaintStyle:function(a,b){this._jsPlumb.hoverPaintStyle=a,d(this),b||this.repaint()},getHoverPaintStyle:function(){return this._jsPlumb.hoverPaintStyle},destroy:function(a){(a||null==this.typeId)&&(this.cleanupListeners(),this.clone=null,this._jsPlumb=null)},isHover:function(){return this._jsPlumb.hover},setHover:function(a,b,d){if(this._jsPlumb&&!this._jsPlumb.instance.currentlyDragging&&!this._jsPlumb.instance.isHoverSuspended()){this._jsPlumb.hover=a;var e=a?"addClass":"removeClass";null!=this.canvas&&(null!=this._jsPlumb.instance.hoverClass&&this._jsPlumb.instance[e](this.canvas,this._jsPlumb.instance.hoverClass),null!=this._jsPlumb.hoverClass&&this._jsPlumb.instance[e](this.canvas,this._jsPlumb.hoverClass)),null!=this._jsPlumb.hoverPaintStyle&&(this._jsPlumb.paintStyleInUse=a?this._jsPlumb.hoverPaintStyle:this._jsPlumb.paintStyle,this._jsPlumb.instance.isSuspendDrawing()||(d=d||c(),this.repaint({timestamp:d,recalc:!1}))),this.getAttachedElements&&!b&&f(this,a,c(),this)}}});var l=0,m=function(){var a=l+1;return l++,a},n=a.jsPlumbInstance=function(d){this.version="2.14.3",this.Defaults={Anchor:"Bottom",Anchors:[null,null],ConnectionsDetachable:!0,ConnectionOverlays:[],Connector:"Bezier",Container:null,DoNotThrowErrors:!1,DragOptions:{},DropOptions:{},Endpoint:"Dot",EndpointOverlays:[],Endpoints:[null,null],EndpointStyle:{fill:"#456"},EndpointStyles:[null,null],EndpointHoverStyle:null,EndpointHoverStyles:[null,null],HoverPaintStyle:null,LabelStyle:{color:"black"},ListStyle:{},LogEnabled:!1,Overlays:[],MaxConnections:1,PaintStyle:{"stroke-width":4,stroke:"#456"},ReattachConnections:!1,RenderMode:"svg",Scope:"jsPlumb_DefaultScope"},d&&o.extend(this.Defaults,d),this.logEnabled=this.Defaults.LogEnabled,this._connectionTypes={},this._endpointTypes={},b.EventGenerator.apply(this);var f=this,g=m(),h=f.bind,i={},k=1,l=function(a){if(null==a)return null;if(3===a.nodeType||8===a.nodeType)return{el:a,text:!0};var c=f.getElement(a);return{el:c,id:b.isString(a)&&null==c?a:T(c)}};this.getInstanceIndex=function(){return g},this.setZoom=function(a,b){return k=a,f.fire("zoom",k),b&&f.repaintEverything(),!0},this.getZoom=function(){return k};for(var n in this.Defaults)i[n]=this.Defaults[n];var p,q=[];this.unbindContainer=function(){if(null!=p&&q.length>0)for(var a=0;ae;e++)d.overlays.push(d.sourceEndpoint.connectorOverlays[e])}d.sourceEndpoint&&d.sourceEndpoint.scope&&(d.scope=d.sourceEndpoint.scope),!d["pointer-events"]&&d.sourceEndpoint&&d.sourceEndpoint.connectorPointerEvents&&(d["pointer-events"]=d.sourceEndpoint.connectorPointerEvents);var h=function(a,b,c){var e=K(b,{anchor:d.anchors?d.anchors[c]:d.anchor,endpoint:d.endpoints?d.endpoints[c]:d.endpoint,paintStyle:d.endpointStyles?d.endpointStyles[c]:d.endpointStyle,hoverPaintStyle:d.endpointHoverStyles?d.endpointHoverStyles[c]:d.endpointHoverStyle});return f.addEndpoint(a,e)},i=function(a,b,c,e){if(d[a]&&!d[a].endpoint&&!d[a+"Endpoint"]&&!d.newConnection){var f=T(d[a]),g=c[f];if(g=g?g[e]:null){if(!g.enabled)return!1;var i=o.extend({},g.def);delete i.label;var j=null!=g.endpoint&&g.endpoint._jsPlumb?g.endpoint:h(d[a],i,b);if(j.isFull())return!1;d[a+"Endpoint"]=j,!d.scope&&i.scope&&(d.scope=i.scope),g.uniqueEndpoint?g.endpoint?j.finalEndpoint=g.endpoint:(g.endpoint=j,j.setDeleteOnEmpty(!1)):j.setDeleteOnEmpty(!0),0===b&&g.def.connectorOverlays&&(d.overlays=d.overlays||[],Array.prototype.push.apply(d.overlays,g.def.connectorOverlays))}}};return i("source",0,this.sourceEndpointDefinitions,d.type||"default")!==!1&&i("target",1,this.targetEndpointDefinitions,d.type||"default")!==!1?(d.sourceEndpoint&&d.targetEndpoint&&(J(d.sourceEndpoint,d.targetEndpoint)||(d=null)),d):void 0}.bind(f),M=function(a){var b=f.Defaults.ConnectionType||f.getDefaultConnectionType();a._jsPlumb=f,a.newConnection=M,a.newEndpoint=O,a.endpointsByUUID=v,a.endpointsByElement=u,a.finaliseConnection=N,a.id="con_"+F();var c=new b(a);return c.isDetachable()&&(c.endpoints[0].initDraggable("_jsPlumbSource"),c.endpoints[1].initDraggable("_jsPlumbTarget")),c},N=f.finaliseConnection=function(a,b,c,d){if(b=b||{},a.suspendedEndpoint||t.push(a),a.pending=null,a.endpoints[0].isTemporarySource=!1,d!==!1&&f.router.newConnection(a),H(a.source),!b.doNotFireConnectionEvent&&b.fireEvent!==!1){var e={connection:a,source:a.source,target:a.target,sourceId:a.sourceId,targetId:a.targetId,sourceEndpoint:a.endpoints[0],targetEndpoint:a.endpoints[1]};f.fire("connection",e,c)}},O=function(a,b){var c=f.Defaults.EndpointType||o.Endpoint,d=o.extend({},a);d._jsPlumb=f,d.newConnection=M,d.newEndpoint=O,d.endpointsByUUID=v,d.endpointsByElement=u,d.fireDetachEvent=W,d.elementId=b||T(d.source);var e=new c(d);return e.id="ep_"+F(),na(d.elementId,d.source),o.headless||f.getDragManager().endpointAdded(d.source,b),e},P=function(a,b,c){var d=u[a];if(d&&d.length)for(var e=0,f=d.length;f>e;e++){for(var g=0,h=d[e].connections.length;h>g;g++){var i=b(d[e].connections[g]);if(i)return}c&&c(d[e])}},Q=function(a,b,c){b="block"===b;var d=null;c&&(d=function(a){a.setVisible(b,!0,!0)});var e=l(a);P(e.id,function(a){if(b&&c){var d=a.sourceId===e.id?1:0;a.endpoints[d].isVisible()&&a.setVisible(!0)}else a.setVisible(b)},d)},R=function(a,b){var c=null;b&&(c=function(a){var b=a.isVisible();a.setVisible(!b)}),P(a,function(a){var b=a.isVisible();a.setVisible(!b)},c)},S=function(a){var b=x[a];return b?{o:b,s:A[a]}:oa({elId:a})},T=function(a,c,d){if(b.isString(a))return a;if(null==a)return null;var e=f.getAttribute(a,"id");return e&&"undefined"!==e||(2===arguments.length&&void 0!==arguments[1]?e=c:(1===arguments.length||3===arguments.length&&!arguments[2])&&(e="jsPlumb_"+g+"_"+F()),d||f.setAttribute(a,"id",e)),e};this.setConnectionBeingDragged=function(a){z=a},this.isConnectionBeingDragged=function(){return z},this.getManagedElements=function(){return w},this.connectorClass="jtk-connector",this.connectorOutlineClass="jtk-connector-outline",this.connectedClass="jtk-connected",this.hoverClass="jtk-hover",this.endpointClass="jtk-endpoint",this.endpointConnectedClass="jtk-endpoint-connected",this.endpointFullClass="jtk-endpoint-full",this.endpointDropAllowedClass="jtk-endpoint-drop-allowed",this.endpointDropForbiddenClass="jtk-endpoint-drop-forbidden",this.overlayClass="jtk-overlay",this.draggingClass="jtk-dragging",this.elementDraggingClass="jtk-element-dragging",this.sourceElementDraggingClass="jtk-source-element-dragging",this.targetElementDraggingClass="jtk-target-element-dragging",this.endpointAnchorClassPrefix="jtk-endpoint-anchor",this.hoverSourceClass="jtk-source-hover",this.hoverTargetClass="jtk-target-hover",this.dragSelectClass="jtk-drag-select",this.Anchors={},this.Connectors={svg:{}},this.Endpoints={svg:{}},this.Overlays={svg:{}},this.ConnectorRenderers={},this.SVG="svg",this.addEndpoint=function(a,c,d){d=d||{};var e=o.extend({},d);o.extend(e,c),e.endpoint=e.endpoint||f.Defaults.Endpoint,e.paintStyle=e.paintStyle||f.Defaults.EndpointStyle;for(var g=[],h=b.isArray(a)||null!=a.length&&!b.isString(a)?a:[a],i=0,j=h.length;j>i;i++){e.source=f.getElement(h[i]),la(e.source);var k=T(e.source),l=O(e,k),m=na(k,e.source,null,!B).info.o;b.addToList(u,k,l),B||l.paint({anchorLoc:l.anchor.compute({xy:[m.left,m.top],wh:A[k],element:l,timestamp:C}),timestamp:C}),g.push(l)}return 1===g.length?g[0]:g},this.addEndpoints=function(a,c,d){for(var e=[],g=0,h=c.length;h>g;g++){var i=f.addEndpoint(a,c[g],d);b.isArray(i)?Array.prototype.push.apply(e,i):e.push(i)}return e},this.animate=function(a,c,d){if(!this.animationSupported)return!1;d=d||{};var e=f.getElement(a),g=T(e),h=o.animEvents.step,i=o.animEvents.complete;d[h]=b.wrap(d[h],function(){f.revalidate(g)}),d[i]=b.wrap(d[i],function(){f.revalidate(g)}),f.doAnimate(e,c,d)},this.checkCondition=function(a,c){var d=f.getListener(a),e=!0;if(d&&d.length>0){var g=Array.prototype.slice.call(arguments,1);try{for(var h=0,i=d.length;i>h;h++)e=e&&d[h].apply(d[h],g)}catch(j){b.log(f,"cannot check condition ["+a+"]"+j)}}return e},this.connect=function(a,c){var d,e=L(a,c);if(e){if(null==e.source&&null==e.sourceEndpoint)return void b.log("Cannot establish connection - source does not exist");if(null==e.target&&null==e.targetEndpoint)return void b.log("Cannot establish connection - target does not exist");la(e.source),d=M(e),N(d,e)}return d};var U=[{el:"source",elId:"sourceId",epDefs:"sourceEndpointDefinitions"},{el:"target",elId:"targetId",epDefs:"targetEndpointDefinitions"}],V=function(a,b,c,d){var e,f,g,h=U[c],i=a[h.elId],j=(a[h.el],a.endpoints[c]),k={index:c,originalSourceId:0===c?i:a.sourceId,newSourceId:a.sourceId,originalTargetId:1===c?i:a.targetId,newTargetId:a.targetId,connection:a};if(b.constructor===o.Endpoint)e=b,e.addConnection(a),b=e.element;else if(f=T(b),g=this[h.epDefs][f],f===a[h.elId])e=null;else if(g)for(var l in g){if(!g[l].enabled)return;e=null!=g[l].endpoint&&g[l].endpoint._jsPlumb?g[l].endpoint:this.addEndpoint(b,g[l].def),g[l].uniqueEndpoint&&(g[l].endpoint=e),e.addConnection(a)}else e=a.makeEndpoint(0===c,b,f);return null!=e&&(j.detachFromConnection(a),a.endpoints[c]=e,a[h.el]=e.element,a[h.elId]=e.elementId,k[0===c?"newSourceId":"newTargetId"]=e.elementId,X(k),d||a.repaint()),k.element=b,k}.bind(this);this.setSource=function(a,b,c){var d=V(a,b,0,c);this.router.sourceOrTargetChanged(d.originalSourceId,d.newSourceId,a,d.el,0)},this.setTarget=function(a,b,c){var d=V(a,b,1,c);this.router.sourceOrTargetChanged(d.originalTargetId,d.newTargetId,a,d.el,1)},this.deleteEndpoint=function(a,b,c){ +var d="string"==typeof a?v[a]:a;return d&&f.deleteObject({endpoint:d,dontUpdateHover:b,deleteAttachedObjects:c}),f},this.deleteEveryEndpoint=function(){var a=f.setSuspendDrawing(!0);for(var b in u){var c=u[b];if(c&&c.length)for(var d=0,e=c.length;e>d;d++)f.deleteEndpoint(c[d],!0)}u={},w={},v={},x={},y={},f.router.reset();var g=f.getDragManager();return g&&g.reset(),a||f.setSuspendDrawing(!1),f};var W=function(a,b,c){var d=f.Defaults.ConnectionType||f.getDefaultConnectionType(),e=a.constructor===d,g=e?{connection:a,source:a.source,target:a.target,sourceId:a.sourceId,targetId:a.targetId,sourceEndpoint:a.endpoints[0],targetEndpoint:a.endpoints[1]}:a;b&&f.fire("connectionDetached",g,c),f.fire("internal.connectionDetached",g,c),f.router.connectionDetached(g)},X=f.fireMoveEvent=function(a,b){f.fire("connectionMoved",a,b)};this.unregisterEndpoint=function(a){a._jsPlumb.uuid&&(v[a._jsPlumb.uuid]=null),f.anchorManager.deleteEndpoint(a);for(var b in u){var c=u[b];if(c){for(var d=[],e=0,g=c.length;g>e;e++)c[e]!==a&&d.push(c[e]);u[b]=d}u[b].length<1&&delete u[b]}};var Y="isDetachAllowed",Z="beforeDetach",$="checkCondition";this.deleteConnection=function(a,c){return null!=a&&(c=c||{},c.force||b.functionChain(!0,!1,[[a.endpoints[0],Y,[a]],[a.endpoints[1],Y,[a]],[a,Y,[a]],[f,$,[Z,a]]]))?(a.setHover(!1),W(a,!a.pending&&c.fireEvent!==!1,c.originalEvent),a.endpoints[0].detachFromConnection(a),a.endpoints[1].detachFromConnection(a),b.removeWithFunction(t,function(b){return a.id===b.id}),a.cleanup(),a.destroy(),!0):!1},this.deleteEveryConnection=function(a){a=a||{};var b=t.length,c=0;return f.batch(function(){for(var d=0;b>d;d++)c+=f.deleteConnection(t[0],a)?1:0}),c},this.deleteConnectionsForElement=function(a,b){b=b||{},a=f.getElement(a);var c=T(a),d=u[c];if(d&&d.length)for(var e=0,g=d.length;g>e;e++)d[e].deleteEveryConnection(b);return f},this.deleteObject=function(a){var c={endpoints:{},connections:{},endpointCount:0,connectionCount:0},d=a.deleteAttachedObjects!==!1,e=function(b){null!=b&&null==c.connections[b.id]&&(a.dontUpdateHover||null==b._jsPlumb||b.setHover(!1),c.connections[b.id]=b,c.connectionCount++)},g=function(b){if(null!=b&&null==c.endpoints[b.id]&&(a.dontUpdateHover||null==b._jsPlumb||b.setHover(!1),c.endpoints[b.id]=b,c.endpointCount++,d))for(var f=0;fe;e++)a[e][b].apply(a[e],c);return d(a)},aa=function(a,b,c){for(var d=[],e=0,f=a.length;f>e;e++)d.push([a[e][b].apply(a[e],c),a[e]]);return d},ba=function(a,b,c){return function(){return _(a,b,arguments,c)}},ca=function(a,b){return function(){return aa(a,b,arguments)}},da=function(a,b){var c=[];if(a)if("string"==typeof a){if("*"===a)return a;c.push(a)}else if(b)c=a;else if(a.length)for(var d=0,e=a.length;e>d;d++)c.push(l(a[d]).id);else c.push(l(a).id);return c},ea=function(a,b,c){return"*"===a?!0:a.length>0?-1!==a.indexOf(b):!c};this.getConnections=function(a,b){a?a.constructor===String&&(a={scope:a}):a={};for(var c=a.scope||f.getDefaultScope(),d=da(c,!0),e=da(a.source),g=da(a.target),h=!b&&d.length>1?{}:[],i=function(a,c){if(!b&&d.length>1){var e=h[a];null==e&&(e=h[a]=[]),e.push(c)}else h.push(c)},j=0,k=t.length;k>j;j++){var l=t[j],m=l.proxies&&l.proxies[0]?l.proxies[0].originalEp.elementId:l.sourceId,n=l.proxies&&l.proxies[1]?l.proxies[1].originalEp.elementId:l.targetId;ea(d,l.scope)&&ea(e,m)&&ea(g,n)&&i(l.scope,l)}return h};var fa=function(a,b){return function(c){for(var d=0,e=a.length;e>d;d++)c(a[d]);return b(a)}},ga=function(a){return function(b){return a[b]}},ha=function(a,b){var c,d,e={length:a.length,each:fa(a,b),get:ga(a)},f=["setHover","removeAllOverlays","setLabel","addClass","addOverlay","removeOverlay","removeOverlays","showOverlay","hideOverlay","showOverlays","hideOverlays","setPaintStyle","setHoverPaintStyle","setSuspendEvents","setParameter","setParameters","setVisible","repaint","addType","toggleType","removeType","removeClass","setType","bind","unbind"],g=["getLabel","getOverlay","isHover","getParameter","getParameters","getPaintStyle","getHoverPaintStyle","isVisible","hasType","getType","isSuspendEvents"];for(c=0,d=f.length;d>c;c++)e[f[c]]=ba(a,f[c],b);for(c=0,d=g.length;d>c;c++)e[g[c]]=ca(a,g[c]);return e},ia=function(a){var b=ha(a,ia);return o.extend(b,{setDetachable:ba(a,"setDetachable",ia),setReattach:ba(a,"setReattach",ia),setConnector:ba(a,"setConnector",ia),"delete":function(){for(var b=0,c=a.length;c>b;b++)f.deleteConnection(a[b])},isDetachable:ca(a,"isDetachable"),isReattach:ca(a,"isReattach")})},ja=function(a){var b=ha(a,ja);return o.extend(b,{setEnabled:ba(a,"setEnabled",ja),setAnchor:ba(a,"setAnchor",ja),isEnabled:ca(a,"isEnabled"),deleteEveryConnection:function(){for(var b=0,c=a.length;c>b;b++)a[b].deleteEveryConnection()},"delete":function(){for(var b=0,c=a.length;c>b;b++)f.deleteEndpoint(a[b])}})};this.select=function(a){return a=a||{},a.scope=a.scope||"*",ia(a.connections||f.getConnections(a,!0))},this.selectEndpoints=function(a){a=a||{},a.scope=a.scope||"*";var b=!a.element&&!a.source&&!a.target,c=b?"*":da(a.element),d=b?"*":da(a.source),e=b?"*":da(a.target),f=da(a.scope,!0),g=[];for(var h in u){var i=ea(c,h,!0),j=ea(d,h,!0),k="*"!==d,l=ea(e,h,!0),m="*"!==e;if(i||j||l)a:for(var n=0,o=u[h].length;o>n;n++){var p=u[h][n];if(ea(f,p.scope,!0)){var q=k&&d.length>0&&!p.isSource,r=m&&e.length>0&&!p.isTarget;if(q||r)continue a;g.push(p)}}}return ja(g)},this.getAllConnections=function(){return t},this.getDefaultScope=function(){return D},this.getEndpoint=I,this.getEndpoints=function(a){return u[l(a).id]||[]},this.getDefaultEndpointType=function(){return o.Endpoint},this.getDefaultConnectionType=function(){return o.Connection},this.getId=T,this.draw=H,this.info=l,this.appendElement=G;var ka=!1;this.isHoverSuspended=function(){return ka},this.setHoverSuspended=function(a){ka=a},this.hide=function(a,b){return Q(a,"none",b),f},this.idstamp=F;var la=function(a){if(!p&&a){var b=f.getElement(a);b.offsetParent&&f.setContainer(b.offsetParent)}},ma=function(){f.Defaults.Container&&f.setContainer(f.Defaults.Container)},na=f.manage=function(a,b,c,d){return w[a]?d&&(w[a].info=oa({elId:a,timestamp:C,recalc:!0})):(w[a]={el:b,endpoints:[],connections:[]},w[a].info=oa({elId:a,timestamp:C}),f.addClass(b,"jtk-managed"),c||f.fire("manageElement",{id:a,info:w[a].info,el:b})),w[a]},oa=(f.unmanage=function(a){if(w[a]){var b=w[a].el;f.removeClass(b,"jtk-managed"),delete w[a],f.fire("unmanageElement",{id:a,el:b})}},function(a){var b,c=a.timestamp,d=a.recalc,e=a.offset,g=a.elId;return B&&!c&&(c=C),!d&&c&&c===y[g]?{o:a.offset||x[g],s:A[g]}:(d||!e&&null==x[g]?(b=w[g]?w[g].el:null,null!=b&&(A[g]=f.getSize(b),x[g]=f.getOffset(b),y[g]=c)):(x[g]=e||x[g],null==A[g]&&(b=w[g].el,null!=b&&(A[g]=f.getSize(b))),y[g]=c),x[g]&&!x[g].right&&(x[g].right=x[g].left+A[g][0],x[g].bottom=x[g].top+A[g][1],x[g].width=A[g][0],x[g].height=A[g][1],x[g].centerx=x[g].left+x[g].width/2,x[g].centery=x[g].top+x[g].height/2),{o:x[g],s:A[g]})});this.updateOffset=oa,this.init=function(){s||(ma(),f.router=new a.jsPlumb.DefaultRouter(f),f.anchorManager=f.router.anchorManager,s=!0,f.fire("ready",f))}.bind(this),this.log=r,this.jsPlumbUIComponent=j,this.makeAnchor=function(){var c,d=function(b,c){if(a.jsPlumb.Anchors[b])return new a.jsPlumb.Anchors[b](c);if(!f.Defaults.DoNotThrowErrors)throw{msg:"jsPlumb: unknown anchor type '"+b+"'"}};if(0===arguments.length)return null;var e=arguments[0],g=arguments[1],h=(arguments[2],null);if(e.compute&&e.getOrientation)return e;if("string"==typeof e)h=d(arguments[0],{elementId:g,jsPlumbInstance:f});else if(b.isArray(e))if(b.isArray(e[0])||b.isString(e[0]))2===e.length&&b.isObject(e[1])?b.isString(e[0])?(c=a.jsPlumb.extend({elementId:g,jsPlumbInstance:f},e[1]),h=d(e[0],c)):(c=a.jsPlumb.extend({elementId:g,jsPlumbInstance:f,anchors:e[0]},e[1]),h=new a.jsPlumb.DynamicAnchor(c)):h=new o.DynamicAnchor({anchors:e,selector:null,elementId:g,jsPlumbInstance:f});else{var i={x:e[0],y:e[1],orientation:e.length>=4?[e[2],e[3]]:[0,0],offsets:e.length>=6?[e[4],e[5]]:[0,0],elementId:g,jsPlumbInstance:f,cssClass:7===e.length?e[6]:null};h=new a.jsPlumb.Anchor(i),h.clone=function(){return new a.jsPlumb.Anchor(i)}}return h.id||(h.id="anchor_"+F()),h},this.makeAnchors=function(c,d,e){for(var g=[],h=0,i=c.length;i>h;h++)"string"==typeof c[h]?g.push(a.jsPlumb.Anchors[c[h]]({elementId:d,jsPlumbInstance:e})):b.isArray(c[h])&&g.push(f.makeAnchor(c[h],d,e));return g},this.makeDynamicAnchor=function(b,c){return new a.jsPlumb.DynamicAnchor({anchors:b,selector:c,elementId:null,jsPlumbInstance:f})},this.targetEndpointDefinitions={},this.sourceEndpointDefinitions={};var pa=function(a,b,c,d,e){for(var f=a.target||a.srcElement,g=!1,h=d.getSelector(b,c),i=0;i0&&a>=c.def.maxConnections},element:c.el,elementId:c.id,isSource:g,isTarget:h,addClass:function(a){f.addClass(c.el,a)},removeClass:function(a){f.removeClass(c.el,a)},onDrop:function(a){var b=a.endpoints[0];b.anchor.unlock()},isDropAllowed:function(){return i.isDropAllowed.apply(i,arguments)},isRedrop:function(a){return null!=a.suspendedElement&&null!=a.suspendedEndpoint&&a.suspendedEndpoint.element===c.el},getEndpoint:function(b){var e=c.def.endpoint;if(null==e||null==e._jsPlumb){var g=f.deriveEndpointAndAnchorSpec(b.getType().join(" "),!0),h=g.endpoints?a.jsPlumb.extend(d,{endpoint:c.def.def.endpoint||g.endpoints[1]}):d;g.anchors&&(h=a.jsPlumb.extend(h,{anchor:c.def.def.anchor||g.anchors[1]})),e=f.addEndpoint(c.el,h),e._mtNew=!0}if(d.uniqueEndpoint&&(c.def.endpoint=e),e.setDeleteOnEmpty(!0),b.isDetachable()&&e.initDraggable(),null!=e.anchor.positionFinder){var i=f.getUIPosition(arguments,f.getZoom()),j=f.getOffset(c.el),k=f.getSize(c.el),l=null==i?[0,0]:e.anchor.positionFinder(i,j,k,e.anchor.constructorParams);e.anchor.x=l[0],e.anchor.y=l[1]}return e},maybeCleanup:function(a){a._mtNew&&0===a.connections.length?f.deleteObject({endpoint:a}):delete a._mtNew}}),l=a.jsPlumb.dragEvents.drop;return e.scope=e.scope||d.scope||f.Defaults.Scope,e[l]=b.wrap(e[l],k,!0),e.rank=d.rank||0,h&&(e[a.jsPlumb.dragEvents.over]=function(){return!0}),d.allowLoopback===!1&&(e.canDrop=function(a){var b=a.getDragElement()._jsPlumbRelatedElement;return b!==c.el}),f.initDroppable(c.el,e,"internal"),k};this.makeTarget=function(b,c,d){var e=a.jsPlumb.extend({_jsPlumb:this},d);a.jsPlumb.extend(e,c);for(var g=e.maxConnections||-1,h=function(b){var c=l(b),d=c.id,h=a.jsPlumb.extend({},e.dropOptions||{}),i=e.connectionType||"default";this.targetEndpointDefinitions[d]=this.targetEndpointDefinitions[d]||{},la(d),c.el._isJsPlumbGroup&&null==h.rank&&(h.rank=-1);var j={def:a.jsPlumb.extend({},e),uniqueEndpoint:e.uniqueEndpoint,maxConnections:g,enabled:!0};e.createEndpoint&&(j.uniqueEndpoint=!0,j.endpoint=f.addEndpoint(b,j.def),j.endpoint.setDeleteOnEmpty(!1)),c.def=j,this.targetEndpointDefinitions[d][i]=j,qa(c,e,h,e.isSource===!0,!0),c.el._katavorioDrop[c.el._katavorioDrop.length-1].targetDef=j}.bind(this),i=b.length&&b.constructor!==String?b:[b],j=0,k=i.length;k>j;j++)h(i[j]);return this},this.unmakeTarget=function(a,b){var c=l(a);return f.destroyDroppable(c.el,"internal"),b||delete this.targetEndpointDefinitions[c.id],this},this.makeSource=function(c,d,e){var g=a.jsPlumb.extend({_jsPlumb:this},e);a.jsPlumb.extend(g,d);var h=g.connectionType||"default",i=f.deriveEndpointAndAnchorSpec(h);g.endpoint=g.endpoint||i.endpoints[0],g.anchor=g.anchor||i.anchors[0];for(var j=g.maxConnections||-1,m=g.onMaxConnections,n=function(d){var e=d.id,i=this.getElement(d.el);this.sourceEndpointDefinitions[e]=this.sourceEndpointDefinitions[e]||{},la(e);var l={def:a.jsPlumb.extend({},g),uniqueEndpoint:g.uniqueEndpoint,maxConnections:j,enabled:!0};g.createEndpoint&&(l.uniqueEndpoint=!0,l.endpoint=f.addEndpoint(c,l.def),l.endpoint.setDeleteOnEmpty(!1)),this.sourceEndpointDefinitions[e][h]=l,d.def=l;var n=a.jsPlumb.dragEvents.stop,o=a.jsPlumb.dragEvents.drag,p=a.jsPlumb.extend({},g.dragOptions||{}),q=p.drag,r=p.stop,s=null,t=!1;p.scope=p.scope||g.scope,p[o]=b.wrap(p[o],function(){q&&q.apply(this,arguments),t=!1}),p[n]=b.wrap(p[n],function(){if(r&&r.apply(this,arguments),this.currentlyDragging=!1,null!=s._jsPlumb){var a=g.anchor||this.Defaults.Anchor,b=s.anchor,c=s.connections[0],d=this.makeAnchor(a,e,this),h=s.element;if(null!=d.positionFinder){var i=f.getOffset(h),j=this.getSize(h),k={left:i.left+b.x*j[0],top:i.top+b.y*j[1]},l=d.positionFinder(k,i,j,d.constructorParams);d.x=l[0],d.y=l[1]}s.setAnchor(d,!0),s.repaint(),this.repaint(s.elementId),null!=c&&this.repaint(c.targetId)}}.bind(this));var u=function(c){if(3!==c.which&&2!==c.button){e=this.getId(this.getElement(d.el));var l=this.sourceEndpointDefinitions[e][h];if(l.enabled){if(g.filter){var n=b.isString(g.filter)?pa(c,d.el,g.filter,this,g.filterExclude):g.filter(c,d.el);if(n===!1)return}var o=this.select({source:e}).length;if(l.maxConnections>=0&&o>=l.maxConnections)return m&&m({element:d.el,maxConnections:j},c),!1;var q=a.jsPlumb.getPositionOnElement(c,i,k),r={};a.jsPlumb.extend(r,l.def),r.isTemporarySource=!0,r.anchor=[q[0],q[1],0,0],r.dragOptions=p,l.def.scope&&(r.scope=l.def.scope),s=this.addEndpoint(e,r),t=!0,s.setDeleteOnEmpty(!0),l.uniqueEndpoint&&(l.endpoint?s.finalEndpoint=l.endpoint:(l.endpoint=s,s.setDeleteOnEmpty(!1)));var u=function(){f.off(s.canvas,"mouseup",u),f.off(d.el,"mouseup",u),t&&(t=!1,f.deleteEndpoint(s))};f.on(s.canvas,"mouseup",u),f.on(d.el,"mouseup",u);var v={};if(l.def.extract)for(var w in l.def.extract){var x=(c.srcElement||c.target).getAttribute(w);x&&(v[l.def.extract[w]]=x)}f.trigger(s.canvas,"mousedown",c,v),b.consume(c)}}}.bind(this);this.on(d.el,"mousedown",u),l.trigger=u,g.filter&&(b.isString(g.filter)||b.isFunction(g.filter))&&f.setDragFilter(d.el,g.filter);var v=a.jsPlumb.extend({},g.dropOptions||{});qa(d,g,v,!0,g.isTarget===!0)}.bind(this),o=c.length&&c.constructor!==String?c:[c],p=0,q=o.length;q>p;p++)n(l(o[p]));return this},this.unmakeSource=function(a,b,c){var d=l(a);f.destroyDroppable(d.el,"internal");var e=this.sourceEndpointDefinitions[d.id];if(e)for(var g in e)if(null==b||b===g){var h=e[g].trigger;h&&f.off(d.el,"mousedown",h),c||delete this.sourceEndpointDefinitions[d.id][g]}return this},this.unmakeEverySource=function(){for(var a in this.sourceEndpointDefinitions)f.unmakeSource(a,null,!0);return this.sourceEndpointDefinitions={},this};var ra=function(a,c,d){c=b.isArray(c)?c:[c];var e=T(a);d=d||"default";for(var f=0;fm;m++)i=l(c[m]),k[i.id]&&k[i.id][g]&&(h[m]=k[i.id][g].enabled,j=e?!h[m]:d,k[i.id][g].enabled=j,f[j?"removeClass":"addClass"](i.el,"jtk-"+a+"-disabled"))}else{i=l(c);var o=i.id;k[o]&&k[o][g]&&(h=k[o][g].enabled,j=e?!h:d,k[o][g].enabled=j,f[j?"removeClass":"addClass"](i.el,"jtk-"+a+"-disabled"))}return h}.bind(this),ua=function(a,c){return b.isString(a)||!a.length?c.apply(this,[a]):a.length?c.apply(this,[a[0]]):void 0}.bind(this);this.toggleSourceEnabled=function(a,b){return ta("source",a,null,!0,b),this.isSourceEnabled(a,b)},this.setSourceEnabled=function(a,b,c){return ta("source",a,b,null,c)},this.isSource=function(a,b){return b=b||"default",ua(a,function(a){var c=this.sourceEndpointDefinitions[l(a).id];return null!=c&&null!=c[b]}.bind(this))},this.isSourceEnabled=function(a,b){return b=b||"default",ua(a,function(a){var c=this.sourceEndpointDefinitions[l(a).id];return c&&c[b]&&c[b].enabled===!0}.bind(this))},this.toggleTargetEnabled=function(a,b){return ta("target",a,null,!0,b),this.isTargetEnabled(a,b)},this.isTarget=function(a,b){return b=b||"default",ua(a,function(a){var c=this.targetEndpointDefinitions[l(a).id];return null!=c&&null!=c[b]}.bind(this))},this.isTargetEnabled=function(a,b){return b=b||"default",ua(a,function(a){var c=this.targetEndpointDefinitions[l(a).id];return c&&c[b]&&c[b].enabled===!0}.bind(this))},this.setTargetEnabled=function(a,b,c){return ta("target",a,b,null,c)},this.ready=function(a){f.bind("ready",a)};var va=function(a,b){if("object"==typeof a&&a.length)for(var c=0,d=a.length;d>c;c++)b(a[c]);else b(a);return f};this.repaint=function(a,b,c){return va(a,function(a){H(a,b,c)})},this.revalidate=function(a,b,c){return va(a,function(a){var d=c?a:f.getId(a);f.updateOffset({elId:d,recalc:!0,timestamp:b});var e=f.getDragManager();e&&e.updateOffsets(d),f.repaint(a)})},this.repaintEverything=function(){var a,b=c();for(a in u)f.updateOffset({elId:a,recalc:!0,timestamp:b});for(a in u)H(a,null,b);return this},this.removeAllEndpoints=function(a,b,c){c=c||[];var d=function(a){var e,g,h=l(a),i=u[h.id];if(i)for(c.push(h),e=0,g=i.length;g>e;e++)f.deleteEndpoint(i[e],!1);if(delete u[h.id],b&&h.el&&3!==h.el.nodeType&&8!==h.el.nodeType)for(e=0,g=h.el.childNodes.length;g>e;e++)d(h.el.childNodes[e])};return d(a),this};var wa=function(a,b){f.removeAllEndpoints(a.id,!0,b);for(var c=f.getDragManager(),d=function(a){c&&c.elementRemoved(a.id),f.router.elementRemoved(a.id),f.isSource(a.el)&&f.unmakeSource(a.el),f.isTarget(a.el)&&f.unmakeTarget(a.el),f.destroyDraggable(a.el),f.destroyDroppable(a.el),delete f.floatingConnections[a.id],delete w[a.id],delete x[a.id],a.el&&(f.removeElement(a.el),a.el._jsPlumb=null)},e=1;e0;)d(e.el.childNodes[0]);b||wa(e,c)}};return f.batch(function(){d(a,!0)},b===!1),f},this.reset=function(a){f.silently(function(){ka=!1,f.removeAllGroups(),f.removeGroupManager(),f.deleteEveryEndpoint(),a||f.unbind(),this.targetEndpointDefinitions={},this.sourceEndpointDefinitions={},t.length=0,this.doReset&&this.doReset()}.bind(this))};var xa=function(a){a.canvas&&a.canvas.parentNode&&a.canvas.parentNode.removeChild(a.canvas),a.cleanup(),a.destroy()};this.clear=function(){f.select().each(xa),f.selectEndpoints().each(xa),u={},v={}},this.setDefaultScope=function(a){return D=a,f},this.deriveEndpointAndAnchorSpec=function(a,b){for(var c=((b?"":"default ")+a).split(/[\s]/),d=null,e=null,g=null,h=null,i=0;ih;h++)u[c][h].setElementId(c),u[c][h].setReferenceElement(a);delete u[e],this.sourceEndpointDefinitions[c]=this.sourceEndpointDefinitions[e],delete this.sourceEndpointDefinitions[e],this.targetEndpointDefinitions[c]=this.targetEndpointDefinitions[e],delete this.targetEndpointDefinitions[e],this.router.changeId(e,c);var j=this.getDragManager();j&&j.changeId(e,c),w[c]=w[e],delete w[e];var k=function(b,d,e){for(var f=0,g=b.length;g>f;f++)b[f].endpoints[d].setElementId(c),b[f].endpoints[d].setReferenceElement(a),b[f][e+"Id"]=c,b[f][e]=a};k(f,0,"source"),k(g,1,"target"),this.repaint(c)},this.setDebugLog=function(a){r=a},this.setSuspendDrawing=function(a,b){var c=B;return B=a,C=a?(new Date).getTime():null,b&&this.repaintEverything(),c},this.isSuspendDrawing=function(){return B},this.getSuspendedAt=function(){return C},this.batch=function(a,c){var d=this.isSuspendDrawing();d||this.setSuspendDrawing(!0);try{a()}catch(e){b.log("Function run while suspended failed",e)}d||this.setSuspendDrawing(!1,!c)},this.doWhileSuspended=this.batch,this.getCachedData=S,this.timestamp=c,this.show=function(a,b){return Q(a,"block",b),f},this.toggleVisible=R,this.addListener=this.bind;var ya=[];this.registerFloatingConnection=function(a,c,d){ya[a.id]=c,b.addToList(u,a.id,d)},this.getFloatingConnectionFor=function(a){return ya[a]},this.listManager=new a.jsPlumbListManager(this,this.Defaults.ListStyle)};b.extend(a.jsPlumbInstance,b.EventGenerator,{setAttribute:function(a,b,c){this.setAttribute(a,b,c)},getAttribute:function(b,c){return this.getAttribute(a.jsPlumb.getElement(b),c)},convertToFullOverlaySpec:function(a){return b.isString(a)&&(a=[a,{}]),a[1].id=a[1].id||b.uuid(),a},registerConnectionType:function(b,c){if(this._connectionTypes[b]=a.jsPlumb.extend({},c),c.overlays){for(var d={},e=0;ea;a++)this.removeOverlay(arguments[a])},moveParent:function(a){if(this.bgCanvas&&(this.bgCanvas.parentNode.removeChild(this.bgCanvas),a.appendChild(this.bgCanvas)),this.canvas&&this.canvas.parentNode){this.canvas.parentNode.removeChild(this.canvas),a.appendChild(this.canvas);for(var b in this._jsPlumb.overlays)if(this._jsPlumb.overlays[b].isAppendedAtTopLevel){var c=this._jsPlumb.overlays[b].getElement();c.parentNode.removeChild(c),a.appendChild(c)}}},getLabel:function(){var a=this.getOverlay(d);return null!=a?a.getLabel():null},getLabelOverlay:function(){return this.getOverlay(d)},setLabel:function(a){var b=this.getOverlay(d);if(b)a.constructor===String||a.constructor===Function?b.setLabel(a):(a.label&&b.setLabel(a.label),a.location&&b.setLocation(a.location));else{var c=a.constructor===String||a.constructor===Function?{label:a}:a;b=e(this,c),this._jsPlumb.overlays[d]=b}this._jsPlumb.instance.isSuspendDrawing()||this.repaint()},cleanup:function(a){for(var b in this._jsPlumb.overlays)this._jsPlumb.overlays[b].cleanup(a),this._jsPlumb.overlays[b].destroy(a);a&&(this._jsPlumb.overlays={},this._jsPlumb.overlayPositions=null)},setVisible:function(a){this[a?"showOverlays":"hideOverlays"]()},setAbsoluteOverlayPosition:function(a,b){this._jsPlumb.overlayPositions[a.id]=b},getAbsoluteOverlayPosition:function(a){return this._jsPlumb.overlayPositions?this._jsPlumb.overlayPositions[a.id]:null},_clazzManip:function(a,b,c){if(!c)for(var d in this._jsPlumb.overlays)this._jsPlumb.overlays[d][a+"Class"](b)},addClass:function(a,b){this._clazzManip("add",a,b)},removeClass:function(a,b){this._clazzManip("remove",a,b)}})}.call("undefined"!=typeof window?window:this),function(){"use strict";var a=this,b=a.jsPlumb,c=a.jsPlumbUtil,d=function(a,b,c){var d=!1;return{drag:function(){if(d)return d=!1,!0;if(b.element){var e=c.getUIPosition(arguments,c.getZoom());null!=e&&c.setPosition(b.element,e),c.repaint(b.element,e),a.paint({anchorPoint:a.anchor.getCurrentLocation({element:a})})}},stopDrag:function(){d=!0}}},e=function(a,b,c,d){var e=b.createElement("div",{position:"absolute"});b.appendElement(e);var f=b.getId(e);b.setPosition(e,c),e.style.width=d[0]+"px",e.style.height=d[1]+"px",b.manage(f,e,!0),a.id=f,a.element=e},f=function(a,c,d,e,f,g,h,i){var j=new b.FloatingAnchor({reference:c,referenceCanvas:e,jsPlumbInstance:g});return h({paintStyle:a,endpoint:d,anchor:j,source:f,scope:i})},g=["connectorStyle","connectorHoverStyle","connectorOverlays","connector","connectionType","connectorClass","connectorHoverClass"],h=function(a,b){var c=0;if(null!=b)for(var d=0;d0)for(var b=0;b0?"add":"remove")+"Class"](i.endpointConnectedClass),this[(this.isFull()?"add":"remove")+"Class"](i.endpointFullClass)},this.detachFromConnection=function(a,b,c){b=null==b?this.connections.indexOf(a):b,b>=0&&(this.connections.splice(b,1),this[(this.connections.length>0?"add":"remove")+"Class"](i.endpointConnectedClass),this[(this.isFull()?"add":"remove")+"Class"](i.endpointFullClass)),!c&&m&&0===this.connections.length&&i.deleteObject({endpoint:this,fireEvent:!1,deleteAttachedObjects:c!==!0})},this.deleteEveryConnection=function(a){for(var b=this.connections.length,c=0;b>c;c++)i.deleteConnection(this.connections[0],a)},this.detachFrom=function(a,b,c){for(var d=[],e=0;ef;f++)i.deleteConnection(d[0]);return this},this.getElement=function(){return this.element},this.setElement=function(d){var e=this._jsPlumb.instance.getId(d),f=this.elementId;return c.removeWithFunction(a.endpointsByElement[this.elementId],function(a){return a.id===this.id}.bind(this)),this.element=b.getElement(d),this.elementId=i.getId(this.element),i.anchorManager.rehomeEndpoint(this,f,this.element),i.dragManager.endpointAdded(this.element),c.addToList(a.endpointsByElement,e,this),this},this.makeInPlaceCopy=function(){var b=this.anchor.getCurrentLocation({element:this}),c=this.anchor.getOrientation(this),d=this.anchor.getCssClass(),e={bind:function(){},compute:function(){return[b[0],b[1]]},getCurrentLocation:function(){return[b[0],b[1]]},getOrientation:function(){return c},getCssClass:function(){return d}};return k({dropOptions:a.dropOptions,anchor:e,source:this.element,paintStyle:this.getPaintStyle(),endpoint:a.hideOnDrag?"Blank":this.endpoint,_transient:!0,scope:this.scope,reference:this})},this.connectorSelector=function(){return this.connections[0]},this.setStyle=this.setPaintStyle,this.paint=function(a){a=a||{};var b=a.timestamp,c=!(a.recalc===!1);if(!b||this.timestamp!==b){var d=i.updateOffset({elId:this.elementId,timestamp:b}),e=a.offset?a.offset.o:d.o;if(null!=e){var f=a.anchorPoint,g=a.connectorPaintStyle;if(null==f){var j=a.dimensions||d.s,k={xy:[e.left,e.top],wh:j,element:this,timestamp:b};if(c&&this.anchor.isDynamic&&this.connections.length>0){var l=h(this,a.elementWithPrecedence),m=l.endpoints[0]===this?1:0,n=0===m?l.sourceId:l.targetId,o=i.getCachedData(n),p=o.o,q=o.s;k.index=0===m?1:0,k.connection=l,k.txy=[p.left,p.top],k.twh=q,k.tElement=l.endpoints[m]}else this.connections.length>0&&(k.connection=this.connections[0]);f=this.anchor.compute(k)}this.endpoint.compute(f,this.anchor.getOrientation(this),this._jsPlumb.paintStyleInUse,g||this.paintStyleInUse),this.endpoint.paint(this._jsPlumb.paintStyleInUse,this.anchor),this.timestamp=b;for(var r in this._jsPlumb.overlays)if(this._jsPlumb.overlays.hasOwnProperty(r)){var s=this._jsPlumb.overlays[r];s.isVisible()&&(this._jsPlumb.overlayPlacements[r]=s.draw(this.endpoint,this._jsPlumb.paintStyleInUse),s.paint(this._jsPlumb.overlayPlacements[r]))}}}},this.getTypeDescriptor=function(){return"endpoint"},this.isVisible=function(){return this._jsPlumb.visible},this.repaint=this.paint;var p=!1;this.initDraggable=function(){if(!p&&b.isDragSupported(this.element)){var g,h={id:null,element:null},m=null,n=!1,o=null,q=d(this,h,i),r=a.dragOptions||{},s={},t=b.dragEvents.start,u=b.dragEvents.stop,v=b.dragEvents.drag,w=b.dragEvents.beforeStart,x=function(a){g=a.e.payload||{}},y=function(c){m=this.connectorSelector();var d=!0;this.isEnabled()||(d=!1),null!=m||this.isSource||this.isTemporarySource||(d=!1),!this.isSource||!this.isFull()||null!=m&&this.dragAllowedWhenFull||(d=!1),null==m||m.isDetachable(this)||(this.isFull()?d=!1:m=null);var l=i.checkCondition(null==m?"beforeDrag":"beforeStartDetach",{endpoint:this,source:this.element,sourceId:this.elementId,connection:m});if(l===!1?d=!1:"object"==typeof l?b.extend(l,g||{}):l=g||{},d===!1)return i.stopDrag&&i.stopDrag(this.canvas),q.stopDrag(),!1;for(var p=0;p0&&this.connectionsDetachable!==!1}.bind(this),i.initDraggable(this.canvas,r,"internal"),this.canvas._jsPlumbRelatedElement=this.element,p=!0}};var q=a.endpoint||this._jsPlumb.instance.Defaults.Endpoint||b.Defaults.Endpoint;this.setEndpoint(q,!0);var r=a.anchor?a.anchor:a.anchors?a.anchors:i.Defaults.Anchor||"Top";this.setAnchor(r,!0);var s=["default",a.type||""].join(" ");this.addType(s,a.data,!0),this.canvas=this.endpoint.canvas,this.canvas._jsPlumb=this,this.initDraggable();var t=function(d,e,f,g){if(b.isDropSupported(this.element)){var h=a.dropOptions||i.Defaults.DropOptions||b.Defaults.DropOptions;h=b.extend({},h),h.scope=h.scope||this.scope;var j=b.dragEvents.drop,k=b.dragEvents.over,l=b.dragEvents.out,m=this,n=i.EndpointDropHandler({getEndpoint:function(){return m},jsPlumb:i,enabled:function(){return null!=f?f.isEnabled():!0},isFull:function(){return f.isFull()},element:this.element,elementId:this.elementId,isSource:this.isSource,isTarget:this.isTarget,addClass:function(a){m.addClass(a)},removeClass:function(a){m.removeClass(a)},isDropAllowed:function(){return m.isDropAllowed.apply(m,arguments)},reference:g,isRedrop:function(a,b){return a.suspendedEndpoint&&b.reference&&a.suspendedEndpoint.id===b.reference.id}});h[j]=c.wrap(h[j],n,!0),h[k]=c.wrap(h[k],function(){var a=b.getDragObject(arguments),c=i.getAttribute(b.getElement(a),"dragId"),d=i.getFloatingConnectionFor(c);if(null!=d){var e=i.getFloatingAnchorIndex(d),f=this.isTarget&&0!==e||d.suspendedEndpoint&&this.referenceEndpoint&&this.referenceEndpoint.id===d.suspendedEndpoint.id;if(f){var g=i.checkCondition("checkDropAllowed",{sourceEndpoint:d.endpoints[e],targetEndpoint:this,connection:d});this[(g?"add":"remove")+"Class"](i.endpointDropAllowedClass),this[(g?"remove":"add")+"Class"](i.endpointDropForbiddenClass),d.endpoints[e].anchor.over(this.anchor,this)}}}.bind(this)),h[l]=c.wrap(h[l],function(){var a=b.getDragObject(arguments),c=null==a?null:i.getAttribute(b.getElement(a),"dragId"),d=c?i.getFloatingConnectionFor(c):null;if(null!=d){var e=i.getFloatingAnchorIndex(d),f=this.isTarget&&0!==e||d.suspendedEndpoint&&this.referenceEndpoint&&this.referenceEndpoint.id===d.suspendedEndpoint.id;f&&(this.removeClass(i.endpointDropAllowedClass),this.removeClass(i.endpointDropForbiddenClass),d.endpoints[e].anchor.out())}}.bind(this)),i.initDroppable(d,h,"internal",e)}}.bind(this);return this.anchor.isFloating||t(this.canvas,!(a._transient||this.anchor.isFloating),this,a.reference),this},c.extend(b.Endpoint,b.OverlayCapableJsPlumbUIComponent,{setVisible:function(a,b,c){if(this._jsPlumb.visible=a,this.canvas&&(this.canvas.style.display=a?"block":"none"),this[a?"showOverlays":"hideOverlays"](),!b)for(var d=0;d0)for(var k=p(g,o[b]),m="right"===b||"top"===b,n=l(b,c,d,k,h,i,m),q=function(a,b){e[a.id]=[b[0],b[1],b[2],b[3]],f[a.id]=j},r=0;r-1&&(g[e.targetId][i][0]=e,g[e.targetId][i][1]=e.endpoints[0],g[e.targetId][i][2]=e.endpoints[0].anchor.constructor===c.DynamicAnchor),b.addToList(g,d,[e,e.endpoints[1],e.endpoints[1].anchor.constructor===c.DynamicAnchor]),e.endpoints[1].anchor.isContinuous&&(e.source===e.target?e._jsPlumb.instance.removeElement(e.endpoints[1].canvas):null==e.endpoints[1].canvas.parentNode&&e._jsPlumb.instance.appendElement(e.endpoints[1].canvas)),e.updateConnectedClass()}}else if(1===h){var j=e.endpoints[0].elementId;e.target=f,e.targetId=d;var k=b.findWithFunction(g[j],function(a){return a[0].id===e.id}),l=b.findWithFunction(g[a],function(a){return a[0].id===e.id});-1!==k&&(g[j][k][0]=e,g[j][k][1]=e.endpoints[1],g[j][k][2]=e.endpoints[1].anchor.constructor===c.DynamicAnchor),l>-1&&(g[a].splice(l,1),b.addToList(g,d,[e,e.endpoints[0],e.endpoints[0].anchor.constructor===c.DynamicAnchor])),e.updateConnectedClass()}},this.rehomeEndpoint=function(a,b,c){var e=d[b]||[],f=j.getId(c);if(f!==b){var g=e.indexOf(a);if(g>-1){var i=e.splice(g,1)[0];h.add(i,f)}}for(var k=0;kb.dist?1:0});for(var o=k[0].source,p=k[0].target,q=0;q0?this.anchors[0]:null,f=e,g=this,h=function(a,b,c,d,e){var f=d[0]+a.x*e[0],g=d[1]+a.y*e[1],h=d[0]+e[0]/2,i=d[1]+e[1]/2;return Math.sqrt(Math.pow(b-f,2)+Math.pow(c-g,2))+Math.sqrt(Math.pow(h-f,2)+Math.pow(i-g,2))},i=a.selector||function(a,b,c,d,e){for(var f=c[0]+d[0]/2,g=c[1]+d[1]/2,i=-1,j=1/0,k=0;kl&&(i=k+0,j=l)}return e[i]};this.compute=function(a){var b=a.xy,c=a.wh,d=a.txy,h=a.twh;this.timestamp=a.timestamp;var j=g.getUserDefinedLocation();return null!=j?j:this.isLocked()||null==d||null==h?e.compute(a):(a.timestamp=null,e=i(b,c,d,h,this.anchors),this.x=e.x,this.y=e.y,e!==f&&this.fire("anchorChanged",e),f=e,e.compute(a))},this.getCurrentLocation=function(a){return this.getUserDefinedLocation()||(null!=e?e.getCurrentLocation(a):null)},this.getOrientation=function(a){return null!=e?e.getOrientation(a):[0,0]},this.over=function(a,b){null!=e&&e.over(a,b)},this.out=function(){null!=e&&e.out()},this.setAnchor=function(a){e=a},this.getCssClass=function(){return e&&e.getCssClass()||""},this.setAnchorCoordinates=function(a){var b=jsPlumbUtil.findWithFunction(this.anchors,function(b){return b.x===a[0]&&b.y===a[1]});return-1!==b?(this.setAnchor(this.anchors[b]),!0):!1}},b.extend(c.DynamicAnchor,c.Anchor);var e=function(a,b,d,e,f,g){c.Anchors[f]=function(c){var h=c.jsPlumbInstance.makeAnchor([a,b,d,e,0,0],c.elementId,c.jsPlumbInstance);return h.type=f,g&&g(h,c),h}};e(.5,0,0,-1,"TopCenter"),e(.5,1,0,1,"BottomCenter"),e(0,.5,-1,0,"LeftMiddle"),e(1,.5,1,0,"RightMiddle"),e(.5,0,0,-1,"Top"),e(.5,1,0,1,"Bottom"),e(0,.5,-1,0,"Left"),e(1,.5,1,0,"Right"),e(.5,.5,0,0,"Center"),e(1,0,0,-1,"TopRight"),e(1,1,0,1,"BottomRight"),e(0,0,0,-1,"TopLeft"),e(0,1,0,1,"BottomLeft"),c.Defaults.DynamicAnchors=function(a){return a.jsPlumbInstance.makeAnchors(["TopCenter","RightMiddle","BottomCenter","LeftMiddle"],a.elementId,a.jsPlumbInstance)},c.Anchors.AutoDefault=function(a){var b=a.jsPlumbInstance.makeDynamicAnchor(c.Defaults.DynamicAnchors(a));return b.type="AutoDefault",b};var f=function(a,b){c.Anchors[a]=function(c){var d=c.jsPlumbInstance.makeAnchor(["Continuous",{faces:b}],c.elementId,c.jsPlumbInstance);return d.type=a,d}};c.Anchors.Continuous=function(a){return a.jsPlumbInstance.continuousAnchorFactory.get(a)},f("ContinuousLeft",["left"]),f("ContinuousTop",["top"]),f("ContinuousBottom",["bottom"]),f("ContinuousRight",["right"]),e(0,0,0,0,"Assign",function(a,b){var c=b.position||"Fixed";a.positionFinder=c.constructor===String?b.jsPlumbInstance.AnchorPositionFinders[c]:c,a.constructorParams=b}),a.jsPlumbInstance.prototype.AnchorPositionFinders={Fixed:function(a,b,c){return[(a.left-b.left)/c[0],(a.top-b.top)/c[1]]},Grid:function(a,b,c,d){var e=a.left-b.left,f=a.top-b.top,g=c[0]/d.grid[0],h=c[1]/d.grid[1],i=Math.floor(e/g),j=Math.floor(f/h);return[(i*g+g/2)/c[0],(j*h+h/2)/c[1]]}},c.Anchors.Perimeter=function(a){a=a||{};var b=a.anchorCount||60,c=a.shape;if(!c)throw new Error("no shape supplied to Perimeter Anchor type");var d=function(){for(var a=.5,c=2*Math.PI/b,d=0,e=[],f=0;b>f;f++){var g=a+a*Math.sin(d),h=a+a*Math.cos(d);e.push([g,h,0,0]),d+=c}return e},e=function(a){for(var c=b/a.length,d=[],e=function(a,e,f,g,h,i,j){c=b*h;for(var k=(f-a)/c,l=(g-e)/c,m=0;c>m;m++)d.push([a+k*m,e+l*m,null==i?0:i,null==j?0:j])},f=0;f0?a:c+a:a*c;return d.pointOnLine({x:g,y:i},{x:h,y:j},e)}return{x:h,y:j}}return{x:g,y:i}},this.gradientAtPoint=function(a){return e},this.pointAlongPathFrom=function(a,b,c){var e=this.pointOnPath(a,c),f=0>=b?{x:g,y:i}:{x:h,y:j};return 0>=b&&Math.abs(b)>1&&(b*=-1),d.pointOnLine(e,f,b)};var l=function(a,b,c){return c>=Math.min(a,b)&&c<=Math.max(a,b)},m=function(a,b,c){return Math.abs(c-a)b?a>=b&&c>=a:b>=a&&a>=c},o=n;this.lineIntersection=function(a,b,c,f){var k=Math.abs(d.gradient({x:a,y:b},{x:c,y:f})),l=Math.abs(e),m=l===1/0?g:i-l*g,n=[],p=k===1/0?a:b-k*a;if(k!==l)if(k===1/0&&0===l)o(a,g,h)&&o(i,b,f)&&(n=[a,i]);else if(0===k&&l===1/0)o(b,i,j)&&o(g,a,c)&&(n=[g,b]);else{var q,r;k===1/0?(q=a,o(q,g,h)&&(r=l*a+m,o(r,b,f)&&(n=[q,r]))):0===k?(r=b,o(r,i,j)&&(q=(b-m)/l,o(q,a,c)&&(n=[q,r]))):(q=(p-m)/(l-k),r=l*q+m,o(q,g,h)&&o(r,i,j)&&(n=[q,r]))}return n},this.boxIntersection=function(a,b,c,d){var e=[];return e.push.apply(e,this.lineIntersection(a,b,a+c,b)),e.push.apply(e,this.lineIntersection(a+c,b,a+c,b+d)),e.push.apply(e,this.lineIntersection(a+c,b+d,a,b+d)),e.push.apply(e,this.lineIntersection(a,b+d,a,b)),e},this.boundingBoxIntersection=function(a){return this.boxIntersection(a.x,a.y,a.w,a.h)}},Arc:function(a){var c=(b.Segments.AbstractSegment.apply(this,arguments),function(b,c){return d.theta([a.cx,a.cy],[b,c])}),e=function(a,b){if(a.anticlockwise){var c=a.startAnglea-b?b:k>c-a?c:a};this.pointOnPath=function(b,c){if(0===b)return{x:this.x1,y:this.y1,theta:this.startAngle};if(1===b)return{x:this.x2,y:this.y2,theta:this.endAngle};c&&(b/=j);var d=e(this,b),f=a.cx+a.r*Math.cos(d),g=a.cy+a.r*Math.sin(d);return{x:l(f),y:l(g),theta:d}},this.gradientAtPoint=function(b,c){var e=this.pointOnPath(b,c),f=d.normal([a.cx,a.cy],[e.x,e.y]);return this.anticlockwise||f!==1/0&&f!==-(1/0)||(f*=-1),f},this.pointAlongPathFrom=function(b,c,d){var e=this.pointOnPath(b,d),f=c/h*2*Math.PI,g=this.anticlockwise?-1:1,i=e.theta+g*f,j=a.cx+this.radius*Math.cos(i),k=a.cy+this.radius*Math.sin(i);return{x:j,y:k}}},Bezier:function(c){this.curve=[{x:c.x1,y:c.y1},{x:c.cp1x,y:c.cp1y},{x:c.cp2x,y:c.cp2y},{x:c.x2,y:c.y2}];var d=function(a){return a[0].x===a[1].x&&a[0].y===a[1].y},e=function(a,b){return Math.sqrt(Math.pow(a.x-b.x,2)+Math.pow(a.y-b.y,2))},f=function(a){var b={x:0,y:0};if(0===a)return this.curve[0];var c=this.curve.length-1;if(1===a)return this.curve[c];var d=this.curve,e=1-a;if(0===c)return this.curve[0];if(1===c)return{x:e*d[0].x+a*d[1].x,y:e*d[0].y+a*d[1].y};if(4>c){var f,g,h,i=e*e,j=a*a,k=0;return 2===c?(d=[d[0],d[1],d[2],b],f=i,g=2*(e*a),h=j):3===c&&(f=i*e,g=3*(i*a),h=3*(e*j),k=a*j),{x:f*d[0].x+g*d[1].x+h*d[2].x+k*d[3].x,y:f*d[0].y+g*d[1].y+h*d[2].y+k*d[3].y}}return b}.bind(this),g=function(a){var b=[];a--;for(var c=0;a>=c;c++)b.push(f(c/a));return b},h=function(){d(this.curve)&&(this.length=0);var a=16,b=g(a);this.length=0;for(var c=0;a-1>c;c++){var f=b[c],h=b[c+1];this.length+=e(f,h)}}.bind(this);b.Segments.AbstractSegment.apply(this,arguments);this.bounds={minX:Math.min(c.x1,c.x2,c.cp1x,c.cp2x),minY:Math.min(c.y1,c.y2,c.cp1y,c.cp2y),maxX:Math.max(c.x1,c.x2,c.cp1x,c.cp2x),maxY:Math.max(c.y1,c.y2,c.cp1y,c.cp2y)},this.type="Bezier",h();var i=function(b,c,d){return d&&(c=a.jsBezier.locationAlongCurveFrom(b,c>0?0:1,c)),c};this.pointOnPath=function(b,c){return b=i(this.curve,b,c),a.jsBezier.pointOnCurve(this.curve,b)},this.gradientAtPoint=function(b,c){return b=i(this.curve,b,c),a.jsBezier.gradientAtPoint(this.curve,b)},this.pointAlongPathFrom=function(b,c,d){return b=i(this.curve,b,d),a.jsBezier.pointAlongCurveFrom(this.curve,b,c)},this.getLength=function(){return this.length},this.getBounds=function(){return this.bounds},this.findClosestPointOnPath=function(b,c){var d=a.jsBezier.nearestPointOnCurve({x:b,y:c},this.curve);return{d:Math.sqrt(Math.pow(d.point.x-b,2)+Math.pow(d.point.y-c,2)),x:d.point.x,y:d.point.y,l:1-d.location,s:this}},this.lineIntersection=function(b,c,d,e){return a.jsBezier.lineIntersection(b,c,d,e,this.curve)}}},b.SegmentRenderer={getPath:function(a,b){return{Straight:function(b){var c=a.getCoordinates();return(b?"M "+c.x1+" "+c.y1+" ":"")+"L "+c.x2+" "+c.y2},Bezier:function(b){var c=a.params;return(b?"M "+c.x2+" "+c.y2+" ":"")+"C "+c.cp2x+" "+c.cp2y+" "+c.cp1x+" "+c.cp1y+" "+c.x1+" "+c.y1},Arc:function(b){var c=a.params,d=a.sweep>Math.PI?1:0,e=a.anticlockwise?0:1;return(b?"M"+a.x1+" "+a.y1+" ":"")+"A "+a.radius+" "+c.r+" 0 "+d+","+e+" "+a.x2+" "+a.y2}}[a.type](b)}};var e=function(){this.resetBounds=function(){this.bounds={minX:1/0,minY:1/0,maxX:-(1/0),maxY:-(1/0)}},this.resetBounds()};b.Connectors.AbstractConnector=function(a){e.apply(this,arguments);var f=[],g=0,h=[],i=[],j=a.stub||0,k=c.isArray(j)?j[0]:j,l=c.isArray(j)?j[1]:j,m=a.gap||0,n=c.isArray(m)?m[0]:m,o=c.isArray(m)?m[1]:m,p=null,q=null;this.getPathData=function(){for(var a="",c=0;c0?a/g:(g+a)/g),1===a)c=f.length-1,e=1;else if(0===a)e=0,c=0;else if(a>=.5){for(c=0,e=0,d=h.length-1;d>-1;d--)if(h[d][1]>=a&&h[d][0]<=a){c=d,e=(a-h[d][0])/i[d];break}}else for(c=h.length-1,e=1,d=0;d=a){c=d,e=(a-h[d][0])/i[d];break}return{segment:f[c],proportion:e,index:c}},t=function(a,c,d){if(d.x1!==d.x2||d.y1!==d.y2){var e=new b.Segments[c](d);f.push(e),g+=e.getLength(),a.updateBounds(e)}},u=function(){g=f.length=h.length=i.length=0};this.setSegments=function(a){p=[],g=0;for(var b=0;bp?0:1,r=[1,0][q];g=[],h=[],g[q]=a.sourcePos[q]>a.targetPos[q]?-1:1,h[q]=a.sourcePos[q]>a.targetPos[q]?1:-1,g[r]=0,h[r]=0}var s=c?m+n*g[0]:n*g[0],t=e?p+n*g[1]:n*g[1],u=c?o*h[0]:m+o*h[0],v=e?o*h[1]:p+o*h[1],w=g[0]*h[0]+g[1]*h[1],x={sx:s,sy:t,tx:u,ty:v,lw:f,xSpan:Math.abs(u-s),ySpan:Math.abs(v-t),mx:(s+u)/2,my:(t+v)/2,so:g,to:h,x:i,y:j,w:m,h:p,segment:b,startStubX:s+g[0]*k,startStubY:t+g[1]*k,endStubX:u+h[0]*l,endStubY:v+h[1]*l,isXGreaterThanStubTimes2:Math.abs(s-u)>k+l,isYGreaterThanStubTimes2:Math.abs(t-v)>k+l,opposite:-1===w,perpendicular:0===w,orthogonal:1===w,sourceAxis:0===g[0]?"y":"x",points:[i,j,m,p,s,t,u,v],stubs:[k,l]};return x.anchorOrientation=x.opposite?"opposite":x.orthogonal?"orthogonal":"perpendicular",x};this.getSegments=function(){return f},this.updateBounds=function(a){var b=a.getBounds();this.bounds.minX=Math.min(this.bounds.minX,b.minX),this.bounds.maxX=Math.max(this.bounds.maxX,b.maxX),this.bounds.minY=Math.min(this.bounds.minY,b.minY),this.bounds.maxY=Math.max(this.bounds.maxY,b.maxY)};return this.pointOnPath=function(a,b){var c=s(a,b);return c.segment&&c.segment.pointOnPath(c.proportion,!1)||[0,0]},this.gradientAtPoint=function(a,b){var c=s(a,b);return c.segment&&c.segment.gradientAtPoint(c.proportion,!1)||0},this.pointAlongPathFrom=function(a,b,c){var d=s(a,c);return d.segment&&d.segment.pointAlongPathFrom(d.proportion,b,!1)||[0,0]},this.compute=function(a){q=v.call(this,a),u(),this._compute(q,a),this.x=q.points[0],this.y=q.points[1],this.w=q.points[2],this.h=q.points[3],this.segment=q.segment,r()},{addSegment:t,prepareCompute:v,sourceStub:k,targetStub:l,maxStub:Math.max(k,l),sourceGap:n,targetGap:o,maxGap:Math.max(n,o)}},c.extend(b.Connectors.AbstractConnector,e),b.Endpoints.AbstractEndpoint=function(a){e.apply(this,arguments);var b=this.compute=function(a,b,c,d){var e=this._compute.apply(this,arguments);return this.x=e[0],this.y=e[1],this.w=e[2],this.h=e[3],this.bounds.minX=this.x,this.bounds.minY=this.y,this.bounds.maxX=this.x+this.w,this.bounds.maxY=this.y+this.h,e};return{compute:b,cssClass:a.cssClass}},c.extend(b.Endpoints.AbstractEndpoint,e),b.Endpoints.Dot=function(a){this.type="Dot";b.Endpoints.AbstractEndpoint.apply(this,arguments);a=a||{},this.radius=a.radius||10,this.defaultOffset=.5*this.radius,this.defaultInnerRadius=this.radius/3,this._compute=function(a,b,c,d){this.radius=c.radius||this.radius;var e=a[0]-this.radius,f=a[1]-this.radius,g=2*this.radius,h=2*this.radius;if(c.stroke){var i=c.strokeWidth||1;e-=i,f-=i,g+=2*i,h+=2*i}return[e,f,g,h,this.radius]}},c.extend(b.Endpoints.Dot,b.Endpoints.AbstractEndpoint),b.Endpoints.Rectangle=function(a){this.type="Rectangle";b.Endpoints.AbstractEndpoint.apply(this,arguments);a=a||{},this.width=a.width||20,this.height=a.height||20,this._compute=function(a,b,c,d){var e=c.width||this.width,f=c.height||this.height,g=a[0]-e/2,h=a[1]-f/2;return[g,h,e,f]}},c.extend(b.Endpoints.Rectangle,b.Endpoints.AbstractEndpoint);var f=function(a){b.jsPlumbUIComponent.apply(this,arguments),this._jsPlumb.displayElements=[]};c.extend(f,b.jsPlumbUIComponent,{getDisplayElements:function(){return this._jsPlumb.displayElements},appendDisplayElement:function(a){this._jsPlumb.displayElements.push(a)}}),b.Endpoints.Image=function(d){this.type="Image",f.apply(this,arguments),b.Endpoints.AbstractEndpoint.apply(this,arguments);var e=d.onload,g=d.src||d.url,h=d.cssClass?" "+d.cssClass:"";this._jsPlumb.img=new Image,this._jsPlumb.ready=!1,this._jsPlumb.initialized=!1,this._jsPlumb.deleted=!1,this._jsPlumb.widthToUse=d.width,this._jsPlumb.heightToUse=d.height,this._jsPlumb.endpoint=d.endpoint,this._jsPlumb.img.onload=function(){ +null!=this._jsPlumb&&(this._jsPlumb.ready=!0,this._jsPlumb.widthToUse=this._jsPlumb.widthToUse||this._jsPlumb.img.width,this._jsPlumb.heightToUse=this._jsPlumb.heightToUse||this._jsPlumb.img.height,e&&e(this))}.bind(this),this._jsPlumb.endpoint.setImage=function(a,b){var c=a.constructor===String?a:a.src;e=b,this._jsPlumb.img.src=c,null!=this.canvas&&this.canvas.setAttribute("src",this._jsPlumb.img.src)}.bind(this),this._jsPlumb.endpoint.setImage(g,e),this._compute=function(a,b,c,d){return this.anchorPoint=a,this._jsPlumb.ready?[a[0]-this._jsPlumb.widthToUse/2,a[1]-this._jsPlumb.heightToUse/2,this._jsPlumb.widthToUse,this._jsPlumb.heightToUse]:[0,0,0,0]},this.canvas=b.createElement("img",{position:"absolute",margin:0,padding:0,outline:0},this._jsPlumb.instance.endpointClass+h),this._jsPlumb.widthToUse&&this.canvas.setAttribute("width",this._jsPlumb.widthToUse),this._jsPlumb.heightToUse&&this.canvas.setAttribute("height",this._jsPlumb.heightToUse),this._jsPlumb.instance.appendElement(this.canvas),this.actuallyPaint=function(a,b,d){if(!this._jsPlumb.deleted){this._jsPlumb.initialized||(this.canvas.setAttribute("src",this._jsPlumb.img.src),this.appendDisplayElement(this.canvas),this._jsPlumb.initialized=!0);var e=this.anchorPoint[0]-this._jsPlumb.widthToUse/2,f=this.anchorPoint[1]-this._jsPlumb.heightToUse/2;c.sizeElement(this.canvas,e,f,this._jsPlumb.widthToUse,this._jsPlumb.heightToUse)}},this.paint=function(b,c){null!=this._jsPlumb&&(this._jsPlumb.ready?this.actuallyPaint(b,c):a.setTimeout(function(){this.paint(b,c)}.bind(this),200))}},c.extend(b.Endpoints.Image,[f,b.Endpoints.AbstractEndpoint],{cleanup:function(a){a&&(this._jsPlumb.deleted=!0,this.canvas&&this.canvas.parentNode.removeChild(this.canvas),this.canvas=null)}}),b.Endpoints.Blank=function(a){b.Endpoints.AbstractEndpoint.apply(this,arguments);this.type="Blank",f.apply(this,arguments),this._compute=function(a,b,c,d){return[a[0],a[1],10,0]};var d=a.cssClass?" "+a.cssClass:"";this.canvas=b.createElement("div",{display:"block",width:"1px",height:"1px",background:"transparent",position:"absolute"},this._jsPlumb.instance.endpointClass+d),this._jsPlumb.instance.appendElement(this.canvas),this.paint=function(a,b){c.sizeElement(this.canvas,this.x,this.y,this.w,this.h)}},c.extend(b.Endpoints.Blank,[b.Endpoints.AbstractEndpoint,f],{cleanup:function(){this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas)}}),b.Endpoints.Triangle=function(a){this.type="Triangle",b.Endpoints.AbstractEndpoint.apply(this,arguments);var c=this;a=a||{},a.width=a.width||55,a.height=a.height||55,this.width=a.width,this.height=a.height,this._compute=function(a,b,d,e){var f=d.width||c.width,g=d.height||c.height,h=a[0]-f/2,i=a[1]-g/2;return[h,i,f,g]}};var g=b.Overlays.AbstractOverlay=function(a){this.visible=!0,this.isAppendedAtTopLevel=!0,this.component=a.component,this.loc=null==a.location?.5:a.location,this.endpointLoc=null==a.endpointLocation?[.5,.5]:a.endpointLocation,this.visible=a.visible!==!1};g.prototype={cleanup:function(a){a&&(this.component=null,this.canvas=null,this.endpointLoc=null)},reattach:function(a,b){},setVisible:function(a){this.visible=a,this.component.repaint()},isVisible:function(){return this.visible},hide:function(){this.setVisible(!1)},show:function(){this.setVisible(!0)},incrementLocation:function(a){this.loc+=a,this.component.repaint()},setLocation:function(a){this.loc=a,this.component.repaint()},getLocation:function(){return this.loc},updateFrom:function(){}},b.Overlays.Arrow=function(a){this.type="Arrow",g.apply(this,arguments),this.isAppendedAtTopLevel=!1,a=a||{};var e=this;this.length=a.length||20,this.width=a.width||20,this.id=a.id,this.direction=(a.direction||1)<0?-1:1;var f=a.paintStyle||{"stroke-width":1},h=a.foldback||.623;this.computeMaxSize=function(){return 1.5*e.width},this.elementCreated=function(c,d){if(this.path=c,a.events)for(var e in a.events)b.on(c,e,a.events[e])},this.draw=function(a,b){var e,g,i,j,k;if(a.pointAlongPathFrom){if(c.isString(this.loc)||this.loc>1||this.loc<0){var l=parseInt(this.loc,10),m=this.loc<0?1:0;e=a.pointAlongPathFrom(m,l,!1),g=a.pointAlongPathFrom(m,l-this.direction*this.length/2,!1),i=d.pointOnLine(e,g,this.length)}else if(1===this.loc){if(e=a.pointOnPath(this.loc),g=a.pointAlongPathFrom(this.loc,-this.length),i=d.pointOnLine(e,g,this.length),-1===this.direction){var n=i;i=e,e=n}}else if(0===this.loc){if(i=a.pointOnPath(this.loc),g=a.pointAlongPathFrom(this.loc,this.length),e=d.pointOnLine(i,g,this.length),-1===this.direction){var o=i;i=e,e=o}}else e=a.pointAlongPathFrom(this.loc,this.direction*this.length/2),g=a.pointOnPath(this.loc),i=d.pointOnLine(e,g,this.length);j=d.perpendicularLineTo(e,i,this.width),k=d.pointOnLine(e,i,h*this.length);var p={hxy:e,tail:j,cxy:k},q=f.stroke||b.stroke,r=f.fill||b.stroke,s=f.strokeWidth||b.strokeWidth;return{component:a,d:p,"stroke-width":s,stroke:q,fill:r,minX:Math.min(e.x,j[0].x,j[1].x),maxX:Math.max(e.x,j[0].x,j[1].x),minY:Math.min(e.y,j[0].y,j[1].y),maxY:Math.max(e.y,j[0].y,j[1].y)}}return{component:a,minX:0,maxX:0,minY:0,maxY:0}}},c.extend(b.Overlays.Arrow,g,{updateFrom:function(a){this.length=a.length||this.length,this.width=a.width||this.width,this.direction=null!=a.direction?a.direction:this.direction,this.foldback=a.foldback||this.foldback},cleanup:function(){this.path&&this.path.parentNode&&this.path.parentNode.removeChild(this.path)}}),b.Overlays.PlainArrow=function(a){a=a||{};var c=b.extend(a,{foldback:1});b.Overlays.Arrow.call(this,c),this.type="PlainArrow"},c.extend(b.Overlays.PlainArrow,b.Overlays.Arrow),b.Overlays.Diamond=function(a){a=a||{};var c=a.length||40,d=b.extend(a,{length:c/2,foldback:2});b.Overlays.Arrow.call(this,d),this.type="Diamond"},c.extend(b.Overlays.Diamond,b.Overlays.Arrow);var h=function(a,b){return(null==a._jsPlumb.cachedDimensions||b)&&(a._jsPlumb.cachedDimensions=a.getDimensions()),a._jsPlumb.cachedDimensions},i=function(a){b.jsPlumbUIComponent.apply(this,arguments),g.apply(this,arguments);var d=this.fire;this.fire=function(){d.apply(this,arguments),this.component&&this.component.fire.apply(this.component,arguments)},this.detached=!1,this.id=a.id,this._jsPlumb.div=null,this._jsPlumb.initialised=!1,this._jsPlumb.component=a.component,this._jsPlumb.cachedDimensions=null,this._jsPlumb.create=a.create,this._jsPlumb.initiallyInvisible=a.visible===!1,this.getElement=function(){if(null==this._jsPlumb.div){var c=this._jsPlumb.div=b.getElement(this._jsPlumb.create(this._jsPlumb.component));c.style.position="absolute",jsPlumb.addClass(c,this._jsPlumb.instance.overlayClass+" "+(this.cssClass?this.cssClass:a.cssClass?a.cssClass:"")),this._jsPlumb.instance.appendElement(c),this._jsPlumb.instance.getId(c),this.canvas=c;var d="translate(-50%, -50%)";c.style.webkitTransform=d,c.style.mozTransform=d,c.style.msTransform=d,c.style.oTransform=d,c.style.transform=d,c._jsPlumb=this,a.visible===!1&&(c.style.display="none")}return this._jsPlumb.div},this.draw=function(a,b,d){var e=h(this);if(null!=e&&2===e.length){var f={x:0,y:0};if(d)f={x:d[0],y:d[1]};else if(a.pointOnPath){var g=this.loc,i=!1;(c.isString(this.loc)||this.loc<0||this.loc>1)&&(g=parseInt(this.loc,10),i=!0),f=a.pointOnPath(g,i)}else{var j=this.loc.constructor===Array?this.loc:this.endpointLoc;f={x:j[0]*a.w,y:j[1]*a.h}}var k=f.x-e[0]/2,l=f.y-e[1]/2;return{component:a,d:{minx:k,miny:l,td:e,cxy:f},minX:k,maxX:k+e[0],minY:l,maxY:l+e[1]}}return{minX:0,maxX:0,minY:0,maxY:0}}};c.extend(i,[b.jsPlumbUIComponent,g],{getDimensions:function(){return[1,1]},setVisible:function(a){this._jsPlumb.div&&(this._jsPlumb.div.style.display=a?"block":"none",a&&this._jsPlumb.initiallyInvisible&&(h(this,!0),this.component.repaint(),this._jsPlumb.initiallyInvisible=!1))},clearCachedDimensions:function(){this._jsPlumb.cachedDimensions=null},cleanup:function(a){a?null!=this._jsPlumb.div&&(this._jsPlumb.div._jsPlumb=null,this._jsPlumb.instance.removeElement(this._jsPlumb.div)):(this._jsPlumb&&this._jsPlumb.div&&this._jsPlumb.div.parentNode&&this._jsPlumb.div.parentNode.removeChild(this._jsPlumb.div),this.detached=!0)},reattach:function(a,b){null!=this._jsPlumb.div&&a.getContainer().appendChild(this._jsPlumb.div),this.detached=!1},computeMaxSize:function(){var a=h(this);return Math.max(a[0],a[1])},paint:function(a,b){this._jsPlumb.initialised||(this.getElement(),a.component.appendDisplayElement(this._jsPlumb.div),this._jsPlumb.initialised=!0,this.detached&&this._jsPlumb.div.parentNode.removeChild(this._jsPlumb.div)),this._jsPlumb.div.style.left=a.component.x+a.d.minx+"px",this._jsPlumb.div.style.top=a.component.y+a.d.miny+"px"}}),b.Overlays.Custom=function(a){this.type="Custom",i.apply(this,arguments)},c.extend(b.Overlays.Custom,i),b.Overlays.GuideLines=function(){var a=this;a.length=50,a.strokeWidth=5,this.type="GuideLines",g.apply(this,arguments),b.jsPlumbUIComponent.apply(this,arguments),this.draw=function(b,c){var e=b.pointAlongPathFrom(a.loc,a.length/2),f=b.pointOnPath(a.loc),g=d.pointOnLine(e,f,a.length),h=d.perpendicularLineTo(e,g,40),i=d.perpendicularLineTo(g,e,20);return{connector:b,head:e,tail:g,headLine:i,tailLine:h,minX:Math.min(e.x,g.x,i[0].x,i[1].x),minY:Math.min(e.y,g.y,i[0].y,i[1].y),maxX:Math.max(e.x,g.x,i[0].x,i[1].x),maxY:Math.max(e.y,g.y,i[0].y,i[1].y)}}},b.Overlays.Label=function(a){this.labelStyle=a.labelStyle;this.cssClass=null!=this.labelStyle?this.labelStyle.cssClass:null;var c=b.extend({create:function(){return b.createElement("div")}},a);if(b.Overlays.Custom.call(this,c),this.type="Label",this.label=a.label||"",this.labelText=null,this.labelStyle){var d=this.getElement();if(this.labelStyle.font=this.labelStyle.font||"12px sans-serif",d.style.font=this.labelStyle.font,d.style.color=this.labelStyle.color||"black",this.labelStyle.fill&&(d.style.background=this.labelStyle.fill),this.labelStyle.borderWidth>0){var e=this.labelStyle.borderStyle?this.labelStyle.borderStyle:"black";d.style.border=this.labelStyle.borderWidth+"px solid "+e}this.labelStyle.padding&&(d.style.padding=this.labelStyle.padding)}},c.extend(b.Overlays.Label,b.Overlays.Custom,{cleanup:function(a){a&&(this.div=null,this.label=null,this.labelText=null,this.cssClass=null,this.labelStyle=null)},getLabel:function(){return this.label},setLabel:function(a){this.label=a,this.labelText=null,this.clearCachedDimensions(),this.update(),this.component.repaint()},getDimensions:function(){return this.update(),i.prototype.getDimensions.apply(this,arguments)},update:function(){if("function"==typeof this.label){var a=this.label(this);this.getElement().innerHTML=a.replace(/\r\n/g,"
")}else null==this.labelText&&(this.labelText=this.label,this.getElement().innerHTML=this.labelText.replace(/\r\n/g,"
"))},updateFrom:function(a){null!=a.label&&this.setLabel(a.label)}})}.call("undefined"!=typeof window?window:this),function(){"use strict";var a=this,b=a.jsPlumbUtil,c=a.jsPlumbInstance,d="jtk-group-collapsed",e="jtk-group-expanded",f="[jtk-group-content]",g="elementDraggable",h="stop",i="revert",j="_groupManager",k="_jsPlumbGroup",l="_jsPlumbGroupDrag",m="group:addMember",n="group:removeMember",o="group:add",p="group:remove",q="group:expand",r="group:collapse",s="groupDragStop",t="connectionMoved",u="internal.connectionDetached",v="removeAll",w="orphanAll",x="show",y="hide",z=function(a){function c(b,c){for(var d=a.getContainer(),e=!1;!e;){if(null==b||b===d)return!1;if(b===c)return!0;b=b.parentNode}}function f(a){delete a.proxies;var c,d=j[a.id];null!=d&&(c=function(b){return b.id===a.id},b.removeWithFunction(d.connections.source,c),b.removeWithFunction(d.connections.target,c),delete j[a.id]),d=l[a.id],null!=d&&(c=function(b){return b.id===a.id},b.removeWithFunction(d.connections.source,c),b.removeWithFunction(d.connections.target,c),delete l[a.id])}function g(b,c){for(var d=b.getEl().querySelectorAll(".jtk-managed"),e=0;e0&&h0&&j0?a[0]:u},w=d.ghost===!0,x=w||d.constrain===!0,y=d.revert!==!1,z=d.orphan===!0,A=d.prune===!0,B=d.dropOverride===!0,C=d.proxied!==!1,D=[];if(this.connections={source:[],target:[],internal:[]},this.getAnchor=function(a,b){return d.anchor||"Continuous"},this.getEndpoint=function(a,b){return d.endpoint||["Dot",{radius:10}]},this.collapsed=!1,d.draggable!==!1){var E={drag:function(){for(var a=0;ad;d++){var f=D[0];t.remove(f,a,b,!0),c.remove(f,!0)}D.length=0,c.getGroupManager().updateConnectionsForGroup(t)},this.orphanAll=function(){for(var a={},b=0;ba?-1:0===a?0:1}),n=function(a){return[m(a[2]-a[0]),m(a[3]-a[1])]},o=function(a,b,c,d){if(j!==b||k!==c){var e=null==j?d.sx:j,f=null==k?d.sy:k,g=e===b?"v":"h";j=b,k=c,a.push([e,f,b,c,g])}},p=function(a){return Math.sqrt(Math.pow(a[0]-a[2],2)+Math.pow(a[1]-a[3],2))},q=function(a){var b=[];return b.push.apply(b,a),b},r=function(a,b,c){for(var f,h,i,j=null,k=0;k0&&j[4]!==f[4]){var m=Math.min(p(j),p(f)),o=Math.min(l,m/2);j[2]-=h[0]*o,j[3]-=h[1]*o,f[0]+=i[0]*o,f[1]+=i[1]*o;var r=h[1]===i[0]&&1===i[0]||h[1]===i[0]&&0===i[0]&&h[0]!==i[1]||h[1]===i[0]&&-1===i[0],s=f[1]>j[3]?1:-1,t=f[0]>j[2]?1:-1,u=s===t,v=u&&r||!u&&!r?f[0]:j[2],w=u&&r||!u&&!r?j[3]:f[1];g.addSegment(a,d,{x1:j[0],y1:j[1],x2:j[2],y2:j[3]}),g.addSegment(a,e,{r:o,x1:j[2],y1:j[3],x2:f[0],y2:f[1],cx:v,cy:w,ac:r})}else{var x=j[2]===j[0]?0:j[2]>j[0]?c.lw/2:-(c.lw/2),y=j[3]===j[1]?0:j[3]>j[1]?c.lw/2:-(c.lw/2);g.addSegment(a,d,{x1:j[0]-x,y1:j[1]-y,x2:j[2]+x,y2:j[3]+y})}j=f}null!=f&&g.addSegment(a,d,{x1:f[0],y1:f[1],x2:f[2],y2:f[3]})};this._compute=function(a,b){c=[],j=null,k=null,f=null;var d=function(){return[a.startStubX,a.startStubY,a.endStubX,a.endStubY]},e={perpendicular:d,orthogonal:d,opposite:function(b){var c=a,d="x"===b?0:1,e={x:function(){return 1===c.so[d]&&(c.startStubX>c.endStubX&&c.tx>c.startStubX||c.sx>c.endStubX&&c.tx>c.sx)||-1===c.so[d]&&(c.startStubXc.endStubY&&c.ty>c.startStubY||c.sy>c.endStubY&&c.ty>c.sy)||-1===c.so[d]&&(c.startStubYj[b][0],p=e[b][m][0],q=e[b][m][1],r=d[b][m][n];return c.segment===r[3]||c.segment===r[2]&&o?f[b]:c.segment===r[2]&&p>q?g[b]:c.segment===r[2]&&q>=p||c.segment===r[1]&&!o?i[b]:c.segment===r[0]||c.segment===r[1]&&o?h[b]:void 0},orthogonal:function(b,c,d,e,f){var g=a,h={x:-1===g.so[0]?Math.min(c,e):Math.max(c,e),y:-1===g.so[1]?Math.min(c,e):Math.max(c,e)}[b];return{x:[[h,d],[h,f],[e,f]],y:[[d,h],[f,h],[f,e]]}[b]},opposite:function(c,d,e,f){var h=a,i={x:"y",y:"x"}[c],j={x:"height",y:"width"}[c],k=h["is"+c.toUpperCase()+"GreaterThanStubTimes2"];if(b.sourceEndpoint.elementId===b.targetEndpoint.elementId){var l=e+(1-b.sourceEndpoint.anchor[i])*b.sourceInfo[j]+g.maxStub;return{x:[[d,l],[f,l]],y:[[l,d],[l,f]]}[c]}return!k||1===h.so[m]&&d>f||-1===h.so[m]&&f>d?{x:[[d,v],[f,v]],y:[[u,d],[u,f]]}[c]:1===h.so[m]&&f>d||-1===h.so[m]&&d>f?{x:[[u,h.sy],[u,h.ty]],y:[[h.sx,v],[h.tx,v]]}[c]:void 0}},y=x[a.anchorOrientation](a.sourceAxis,p,q,s,t);if(y)for(var z=0;z=a&&b>=d?1:c>=a&&d>=b?2:a>=c&&d>=b?3:4},e=function(a,b,c,d,e,f,g,h,i){return i>=h?[a,b]:1===c?d[3]<=0&&e[3]>=1?[a+(d[2]<.5?-1*f:f),b]:d[2]>=1&&e[2]<=0?[a,b+(d[3]<.5?-1*g:g)]:[a+-1*f,b+-1*g]:2===c?d[3]>=1&&e[3]<=0?[a+(d[2]<.5?-1*f:f),b]:d[2]>=1&&e[2]<=0?[a,b+(d[3]<.5?-1*g:g)]:[a+f,b+-1*g]:3===c?d[3]>=1&&e[3]<=0?[a+(d[2]<.5?-1*f:f),b]:d[2]<=0&&e[2]>=1?[a,b+(d[3]<.5?-1*g:g)]:[a+-1*f,b+-1*g]:4===c?d[3]<=0&&e[3]>=1?[a+(d[2]<.5?-1*f:f),b]:d[2]<=0&&e[2]>=1?[a,b+(d[3]<.5?-1*g:g)]:[a+f,b+-1*g]:void 0},f=function(a){a=a||{},this.type="StateMachine";var c,f=b.Connectors.AbstractBezierConnector.apply(this,arguments),g=a.curviness||10,h=a.margin||5,i=a.proximityLimit||80;a.orientation&&"clockwise"===a.orientation;this._computeBezier=function(a,b,j,k,l,m){var n=b.sourcePos[0]c?a.insertBefore(b,a.childNodes[c]):a.appendChild(b)};c.svg={node:t,attr:s,pos:u};var z=function(a){var d=a.pointerEventsSpec||"all",e={};b.jsPlumbUIComponent.apply(this,a.originalArgs),this.canvas=null,this.path=null,this.svg=null,this.bgCanvas=null;var f=a.cssClass+" "+(a.originalArgs[0].cssClass||""),g={style:"",width:0,height:0,"pointer-events":d,position:"absolute"};this.svg=t("svg",g),a.useDivWrapper?(this.canvas=b.createElement("div",{position:"absolute"}),c.sizeElement(this.canvas,0,0,1,1),this.canvas.className=f):(s(this.svg,{"class":f}),this.canvas=this.svg),a._jsPlumb.appendElement(this.canvas,a.originalArgs[0].parent),a.useDivWrapper&&this.canvas.appendChild(this.svg);var h=[this.canvas];return this.getDisplayElements=function(){ +return h},this.appendDisplayElement=function(a){h.push(a)},this.paint=function(b,d,f){if(null!=b){var g,h=[this.x,this.y],i=[this.w,this.h];null!=f&&(f.xmin<0&&(h[0]+=f.xmin),f.ymin<0&&(h[1]+=f.ymin),i[0]=f.xmax+(f.xmin<0?-f.xmin:0),i[1]=f.ymax+(f.ymin<0?-f.ymin:0)),a.useDivWrapper?(c.sizeElement(this.canvas,h[0],h[1],i[0]>0?i[0]:1,i[1]>0?i[1]:1),h[0]=0,h[1]=0,g=u([0,0])):g=u([h[0],h[1]]),e.paint.apply(this,arguments),s(this.svg,{style:g,width:i[0]||1,height:i[1]||1})}},{renderer:e}};c.extend(z,b.jsPlumbUIComponent,{cleanup:function(a){a||null==this.typeId?(this.canvas&&(this.canvas._jsPlumb=null),this.svg&&(this.svg._jsPlumb=null),this.bgCanvas&&(this.bgCanvas._jsPlumb=null),this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.bgCanvas&&this.bgCanvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.svg=null,this.canvas=null,this.path=null,this.group=null):(this.canvas&&this.canvas.parentNode&&this.canvas.parentNode.removeChild(this.canvas),this.bgCanvas&&this.bgCanvas.parentNode&&this.bgCanvas.parentNode.removeChild(this.bgCanvas))},reattach:function(a){var b=a.getContainer();this.canvas&&null==this.canvas.parentNode&&b.appendChild(this.canvas),this.bgCanvas&&null==this.bgCanvas.parentNode&&b.appendChild(this.bgCanvas)},setVisible:function(a){this.canvas&&(this.canvas.style.display=a?"block":"none")}}),b.ConnectorRenderers.svg=function(a){var c=this,d=z.apply(this,[{cssClass:a._jsPlumb.connectorClass,originalArgs:arguments,pointerEventsSpec:"none",_jsPlumb:a._jsPlumb}]);d.renderer.paint=function(d,e,f){var g=c.getSegments(),h="",i=[0,0];if(f.xmin<0&&(i[0]=-f.xmin),f.ymin<0&&(i[1]=-f.ymin),g.length>0){h=c.getPathData();var j={d:h,transform:"translate("+i[0]+","+i[1]+")","pointer-events":a["pointer-events"]||"visibleStroke"},k=null,l=[c.x,c.y,c.w,c.h];if(d.outlineStroke){var m=d.outlineWidth||1,n=d.strokeWidth+2*m;k=b.extend({},d),delete k.gradient,k.stroke=d.outlineStroke,k.strokeWidth=n,null==c.bgPath?(c.bgPath=t("path",j),b.addClass(c.bgPath,b.connectorOutlineClass),y(c.svg,c.bgPath,0)):s(c.bgPath,j),x(c.svg,c.bgPath,k,l,c)}null==c.path?(c.path=t("path",j),y(c.svg,c.path,d.outlineStroke?1:0)):s(c.path,j),x(c.svg,c.path,d,l,c)}}},c.extend(b.ConnectorRenderers.svg,z);var A=b.SvgEndpoint=function(a){var c=z.apply(this,[{cssClass:a._jsPlumb.endpointClass,originalArgs:arguments,pointerEventsSpec:"all",useDivWrapper:!0,_jsPlumb:a._jsPlumb}]);c.renderer.paint=function(a){var c=b.extend({},a);c.outlineStroke&&(c.stroke=c.outlineStroke),null==this.node?(this.node=this.makeNode(c),this.svg.appendChild(this.node)):null!=this.updateNode&&this.updateNode(this.node),x(this.svg,this.node,c,[this.x,this.y,this.w,this.h],this),u(this.node,[this.x,this.y])}.bind(this)};c.extend(A,z),b.Endpoints.svg.Dot=function(){b.Endpoints.Dot.apply(this,arguments),A.apply(this,arguments),this.makeNode=function(a){return t("circle",{cx:this.w/2,cy:this.h/2,r:this.radius})},this.updateNode=function(a){s(a,{cx:this.w/2,cy:this.h/2,r:this.radius})}},c.extend(b.Endpoints.svg.Dot,[b.Endpoints.Dot,A]),b.Endpoints.svg.Rectangle=function(){b.Endpoints.Rectangle.apply(this,arguments),A.apply(this,arguments),this.makeNode=function(a){return t("rect",{width:this.w,height:this.h})},this.updateNode=function(a){s(a,{width:this.w,height:this.h})}},c.extend(b.Endpoints.svg.Rectangle,[b.Endpoints.Rectangle,A]),b.Endpoints.svg.Image=b.Endpoints.Image,b.Endpoints.svg.Blank=b.Endpoints.Blank,b.Overlays.svg.Label=b.Overlays.Label,b.Overlays.svg.Custom=b.Overlays.Custom;var B=function(a,c){a.apply(this,c),b.jsPlumbUIComponent.apply(this,c),this.isAppendedAtTopLevel=!1;this.path=null,this.paint=function(a,b){if(a.component.svg&&b){null==this.path&&(this.path=t("path",{"pointer-events":"all"}),a.component.svg.appendChild(this.path),this.elementCreated&&this.elementCreated(this.path,a.component),this.canvas=a.component.svg);var e=c&&1===c.length?c[0].cssClass||"":"",f=[0,0];b.xmin<0&&(f[0]=-b.xmin),b.ymin<0&&(f[1]=-b.ymin),s(this.path,{d:d(a.d),"class":e,stroke:a.stroke?a.stroke:null,fill:a.fill?a.fill:null,transform:"translate("+f[0]+","+f[1]+")"})}};var d=function(a){return isNaN(a.cxy.x)||isNaN(a.cxy.y)?"":"M"+a.hxy.x+","+a.hxy.y+" L"+a.tail[0].x+","+a.tail[0].y+" L"+a.cxy.x+","+a.cxy.y+" L"+a.tail[1].x+","+a.tail[1].y+" L"+a.hxy.x+","+a.hxy.y};this.transfer=function(a){a.canvas&&this.path&&this.path.parentNode&&(this.path.parentNode.removeChild(this.path),a.canvas.appendChild(this.path))}},C={cleanup:function(a){null!=this.path&&(a?this._jsPlumb.instance.removeElement(this.path):this.path.parentNode&&this.path.parentNode.removeChild(this.path))},reattach:function(a,b){this.path&&b.canvas&&b.canvas.appendChild(this.path)},setVisible:function(a){null!=this.path&&(this.path.style.display=a?"block":"none")}};c.extend(B,[b.jsPlumbUIComponent,b.Overlays.AbstractOverlay]),b.Overlays.svg.Arrow=function(){B.apply(this,[b.Overlays.Arrow,arguments])},c.extend(b.Overlays.svg.Arrow,[b.Overlays.Arrow,B],C),b.Overlays.svg.PlainArrow=function(){B.apply(this,[b.Overlays.PlainArrow,arguments])},c.extend(b.Overlays.svg.PlainArrow,[b.Overlays.PlainArrow,B],C),b.Overlays.svg.Diamond=function(){B.apply(this,[b.Overlays.Diamond,arguments])},c.extend(b.Overlays.svg.Diamond,[b.Overlays.Diamond,B],C),b.Overlays.svg.GuideLines=function(){var a,c,d=null,e=this;b.Overlays.GuideLines.apply(this,arguments),this.paint=function(b,g){null==d&&(d=t("path"),b.connector.svg.appendChild(d),e.attachListeners(d,b.connector),e.attachListeners(d,e),a=t("path"),b.connector.svg.appendChild(a),e.attachListeners(a,b.connector),e.attachListeners(a,e),c=t("path"),b.connector.svg.appendChild(c),e.attachListeners(c,b.connector),e.attachListeners(c,e));var h=[0,0];g.xmin<0&&(h[0]=-g.xmin),g.ymin<0&&(h[1]=-g.ymin),s(d,{d:f(b.head,b.tail),stroke:"red",fill:null,transform:"translate("+h[0]+","+h[1]+")"}),s(a,{d:f(b.tailLine[0],b.tailLine[1]),stroke:"blue",fill:null,transform:"translate("+h[0]+","+h[1]+")"}),s(c,{d:f(b.headLine[0],b.headLine[1]),stroke:"green",fill:null,transform:"translate("+h[0]+","+h[1]+")"})};var f=function(a,b){return"M "+a.x+","+a.y+" L"+b.x+","+b.y}},c.extend(b.Overlays.svg.GuideLines,b.Overlays.GuideLines)}.call("undefined"!=typeof window?window:this),function(){"use strict";var a=this,b=a.jsPlumb,c=a.jsPlumbUtil,d=a.Katavorio,e=a.Biltong,f=function(b){var c=b._mottle;return c||(c=b._mottle=new a.Mottle),c},g=function(a,c){c=c||"main";var f="_katavorio_"+c,g=a[f],h=a.getEventManager();return g||(g=new d({bind:h.on,unbind:h.off,getSize:b.getSize,getConstrainingRectangle:function(a){return[a.parentNode.scrollWidth,a.parentNode.scrollHeight]},getPosition:function(b,c){var d=a.getOffset(b,c,b._katavorioDrag?b.offsetParent:null);return[d.left,d.top]},setPosition:function(a,b){a.style.left=b[0]+"px",a.style.top=b[1]+"px"},addClass:b.addClass,removeClass:b.removeClass,intersects:e.intersects,indexOf:function(a,b){return a.indexOf(b)},scope:a.getDefaultScope(),css:{noSelect:a.dragSelectClass,droppable:"jtk-droppable",draggable:"jtk-draggable",drag:"jtk-drag",selected:"jtk-drag-selected",active:"jtk-drag-active",hover:"jtk-drag-hover",ghostProxy:"jtk-ghost-proxy"}}),g.setZoom(a.getZoom()),a[f]=g,a.bind("zoom",g.setZoom)),g},h=function(a){var b=a.el._jsPlumbDragOptions,c=!0;return b.canDrag&&(c=b.canDrag()),c&&(this.setHoverSuspended(!0),this.select({source:a.el}).addClass(this.elementDraggingClass+" "+this.sourceElementDraggingClass,!0),this.select({target:a.el}).addClass(this.elementDraggingClass+" "+this.targetElementDraggingClass,!0),this.setConnectionBeingDragged(!0)),c},i=function(a){var b=this.getUIPosition(arguments,this.getZoom());if(null!=b){var c=a.el._jsPlumbDragOptions;this.draw(a.el,b,null,!0),c._dragging&&this.addClass(a.el,"jtk-dragged"),c._dragging=!0}},j=function(a){for(var b,c=a.selection,d=function(a){null!=a[1]&&(b=this.getUIPosition([{el:a[2].el,pos:[a[1].left,a[1].top]}]),this.draw(a[2].el,b)),null!=a[0]._jsPlumbDragOptions&&delete a[0]._jsPlumbDragOptions._dragging,this.removeClass(a[0],"jtk-dragged"),this.select({source:a[2].el}).removeClass(this.elementDraggingClass+" "+this.sourceElementDraggingClass,!0),this.select({target:a[2].el}).removeClass(this.elementDraggingClass+" "+this.targetElementDraggingClass,!0),this.getDragManager().dragEnded(a[2].el)}.bind(this),e=0;e0?a.touches:a.changedTouches&&a.changedTouches.length>0?a.changedTouches:a.targetTouches&&a.targetTouches.length>0?a.targetTouches:[a]},r=function(a){var b={},c=[],d={},e={},f={};this.register=function(g){var h,i=a.getId(g);b[i]||(b[i]=g,c.push(g),d[i]={});var j=function(b){if(b)for(var c=0;c0){h||(h=a.getOffset(g));var m=a.getOffset(k);d[i][l]={id:l,offset:{left:m.left-h.left,top:m.top-h.top}},f[l]=i}j(b.childNodes[c])}};j(g)},this.updateOffsets=function(b,c){if(null!=b){c=c||{};var e,g=jsPlumb.getElement(b),h=a.getId(g),i=d[h];if(i)for(var j in i)if(i.hasOwnProperty(j)){var k=jsPlumb.getElement(j),l=c[j]||a.getOffset(k);if(null==k.offsetParent&&null!=d[h][j])continue;e||(e=a.getOffset(g)),d[h][j]={id:j,offset:{left:l.left-e.left,top:l.top-e.top}},f[j]=h}}},this.endpointAdded=function(c,g){g=g||a.getId(c);var h=document.body,i=c.parentNode;for(e[g]=e[g]?e[g]+1:1;null!=i&&i!==h;){var j=a.getId(i,null,!0);if(j&&b[j]){var k=a.getOffset(i);if(null==d[j][g]){var l=a.getOffset(c);d[j][g]={id:g,offset:{left:l.left-k.left,top:l.top-k.top}},f[g]=j}break}i=i.parentNode}},this.endpointDeleted=function(a){if(e[a.elementId]&&(e[a.elementId]--,e[a.elementId]<=0))for(var b in d)d.hasOwnProperty(b)&&d[b]&&(delete d[b][a.elementId],delete f[a.elementId])},this.changeId=function(a,b){d[b]=d[a],d[a]={},f[b]=f[a],f[a]=null},this.getElementsForDraggable=function(a){return d[a]},this.elementRemoved=function(a){var b=f[a];b&&(delete d[b][a],delete f[a])},this.reset=function(){b={},c=[],d={},e={}},this.dragEnded=function(b){if(null!=b.offsetParent){var c=a.getId(b),d=f[c];d&&this.updateOffsets(d)}},this.setParent=function(b,c,e,g,h){var i=f[c];d[g]||(d[g]={});var j=a.getOffset(e),k=h||a.getOffset(b);i&&d[i]&&delete d[i][c],d[g][c]={id:c,offset:{left:k.left-j.left,top:k.top-j.top}},f[c]=g},this.clearParent=function(a,b){var c=f[b];c&&(delete d[c][b],delete f[b])},this.revalidateParent=function(b,c,d){var e=f[c];if(e){var g={};g[c]=d,this.updateOffsets(e,g),a.revalidate(e)}},this.getDragAncestor=function(b){var c=jsPlumb.getElement(b),d=a.getId(c),e=f[d];return e?jsPlumb.getElement(e):null}},s=function(a,b,d){b=c.fastTrim(b),"undefined"!=typeof a.className.baseVal?a.className.baseVal=b:a.className=b;try{var e=a.classList;if(null!=e){for(;e.length>0;)e.remove(e.item(0));for(var f=0;f0||a.scrollLeft>0)&&(d.left-=a.scrollLeft,d.top-=a.scrollTop)}.bind(this);null!=e;)d.left+=e.offsetLeft,d.top+=e.offsetTop,f(e),e=b?e.offsetParent:e.offsetParent===c?null:e.offsetParent;if(null!=c&&!b&&(c.scrollTop>0||c.scrollLeft>0)){var g=null!=a.offsetParent?this.getStyle(a.offsetParent,"position"):"static",h=this.getStyle(a,"position");"absolute"!==h&&"fixed"!==h&&"absolute"!==g&&"fixed"!==g&&(d.left-=c.scrollLeft,d.top-=c.scrollTop)}return d},getPositionOnElement:function(a,b,c){var d="undefined"!=typeof b.getBoundingClientRect?b.getBoundingClientRect():{left:0,top:0,width:0,height:0},e=document.body,f=document.documentElement,g=window.pageYOffset||f.scrollTop||e.scrollTop,h=window.pageXOffset||f.scrollLeft||e.scrollLeft,i=f.clientTop||e.clientTop||0,j=f.clientLeft||e.clientLeft||0,k=0,l=0,m=d.top+g-i+k*c,n=d.left+h-j+l*c,o=jsPlumb.pageLocation(a),p=d.width||b.offsetWidth*c,q=d.height||b.offsetHeight*c,r=(o[0]-n)/p,s=(o[1]-m)/q;return[r,s]},getAbsolutePosition:function(a){var b=function(b){var c=a.style[b];return c?parseFloat(c.substring(0,c.length-2)):void 0};return[b("left"),b("top")]},setAbsolutePosition:function(a,b,c,d){c?this.animate(a,{left:"+="+(b[0]-c[0]),top:"+="+(b[1]-c[1])},d):(a.style.left=b[0]+"px",a.style.top=b[1]+"px")},getSize:function(a){return[a.offsetWidth,a.offsetHeight]},getWidth:function(a){return a.offsetWidth},getHeight:function(a){return a.offsetHeight},getRenderMode:function(){return"svg"},draggable:function(a,b){var d;return a=c.isArray(a)||null!=a.length&&!c.isString(a)?a:[a],Array.prototype.slice.call(a).forEach(function(a){d=this.info(a),d.el&&this._initDraggableIfNecessary(d.el,!0,b,d.id,!0)}.bind(this)),this},snapToGrid:function(a,b,c){var d=[],e=function(a){var e=this.info(a);if(null!=e.el&&e.el._katavorioDrag){var f=e.el._katavorioDrag.snap(b,c);this.revalidate(e.el),d.push([e.el,f])}}.bind(this);if(1===arguments.length||3===arguments.length)e(a,b,c);else{var f=this.getManagedElements();for(var g in f)e(g,arguments[0],arguments[1])}return d},initDraggable:function(a,b,c){g(this,c).draggable(a,b),a._jsPlumbDragOptions=b},destroyDraggable:function(a,b){g(this,b).destroyDraggable(a),delete a._jsPlumbDragOptions},unbindDraggable:function(a,b,c,d){g(this,d).destroyDraggable(a,b,c)},setDraggable:function(a,b){return jsPlumb.each(a,function(a){this.isDragSupported(a)&&(this._draggableStates[this.getAttribute(a,"id")]=b,this.setElementDraggable(a,b))}.bind(this))},_draggableStates:{},toggleDraggable:function(a){var b;return jsPlumb.each(a,function(a){var c=this.getAttribute(a,"id");return b=null==this._draggableStates[c]?!1:this._draggableStates[c],b=!b,this._draggableStates[c]=b,this.setDraggable(a,b),b}.bind(this)),b},_initDraggableIfNecessary:function(a,b,d,e,f){if(!jsPlumb.headless){var g=null==b?!1:b;if(g&&jsPlumb.isDragSupported(a,this)){var k=d||this.Defaults.DragOptions;if(k=jsPlumb.extend({},k),jsPlumb.isAlreadyDraggable(a,this))d.force&&this.initDraggable(a,k);else{var l=jsPlumb.dragEvents.drag,m=jsPlumb.dragEvents.stop,n=jsPlumb.dragEvents.start;this.manage(e,a),k[n]=c.wrap(k[n],h.bind(this)),k[l]=c.wrap(k[l],i.bind(this)),k[m]=c.wrap(k[m],j.bind(this));var o=this.getId(a);this._draggableStates[o]=!0;var p=this._draggableStates[o];k.disabled=null==p?!1:!p,this.initDraggable(a,k),this.getDragManager().register(a),f&&this.fire("elementDraggable",{el:a,options:k})}}}},animationSupported:!0,getElement:function(a){return null==a?null:(a="string"==typeof a?a:null!=a.length&&null==a.enctype?a[0]:a,"string"==typeof a?document.getElementById(a):a)},removeElement:function(a){g(this).elementRemoved(a),this.getEventManager().remove(a)},doAnimate:function(a,c,d){d=d||{};var e=this.getOffset(a),f=k(e,c),g=f[0]-e.left,h=f[1]-e.top,i=d.duration||250,j=15,l=i/j,m=j/i*g,n=j/i*h,o=0,p=setInterval(function(){b.setPosition(a,{left:e.left+m*(o+1),top:e.top+n*(o+1)}),null!=d.step&&d.step(o,Math.ceil(l)),o++,o>=l&&(window.clearInterval(p),null!=d.complete&&d.complete())},j)},destroyDroppable:function(a,b){g(this,b).destroyDroppable(a)},unbindDroppable:function(a,b,c,d){g(this,d).destroyDroppable(a,b,c)},droppable:function(a,b){a=c.isArray(a)||null!=a.length&&!c.isString(a)?a:[a];var d;return b=b||{},b.allowLoopback=!1,Array.prototype.slice.call(a).forEach(function(a){d=this.info(a),d.el&&this.initDroppable(d.el,b)}.bind(this)),this},initDroppable:function(a,b,c){g(this,c).droppable(a,b)},isAlreadyDraggable:function(a){return null!=a._katavorioDrag},isDragSupported:function(a,b){return!0},isDropSupported:function(a,b){return!0},isElementDraggable:function(a){return a=b.getElement(a),a._katavorioDrag&&a._katavorioDrag.isEnabled()},getDragObject:function(a){return a[0].drag.getDragElement()},getDragScope:function(a){return a._katavorioDrag&&a._katavorioDrag.scopes.join(" ")||""},getDropEvent:function(a){return a[0].e},getUIPosition:function(a,b){var c=a[0].el;if(null==c.offsetParent)return null;var d=a[0].finalPos||a[0].pos,e={left:d[0],top:d[1]};if(c._katavorioDrag&&c.offsetParent!==this.getContainer()){var f=this.getOffset(c.offsetParent);e.left+=f.left,e.top+=f.top}return e},setDragFilter:function(a,b,c){a._katavorioDrag&&a._katavorioDrag.setFilter(b,c)},setElementDraggable:function(a,c){a=b.getElement(a),a._katavorioDrag&&a._katavorioDrag.setEnabled(c)},setDragScope:function(a,b){a._katavorioDrag&&a._katavorioDrag.k.setDragScope(a,b)},setDropScope:function(a,b){a._katavorioDrop&&a._katavorioDrop.length>0&&a._katavorioDrop[0].k.setDropScope(a,b)},addToPosse:function(a,c){var d=Array.prototype.slice.call(arguments,1),e=g(this);b.each(a,function(a){a=[b.getElement(a)],a.push.apply(a,d),e.addToPosse.apply(e,a)})},setPosse:function(a,c){var d=Array.prototype.slice.call(arguments,1),e=g(this);b.each(a,function(a){a=[b.getElement(a)],a.push.apply(a,d),e.setPosse.apply(e,a)})},removeFromPosse:function(a,c){var d=Array.prototype.slice.call(arguments,1),e=g(this);b.each(a,function(a){a=[b.getElement(a)],a.push.apply(a,d),e.removeFromPosse.apply(e,a)})},removeFromAllPosses:function(a){var c=g(this);b.each(a,function(a){c.removeFromAllPosses(b.getElement(a))})},setPosseState:function(a,c,d){var e=g(this);b.each(a,function(a){e.setPosseState(b.getElement(a),c,d)})},dragEvents:{start:"start",stop:"stop",drag:"drag",step:"step",over:"over",out:"out",drop:"drop",complete:"complete",beforeStart:"beforeStart"},animEvents:{step:"step",complete:"complete"},stopDrag:function(a){a._katavorioDrag&&a._katavorioDrag.abort()},addToDragSelection:function(a){var b=this.getElement(a);null==b||!b._isJsPlumbGroup&&null!=b._jsPlumbGroup||g(this).select(a)},removeFromDragSelection:function(a){g(this).deselect(a)},getDragSelection:function(){return g(this).getSelection()},clearDragSelection:function(){g(this).deselectAll()},trigger:function(a,b,c,d){this.getEventManager().trigger(a,b,c,d)},doReset:function(){for(var a in this)0===a.indexOf("_katavorio_")&&this[a].reset()},getEventManager:function(){return f(this)},on:function(a,b,c){return this.getEventManager().on.apply(this,arguments),this},off:function(a,b,c){return this.getEventManager().off.apply(this,arguments),this}});var v=function(a){var b=function(){/complete|loaded|interactive/.test(document.readyState)&&"undefined"!=typeof document.body&&null!=document.body?a():setTimeout(b,9)};b()};v(b.init)}.call("undefined"!=typeof window?window:this); \ No newline at end of file diff --git a/ui/static/JS/qrcode.min.js b/ui/static/JS/qrcode.min.js new file mode 100755 index 0000000..993e88f --- /dev/null +++ b/ui/static/JS/qrcode.min.js @@ -0,0 +1 @@ +var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j=0?p.get(q):0}}for(var r=0,m=0;mm;m++)for(var j=0;jm;m++)for(var j=0;j=0;)b^=f.G15<=0;)b^=f.G18<>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;cf;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push('');g.push("")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}(); \ No newline at end of file diff --git a/ui/static/Style/style.css b/ui/static/Style/style.css new file mode 100755 index 0000000..d8f42f7 --- /dev/null +++ b/ui/static/Style/style.css @@ -0,0 +1,269 @@ +html{ + margin:0; + padding: 0; + width: 100%; + height: 100%;; + background-image: url('/ui/static/Images/BG.png'); + background-position: 0 0; + background-size: 100% 100%; + margin: 0; + height: 100%; +} + +body{ + margin: 0; + font-family:'HelveticaNeue-Light', 'Calibri Light', Roboto; + color:rgb(0,0,0); + font-size:18px; +} + +.LoginLogo{ + position: absolute; + top: 100px; + left: calc(50% - 264px); +} +.LoginBanner{ + margin-top: 40%; + width: 100%; + height: 150px; + background-color: rgba(0,0,0,0.4); + display: flex; + -webkit-box-shadow: 0px 0px 18px -3px rgba(0,0,0,0.75); + -moz-box-shadow: 0px 0px 18px -3px rgba(0,0,0,0.75); + box-shadow: 0px 0px 18px -3px rgba(0,0,0,0.75); +} + +.LoginBanner .LoginTable{ + width: 400px; + margin-left: auto; + margin-right: 25px; + padding-top: 23px; + color: rgb(255,255,255); +} + +.LoginBanner .LoginDisclaimer{ + margin-right: auto; + margin-left: 25px; + color: rgb(255,255,255); + font-size: 16px; + margin-top: 22px; +} + +.Container{ + display: flex; + height: 100vh; + width: 100%; +} + +.Container .Menu{ + color: rgb(255,255,255); + width: 300px; + background: rgba(0,0,0,0.4); + font-size: 22px; + -webkit-box-shadow: -6px 3px 16px 12px rgba(0,0,0,0.75); + -moz-box-shadow: -6px 3px 16px 12px rgba(0,0,0,0.75); + box-shadow: -6px 3px 16px 12px rgba(0,0,0,0.75); + height: 100vh; +} + +.Container .Content{ + width: 100%; + height: 100vh; +} + +.StyledButton{ + border:0; + color:rgb(255,255,255); + background-color:#F36B08; + padding-top:10px; + padding-bottom:10px; + width:70px; + cursor:pointer; +} + +.StyledButton[withborder] +{ + border-color: rgb(0,0,0); + border-style: solid; + border-width: 5px; + font-size: 16px; + -webkit-border-radius: 6px; + -moz-border-radius: 6px; + border-radius: 6px; +} + + +.Middle { + margin-left: auto; + margin-right: auto; +} + +.Dialog { + border-width: 2px; + border-style: solid; + border-color: #F36B08; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + -webkit-box-shadow: 6px 6px 5px 0px rgba(0,0,0,1); + -moz-box-shadow: 6px 6px 5px 0px rgba(0,0,0,1); + box-shadow: 6px 6px 5px 0px rgba(0,0,0,1); + overflow:hidden; + background-color:rgba(255,255,255,1.0) +} + + .Dialog .Title { + background-color: #F36B08; + padding-left: 10px; + height: 40px; + line-height: 40px; + /*font-weight: bold;*/ + color: rgb(255,255,255); + } + +.TopBanner { + width: 100%; + height: 40px; + line-height: 40px; + padding-left: 5px; + padding-right:5px; + color: white; + font-size: 22px; + background-color: #F36B08; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + overflow: hidden; + + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + +} + +img.invert { + filter: invert(100%); +} + + +.Accessory { + display: inline-block; + width: 200px; + margin-top:20px; + margin-left: 10px; + margin-bottom: 10px; + + height: 65px; + padding-left: 5px; + color: rgb(255,255,255); + font-size: 16px; + background-color: #F36B08; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + border-width:1px; + border-color:rgb(0,0,0); + border-style:solid; + padding:10px; + animation-delay:0.3s; + + + + -webkit-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + + +} + +.Route { + display: inline-block; + width: 200px; + margin-top:20px; + margin-left: 10px; + margin-bottom: 10px; + + height: 65px; + padding-left: 5px; + color: rgb(255,255,255); + font-size: 16px; + background-color: #F36B08; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + overflow: hidden; + border-width:1px; + border-color:rgb(0,0,0); + border-style:solid; + padding:10px; + animation-delay:0.3s; + + word-break: break-all; + + + + -webkit-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + -moz-box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + box-shadow: 3px 3px 5px 0px rgba(0,0,0,0.75); + + +} +table{ + color:rgb(0,0,0) +} + + +.config{ + width: 200px; +} + + + + + + svg rect{ + + fill:rgb(0,0,0); + stroke-width:4; + stroke:#F36B08; + cursor: pointer; + } + svg path{ + stroke:rgb(0,0,0); + stroke-width:3; + z-index: -99; + + } + + .Popup{ + position: absolute; + top:0; + left: 0; + z-index:100; + background-color: rgba(0,0,0,0.6); + width: 100%; + height: 100%; + display: none; + + } + + .MDNSType{ + position: relative; + left: 80px; + bottom: -20px; + color: black; + font-size: 12px; + background-color: white; + border-color: black; + border-width: 1px; + border-style: solid; + padding: 5px; + -webkit-border-radius: 4px; +-moz-border-radius: 4px; +border-radius: 4px; + } + + + + +