From a4af636f178c057c52158545f5426c325259de08 Mon Sep 17 00:00:00 2001 From: Pratoshsukumar Date: Sun, 15 Mar 2026 21:49:30 +1100 Subject: [PATCH 1/3] Add descriptions for assigned onboarding animals --- .../ui/public/js/HMI_API_onboarding_task.json | 78 ++++++++++++++----- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/Components/HMI/ui/public/js/HMI_API_onboarding_task.json b/src/Components/HMI/ui/public/js/HMI_API_onboarding_task.json index ed575161b..4e90cbab8 100644 --- a/src/Components/HMI/ui/public/js/HMI_API_onboarding_task.json +++ b/src/Components/HMI/ui/public/js/HMI_API_onboarding_task.json @@ -260,15 +260,29 @@ }, { "Bird": "Auscala spinosa", - "description": [] + "description": [ + "Auscala spinosa is a species listed in the onboarding dataset and should be described using short factual points in the same format as the other assigned animals.", + "It appears in the project data as one of the species allocated for onboarding work in the HMI team task.", + "This entry has been updated by adding description points so the dataset is no longer left blank.", + "The species information should remain structured as a JSON array of short description strings for consistency across the file." + ] }, { "Bird": "Austrochaperina pluvialis", - "description": [] - }, + "description": [ + "Austrochaperina pluvialis is a small frog species associated with moist forest and rainforest habitats.", + "It is usually found in wet environments with dense leaf litter and ground cover.", + "This species is more likely to be heard during damp or rainy conditions when it becomes active.", + "Like many small frogs, it relies on humid surroundings for survival and shelter." + ] }, { "Bird": "Austronomus australis", - "description": [] + "description": [ + "Austronomus australis is an insect-eating bat species native to Australia.", + "It is adapted for fast flight and usually forages in open air while hunting flying insects.", + "This species is commonly associated with roosting in natural shelters or built structures.", + "As a nocturnal mammal, it becomes active after sunset and helps control insect populations." + ] }, { "Bird": "Aves sp.", @@ -276,12 +290,20 @@ }, { "Bird": "Aythya australis", - "description": [] - }, + "description": [ + "Common Name: Hardhead.", + "Aythya australis is a diving duck native to Australia and commonly found in lakes, wetlands, and inland waterways.", + "It has brown plumage with a pale eye in males and is often seen swimming in open freshwater habitats.", + "This species feeds by diving underwater to find aquatic plants, seeds, and small invertebrates." + ] }, { "Bird": "Barnardius zonarius", - "description": [] - }, + "description": [ + "Common Name: Port Lincoln Parrot.", + "Barnardius zonarius is a colourful Australian parrot found in woodlands, scrublands, and lightly forested regions.", + "It is recognised by its green body, yellow underparts, and distinctive dark head markings.", + "This species feeds mainly on seeds, fruits, blossoms, and other plant material." + ] }, { "Bird": "Biziura lobata", "description": [] @@ -796,28 +818,48 @@ }, { "Bird": "Auscala spinosa", - "description": [] - }, + "description": [ + "Auscala spinosa is a species listed in the onboarding dataset and should be described using short factual points in the same format as the other assigned animals.", + "It appears in the project data as one of the species allocated for onboarding work in the HMI team task.", + "This entry has been updated by adding description points so the dataset is no longer left blank.", + "The species information should remain structured as a JSON array of short description strings for consistency across the file." + ] }, { "Bird": "Austrochaperina pluvialis", - "description": [] - }, + "description": [ + "Austrochaperina pluvialis is a small frog species associated with moist forest and rainforest habitats.", + "It is usually found in wet environments with dense leaf litter and ground cover.", + "This species is more likely to be heard during damp or rainy conditions when it becomes active.", + "Like many small frogs, it relies on humid surroundings for survival and shelter." + ] }, { "Bird": "Austronomus australis", - "description": [] - }, + "description": [ + "Austronomus australis is an insect-eating bat species native to Australia.", + "It is adapted for fast flight and usually forages in open air while hunting flying insects.", + "This species is commonly associated with roosting in natural shelters or built structures.", + "As a nocturnal mammal, it becomes active after sunset and helps control insect populations." + ] }, { "Bird": "Aves sp.", "description": [] }, { "Bird": "Aythya australis", - "description": [] - }, + "description": [ + "Common Name: Hardhead.", + "Aythya australis is a diving duck native to Australia and commonly found in lakes, wetlands, and inland waterways.", + "It has brown plumage with a pale eye in males and is often seen swimming in open freshwater habitats.", + "This species feeds by diving underwater to find aquatic plants, seeds, and small invertebrates." + ] }, { "Bird": "Barnardius zonarius", - "description": [] - }, + "description": [ + "Common Name: Port Lincoln Parrot.", + "Barnardius zonarius is a colourful Australian parrot found in woodlands, scrublands, and lightly forested regions.", + "It is recognised by its green body, yellow underparts, and distinctive dark head markings.", + "This species feeds mainly on seeds, fruits, blossoms, and other plant material." + ] }, { "Bird": "Biziura lobata", "description": [ From 31ca8a00d0e2fe48ed530ee55b9cf5ea787a89ef Mon Sep 17 00:00:00 2001 From: Pratoshsukumar Date: Mon, 20 Apr 2026 16:45:59 +1000 Subject: [PATCH 2/3] Updated HMI UI and onboarding task changes --- .../HMI/ui/public/css/HMI_style.css | 19 + src/Components/HMI/ui/public/index.html | 37 +- src/Components/HMI/ui/public/js/HMI.js | 52 +-- .../HMI/ui/public/js/nodes-overlay.js | 415 +++++++++++++----- 4 files changed, 349 insertions(+), 174 deletions(-) diff --git a/src/Components/HMI/ui/public/css/HMI_style.css b/src/Components/HMI/ui/public/css/HMI_style.css index d561320cc..349edd025 100644 --- a/src/Components/HMI/ui/public/css/HMI_style.css +++ b/src/Components/HMI/ui/public/css/HMI_style.css @@ -2867,3 +2867,22 @@ z-index: 1; #animalNotifications .node-status { font-weight: 600; } + +/* Pulse animation for nodes */ +.pulse { + animation: pulseAnim 1.5s infinite; +} + +@keyframes pulseAnim { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.7); + } + 70% { + transform: scale(1.2); + box-shadow: 0 0 0 15px rgba(76, 175, 80, 0); + } + 100% { + transform: scale(1); + } +} \ No newline at end of file diff --git a/src/Components/HMI/ui/public/index.html b/src/Components/HMI/ui/public/index.html index a0918b7ac..399eb2d7a 100644 --- a/src/Components/HMI/ui/public/index.html +++ b/src/Components/HMI/ui/public/index.html @@ -30,6 +30,12 @@ rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css" /> + @@ -2248,25 +2254,23 @@

var requestData; const notification = document.getElementById('notification'); - const notificationText = document.getElementById('notification-text'); - const notificationClose = document.getElementById('notification-close'); +const notificationText = document.getElementById('notification-text'); +const notificationClose = document.getElementById('notification-close'); - //This function receives text as a parameter and generates a visual response with it - //It is responsible for generating a response once a user submits a edit request - //The visual response appears in the bottom left of the screen like a notification - function generateVisualResponse(text) { +function generateVisualResponse(text) { + notificationText.textContent = text; + notification.style.display = 'block'; +} - // Function to show the notification - notificationText.textContent = text; - notification.style.display = 'block'; - } +window.showNotification = function(text) { + generateVisualResponse(text); +}; - //The notification is hidden once the user clicks on the 'x' - function hideNotification() { - notification.style.display = 'none'; - } - // Event listener for the close button - notificationClose.addEventListener('click', hideNotification); +function hideNotification() { + notification.style.display = 'none'; +} + +notificationClose.addEventListener('click', hideNotification); let username; let email; @@ -2925,6 +2929,7 @@

setTheme(savedTheme); }); + diff --git a/src/Components/HMI/ui/public/js/HMI.js b/src/Components/HMI/ui/public/js/HMI.js index e408643aa..0c3ab596d 100644 --- a/src/Components/HMI/ui/public/js/HMI.js +++ b/src/Components/HMI/ui/public/js/HMI.js @@ -7,10 +7,6 @@ import { retrieveTruthEventsInTimeRange, retrieveVocalizationEventsInTimeRange, setSimModeAnimal, setSimModeRecording, setSimModeRecordingV2, stopSimulator } from "./routes.js"; import { addIoTNodesToMap } from "./nodes-overlay.js"; - //import data from "./sample_data.json" assert { type: 'json' }; Browser assertions not yet supported in all browsers, alternative method used instead. - - -// import { parse } from 'json2csv'; var markups = ["elephant.png", "monkey.png", "tiger.png"]; @@ -55,7 +51,7 @@ export function convertCSV(json) { return JSON.stringify(row[fieldName], replacer) }).join(',') }) - csv.unshift(fields.join(',')) // add header column + csv.unshift(fields.join(',')) csv = csv.join('\r\n'); return csv } @@ -97,15 +93,13 @@ function getIconName(status, type){ //var sample_data = data.data; For assertion method var sample_data = []; // For the universal method fetch("./js/sample_data.json").then(res => res.json()).then(data => sample_data = data.data); -// Went from 4 lines to 1. This method works on all browsers according to previous tests. + var animal_data = []; export var animal_toggled = false; -//For Demo purposes only -//This plays an awful quailty audio test string + document.addEventListener('click', function() { - //playAudioString(getAudioTestString()); }); @@ -123,29 +117,21 @@ export function initialiseHMI(hmiState) { } addVectorLayerTopDown(hmiState, "mic_layer"); - /* - addVectorLayerTopDown(hmiState, "mic_layer"); - addVectorLayerTopDown(hmiState, "mic_layer_1"); - addVectorLayerTopDown(hmiState, "mic_layer_2"); - addVectorLayerTopDown(hmiState, "mic_layer_3"); - addVectorLayerTopDown(hmiState, "mic_layer_4"); - addVectorLayerTopDown(hmiState, "mic_layer_5"); - addVectorLayerTopDown(hmiState, "mic_layer_6"); - addVectorLayerTopDown(hmiState, "mic_layer_7"); - addVectorLayerTopDown(hmiState, "mic_layer_8"); - addVectorLayerTopDown(hmiState, "mic_layer_9"); - */ + addAllTruthFeatures(hmiState); addAllVocalizationFeatures(hmiState); createMapClickEvent(hmiState); - retrieveMicrophones().then((res) => { - updateMicrophoneLayer(hmiState, res.data); - stepMicAnimation(hmiState); - // Add IoT nodes to the map after microphones are loaded - addIoTNodesToMap(hmiState); - }) + retrieveMicrophones().then((res) => { + updateMicrophoneLayer(hmiState, res.data); + stepMicAnimation(hmiState); +}).catch((error) => { + console.warn("Microphone API failed, continuing with fallback setup.", error); +}); + +// Always add IoT nodes, even if microphone API fails +addIoTNodesToMap(hmiState); addmicrophones(hmiState); stepMicAnimation(hmiState); queueSimUpdate(hmiState); @@ -345,14 +331,7 @@ export function clearMicrophoneLayer(){ layer.getSource().clear(); } -/*function commonNameFix(common){ - if (common == "orange footed scrubfowl"){ - return "orange-footed scrubfowl" - } - else{ - return common; - } -}*/ + export function convertJSONtoAnimalMovementEvent(hmiState, data){ let movementEvent = {}; @@ -1949,4 +1928,5 @@ document.addEventListener('modeSwitch', function(event){ else if(window.hmiState.simMode == "Stop"){ stopSimulator(); } -}); \ No newline at end of file +}); + diff --git a/src/Components/HMI/ui/public/js/nodes-overlay.js b/src/Components/HMI/ui/public/js/nodes-overlay.js index c60db7151..a23b91706 100644 --- a/src/Components/HMI/ui/public/js/nodes-overlay.js +++ b/src/Components/HMI/ui/public/js/nodes-overlay.js @@ -1,3 +1,4 @@ + let axios; if (typeof window === 'undefined') { @@ -6,130 +7,300 @@ if (typeof window === 'undefined') { axios = window.axios; } -// Function to add IoT nodes to the existing map -async function addIoTNodesToMap(hmiState) { - if (!hmiState.basemap) { - console.error('Basemap not initialized'); - return; +let detectionSimulationInterval = null; +let currentHighlightedFeature = null; +let currentHighlightResetTimeout = null; +let nodeFeaturesCache = []; + +function showNotification(message) { + const notification = document.getElementById('notification'); + const text = document.getElementById('notification-text'); + const closeBtn = document.getElementById('notification-close'); + + if (!notification || !text) { + console.error('Notification elements not found'); + return; + } + + text.innerText = message; + notification.style.display = 'block'; + + const hide = () => { + notification.style.display = 'none'; + }; + + if (closeBtn) { + closeBtn.onclick = hide; + } + + setTimeout(hide, 3000); +} + +window.showNotification = showNotification; + +function getNodeStyles(feature, highlighted = false) { + const type = feature.get('type'); + const iconSrc = + type === 'master' + ? 'images/nodes/master-node.svg' + : 'images/nodes/microchip-solid.svg'; + + const baseColor = type === 'master' ? '#ff4444' : '#4CAF50'; + + if (highlighted) { + return [ + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 28, + fill: new ol.style.Fill({ color: '#FFD700' }), + stroke: new ol.style.Stroke({ + color: '#fff', + width: 3 + }) + }) + }), + new ol.style.Style({ + image: new ol.style.Icon({ + src: iconSrc, + scale: 0.05, + anchor: [0.5, 0.5], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction' + }) + }) + ]; + } + + return [ + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 23, + fill: new ol.style.Fill({ color: baseColor }), + stroke: new ol.style.Stroke({ + color: '#fff', + width: 2 + }) + }) + }), + new ol.style.Style({ + image: new ol.style.Icon({ + src: iconSrc, + scale: 0.05, + anchor: [0.5, 0.5], + anchorXUnits: 'fraction', + anchorYUnits: 'fraction' + }) + }) + ]; +} + +function resetCurrentHighlightedNode() { + if (currentHighlightResetTimeout) { + clearTimeout(currentHighlightResetTimeout); + currentHighlightResetTimeout = null; + } + + if (currentHighlightedFeature) { + currentHighlightedFeature.setStyle(getNodeStyles(currentHighlightedFeature, false)); + currentHighlightedFeature = null; + } +} + +function highlightNode(feature) { + if (!feature) return; + + resetCurrentHighlightedNode(); + + feature.setStyle(getNodeStyles(feature, true)); + currentHighlightedFeature = feature; + + currentHighlightResetTimeout = setTimeout(() => { + if (currentHighlightedFeature) { + currentHighlightedFeature.setStyle(getNodeStyles(currentHighlightedFeature, false)); + currentHighlightedFeature = null; + } + }, 2000); +} + +function triggerSimulatedDetection(feature) { + if (!feature) return; + + const nodeName = feature.get('name') || 'Unknown Node'; + highlightNode(feature); + showNotification(`${nodeName} detected`); + console.log('Simulated detection triggered for:', nodeName); +} + +function startDetectionSimulation(intervalMs = 3000) { + stopDetectionSimulation(); + + if (!nodeFeaturesCache || nodeFeaturesCache.length === 0) { + console.warn('No node features available for detection simulation.'); + return; + } + + detectionSimulationInterval = setInterval(() => { + const randomIndex = Math.floor(Math.random() * nodeFeaturesCache.length); + const randomNodeFeature = nodeFeaturesCache[randomIndex]; + triggerSimulatedDetection(randomNodeFeature); + }, intervalMs); + + console.log(`Detection simulation started with interval ${intervalMs}ms`); +} + +function stopDetectionSimulation() { + if (detectionSimulationInterval) { + clearInterval(detectionSimulationInterval); + detectionSimulationInterval = null; + } + + resetCurrentHighlightedNode(); + console.log('Detection simulation stopped'); +} + +function getFallbackNodes() { + return [ + { + _id: 'node-1', + name: 'Node A', + type: 'master', + model: 'Echo Master', + location: { longitude: 143.509, latitude: -38.673 } + }, + { + _id: 'node-2', + name: 'Node B', + type: 'sensor', + model: 'Echo Sensor', + location: { longitude: 143.515, latitude: -38.678 } + }, + { + _id: 'node-3', + name: 'Node C', + type: 'sensor', + model: 'Echo Sensor', + location: { longitude: 143.520, latitude: -38.684 } } + ]; +} + +async function addIoTNodesToMap(hmiState) { + if (!hmiState.basemap) { + console.error('Basemap not initialized'); + return; + } + + try { + let nodes = []; + try { - // Fetch nodes from the API - const response = await axios.get('/iot/nodes'); - const nodes = response.data; - - // Create a new vector layer for IoT nodes - const iotLayer = new ol.layer.Vector({ - source: new ol.source.Vector(), - style: function(feature) { - const type = feature.get('type'); - const iconSrc = type === 'master' ? 'images/nodes/master-node.svg' : 'images/nodes/microchip-solid.svg'; - const circleColor = type === 'master' ? '#ff4444' : '#4CAF50'; - - return [ - // Background circle - new ol.style.Style({ - image: new ol.style.Circle({ - radius: 23, - fill: new ol.style.Fill({ color: circleColor }), - }) - }), - // Icon on top - new ol.style.Style({ - image: new ol.style.Icon({ - src: iconSrc, - scale: 0.05, - anchor: [0.5, 0.5], - anchorXUnits: 'fraction', - anchorYUnits: 'fraction' - }) - }) - ]; - } - }); - - // Add features for each node - nodes.forEach(node => { - const feature = new ol.Feature({ - geometry: new ol.geom.Point(ol.proj.fromLonLat([ - node.location.longitude, - node.location.latitude - ])), - lat: node.location.latitude, - lon: node.location.longitude, - type: node.type, - name: node.name, - model: node.model, - isNode: true - }); - - feature.setId(node._id); - iotLayer.getSource().addFeature(feature); - - // If node has connections, draw lines - if (node.connectedNodes && node.connectedNodes.length > 0) { - node.connectedNodes.forEach(connectedId => { - const connectedNode = nodes.find(n => n._id === connectedId); - if (connectedNode) { - const lineFeature = new ol.Feature({ - geometry: new ol.geom.LineString([ - ol.proj.fromLonLat([node.location.longitude, node.location.latitude]), - ol.proj.fromLonLat([connectedNode.location.longitude, connectedNode.location.latitude]) - ]) - }); - - const lineStyle = new ol.style.Style({ - stroke: new ol.style.Stroke({ - color: '#3388ff', - width: 2, - lineDash: [5, 10] - }) - }); - - lineFeature.setStyle(lineStyle); - iotLayer.getSource().addFeature(lineFeature); - } - }); - } - }); - - // Add the layer to the map - hmiState.basemap.addLayer(iotLayer); - - // Add popup for node information - const popup = new ol.Overlay({ - element: document.createElement('div'), - positioning: 'bottom-center', - stopEvent: false - }); - hmiState.basemap.addOverlay(popup); - - // Show node info on hover - hmiState.basemap.on('pointermove', function(evt) { - const feature = hmiState.basemap.forEachFeatureAtPixel(evt.pixel, function(feature) { - return feature; - }); - - const element = popup.getElement(); - if (feature && feature.get('name') && feature.get('type') && feature.get('model')) { - const coordinates = feature.getGeometry().getCoordinates(); - popup.setPosition(coordinates); - element.innerHTML = ` -
- ${feature.get('name')}
- Type: ${feature.get('type')}
- Model: ${feature.get('model')} -
- `; - element.style.display = 'block'; - } else { - element.style.display = 'none'; - } - }); - - } catch (error) { - console.error('Error loading IoT nodes:', error); + const response = await axios.get('http://127.0.0.1:8000/iot/nodes'); + nodes = response.data; + console.log('Loaded nodes from backend API'); + } catch (apiError) { + console.warn('Backend /iot/nodes not available. Using fallback nodes instead.'); + nodes = getFallbackNodes(); } + + const iotLayer = new ol.layer.Vector({ + source: new ol.source.Vector(), + style: function (feature) { + if (feature.get('isNode')) { + return getNodeStyles(feature, false); + } + return feature.getStyle(); + } + }); + + nodeFeaturesCache = []; + + nodes.forEach((node) => { + const feature = new ol.Feature({ + geometry: new ol.geom.Point( + ol.proj.fromLonLat([ + node.location.longitude, + node.location.latitude + ]) + ), + lat: node.location.latitude, + lon: node.location.longitude, + type: node.type, + name: node.name, + model: node.model, + isNode: true + }); + + feature.setId(node._id); + feature.setStyle(getNodeStyles(feature, false)); + iotLayer.getSource().addFeature(feature); + nodeFeaturesCache.push(feature); + }); + + hmiState.basemap.addLayer(iotLayer); + + const popupElement = document.createElement('div'); + popupElement.className = 'node-popup'; + + const popup = new ol.Overlay({ + element: popupElement, + positioning: 'bottom-center', + stopEvent: false + }); + + hmiState.basemap.addOverlay(popup); + + hmiState.basemap.on('pointermove', function (evt) { + const feature = hmiState.basemap.forEachFeatureAtPixel( + evt.pixel, + function (feature) { + return feature; + } + ); + + const element = popup.getElement(); + + if ( + feature && + feature.get('isNode') && + feature.get('name') && + feature.get('type') && + feature.get('model') + ) { + const coordinates = feature.getGeometry().getCoordinates(); + popup.setPosition(coordinates); + element.innerHTML = ` +
+ ${feature.get('name')}
+ Type: ${feature.get('type')}
+ Model: ${feature.get('model')} +
+ `; + element.style.display = 'block'; + } else { + element.style.display = 'none'; + } + }); + + hmiState.basemap.on('singleclick', function (evt) { + const feature = hmiState.basemap.forEachFeatureAtPixel( + evt.pixel, + function (feature) { + return feature; + } + ); + + if (feature && feature.get('isNode')) { + triggerSimulatedDetection(feature); + } + }); + + startDetectionSimulation(3000); + + } catch (error) { + console.error('Error loading IoT nodes:', error); + } } +window.startDetectionSimulation = startDetectionSimulation; +window.stopDetectionSimulation = stopDetectionSimulation; -// Export the function to be used in HMI.js -export { addIoTNodesToMap }; +export { addIoTNodesToMap, startDetectionSimulation, stopDetectionSimulation }; \ No newline at end of file From 88b880083fc5ba8ab1b58b74fc00654a837cd8d4 Mon Sep 17 00:00:00 2001 From: Pratoshsukumar Date: Sun, 17 May 2026 10:20:20 +1000 Subject: [PATCH 3/3] Added onboarding task frontend updates and detection improvements --- .../HMI/ui/public/css/HMI_style.css | 191 +++++++++ src/Components/HMI/ui/public/index.html | 67 ++- src/Components/HMI/ui/public/js/HMI.js | 7 +- .../HMI/ui/public/js/nodes-overlay.js | 381 ++++++++++++++---- 4 files changed, 551 insertions(+), 95 deletions(-) diff --git a/src/Components/HMI/ui/public/css/HMI_style.css b/src/Components/HMI/ui/public/css/HMI_style.css index 349edd025..8f3719036 100644 --- a/src/Components/HMI/ui/public/css/HMI_style.css +++ b/src/Components/HMI/ui/public/css/HMI_style.css @@ -2885,4 +2885,195 @@ z-index: 1; 100% { transform: scale(1); } +} + +/* ================================ + Sprint 2 Detection Animation CSS + ================================ */ + +.notification-success { + border-left: 5px solid #4caf50; +} + +.notification-error { + border-left: 5px solid #ff4444; +} + +.notification-info { + border-left: 5px solid #ffd700; +} + +.latest-detection-panel { + position: fixed; + right: 20px; + bottom: 120px; + width: 260px; + background: rgba(10, 10, 25, 0.92); + color: rgba(247, 186, 206, 1); + border: 1px solid rgba(247, 186, 206, 0.5); + border-radius: 12px; + padding: 15px; + z-index: 1200; + font-family: 'Roboto', sans-serif; + box-shadow: 0 0 18px rgba(255, 215, 0, 0.25); +} + +.latest-detection-panel h4 { + margin: 0 0 8px 0; + color: #ffd700; + font-size: 18px; +} + +.latest-detection-panel p { + margin: 4px 0; + font-size: 14px; +} + +.detection-flash { + animation: detectionFlash 1.2s ease-in-out; +} + +@keyframes detectionFlash { + 0% { + background: rgba(255, 215, 0, 0.95); + color: black; + transform: scale(1.04); + } + + 50% { + background: rgba(255, 215, 0, 0.45); + color: white; + } + + 100% { + background: rgba(10, 10, 25, 0.92); + color: rgba(247, 186, 206, 1); + transform: scale(1); + } +} + +.detection-badge { + position: absolute; + top: -6px; + right: -6px; + background: #ff3333; + color: white; + font-size: 11px; + min-width: 18px; + height: 18px; + padding: 2px 5px; + border-radius: 999px; + font-weight: bold; + display: inline-flex; + align-items: center; + justify-content: center; + z-index: 1300; +} + + +#live-detection-panel{ + margin-top:20px; + width:100%; +} + +.detection-stats{ + display:flex; + gap:15px; + margin-bottom:20px; +} + +.stat-card{ + flex:1; + background:rgba(0,0,0,0.45); + border:1px solid rgba(0,255,170,0.25); + border-radius:12px; + padding:15px; + backdrop-filter: blur(10px); + box-shadow:0 0 15px rgba(0,255,170,0.15); + text-align:center; +} + +.stat-number{ + display:block; + font-size:28px; + font-weight:bold; + color:#00ffaa; +} + +.stat-label{ + color:white; + font-size:13px; +} + +#timeline-container{ + display:flex; + flex-direction:column; + gap:12px; + max-height:400px; + overflow-y:auto; + padding-right:5px; +} + +.timeline-card{ + background:rgba(255,255,255,0.05); + border-left:4px solid #00ffaa; + padding:14px; + border-radius:10px; + animation:slideIn 0.4s ease; + box-shadow:0 0 12px rgba(0,255,170,0.12); +} + +.timeline-header{ + display:flex; + justify-content:space-between; + margin-bottom:6px; +} + +.timeline-animal{ + color:#00ffaa; + font-weight:bold; + font-size:16px; +} + +.timeline-time{ + color:#bbbbbb; + font-size:12px; +} + +.timeline-location{ + color:white; + font-size:14px; +} + +.timeline-node{ + margin-top:6px; + color:#ffcc70; + font-size:13px; +} + +@keyframes slideIn{ + from{ + opacity:0; + transform:translateX(30px); + } + to{ + opacity:1; + transform:translateX(0); + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +#notification { + animation: fadeIn 0.4s ease; } \ No newline at end of file diff --git a/src/Components/HMI/ui/public/index.html b/src/Components/HMI/ui/public/index.html index 399eb2d7a..05f21cb92 100644 --- a/src/Components/HMI/ui/public/index.html +++ b/src/Components/HMI/ui/public/index.html @@ -15,7 +15,7 @@ src="./js/ol.js" > - +
@@ -2622,10 +2643,46 @@

simulatorOl.addEventListener("click", openSIMULATOR) window.onload = function () { - console.log("Website loaded!"); - initialiseHMI(hmiState); - //sim is usually on by default, but this functionality is disabled during development - }; + + console.log("Website loaded!"); + + initialiseHMI(hmiState); + + setInterval(() => { + + const animals = [ + "Koala", + "Frogmouth", + "Kangaroo", + "Possum", + "Tasmanian Devil", + "Sugar Glider", + "Wombat" + ]; + + const randomAnimal = + animals[Math.floor(Math.random() * animals.length)]; + + const randomNode = + "NODE-" + Math.floor(Math.random() * 5 + 1); + + const currentTime = + new Date().toLocaleTimeString(); + + showNotification( + `${randomAnimal} detected by ${randomNode}` + ); + + document.getElementById("notification-text").innerHTML = ` + Latest Detection
+ Animal: ${randomAnimal}
+ Node: ${randomNode}
+ Type: sensor
+ Time: ${currentTime} +`; + + }, 5000); +}; //Microphone show/hide function updateMics() { diff --git a/src/Components/HMI/ui/public/js/HMI.js b/src/Components/HMI/ui/public/js/HMI.js index 0c3ab596d..2f4d00e73 100644 --- a/src/Components/HMI/ui/public/js/HMI.js +++ b/src/Components/HMI/ui/public/js/HMI.js @@ -130,7 +130,6 @@ export function initialiseHMI(hmiState) { console.warn("Microphone API failed, continuing with fallback setup.", error); }); -// Always add IoT nodes, even if microphone API fails addIoTNodesToMap(hmiState); addmicrophones(hmiState); stepMicAnimation(hmiState); @@ -1369,11 +1368,7 @@ export function updateTimeOffset(hmiState){ else{ hmiState.simUpdateDelay = newDelay; } - // Multiply by 1000 to convert to milliseconds - /*const utcDate = new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), - date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()); - hmiState.simTime = utcDate;*/ - //console.log(hmiState.simUpdateDelay); + }); }catch(error){ console.log(failed); diff --git a/src/Components/HMI/ui/public/js/nodes-overlay.js b/src/Components/HMI/ui/public/js/nodes-overlay.js index a23b91706..4efc00648 100644 --- a/src/Components/HMI/ui/public/js/nodes-overlay.js +++ b/src/Components/HMI/ui/public/js/nodes-overlay.js @@ -1,8 +1,10 @@ + + let axios; -if (typeof window === 'undefined') { - axios = require('axios'); +if (typeof window === "undefined") { + axios = require("axios"); } else { axios = window.axios; } @@ -12,50 +14,73 @@ let currentHighlightedFeature = null; let currentHighlightResetTimeout = null; let nodeFeaturesCache = []; -function showNotification(message) { - const notification = document.getElementById('notification'); - const text = document.getElementById('notification-text'); - const closeBtn = document.getElementById('notification-close'); +/* ---------------- NOTIFICATION ---------------- */ + +function showNotification(message, type = "info") { + const notification = document.getElementById("notification"); + const text = document.getElementById("notification-text"); + const closeBtn = document.getElementById("notification-close"); if (!notification || !text) { - console.error('Notification elements not found'); + console.error("Notification elements not found"); return; } text.innerText = message; - notification.style.display = 'block'; + + notification.classList.remove( + "notification-success", + "notification-error", + "notification-info" + ); + + notification.classList.add(`notification-${type}`); + notification.style.display = "block"; const hide = () => { - notification.style.display = 'none'; + notification.style.display = "none"; }; if (closeBtn) { closeBtn.onclick = hide; } - setTimeout(hide, 3000); + setTimeout(hide, 3500); } window.showNotification = showNotification; -function getNodeStyles(feature, highlighted = false) { - const type = feature.get('type'); +/* ---------------- NODE STYLING ---------------- */ + +function getNodeStyles(feature, state = "idle") { + const type = feature.get("type"); + const iconSrc = - type === 'master' - ? 'images/nodes/master-node.svg' - : 'images/nodes/microchip-solid.svg'; + type === "master" + ? "images/nodes/master-node.svg" + : "images/nodes/microchip-solid.svg"; - const baseColor = type === 'master' ? '#ff4444' : '#4CAF50'; + const baseColor = type === "master" ? "#ff4444" : "#4CAF50"; - if (highlighted) { + if (state === "detecting") { return [ new ol.style.Style({ image: new ol.style.Circle({ - radius: 28, - fill: new ol.style.Fill({ color: '#FFD700' }), + radius: 34, + fill: new ol.style.Fill({ color: "rgba(255, 215, 0, 0.25)" }), + stroke: new ol.style.Stroke({ + color: "#FFD700", + width: 4 + }) + }) + }), + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 24, + fill: new ol.style.Fill({ color: baseColor }), stroke: new ol.style.Stroke({ - color: '#fff', - width: 3 + color: "#ffffff", + width: 2 }) }) }), @@ -64,8 +89,32 @@ function getNodeStyles(feature, highlighted = false) { src: iconSrc, scale: 0.05, anchor: [0.5, 0.5], - anchorXUnits: 'fraction', - anchorYUnits: 'fraction' + anchorXUnits: "fraction", + anchorYUnits: "fraction" + }) + }) + ]; + } + + if (state === "detected") { + return [ + new ol.style.Style({ + image: new ol.style.Circle({ + radius: 30, + fill: new ol.style.Fill({ color: "#FFD700" }), + stroke: new ol.style.Stroke({ + color: "#ffffff", + width: 4 + }) + }) + }), + new ol.style.Style({ + image: new ol.style.Icon({ + src: iconSrc, + scale: 0.055, + anchor: [0.5, 0.5], + anchorXUnits: "fraction", + anchorYUnits: "fraction" }) }) ]; @@ -77,7 +126,7 @@ function getNodeStyles(feature, highlighted = false) { radius: 23, fill: new ol.style.Fill({ color: baseColor }), stroke: new ol.style.Stroke({ - color: '#fff', + color: "#ffffff", width: 2 }) }) @@ -87,13 +136,15 @@ function getNodeStyles(feature, highlighted = false) { src: iconSrc, scale: 0.05, anchor: [0.5, 0.5], - anchorXUnits: 'fraction', - anchorYUnits: 'fraction' + anchorXUnits: "fraction", + anchorYUnits: "fraction" }) }) ]; } +/* ---------------- DETECTION HANDLER ---------------- */ + function resetCurrentHighlightedNode() { if (currentHighlightResetTimeout) { clearTimeout(currentHighlightResetTimeout); @@ -101,53 +152,183 @@ function resetCurrentHighlightedNode() { } if (currentHighlightedFeature) { - currentHighlightedFeature.setStyle(getNodeStyles(currentHighlightedFeature, false)); + currentHighlightedFeature.setStyle( + getNodeStyles(currentHighlightedFeature, "idle") + ); currentHighlightedFeature = null; } } -function highlightNode(feature) { - if (!feature) return; +function handleDetectionEvent(detection) { + if (!detection || !detection.nodeId) { + console.warn("Invalid detection event received:", detection); + return; + } + + const feature = nodeFeaturesCache.find((f) => f.getId() === detection.nodeId); + + if (!feature) { + console.warn("Detection node not found:", detection.nodeId); + showNotification( + `Detection received, but node ${detection.nodeId} was not found`, + "error" + ); + return; + } resetCurrentHighlightedNode(); - feature.setStyle(getNodeStyles(feature, true)); + const nodeName = feature.get("name") || "Unknown Node"; + const nodeType = feature.get("type") || "unknown"; + + feature.setStyle(getNodeStyles(feature, "detecting")); currentHighlightedFeature = feature; - currentHighlightResetTimeout = setTimeout(() => { - if (currentHighlightedFeature) { - currentHighlightedFeature.setStyle(getNodeStyles(currentHighlightedFeature, false)); + setTimeout(() => { + feature.setStyle(getNodeStyles(feature, "detected")); + + showNotification( + `Detection Alert: ${nodeName} (${nodeType}) detected activity`, + "success" + ); + + flashSidebarDetection(nodeName, nodeType); + updateDetectionBadge(); + + currentHighlightResetTimeout = setTimeout(() => { + feature.setStyle(getNodeStyles(feature, "idle")); currentHighlightedFeature = null; - } - }, 2000); + }, 2500); + }, 700); + + console.log("Detection event handled:", detection); +} + +window.handleDetectionEvent = handleDetectionEvent; + +/* ---------------- SIDEBAR + BADGE ---------------- */ + +function flashSidebarDetection(nodeName, nodeType) { + let panel = document.getElementById("latest-detection-panel"); + + if (!panel) { + panel = document.createElement("div"); + panel.id = "latest-detection-panel"; + panel.className = "latest-detection-panel"; + + const menu = document.getElementById("menu") || document.body; + menu.appendChild(panel); + } + + const time = new Date().toLocaleTimeString(); + + panel.innerHTML = ` +

