diff --git a/backend/config/functions/cronTask.js b/backend/config/functions/cronTask.js new file mode 100644 index 0000000..b39c59e --- /dev/null +++ b/backend/config/functions/cronTask.js @@ -0,0 +1,27 @@ +// backend/config/functions/cronTask.js +/* + * Function to fetch data from Space-Track such as Eccentricy, SMA, Inclination every month + * and update the database with the new data + */ +'use strict'; + +module.exports = { + updateAllSatellitesData: { + task: async ({ strapi }) => { + try { + // Fetching all satellites + const satellites = await strapi.entityService.findMany('api::satellite.satellite'); + + // Waiting for all promises to be resolved + await Promise.all( + satellites.map(async satellite => { + await strapi.service('api::satellite.satellite').fetchOrbitalData(satellite.id); + }) + ); + } catch (error) { + console.error(error); + } + }, + options: new Date(Date.now() + 10000), + }, +}; diff --git a/backend/config/middlewares.js b/backend/config/middlewares.js index 6eaf586..72ae482 100644 --- a/backend/config/middlewares.js +++ b/backend/config/middlewares.js @@ -5,7 +5,17 @@ module.exports = [ 'strapi::cors', 'strapi::poweredBy', 'strapi::query', - 'strapi::body', + { + name: "strapi::body", + config: { + formLimit: "256mb", // modify form body + jsonLimit: "256mb", // modify JSON body + textLimit: "256mb", // modify text body + formidable: { + maxFileSize: 200 * 1024 * 1024, // multipart data, modify here limit of uploaded file size + }, + }, + }, 'strapi::session', 'strapi::favicon', 'strapi::public', diff --git a/backend/config/server.js b/backend/config/server.js index fe927ab..2133f76 100644 --- a/backend/config/server.js +++ b/backend/config/server.js @@ -1,3 +1,5 @@ +const cronTask = require('./functions/cronTask'); + module.exports = ({ env }) => ({ host: env("HOST", "127.0.0.1"), port: env.int("PORT", 1337), @@ -7,4 +9,9 @@ module.exports = ({ env }) => ({ webhooks: { populateRelations: env.bool("WEBHOOKS_POPULATE_RELATIONS", false), }, + cron: { + // Enable or disable the cron tasks + enabled: env.bool("CRON_ENABLED", false), + tasks: cronTask, + }, }); diff --git a/backend/src/api/satellite/content-types/satellite/schema.json b/backend/src/api/satellite/content-types/satellite/schema.json index 166acba..f06a1ec 100644 --- a/backend/src/api/satellite/content-types/satellite/schema.json +++ b/backend/src/api/satellite/content-types/satellite/schema.json @@ -51,6 +51,9 @@ }, "massKg": { "type": "float" + }, + "historicalOrbitalData": { + "type": "json" } } } diff --git a/backend/src/api/satellite/controllers/satellite.js b/backend/src/api/satellite/controllers/satellite.js index d4d610a..58fba9d 100644 --- a/backend/src/api/satellite/controllers/satellite.js +++ b/backend/src/api/satellite/controllers/satellite.js @@ -6,4 +6,17 @@ const { createCoreController } = require('@strapi/strapi').factories; -module.exports = createCoreController('api::satellite.satellite'); +module.exports = createCoreController('api::satellite.satellite', ({ strapi }) => ({ + async findOne(ctx) { + // Fetching data from Space-Track + const entity = await strapi.entityService.findOne('api::satellite.satellite', ctx.params.id); + const updatedSatellite = await strapi.service('api::satellite.satellite').fetchOrbitalData(ctx.params.id); + + if (!entity.historicalOrbitalData) { + return updatedSatellite; + } + + return entity; + } +}) +); diff --git a/backend/src/api/satellite/services/satellite.js b/backend/src/api/satellite/services/satellite.js index 6dfd176..87a7669 100644 --- a/backend/src/api/satellite/services/satellite.js +++ b/backend/src/api/satellite/services/satellite.js @@ -4,6 +4,70 @@ * satellite service */ + const { createCoreService } = require('@strapi/strapi').factories; -module.exports = createCoreService('api::satellite.satellite'); +const axios = require('axios'); + +// Function to fetch data from Space-Track such as Eccentricy, SMA, Inclination +async function fetchOrbitalData(contextId) { + try { + // Fetching the satellite + const satellite = await strapi.entityService.findOne('api::satellite.satellite', contextId); + const noradId = satellite.catalogNumberNORAD; + + // Authentification to Space-Track + const authResponse = await axios.post('https://www.space-track.org/ajaxauth/login', { + identity: 'grauleflorian@gmail.com', + password : 'Vm5JxTtD3-hYBdq' + }); + + if (authResponse.status === 200) { + // Fetching data from Space-Track + const satelliteResponse = await axios.get(`https://www.space-track.org/basicspacedata/query/class/gp_history/NORAD_CAT_ID/${noradId}/orderby/TLE_LINE1%20ASC/EPOCH/1950-07-02--2024-07-02/format/json`, { + headers: { + Cookie: authResponse.headers['set-cookie'] + } + }); + + if (satelliteResponse.status === 200) { + // Collecting data + const satelliteData = satelliteResponse.data; + // Parsing correctly the data with wanted data : Inclination, Eccentricity, SMA, and Epoch + const historicalOrbitalData = satelliteData.map(data => { + return { + epoch: data.EPOCH, + inclination: data.INCLINATION, + eccentricity: data.ECCENTRICITY, + semiMajorAxis: data.SEMIMAJOR_AXIS + } + }); + + if (satellite) { + // Updating the satellite with the new data + const updatedSatellite = await strapi.entityService.update('api::satellite.satellite', contextId, { + data: { + historicalOrbitalData: historicalOrbitalData, + }, + }); + return updatedSatellite; + } else { + throw new Error('Satellite not found while updating orbit data'); + } + + + } else { + throw new Error('Error while fetching data from Space-Track'); + } + } else { + throw new Error('Authentication failed'); + } + } catch (error) { + console.error('Error while fetching data to Space-Track: ', error); + } +} + +module.exports = { + ...createCoreService('api::satellite.satellite'), + fetchOrbitalData, +}; diff --git a/frontend/package.json b/frontend/package.json index ab91f44..351521e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,7 +37,7 @@ "@visx/shape": "^3.5.0", "@visx/vendor": "^3.5.0", "add": "^2.0.6", - "chart.js": "^4.4.1", + "chart.js": "^4.4.3", "chartjs-adapter-luxon": "^1.3.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", @@ -59,6 +59,7 @@ "playwright": "^1.43.1", "qs": "^6.11.2", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-error-boundary": "^4.0.13", "react-markdown": "^9.0.1", diff --git a/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx b/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx new file mode 100644 index 0000000..b412ff6 --- /dev/null +++ b/frontend/src/app/satellites/[satelliteSlug]/_orbitDataGraphComponents/ScrollBarThumb.tsx @@ -0,0 +1,83 @@ +"use client"; + +import React, { useState, useEffect, useRef } from 'react'; + +interface ScrollBarThumbProps { + scrollBarThumbWidth: number; + svgContainerRect: {topLeft: number, width: number, height: number}; +} + +const ScrollBarThumb : React.FC = ({ scrollBarThumbWidth, svgContainerRect}) => { + const isDragging = useRef(false); + {/* SB is used for ScrollBar */} + const [sBThumbX, setSBThumbX] = useState(0); + const thumbRef = useRef(null); + // Distance between the left of the thumb and the mouse click + const distThumbClick = useRef(null); + + /* Be careful useEffect runs before parent props are received */ + useEffect(() => { + const handleMouseUp = (e: any) => { + if (thumbRef.current) { + isDragging.current = false; + } + } + + const handleMouseMove = (e: MouseEvent) => { + if (isDragging.current && thumbRef.current) { + // Scrollbar starts at the right of the svg container and goes to the left by increasing SBThumbX + setSBThumbX(() => { + // Calculating min and max x positions following the SBThumbX axis for moving the thumb with mouse movement + const minX= 0.5; + const maxX = svgContainerRect.width - scrollBarThumbWidth; + // newPos represents left border of the thumb + const newPos = svgContainerRect.topLeft + svgContainerRect.width - scrollBarThumbWidth - (e.clientX - (distThumbClick.current? distThumbClick.current : 0)); + + // If mouse movement isn't in the scrollable area + if (newPos <= minX) { + return minX; + } else if (newPos >= maxX) { + return maxX; + } + return newPos; + }); + } + } + + window.addEventListener('mouseup', handleMouseUp); + window.addEventListener('mousemove', handleMouseMove); + + // Managing the resize of the thumb + if (thumbRef.current && thumbRef.current.getBoundingClientRect().x < svgContainerRect.topLeft) { + setSBThumbX(svgContainerRect.width - scrollBarThumbWidth); + } + + return () => { + window.removeEventListener('mouseup', handleMouseUp); + window.removeEventListener('mousemove', handleMouseMove); + } + }, [scrollBarThumbWidth, svgContainerRect]); + + const handleMouseDown = (e: React.MouseEvent) => { + // If the thumb is clicked with left mouse button, we start dragging + if (thumbRef.current && e.button === 0) { + isDragging.current = true; + distThumbClick.current = e.clientX - thumbRef.current.getBoundingClientRect().left; + } + } + + return ( + <> + + + + + + + ); +} + +export default ScrollBarThumb; \ No newline at end of file diff --git a/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx b/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx index e791be4..eef6d79 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/launchDateCountDown.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState, useEffect } from "react"; -type LaunchDateCountDownProps = { +export type LaunchDateCountDownProps = { launchDate: string | Date | undefined; }; diff --git a/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx new file mode 100644 index 0000000..7fc4b8e --- /dev/null +++ b/frontend/src/app/satellites/[satelliteSlug]/orbitDataGraph.tsx @@ -0,0 +1,121 @@ +"use client"; + +import React, { useState, useLayoutEffect, useRef, SyntheticEvent } from 'react' +import { SatelliteNumber } from '@/lib/store' +import { Line } from 'react-chartjs-2' +import Chart from 'chart.js' +import { LaunchDateCountDownProps } from './launchDateCountDown'; +import ScrollBarThumb from './_orbitDataGraphComponents/ScrollBarThumb'; + +type OrbitDataProps = { + satNum : SatelliteNumber; + launchDateString: LaunchDateCountDownProps['launchDate']; + orbitalData: any; +} + +const OrbitDataGraph : React.FC = ({ satNum, launchDateString, orbitalData }) => { + + // href for the svg component, tracking the size of the container + const svgContainer = useRef(null); + const [svgSize, setSvgSize] = useState({width: 0, height: 0}); + {/* SB use for ScrollBar*/} + const [scrollBarThumbWidth, setSBThumbWidth] = useState(0); + + // Handling button for zooming in and out of the graph on a time scale + const launchDate = launchDateString? new Date(launchDateString) : new Date(); + const calculateMonthsDiff = () => { + const currentDate = new Date(); + return currentDate.getMonth() - launchDate.getMonth() + (12 * (currentDate.getFullYear() - launchDate.getFullYear())); + } + + const months = calculateMonthsDiff(); + + const handleZoomClick = (e: React.MouseEvent) => { + // We round the width of the scrollbar thumb at one decimal + const period = e.currentTarget.textContent; + const regExp = new RegExp('^([1234567890]+)([my])$'); + const match = period?.match(regExp); + let SBThumbWidth = 0; + + if (period === "All") { + SBThumbWidth = svgSize.width*0.8-40.5; + setSBThumbWidth(SBThumbWidth); + } else { + const number = match ? parseInt(match[1]) : 0; + const periodType = match? match[2] : ""; + + if (periodType === "m") { + SBThumbWidth = Math.round((svgSize.width*0.8 * number *10) / months) / 10; + setSBThumbWidth(SBThumbWidth); + } else if (periodType === "y") { + SBThumbWidth = Math.round((svgSize.width*0.8 * number * 12 *10) / months) / 10 + setSBThumbWidth(SBThumbWidth); + } + } + } + + // Layout effect to track the size of the container and update the svg size + useLayoutEffect(() => { + const updateSize = () => { + if (svgContainer.current) { + const width = svgContainer.current.offsetWidth; + const height = width / 2; + setSvgSize({width, height}); + setSBThumbWidth( width*0.1 ); + } + } + window.addEventListener('resize', updateSize); + updateSize(); + + return () => window.removeEventListener('resize', updateSize); + }, []); + + return ( + <> +

