hlc-server
Version:
Serves real-time real-world context at a human scale by combining RFID, RTLS and M2M with structured, linked data on the web. We believe in an open Internet of Things.
316 lines (251 loc) • 9.11 kB
JavaScript
/**
* Copyright reelyActive 2020
* We believe in an open Internet of Things
*/
// Constant definitions
const DEFAULT_RSSI_THRESHOLD = -70;
const DEFAULT_LAYOUT_INTERVAL = 5000;
const SIGNATURE_SEPARATOR = '/';
const COSE_LAYOUT_OPTIONS = {
name: "cose",
animate: false,
randomize: false,
initialTemp: 40
};
const CONCENTRIC_LAYOUT_OPTIONS = {
name: "concentric",
startAngle: 0,
sweep: Math.PI,
concentric: concentric
};
const BREADTHFIRST_LAYOUT_OPTIONS = {
name: "breadthfirst"
};
const GRAPH_STYLE = [
{ selector: "node[type='transmitter']",
style: { "background-color": "#83b7d0" } },
{ selector: "node[type='receiver']",
style: { "background-color": "#aec844", label: "data(name)" } },
{ selector: "node[image]",
style: { "background-image": "data(image)", "border-color": "#aec844",
"background-fit": "cover cover", "border-width": "2px" } },
{ selector: "edge", style: { "curve-style": "haystack",
"line-color": "#ddd" } },
];
// DOM elements
// Other variables
let rssiThreshold = DEFAULT_RSSI_THRESHOLD;
let layoutUpdateInterval = DEFAULT_LAYOUT_INTERVAL;
let isInitialLayoutPending = true;
let layoutOptions = COSE_LAYOUT_OPTIONS;
let layoutPromise;
let selectedReceiverId;
let selectedTransmitterId;
// Initialise Cytoscape
let cy = cytoscape({
container: document.getElementById('cy'),
layout: COSE_LAYOUT_OPTIONS,
style: GRAPH_STYLE
});
let layout = cy.layout({ name: "cose", cy: cy });
cy.on("tap", "node[type='receiver']", handleReceiverTap);
cy.on("tap", "node[type='transmitter']", handleTransmitterTap);
// Connect to the socket.io stream and feed to beaver
let baseUrl = window.location.protocol + '//' + window.location.hostname +
':' + window.location.port;
let socket = io.connect(baseUrl);
beaver.listen(socket, true);
// Handle raddec events
beaver.on([ 0 ], addTransmitter); // Appearance
beaver.on([ 1, 2 ], updateTransmitter); // Displacement or packets
beaver.on([ 4 ], removeTransmitter); // Disappearance
// Add a transmitter node and its receiver edges
function addTransmitter(raddec) {
let transmitterSignature = determineTransmitterSignature(raddec);
let isExistingNode = (cy.getElementById(transmitterSignature).size() > 0);
if(isExistingNode) {
return updateTransmitter(raddec);
}
if(!isAboveRssiThreshold(raddec)) {
return;
}
let renderedPosition = determineInitialRenderingPosition(raddec);
cy.add({ group: "nodes", renderedPosition: renderedPosition,
data: { id: transmitterSignature, type: 'transmitter' } });
addReceiverEdges(raddec);
if(isInitialLayoutPending) {
updateLayout();
isInitialLayoutPending = false;
}
}
// Update an existing transmitter node's receiver edges
function updateTransmitter(raddec) {
let transmitterSignature = determineTransmitterSignature(raddec);
let isExistingNode = (cy.getElementById(transmitterSignature).size() > 0);
if(!isExistingNode) {
return addTransmitter(raddec);
}
if(!isAboveRssiThreshold(raddec)) {
return removeTransmitter(raddec);
}
let transmitterEdges = cy.elements('edge[source = "' + transmitterSignature +
'" ]');
transmitterEdges.forEach(function(ele) {
let receiverSignature = ele.data('target');
let isPersistentEdge = false;
raddec.rssiSignature.forEach(function(entry) {
if((receiverSignature === determineReceiverSignature(entry)) &&
(entry.rssi >= rssiThreshold)) {
isPersisentEdge = true;
}
});
if(!isPersistentEdge) {
cy.remove(ele);
}
});
addReceiverEdges(raddec);
removeOrphanNodes();
}
// Remove the transmitter and its edges
function removeTransmitter(raddec) {
let transmitterSignature = determineTransmitterSignature(raddec);
let transmitter = cy.getElementById(transmitterSignature);
let transmitterEdges = cy.elements('edge[source = "' + transmitterSignature +
'" ]');
cy.remove(transmitterEdges);
cy.remove(transmitter);
removeOrphanNodes();
}
// Add the edges, and, if necessary, receiver node, for the given raddec
function addReceiverEdges(raddec) {
let transmitterSignature = determineTransmitterSignature(raddec);
raddec.rssiSignature.forEach(function(entry) {
let receiverSignature = determineReceiverSignature(entry);
let edgeSignature = transmitterSignature + '@' + receiverSignature;
isExistingNode = (cy.getElementById(receiverSignature).size() > 0);
isExistingEdge = (cy.getElementById(edgeSignature).size() > 0);
if(entry.rssi >= rssiThreshold) {
if(!isExistingNode) {
let receiverName = entry.receiverId.substr(-2);
cy.add({ group: "nodes", data: { id: receiverSignature,
type: 'receiver',
name: receiverName } });
fetchContextAndUpdateNode(receiverSignature);
}
if(!isExistingEdge) {
cy.add({ group: "edges", data: { id: edgeSignature,
source: transmitterSignature,
target: receiverSignature } });
}
}
});
}
// Remove any nodes that have no edges
function removeOrphanNodes() {
cy.nodes(function(node) {
if(node.degree() < 1) {
cy.remove(node);
}
});
}
// Fetch the context for the given ID signature and update the node
function fetchContextAndUpdateNode(signature) {
cormorant.retrieveAssociations(baseUrl, signature, true,
function(associations, story) {
if(story) {
let title = cuttlefish.determineTitle(story);
let imageUrl = cuttlefish.determineImageUrl(story);
let node = cy.getElementById(signature);
if(node && imageUrl) {
node.data('image', imageUrl);
node.data('name', '');
}
else if(node && title) {
node.data('name', title);
}
}
});
}
// Determine the transmitter signature for the given raddec
function determineTransmitterSignature(raddec) {
return raddec.transmitterId + SIGNATURE_SEPARATOR + raddec.transmitterIdType;
}
// Determine the strongest receiver signature for the given raddec
function determineReceiverSignature(entry) {
return entry.receiverId + SIGNATURE_SEPARATOR + entry.receiverIdType;
}
// Determine an initial rendering position for the given raddec
function determineInitialRenderingPosition(raddec) {
if(!Array.isArray(raddec.rssiSignature) ||
(raddec.rssiSignature.length < 1)) {
return { x: 0, y: 0 };
}
let receiverSignature = determineReceiverSignature(raddec.rssiSignature[0]);
let receiverNode = cy.getElementById(receiverSignature);
let isExistingNode = (receiverNode.size() > 0);
if(!isExistingNode) {
return { x: 0, y: 0 };
}
let receiverX = receiverNode.renderedPosition('x') || 0;
let receiverY = receiverNode.renderedPosition('y') || 0;
let receiverW = receiverNode.renderedWidth() || 50;
let randomRadians = Math.random() * Math.PI * 2;
return { x: receiverX + (Math.cos(randomRadians) * receiverW),
y: receiverY + (Math.sin(randomRadians) * receiverW) };
}
// Determine if the strongest receiver is above the RSSI threshold
function isAboveRssiThreshold(raddec) {
return (Array.isArray(raddec.rssiSignature) &&
(raddec.rssiSignature.length > 0) &&
(raddec.rssiSignature[0].rssi >= rssiThreshold));
}
// Determine the level of the given node in the concentric layout
function concentric(node) {
if(node.id() === selectedReceiverId) {
return 100;
}
return 0;
}
// Handle the tap of a receiver node
function handleReceiverTap(evt) {
let node = evt.target;
let isNewSelectedReceiver = (node.id() !== selectedReceiverId);
if(isNewSelectedReceiver) {
selectedReceiverId = node.id();
updateLayout(CONCENTRIC_LAYOUT_OPTIONS);
}
else if(layoutOptions.name === 'concentric') {
updateLayout(COSE_LAYOUT_OPTIONS);
}
else {
updateLayout(CONCENTRIC_LAYOUT_OPTIONS);
}
}
// Handle the tap of a transmitter node
function handleTransmitterTap(evt) {
let node = evt.target;
let isNewSelectedTransmitter = (node.id() !== selectedTransmitterId);
let breadthfirstLayoutOptions = Object.assign({ roots: node },
BREADTHFIRST_LAYOUT_OPTIONS);
if(isNewSelectedTransmitter) {
selectedTransmitterId = node.id();
updateLayout(breadthfirstLayoutOptions);
}
else if(layoutOptions.name === 'breadthfirst') {
updateLayout(COSE_LAYOUT_OPTIONS);
}
else {
updateLayout(breadthfirstLayoutOptions);
}
}
// Update the layout and set a timeout for the next update
function updateLayout(newLayoutOptions) {
if(newLayoutOptions) {
layoutOptions = newLayoutOptions;
clearTimeout(layoutPromise);
}
layout.stop();
layout = cy.elements().makeLayout(layoutOptions);
layout.run();
layoutPromise = setTimeout(updateLayout, layoutUpdateInterval);
}