├── frontend
├── assets
│ └── plane2.gif
├── index.html
├── routes
│ ├── destination.jsx
│ ├── dataview.jsx
│ ├── signin.jsx
│ ├── signup.jsx
│ ├── icons
│ │ └── plane.svg
│ └── mapview.jsx
├── index.js
├── styles.css
├── components
│ ├── DestinationCard.jsx
│ └── Navbar.jsx
└── App.jsx
├── backend
├── models
│ └── db.js
├── controllers
│ ├── validation.js
│ ├── favorites.js
│ ├── data.js
│ └── users.js
├── server.js
└── routers
│ └── api.js
├── LICENSE
├── webpack.config.js
├── package.json
├── .gitignore
├── API.md
├── README.md
└── us-west-1-bundle.pem
/frontend/assets/plane2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SkyL-i-ght/Skylight/HEAD/frontend/assets/plane2.gif
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SkyLight
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/frontend/routes/destination.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 |
4 | export function Destination(props){
5 | return (
6 |
7 | {/* props.restaurants, props.history, props.culture, props.activities, etc */}
8 |
9 | )
10 | }
--------------------------------------------------------------------------------
/backend/models/db.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const { Pool } = require('pg');
3 | const fs = require('fs');
4 |
5 |
6 | const pool = new Pool({ // uses environment variables
7 | max: 1,
8 | ssl: {
9 | rejectUnauthorized: true,
10 | ca: fs.readFileSync('./us-west-1-bundle.pem').toString()
11 | }
12 | });
13 |
14 | module.exports = pool;
--------------------------------------------------------------------------------
/backend/controllers/validation.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const { ColorLensOutlined } = require('@material-ui/icons');
3 | const express = require('express');
4 |
5 |
6 | const validation = {};
7 |
8 |
9 | validation.validateCoordinates = (req, res, next) => {
10 |
11 | console.log(req.query);
12 | const isLatitude = num => isFinite(num) && Math.abs(num) <= 90;
13 | const isLongitude = num => isFinite(num) && Math.abs(num) <= 180;
14 |
15 | if (!isLatitude(req.query.lat) || !isLongitude(req.query.lng)) {
16 | const invalidInputErr = {
17 | log: 'The latitude and/or longitude are incorrect',
18 | status: 400,
19 | message: { err: 'The latitude and/or longitude are incorrect' }
20 | };
21 | return next(invalidInputErr);
22 | }
23 |
24 | return next();
25 | };
26 |
27 |
28 | module.exports = validation;
--------------------------------------------------------------------------------
/frontend/routes/dataview.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { styled } from '@mui/material/styles';
3 | import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
4 | import MuiAccordion from '@mui/material/Accordion';
5 | import MuiAccordionSummary from '@mui/material/AccordionSummary';
6 | import MuiAccordionDetails from '@mui/material/AccordionDetails';
7 | import Typography from '@mui/material/Typography';
8 | import DestinationCard from '../components/DestinationCard.jsx'
9 |
10 |
11 | export default function DataView(props){
12 | return (
13 |
14 |
15 | My destinations
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | )
24 | }
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const express = require('express');
3 | const { authenticate } = require('./controllers/users')
4 | const app = express();
5 | const cookieParser = require('cookie-parser');
6 | const PORT = process.env.PORT;
7 | const api = require('./routers/api');
8 |
9 | app.use(express.json());
10 | app.use(cookieParser());
11 | app.use(authenticate);
12 |
13 | app.use('/api', api);
14 |
15 | // 404 error handler
16 | app.use('*', (req, res) => res.sendStatus(404));
17 |
18 | // Global Error Handler
19 | app.use((err, req, res, next) => {
20 | console.log(err);
21 | const defaultErr = {
22 | log: 'Express error handler caught unknown middleware error',
23 | status: 500,
24 | message: { err: 'An error occurred' }
25 | };
26 | const errorObj = Object.assign({}, defaultErr, err);
27 | console.log(errorObj.log);
28 | return res.status(errorObj.status).json(errorObj.message);
29 | });
30 |
31 | app.listen(PORT, () => {
32 | console.log(`Listening on port: ${PORT}`);
33 | });
--------------------------------------------------------------------------------
/frontend/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './styles.css';
3 | import { render } from 'react-dom';
4 | import App from './App.jsx';
5 | import { BrowserRouter, Routes, Route, Switch } from "react-router-dom";
6 | import Mapview from "./routes/mapview.jsx";
7 | import Dataview from "./routes/dataview.jsx";
8 | import SignIn from"./routes/signin.jsx";
9 | import SignUp from"./routes/signup.jsx";
10 | import '@fontsource/roboto/300.css';
11 | import '@fontsource/roboto/400.css';
12 | import '@fontsource/roboto/500.css';
13 | import '@fontsource/roboto/700.css';
14 | import Navbar from './components/Navbar.jsx';
15 |
16 |
17 | render(
18 |
19 |
20 |
21 |
22 | } />
23 | } />
24 | } />
25 | } />
26 | } />
27 |
28 | ,
29 |
30 | document.getElementById('root')
31 | );
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 SkyL-i-ght
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const HtmlWebpackPlugin = require('html-webpack-plugin')
3 |
4 | module.exports = {
5 | mode: 'development',
6 | entry: path.join(__dirname, 'frontend', 'index.js'),
7 | output: {
8 | path: path.resolve(__dirname, 'dist'),
9 | },
10 | module: {
11 | rules: [
12 | {
13 | test: /\.jsx?$/,
14 | exclude: /node_modules/,
15 | use: {
16 | loader: 'babel-loader',
17 | options: {
18 | presets: ['@babel/preset-env', '@babel/preset-react']
19 | },
20 | },
21 | },
22 | {
23 | test: /\.css$/i,
24 | use: ["style-loader", "css-loader"],
25 | },
26 | {
27 | test: /\.(png|svg|jpe?g|gif)$/i,
28 | loader: 'file-loader',
29 | },
30 | ]
31 | },
32 |
33 | devServer: {
34 | static: {
35 | directory: path.resolve(__dirname, './build'),
36 | publicPath: path.resolve(__dirname, './build'),
37 | },
38 | port: 8080,
39 | proxy: {
40 | '/api': 'http://localhost:3000'
41 | }
42 | },
43 | plugins: [
44 | new HtmlWebpackPlugin({
45 | template: path.join(__dirname, 'frontend', 'index.html')
46 | })
47 | ],
48 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "dev": "nodemon --watch \"cross-env NODE_ENV=development webpack serve .\"",
4 | "build": "cross-env NODE_ENV=production webpack",
5 | "start": "cross-env NODE_ENV=production webpack serve"
6 | },
7 | "devDependencies": {
8 | "@babel/core": "^7.17.7",
9 | "@babel/preset-env": "^7.16.11",
10 | "@babel/preset-react": "^7.16.7",
11 | "babel-loader": "^8.2.3",
12 | "css-loader": "^6.7.1",
13 | "file-loader": "^6.2.0",
14 | "html-webpack-plugin": "^5.5.0",
15 | "style-loader": "^3.3.1",
16 | "webpack": "^5.70.0",
17 | "webpack-cli": "^4.9.2",
18 | "webpack-dev-server": "^4.7.4"
19 | },
20 | "dependencies": {
21 | "@emotion/react": "^11.8.2",
22 | "@emotion/styled": "^11.8.1",
23 | "@fontsource/roboto": "^4.5.3",
24 | "@material-ui/icons": "^4.11.2",
25 | "@mui/icons-material": "^5.5.1",
26 | "@mui/material": "^5.5.1",
27 | "@react-google-maps/api": "^2.7.0",
28 | "axios": "^0.26.1",
29 | "bcrypt": "^5.0.1",
30 | "cookie-parser": "^1.4.6",
31 | "cross-env": "^7.0.3",
32 | "dotenv": "^16.0.0",
33 | "express": "^4.17.3",
34 | "fs": "^0.0.1-security",
35 | "nodemon": "^2.0.15",
36 | "path": "^0.12.7",
37 | "pg": "^8.7.3",
38 | "react": "^17.0.2",
39 | "react-router": "^6.2.2",
40 | "react-router-dom": "^6.2.2",
41 | "uuid": "^8.3.2"
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/backend/routers/api.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const userController = require('../controllers/users');
3 | const express = require('express');
4 | const data = require('../controllers/data');
5 | const validation = require('../controllers/validation');
6 | const favoritesController = require('../controllers/favorites');
7 |
8 | const api = express.Router();
9 |
10 | api.get('/test', (req, res) => res.send('Success'));
11 |
12 | api.get('/flights', validation.validateCoordinates, data.getFlightsData, (req, res) => {
13 | res.json(res.locals.opensky);
14 | });
15 |
16 | api.get('/flightinfo/:callsign', data.getFlightDetails, (req, res) => {
17 | res.json(res.locals.flightDetails);
18 | });
19 |
20 | api.post('/user/signup', userController.signUp, (req, res) => {
21 | res.json({valid: true});
22 | });
23 |
24 | api.post('/user/login', userController.login, (req, res) => {
25 | res.send('Successfully logged in');
26 | });
27 |
28 | api.post('/user/logout', userController.logout, (req, res) => {
29 | res.send('Successfully logged out');
30 | });
31 |
32 | api.get('/favorites', favoritesController.getAll, (req, res) => {
33 | res.json(res.locals.rows);
34 | });
35 |
36 | api.post('/favorites/add', favoritesController.addOne, (req, res) => {
37 | res.json(res.locals);
38 | });
39 |
40 | api.delete('/favorites/delete', favoritesController.removeOne, (req, res) => {
41 | res.json('Successfully deleted');
42 | });
43 |
44 |
45 | module.exports = api;
--------------------------------------------------------------------------------
/frontend/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | margin: 0;
3 | padding: 0;
4 | }
5 |
6 | .myToolbar {
7 | width: 50px;
8 | height: 50px;
9 | }
10 |
11 | .mainContainer {
12 | display: flex;
13 | justify-content: center;
14 | flex-direction: column;
15 | align-items: center;
16 | margin-top: 100px;
17 | }
18 |
19 | .findPlanesButton {
20 | position: relative;
21 | bottom: 100px;
22 | right: 200px;
23 | text-transform: unset !important;
24 | }
25 |
26 | .plane {
27 | position: relative;
28 | top: 60px;
29 | right: 70px;
30 | width: 380px;
31 | }
32 |
33 | .banner {
34 | display: flex;
35 | margin-top: 130px;
36 | height: 800px;
37 | width: 100%;
38 | justify-content: center;
39 | }
40 |
41 | .signinbtn {
42 | text-transform: unset !important;
43 | color: black;
44 | }
45 |
46 | .dataviewbtn {
47 | text-transform: unset !important;
48 | color: black;
49 | margin-bottom: 500px;
50 | }
51 |
52 | .signupbtn {
53 | text-transform: unset !important;
54 | color: black;
55 | }
56 |
57 | .buttondiv {
58 | display: flex;
59 | flex-direction: column;
60 | padding: 10px;
61 | justify-content: space-between
62 | }
63 |
64 | .btnpadding {
65 | display: flex;
66 | flex-direction: column;
67 | padding-top: 10px;
68 | justify-content: space-between
69 | }
70 |
71 | .destCard {
72 | width: 1000px;
73 | }
74 |
75 | .accordian {
76 | margin-top: 100px;
77 | display: flex;
78 | flex-direction: column;
79 | justify-content: center;
80 | align-items: center
81 | }
82 |
83 | .myDests {
84 | margin-bottom: 60px;
85 | }
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
--------------------------------------------------------------------------------
/backend/controllers/favorites.js:
--------------------------------------------------------------------------------
1 | const pool = require('../models/db');
2 |
3 |
4 | const favoritesController = {};
5 |
6 |
7 | favoritesController.getAll = (req, res, next) => {
8 |
9 | if (!res.locals.userId) return next("User must be logged in to access this feature");
10 |
11 | const q = 'SELECT _id as id, name, description FROM favorites WHERE user_id = $1';
12 | const params = [res.locals.userId];
13 | pool.query(q, params)
14 | .then(result => {
15 | if (!result.rows.length) res.locals.rows = null;
16 | else res.locals.rows = result.rows;
17 | return next();
18 | })
19 | .catch(e => next(e));
20 |
21 |
22 | };
23 |
24 |
25 | favoritesController.addOne = (req, res, next) => {
26 |
27 | if (!res.locals.userId) return next("User must be logged in to access this feature");
28 |
29 | const q = 'INSERT INTO favorites (user_id, name, description) VALUES ($1, $2, $3) RETURNING _id';
30 | const params = [res.locals.userId, req.body.name, req.body.description];
31 | pool.query(q, params)
32 | .then(result => {
33 | if (!result.rows.length) res.locals.id = null;
34 | else res.locals.id = result.rows[0]._id;
35 | res.locals.userId = null;
36 | return next();
37 | })
38 | .catch(e => next(e));
39 |
40 | };
41 |
42 | favoritesController.removeOne = (req, res, next) => {
43 |
44 | if (!res.locals.userId) return next("User must be logged in to access this feature");
45 |
46 | const q = 'DELETE FROM favorites WHERE _id = $1 and user_id = $2';
47 | const params = [req.body.id, res.locals.userId];
48 | pool.query(q, params)
49 | .then(() => next())
50 | .catch(e => next(e));
51 |
52 | };
53 |
54 | module.exports = favoritesController;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | lerna-debug.log*
8 |
9 | # Diagnostic reports (https://nodejs.org/api/report.html)
10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
11 |
12 | # Runtime data
13 | pids
14 | *.pid
15 | *.seed
16 | *.pid.lock
17 |
18 | # Directory for instrumented libs generated by jscoverage/JSCover
19 | lib-cov
20 |
21 | # Coverage directory used by tools like istanbul
22 | coverage
23 | *.lcov
24 |
25 | # nyc test coverage
26 | .nyc_output
27 |
28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
29 | .grunt
30 |
31 | # Bower dependency directory (https://bower.io/)
32 | bower_components
33 |
34 | # node-waf configuration
35 | .lock-wscript
36 |
37 | # Compiled binary addons (https://nodejs.org/api/addons.html)
38 | build/Release
39 |
40 | # Dependency directories
41 | node_modules/
42 | jspm_packages/
43 | package-lock.json
44 |
45 | # TypeScript v1 declaration files
46 | typings/
47 |
48 | # TypeScript cache
49 | *.tsbuildinfo
50 |
51 | # Optional npm cache directory
52 | .npm
53 |
54 | # Optional eslint cache
55 | .eslintcache
56 |
57 | # Microbundle cache
58 | .rpt2_cache/
59 | .rts2_cache_cjs/
60 | .rts2_cache_es/
61 | .rts2_cache_umd/
62 |
63 | # Optional REPL history
64 | .node_repl_history
65 |
66 | # Output of 'npm pack'
67 | *.tgz
68 |
69 | # Yarn Integrity file
70 | .yarn-integrity
71 |
72 | # dotenv environment variables file
73 | .env
74 | .env.test
75 |
76 | # parcel-bundler cache (https://parceljs.org/)
77 | .cache
78 |
79 | # Next.js build output
80 | .next
81 |
82 | # Nuxt.js build / generate output
83 | .nuxt
84 | dist
85 |
86 | # Gatsby files
87 | .cache/
88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js
89 | # https://nextjs.org/blog/next-9-1#public-directory-support
90 | # public
91 |
92 | # vuepress build output
93 | .vuepress/dist
94 |
95 | # Serverless directories
96 | .serverless/
97 |
98 | # FuseBox cache
99 | .fusebox/
100 |
101 | # DynamoDB Local files
102 | .dynamodb/
103 |
104 | # TernJS port file
105 | .tern-port
106 |
--------------------------------------------------------------------------------
/backend/controllers/data.js:
--------------------------------------------------------------------------------
1 | require('dotenv').config();
2 | const axios = require('axios');
3 |
4 | const data = {};
5 |
6 | data.getFlightsData = (req, res, next) => {
7 | const lat = parseFloat(req.query.lat);
8 | const lng = parseFloat(req.query.lng);
9 |
10 | console.log(lat, lng);
11 |
12 | const params = {
13 | lamin: lat - 1,
14 | lamax: lat + 1,
15 | lomin: lng - 1,
16 | lomax: lng + 1
17 | };
18 |
19 | axios.get('https://opensky-network.org/api/states/all', { params })
20 | .then(response => {
21 |
22 | console.log(response);
23 | if (!response.data.states) {
24 | res.locals.opensky = [];
25 | return next();
26 | }
27 | res.locals.opensky = response.data.states.map(elem => {
28 | return {
29 | id: elem[0],
30 | callsign: elem[1].trim(),
31 | lastContact: elem[3] || elem[4],
32 | lng: elem[5],
33 | lat: elem[6],
34 | direction: elem[10],
35 | altitude: elem[13] || elem[7],
36 | speed: elem[9]
37 | };
38 | });
39 | return next();
40 | })
41 | .catch(e => {
42 | return next(e);
43 | })
44 |
45 | };
46 |
47 | data.getFlightDetails = (req, res, next) => {
48 |
49 | const date = new Date();
50 | const flight_date = `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2, 0)}-${String(date.getDate()).padStart(2, 0)}`
51 | const regex = /([\D]+)([\d]+)$/i;
52 | flight_icao = req.params.callsign.trim();
53 | airline_icao = req.params.callsign.match(regex)[1];
54 | flight_number = req.params.callsign.match(regex)[2];
55 |
56 | const params = {
57 | access_key: process.env.AVIATIONSTACK_KEY,
58 | flight_date,
59 | flight_icao,
60 | airline_icao,
61 | flight_number
62 | };
63 |
64 | console.log(params);
65 |
66 | const timeoutErr = {
67 | log: 'Request timed out',
68 | status: 500,
69 | message: { err: 'Request timed out' }
70 | };
71 |
72 | const wrapperPromise = new Promise((resolve, reject) => {
73 | setTimeout(() => reject(timeoutErr), 3000);
74 | axios.get('https://api.aviationstack.com/v1/flights', { params })
75 | .then(response => {
76 | res.locals.flightDetails = response.data.data[0];
77 | resolve('Success');
78 | })
79 | .catch(e => reject(e));
80 | });
81 |
82 | wrapperPromise
83 | .then(() => next())
84 | .catch(e => next(e));
85 |
86 | };
87 |
88 |
89 | module.exports = data;
--------------------------------------------------------------------------------
/frontend/components/DestinationCard.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { styled } from '@mui/material/styles';
3 | import ArrowForwardIosSharpIcon from '@mui/icons-material/ArrowForwardIosSharp';
4 | import MuiAccordion from '@mui/material/Accordion';
5 | import MuiAccordionSummary from '@mui/material/AccordionSummary';
6 | import MuiAccordionDetails from '@mui/material/AccordionDetails';
7 | import Typography from '@mui/material/Typography';
8 |
9 | const Accordion = styled((props) => (
10 |
11 | ))(({ theme }) => ({
12 | border: `1px solid ${theme.palette.divider}`,
13 | '&:not(:last-child)': {
14 | borderBottom: 0,
15 | },
16 | '&:before': {
17 | display: 'none',
18 | },
19 | }));
20 |
21 | const AccordionSummary = styled((props) => (
22 | }
24 | {...props}
25 | />
26 | ))(({ theme }) => ({
27 | backgroundColor:
28 | theme.palette.mode === 'dark'
29 | ? 'rgba(255, 255, 255, .05)'
30 | : 'rgba(0, 0, 0, .03)',
31 | flexDirection: 'row-reverse',
32 | '& .MuiAccordionSummary-expandIconWrapper.Mui-expanded': {
33 | transform: 'rotate(90deg)',
34 | },
35 | '& .MuiAccordionSummary-content': {
36 | marginLeft: theme.spacing(1),
37 | },
38 | }));
39 |
40 | const AccordionDetails = styled(MuiAccordionDetails)(({ theme }) => ({
41 | padding: theme.spacing(2),
42 | borderTop: '1px solid rgba(0, 0, 0, .125)',
43 | }));
44 |
45 | export default function DestinationCard() {
46 | const [expanded, setExpanded] = React.useState('panel1');
47 |
48 | const handleChange = (panel) => (event, newExpanded) => {
49 | setExpanded(newExpanded ? panel : false);
50 | };
51 |
52 | return (
53 |
54 |
57 |
58 |
59 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse
60 | malesuada lacus ex, sit amet blandit leo lobortis eget. Lorem ipsum dolor
61 | sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex,
62 | sit amet blandit leo lobortis eget.
63 |
64 |
65 |
66 | );
67 | }
--------------------------------------------------------------------------------
/frontend/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import { Outlet, Link } from 'react-router-dom';
3 | import { Button } from '@mui/material';
4 | import Plane from '../frontend/assets/plane2.gif';
5 |
6 |
7 | class App extends Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | coords: {
12 | lat: 33.91,
13 | lng: -118.42,
14 | },
15 |
16 | bindingBox: {
17 | lamin: 33.85,
18 | lamax: 33.96,
19 | lomin: -118.47,
20 | lomax: -118.37,
21 | },
22 | }
23 | }
24 |
25 | // get browser location
26 | // need to determine if we need to convert lat/long to string or not
27 | componentDidMount() {
28 | navigator.geolocation.getCurrentPosition(location => {
29 | const lat = Math.floor(location.coords.latitude * 100) / 100,
30 | lng = Math.floor(location.coords.longitude * 100) / 100,
31 | locInfo = {};
32 | locInfo.lat = lat;
33 | locInfo.lng = lng;
34 | const bindingBox = {
35 | lamin: lat - 0.05,
36 | lamax: lat + 0.05,
37 | lomin: lng - 0.05,
38 | lomax: lng + 0.05
39 | };
40 |
41 | // set default coordinates
42 | // Center: LA 33.9108174, -118.4288793
43 | // top left: 34.511582, -119.197922
44 | // top right: 34.473099, -117.552720
45 | // bottom left: 33.226470, -118.720551
46 | // bottom right: 33.244027, -117.440136
47 | // lat min 33.22
48 | // lat max 34.51
49 | // lng min -119.10
50 | // lng max -117.44
51 | //
52 |
53 | // logic that sets the default latitude or longitude if the user denies access to browser geolocation
54 | this.setState((state, props) => {
55 | return { coords: locInfo, bindingBox: bindingBox }
56 | })
57 | });
58 | }
59 |
60 | render() {
61 | return(
62 |
63 |
64 |
65 |
66 |

67 |
68 |
69 |
70 |
71 |
72 | {/*
MapView */}
73 |
74 |
75 | );
76 | }
77 | }
78 |
79 | export default App;
--------------------------------------------------------------------------------
/frontend/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import AppBar from '@mui/material/AppBar';
3 | import Toolbar from '@mui/material/Toolbar';
4 | import Typography from '@mui/material/Typography';
5 | import IconButton from '@mui/material/IconButton';
6 | import AccountCircle from '@mui/icons-material/AccountCircle';
7 | import Menu from '@mui/material/Menu';
8 | import { Link } from 'react-router-dom';
9 | import Button from '@mui/material/Button';
10 |
11 | export default function Navbar() {
12 | const [anchorEl, setAnchorEl] = React.useState(null);
13 |
14 | const handleMenu = (event) => {
15 | setAnchorEl(event.currentTarget);
16 | };
17 |
18 | const handleClose = () => {
19 | setAnchorEl(null);
20 | };
21 |
22 | return (
23 |
24 |
25 |
26 | SkyLight
27 |
28 |
29 |
36 |
37 |
38 |
74 |
75 |
76 |
77 |
78 |
79 | );
80 | }
--------------------------------------------------------------------------------
/backend/controllers/users.js:
--------------------------------------------------------------------------------
1 | const pool = require('../models/db');
2 | const bcrypt = require('bcrypt');
3 | const uuid = require('uuid');
4 |
5 | const SALT_ROUNDS = 12;
6 |
7 | const userController = {};
8 |
9 | userController.signUp = (req, res, next) => {
10 |
11 | const q = `INSERT INTO users (username, password) VALUES ($1, $2) RETURNING _id;`;
12 | bcrypt.hash(req.body.password, SALT_ROUNDS)
13 | .then(hash => pool.query(q, [req.body.username, hash]))
14 | .then(r => {
15 | res.locals.id = r.rows[0]._id;
16 | console.log(`User ${res.locals.id} created`);
17 | return next();
18 | })
19 | .catch(e => {
20 | /* Error handling if the username already exists */
21 | if (e.constraint === 'users_username_key') {
22 | // constraint is returned from pg command/error when same username is entered
23 | const err = {
24 | log: 'This username already exists.',
25 | status: 400,
26 | message: {err: 'This username already exists.'}
27 | }
28 | return next(err);
29 | }
30 | return next(e);
31 | });
32 |
33 | };
34 |
35 | userController.login = (req, res, next) => {
36 |
37 | const q = `SELECT _id, password FROM users WHERE username = $1;`;
38 | const err = {
39 | log: 'Incorrect username and/or password',
40 | status: 401,
41 | message: {err: 'Incorrect username and/or password'}
42 | };
43 | pool.query(q, [req.body.username])
44 | .then(r => {
45 | if (!r.rows.length) return next(err);
46 | res.locals.userId = r.rows[0]._id;
47 | return bcrypt.compare(req.body.password, r.rows[0].password);
48 | })
49 | .then(result => {
50 | if(!result) return next(err);
51 | const ssid = uuid.v4();
52 | const ssidString = `INSERT INTO sessions (user_id, ssid) VALUES ($1, $2) RETURNING ssid;`;
53 | const queryParams = [res.locals.userId, ssid];
54 | return pool.query(ssidString, queryParams);
55 | })
56 | .then(sessions => {
57 | res.cookie('ssid', sessions.rows[0].ssid, { maxAge: 1000 * 60 * 60 * 24 * 30, httpOnly: true });
58 | return next();
59 | })
60 | .catch(e => next(e));
61 | }
62 |
63 | userController.logout = (req, res, next) => {
64 | res.cookie('ssid', '', { maxAge: 1000 * 60 * 60 * 24 * 30, httpOnly: true });
65 | return next();
66 | };
67 |
68 | userController.authenticate = (req, res, next) => {
69 | const ssidQuery = `SELECT user_id FROM sessions WHERE ssid = $1`;
70 | const params = [req.cookies.ssid];
71 | pool.query(ssidQuery, params)
72 | .then(r => {
73 | if (!r.rows.length) res.locals.userId = null;
74 | else res.locals.userId = r.rows[0].user_id;
75 | console.log(`User ${res.locals.userId} authenticated`)
76 | return next();
77 | })
78 | .catch(e => {
79 | res.locals.userId = null;
80 | return next();
81 | });
82 | };
83 |
84 |
85 | module.exports = userController;
--------------------------------------------------------------------------------
/frontend/routes/signin.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Avatar from '@mui/material/Avatar';
3 | import Button from '@mui/material/Button';
4 | import CssBaseline from '@mui/material/CssBaseline';
5 | import TextField from '@mui/material/TextField';
6 | import Grid from '@mui/material/Grid';
7 | import Box from '@mui/material/Box';
8 | import Typography from '@mui/material/Typography';
9 | import Container from '@mui/material/Container';
10 | import { createTheme, ThemeProvider } from '@mui/material/styles';
11 | import Plane from '../assets/plane2.gif';
12 | import { Link } from 'react-router-dom';
13 |
14 |
15 |
16 | const theme = createTheme();
17 |
18 | export default function SignIn() {
19 | const handleSubmit = (event) => {
20 | event.preventDefault();
21 | const data = new FormData(event.currentTarget);
22 | fetch(`/api/user/login`,{
23 | method: 'POST',
24 | body: JSON.stringify({username: data.username, password: data.password})
25 | })
26 | };
27 |
28 | return (
29 |
30 |
31 |
32 |
40 |
43 |
44 |
45 | Sign in to SkyLight
46 |
47 |
48 |
57 |
66 |
67 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | );
87 | }
88 |
89 |
--------------------------------------------------------------------------------
/frontend/routes/signup.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Avatar from '@mui/material/Avatar';
3 | import Button from '@mui/material/Button';
4 | import CssBaseline from '@mui/material/CssBaseline';
5 | import TextField from '@mui/material/TextField';
6 | import Grid from '@mui/material/Grid';
7 | import Box from '@mui/material/Box';
8 | import Typography from '@mui/material/Typography';
9 | import Container from '@mui/material/Container';
10 | import { createTheme, ThemeProvider } from '@mui/material/styles';
11 | import Plane from '../assets/plane2.gif';
12 | import { Link } from 'react-router-dom';
13 |
14 | const theme = createTheme();
15 |
16 | export default function SignUp() {
17 | const handleSubmit = (event) => {
18 | event.preventDefault();
19 | const data = new FormData(event.currentTarget);
20 | fetch('/api/user/signup',
21 | {
22 | method: 'POST',
23 | body: JSON.stringify({username: data.username, password: data.password})
24 | })
25 | .then(res => {
26 | return res.json()
27 | })
28 | .then(res => {
29 | if(res.status === 200){
30 | res.redirect('/');
31 | } else{res.redirect('/')
32 | alert('username is already taken!');
33 | }
34 | })
35 | .catch(err => console.log(err));
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 |
50 |
53 |
54 |
55 |
56 | Sign up for SkyLight
57 |
58 |
59 |
60 |
61 |
68 |
69 |
70 |
78 |
79 |
80 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | }
--------------------------------------------------------------------------------
/frontend/routes/icons/plane.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
50 |
--------------------------------------------------------------------------------
/API.md:
--------------------------------------------------------------------------------
1 | # API Endpoints
2 |
3 | ## Summay of endpoints
4 |
5 | - `GET` to `/api/test` (returns `Success`)
6 | - `GET` to `/api/flights` (requires `lat` and `lng` params)
7 | - `GET` to `/api/flightinfo/:callsign` (requires `callsign`)
8 | - `POST` to `/api/user/signup` (requires `username` and `password` in request body)
9 | - `POST` to `/api/user/login` (requires `username` and `password` in request body)
10 | - `POST` to `/api/user/logout`
11 | - `GET` to `/api/favorites`
12 | - `POST` to `/api/favorites/add` (requires `name` and `description` in request body)
13 | - `DELETE` to `/api/favorites/delete` (requires `id` in request body)
14 |
15 |
16 | ## Flights endpoint
17 |
18 | Make a `GET` request to `/api/flights?lat=35&lng=-118` to get flight information.
19 | You need to pass in a `lat` key and a `lng` key in the parameters. They need to be valid latitude and longitude coordinates.
20 |
21 |
22 | A sample response would look like this:
23 | ```JSON
24 | [
25 | {
26 | "id": "a65408",
27 | "callsign": "AAL1333",
28 | "lastContact": 1647452171,
29 | "lng": -77.9382,
30 | "lat": 35.0045,
31 | "direction": 40.29,
32 | "altitude": 10911.84,
33 | "speed": 279.22
34 | },
35 | {
36 | "id": "a6eadd",
37 | "callsign": "N545DU",
38 | "lastContact": 1647452171,
39 | "lng": -78.8267,
40 | "lat": 35.707,
41 | "direction": 151.74,
42 | "altitude": 472.44,
43 | "speed": 54.32
44 | },
45 | ...
46 | ]
47 | ```
48 |
49 | ## Flight info endpoint
50 |
51 | Make a `GET` request to `/api/flightinfo/:callsign`.
52 |
53 | The callsign is expected to be in ICAO format. This sample response below is for a `GET` request to `/api/flightinfo/SWA2520`.
54 |
55 | A sample response would contain a single object like this:
56 |
57 | ```JSON
58 | {
59 | "flight_date": "2022-03-16",
60 | "flight_status": "active",
61 | "departure": {
62 | "airport": "San Diego International Airport",
63 | "timezone": "America/Los_Angeles",
64 | "iata": "SAN",
65 | "icao": "KSAN",
66 | "terminal": "1",
67 | "gate": "7",
68 | "delay": 14,
69 | "scheduled": "2022-03-16T10:10:00+00:00",
70 | "estimated": "2022-03-16T10:10:00+00:00",
71 | "actual": null,
72 | "estimated_runway": null,
73 | "actual_runway": null
74 | },
75 | "arrival": {
76 | "airport": "Kona International Airport",
77 | "timezone": "Pacific/Honolulu",
78 | "iata": "KOA",
79 | "icao": "PHKO",
80 | "terminal": null,
81 | "gate": "6",
82 | "baggage": null,
83 | "delay": null,
84 | "scheduled": "2022-03-16T13:35:00+00:00",
85 | "estimated": "2022-03-16T13:35:00+00:00",
86 | "actual": null,
87 | "estimated_runway": null,
88 | "actual_runway": null
89 | },
90 | "airline": {
91 | "name": "Southwest Airlines",
92 | "iata": "WN",
93 | "icao": "SWA"
94 | },
95 | "flight": {
96 | "number": "2520",
97 | "iata": "WN2520",
98 | "icao": "SWA2520",
99 | "codeshared": null
100 | },
101 | "aircraft": null,
102 | "live": null
103 | }
104 | ```
105 |
106 | ## Get user's destinations
107 |
108 | Make a `GET` request to `/api/favorites`.
109 |
110 | The response body will look like this:
111 |
112 | ```JSON
113 | [
114 | {
115 | "id": 1,
116 | "name": "Kona International Airport, Honolulu, USA",
117 | "description": "Beautiful city with magnificent views of the Pacific Ocean and delightful beaches."
118 | },
119 | ...
120 | ]
121 | ```
122 |
123 | ## Add to destinations endpoint
124 | Make a `POST` request to `/api/favorites/add`.
125 |
126 | The request body will look like this:
127 |
128 | ```JSON
129 | {
130 | "name": "Kona International Airport, Honolulu, USA",
131 | "description": "Beautiful city with magnificent views of the Pacific Ocean and delightful beaches."
132 | }
133 | ```
134 |
135 | If successful, the request will return the id for the new favorite. This id will be needed to delete it later.
136 |
137 | A sample response would look like:
138 |
139 | ```JSON
140 | {
141 | "id": 5
142 | }
143 | ```
144 |
145 | ## Remove from destinations endpoint
146 | Make a `DELETE` request to `/api/favorites/delete`.
147 |
148 | The favorite id will need to be specified in the request body as such:
149 | ```JSON
150 | {
151 | "id": 5
152 | }
153 | ```
154 |
155 | A sample response would simply contain a successful status code.
156 |
157 | ## User login endpoint
158 | Make a `POST` request to `/api/user/login`.
159 |
160 | A sample request body would look like:
161 |
162 | ```JSON
163 | {
164 | "username": "digitalnomad1",
165 | "password": "myverysafepassword"
166 | }
167 | ```
168 |
169 | If successful, a sample response would simply return a successful status code.
170 |
171 | ## User signup endpoint
172 | Make a `POST` request to `/api/user/signup`.
173 |
174 | A sample request would look like:
175 |
176 | ```JSON
177 | {
178 | "username": "digitalnomad1",
179 | "password": "myverysafepassword"
180 | }
181 | ```
182 | A sample response would look like:
183 |
184 | ```JSON
185 | {
186 | "id": 3
187 | }
188 | ```
189 |
190 | ## User logout
191 |
192 | Make a `POST` request to `/api/user/logout`.
193 |
194 | No request body or params necessary.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ✈️ Skylight
2 |
3 | **Real-time flight tracking application for monitoring overhead aircraft**
4 |
5 | [](https://opensource.org/licenses/MIT)
6 | [](https://nodejs.org/)
7 | [](https://reactjs.org/)
8 |
9 | ## 📋 Overview
10 |
11 | Skylight is a comprehensive web application that allows users to track and monitor aircraft flying overhead in real-time. With an intuitive interface built on Google Maps integration, users can visualize flight paths, access detailed flight information, and manage their favorite destinations for enhanced flight tracking experiences.
12 |
13 | ## ✨ Features
14 |
15 | ### 🛩️ Flight Tracking
16 | - **Real-time Flight Data**: View live aircraft positions, altitudes, speeds, and directions
17 | - **Interactive Map**: Google Maps integration for geographic visualization
18 | - **Flight Details**: Access comprehensive flight information including:
19 | - Departure and arrival airports
20 | - Flight status and schedules
21 | - Airline and aircraft information
22 | - Terminal and gate details
23 |
24 | ### 👤 User Management
25 | - **User Authentication**: Secure signup, login, and logout functionality
26 | - **Personal Accounts**: Individual user sessions with personalized data
27 |
28 | ### ⭐ Favorites System
29 | - **Destination Management**: Save and organize favorite airports and destinations
30 | - **Custom Descriptions**: Add personal notes and descriptions to saved locations
31 | - **Easy Access**: Quick retrieval of frequently tracked destinations
32 |
33 | ## 🛠️ Technology Stack
34 |
35 | ### Frontend
36 | - **React 17.0.2** - Modern UI framework
37 | - **Material-UI (MUI) 5.5.1** - Professional component library
38 | - **Google Maps API** - Interactive mapping and geolocation
39 | - **React Router 6.2.2** - Client-side routing
40 | - **Emotion** - CSS-in-JS styling
41 |
42 | ### Backend
43 | - **Node.js** - Server runtime environment
44 | - **Express.js 4.17.3** - Web application framework
45 | - **PostgreSQL** - Relational database
46 | - **bcrypt** - Password hashing and security
47 | - **Cookie Parser** - Session management
48 |
49 | ### Development Tools
50 | - **Webpack 5** - Module bundling and build system
51 | - **Babel** - JavaScript transpilation
52 | - **Nodemon** - Development server auto-restart
53 | - **ESLint** - Code linting and formatting
54 |
55 | ## 🚀 Quick Start
56 |
57 | ### Prerequisites
58 | - Node.js (v16 or higher)
59 | - PostgreSQL database
60 | - Google Maps API key
61 |
62 | ### Installation
63 |
64 | 1. **Clone the repository**
65 | ```bash
66 | git clone https://github.com/yourusername/skylight.git
67 | cd skylight
68 | ```
69 |
70 | 2. **Install dependencies**
71 | ```bash
72 | npm install
73 | ```
74 |
75 | 3. **Environment Setup**
76 | Create a `.env` file in the root directory:
77 | ```env
78 | NODE_ENV=development
79 | DATABASE_URL=postgresql://username:password@localhost:5432/skylight
80 | GOOGLE_MAPS_API_KEY=your_google_maps_api_key
81 | SESSION_SECRET=your_session_secret
82 | ```
83 |
84 | 4. **Database Setup**
85 | ```bash
86 | # Create and configure your PostgreSQL database
87 | # Run any necessary migrations (refer to backend/models/)
88 | ```
89 |
90 | 5. **Start the Development Server**
91 | ```bash
92 | npm run dev
93 | ```
94 |
95 | 6. **Access the Application**
96 | Open your browser and navigate to `http://localhost:8080`
97 |
98 | ## 📖 Usage
99 |
100 | ### Getting Started
101 | 1. **Create an Account**: Sign up with a username and password
102 | 2. **Login**: Access your personal dashboard
103 | 3. **Explore Flights**: Use the map interface to view real-time flight data
104 | 4. **Add Favorites**: Save interesting destinations for quick access
105 | 5. **Track Details**: Click on aircraft markers for detailed flight information
106 |
107 | ### API Usage
108 | Skylight provides a comprehensive REST API for flight data and user management. For detailed API documentation, see [API.md](./API.md).
109 |
110 | Key endpoints include:
111 | - `GET /api/flights` - Retrieve flight data by coordinates
112 | - `GET /api/flightinfo/:callsign` - Get detailed flight information
113 | - `POST /api/user/signup` - User registration
114 | - `GET /api/favorites` - Manage favorite destinations
115 |
116 | ## 🏗️ Project Structure
117 |
118 | ```
119 | skylight/
120 | ├── frontend/ # React application
121 | │ ├── components/ # Reusable UI components
122 | │ ├── routes/ # Application routing
123 | │ ├── assets/ # Static assets
124 | │ └── styles.css # Global styles
125 | ├── backend/ # Express server
126 | │ ├── controllers/ # Request handlers
127 | │ ├── models/ # Database models
128 | │ ├── routers/ # API route definitions
129 | │ └── server.js # Application entry point
130 | ├── API.md # Comprehensive API documentation
131 | └── README.md # Project documentation
132 | ```
133 |
134 | ## 🔧 Development
135 |
136 | ### Available Scripts
137 | - `npm run dev` - Start development server with hot reload
138 | - `npm run build` - Build production bundle
139 | - `npm start` - Start production server
140 |
141 | ### Contributing
142 | 1. Fork the repository
143 | 2. Create a feature branch (`git checkout -b feature/amazing-feature`)
144 | 3. Commit your changes (`git commit -m 'Add amazing feature'`)
145 | 4. Push to the branch (`git push origin feature/amazing-feature`)
146 | 5. Open a Pull Request
147 |
148 | ## 📝 API Documentation
149 |
150 | For comprehensive API documentation including request/response examples, authentication details, and endpoint specifications, please refer to [API.md](./API.md).
151 |
152 | ## 🤝 Contributing
153 |
154 | We welcome contributions to Skylight! Please feel free to submit issues, feature requests, or pull requests. For major changes, please open an issue first to discuss what you would like to change.
155 |
156 | ## 📄 License
157 |
158 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
159 |
160 | ## 🙏 Acknowledgments
161 |
162 | - Flight data provided by aviation APIs
163 | - Google Maps for mapping and geolocation services
164 | - Material-UI for the component library
165 | - The open-source community for various tools and libraries
166 |
167 | ## 📞 Support
168 |
169 | If you encounter any issues or have questions, please:
170 | 1. Check the [API documentation](./API.md)
171 | 2. Search existing [GitHub issues](https://github.com/yourusername/skylight/issues)
172 | 3. Create a new issue with detailed information
173 |
174 | ---
175 |
176 | **Built with ❤️ for aviation enthusiasts and developers**
177 |
--------------------------------------------------------------------------------
/us-west-1-bundle.pem:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIEBjCCAu6gAwIBAgIJAMc0ZzaSUK51MA0GCSqGSIb3DQEBCwUAMIGPMQswCQYD
3 | VQQGEwJVUzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEi
4 | MCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1h
5 | em9uIFJEUzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkw
6 | ODIyMTcwODUwWhcNMjQwODIyMTcwODUwWjCBjzELMAkGA1UEBhMCVVMxEDAOBgNV
7 | BAcMB1NlYXR0bGUxEzARBgNVBAgMCldhc2hpbmd0b24xIjAgBgNVBAoMGUFtYXpv
8 | biBXZWIgU2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxIDAeBgNV
9 | BAMMF0FtYXpvbiBSRFMgUm9vdCAyMDE5IENBMIIBIjANBgkqhkiG9w0BAQEFAAOC
10 | AQ8AMIIBCgKCAQEArXnF/E6/Qh+ku3hQTSKPMhQQlCpoWvnIthzX6MK3p5a0eXKZ
11 | oWIjYcNNG6UwJjp4fUXl6glp53Jobn+tWNX88dNH2n8DVbppSwScVE2LpuL+94vY
12 | 0EYE/XxN7svKea8YvlrqkUBKyxLxTjh+U/KrGOaHxz9v0l6ZNlDbuaZw3qIWdD/I
13 | 6aNbGeRUVtpM6P+bWIoxVl/caQylQS6CEYUk+CpVyJSkopwJlzXT07tMoDL5WgX9
14 | O08KVgDNz9qP/IGtAcRduRcNioH3E9v981QO1zt/Gpb2f8NqAjUUCUZzOnij6mx9
15 | McZ+9cWX88CRzR0vQODWuZscgI08NvM69Fn2SQIDAQABo2MwYTAOBgNVHQ8BAf8E
16 | BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUc19g2LzLA5j0Kxc0LjZa
17 | pmD/vB8wHwYDVR0jBBgwFoAUc19g2LzLA5j0Kxc0LjZapmD/vB8wDQYJKoZIhvcN
18 | AQELBQADggEBAHAG7WTmyjzPRIM85rVj+fWHsLIvqpw6DObIjMWokpliCeMINZFV
19 | ynfgBKsf1ExwbvJNzYFXW6dihnguDG9VMPpi2up/ctQTN8tm9nDKOy08uNZoofMc
20 | NUZxKCEkVKZv+IL4oHoeayt8egtv3ujJM6V14AstMQ6SwvwvA93EP/Ug2e4WAXHu
21 | cbI1NAbUgVDqp+DRdfvZkgYKryjTWd/0+1fS8X1bBZVWzl7eirNVnHbSH2ZDpNuY
22 | 0SBd8dj5F6ld3t58ydZbrTHze7JJOd8ijySAp4/kiu9UfZWuTPABzDa/DSdz9Dk/
23 | zPW4CXXvhLmE02TA9/HeCw3KEHIwicNuEfw=
24 | -----END CERTIFICATE-----
25 | -----BEGIN CERTIFICATE-----
26 | MIIECDCCAvCgAwIBAgIDAIkHMA0GCSqGSIb3DQEBCwUAMIGPMQswCQYDVQQGEwJV
27 | UzEQMA4GA1UEBwwHU2VhdHRsZTETMBEGA1UECAwKV2FzaGluZ3RvbjEiMCAGA1UE
28 | CgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEGA1UECwwKQW1hem9uIFJE
29 | UzEgMB4GA1UEAwwXQW1hem9uIFJEUyBSb290IDIwMTkgQ0EwHhcNMTkwOTA2MTc0
30 | MDIxWhcNMjQwODIyMTcwODUwWjCBlDELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldh
31 | c2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxIjAgBgNVBAoMGUFtYXpvbiBXZWIg
32 | U2VydmljZXMsIEluYy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxJTAjBgNVBAMMHEFt
33 | YXpvbiBSRFMgdXMtd2VzdC0xIDIwMTkgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IB
34 | DwAwggEKAoIBAQDD2yzbbAl77OofTghDMEf624OvU0eS9O+lsdO0QlbfUfWa1Kd6
35 | 0WkgjkLZGfSRxEHMCnrv4UPBSK/Qwn6FTjkDLgemhqBtAnplN4VsoDL+BkRX4Wwq
36 | /dSQJE2b+0hm9w9UMVGFDEq1TMotGGTD2B71eh9HEKzKhGzqiNeGsiX4VV+LJzdH
37 | uM23eGisNqmd4iJV0zcAZ+Gbh2zK6fqTOCvXtm7Idccv8vZZnyk1FiWl3NR4WAgK
38 | AkvWTIoFU3Mt7dIXKKClVmvssG8WHCkd3Xcb4FHy/G756UZcq67gMMTX/9fOFM/v
39 | l5C0+CHl33Yig1vIDZd+fXV1KZD84dEJfEvHAgMBAAGjZjBkMA4GA1UdDwEB/wQE
40 | AwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBR+ap20kO/6A7pPxo3+
41 | T3CfqZpQWjAfBgNVHSMEGDAWgBRzX2DYvMsDmPQrFzQuNlqmYP+8HzANBgkqhkiG
42 | 9w0BAQsFAAOCAQEAHCJky2tPjPttlDM/RIqExupBkNrnSYnOK4kr9xJ3sl8UF2DA
43 | PAnYsjXp3rfcjN/k/FVOhxwzi3cXJF/2Tjj39Bm/OEfYTOJDNYtBwB0VVH4ffa/6
44 | tZl87jaIkrxJcreeeHqYMnIxeN0b/kliyA+a5L2Yb0VPjt9INq34QDc1v74FNZ17
45 | 4z8nr1nzg4xsOWu0Dbjo966lm4nOYIGBRGOKEkHZRZ4mEiMgr3YLkv8gSmeitx57
46 | Z6dVemNtUic/LVo5Iqw4n3TBS0iF2C1Q1xT/s3h+0SXZlfOWttzSluDvoMv5PvCd
47 | pFjNn+aXLAALoihL1MJSsxydtsLjOBro5eK0Vw==
48 | -----END CERTIFICATE-----
49 | -----BEGIN CERTIFICATE-----
50 | MIIF/zCCA+egAwIBAgIRAOLV6zZcL4IV2xmEneN1GwswDQYJKoZIhvcNAQEMBQAw
51 | gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ
52 | bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn
53 | QW1hem9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBSU0E0MDk2IEcxMRAwDgYDVQQH
54 | DAdTZWF0dGxlMCAXDTIxMDUxOTE5MDg1OFoYDzIxMjEwNTE5MjAwODU4WjCBlzEL
55 | MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x
56 | EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6
57 | b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNVBAcMB1Nl
58 | YXR0bGUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC7koAKGXXlLixN
59 | fVjhuqvz0WxDeTQfhthPK60ekRpftkfE5QtnYGzeovaUAiS58MYVzqnnTACDwcJs
60 | IGTFE6Wd7sB6r8eI/3CwI1pyJfxepubiQNVAQG0zJETOVkoYKe/5KnteKtnEER3X
61 | tCBRdV/rfbxEDG9ZAsYfMl6zzhEWKF88G6xhs2+VZpDqwJNNALvQuzmTx8BNbl5W
62 | RUWGq9CQ9GK9GPF570YPCuURW7kl35skofudE9bhURNz51pNoNtk2Z3aEeRx3ouT
63 | ifFJlzh+xGJRHqBG7nt5NhX8xbg+vw4xHCeq1aAe6aVFJ3Uf9E2HzLB4SfIT9bRp
64 | P7c9c0ySGt+3n+KLSHFf/iQ3E4nft75JdPjeSt0dnyChi1sEKDi0tnWGiXaIg+J+
65 | r1ZtcHiyYpCB7l29QYMAdD0TjfDwwPayLmq//c20cPmnSzw271VwqjUT0jYdrNAm
66 | gV+JfW9t4ixtE3xF2jaUh/NzL3bAmN5v8+9k/aqPXlU1BgE3uPwMCjrfn7V0I7I1
67 | WLpHyd9jF3U/Ysci6H6i8YKgaPiOfySimQiDu1idmPld659qerutUSemQWmPD3bE
68 | dcjZolmzS9U0Ujq/jDF1YayN3G3xvry1qWkTci0qMRMu2dZu30Herugh9vsdTYkf
69 | 00EqngPbqtIVLDrDjEQLqPcb8QvWFQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/
70 | MB0GA1UdDgQWBBQBqg8Za/L0YMHURGExHfvPyfLbOTAOBgNVHQ8BAf8EBAMCAYYw
71 | DQYJKoZIhvcNAQEMBQADggIBACAGPMa1QL7P/FIO7jEtMelJ0hQlQepKnGtbKz4r
72 | Xq1bUX1jnLvnAieR9KZmeQVuKi3g3CDU6b0mDgygS+FL1KDDcGRCSPh238Ou8KcG
73 | HIxtt3CMwMHMa9gmdcMlR5fJF9vhR0C56KM2zvyelUY51B/HJqHwGvWuexryXUKa
74 | wq1/iK2/d9mNeOcjDvEIj0RCMI8dFQCJv3PRCTC36XS36Tzr6F47TcTw1c3mgKcs
75 | xpcwt7ezrXMUunzHS4qWAA5OGdzhYlcv+P5GW7iAA7TDNrBF+3W4a/6s9v2nQAnX
76 | UvXd9ul0ob71377UhZbJ6SOMY56+I9cJOOfF5QvaL83Sz29Ij1EKYw/s8TYdVqAq
77 | +dCyQZBkMSnDFLVe3J1KH2SUSfm3O98jdPORQrUlORQVYCHPls19l2F6lCmU7ICK
78 | hRt8EVSpXm4sAIA7zcnR2nU00UH8YmMQLnx5ok9YGhuh3Ehk6QlTQLJux6LYLskd
79 | 9YHOLGW/t6knVtV78DgPqDeEx/Wu/5A8R0q7HunpWxr8LCPBK6hksZnOoUhhb8IP
80 | vl46Ve5Tv/FlkyYr1RTVjETmg7lb16a8J0At14iLtpZWmwmuv4agss/1iBVMXfFk
81 | +ZGtx5vytWU5XJmsfKA51KLsMQnhrLxb3X3zC+JRCyJoyc8++F3YEcRi2pkRYE3q
82 | Hing
83 | -----END CERTIFICATE-----
84 | -----BEGIN CERTIFICATE-----
85 | MIID/jCCAuagAwIBAgIQGyUVTaVjYJvWhroVEiHPpDANBgkqhkiG9w0BAQsFADCB
86 | lzELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu
87 | Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdB
88 | bWF6b24gUkRTIHVzLXdlc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcM
89 | B1NlYXR0bGUwIBcNMjEwNTE5MTkwNDA2WhgPMjA2MTA1MTkyMDA0MDZaMIGXMQsw
90 | CQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjET
91 | MBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMDAuBgNVBAMMJ0FtYXpv
92 | biBSRFMgdXMtd2VzdC0xIFJvb3QgQ0EgUlNBMjA0OCBHMTEQMA4GA1UEBwwHU2Vh
93 | dHRsZTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANhyXpJ0t4nigRDZ
94 | EwNtFOem1rM1k8k5XmziHKDvDk831p7QsX9ZOxl/BT59Pu/P+6W6SvasIyKls1sW
95 | FJIjFF+6xRQcpoE5L5evMgN/JXahpKGeQJPOX9UEXVW5B8yi+/dyUitFT7YK5LZA
96 | MqWBN/LtHVPa8UmE88RCDLiKkqiv229tmwZtWT7nlMTTCqiAHMFcryZHx0pf9VPh
97 | x/iPV8p2gBJnuPwcz7z1kRKNmJ8/cWaY+9w4q7AYlAMaq/rzEqDaN2XXevdpsYAK
98 | TMMj2kji4x1oZO50+VPNfBl5ZgJc92qz1ocF95SAwMfOUsP8AIRZkf0CILJYlgzk
99 | /6u6qZECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm5jfcS9o
100 | +LwL517HpB6hG+PmpBswDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4IB
101 | AQAcQ6lsqxi63MtpGk9XK8mCxGRLCad51+MF6gcNz6i6PAqhPOoKCoFqdj4cEQTF
102 | F8dCfa3pvfJhxV6RIh+t5FCk/y6bWT8Ls/fYKVo6FhHj57bcemWsw/Z0XnROdVfK
103 | Yqbc7zvjCPmwPHEqYBhjU34NcY4UF9yPmlLOL8uO1JKXa3CAR0htIoW4Pbmo6sA4
104 | 6P0co/clW+3zzsQ92yUCjYmRNeSbdXbPfz3K/RtFfZ8jMtriRGuO7KNxp8MqrUho
105 | HK8O0mlSUxGXBZMNicfo7qY8FD21GIPH9w5fp5oiAl7lqFzt3E3sCLD3IiVJmxbf
106 | fUwpGd1XZBBSdIxysRLM6j48
107 | -----END CERTIFICATE-----
108 | -----BEGIN CERTIFICATE-----
109 | MIICrjCCAjSgAwIBAgIRAMkvdFnVDb0mWWFiXqnKH68wCgYIKoZIzj0EAwMwgZYx
110 | CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu
111 | MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h
112 | em9uIFJEUyB1cy13ZXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl
113 | YXR0bGUwIBcNMjEwNTE5MTkxMzI0WhgPMjEyMTA1MTkyMDEzMjRaMIGWMQswCQYD
114 | VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG
115 | A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS
116 | RFMgdXMtd2VzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl
117 | MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEy86DB+9th/0A5VcWqMSWDxIUblWTt/R0
118 | ao6Z2l3vf2YDF2wt1A2NIOGpfQ5+WAOJO/IQmnV9LhYo+kacB8sOnXdQa6biZZkR
119 | IyouUfikVQAKWEJnh1Cuo5YMM4E2sUt5o0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G
120 | A1UdDgQWBBQ8u3OnecANmG8OoT7KLWDuFzZwBTAOBgNVHQ8BAf8EBAMCAYYwCgYI
121 | KoZIzj0EAwMDaAAwZQIwQ817qkb7mWJFnieRAN+m9W3E0FLVKaV3zC5aYJUk2fcZ
122 | TaUx3oLp3jPLGvY5+wgeAjEA6wAicAki4ZiDfxvAIuYiIe1OS/7H5RA++R8BH6qG
123 | iRzUBM/FItFpnkus7u/eTkvo
124 | -----END CERTIFICATE-----
125 |
--------------------------------------------------------------------------------
/frontend/routes/mapview.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { GoogleMap, useJsApiLoader } from '@react-google-maps/api';
3 | import { useLocation } from 'react-router-dom';
4 |
5 | const icon = {
6 | path: `M1629.054,730.768c0,0,4.477,1.853,8.182-0.463c2.007-3.551,56.7-65.065,59.593-63.607l-12.379-8.913c-1.5-1.08-1.493-3.315,0.015-4.385c12.237-8.688,11.906-8.709,13.77-8.994l30.861-4.725c0,0,16.674-18.681,39.368-12.66c0,0,45.544,5.249,82.75,71.48c0,0,19.761,32.112,4.477,51.565c0,0-4.547,7.027-8.063,10.597c-2.386,2.423-1.322,4.087-3.209,6.589c-1.96,2.599-6.465,8.302-10.61,11.586c-1.075,0.851-2.53,1.079-3.823,0.561c0,0-59.361,58.975-76.807,66.54c0,0-10.189,5.712-11.116,9.881l-0.474,2.133c-0.758,3.412-5.849,3.11-7.902,2.638c-2.841-0.653-2.375-2.417-3.999-3.431c-0.748-0.467-1.658-0.6-2.532-0.481c-24.519,3.352-24.726,4.02-27.167,1.794c-1.897-1.73-1.723-3.668-1.771-5.535c0,0-6.484,6.073-18.32,3.499l224.167,526.248c0,0,19.761,27.995,18.115,80.898s0.823,111.363-4.94,127.213s-16.674-14.203-16.674-14.203s-20.379-64.43-26.143-86.662c-6.476-24.977-84.504-136.362-83.162-137.094L1672.9,1166.134c0,0-5.558,4.94-9.366,5.352c-3.808,0.412-5.661,0-3.294-5.558c2.367-5.558,6.175-9.675,6.175-9.675l-56.093-87.382c0,0-10.087,6.69-12.557,7.102c-2.47,0.412-7.307,1.235-4.117-5.146c3.191-6.381,9.984-13.792,9.984-13.792s-66.489-103.129-71.943-108.481c-5.455-5.352-8.543-1.956-8.543-1.956s-6.793,5.146-8.337,7.719c-1.544,2.573-4.426-0.823-4.426-0.823s-23.364-22.54-19.658-32.524c3.705-9.984,7.205-11.424,4.117-18.32c-12.265-27.393-20.013-32.529-50.227-76.575c-8.193,5.791-19.29,8.022-14.409-1.544c2.573-5.043,6.587-9.366,6.587-9.366l-42.713-56.196c0,0-12.454,8.44-17.6,8.646s-8.337-2.779-5.249-7.308c3.088-4.529,12.042-14.203,12.042-14.203s-28.199-32.143-42.468-45.843c-3.897-3.742-9.951-4.041-14.179-0.677c-42.397,33.74-271.207,215.961-284.853,229.621c-14.951,14.966-163.478,116.28-170.853,120.215c-5.909,3.152-18.732,46.933-2.367,173.22l32.421,239.915c9.372,36.52,7.086,40.779,9.269,52.894l5.387,29.909c1.429,7.932-1.693,15.997-8.089,20.901l-63.69,48.829c-6.918,3.385-13.814,3.306-20.688-0.309c-27.598-19.094-36.619-15.807-52.8-14.512c-5.602,0.875-7.638-3.072-7.102-10.498c0,0-3.191-0.926-1.956-2.47c1.235-1.544,4.837-3.808,4.734-8.44c-0.103-4.632-4.549-35.448-6.278-43.537c-3.857-18.042-67.835-189.474-75.134-213.978c-2.882-9.675-21.202-59.696-22.232-65.253s-12.763-39.111-17.497-45.904c-4.734-6.793-12.763-16.879-20.996-18.32c0,0-30.465,12.968-33.759,13.174c-3.294,0.206-5.558-4.117-5.558-4.117s-51.256-45.698-54.138-51.873c-2.882-6.175,3.705-6.587,6.381-15.439c2.676-8.851,10.086-36.023-18.938-47.962C246.601,974.053,155.641,932.851,123.284,929.616l1.853-2.882c-8.955-0.101-19.042,0.871-29.824,2.502c-2.01,0.304-3.918-1.042-4.26-3.047c-2.097-12.278-6.494-24.358-14.917-32.814c-13.624-13.678-31.832-26.985-53.087-40.046c-2.277-1.399-2.609-4.578-0.663-6.41l48.267-45.436c3.634-3.421,8.625-5.009,13.568-4.315l100.551,14.098c14.266,2.224,26.212-1.041,45.617,7.296c5.187,2.228,10.579,3.941,16.099,5.121c59.695,12.758,367.676,78.384,396.662,80.614c32.112,2.47,71.017,3.705,121.964-21.923c35.813-18.015,75.858-45.812,81.495-52.433c1.354-1.591,2.662-3.276,4.382-4.462c31.727-21.879,215.695-183.6,216.695-184.463c8.995-7.762,17.057-16.535,24.022-26.161c15.231-21.051,38.887-56.321,38.991-72.437c0.154-23.775-17.754-45.389-85.066-88.154c-31.96,19.533-31.942,4.712-14.667-10.035l-69.937-40.603c-4.993,3.812-10.097,6.799-15.535,7.277c-1.213,0.107-2.243-0.893-2.211-2.111c0.131-5.081,2.412-9.029,6.476-12.037c-61.193-33.484-85.552-48.889-100.273-46.624c-6.113,0.941-10.056,9.11-24.702,1.389c-6.158-3.246-23.168-15.445-23.168-15.445c-0.693-0.462-0.753-1.457-0.122-2l8.624-7.411c0,0,2.779-2.239-0.386-5.095s-23.312-15.439-34.351-20.147c-11.039-4.709-84.835-41.607-84.835-41.607c-13.643,8.258-15.197,10.025-18.537,8.41c-1.61-0.779-1.949-2.943-0.78-4.297l8.279-9.593c0,0-87.228-42.456-91.242-44.695c-4.115,3.549-8.626,5.925-13.614,6.891c-3.173,0.615-3.096-6.487,4.428-11.909L284.822,62.28c0,0-9.881-6.33-52.028-13.432s-61.445-13.586-63.761-16.21s-1.235-4.477,1.544-5.249c2.779-0.772,103.284-10.961,125.515-9.726c49.246,2.736-44.712-25.546,620.32,183.873c0,0,27.583,10.087,35.2,4.94s12.042-9.263,12.042-9.263s-3.396-6.69,10.704-23.261s60.21-58.255,60.21-58.255l-12.295-8.588c-0.845-0.591-0.877-1.831-0.063-2.464c6.694-5.2,9.985-8.452,16.994-9.781c8.144-1.544,17.064-2.239,26.446-2.427c0,0,12.454-12.454,21.305-13.689c0,0,18.115-7.205,42.302,11.322s41.684,40.655,47.962,56.505c6.278,15.85,17.703,41.581,2.985,57.225s-59.078,57.637-73.899,57.637c111.869,31.56,174.787,50.081,189.276,50.638c12.042,0.463,61.658,14.313,100.813,2.625c41.375-12.351,87.536-34.582,97.572-42.456l117.024-101.74c66.02-60.768,143.998-110.162,191.273-114.025c10.286-0.84,35.782-13.362,81.834-5.624c13.862,2.329,22.081,9.129,21.459,20.533c-5.332,97.768-133.507,207.419-227.306,289.884l-135.447,121.861c8.234,74.517,46.727,159.737,87.073,248.663`,
7 | scale: 0.02,
8 | strokeWeight: 0,
9 | fillOpacity: 1,
10 | fillColor: '#000000'
11 | };
12 |
13 | function MapView (props) {
14 |
15 | const location = useLocation();
16 | const center = { ...location.state.coords };
17 |
18 | const containerStyle = {
19 | width: '900px',
20 | height: '900px',
21 | };
22 |
23 | const { isLoaded } = useJsApiLoader({
24 | id: 'google-map-script',
25 | googleMapsApiKey: 'AIzaSyCMKXzIAJHi26-2IKJUDM9RO_p58CAdTeM',
26 | });
27 |
28 | const [map, setMap] = React.useState(null);
29 | const markers = [];
30 |
31 | const onLoad = React.useCallback((map) => {
32 | console.log(center);
33 | const bounds = new window.google.maps.LatLngBounds({ lat: center.lat-0.05, lng: center.lng-0.05 });
34 | bounds.extend({ lat: center.lat+0.05, lng: center.lng+0.05 });
35 | map.fitBounds(bounds);
36 | setMap(map);
37 |
38 |
39 | function genPlanes () {
40 | while (markers.length) {
41 | const val = markers.pop();
42 | val.setMap(null);
43 | };
44 |
45 | fetch(`/api/flights?lat=${center.lat}&lng=${center.lng}`)
46 | .then(res => {
47 | return res.json()
48 | })
49 | .then(respon => {
50 | for (let i = 0; i < respon.length; i++) {
51 | const mark = new google.maps.Marker({
52 | position: {lat:respon[i].lat, lng:respon[i].lng},
53 | icon: {...icon, rotation: -50 + respon[i].direction},
54 | map: map,
55 | });
56 | markers.push(mark);
57 |
58 | mark.addListener('click', () => {
59 | fetch(`/api/flightinfo/${respon[i].callsign}`)
60 | .then(res => res.json())
61 | .then(res => {
62 | console.log(res);
63 | const contentstring = `
64 |
65 |
Callsign: ${respon[i].callsign}
66 |
Departed From: ${res.departure ? res.departure.airport : "unknown" }
67 |
Destination: ${res.arrival ? res.arrival.airport : 'unknown '}
68 |
Arrival Timezone: ${res.arrival ? res.arrival.timezone : 'unknown' }
69 |
Altitude: ${respon[i].altitude}
70 |
71 |
72 | `
73 | const infoWindow = new window.google.maps.InfoWindow({
74 | content: contentstring
75 | });
76 | infoWindow.open({
77 | anchor: mark,
78 | map,
79 | shouldFocus: false,
80 | })
81 | })
82 | });
83 |
84 | };
85 | })
86 | }
87 |
88 | genPlanes();
89 | setInterval((() => genPlanes()), 15000);
90 |
91 | }, []);
92 |
93 | const onUnmount = React.useCallback((map) => {
94 | setMap(null);
95 | }, []);
96 |
97 | return isLoaded ? (
98 |
99 |
100 |
107 | <>>
108 |
109 |
110 |
111 |
112 | ) : <>>
113 | };
114 |
115 | export default MapView;
--------------------------------------------------------------------------------