├── 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 | 55 | Spain 56 | 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 | 53 |
54 | 62 |
63 | 71 |
72 |
73 |
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 | 6 | 49 | 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 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 6 | [![Node.js](https://img.shields.io/badge/Node.js-v16+-green.svg)](https://nodejs.org/) 7 | [![React](https://img.shields.io/badge/React-17.0.2-blue.svg)](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; --------------------------------------------------------------------------------