+ Graph goes here. +

+
+
+

Zoom :

+ {/* Scrollbar thumb represents the zoom period selected, in case it fits bad we don't display + ie. containerSize > SBThumbWidth > 20px */} + { (Math.round((svgSize.width*0.8 * 1 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 1 * 10) / months) / 10 > 20) && } + { (Math.round((svgSize.width*0.8 * 3 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 3 * 10) / months) / 10 > 40) && } + { (Math.round((svgSize.width*0.8 * 6 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 6 * 10) / months) / 10 > 40) && } + { (Math.round((svgSize.width*0.8 * 12 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 12 * 10) / months) / 10 > 40) && } + { (Math.round((svgSize.width*0.8 * 12 * 5 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 12 * 5 * 10) / months) / 10 > 40) && } + { (Math.round((svgSize.width*0.8 * 12 * 10 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 12 * 10 * 10) / months) / 10 > 40) && } + { (Math.round((svgSize.width*0.8 * 12 * 20 * 10) / months) / 10 < svgSize.width) && (Math.round((svgSize.width*0.8 * 12 * 20 * 10) / months) / 10 > 40) && } + + +
+
+ + {/* Scrollbar for time navigation */} + + + + + + {/* Scrollbar left navigation arrow */} + + + + + {/* Scrollbar right navigation arrow */} + + + + + {/* Scrollbar thumb */} + + + +
+
+ + ) +} + +export default OrbitDataGraph; \ No newline at end of file diff --git a/frontend/src/app/satellites/[satelliteSlug]/page.tsx b/frontend/src/app/satellites/[satelliteSlug]/page.tsx index 21857ed..8a6e10a 100644 --- a/frontend/src/app/satellites/[satelliteSlug]/page.tsx +++ b/frontend/src/app/satellites/[satelliteSlug]/page.tsx @@ -13,6 +13,7 @@ import Image from "next/image"; import { SatelliteNumber } from "@/lib/store"; import { graphql } from "@/lib/tada/graphql"; import { getClient } from "@/lib/ApolloClient"; +import OrbitDataGraph from "./orbitDataGraph"; export interface ProjectOrSatellite { id: string; @@ -154,6 +155,17 @@ export default async function SatelliteInfoPage({ + {/* Orbit data */} +
+ {/*Pass the satNum and the launchDate as props to OrbitDataGraph*/} + { noradId? ( + satAttributes?.launchDate ? ( + + ) : null + + ) : null} +
+ {/* Related projects */}
{relatedProjects?.length != 0 ? ( @@ -189,6 +201,7 @@ const GET_SATELLITE_INFO = graphql(` name massKg missionStatus + historicalOrbitalData satelliteImage { data { attributes { diff --git a/frontend/src/components/layout/Footer.tsx b/frontend/src/components/layout/Footer.tsx index 4d6f1fc..846f34a 100644 --- a/frontend/src/components/layout/Footer.tsx +++ b/frontend/src/components/layout/Footer.tsx @@ -6,6 +6,7 @@ import NTNULogo from "./NTNULogo"; * It includes the NTNU logo, social media links, and contact information. */ export default function Footer() { + const now = new Date(); return (
diff --git a/package.json b/package.json index d65ffd2..f4635ff 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "frontend": "cd frontend && npm run dev", "backend": "cd backend && npm run develop", - "dev": "concurrently \"npm run server\" \"npm run client\"" + "dev": "concurrently \"npm run backend\" \"npm run frontend\"" }, "author": "", "license": "", diff --git a/start_project.sh b/start_project.sh old mode 100644 new mode 100755