// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import { useState, useEffect } from 'react'; import { useLocation } from 'react-router'; import { Redirect } from 'react-router-dom'; import { I18n, Logger } from '@aws-amplify/core'; import { API_NAME } from '../util/Utils'; import { IPageProps, ISimulation, simTypes } from '../components/Shared/Interfaces'; import { API } from '@aws-amplify/api'; import { PubSub } from '@aws-amplify/pubsub'; import moment from 'moment'; import PageTitleBar from '../components/Shared/PageTitleBar'; import Footer from '../components/Shared/Footer'; import Card from 'react-bootstrap/Card'; import Row from 'react-bootstrap/Row'; import Col from 'react-bootstrap/Col'; import Button from 'react-bootstrap/Button'; import Tab from 'react-bootstrap/Tab'; import Nav from 'react-bootstrap/Nav'; import Dropdown from 'react-bootstrap/Dropdown'; import DropdownButton from 'react-bootstrap/DropdownButton'; import { Coordinates } from "maplibre-gl-js-amplify/lib/esm/types"; import { createMap, drawPoints } from "maplibre-gl-js-amplify"; import { Feature, FeatureCollection } from "geojson"; export default function SimulationDetails(props: IPageProps): JSX.Element { const location = useLocation(); const logger = new Logger("Simulation Details"); const [sim, setSim] = useState(); const [topics, setTopics] = useState<{ [key: string]: Array }>({}); const [messages, setMessages] = useState>([]); const [activeTopic, setActiveTopic] = useState(); const [activeDevice, setActiveDevice] = useState<{ [key: string]: string }>({ id: "All", name: "All" }); const [map, setMap] = useState(); const [shouldRedirect, setShouldRedirect] = useState(false); /** * Load simulation from ddb */ const loadSimulation = async () => { const simId = location.pathname.split('/').pop(); try { const results = await API.get(API_NAME, `/simulation/${simId}`, {}); setSim(results); if (results?.simId.includes(simTypes.autoDemo)) { initializeMap(); } } catch (err: any) { logger.error(I18n.get("simulation.get.error"), err); if (err.response?.data?.error === "MissingSimulation") { setShouldRedirect(true); } else { throw err; } } } /** * load devices belonging to a simulation */ const loadDevices = async () => { const simId = location.pathname.split('/').pop(); try { const results = await API.get(API_NAME, `/simulation/${simId}`, { queryStringParameters: { op: "list dtype attributes", filter: "topic, typeId" } }) let newTopics: { [key: string]: Array } = {}; results.forEach((result: any) => { if (!newTopics[result.topic]) { newTopics[result.topic] = [result.typeId]; } else { newTopics[result.topic].push(result.typeId); } }) setTopics({ ...newTopics }); setActiveTopic((Object.keys(newTopics))[0]); } catch (err) { logger.error(I18n.get("device.types.get.error"), err); throw err; } } async function initializeMap() { const newMap = await createMap({ container: "map", // An HTML Element or HTML element ID to render the map in https://maplibre.org/maplibre-gl-js-docs/api/map/ center: [-123.1187, 49.2819], zoom: 3, }) setMap(newMap); } /** * parse and save incoming IoT message * @param data */ const handleMessage = (data: any) => { let message = { title: data.value[Object.getOwnPropertySymbols(data.value)[0]], content: data.value, timestamp: moment().format('MMM Do YYYY HH:mm:ss') } if (messages.length >= 100) { messages.pop(); } messages.unshift(message); setMessages([...messages]); } /** * react useEffect hook * load simulation and needed device type info (topic, id) on load * and initializes map if auto demo */ useEffect(() => { loadSimulation(); loadDevices(); }, []); /** * updates the map coordinates with new messages if a map exists */ useEffect(() => { if (map && map.isStyleLoaded()) { let uniqueDeviceMessages = messages.filter((message, index, self) => self.findIndex((uniqueMsg) => uniqueMsg.content._id_ === message.content._id_) === index); let centerLat = 0; let centerLng = 0; let coordinates = uniqueDeviceMessages.map((message) => { centerLat = centerLat + message.content.location.latitude; centerLng = centerLng + message.content.location.longitude; return [message.content.location.longitude, message.content.location.latitude] }); centerLat = Number((centerLat / coordinates.length).toFixed(6)); centerLng = Number((centerLng / coordinates.length).toFixed(6)); let mapSource = map.getSource('IoTMessage-source-points'); if (!mapSource) { drawPoints("IoTMessage", coordinates as Coordinates[], map, { clusterOptions: { showCount: true, smCircleSize: 20, mdCircleSize: 40 } } ); //jumpTo center point if possible if (!isNaN(centerLng) && !isNaN(centerLat)) { map.jumpTo({ center: [ centerLng, centerLat ], }); } } else { const source = mapSource as maplibregl.GeoJSONSource; let features = coordinates.map((coordinate) => { return { "type": "Feature", "geometry": { "type": "Point", "coordinates": coordinate } } as Feature }); const data = { "type": "FeatureCollection", "features": features } as FeatureCollection; source.setData(data); if (coordinates.length === 1) { map.panTo([ centerLng, centerLat ]); let zoom = map.getZoom(); if (zoom < 14) { map.setZoom(14) } } } } }, [messages]); /** * react useEffect hook * on topics changes subscribe and unsubscribe. */ useEffect(() => { const iotSub = PubSub.subscribe(Object.keys(topics)).subscribe({ next: (data: any) => { handleMessage(data); }, error: (error: any) => logger.error(error) }) return () => { iotSub.unsubscribe(); } }, [topics]) /** * start simulation */ const startSim = async () => { if (sim && sim.stage === "sleeping") { const body = { action: "start", simulations: [sim] } try { await API.put(API_NAME, `/simulation/${sim.simId}`, { body: body }); sim.stage = 'running'; setSim({ ...sim }); } catch (err) { logger.error(I18n.get("simulation.start.error"), err); throw err; } } } /** * stop simulation */ const stopSim = async () => { if (sim && sim.stage === "running") { const body = { action: "stop", simulations: [sim] } try { await API.put(API_NAME, `/simulation/${sim.simId}`, { body: body }); sim.stage = "stopping"; setSim({ ...sim }); } catch (err) { logger.error(I18n.get("simulation.stop.error"), err); throw err; } } } return ( shouldRedirect ? :
{sim?.name} {I18n.get("name")}: {sim?.name} {I18n.get("stage")}: {sim?.stage} {I18n.get("created")}: {sim?.createdAt} {I18n.get("runs")}: {sim?.runs} {I18n.get("last.run")}: {sim?.lastRun} {I18n.get("last.updated")}: {sim?.updatedAt} {sim?.simId.includes(simTypes.autoDemo) ?
: ""} {I18n.get("topic")} {I18n.get("messages")} setActiveDevice({ id: "All", name: "All" })} > {I18n.get("all")} {sim?.devices.map((device, i) => { if (activeTopic && topics[activeTopic].includes(device.typeId)) { let items = []; for (let j = 0; j < device.amount; j++) { items.push(`${device.name}-${j + 1}`); } const prefix = `${sim.simId.slice(0, 3)}${device.typeId.slice(0, 3)}`; return (items.map((item, k) => ( setActiveDevice({ id: `${prefix}${k}`, name: `${item}` })} > {item} ))); } })} {Object.entries(topics).map((aTopic, i) => ( {messages.filter((message) => activeDevice.id === "All" ? message.title === aTopic[0] : message.title === aTopic[0] && message.content['_id_'] === activeDevice.id ).map((message, j) => ( {message.timestamp}
{JSON.stringify(message.content, null, 2)}
))}
))}
) }