SoarView is a full-stack web application for glider pilots to upload, review and share flights they've recorded on a GPS. It is inspired on OLC. For more information on the world of soaring visit The Soaring Society of America.
- JavaScript
- Python
- HTML5
- CSS3
- SQL
- React
- Redux
- Node.js
- OpenLayers
- Chart.js
- WTForms
- Flask
- SQLAlchemy
- Alembic
- PostgreSQL
- Amazon Web Services S3
- Docker
- FontAwesome
- TailwindCSS
- Heroku
Below are instructions to run the application on a local development environment.
- Python v3.8
- PostgreSQL
- Pipenv
- Node.js
-
Clone this repository
git clone https://github.com/guipace/SoarView.git
-
Change directory
cd SoarView
-
Create python environment & install dependencies
pipenv install -r --dev dev-requirements.txt && pipenv install -r requirements.txt
-
Create your own environment variables files (
.env
) based on the provided examples (.env.example
) in the project's root directory and react-app directory. -
Create a user and database in your PostgreSQL that matches your environment variables configuration.
-
In a terminal activate the Pipenv environment
pipenv shell
-
Apply migrations to the database
flask db upgrade
-
Seed the database
flask seed all
-
In another terminal, change directories into the react-app directory
cd react-app
-
Install node modules
npm install
-
Run backend application in first terminal
flask run
-
Run the frontend application in second terminal
npm start
-
The application should open in your default browser.
Some of the challenges faced in the development of SoarView include the following:
- Understanding and parsing niche .IGC GPS files that are only used by soaring pilots. Researched and implemented little-known parsing library capable of handling IGC files. Manipulated parsed output into a format digestible by OpenLayers mapping and Chart.js charting libraries.
- Rendering of recorded GPS tracks on a map proved challenging. GPS track objects can contain upwards of ten thousand GPS fixes that need to be fed into the OpenLayers map. Implementing a solution that would render quickly took considerable effort and review of OpenLayers and React documentation.
-
Implementation of OpenLayers map with GPS track rendering and Charts.js graph for altitude profile
function MapWrapper({ features, igcParsedData }) { const [ map, setMap ] = useState(); const [ featuresLayer, setFeaturesLayer ] = useState(); const [ selectedCoord, setSelectedCoord ] = useState(); const mapElement = useRef(); // Create state ref that can be accessed in OpenLayers onclick callback function const mapRef = useRef() mapRef.current = map // Initialize map on first render useEffect(() => { // Create and add vector source layer const initialFeaturesLayer = new VectorLayer({ source: new VectorSource(), style: polygonStyle }) // Create map const initialMap = new Map({ target: mapElement.current, layers: [ // Bing Maps Satelite new TileLayer({ source: new BingMaps({ key: bingApiKey, imagerySet: 'AerialWithLabelsOnDemand', }), title: 'Satelite', type: 'base', }), // Bing Maps Roads new TileLayer({ source: new BingMaps({ key: bingApiKey, imagerySet: 'RoadOnDemand', }), title: 'Standard', type: 'base', }), // Bing Maps Dark new TileLayer({ source: new BingMaps({ key: bingApiKey, imagerySet: 'CanvasDark', }), title: 'Dark', type: 'base', }), initialFeaturesLayer, ], view: new View({ projection: 'EPSG:3857', center: [0, 0], zoom: 2 }), controls: defaults(), }); const layerSwitcher = new LayerSwitcher({ reverse: true, groupSelectStyle: 'group' }); initialMap.addControl(layerSwitcher); // Save map and vector layer references to state setMap(initialMap); setFeaturesLayer(initialFeaturesLayer); initialMap.on('click', handleMapClick) }, []); // Update map if features prop changes useEffect(() => { if (features.length) {// May be empty on first render // Set features to map featuresLayer.setSource( new VectorSource({ features: features // Make sure features is an array }) ); // Fit map to feature extent (with 50px of padding) map.getView().fit(featuresLayer.getSource().getExtent(), { padding: [50, 50, 50, 50] }); } }, [features, featuresLayer, map]); // Map click handler const handleMapClick = (event) => { // Get clicked coordinate using mapRef to access current React state inside OpenLayers callback const clickedCoord = mapRef.current.getCoordinateFromPixel(event.pixel); // Transform coord to EPSG 4326 standard Lat Long const transormedCoord = transform(clickedCoord, 'EPSG:3857', 'EPSG:4326') // Set React state setSelectedCoord( transormedCoord ) } // Graph options const options = { maintainAspectRatio: false, scales: { yAxes: [ { ticks: { beginAtZero: true, maxTicksLimit: 6, }, }, ], xAxes: [ { ticks: { beginAtZero: false, maxRotation: 0, maxTicksLimit: 10, }, type: 'time', time: { displayFormats: { minute: 'H:mm' }, }, }, ], }, legend: { display: false, labels: { fontColor: 'rgb(255, 99, 132)' } }, title: { display: true, text: 'Flight height profile', } } // Graph data let data; if (igcParsedData) { data = { // labels: igcParsedData.fixes.map(el => el.timestamp), datasets: [ { label: 'Height', data: igcParsedData.fixes.map(el => { let obj = { y:el.gpsAltitude, x:new Date(el.timestamp) } return obj }), fill: false, backgroundColor: 'rgb(255, 99, 132)', borderColor: 'rgba(236, 70, 70, 1)', borderWidth: 3, pointRadius: 0, }, ], } } return ( <div className='bg-background w-full h-full md:w-9/12 md:order-2'> <div className='map-container w-full h-5/6' ref={mapElement}></div> <div className='w-full px-2'> <Line className='' height={120} data={data} options={options} /> </div> </div> ) }