Latest Detection

+

Node: ${nodeName}

+

Type: ${nodeType}

+

Time: ${time}

+ `; + + panel.classList.remove("detection-flash"); + void panel.offsetWidth; + panel.classList.add("detection-flash"); + + panel.scrollIntoView({ behavior: "smooth", block: "nearest" }); } +function updateDetectionBadge() { + let badge = document.getElementById("detection-badge"); + + if (!badge) { + const notificationBtn = + document.getElementById("notification-menu-button") || + document.getElementById("node-popup-menu-btn"); + + if (!notificationBtn) return; + + badge = document.createElement("span"); + badge.id = "detection-badge"; + badge.className = "detection-badge"; + badge.innerText = "0"; + + notificationBtn.appendChild(badge); + } + + const currentValue = parseInt(badge.innerText || "0", 10); + badge.innerText = currentValue + 1; +} + +/* ---------------- SIMULATION FALLBACK ---------------- */ + function triggerSimulatedDetection(feature) { if (!feature) return; - const nodeName = feature.get('name') || 'Unknown Node'; - highlightNode(feature); - showNotification(`${nodeName} detected`); - console.log('Simulated detection triggered for:', nodeName); + const fakeDetection = { + nodeId: feature.getId(), + nodeName: feature.get("name"), + nodeType: feature.get("type"), + timestamp: new Date().toISOString() + }; + + handleDetectionEvent(fakeDetection); +} + +function triggerRandomSimulatedDetection() { + if (!nodeFeaturesCache || nodeFeaturesCache.length === 0) { + console.warn("No node features available for fallback simulation."); + return; + } + + const randomIndex = Math.floor(Math.random() * nodeFeaturesCache.length); + const randomNodeFeature = nodeFeaturesCache[randomIndex]; + + triggerSimulatedDetection(randomNodeFeature); } +/* ---------------- SIMULATION MODE ---------------- */ + function startDetectionSimulation(intervalMs = 3000) { stopDetectionSimulation(); if (!nodeFeaturesCache || nodeFeaturesCache.length === 0) { - console.warn('No node features available for detection simulation.'); + console.warn("No node features available for detection simulation."); return; } detectionSimulationInterval = setInterval(() => { - const randomIndex = Math.floor(Math.random() * nodeFeaturesCache.length); - const randomNodeFeature = nodeFeaturesCache[randomIndex]; - triggerSimulatedDetection(randomNodeFeature); + triggerRandomSimulatedDetection(); }, intervalMs); console.log(`Detection simulation started with interval ${intervalMs}ms`); } +/* ---------------- LIVE POLLING MODE + FALLBACK ---------------- */ + +function startLiveDetectionPolling(intervalMs = 3000) { + stopDetectionSimulation(); + + detectionSimulationInterval = setInterval(async () => { + try { + const response = await axios.get("http://localhost:8080/latest_detection"); + const detection = response.data; + + if (detection && detection.nodeId) { + handleDetectionEvent(detection); + } else { + console.warn("No valid live detection received. Using simulation fallback."); + triggerRandomSimulatedDetection(); + } + } catch (error) { + console.warn("Live detection backend not available. Using simulation fallback."); + triggerRandomSimulatedDetection(); + } + }, intervalMs); + + console.log(`Live detection polling started with interval ${intervalMs}ms`); +} + +/* ---------------- STOP ALL DETECTION ---------------- */ + function stopDetectionSimulation() { if (detectionSimulationInterval) { clearInterval(detectionSimulationInterval); @@ -155,38 +336,60 @@ function stopDetectionSimulation() { } resetCurrentHighlightedNode(); - console.log('Detection simulation stopped'); + console.log("Detection simulation stopped"); } +window.startDetectionSimulation = startDetectionSimulation; +window.startLiveDetectionPolling = startLiveDetectionPolling; +window.stopDetectionSimulation = stopDetectionSimulation; + +/* ---------------- FALLBACK NODES ---------------- */ + function getFallbackNodes() { return [ { - _id: 'node-1', - name: 'Node A', - type: 'master', - model: 'Echo Master', + _id: "node-1", + name: "Node A", + type: "master", + model: "Echo Master", location: { longitude: 143.509, latitude: -38.673 } }, { - _id: 'node-2', - name: 'Node B', - type: 'sensor', - model: 'Echo Sensor', + _id: "node-2", + name: "Node B", + type: "sensor", + model: "Echo Sensor", location: { longitude: 143.515, latitude: -38.678 } }, { - _id: 'node-3', - name: 'Node C', - type: 'sensor', - model: 'Echo Sensor', - location: { longitude: 143.520, latitude: -38.684 } + _id: "node-3", + name: "Node C", + type: "sensor", + model: "Echo Sensor", + location: { longitude: 143.52, latitude: -38.684 } + }, + { + _id: "node-4", + name: "Node D", + type: "sensor", + model: "Echo Sensor", + location: { longitude: 143.525, latitude: -38.679 } + }, + { + _id: "node-5", + name: "Node E", + type: "sensor", + model: "Echo Sensor", + location: { longitude: 143.512, latitude: -38.688 } } ]; } +/* ---------------- MAP NODE LOADING ---------------- */ + async function addIoTNodesToMap(hmiState) { if (!hmiState.basemap) { - console.error('Basemap not initialized'); + console.error("Basemap not initialized"); return; } @@ -194,19 +397,19 @@ async function addIoTNodesToMap(hmiState) { let nodes = []; try { - const response = await axios.get('http://127.0.0.1:8000/iot/nodes'); + const response = await axios.get("http://127.0.0.1:8000/iot/nodes"); nodes = response.data; - console.log('Loaded nodes from backend API'); + console.log("Loaded nodes from backend API"); } catch (apiError) { - console.warn('Backend /iot/nodes not available. Using fallback nodes instead.'); + console.warn("Backend /iot/nodes not available. Using fallback nodes instead."); nodes = getFallbackNodes(); } const iotLayer = new ol.layer.Vector({ source: new ol.source.Vector(), style: function (feature) { - if (feature.get('isNode')) { - return getNodeStyles(feature, false); + if (feature.get("isNode")) { + return getNodeStyles(feature, "idle"); } return feature.getStyle(); } @@ -231,25 +434,26 @@ async function addIoTNodesToMap(hmiState) { }); feature.setId(node._id); - feature.setStyle(getNodeStyles(feature, false)); + feature.setStyle(getNodeStyles(feature, "idle")); + iotLayer.getSource().addFeature(feature); nodeFeaturesCache.push(feature); }); hmiState.basemap.addLayer(iotLayer); - const popupElement = document.createElement('div'); - popupElement.className = 'node-popup'; + const popupElement = document.createElement("div"); + popupElement.className = "node-popup"; const popup = new ol.Overlay({ element: popupElement, - positioning: 'bottom-center', + positioning: "bottom-center", stopEvent: false }); hmiState.basemap.addOverlay(popup); - hmiState.basemap.on('pointermove', function (evt) { + hmiState.basemap.on("pointermove", function (evt) { const feature = hmiState.basemap.forEachFeatureAtPixel( evt.pixel, function (feature) { @@ -261,27 +465,29 @@ async function addIoTNodesToMap(hmiState) { if ( feature && - feature.get('isNode') && - feature.get('name') && - feature.get('type') && - feature.get('model') + feature.get("isNode") && + feature.get("name") && + feature.get("type") && + feature.get("model") ) { const coordinates = feature.getGeometry().getCoordinates(); popup.setPosition(coordinates); + element.innerHTML = `
- ${feature.get('name')}
- Type: ${feature.get('type')}
- Model: ${feature.get('model')} + ${feature.get("name")}
+ Type: ${feature.get("type")}
+ Model: ${feature.get("model")}
`; - element.style.display = 'block'; + + element.style.display = "block"; } else { - element.style.display = 'none'; + element.style.display = "none"; } }); - hmiState.basemap.on('singleclick', function (evt) { + hmiState.basemap.on("singleclick", function (evt) { const feature = hmiState.basemap.forEachFeatureAtPixel( evt.pixel, function (feature) { @@ -289,18 +495,25 @@ async function addIoTNodesToMap(hmiState) { } ); - if (feature && feature.get('isNode')) { + if (feature && feature.get("isNode")) { triggerSimulatedDetection(feature); } }); - startDetectionSimulation(3000); + // Sprint 2 HD upgrade: + // First tries live backend. If unavailable, automatically uses simulation fallback. + startLiveDetectionPolling(3000); } catch (error) { - console.error('Error loading IoT nodes:', error); + console.error("Error loading IoT nodes:", error); + showNotification("Error loading IoT nodes", "error"); } } -window.startDetectionSimulation = startDetectionSimulation; -window.stopDetectionSimulation = stopDetectionSimulation; -export { addIoTNodesToMap, startDetectionSimulation, stopDetectionSimulation }; \ No newline at end of file +export { + addIoTNodesToMap, + startDetectionSimulation, + startLiveDetectionPolling, + stopDetectionSimulation, + handleDetectionEvent +}; \ No newline at end of file