diff --git a/src/Components/HMI/ui/public/css/HMI_style.css b/src/Components/HMI/ui/public/css/HMI_style.css index d561320cc..8f3719036 100644 --- a/src/Components/HMI/ui/public/css/HMI_style.css +++ b/src/Components/HMI/ui/public/css/HMI_style.css @@ -2867,3 +2867,213 @@ 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); + } +} + +/* ================================ + 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 a0918b7ac..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" > - +
@@ -2248,25 +2275,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; @@ -2618,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() { @@ -2925,6 +2986,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..2f4d00e73 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,20 @@ 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); +}); + +addIoTNodesToMap(hmiState); addmicrophones(hmiState); stepMicAnimation(hmiState); queueSimUpdate(hmiState); @@ -345,14 +330,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 = {}; @@ -1390,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); @@ -1949,4 +1923,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/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": [ diff --git a/src/Components/HMI/ui/public/js/nodes-overlay.js b/src/Components/HMI/ui/public/js/nodes-overlay.js index c60db7151..4efc00648 100644 --- a/src/Components/HMI/ui/public/js/nodes-overlay.js +++ b/src/Components/HMI/ui/public/js/nodes-overlay.js @@ -1,135 +1,519 @@ + + + let axios; -if (typeof window === 'undefined') { - axios = require('axios'); +if (typeof window === "undefined") { + axios = require("axios"); } else { 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 = []; + +/* ---------------- 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"); + return; + } + + text.innerText = message; + + notification.classList.remove( + "notification-success", + "notification-error", + "notification-info" + ); + + notification.classList.add(`notification-${type}`); + notification.style.display = "block"; + + const hide = () => { + notification.style.display = "none"; + }; + + if (closeBtn) { + closeBtn.onclick = hide; + } + + setTimeout(hide, 3500); +} + +window.showNotification = showNotification; + +/* ---------------- 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"; + + const baseColor = type === "master" ? "#ff4444" : "#4CAF50"; + + if (state === "detecting") { + return [ + new ol.style.Style({ + image: new ol.style.Circle({ + 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: "#ffffff", + 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" + }) + }) + ]; + } + + 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" + }) + }) + ]; + } + + 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: "#ffffff", + 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" + }) + }) + ]; +} + +/* ---------------- DETECTION HANDLER ---------------- */ + +function resetCurrentHighlightedNode() { + if (currentHighlightResetTimeout) { + clearTimeout(currentHighlightResetTimeout); + currentHighlightResetTimeout = null; + } + + if (currentHighlightedFeature) { + currentHighlightedFeature.setStyle( + getNodeStyles(currentHighlightedFeature, "idle") + ); + currentHighlightedFeature = null; + } +} + +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(); + + const nodeName = feature.get("name") || "Unknown Node"; + const nodeType = feature.get("type") || "unknown"; + + feature.setStyle(getNodeStyles(feature, "detecting")); + currentHighlightedFeature = feature; + + 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; + }, 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 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."); + return; + } + + detectionSimulationInterval = setInterval(() => { + triggerRandomSimulatedDetection(); + }, intervalMs); + + console.log(`Detection simulation started with interval ${intervalMs}ms`); +} + +/* ---------------- LIVE POLLING MODE + FALLBACK ---------------- */ + +function startLiveDetectionPolling(intervalMs = 3000) { + stopDetectionSimulation(); + + detectionSimulationInterval = setInterval(async () => { 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'; - } - }); + 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.error('Error loading IoT nodes:', 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); + detectionSimulationInterval = null; + } + + resetCurrentHighlightedNode(); + 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", + 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.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"); + return; + } + + try { + let nodes = []; + + try { + 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, "idle"); + } + 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, "idle")); + + 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); + } + }); + + // 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); + showNotification("Error loading IoT nodes", "error"); + } } -// Export the function to be used in HMI.js -export { addIoTNodesToMap }; +export { + addIoTNodesToMap, + startDetectionSimulation, + startLiveDetectionPolling, + stopDetectionSimulation, + handleDetectionEvent +}; \ No newline at end of file