diff --git a/AR_README.md b/AR_README.md new file mode 100644 index 0000000..ae0f902 --- /dev/null +++ b/AR_README.md @@ -0,0 +1,166 @@ +# Earth AR Visualization + +This project adds Apple ARKit support to the existing Three.js Earth visualization using the WebXR Device API. + +## Features + +- **AR Earth Placement**: Place a 3D Earth globe in your real environment +- **Flight Route Visualization**: View your flight routes in augmented reality +- **Touch Interaction**: Tap to place the Earth and interact with it +- **Cross-Platform**: Works on iOS devices with ARKit support + +## How It Works + +### WebXR Device API +The AR functionality uses the **WebXR Device API**, which is the web standard for AR/VR experiences. This allows the Three.js visualization to work with: + +- **Apple ARKit** (iOS devices) +- **Google ARCore** (Android devices) +- **WebXR-compatible browsers** + +### Key Components + +1. **AR Session Management**: Handles the AR session lifecycle +2. **Hit Testing**: Detects surfaces for Earth placement +3. **Reticle System**: Visual indicator for placement location +4. **Simplified Earth Model**: Optimized for AR performance + +## Browser Support + +### iOS (Apple ARKit) +- **Safari**: Full support with ARKit +- **Chrome for iOS**: Limited support +- **Firefox for iOS**: Limited support + +### Android (ARCore) +- **Chrome**: Full support with ARCore +- **Firefox**: Limited support + +## Usage + +### Basic AR Experience + +1. Open `earth-ar.html` on a supported device +2. Allow camera permissions when prompted +3. Point your camera at a flat surface +4. Tap the screen to place the Earth +5. Explore your flight routes in AR space + +### Integration with Existing Earth + +To integrate AR with your existing Earth visualization: + +```javascript +// In your existing earth.js file +import { EarthAR } from './earth-ar.js'; + +// After initializing your existing earth +const earthAR = new EarthAR(); +await earthAR.init(); + +// Integrate with existing earth mesh +earthAR.integrateWithExistingEarth(existingEarthMesh); +``` + +## Technical Implementation + +### AR Session Setup +```javascript +const session = await navigator.xr.requestSession('immersive-ar', { + requiredFeatures: ['hit-test'], + optionalFeatures: ['dom-overlay'] +}); +``` + +### Hit Testing +```javascript +const hitTestSource = await session.requestHitTestSource({ + space: referenceSpace +}); +``` + +### Rendering Loop +```javascript +renderer.setAnimationLoop((timestamp, frame) => { + // AR rendering logic + renderer.render(scene, camera); +}); +``` + +## Performance Considerations + +### AR Optimizations +- **Reduced Geometry**: Simplified Earth model for AR +- **Efficient Materials**: Basic materials instead of complex textures +- **Frame Rate**: Maintains 60fps for smooth AR experience +- **Memory Management**: Proper cleanup of AR resources + +### Device Requirements +- **iOS 11+** with ARKit support +- **Modern browser** with WebXR support +- **A9 processor or newer** for optimal performance + +## Troubleshooting + +### Common Issues + +1. **"AR Not Supported" Error** + - Check if device supports ARKit/ARCore + - Ensure browser supports WebXR + - Try Safari on iOS for best compatibility + +2. **Camera Permission Denied** + - Allow camera access in browser settings + - Refresh page and try again + +3. **Poor Performance** + - Close other AR apps + - Ensure good lighting conditions + - Restart browser if needed + +### Debug Mode +Enable console logging for debugging: +```javascript +// In earth-ar.js +console.log('AR Session State:', this.isARSession); +console.log('Hit Test Results:', hitTestResults); +``` + +## Future Enhancements + +### Planned Features +- **Gesture Controls**: Pinch to zoom, rotate Earth +- **Multiple Earths**: Place multiple globes +- **Flight Path Animation**: Animated flight routes +- **Voice Commands**: Voice-controlled interactions +- **Social Sharing**: Share AR experiences + +### Advanced Integration +- **Real-time Data**: Live flight data in AR +- **Weather Overlay**: Real weather on Earth +- **Time Zones**: Dynamic day/night cycles +- **Custom Markers**: Personal location pins + +## Development + +### Local Development +1. Serve files over HTTPS (required for WebXR) +2. Use a local server: `python -m http.server 8000` +3. Access via `https://localhost:8000/earth-ar.html` + +### Testing +- Test on physical iOS device (simulator doesn't support AR) +- Use Safari for best iOS compatibility +- Test various lighting conditions +- Verify performance on different devices + +## Resources + +- [WebXR Device API Documentation](https://developer.mozilla.org/en-US/docs/Web/API/WebXR_Device_API) +- [Three.js WebXR Examples](https://threejs.org/docs/#examples/en/webxr/AR_handling_and_displaying_a_model) +- [Apple ARKit Documentation](https://developer.apple.com/augmented-reality/) +- [WebXR Polyfill](https://github.com/immersive-web/webxr-polyfill) for broader compatibility + +## License + +This AR implementation is part of the existing project and follows the same licensing terms. \ No newline at end of file diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..7e98ee0 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +patrickfreyer.com \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..37f0f21 --- /dev/null +++ b/README.md @@ -0,0 +1,55 @@ +# Patrick Freyer's Personal Website + +This is my personal website hosted on GitHub Pages. It showcases my portfolio, projects, and professional information. + +## Features + +- Modern, responsive design +- Smooth scrolling and animations +- Mobile-friendly navigation +- Project showcase section +- Contact information + +## Technologies Used + +- HTML5 +- CSS3 (with CSS Variables and Flexbox/Grid) +- JavaScript (ES6+) +- Font Awesome for icons +- Google Fonts (Inter) + +## Development + +To run this project locally: + +1. Clone the repository: +```bash +git clone https://github.com/patrickfreyer/patrickfreyer.github.io.git +``` + +2. Navigate to the project directory: +```bash +cd patrickfreyer.github.io +``` + +3. Open `index.html` in your browser or use a local server. + +## Deployment + +This website is automatically deployed to GitHub Pages when changes are pushed to the main branch. + +## Customization + +To customize the website: + +1. Update the content in `index.html` +2. Modify styles in `css/style.css` +3. Add any additional JavaScript functionality in `js/main.js` + +## License + +MIT License - feel free to use this code for your own personal website! + +## Contact + +Feel free to reach out to me for any questions or suggestions. \ No newline at end of file diff --git a/README.pdf b/README.pdf new file mode 100644 index 0000000..baf32c4 Binary files /dev/null and b/README.pdf differ diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..49d7fc7 --- /dev/null +++ b/_config.yml @@ -0,0 +1,38 @@ +title: Patrick Freyer +description: Personal website of Patrick Freyer, Software Developer & Technology Enthusiast +url: https://patrickfreyer.github.io +baseurl: "" + +# Build settings +markdown: kramdown +plugins: + - jekyll-feed + +# Include processing +include: + - _includes + - _layouts + - _data + - assets + - css + +# Exclude from processing +exclude: + - README.md + - LICENSE + - .gitignore + - Gemfile + - Gemfile.lock + - node_modules + - vendor + +# Sass +sass: + style: compressed + +# Default front matter +defaults: + - scope: + path: "" + values: + layout: default \ No newline at end of file diff --git a/_data/about.yaml b/_data/about.yaml new file mode 100644 index 0000000..117f88e --- /dev/null +++ b/_data/about.yaml @@ -0,0 +1,2 @@ +about_text: >- + GenAI builder & developer, passionate about building the products of the future and reshaping strategies to keep up with the new age of AI. Built and published a range of successful apps, ranging from podcasts to health and forestry. Always working on one more thing... \ No newline at end of file diff --git a/_data/flightRoutes.yaml b/_data/flightRoutes.yaml new file mode 100644 index 0000000..108bf8d --- /dev/null +++ b/_data/flightRoutes.yaml @@ -0,0 +1,3282 @@ +### 2000 + +- origin: "Frankfurt" + destination: "Santo Domingo" + airline: "XXX" + year: 2000 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Santo Domingo" + destination: "Frankfurt" + airline: "XXX" + year: 2000 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +### 2002 + +- origin: "Frankfurt" + destination: "Lisbon" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Lisbon" + destination: "Frankfurt" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Frankfurt" + destination: "Mombasa" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Mombasa" + destination: "Masai Mara" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Masai Mara" + destination: "Mombasa" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Mombasa" + destination: "Frankfurt" + airline: "XXX" + year: 2002 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +### 2004 + +- origin: "Frankfurt" + destination: "Cape Town" + airline: "XXX" + year: 2004 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Cape Town" + destination: "Frankfurt" + airline: "XXX" + year: 2004 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Frankfurt" + destination: "Mauritius" + airline: "XXX" + year: 2004 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +- origin: "Mauritius" + destination: "Frankfurt" + airline: "XXX" + year: 2004 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Regina", "Harald"] + +### 2005 + +### 2006 + +- origin: "Frankfurt" + destination: "Cairo" + airline: "XXX" + year: 2006 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +- origin: "Cairo" + destination: "Frankfurt" + airline: "XXX" + year: 2006 + month: "XXX" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +### 2007 + +- origin: "Frankfurt" + destination: "Reykjavik" + airline: "XXX" + year: 2007 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Reykjavik" + destination: "Frankfurt" + airline: "XXX" + year: 2007 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +### 2008 + +- origin: "Stuttgart" + destination: "London" + airline: "British Airways" + year: 2008 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred"] + +- origin: "London" + destination: "Cape Town" + airline: "British Airways" + year: 2008 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred"] + +### 2009 + +- origin: "Cape Town" + destination: "London" + airline: "British Airways" + year: 2009 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred", "Zita"] + +- origin: "London" + destination: "Stuttgart" + airline: "British Airways" + year: 2009 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Harald", "Pascal", "Regina", "Maria", "Manfred", "Zita"] + + +### 2013 +- origin: "Stuttgart" + destination: "Zurich" + airline: "Lufthansa" + year: 2013 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +- origin: "Zurich" + destination: "New York" + airline: "Lufthansa" + year: 2013 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +- origin: "New York" + destination: "Zurich" + airline: "Lufthansa" + year: 2013 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +- origin: "Zurich" + destination: "Stuttgart" + airline: "Lufthansa" + year: 2013 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Harald"] + +- origin: "Frankfurt" + destination: "Beijing" + airline: "Air China" + year: 2013 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Beijing" + destination: "Xi'an" + airline: "Air China" + year: 2013 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Shanghai" + destination: "Beijing" + airline: "Air China" + year: 2013 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Beijing" + destination: "Frankfurt" + airline: "Air China" + year: 2013 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + + +### 2014 +- origin: "Frankfurt" + destination: "Los Angeles" + airline: "Lufthansa" + year: 2014 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Los Angeles" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2014 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +### 2015 + +- origin: "Brussels" + destination: "Valeta" + airline: "XXX" + year: 2015 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Valeta" + destination: "Brussels" + airline: "XXX" + year: 2015 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Frankfurt" + destination: "Cancun" + airline: "Condor" + year: 2015 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Cancun" + destination: "Frankfurt" + airline: "Condor" + year: 2015 + month: "July" + +### 2016 +- origin: "Frankfurt" + destination: "Singapore" + airline: "Lufthansa" + year: 2016 + month: "January" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Singapore" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2016 + month: "February" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Brussels" + destination: "Moscow" + airline: "Aeroflot" + year: 2016 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "St. Petersburg" + destination: "Berlin" + airline: "Aeroflot" + year: 2016 + month: "April" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Berlin" + destination: "Brussels" + airline: "Aeroflot" + year: 2016 + month: "April" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Brussels" + destination: "Venice" + airline: "Ryanair" + year: 2016 + month: "May" + occasion: "School" + travelers: ["Patrick"] + +- origin: "Venice" + destination: "Brussels" + airline: "Ryanair" + year: 2016 + month: "May" + occasion: "School" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Miami" + airline: "XXX" + year: 2016 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Miami" + destination: "Belize" + airline: "American Airlines" + year: 2016 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Belize" + destination: "Miami" + airline: "American Airlines" + year: 2016 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Miami" + destination: "Frankfurt" + airline: "XXX" + year: 2016 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +### 2017 + +- origin: "Brussels" + destination: "Budapest" + airline: "XXX" + year: 2017 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Budapest" + destination: "Brussels" + airline: "XXX" + year: 2017 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Frankfurt" + destination: "Rome" + airline: "XXX" + year: 2017 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Zita"] + +- origin: "Rome" + destination: "Frankfurt" + airline: "XXX" + year: 2017 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Zita"] + +- origin: "Frankfurt" + destination: "Singapore" + airline: "Lufthansa" + year: 2017 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Singapore" + destination: "Kota Kinabalu" + airline: "AirAsia" + year: 2017 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Kota Kinabalu" + destination: "Singapore" + airline: "AirAsia" + year: 2017 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Singapore" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2017 + month: "July" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Brussels" + destination: "Hamburg" + airline: "XXX" + year: 2017 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Hamburg" + destination: "Brussels" + airline: "XXX" + year: 2017 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Brussels" + destination: "Venice" + airline: "XXX" + year: 2017 + month: "November" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Venice" + destination: "Brussels" + airline: "XXX" + year: 2017 + month: "November" + occasion: "Private" + travelers: ["Patrick"] + +### 2018 + +- origin: "Brussels" + destination: "New York" + airline: "Brussels Airlines" + year: 2018 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Julia"] + +- origin: "New York" + destination: "Brussels" + airline: "Brussels Airlines" + year: 2018 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Julia"] + +- origin: "Brussels" + destination: "Dubai" + airline: "Emirates" + year: 2018 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Dubai" + destination: "Durban" + airline: "Emirates" + year: 2018 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Durban" + destination: "Dubai" + airline: "Emirates" + year: 2018 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Dubai" + destination: "Brussels" + airline: "Emirates" + year: 2018 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Brussels" + destination: "Berlin" + airline: "Brussels Airlines" + year: 2018 + month: "May" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Berlin" + destination: "Brussels" + airline: "Brussels Airlines" + year: 2018 + month: "May" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Helsinki" + airline: "Finnair" + year: 2018 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sophia"] + +- origin: "Helsinki" + destination: "New Delhi" + airline: "Finnair" + year: 2018 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sophia"] + +- origin: "Jaipur" + destination: "Mumbai" + airline: "xxx" + year: 2018 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sophia"] + +- origin: "Bangalore" + destination: "New Delhi" + airline: "xxx" + year: 2018 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sophia"] + +- origin: "New Delhi" + destination: "Frankfurt" + airline: "Air India" + year: 2018 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sophia"] + +- origin: "Birmingham" + destination: "Stuttgart" + airline: "FlyBe" + year: 2018 + month: "December" + occasion: "Warwick" + travelers: ["Patrick"] + +### 2019 + +- origin: "Stuttgart" + destination: "Birmingham" + airline: "FlyBe" + year: 2019 + month: "January" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Birmingham" + destination: "Stuttgart" + airline: "FlyBe" + year: 2019 + month: "February" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Stuttgart" + destination: "Birmingham" + airline: "FlyBe" + year: 2019 + month: "February" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Birmingham" + destination: "Stuttgart" + airline: "FlyBe" + year: 2019 + month: "June" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Doha" + airline: "Qatar" + year: 2019 + month: "June" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Doha" + destination: "Hong Kong" + airline: "Qatar" + year: 2019 + month: "June" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Hong Kong" + destination: "Bangkok" + airline: "Ethiopian" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Bangkok" + destination: "Addis Ababa" + airline: "Ethiopian" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Addis Ababa" + destination: "Mombasa" + airline: "Ethiopian" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Mombasa" + destination: "Nairobi" + airline: "XXX" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Nairobi" + destination: "Masai Mara" + airline: "XXX" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Masai Mara" + destination: "Nairobi" + airline: "XXX" + year: 2019 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Nairobi" + destination: "Dubai" + airline: "Emirates" + year: 2019 + month: "September" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Frankfurt" + airline: "Emirates" + year: 2019 + month: "September" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Stuttgart" + destination: "Birmingham" + airline: "FlyBe" + year: 2019 + month: "September" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Birmingham" + destination: "Stuttgart" + airline: "FlyBe" + year: 2019 + month: "December" + occasion: "Warwick" + travelers: ["Patrick"] + +### 2020 + +- origin: "Stuttgart" + destination: "Birmingham" + airline: "FlyBe" + year: 2020 + month: "January" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Birmingham" + destination: "Dublin" + airline: "AerLingus" + year: 2020 + month: "February" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Dublin" + destination: "New York" + airline: "AerLingus" + year: 2020 + month: "February" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Dublin" + airline: "AerLingus" + year: 2020 + month: "February" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Dublin" + destination: "Birmingham" + airline: "AerLingus" + year: 2020 + month: "February" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Birmingham" + destination: "Amsterdam" + airline: "KLM" + year: 2020 + month: "March" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Amsterdam" + destination: "Stuttgart" + airline: "KLM" + year: 2020 + month: "March" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "London" + airline: "Lufthansa" + year: 2020 + month: "September" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "London" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2020 + month: "December" + occasion: "Warwick" + travelers: ["Patrick"] + +### 2021 + +- origin: "Frankfurt" + destination: "London" + airline: "Lufthansa" + year: 2021 + month: "February" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "London" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2021 + month: "June" + occasion: "Warwick" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Athens" + airline: "Lufthansa" + year: 2021 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Athens" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2021 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Frankfurt" + destination: "New York" + airline: "Lufthansa" + year: 2021 + month: "August" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Aruba" + airline: "United" + year: 2021 + month: "October" + occasion: "Private" + travelers: ["Patrick", "Fabio", "Rutvik"] + +- origin: "Aruba" + destination: "New York" + airline: "United" + year: 2021 + month: "October" + occasion: "Private" + travelers: ["Patrick", "Fabio", "Rutvik"] + +- origin: "New York" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2021 + month: "December" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Nursultan" + airline: "Lufthansa" + year: 2021 + month: "December" + occasion: "Private" + travelers: ["Patrick"] + +### 2022 + +- origin: "Nursultan" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2022 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Frankfurt" + destination: "New York" + airline: "Singapore Airlines" + year: 2022 + month: "January" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Panama" + airline: "Copa Airlines" + year: 2022 + month: "March" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Panama" + destination: "Montevideo" + airline: "Copa Airlines" + year: 2022 + month: "March" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Montevideo" + destination: "Panama" + airline: "Copa Airlines" + year: 2022 + month: "March" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Panama" + destination: "New York" + airline: "Copa Airlines" + year: 2022 + month: "March" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Frankfurt" + airline: "Singapore Airlines" + year: 2022 + month: "June" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Stockholm" + airline: "Lufthansa" + year: 2022 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Stockholm" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Copenhagen" + airline: "Lufthansa" + year: 2022 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Copenhagen" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Copenhagen" + airline: "Lufthansa" + year: 2022 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Copenhagen" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Copenhagen" + airline: "Lufthansa" + year: 2022 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Copenhagen" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Berlin" + airline: "Lufthansa" + year: 2022 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "San Francisco" + airline: "Condor" + year: 2022 + month: "September" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Oakland" + destination: "Big Island" + airline: "Southwest" + year: 2022 + month: "September" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Big Island" + destination: "Honolulu" + airline: "Southwest" + year: 2022 + month: "September" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Honolulu" + destination: "Oakland" + airline: "Southwest" + year: 2022 + month: "September" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "San Francisco" + destination: "Frankfurt" + airline: "Condor" + year: 2022 + month: "September" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Eurowings" + year: 2022 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "London" + airline: "Eurowings" + year: 2022 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "London" + destination: "Cologne" + airline: "Eurowings" + year: 2022 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Cologne" + destination: "Munich" + airline: "Eurowings" + year: 2022 + month: "October" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2022 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2022 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Doha" + airline: "Qatar" + year: 2022 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Doha" + destination: "Muscat" + airline: "Qatar" + year: 2022 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +### 2023 + +- origin: "Muscat" + destination: "Doha" + airline: "Qatar" + year: 2023 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Doha" + destination: "Frankfurt" + airline: "Qatar" + year: 2023 + month: "January" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2023 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Vienna" + airline: "Lufthansa" + year: 2023 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Vienna" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "FlyDubai" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Saudi" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Emirates" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Kuwait" + airline: "Emirates" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Kuwait" + destination: "Riyadh" + airline: "Saudi" + year: 2023 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dammam" + airline: "Saudi" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dammam" + destination: "Riyadh" + airline: "Saudi" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Washington DC" + airline: "United" + year: 2023 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Washington DC" + destination: "Munich" + airline: "United" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Doha" + airline: "Qatar" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Doha" + destination: "Abu Dhabi" + airline: "Qatar" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Abu Dhabi" + destination: "Riyadh" + airline: "Etihad" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Dubai" + airline: "Lufthansa" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Doha" + destination: "Riyadh" + airline: "Qatar" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Tabuk" + airline: "Saudia" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Tabuk" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Zurich" + airline: "Swiss" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Zurich" + destination: "Munich" + airline: "Swiss" + year: 2023 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Arar" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Arar" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Tabuk" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Tabuk" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Zurich" + airline: "Swiss" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Zurich" + destination: "Munich" + airline: "Swiss" + year: 2023 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Zurich" + airline: "Swiss" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Zurich" + destination: "Dubai" + airline: "Swiss" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Abu Dhabi" + destination: "Riyadh" + airline: "Etihad" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Riyadh" + airline: "Lufthansa" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Muscat" + airline: "Omanair" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Muscat" + destination: "Dubai" + airline: "Omanair" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Dammam" + airline: "Saudia" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dammam" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Budapest" + airline: "Lufthansa" + year: 2023 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Budapest" + destination: "Vienna" + airline: "Austrian" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Vienna" + destination: "Munich" + airline: "Austrian" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Dublin" + airline: "Air Lingus" + year: 2023 + month: "August" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Dublin" + destination: "Munich" + airline: "Air Lingus" + year: 2023 + month: "August" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Zurich" + airline: "Swiss" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Zurich" + destination: "Dubai" + airline: "Swiss" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Brussels" + airline: "Lufthansa" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Brussels" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Zurich" + airline: "Swiss" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Zurich" + destination: "Dubai" + airline: "Swiss" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Saudia" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Emirates" + year: 2023 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "FlyDubai" + year: 2023 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2023 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Vienna" + airline: "Lufthansa" + year: 2023 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Vienna" + destination: "Berlin" + airline: "Lufthansa" + year: 2023 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Berlin" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Berlin" + airline: "Lufthansa" + year: 2023 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Berlin" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Doha" + airline: "Qatar" + year: 2023 + month: "November" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Doha" + destination: "Hanoi" + airline: "Qatar" + year: 2023 + month: "November" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Hanoi" + destination: "Da Nang" + airline: "Vietjet" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Da Nang" + destination: "Ho Chi Minh" + airline: "Bamboo Airways" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Ho Chi Minh" + destination: "Phu Quoc" + airline: "Vietjet" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Phu Quoc" + destination: "Hanoi" + airline: "Vietjet" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Hanoi" + destination: "Doha" + airline: "Qatar" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Doha" + destination: "Munich" + airline: "Qatar" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "Hamburg" + airline: "Lufthansa" + year: 2023 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Hamburg" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "London" + airline: "Lufthansa" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "London" + destination: "Munich" + airline: "Lufthansa" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Doha" + airline: "Qatar" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Doha" + destination: "Almaty" + airline: "Qatar" + year: 2023 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +### 2024 + +- origin: "Almaty" + destination: "Doha" + airline: "Qatar" + year: 2024 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Doha" + destination: "Dubai" + airline: "Qatar" + year: 2024 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Munich" + airline: "Emirates" + year: 2024 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Dubai" + airline: "Emirates" + year: 2024 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Riyadh" + airline: "Saudia" + year: 2024 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Riyadh" + destination: "Dubai" + airline: "Saudia" + year: 2024 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Munich" + airline: "Emirates" + year: 2024 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Dubai" + airline: "Emirates" + year: 2024 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Munich" + airline: "Emirates" + year: 2024 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Dubai" + airline: "Emirates" + year: 2024 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dubai" + destination: "Munich" + airline: "Emirates" + year: 2024 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2024 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Seattle" + airline: "Lufthansa" + year: 2024 + month: "March" + occasion: "Projects" + travelers: ["Patrick", "Sofya"] + +- origin: "Seattle" + destination: "Seattle" + airline: "Patrick" + year: 2024 + month: "March" + occasion: "Pilots License" + travelers: ["Patrick", "Sofya"] + +- origin: "Seattle" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2024 + month: "April" + occasion: "Projects" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "London" + airline: "Lufthansa" + year: 2024 + month: "May" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "London" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "May" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Düsseldorf" + airline: "Lufthansa" + year: 2024 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Düsseldorf" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Hamburg" + airline: "Lufthansa" + year: 2024 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Hamburg" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Barcelona" + airline: "Lufthansa" + year: 2024 + month: "June" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Barcelona" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "June" + occasion: "Work" + travelers: ["Patrick", "Michael"] + +- origin: "Munich" + destination: "Paris" + airline: "AirFrance" + year: 2024 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Paris" + destination: "Marseille" + airline: "AirFrance" + year: 2024 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Marseille" + destination: "Paris" + airline: "AirFrance" + year: 2024 + month: "June" + occasion: "Private" + +- origin: "Paris" + destination: "Munich" + airline: "AirFrance" + year: 2024 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "Hamburg" + airline: "Lufthansa" + year: 2024 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Hamburg" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Bodrum" + airline: "Eurowings Discover" + year: 2024 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sofya", "Masha"] + +- origin: "Bodrum" + destination: "Munich" + airline: "Eurowings Discover" + year: 2024 + month: "August" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "Vienna" + airline: "Austrian" + year: 2024 + month: "August" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "Vienna" + destination: "New York" + airline: "Austrian" + year: 2024 + month: "August" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Hong Kong" + airline: "Cathay Pacific" + year: 2024 + month: "September" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Hong Kong" + destination: "Chengdu" + airline: "Air China" + year: 2024 + month: "September" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Chengdu" + destination: "Hong Kong" + airline: "Air China" + year: 2024 + month: "September" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Hong Kong" + destination: "New York" + airline: "Cathay Pacific" + year: 2024 + month: "September" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "Delta" + year: 2024 + month: "October" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Denver" + destination: "New York" + airline: "Delta" + year: 2024 + month: "October" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "New York" + destination: "Beijing" + airline: "Air China" + year: 2024 + month: "November" + occasion: "Rowing" + travelers: ["Patrick"] + +- origin: "Beijing" + destination: "Shenzhen" + airline: "Air China" + year: 2024 + month: "November" + occasion: "Rowing" + travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"] + +- origin: "Shenzhen" + destination: "Beijing" + airline: "Air China" + year: 2024 + month: "November" + occasion: "Rowing" + travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"] + +- origin: "Beijing" + destination: "New York" + airline: "Air China" + year: 2024 + month: "November" + occasion: "Rowing" + travelers: ["Patrick", "Ioanna", "Jay", "Alex", "Bastian"] + +- origin: "New York" + destination: "Toronto" + airline: "Air Canada" + year: 2024 + month: "November" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Halifax" + airline: "Air Canada" + year: 2024 + month: "November" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "Halifax" + destination: "Toronto" + airline: "Air Canada" + year: 2024 + month: "November" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "New York" + airline: "Air Canada" + year: 2024 + month: "November" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Munich" + airline: "Lufthansa" + year: 2024 + month: "December" + occasion: "Private" + travelers: ["Patrick"] + +### 2025 + +- origin: "Munich" + destination: "Cologne" + airline: "Lufthansa" + year: 2025 + month: "January" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "Cologne" + destination: "Munich" + airline: "Lufthansa" + year: 2025 + month: "January" + occasion: "Projects" + travelers: ["Patrick", "Matthias"] + +- origin: "Munich" + destination: "New York" + airline: "Lufthansa" + year: 2025 + month: "January" + occasion: "Yale" + travelers: ["Patrick"] + +- origin: "New York" + destination: "San Diego" + airline: "Delta" + year: 2025 + month: "February" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "San Diego" + destination: "New York" + airline: "Delta" + year: 2025 + month: "March" + occasion: "Projects" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Munich" + airline: "Lufthansa" + year: 2025 + month: "March" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Madrid" + airline: "Lufthansa" + year: 2025 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Madrid" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2025 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Frankfurt" + destination: "Munich" + airline: "Lufthansa" + year: 2025 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Munich" + destination: "New York" + airline: "Lufthansa" + year: 2025 + month: "March" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Munich" + airline: "Lufthansa" + year: 2025 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Vienna" + airline: "Austrian" + year: 2025 + month: "May" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Istanbul" + airline: "Turkish Airlines" + year: 2025 + month: "June" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "Istanbul" + destination: "Astana" + airline: "Turkish Airlines" + year: 2025 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Pascal", "Regina", "Harald","Eric", "Felix", "Max", "Benjamin"] + +- origin: "Astana" + destination: "Istanbul" + airline: "Turkish Airlines" + year: 2025 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Istanbul" + destination: "Munich" + airline: "Turkish Airlines" + year: 2025 + month: "June" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Vienna" + destination: "Frankfurt" + airline: "Austrian" + year: 2025 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Frankfurt" + destination: "Denver" + airline: "Lufthansa" + year: 2025 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2025 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2025 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Boston" + airline: "United" + year: 2025 + month: "July" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Boston" + destination: "Denver" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Washington DC" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Washington DC" + destination: "Denver" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Chicago" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Chicago" + destination: "Denver" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Raleigh Durham" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Raleigh Durham" + destination: "Denver" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "London" + airline: "United" + year: 2025 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "London" + destination: "New York" + airline: "United" + year: 2025 + month: "August" + occasion: "Private" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2025 + month: "August" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "New Delhi" + airline: "Air Canada" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New Delhi" + destination: "Goa" + airline: "Air India" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Goa" + destination: "New Delhi" + airline: "Air India" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Goa" + destination: "New Delhi" + airline: "Air India" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New Delhi" + destination: "Toronto" + airline: "Air Canada" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "Air Canada" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Mexico City" + airline: "Delta" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Mexico City" + destination: "Dallas" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dallas" + destination: "Denver" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Washington DC" + airline: "United" + year: 2025 + month: "September" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Washington DC" + destination: "Denver" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Denver" + destination: "London" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "London" + destination: "Denver" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "San Francisco" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "San Francisco" + destination: "Denver" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +# Week of October 20th +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +# Week of October 27th +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "San Francisco" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "San Francisco" + destination: "Boston" + airline: "United" + year: 2025 + month: "October" + occasion: "Work" + travelers: ["Patrick"] + +# Week of November 3rd +- origin: "Boston" + destination: "Orlando" + airline: "JetBlue" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Orlando" + destination: "Denver" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Denver" + destination: "San Francisco" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "San Francisco" + destination: "Denver" + airline: "United" + year: 2025 + month: "November" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Toronto" + airline: "United" + year: 2025 + month: "Air Canada" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Toronto" + destination: "Denver" + airline: "United" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Munich" + airline: "United" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Berlin" + airline: "Lufthansa" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Berlin" + destination: "Stuttgart" + airline: "Eurowings" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Denver" + airline: "United" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Miami" + airline: "United" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Miami" + destination: "Denver" + airline: "United" + year: 2025 + month: "December" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2025 + month: "December" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +### 2026 + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2026 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Dallas" + airline: "United" + year: 2025 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Dallas" + destination: "Denver" + airline: "United" + year: 2026 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2026 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2026 + month: "January" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "London" + destination: "Munich" + airline: "Lufthansa" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Denver" + airline: "United" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "New York" + airline: "United" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Munich" + airline: "United" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Stuttgart" + airline: "Lufthansa" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Denver" + airline: "United" + year: 2026 + month: "February" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Miami" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick", "Sofya"] + +- origin: "Miami" + destination: "New York" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Houston" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Houston" + destination: "New York" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Miami" + airline: "United" + year: 2026 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Miami" + destination: "New York" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "New York" + destination: "Denver" + airline: "United" + year: 2026 + month: "March" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Los Angeles" + airline: "United" + year: 2026 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Los Angeles" + destination: "Hong Kong" + airline: "United" + year: 2026 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Hong Kong" + destination: "Phu Quoc" + airline: "Hong Kong Airlines" + year: 2026 + month: "March" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Phu Quoc" + destination: "Hong Kong" + airline: "Hong Kong Airlines" + year: 2026 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Hong Kong" + destination: "San Francisco" + airline: "United" + year: 2026 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "San Francisco" + destination: "Denver" + airline: "United" + year: 2026 + month: "April" + occasion: "Private" + travelers: ["Patrick", "Sofya"] + +- origin: "Denver" + destination: "Calgary" + airline: "United" + year: 2026 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Calgary" + destination: "San Francisco" + airline: "United" + year: 2026 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "San Francisco" + destination: "Denver" + airline: "United" + year: 2026 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Denver" + destination: "Munich" + airline: "United" + year: 2026 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Frankfurt" + airline: "Lufthansa" + year: 2026 + month: "April" + occasion: "Work" + travelers: ["Patrick"] + +- origin: "Munich" + destination: "Denver" + airline: "United" + year: 2026 + month: "May" + occasion: "Work" + travelers: ["Patrick"] diff --git a/_data/heroLocations.yaml b/_data/heroLocations.yaml new file mode 100644 index 0000000..6b43653 --- /dev/null +++ b/_data/heroLocations.yaml @@ -0,0 +1,315 @@ +- name: "New Haven" + lat: 41.3083 + lon: -72.9279 +- name: "Coventry" + lat: 52.4068 + lon: -1.5197 +- name: "London" + lat: 51.5074 + lon: -0.1278 +- name: "Munich" + lat: 48.1351 + lon: 11.5820 +- name: "Brussels" + lat: 50.8503 + lon: 4.3517 +- name: "Singapore" + lat: 1.3521 + lon: 103.8198 +- name: "Hong Kong" + lat: 22.3193 + lon: 114.1694 +- name: "Dubai" + lat: 25.2048 + lon: 55.2708 +- name: "Riyadh" + lat: 24.7136 + lon: 46.6753 +- name: "Denver" + lat: 39.7392 + lon: -104.9903 +- name: "Toulouse" + lat: 43.6047 + lon: 1.4442 +- name: "Den Haag" + lat: 52.0705 + lon: 4.3007 +- name: "Muscat" + lat: 23.5859 + lon: 58.4059 +- name: "Doha" + lat: 25.2854 + lon: 51.5310 +- name: "Frankfurt" + lat: 50.0379 + lon: 8.5622 +- name: "Düsseldorf" + lat: 51.2895 + lon: 6.7668 +- name: "Vienna" + lat: 48.1200 + lon: 16.5700 +- name: "Kuwait" + lat: 29.2403 + lon: 47.9698 +- name: "Dammam" + lat: 26.4207 + lon: 50.0888 +- name: "Washington DC" + lat: 38.9072 + lon: -77.0369 +- name: "Abu Dhabi" + lat: 24.4539 + lon: 54.6778 +- name: "Tabuk" + lat: 28.3835 + lon: 36.5662 +- name: "Zurich" + lat: 47.4502 + lon: 8.5616 +- name: "Arar" + lat: 30.9750 + lon: 41.0382 +- name: "Budapest" + lat: 47.4979 + lon: 19.0402 +- name: "Dublin" + lat: 53.4264 + lon: -6.2499 +- name: "Berlin" + lat: 52.5200 + lon: 13.4050 +- name: "Hanoi" + lat: 21.0285 + lon: 105.8542 +- name: "Da Nang" + lat: 16.0544 + lon: 108.2022 +- name: "Ho Chi Minh" + lat: 10.8231 + lon: 106.6297 +- name: "Phu Quoc" + lat: 10.2276 + lon: 103.9809 +- name: "Hamburg" + lat: 53.6302 + lon: 10.0041 +- name: "Almaty" + lat: 43.2551 + lon: 76.9126 +- name: "Nursultan" + lat: 51.0924 + lon: 71.4304 +- name: "Astana" + lat: 51.0924 + lon: 71.4304 +- name: "New York" + lat: 40.7128 + lon: -74.0060 +- name: "Panama" + lat: 8.9824 + lon: -79.5199 +- name: "Montevideo" + lat: -34.9011 + lon: -56.1645 +- name: "Stockholm" + lat: 59.3293 + lon: 18.0686 +- name: "Copenhagen" + lat: 55.6761 + lon: 12.5683 +- name: "San Francisco" + lat: 37.7749 + lon: -122.4194 +- name: "Oakland" + lat: 37.8044 + lon: -122.2712 +- name: "Big Island" + lat: 19.8968 + lon: -155.5828 +- name: "Honolulu" + lat: 21.3069 + lon: -157.8583 +- name: "Cologne" + lat: 50.9375 + lon: 6.9603 +- name: "Seattle" + lat: 47.6062 + lon: -122.3321 +- name: "Barcelona" + lat: 41.3851 + lon: 2.1734 +- name: "Bodrum" + lat: 37.0343 + lon: 27.4305 +- name: "Chengdu" + lat: 30.5728 + lon: 103.8652 +- name: "Beijing" + lat: 39.9042 + lon: 116.4074 +- name: "Shenzhen" + lat: 22.5431 + lon: 114.0579 +- name: "Toronto" + lat: 43.6532 + lon: -79.3832 +- name: "Halifax" + lat: 44.6488 + lon: -63.5752 +- name: "Aruba" + lat: 12.5211 + lon: -69.9683 +- name: "San Diego" + lat: 32.7157 + lon: -117.1611 +- name: "Madrid" + lat: 40.4168 + lon: -3.7038 +- name: "Xi'an" + lat: 34.3416 + lon: 108.9398 +- name: "Shanghai" + lat: 31.2304 + lon: 121.4737 +- name: "Borneo" + lat: 0.9619 + lon: 114.5548 +- name: "Durban" + lat: -29.8587 + lon: 31.0218 +- name: "Helsinki" + lat: 60.1699 + lon: 24.9384 +- name: "New Delhi" + lat: 28.6139 + lon: 77.2090 +- name: "Jaipur" + lat: 26.9124 + lon: 75.7873 +- name: "Mumbai" + lat: 19.0760 + lon: 72.8777 +- name: "Bangalore" + lat: 12.9716 + lon: 77.5946 +- name: "Bangkok" + lat: 13.7563 + lon: 100.5018 +- name: "Addis Ababa" + lat: 9.0320 + lon: 38.7492 +- name: "Mombasa" + lat: -4.0435 + lon: 39.6682 +- name: "Nairobi" + lat: -1.2921 + lon: 36.8219 +- name: "Masai Mara" + lat: -1.5437 + lon: 35.1233 +- name: "Birmingham" + lat: 52.4862 + lon: -1.8904 +- name: "Geneva" + lat: 46.2044 + lon: 6.1432 +- name: "Istanbul" + lat: 41.0082 + lon: 28.9784 +- name: "Cairo" + lat: 30.0444 + lon: 31.2357 +- name: "Rome" + lat: 41.9028 + lon: 12.4964 +- name: "Edinburgh" + lat: 55.9533 + lon: -3.1883 +- name: "Athens" + lat: 37.9838 + lon: 23.7275 +- name: "Stuttgart" + lat: 48.7758 + lon: 9.1829 +- name: "Amsterdam" + lat: 52.3676 + lon: 4.9041 +- name: "Paris" + lat: 48.8566 + lon: 2.3522 +- name: "Marseille" + lat: 43.2965 + lon: 5.3698 +- name: "Cancun" + lat: 21.1619 + lon: -86.8515 +- name: "St. Petersburg" + lat: 59.9311 + lon: 30.3609 +- name: "Moscow" + lat: 55.7558 + lon: 37.6173 +- name: "Venice" + lat: 45.4408 + lon: 12.3155 +- name: "Los Angeles" + lat: 34.0522 + lon: -118.2437 +- name: "Miami" + lat: 25.7617 + lon: -80.1918 +- name: "Belize" + lat: 17.1899 + lon: -88.4976 +- name: "Kota Kinabalu" + lat: 5.9804 + lon: 116.0735 +- name: "Valeta" + lat: 35.8989 + lon: 14.5146 +- name: "Cape Town" + lat: -33.9249 + lon: 18.4241 +- name: "Boston" + lat: 42.3656 + lon: -71.0096 +- name: "Santo Domingo" + lat: 18.4861 + lon: -69.9312 +- name: "Lisbon" + lat: 38.7223 + lon: -9.1393 +- name: "Mauritius" + lat: -20.3484 + lon: 57.5522 +- name: "Cagliari" + lat: 39.2238 + lon: 9.1217 +- name: "Reykjavik" + lat: 64.1466 + lon: -21.9426 +- name: "Chicago" + lat: 41.8781 + lon: -87.6298 +- name: "Raleigh Durham" + lat: 35.7796 + lon: -78.6382 +- name: "Goa" + lat: 15.4909 + lon: 73.8278 +- name: "Mexico City" + lat: 19.4326 + lon: -99.1332 +- name: "Dallas" + lat: 32.7767 + lon: -96.7970 +- name: "Orlando" + lat: 28.5383 + lon: -81.3792 +- name: "Houston" + lat: 29.7604 + lon: -95.3698 +- name: "Calgary" + lat: 51.0447 + lon: -114.0719 diff --git a/_data/social.yaml b/_data/social.yaml new file mode 100644 index 0000000..e2e60e8 --- /dev/null +++ b/_data/social.yaml @@ -0,0 +1,27 @@ +- platform: LinkedIn + username: patrickfreyer + url: https://linkedin.com/in/patrickfreyer + icon: fab fa-linkedin + color: "#0077B5" + description: Connect with me professionally + +- platform: GitHub + username: patrickfreyer + url: https://github.com/patrickfreyer + icon: fab fa-github + color: "#333333" + description: Check out my code and projects + +- platform: X (Twitter) + username: patrickfreyer + url: https://x.com/real_patrick_f + icon: fab fa-x-twitter + color: "#000000" + description: Follow my thoughts and updates + +- platform: Instagram + username: patrickfreyer + url: hhttps://www.instagram.com/brightlyshadowed/ + icon: fab fa-instagram + color: "#E4405F" + description: See my visual journey \ No newline at end of file diff --git a/_includes/about.html b/_includes/about.html new file mode 100644 index 0000000..c05df22 --- /dev/null +++ b/_includes/about.html @@ -0,0 +1,15 @@ +
+
+

About Me

+
+
+
+ Patrick Freyer +
+
+
+

{{ site.data.about.about_text }}

+
+
+
+
\ No newline at end of file diff --git a/_includes/hero.html b/_includes/hero.html new file mode 100644 index 0000000..74ae83e --- /dev/null +++ b/_includes/hero.html @@ -0,0 +1,41 @@ +
+
+
+
+
+
+
+
+
+
+
+

Patrick Freyer

+

Generative AI Builder, Developer, and Strategist

+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/_includes/social.html b/_includes/social.html new file mode 100644 index 0000000..9c62b5a --- /dev/null +++ b/_includes/social.html @@ -0,0 +1,23 @@ +
+
+

Stay in Touch

+ +
+
\ No newline at end of file diff --git a/_layouts/default.html b/_layouts/default.html new file mode 100644 index 0000000..698837b --- /dev/null +++ b/_layouts/default.html @@ -0,0 +1,14 @@ + + + + + + {{ site.title }} + + + + {{ content }} + + \ No newline at end of file diff --git a/assets/8k_earth_nightmap.jpg b/assets/8k_earth_nightmap.jpg new file mode 100644 index 0000000..e211b6c Binary files /dev/null and b/assets/8k_earth_nightmap.jpg differ diff --git a/assets/FineTunedBanner.png b/assets/FineTunedBanner.png new file mode 100644 index 0000000..8886196 Binary files /dev/null and b/assets/FineTunedBanner.png differ diff --git a/assets/earth_albedo.jpg b/assets/earth_albedo.jpg new file mode 100644 index 0000000..00e9928 Binary files /dev/null and b/assets/earth_albedo.jpg differ diff --git a/assets/earth_bump.jpg b/assets/earth_bump.jpg new file mode 100644 index 0000000..46aa4d2 Binary files /dev/null and b/assets/earth_bump.jpg differ diff --git a/assets/earth_clouds.jpg b/assets/earth_clouds.jpg new file mode 100644 index 0000000..5d4d35e Binary files /dev/null and b/assets/earth_clouds.jpg differ diff --git a/assets/earth_elevation.jpg b/assets/earth_elevation.jpg new file mode 100644 index 0000000..46aa4d2 Binary files /dev/null and b/assets/earth_elevation.jpg differ diff --git a/assets/earth_night.jpg b/assets/earth_night.jpg new file mode 100644 index 0000000..7c1a96d Binary files /dev/null and b/assets/earth_night.jpg differ diff --git a/assets/earth_normal.jpg b/assets/earth_normal.jpg new file mode 100644 index 0000000..46aa4d2 Binary files /dev/null and b/assets/earth_normal.jpg differ diff --git a/assets/earth_roughness.jpg b/assets/earth_roughness.jpg new file mode 100644 index 0000000..d1e0b05 Binary files /dev/null and b/assets/earth_roughness.jpg differ diff --git a/assets/earth_specular.jpg b/assets/earth_specular.jpg new file mode 100644 index 0000000..d1e0b05 Binary files /dev/null and b/assets/earth_specular.jpg differ diff --git a/assets/js/earth-ar.js b/assets/js/earth-ar.js new file mode 100644 index 0000000..4a09b6d --- /dev/null +++ b/assets/js/earth-ar.js @@ -0,0 +1,252 @@ +import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js'; +import { ARButton } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/webxr/ARButton.js'; + +// WebXR Polyfill for better iOS support +let WebXRPolyfill = null; +try { + WebXRPolyfill = await import('https://cdn.jsdelivr.net/npm/webxr-polyfill@latest/build/webxr-polyfill.min.js'); +} catch (e) { + console.log('WebXR Polyfill not available, continuing without it'); +} + +class EarthAR { + constructor() { + this.scene = null; + this.camera = null; + this.renderer = null; + this.earthMesh = null; + this.isARSession = false; + this.reticle = null; + this.earthScale = 0.3; // Smaller scale for AR + } + + async init() { + // Initialize WebXR Polyfill if available + if (WebXRPolyfill && !navigator.xr) { + new WebXRPolyfill(); + console.log('WebXR Polyfill initialized'); + } + + // Wait a bit for polyfill to initialize + await new Promise(resolve => setTimeout(resolve, 100)); + + // Check if WebXR is supported + if (!navigator.xr) { + console.warn('WebXR not supported - trying alternative detection'); + + // Alternative detection for iOS Safari + if (navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad')) { + console.log('iOS device detected, attempting AR setup'); + return this.setupForIOS(); + } + + return false; + } + + // Check if AR is supported + try { + const isARSupported = await navigator.xr.isSessionSupported('immersive-ar'); + if (!isARSupported) { + console.warn('AR not supported on this device'); + return false; + } + return true; + } catch (error) { + console.error('Error checking AR support:', error); + return false; + } + } + + async setupForIOS() { + // iOS Safari specific setup + console.log('Setting up for iOS Safari'); + + // Check for iOS version and Safari + const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent); + const isSafari = /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent); + + if (isIOS && isSafari) { + console.log('iOS Safari detected, AR should be available'); + return true; + } + + return false; + } + + createARScene() { + // Create scene + this.scene = new THREE.Scene(); + + // Create camera + this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + + // Create renderer + this.renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true, + preserveDrawingBuffer: true + }); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setPixelRatio(window.devicePixelRatio); + this.renderer.xr.enabled = true; + + // Add to DOM + document.body.appendChild(this.renderer.domElement); + + // Add AR button + const arButton = ARButton.createButton(this.renderer, { + sessionInit: { + requiredFeatures: ['hit-test'], + optionalFeatures: ['dom-overlay'], + domOverlay: { root: document.body } + } + }); + document.body.appendChild(arButton); + + // Setup lighting + const ambientLight = new THREE.AmbientLight(0xffffff, 0.6); + this.scene.add(ambientLight); + + const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); + directionalLight.position.set(0, 10, 0); + this.scene.add(directionalLight); + + // Create reticle for placement + this.createReticle(); + + // Setup session events + this.renderer.xr.addEventListener('sessionstart', () => { + this.isARSession = true; + console.log('AR session started'); + }); + + this.renderer.xr.addEventListener('sessionend', () => { + this.isARSession = false; + console.log('AR session ended'); + }); + + // Start render loop + this.renderer.setAnimationLoop(this.render.bind(this)); + } + + createReticle() { + const geometry = new THREE.RingGeometry(0.15, 0.2, 32).rotateX(-Math.PI / 2); + const material = new THREE.MeshBasicMaterial(); + this.reticle = new THREE.Mesh(geometry, material); + this.reticle.matrixAutoUpdate = false; + this.reticle.visible = false; + this.scene.add(this.reticle); + } + + createEarthForAR() { + // Create a simplified earth for AR + const earthRadius = this.earthScale; + const geometry = new THREE.SphereGeometry(earthRadius, 32, 32); + + // Simple material for AR (no complex textures) + const material = new THREE.MeshPhongMaterial({ + color: 0x4B6CB7, // Blue color + shininess: 30 + }); + + this.earthMesh = new THREE.Mesh(geometry, material); + this.earthMesh.visible = false; + this.scene.add(this.earthMesh); + + // Add some basic flight paths for AR + this.addSimpleFlightPaths(); + } + + addSimpleFlightPaths() { + // Create simple curved paths for AR + const curve = new THREE.CubicBezierCurve3( + new THREE.Vector3(-0.5, 0, 0), + new THREE.Vector3(-0.2, 0.3, 0.2), + new THREE.Vector3(0.2, 0.3, -0.2), + new THREE.Vector3(0.5, 0, 0) + ); + + const points = curve.getPoints(50); + const geometry = new THREE.BufferGeometry().setFromPoints(points); + const material = new THREE.LineBasicMaterial({ color: 0xFFFF00 }); + const line = new THREE.Line(geometry, material); + + this.earthMesh.add(line); + } + + setupHitTesting() { + let hitTestSource = null; + let hitTestSourceRequested = false; + + const session = this.renderer.xr.getSession(); + + session.addEventListener('select', () => { + if (this.reticle.visible) { + // Place earth at reticle position + this.earthMesh.position.setFromMatrixPosition(this.reticle.matrix); + this.earthMesh.visible = true; + this.reticle.visible = false; + } + }); + + session.requestReferenceSpace('viewer').then((referenceSpace) => { + session.requestHitTestSource({ space: referenceSpace }).then((source) => { + hitTestSource = source; + }); + }); + + session.addEventListener('end', () => { + hitTestSourceRequested = false; + hitTestSource = null; + }); + + const frame = this.renderer.xr.getFrame(); + + if (hitTestSourceRequested === false) { + session.requestReferenceSpace('viewer').then((referenceSpace) => { + session.requestHitTestSource({ space: referenceSpace }).then((source) => { + hitTestSource = source; + }); + }); + hitTestSourceRequested = true; + } + + if (hitTestSourceRequested === false) return; + + if (hitTestSource) { + const hitTestResults = frame.getHitTestResults(hitTestSource); + + if (hitTestResults.length) { + const hit = hitTestResults[0]; + const pose = hit.getPose(this.reticle.parent); + + this.reticle.visible = true; + this.reticle.matrix.fromArray(pose.transform.matrix); + } else { + this.reticle.visible = false; + } + } + } + + render() { + if (this.isARSession) { + this.setupHitTesting(); + } + + this.renderer.render(this.scene, this.camera); + } + + // Method to integrate with existing earth visualization + integrateWithExistingEarth(existingEarthMesh) { + if (this.earthMesh && existingEarthMesh) { + // Clone the existing earth and scale it for AR + this.earthMesh = existingEarthMesh.clone(); + this.earthMesh.scale.setScalar(this.earthScale); + this.earthMesh.visible = false; + this.scene.add(this.earthMesh); + } + } +} + +// Export for use +window.EarthAR = EarthAR; \ No newline at end of file diff --git a/assets/js/earth.js b/assets/js/earth.js new file mode 100644 index 0000000..2242000 --- /dev/null +++ b/assets/js/earth.js @@ -0,0 +1,619 @@ +import * as THREE from 'https://cdn.skypack.dev/three@0.128.0/build/three.module.js'; +import { OrbitControls } from 'https://cdn.skypack.dev/three@0.128.0/examples/jsm/controls/OrbitControls.js'; + +// Ensure required data is available +if (typeof locationsData === 'undefined' || typeof flightRoutesData === 'undefined') { + console.error('Required data is not defined. Make sure locationsData and flightRoutesData are passed correctly from Jekyll.'); +} else { + initEarth(); +} + +// Helper function to find location data by name +function findLocationByName(name) { + return locationsData.find(loc => loc.name === name); +} + +// Helper function to count route frequencies +function countRouteFrequencies(routes) { + const frequencies = {}; + + routes.forEach(route => { + // Create a consistent key for the route (sort cities alphabetically to count both directions) + const cities = [route.origin, route.destination].sort(); + const routeKey = `${cities[0]}-${cities[1]}`; + frequencies[routeKey] = (frequencies[routeKey] || 0) + 1; + }); + + return frequencies; +} + +// Function to get route frequency +function getRouteFrequency(origin, destination, frequencies) { + const cities = [origin, destination].sort(); + const routeKey = `${cities[0]}-${cities[1]}`; + return frequencies[routeKey] || 1; +} + +// Function to create curved flight path using great circle +function createFlightPath(startPoint, endPoint, earthRadius, numLines = 1) { + const pathsPoints = []; + const numPoints = 50; + + // Normalize the start and end points to get unit vectors + const startNormalized = startPoint.clone().normalize(); + const endNormalized = endPoint.clone().normalize(); + + // Calculate the great circle distance (angle between points) + const dotProduct = startNormalized.dot(endNormalized); + const angle = Math.acos(Math.max(-1, Math.min(1, dotProduct))); + + // Calculate distance for height scaling + const distance = startPoint.distanceTo(endPoint); + const maxHeightScale = 0.08; + const baseScale = Math.atan(distance) / (Math.PI / 2) * maxHeightScale; + + // Generate base curved path points using great circle interpolation + const basePoints = []; + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + + // Use spherical linear interpolation (slerp) for great circle + const sinAngle = Math.sin(angle); + if (sinAngle === 0) { + // Points are the same or opposite, use direct interpolation + const point = startNormalized.clone().lerp(endNormalized, t); + const heightScale = earthRadius * (1 + baseScale * Math.sin(Math.PI * t)); + point.normalize().multiplyScalar(heightScale); + basePoints.push(point); + } else { + // Use proper spherical interpolation + const sinT = Math.sin(t * angle); + const sinOneMinusT = Math.sin((1 - t) * angle); + + const point = new THREE.Vector3(); + point.addScaledVector(startNormalized, sinOneMinusT / sinAngle); + point.addScaledVector(endNormalized, sinT / sinAngle); + + // Add height curve above the great circle + const heightScale = earthRadius * (1 + baseScale * Math.sin(Math.PI * t)); + point.normalize().multiplyScalar(heightScale); + basePoints.push(point); + } + } + + // Calculate perpendicular direction for parallel lines + const pathDirection = endPoint.clone().sub(startPoint).normalize(); + const globeNormal = startPoint.clone().add(endPoint).normalize(); + const perpDirection = pathDirection.clone().cross(globeNormal).normalize(); + + // Create offset paths + for (let i = 0; i < numLines; i++) { + const offset = perpDirection.clone().multiplyScalar(0.01 * (i - (numLines - 1) / 2)); + const offsetPoints = basePoints.map(point => { + return point.clone().add(offset); + }); + pathsPoints.push(offsetPoints); + } + + return pathsPoints; +} + +// Function to create flight path lines (optional dashed for planned flights) +function createFlightLines(pathsPoints, color = 0x00ff00, isPlanned = false) { + const lines = []; + const dashSize = 0.15; + const gapSize = 0.08; + + pathsPoints.forEach(points => { + const geometry = new THREE.BufferGeometry().setFromPoints(points); + + // Create a brighter version of the color for better contrast + const brightColor = new THREE.Color(color); + brightColor.multiplyScalar(1.5); // Make it 50% brighter + + const material = isPlanned + ? new THREE.LineDashedMaterial({ + color: brightColor, + dashSize, + gapSize, + depthTest: true, + depthWrite: true, + side: THREE.FrontSide + }) + : new THREE.LineBasicMaterial({ + color: brightColor, + transparent: false, + linewidth: 3, + depthTest: true, + depthWrite: true, + side: THREE.FrontSide + }); + + const line = new THREE.Line(geometry, material); + if (isPlanned) { + line.computeLineDistances(); // Required for dashed lines to render + } + line.renderOrder = 1; // Render after earth mesh + + // Add a glow effect by creating a thicker line behind (dashed for planned) + const glowGeometry = new THREE.BufferGeometry().setFromPoints(points); + const glowMaterial = isPlanned + ? new THREE.LineDashedMaterial({ + color: brightColor, + transparent: true, + opacity: 0.4, + dashSize, + gapSize, + depthTest: true, + depthWrite: false, + side: THREE.FrontSide + }) + : new THREE.LineBasicMaterial({ + color: brightColor, + transparent: true, + opacity: 0.4, + linewidth: 5, + depthTest: true, + depthWrite: false, + side: THREE.FrontSide + }); + const glowLine = new THREE.Line(glowGeometry, glowMaterial); + if (isPlanned) { + glowLine.computeLineDistances(); + } + glowLine.renderOrder = 0; // Render before main line + + lines.push(glowLine); // Add glow first (behind) + lines.push(line); // Add main line on top + }); + return lines; +} + +// Add these functions before initEarth() +function getUniqueValues(data, key) { + if (key === 'travelers') { + // Special handling for travelers array + const allTravelers = new Set(); + data.forEach(item => { + if (Array.isArray(item[key])) { + item[key].forEach(traveler => allTravelers.add(traveler)); + } + }); + return [...allTravelers].sort(); + } + return [...new Set(data.map(item => item[key]))].filter(Boolean).sort(); +} + +function populateFilterDropdowns(flightData) { + const years = getUniqueValues(flightData, 'year'); + const airlines = getUniqueValues(flightData, 'airline'); + const occasions = getUniqueValues(flightData, 'occasion'); + const months = getUniqueValues(flightData, 'month'); + const travelers = getUniqueValues(flightData, 'travelers'); + + // Helper function to populate dropdowns + function populateDropdown(elementId, values) { + const select = document.getElementById(elementId); + if (!select) return; + + select.innerHTML = ''; + values.forEach(value => { + const option = document.createElement('option'); + option.value = value; + option.textContent = value; + select.appendChild(option); + }); + } + + populateDropdown('year-filter', years); + populateDropdown('airline-filter', airlines); + populateDropdown('occasion-filter', occasions); + populateDropdown('month-filter', months); + populateDropdown('travelers-filter', travelers); +} + +function getSelectedValues(elementId) { + const element = document.getElementById(elementId); + if (!element) return []; + return Array.from(element.selectedOptions).map(option => option.value); +} + +function filterFlightData(data, filters) { + return data.filter(flight => { + // Convert years to numbers for comparison + const yearMatch = filters.years.length === 0 || + filters.years.map(Number).includes(Number(flight.year)); + const airlineMatch = filters.airlines.length === 0 || filters.airlines.includes(flight.airline); + const occasionMatch = filters.occasions.length === 0 || filters.occasions.includes(flight.occasion); + const monthMatch = filters.months.length === 0 || filters.months.includes(flight.month); + + // Check if any selected travelers are in the flight's travelers array + const travelersMatch = filters.travelers.length === 0 || + (Array.isArray(flight.travelers) && + filters.travelers.some(traveler => flight.travelers.includes(traveler))); + + return yearMatch && airlineMatch && occasionMatch && monthMatch && travelersMatch; + }); +} + +function setupFilterHandlers(earthMesh, initializeFlightPaths, scene) { + const applyButton = document.getElementById('apply-filters'); + const resetButton = document.getElementById('reset-filters'); + const toggleButton = document.getElementById('toggle-filters'); + const filterPanel = document.getElementById('filter-panel'); + + // Setup toggle functionality + if (toggleButton && filterPanel) { + toggleButton.addEventListener('click', () => { + filterPanel.classList.toggle('collapsed'); + }); + } + + if (applyButton) { + applyButton.addEventListener('click', () => { + const filters = { + years: getSelectedValues('year-filter'), + airlines: getSelectedValues('airline-filter'), + occasions: getSelectedValues('occasion-filter'), + months: getSelectedValues('month-filter'), + travelers: getSelectedValues('travelers-filter') + }; + + // Remove existing flight paths from scene + scene.children = scene.children.filter(child => !(child instanceof THREE.Line)); + + // Apply filtered data + const filteredData = filterFlightData(flightRoutesData, filters); + initializeFlightPaths(filteredData, earthMesh); + }); + } + + if (resetButton) { + resetButton.addEventListener('click', () => { + // Clear all selections + ['year-filter', 'airline-filter', 'occasion-filter', 'month-filter', 'travelers-filter'].forEach(id => { + const element = document.getElementById(id); + if (element) element.selectedIndex = -1; + }); + + // Reset to original data + scene.children = scene.children.filter(child => !(child instanceof THREE.Line)); + initializeFlightPaths(flightRoutesData, earthMesh); + }); + } +} + +// Function to generate distinct colors +function generateDistinctColors(count) { + // Simplified, bright color palette for better visibility + const baseColors = [ + 0xFFFF00, // Bright Yellow + 0xFF8C00, // Bright Orange + 0x00BFFF, // Bright Blue + 0xFFFFFF, // White + 0xFF1493, // Deep Pink + 0x00FF00, // Bright Green + 0xFF4500, // Orange Red + 0x00FFFF, // Cyan + 0xFFD700, // Gold + 0xFF69B4, // Hot Pink + 0x00CED1, // Dark Turquoise + 0xFF6347 // Tomato + ]; + + const colors = []; + for (let i = 0; i < count; i++) { + // Use modulo to cycle through colors if we need more than the base palette + colors.push(baseColors[i % baseColors.length]); + } + + return colors; +} + +function initEarth() { + const container = document.getElementById('earth-container'); + if (!container) { + console.error('Earth container not found'); + return; + } + + // Scene setup + const scene = new THREE.Scene(); + const camera = new THREE.PerspectiveCamera(75, container.clientWidth / container.clientHeight, 0.1, 1000); + const renderer = new THREE.WebGLRenderer({ + alpha: true, + antialias: true, + logarithmicDepthBuffer: true // Helps with z-fighting + }); + + renderer.setSize(container.clientWidth, container.clientHeight); + renderer.setPixelRatio(window.devicePixelRatio); + container.appendChild(renderer.domElement); + + // Earth radius and state + const earthRadius = 5; + const state = { + isDaylight: true // Can be toggled for day/night transitions + }; + + // Texture Loader + const textureLoader = new THREE.TextureLoader(); + const loadTexture = (path) => textureLoader.load(`https://patrickfreyer.com/assets/${path}`); + + // Load all textures + const earthDayTexture = loadTexture('earth_albedo.jpg'); + const earthNightTexture = loadTexture('earth_night.jpg'); + const normalTexture = loadTexture('earth_normal.jpg'); + const specularTexture = loadTexture('earth_specular.jpg'); + const roughnessTexture = loadTexture('earth_roughness.jpg'); + const bumpTexture = loadTexture('earth_bump.jpg'); + const cloudsTexture = loadTexture('earth_clouds.jpg'); + + // 0. Solid Base Sphere (to prevent flight routes from showing through) + const baseGeometry = new THREE.SphereGeometry(earthRadius, 64, 64); + const baseMaterial = new THREE.MeshBasicMaterial({ + color: 0x000000, // Black color + side: THREE.FrontSide + }); + const baseMesh = new THREE.Mesh(baseGeometry, baseMaterial); + baseMesh.renderOrder = -1; // Render first, before everything else + scene.add(baseMesh); + + // 1. Base Earth Layer + const globeGeometry = new THREE.SphereGeometry(earthRadius, 64, 64); + const globeMaterial = new THREE.MeshPhongMaterial({ + map: earthDayTexture, + normalMap: normalTexture, + normalScale: new THREE.Vector2(0.8, 0.8), + specularMap: specularTexture, + bumpMap: bumpTexture, + bumpScale: 0.02, + specular: new THREE.Color(0x444444), + shininess: 15 + }); + const earthMesh = new THREE.Mesh(globeGeometry, globeMaterial); + earthMesh.renderOrder = 0; // Render first + scene.add(earthMesh); + + // 2. Night Lights Layer + const nightGeometry = new THREE.SphereGeometry(earthRadius * 1.001, 64, 64); + const nightMaterial = new THREE.MeshBasicMaterial({ + map: earthNightTexture, + blending: THREE.AdditiveBlending, + transparent: true, + opacity: state.isDaylight ? 0 : 0.8, + depthWrite: false + }); + const nightMesh = new THREE.Mesh(nightGeometry, nightMaterial); + nightMesh.renderOrder = 0; // Render first + scene.add(nightMesh); + + // 3. Cloud Layer + const cloudGeometry = new THREE.SphereGeometry(earthRadius * 1.008, 64, 64); + const cloudMaterial = new THREE.MeshPhongMaterial({ + map: cloudsTexture, + transparent: true, + opacity: state.isDaylight ? 0.3 : 0.1, + depthWrite: false, + side: THREE.DoubleSide, + blending: THREE.NormalBlending + }); + const cloudMesh = new THREE.Mesh(cloudGeometry, cloudMaterial); + cloudMesh.renderOrder = 0; // Render first + scene.add(cloudMesh); + + // Enhanced Lighting System + // 1. Ambient Light + const ambientLight = new THREE.AmbientLight(0xffffff, state.isDaylight ? 0.3 : 0.1); + scene.add(ambientLight); + + // 2. Directional Light (Sun) + const directionalLight = new THREE.DirectionalLight(0xffffff, state.isDaylight ? 2.0 : 0.15); + directionalLight.position.set(5, 3, 5); + scene.add(directionalLight); + + // 3. Hemisphere Light + const hemisphereLight = new THREE.HemisphereLight( + 0xffffff, // Sky color + 0x444444, // Ground color + state.isDaylight ? 0.6 : 0 + ); + scene.add(hemisphereLight); + + // Controls + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.3; // Increased for smoother damping + controls.screenSpacePanning = false; + controls.minDistance = 6; + controls.maxDistance = 12; + controls.enablePan = false; + controls.autoRotate = true; + controls.autoRotateSpeed = 0.3; + + // Zoom settings for smoother zooming + controls.zoomSpeed = 0.3; // Reduced zoom speed (default is 1.0) + controls.enableZoom = true; + controls.zoomDampingFactor = 0.1; // Smooth zoom damping + + // Function to convert Lat/Lon to 3D coordinates + function latLonToVector3(lat, lon, radius) { + const phi = (90 - lat) * (Math.PI / 180); + const theta = (lon + 180) * (Math.PI / 180); + + const x = -(radius * Math.sin(phi) * Math.cos(theta)); + const z = radius * Math.sin(phi) * Math.sin(theta); + const y = radius * Math.cos(phi); + + return new THREE.Vector3(x, y, z); + } + + // Add pins for locations + // Create a group for the pin meshes + const createPin = () => { + const pinGroup = new THREE.Group(); + + // Create a simple sphere for the location marker + const headGeometry = new THREE.SphereGeometry(0.015, 8, 8); // Smaller and less detailed sphere + const headMaterial = new THREE.MeshPhongMaterial({ + color: 0x4169E1, + emissive: 0x0000ff, + emissiveIntensity: 0.3, + shininess: 50 + }); + const head = new THREE.Mesh(headGeometry, headMaterial); + + // Create a subtle glow effect + const glowGeometry = new THREE.SphereGeometry(0.04, 12, 12); + const glowMaterial = new THREE.MeshBasicMaterial({ + color: 0x6495ED, + transparent: true, + opacity: 0.25 + }); + const glow = new THREE.Mesh(glowGeometry, glowMaterial); + + pinGroup.add(head); + pinGroup.add(glow); + + return pinGroup; + }; + + locationsData.forEach(location => { + const position = latLonToVector3(location.lat, location.lon, earthRadius); + const pin = createPin(); + + // Calculate the rotation to make the pin point towards the earth's center + const normal = position.clone().normalize(); + pin.position.copy(position); + pin.lookAt(new THREE.Vector3(0, 0, 0)); + pin.rotateX(Math.PI / 2); + + earthMesh.add(pin); + }); + + // After creating earthMesh, add this: + populateFilterDropdowns(flightRoutesData); + + // Extract flight path initialization into a separate function + function initializeFlightPaths(routes, targetMesh) { + const routeFrequencies = countRouteFrequencies(routes); + const processedRoutes = new Set(); + + // Track which route keys have planned vs completed flights (for dashed vs solid lines) + const routeKeyPlanned = {}; + const routeKeyCompleted = {}; + routes.forEach(route => { + const cities = [route.origin, route.destination].sort(); + const routeKey = `${cities[0]}-${cities[1]}`; + if (route.planned) { + routeKeyPlanned[routeKey] = true; + } else { + routeKeyCompleted[routeKey] = true; + } + }); + + // Get unique airlines and generate colors + const uniqueAirlines = [...new Set(routes.map(route => route.airline))].filter(Boolean); + const colors = generateDistinctColors(uniqueAirlines.length); + const airlineColors = Object.fromEntries( + uniqueAirlines.map((airline, index) => [airline, colors[index]]) + ); + // Add default color + airlineColors['default'] = 0x00ff00; + + routes.forEach(route => { + const cities = [route.origin, route.destination].sort(); + const routeKey = `${cities[0]}-${cities[1]}`; + + if (processedRoutes.has(routeKey)) return; + processedRoutes.add(routeKey); + + const originLoc = findLocationByName(route.origin); + const destLoc = findLocationByName(route.destination); + + if (!originLoc || !destLoc) { + console.warn(`Skipping route: ${route.origin} -> ${route.destination} due to missing location data`); + return; + } + + const startPoint = latLonToVector3(originLoc.lat, originLoc.lon, earthRadius); + const endPoint = latLonToVector3(destLoc.lat, destLoc.lon, earthRadius); + + const frequency = getRouteFrequency(route.origin, route.destination, routeFrequencies); + const numLines = Math.min(Math.max(frequency, 1), 10); + + const pathsPoints = createFlightPath(startPoint, endPoint, earthRadius, numLines); + const color = airlineColors[route.airline] || airlineColors.default; + + // Draw solid line for completed flights on this route + if (routeKeyCompleted[routeKey]) { + const flightLines = createFlightLines(pathsPoints, color, false); + flightLines.forEach(line => scene.add(line)); + } + // Draw dashed line for planned flights on this route + if (routeKeyPlanned[routeKey]) { + const plannedLines = createFlightLines(pathsPoints, color, true); + plannedLines.forEach(line => scene.add(line)); + } + }); + } + + // Initialize flight paths with all data + initializeFlightPaths(flightRoutesData, earthMesh); + + // Setup filter handlers + setupFilterHandlers(earthMesh, initializeFlightPaths, scene); + + // Initial Camera Position + camera.position.set(4, 8, 8); // Position camera above and to the side of Europe + camera.lookAt(0, 0, 0); // Look at the center of the Earth + controls.update(); // Update controls after changing camera position + + // Animation Loop with cloud rotation + let frameCount = 0; + function animate() { + requestAnimationFrame(animate); + + frameCount++; + + // Rotate clouds slightly faster than the Earth + if (cloudMesh) { + cloudMesh.rotation.y = earthMesh.rotation.y * 1.1; + } + + // Update Controls + controls.update(); + + renderer.render(scene, camera); + } + + // Handle Window Resize + function onWindowResize() { + if (!container) return; + camera.aspect = container.clientWidth / container.clientHeight; + camera.updateProjectionMatrix(); + renderer.setSize(container.clientWidth, container.clientHeight); + } + + window.addEventListener('resize', onWindowResize, false); + + // Start animation when textures are loaded + Promise.all([ + earthDayTexture, + earthNightTexture, + normalTexture, + specularTexture, + roughnessTexture, + bumpTexture, + cloudsTexture + ]).then(() => { + console.log("All textures loaded successfully"); + animate(); + }).catch(error => { + console.error('Error loading textures:', error); + // Start animation anyway to show at least something + animate(); + }); + + console.log("Three.js Earth initialized with enhanced visualization"); +} \ No newline at end of file diff --git a/assets/profile.png b/assets/profile.png new file mode 100644 index 0000000..411f2f6 Binary files /dev/null and b/assets/profile.png differ diff --git a/cname b/cname new file mode 100644 index 0000000..7e98ee0 --- /dev/null +++ b/cname @@ -0,0 +1 @@ +patrickfreyer.com \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..6927a91 --- /dev/null +++ b/css/style.css @@ -0,0 +1,1677 @@ +:root { + /* Core colors */ + --primary-color: #2563eb; + --secondary-color: #6366f1; + --accent-color: #8b5cf6; + --success-color: #10b981; + --warning-color: #f59e0b; + --error-color: #ef4444; + + /* Text colors */ + --text-color: #1f2937; + --text-light: #6b7280; + --text-lighter: #9ca3af; + + /* Background colors */ + --background: #ffffff; + --background-alt: #f3f4f6; + --background-dark: #111827; + + /* Gradients */ + --gradient-primary: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + --gradient-accent: linear-gradient(135deg, var(--secondary-color), var(--accent-color)); + + /* Shadows */ + --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.05); + --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); + + /* Transitions */ + --transition-fast: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); + --transition-normal: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + --transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + + /* Border Radius */ + --radius-sm: 0.375rem; + --radius-md: 0.5rem; + --radius-lg: 1rem; + --radius-full: 9999px; + + /* Spacing */ + --space-1: 0.25rem; + --space-2: 0.5rem; + --space-3: 0.75rem; + --space-4: 1rem; + --space-6: 1.5rem; + --space-8: 2rem; + --space-12: 3rem; + --space-16: 4rem; + + /* Z-index */ + --z-header: 1000; + --z-modal: 1100; + --z-tooltip: 1200; + + /* Container */ + --container-max: 1400px; + --container-padding: 2rem; + + --primary-color-rgb: 37, 99, 235; + --secondary-color-rgb: 99, 102, 241; + --accent-color-rgb: 139, 92, 246; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: 'Inter', sans-serif; + line-height: 1.6; + color: var(--text-color); + background-color: var(--background); +} + +.container { + max-width: 1200px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Header Styles */ +.header { + position: fixed; + top: 0; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + z-index: 1000; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); +} + +.nav-container { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + max-width: 1400px; + margin: 0 auto; +} + +.logo { + font-size: 1.75rem; + font-weight: 800; + background: linear-gradient(135deg, var(--primary-color), #6366f1); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-decoration: none; + transition: var(--transition); +} + +.logo:hover { + transform: scale(1.05); +} + +.mobile-menu-button { + display: none; + background: none; + border: none; + color: var(--text-color); + font-size: 1.25rem; + cursor: pointer; + padding: 0.5rem; + transition: var(--transition); + margin-left: auto; +} + +.mobile-menu-button:hover { + color: var(--primary-color); +} + +.nav-links { + display: flex; + gap: 2.5rem; + list-style: none; + margin: 0; + padding: 0; +} + +.nav-links a { + text-decoration: none; + color: var(--text-color); + font-weight: 500; + font-size: 1.05rem; + transition: var(--transition); + position: relative; + padding: 0.5rem 0; +} + +.nav-links a::before { + content: ''; + position: absolute; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 0; + height: 2px; + background: linear-gradient(135deg, var(--primary-color), #6366f1); + transition: width 0.3s ease; +} + +.nav-links a:hover::before { + width: 100%; +} + +.nav-links a:hover { + color: var(--primary-color); +} + +@media (min-width: 769px) { + .nav-container { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + .nav-header { + width: auto; + } +} + +@media (max-width: 768px) { + .mobile-menu-button { + display: block; + } + + .nav-container { + display: flex; + align-items: center; + padding: 1rem; + gap: 1rem; + } + + .nav-links { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + padding: 1rem; + flex-direction: column; + gap: 1rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.3); + } + + .nav-links.nav-open { + display: flex; + } + + .nav-links a { + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + } + + .nav-links a:hover { + background: rgba(99, 102, 241, 0.1); + } + + .timeline::before { + left: 24px; + } + + .timeline-item { + width: calc(100% - 48px); + margin-left: 48px !important; + margin-right: 0 !important; + } + + .timeline-item:nth-child(odd) .timeline-icon, + .timeline-item:nth-child(even) .timeline-icon { + left: -36px !important; + right: auto !important; + transform: translateY(-50%) !important; + } + + .timeline-content { + margin-left: 0; + } +} + +@media (max-width: 480px) { + .timeline { + padding: 1rem 0; + } + + .timeline::before { + left: 24px; + } + + .timeline-item { + width: calc(100% - 48px); + margin-left: 48px !important; + } + + .timeline-item .timeline-icon { + width: 2rem; + height: 2rem; + left: -36px; + } + + .timeline-icon i { + font-size: 0.875rem; + } + + .timeline-content { + padding: 1rem; + } +} + +@keyframes slideDown { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Hero Section Styles */ +.hero-section { + position: relative; + overflow: hidden; + min-height: 100vh; + padding: 6rem 0 4rem; + background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1), rgba(var(--secondary-color-rgb), 0.1)); + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; +} + +#earth-container { + position: relative; + width: 90%; + max-width: 600px; + height: 600px; + margin-top: 3rem; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + pointer-events: auto; +} + +#earth-container canvas { + display: block; + width: 100% !important; + height: 100% !important; + max-width: 100%; + max-height: 100%; +} + +.hero-section::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: radial-gradient(circle at 50% 50%, rgba(var(--accent-color-rgb), 0.1) 0%, transparent 50%); + z-index: 0; +} + +.hero-section .container { + position: relative; + z-index: 1; + width: 100%; + max-width: 1140px; + margin: 0 auto; + padding: 0 15px; + text-align: center; + pointer-events: none; +} + +.hero-content { + display: flex; + align-items: center; + justify-content: space-between; + gap: 4rem; + max-width: 1200px; + margin: 0 auto; + width: 100%; +} + +.hero-text { + flex: 1; + width: 100%; + pointer-events: auto; +} + +.hero-section h1 { + font-size: 3.5rem; + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--primary-color), var(--secondary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + animation: fadeIn 1s ease-out; +} + +.subtitle { + font-size: 1.5rem; + color: var(--text-light); + margin-bottom: 2rem; + animation: fadeIn 1s ease-out 0.3s backwards; +} + +.social-links { + display: flex; + gap: 1.5rem; + margin-top: 2rem; + animation: fadeIn 1s ease-out 0.6s backwards; + justify-content: center; + pointer-events: auto; +} + +.about-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 2rem; + animation: fadeIn 1s ease-out 0.9s backwards; +} + +.about-image-wrapper { + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +.about-image { + position: relative; + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +.profile-image { + width: 100%; + height: auto; + display: block; +} + +.about-text { + font-size: clamp(1rem, 2vw, 1.1rem); + text-align: left; +} + +.about-text p { + margin-bottom: 2rem; + line-height: 1.8; +} + +@media (max-width: 1024px) { + .hero-content { + gap: 2rem; + } + + .hero-section h1 { + font-size: 3rem; + } + + .subtitle { + font-size: 1.25rem; + } +} + +@media (max-width: 768px) { + .hero-content { + flex-direction: column; + text-align: center; + gap: 3rem; + } + + .hero-text { + order: 1; + } + + .about-content { + order: 2; + } + + .hero-section h1 { + font-size: 2.5rem; + } + + .about-image-wrapper { + max-width: 250px; + } +} + +@media (max-width: 480px) { + .hero-section { + padding: 6rem 0 3rem; + } + + .hero-section h1 { + font-size: 2rem; + } + + .subtitle { + font-size: 1.1rem; + } + + .about-image-wrapper { + max-width: 200px; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Section Styles */ +section { + padding: 6rem 0; +} + +section:nth-child(even) { + background-color: var(--background-alt); +} + +h2 { + font-size: clamp(2rem, 5vw, 2.5rem); + margin-bottom: 2rem; + color: var(--text-color); +} + +/* Projects Section */ +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); + gap: 2rem; + padding: 2rem 0; +} + +/* Contact Section */ +.contact-button { + display: inline-block; + padding: 1rem 2rem; + background-color: var(--primary-color); + color: white; + text-decoration: none; + border-radius: 8px; + font-weight: 500; + transition: var(--transition); + margin-top: 2rem; +} + +.contact-button:hover { + background-color: var(--secondary-color); + transform: translateY(-2px); +} + +/* Footer */ +.footer { + background-color: var(--text-color); + color: white; + padding: 2rem 0; + text-align: center; +} + +/* Animations */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.hero-section > div, +section > div { + animation: fadeIn 1s ease-out; +} + +/* About Section */ +.about-section { + padding: clamp(3rem, 8vh, 6rem) 0; +} + +.about-content { + display: grid; + grid-template-columns: 1fr 2fr; + gap: clamp(2rem, 5vw, 4rem); + align-items: start; +} + +.about-image-wrapper { + position: relative; + width: 100%; +} + +.about-image { + position: relative; + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +.profile-image { + width: 100%; + height: auto; +} + +.about-text { + font-size: clamp(1rem, 2vw, 1.1rem); +} + +.about-text p { + margin-bottom: 1.5rem; +} + +.about-highlights { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1.5rem; + margin-top: 2rem; +} + +.highlight { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + background-color: var(--background-alt); + border-radius: 0.5rem; + transition: var(--transition-normal); +} + +.highlight:hover { + transform: translateY(-2px); + box-shadow: var(--shadow-md); +} + +.highlight-icon { + font-size: 1.25rem; + color: var(--primary-color); + width: 2.5rem; + height: 2.5rem; + display: flex; + align-items: center; + justify-content: center; + background: var(--background); + border-radius: 50%; + box-shadow: var(--shadow-sm); + transition: var(--transition-normal); +} + +.highlight-icon i { + font-size: 1.25rem; + width: auto; + height: auto; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.highlight:hover .highlight-icon { + transform: scale(1.1); + color: var(--secondary-color); +} + +.highlight-text { + font-weight: 500; +} + +/* Updates Timeline */ +.timeline { + position: relative; + max-width: 800px; + margin: 0 auto; + padding: 2rem 0; +} + +.timeline::before { + content: ''; + position: absolute; + left: 50%; + transform: translateX(-50%); + width: 2px; + height: 100%; + background-color: var(--primary-color); +} + +.timeline-item { + position: relative; + margin-bottom: 2rem; + width: calc(50% - 2rem); +} + +.timeline-item:nth-child(odd) { + margin-left: auto; +} + +.timeline-icon { + position: absolute; + top: 50%; + width: 2.5rem; + height: 2.5rem; + background: var(--background); + border: 2px solid var(--primary-color); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + transform: translateY(-50%); + z-index: 1; +} + +.timeline-item:nth-child(odd) .timeline-icon { + right: calc(100% + 2rem); + transform: translate(50%, -50%); +} + +.timeline-item:nth-child(even) .timeline-icon { + left: calc(100% + 2rem); + transform: translate(-50%, -50%); +} + +.timeline-icon i { + color: var(--primary-color); + font-size: 1rem; + width: auto; + height: auto; + display: inline-flex; + align-items: center; + justify-content: center; +} + +.timeline-content { + background: var(--background); + padding: 1.5rem; + border-radius: 0.5rem; + box-shadow: var(--shadow-md); +} + +/* Projects Grid */ +.project-card { + position: relative; + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(10px) saturate(180%); + -webkit-backdrop-filter: blur(10px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.3); + border-radius: 1rem; + padding: 2rem; + transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); + overflow: hidden; +} + +.project-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, var(--primary-color), #6366f1); + transform: scaleX(0); + transform-origin: left; + transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1); +} + +.project-card:hover { + transform: translateY(-5px); + box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); +} + +.project-card:hover::before { + transform: scaleX(1); +} + +.project-icon { + font-size: 2.5rem; + width: 4rem; + height: 4rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: linear-gradient(135deg, rgba(var(--primary-color-rgb), 0.1)); + border-radius: var(--radius-lg); + margin-bottom: 1.5rem; + position: relative; +} + +.project-icon i { + background: linear-gradient(135deg, var(--primary-color)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.project-title { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 1rem; + background: linear-gradient(135deg, var(--text-color), #4b5563); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.project-description { + color: var(--text-light); + margin-bottom: 1.5rem; + line-height: 1.6; +} + +.project-tags { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 1.5rem; + margin-bottom: 3rem; +} + +.tag { + background: rgba(99, 102, 241, 0.1); + color: var(--primary-color); + padding: 0.5rem 1rem; + border-radius: 2rem; + font-size: 0.875rem; + font-weight: 500; + transition: var(--transition-normal); +} + +.tag:hover { + background: rgba(99, 102, 241, 0.2); + transform: translateY(-2px); +} + +.project-links { + display: flex; + gap: 1rem; + margin-top: 1.5rem; +} + +.project-link { + display: inline-flex; + align-items: center; + gap: 0.5rem; + color: var(--primary-color); + text-decoration: none; + font-weight: 500; + transition: var(--transition-normal); +} + +.project-link:hover { + transform: translateX(5px); +} + +.project-link i { + font-size: 1.25rem; +} + +/* Status Badges */ +.development-badge, +.stealth-badge { + position: absolute; + top: 2rem; + right: -4rem; + padding: 0.5rem 4rem; + transform: rotate(45deg); + font-size: 0.875rem; + font-weight: 500; + z-index: 2; + box-shadow: var(--shadow-md); + transition: all 0.3s ease; +} + +.development-badge { + background: linear-gradient(135deg, var(--success-color), #34d399); + color: white; +} + +.development-badge i { + margin-right: 0.5rem; + animation: spin 2s linear infinite; +} + +.stealth { + background: linear-gradient(135deg, + rgba(23, 23, 23, 0.95), + rgba(32, 32, 32, 0.95) + ); + backdrop-filter: blur(20px); + border: 1px solid rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +.stealth::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: + repeating-linear-gradient( + 45deg, + transparent, + transparent 10px, + rgba(99, 102, 241, 0.03) 10px, + rgba(99, 102, 241, 0.03) 20px + ); + pointer-events: none; +} + +.stealth::after { + content: ''; + position: absolute; + top: -50%; + left: -50%; + right: -50%; + bottom: -50%; + background: radial-gradient( + circle at center, + rgba(99, 102, 241, 0.1) 0%, + transparent 70% + ); + opacity: 0.5; + animation: pulse 4s infinite ease-in-out; + pointer-events: none; +} + +.stealth .project-title { + background: linear-gradient(135deg, #e2e8f0, #94a3b8); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + text-shadow: 0 0 30px rgba(255, 255, 255, 0.1); +} + +.stealth .project-description { + color: #94a3b8; +} + +.stealth .project-icon { + background: rgba(99, 102, 241, 0.1); + border: 1px solid rgba(99, 102, 241, 0.2); + box-shadow: 0 0 20px rgba(99, 102, 241, 0.1); +} + +.stealth .tag { + background: rgba(99, 102, 241, 0.15); + color: #818cf8; + border: 1px solid rgba(99, 102, 241, 0.2); +} + +.stealth-badge { + background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.8)); + color: white; + border: 1px solid rgba(255, 255, 255, 0.2); +} + +.stealth-badge i { + margin-right: 0.5rem; + animation: lockPulse 2s infinite ease-in-out; +} + +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +@keyframes lockPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.2); filter: brightness(1.2); } + 100% { transform: scale(1); } +} + +@keyframes pulse { + 0% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.2); opacity: 0.3; } + 100% { transform: scale(1); opacity: 0.5; } +} + +.stealth:hover { + transform: translateY(-5px); + box-shadow: 0 20px 40px rgba(99, 102, 241, 0.2); + border-color: rgba(99, 102, 241, 0.3); +} + +.stealth:hover::before { + animation: scanline 2s linear infinite; +} + +.stealth:hover .stealth-badge { + box-shadow: 0 0 20px rgba(99, 102, 241, 0.3); +} + +@keyframes scanline { + 0% { + background-position: 0% 0%; + } + 100% { + background-position: 200% 200%; + } +} + +/* Thoughts Section */ +.thoughts-grid { + display: grid; + gap: 2rem; + padding: 2rem 0; +} + +.thought-card { + background: var(--background); + border-radius: 1rem; + box-shadow: var(--shadow-md); + overflow: hidden; +} + +.thought-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.5rem; + cursor: pointer; + transition: var(--transition-normal); +} + +.thought-header:hover { + background: var(--background-alt); +} + +.thought-icon { + font-size: 1.5rem; +} + +.thought-meta { + flex: 1; +} + +.thought-category, +.thought-date { + font-size: 0.875rem; + color: var(--text-light); +} + +.expand-icon { + transition: var(--transition-normal); +} + +.expand-icon.expanded { + transform: rotate(180deg); +} + +.thought-content { + padding: 1.5rem; + border-top: 1px solid var(--background-alt); +} + +/* Social Section */ +.social-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + padding: 2rem 0; +} + +.social-card { + display: flex; + align-items: center; + gap: 1rem; + padding: 1.5rem; + background: var(--background); + border-radius: 1rem; + box-shadow: var(--shadow-md); + text-decoration: none; + color: var(--text-color); + transition: var(--transition-normal); +} + +.social-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +.social-icon { + font-size: 1.5rem; + width: 3rem; + height: 3rem; + display: inline-flex; + align-items: center; + justify-content: center; + background: rgba(var(--accent-color-rgb, var(--primary-color-rgb)), 0.1); + border-radius: var(--radius-lg); + transition: var(--transition-normal); +} + +.social-card:hover .social-icon { + transform: scale(1.1); +} + +.social-info { + flex: 1; +} + +.social-platform { + font-weight: 600; + margin-bottom: 0.25rem; +} + +.social-username { + font-size: 0.875rem; + color: var(--text-light); +} + +.social-arrow { + color: var(--accent-color, var(--primary-color)); +} + +/* Global Responsive Design */ +@media (max-width: 1024px) { + .container { + padding: 0 1.5rem; + } + + section { + padding: clamp(3rem, 6vh, 5rem) 0; + } +} + +@media (max-width: 768px) { + .about-content { + grid-template-columns: 1fr; + gap: 2.5rem; + } + + .about-image { + max-width: 250px; + } + + .about-text { + text-align: center; + } + + .about-highlights { + grid-template-columns: 1fr; + max-width: 500px; + margin: 2rem auto 0; + } + + .highlight { + justify-content: center; + } + + /* Global mobile optimizations */ + .container { + padding: 0 1rem; + } + + .projects-grid, + .social-grid, + .thoughts-grid { + grid-template-columns: 1fr; + max-width: 500px; + margin: 0 auto; + } + + .timeline::before { + left: 24px; + } + + .timeline-item { + width: calc(100% - 48px); + margin-left: 48px !important; + margin-right: 0 !important; + } + + .timeline-item:nth-child(odd) .timeline-icon, + .timeline-item:nth-child(even) .timeline-icon { + left: -36px !important; + right: auto !important; + transform: translateY(-50%) !important; + } + + .timeline-content { + margin-left: 0; + } +} + +@media (max-width: 480px) { + section { + padding: 2.5rem 0; + } + + .about-image { + max-width: 200px; + } + + .highlight { + padding: 0.75rem; + } + + .highlight-icon { + width: 2rem; + height: 2rem; + font-size: 1rem; + } + + .timeline { + padding: 1rem 0; + } + + .timeline::before { + left: 24px; + } + + .timeline-item { + width: calc(100% - 48px); + margin-left: 48px !important; + } + + .timeline-item .timeline-icon { + width: 2rem; + height: 2rem; + left: -36px; + } + + .timeline-icon i { + font-size: 0.875rem; + } + + .timeline-content { + padding: 1rem; + } +} + +/* Add smooth scrolling for all devices */ +html { + scroll-behavior: smooth; +} + +/* Improve touch targets for mobile */ +@media (hover: none) and (pointer: coarse) { + .nav-links a, + .social-links a, + .project-card, + .thought-header, + .social-card { + padding: 0.75rem; + min-height: 44px; + } + + .highlight { + min-height: 44px; + } +} + +/* Markdown Content Styles */ +.markdown-content { + line-height: 1.7; + color: var(--text-color); + margin-top: 1.5rem; +} + +.markdown-content h1, +.markdown-content h2, +.markdown-content h3, +.markdown-content h4, +.markdown-content h5, +.markdown-content h6 { + margin: 1.5rem 0 1rem; + font-weight: 600; + line-height: 1.3; +} + +.markdown-content h2 { + font-size: 1.5rem; + color: var(--primary-color); +} + +.markdown-content h3 { + font-size: 1.25rem; +} + +.markdown-content p { + margin: 1rem 0; +} + +.markdown-content ul, +.markdown-content ol { + margin: 1rem 0; + padding-left: 1.5rem; +} + +.markdown-content li { + margin: 0.5rem 0; +} + +.markdown-content code { + background: var(--background-alt); + padding: 0.2rem 0.4rem; + border-radius: 0.25rem; + font-size: 0.875em; + font-family: monospace; +} + +.markdown-content pre { + background: var(--background-alt); + padding: 1rem; + border-radius: 0.5rem; + overflow-x: auto; + margin: 1rem 0; +} + +.markdown-content pre code { + background: none; + padding: 0; +} + +.markdown-content blockquote { + border-left: 4px solid var(--primary-color); + padding-left: 1rem; + margin: 1rem 0; + color: var(--text-light); +} + +.markdown-content img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + margin: 1rem 0; +} + +.markdown-content a { + color: var(--primary-color); + text-decoration: none; +} + +.markdown-content a:hover { + text-decoration: underline; +} + +/* LinkedIn Section */ +.linkedin-section { + padding: 4rem 0; + background-color: var(--background-alt); +} + +.linkedin-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.linkedin-card { + background: var(--background); + border-radius: var(--radius-lg); + padding: 1.5rem; + box-shadow: var(--shadow-md); + transition: var(--transition-normal); +} + +.linkedin-card:hover { + transform: translateY(-5px); + box-shadow: var(--shadow-lg); +} + +.linkedin-card h3 { + font-size: 1.25rem; + margin-bottom: 1rem; + color: var(--text-color); +} + +.linkedin-embed-container { + position: relative; + width: 100%; + overflow: hidden; + border-radius: var(--radius-md); +} + +.linkedin-embed-container iframe { + width: 100%; + border: none; + transition: var(--transition-normal); + background: var(--background); +} + +@media (max-width: 768px) { + .linkedin-grid { + grid-template-columns: 1fr; + max-width: 600px; + margin: 2rem auto 0; + } + + .linkedin-card { + padding: 1rem; + } +} + +/* Icon Styles */ +.fa, +.fas, +.far, +.fab { + display: inline-flex; + align-items: center; + justify-content: center; + width: 1em; + height: 1em; + vertical-align: middle; + font-style: normal; +} + +/* Mobile Menu Icon */ +.mobile-menu-button i { + font-size: 1.5rem; + width: 1.5rem; + height: 1.5rem; + display: inline-flex; + align-items: center; + justify-content: center; +} + +/* Floating elements (keep them in the background) */ +.floating-elements { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; + pointer-events: none; +} + +/* Responsive adjustments for wider screens */ +@media (min-width: 992px) { + .hero-section { + flex-direction: row; + justify-content: space-between; + padding: 8rem 0 4rem; + } + + .hero-section .container { + flex-basis: 55%; + max-width: none; + margin: 0; + padding-left: 5%; + padding-right: 2%; + z-index: 1; + text-align: left; + } + + .hero-content { + /* Adjust if needed based on .container changes */ + } + + .hero-text { + /* Adjust if needed based on .container changes */ + } + + .social-links { + justify-content: flex-start; + } + + #earth-container { + position: relative; + flex-basis: 45%; + width: 45%; + height: 85vh; + min-height: 600px; + max-height: 1000px; + margin-top: 0; + max-width: none; + z-index: 2; + display: flex; + align-items: center; + justify-content: center; + } + + #earth-container canvas { + max-width: none; + max-height: none; + width: 100% !important; + height: 100% !important; + } + + .hero-section .container { + pointer-events: auto; + z-index: 1; + } +} + +/* Remove the specific max-width media query as defaults handle mobile now */ +/* @media (max-width: 991.98px) { ... } */ + +.cta-button { + margin-top: 2rem; + animation: fadeIn 1s ease-out 0.9s backwards; + pointer-events: auto; +} + +.cta-button .btn { + display: inline-flex; + align-items: center; + gap: 0.75rem; + padding: 1rem 2rem; + font-size: 1.125rem; + font-weight: 600; + color: white; + background: var(--gradient-primary); + border-radius: var(--radius-full); + text-decoration: none; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 4px 6px -1px rgba(var(--primary-color-rgb), 0.2), + 0 2px 4px -1px rgba(var(--primary-color-rgb), 0.1); + position: relative; + overflow: hidden; +} + +.cta-button .btn::before { + content: ''; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0)); + transform: translateX(-100%) rotate(45deg); + transition: transform 0.6s cubic-bezier(0.4, 0, 0.2, 1); +} + +.cta-button .btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 15px -3px rgba(var(--primary-color-rgb), 0.2), + 0 4px 6px -2px rgba(var(--primary-color-rgb), 0.1); +} + +.cta-button .btn:hover::before { + transform: translateX(100%) rotate(45deg); +} + +.cta-button .btn:active { + transform: translateY(0); +} + +.cta-button .btn i { + font-size: 1.25rem; +} + +@media (max-width: 768px) { + .cta-button .btn { + padding: 0.875rem 1.75rem; + font-size: 1rem; + } +} + +/* Filter Panel Styles */ +.filter-panel { + position: absolute; + top: 20px; + left: 20px; + background: rgba(23, 23, 23, 0.85); + padding: 0; + border-radius: 12px; + color: white; + z-index: 1000; + max-width: 300px; + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + transition: transform 0.3s ease, width 0.3s ease; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.filter-panel.collapsed { + transform: translateX(-280px); +} + +.filter-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.filter-header h3 { + margin: 0; + font-size: 1.1em; + font-weight: 600; + color: #fff; +} + +.toggle-filters-btn { + background: none; + border: none; + color: #fff; + cursor: pointer; + padding: 5px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.filter-panel.collapsed .toggle-filters-btn i { + transform: rotate(180deg); +} + +.filter-content { + padding: 20px; + overflow-y: auto; + max-height: calc(100vh - 200px); +} + +.filter-group { + margin-bottom: 20px; +} + +.filter-group label { + display: block; + margin-bottom: 8px; + color: #e2e8f0; + font-size: 0.9em; + font-weight: 500; +} + +.filter-group select { + width: 100%; + padding: 8px 12px; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 6px; + color: white; + outline: none; + font-size: 0.9em; + transition: all 0.2s ease; +} + +.filter-group select:hover { + border-color: rgba(255, 255, 255, 0.3); +} + +.filter-group select:focus { + border-color: var(--primary-color); + box-shadow: 0 0 0 2px rgba(var(--primary-color-rgb), 0.2); +} + +.filter-group select option { + background: #1a1a1a; + color: white; + padding: 8px; +} + +.filter-group select[multiple] { + min-height: 100px; +} + +.filter-actions { + display: flex; + gap: 10px; + margin-top: 20px; +} + +.filter-button { + flex: 1; + padding: 8px 16px; + border: none; + border-radius: 6px; + font-weight: 500; + font-size: 0.9em; + cursor: pointer; + transition: all 0.2s ease; +} + +.filter-button:first-child { + background: var(--primary-color); + color: white; +} + +.filter-button:last-child { + background: rgba(255, 255, 255, 0.1); + color: white; +} + +.filter-button:hover { + transform: translateY(-1px); + filter: brightness(1.1); +} + +.filter-button:active { + transform: translateY(0); +} + +@media (max-width: 768px) { + .filter-panel { + top: 10px; + left: 10px; + max-width: calc(100% - 20px); + } + + .filter-panel.collapsed { + transform: translateX(calc(-100% + 40px)); + } +} \ No newline at end of file diff --git a/earth-ar.html b/earth-ar.html new file mode 100644 index 0000000..7e21601 --- /dev/null +++ b/earth-ar.html @@ -0,0 +1,166 @@ + + + + + + Earth AR Visualization + + + +
+
+

🌍 Earth AR Visualization

+

Experience your flight routes in augmented reality!

+ +
+ +
+
+

Loading AR...

+

Please wait while we initialize the AR session

+
+ +
+ + +
+ + + + + \ No newline at end of file diff --git a/earthGlobeVisualization.md b/earthGlobeVisualization.md new file mode 100644 index 0000000..90d2354 --- /dev/null +++ b/earthGlobeVisualization.md @@ -0,0 +1,222 @@ +# Earth Globe Visualization in Three.js + +This document provides a detailed explanation of how the 3D Earth globe is implemented using Three.js, including all its layers, lighting systems, and visual effects. + +## Layer Structure + +The Earth visualization consists of multiple layered spheres, each serving a specific purpose: + +### 1. Base Earth Layer (Radius: 1.0) +```javascript +const globeGeometry = new THREE.SphereGeometry(1, 64, 64); +const globeMaterial = new THREE.MeshPhongMaterial({ + map: earthTexture, // Day texture (land, seas, etc.) + normalMap: normalTexture, // Surface detail/bump mapping + specularMap: specularTexture,// Reflectivity map + bumpMap: bumpTexture, // Additional surface detail + bumpScale: 0.05, // Intensity of the bump effect + specular: new THREE.Color(0x333333), // Color of specular highlights + shininess: 25 // Size/sharpness of specular highlights +}); +``` + +**Texture Maps Used:** +- `earthTexture`: High-resolution day map showing continents, oceans +- `normalTexture`: Adds surface detail through normal mapping +- `specularTexture`: Controls which areas are reflective +- `bumpTexture`: Provides additional surface relief + +### 2. Night Lights Layer (Radius: 1.001) +```javascript +const nightGeometry = new THREE.SphereGeometry(1.001, 64, 64); +const nightMaterial = new THREE.MeshBasicMaterial({ + map: nightTexture, // City lights texture + blending: THREE.AdditiveBlending, + transparent: true, + opacity: state.isDaylight ? 0 : 0.8, + depthWrite: false +}); +``` + +**Key Features:** +- Slightly larger radius to prevent z-fighting +- Additive blending for realistic light glow +- Dynamic opacity based on day/night state +- No depth writing to prevent visual artifacts + +### 3. Cloud Layer (Radius: 1.008) +```javascript +const cloudGeometry = new THREE.SphereGeometry(1.008, 64, 64); +const cloudMaterial = new THREE.MeshPhongMaterial({ + map: cloudsTexture, + transparent: true, + opacity: state.isDaylight ? 0.3 : 0.1, + depthWrite: false, + side: THREE.DoubleSide, + blending: THREE.NormalBlending +}); +``` + +**Characteristics:** +- Outermost layer for cloud coverage +- Semi-transparent for realistic cloud effect +- Double-sided rendering +- Dynamic opacity based on day/night state + +## Lighting System + +The Earth uses a complex lighting setup with three light types: + +### 1. Ambient Light +```javascript +const ambientLight = new THREE.AmbientLight(0xffffff, state.isDaylight ? 1.0 : 0.1); +``` +- Provides base illumination +- Intensity varies with day/night state +- Ensures shadows aren't too dark + +### 2. Directional Light (Sun) +```javascript +const directionalLight = new THREE.DirectionalLight(0xffffff, state.isDaylight ? 2.0 : 0.15); +directionalLight.position.set(5, 3, 5); +``` +- Simulates sunlight +- Creates realistic shadows and highlights +- Position determines day/night regions + +### 3. Hemisphere Light +```javascript +const hemisphereLight = new THREE.HemisphereLight( + 0xffffff, // Sky color + 0x444444, // Ground color + state.isDaylight ? 1.0 : 0 +); +``` +- Provides subtle atmospheric lighting +- Improves realism of day lighting +- Automatically disabled at night + +## Day/Night Transition System + +The visualization includes a smooth transition system between day and night states: + +```javascript +function transitionDayNight(state, globeMaterial) { + // Target values for day/night transition + const targetValues = { + nightOpacity: state.isDaylight ? 0 : 0.8, + cloudOpacity: state.isDaylight ? 0.4 : 0.2, + ambientIntensity: state.isDaylight ? 1.0 : 0.1, + directionalIntensity: state.isDaylight ? 2.0 : 0.15, + hemisphereIntensity: state.isDaylight ? 1.0 : 0 + }; + + // Smooth easing function + const eased = progress < 0.5 + ? 2 * progress * progress + : 1 - Math.pow(-2 * progress + 2, 2) / 2; + + // Apply transitions + updateLightingValues(eased, currentValues, targetValues); +} +``` + +## Visual Effects and Post-Processing + +### 1. Atmosphere Effect +The atmosphere is simulated through a combination of: +- Cloud layer transparency +- Hemisphere lighting +- Bloom post-processing + +### 2. Bloom Effect +```javascript +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.5, // Intensity + 0.4, // Radius + 0.85 // Threshold +); +``` +- Adds realistic glow to bright areas +- Enhances night lights +- Creates subtle atmospheric scatter + +## Performance Considerations + +### 1. Geometry Optimization +- Using appropriate polygon counts (64 segments) +- Shared geometries between instances +- Proper disposal of unused resources + +### 2. Texture Management +```javascript +const textureLoader = new THREE.TextureLoader(); +// Preload and reuse textures +const earthTexture = textureLoader.load('./textures/earth_albedo.jpg', onTextureLoaded); +``` +- Preloading of textures +- Proper texture compression +- Mipmap generation for distant views + +### 3. Render Layer Organization +- Proper layer ordering +- Strategic use of transparency +- Depth testing optimization + +## Required Textures + +For a complete Earth visualization, you need these texture maps: + +1. **Day Map** (`earth_albedo.jpg`) + - Color texture of Earth during daylight + - Shows continents, oceans, terrain + +2. **Night Map** (`earth_night.jpg`) + - City lights and illuminated areas + - Used for night side visualization + +3. **Normal Map** (`earth_normal.jpg`) + - Surface detail and terrain information + - Enhances visual depth + +4. **Specular Map** (`earth_specular.jpg`) + - Controls surface reflectivity + - Different for land and water + +5. **Cloud Map** (`earth_clouds.jpg`) + - Cloud coverage patterns + - Semi-transparent white clouds + +6. **Bump Map** (`earth_bump.jpg`) + - Additional surface detail + - Enhances terrain visualization + +## Implementation Tips + +1. **Texture Resolution** + - Use power-of-two textures (2048x1024, 4096x2048) + - Consider device performance for texture size + - Implement progressive loading for mobile + +2. **Layer Order** + - Base Earth (1.0) + - Night Lights (1.001) + - Clouds (1.008) + - Maintain small radius differences + +3. **Lighting Setup** + - Position lights for realistic shadows + - Adjust intensities for desired contrast + - Consider performance vs. quality + +4. **Animation** + - Smooth cloud layer rotation + - Day/night transition timing + - Performance-conscious update rate + +## Resources + +- [Earth Texture Maps](https://visibleearth.nasa.gov/collection/1484/blue-marble) +- [Normal Map Generation](http://cpetry.github.io/NormalMap-Online/) +- [Three.js Earth Example](https://threejs.org/examples/#webgl_materials_earth) \ No newline at end of file diff --git a/flightRouteVisualization.md b/flightRouteVisualization.md new file mode 100644 index 0000000..d62dc0e --- /dev/null +++ b/flightRouteVisualization.md @@ -0,0 +1,244 @@ +# Flight Globe Visualization with Three.js + +This document explains the implementation of a 3D flight path visualization using Three.js, similar to the one used in flight tracking applications. + +## Overview + +The visualization creates an interactive 3D globe with flight paths rendered as curved lines between airports. The implementation includes multiple visual layers, dynamic lighting, and smooth animations. + +## Core Components + +### 1. Globe Structure + +The globe consists of multiple layers: +- Base Earth layer with day texture +- Night lights layer for city illumination +- Cloud layer with transparency +- Flight paths rendered on top + +```javascript +const globeGeometry = new THREE.SphereGeometry(1, 64, 64); +const globeMaterial = new THREE.MeshPhongMaterial({ + map: earthTexture, + normalMap: normalTexture, + specularMap: specularTexture, + bumpMap: bumpTexture, + bumpScale: 0.05, + specular: new THREE.Color(0x333333), + shininess: 25 +}); +``` + +### 2. Flight Path Generation + +Each flight path is created using these key steps: + +1. **Coordinate Conversion** +```javascript +// Convert airport lat/long to 3D vectors +const startPoint = latLngToVector3(fromLat, fromLng); +const endPoint = latLngToVector3(toLat, toLng); +``` + +2. **Path Curvature** +```javascript +// Calculate height scale based on distance +const maxHeightScale = 0.08; +const baseScale = Math.atan(distance) / (Math.PI / 2) * maxHeightScale; + +// Generate curved path points +const points = []; +for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const point = startPoint.clone().normalize(); + point.lerp(endPoint.clone().normalize(), t).normalize(); + const heightScale = 1 + baseScale * Math.sin(Math.PI * t); + point.multiplyScalar(heightScale); + points.push(point); +} +``` + +3. **Smooth Curve Creation** +```javascript +const curve = new THREE.CatmullRomCurve3(points, false, 'catmullrom', 0.5); +``` + +### 3. Multiple Flight Lines + +For busy routes, multiple parallel lines are created to represent frequency: + +```javascript +// Calculate perpendicular direction for parallel lines +const pathDirection = curveEnd.clone().sub(curveStart).normalize(); +const globeNormal = curveStart.clone().add(curveEnd).normalize(); +const perpDirection = pathDirection.clone().cross(globeNormal).normalize(); + +// Create offset lines +for (let i = 0; i < numLines; i++) { + const offset = perpDirection.clone().multiplyScalar(0.001 * (i - (numLines - 1) / 2)); + const points = basePoints.map(point => { + return point.clone().add(offset); + }); + // Create line with offset +} +``` + +### 4. Visual Effects + +#### Flight Path Material +```javascript +const lineMaterial = new THREE.LineBasicMaterial({ + color: airlineColors[airline] || 0xffffff, + transparent: true, + opacity: 0.8, + linewidth: state.lineWidth, + depthTest: true, + depthWrite: false +}); +``` + +#### Airport Markers +```javascript +const dotGeometry = new THREE.SphereGeometry(0.005, 16, 16); +const dotMaterial = new THREE.MeshBasicMaterial({ + color: airlineColors[airline] || 0xffffff, + transparent: true, + opacity: 0.8 +}); +``` + +### 5. Post-Processing Effects + +The visualization uses post-processing for enhanced visual appeal: + +```javascript +// Setup effect composer +const composer = new EffectComposer(renderer); +const renderPass = new RenderPass(scene, camera); +composer.addPass(renderPass); + +// Add bloom effect +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.5, // Bloom intensity + 0.4, // Bloom radius + 0.85 // Bloom threshold +); +composer.addPass(bloomPass); +``` + +## Performance Optimizations + +1. **Efficient Geometry** +```javascript +const lineGeometry = new THREE.BufferGeometry().setFromPoints(points); +``` + +2. **Visibility Management** +```javascript +// Update visibility based on filters +flightPath.line.visible = visible; +flightPath.additionalLines.forEach(line => line.visible = visible); +``` + +3. **Resource Cleanup** +```javascript +// Dispose of geometries and materials when removing flight paths +path.line.geometry.dispose(); +path.line.material.dispose(); +``` + +## Implementation Guide + +To implement this visualization in your own project: + +1. **Basic Setup** +```javascript +const scene = new THREE.Scene(); +const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); +const renderer = new THREE.WebGLRenderer({ antialias: true }); +``` + +2. **Create Earth** +```javascript +const globeGeometry = new THREE.SphereGeometry(1, 64, 64); +const globeMaterial = new THREE.MeshPhongMaterial({ + map: earthTexture, + normalMap: normalTexture, + specularMap: specularTexture, + bumpMap: bumpTexture +}); +const globe = new THREE.Mesh(globeGeometry, globeMaterial); +scene.add(globe); +``` + +3. **Add Flight Paths** +```javascript +function createFlightPath(startPoint, endPoint) { + const points = []; + const numPoints = 50; + const maxHeight = 0.08; + + for (let i = 0; i <= numPoints; i++) { + const t = i / numPoints; + const point = startPoint.clone().normalize(); + point.lerp(endPoint.clone().normalize(), t).normalize(); + const heightScale = 1 + maxHeight * Math.sin(Math.PI * t); + point.multiplyScalar(heightScale); + points.push(point); + } + + const curve = new THREE.CatmullRomCurve3(points); + const geometry = new THREE.BufferGeometry().setFromPoints(curve.getPoints(50)); + const material = new THREE.LineBasicMaterial({ + color: 0xffffff, + transparent: true, + opacity: 0.8 + }); + + return new THREE.Line(geometry, material); +} +``` + +4. **Setup Post-Processing** +```javascript +const composer = new EffectComposer(renderer); +const renderPass = new RenderPass(scene, camera); +const bloomPass = new UnrealBloomPass( + new THREE.Vector2(window.innerWidth, window.innerHeight), + 1.5, 0.4, 0.85 +); +composer.addPass(renderPass); +composer.addPass(bloomPass); +``` + +5. **Animation Loop** +```javascript +function animate() { + requestAnimationFrame(animate); + // Update any animations + composer.render(); +} +``` + +## Key Tips for Success + +1. **Smooth Curves**: Use Catmull-Rom curves for natural-looking flight paths +2. **Multiple Lines**: Implement parallel lines for busy routes +3. **Visual Effects**: Add bloom and glow effects for enhanced appearance +4. **Performance**: Use BufferGeometry and implement proper cleanup +5. **Depth Testing**: Configure proper depth testing and transparency +6. **Animation**: Implement smooth transitions and animations + +## Required Dependencies + +- Three.js +- EffectComposer +- UnrealBloomPass +- OrbitControls (for interaction) + +## Resources + +- [Three.js Documentation](https://threejs.org/docs/) +- [WebGL Fundamentals](https://webglfundamentals.org/) +- [Three.js Examples](https://threejs.org/examples/) \ No newline at end of file diff --git a/flights-ar.html b/flights-ar.html new file mode 100644 index 0000000..cea3071 --- /dev/null +++ b/flights-ar.html @@ -0,0 +1,325 @@ +--- +layout: default +title: Flights AR Experience +permalink: /flights/ar/ +--- + + + + + + + Flights AR Experience + + + +
+ ← Back to Flights + +
+

🌍 Flight Routes in AR

+

Experience your flight routes in augmented reality!

+ +
+ +
+
+

Loading AR...

+

Please wait while we initialize the AR session

+
+ +
+ + +
+ + + + + + \ No newline at end of file diff --git a/flights.html b/flights.html new file mode 100644 index 0000000..170de8f --- /dev/null +++ b/flights.html @@ -0,0 +1,267 @@ +--- +layout: default +title: Flights +permalink: /flights/ +--- + +
+
+
+

Filters

+ +
+
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ + +
+ + + +
+ +
+
+ + + + + + \ No newline at end of file diff --git a/flights/stats.html b/flights/stats.html new file mode 100644 index 0000000..0804a02 --- /dev/null +++ b/flights/stats.html @@ -0,0 +1,338 @@ +--- +layout: default +title: Flight Statistics +permalink: /flights/stats/ +--- + + + +
+
+

Flight Statistics

+

Comprehensive analysis of travel patterns and distances

+
+ +
+
+
🌍
+
+
Calculating...
+
Total Distance
+
kilometers
+
+
+ +
+
✈️
+
+
Calculating...
+
Total Flights
+
flights
+
+
+ +
+
🏛️
+
+
Calculating...
+
Countries Visited
+
countries
+
+
+ +
+
📅
+
+
Calculating...
+
Years of Travel
+
years
+
+
+
+ +
+
+

Distance and Flights by Year

+
+ +
+
+ +
+

Distance by Companion

+
+ +
+
+ +
+

Cumulative Distance Over Time

+
+ +
+
+ +
+

Distance by Month

+
+ +
+
+ +
+

Top 10 Airports Over Time

+
+ +
+
+
+ +
+
+

Top Destinations

+
+ +
+
+ +
+

Most Frequent Airlines

+
+ +
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/index.html b/index.html index be5d95a..81b9c92 100644 --- a/index.html +++ b/index.html @@ -1 +1,68 @@ -"Hello World" \ No newline at end of file +--- +layout: default +--- + + + + + + + Patrick Freyer | Personal Website + + + + + + + + + +
+ +
+ +
+ {% include hero.html %} + + {% assign profiles = site.data.social %} + + {% include about.html %} + {% include social.html profiles=profiles %} +
+ + + + + + \ No newline at end of file diff --git a/js/flight-stats.js b/js/flight-stats.js new file mode 100644 index 0000000..bc3cbb8 --- /dev/null +++ b/js/flight-stats.js @@ -0,0 +1,726 @@ +// Flight Statistics Calculator +class FlightStatsCalculator { + constructor(locationsData, flightRoutesData) { + this.locations = locationsData; + this.flights = flightRoutesData; + this.locationMap = this.createLocationMap(); + this.init(); + } + + createLocationMap() { + const map = {}; + this.locations.forEach(location => { + map[location.name] = { + lat: location.lat, + lon: location.lon + }; + }); + return map; + } + + // Calculate distance between two points using Haversine formula + calculateDistance(lat1, lon1, lat2, lon2) { + const R = 6371; // Earth's radius in kilometers + const dLat = this.toRadians(lat2 - lat1); + const dLon = this.toRadians(lon2 - lon1); + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + toRadians(degrees) { + return degrees * (Math.PI / 180); + } + + // Calculate total distance for all flights + calculateTotalDistance() { + let totalDistance = 0; + this.flights.forEach(flight => { + const origin = this.locationMap[flight.origin]; + const destination = this.locationMap[flight.destination]; + + if (origin && destination) { + const distance = this.calculateDistance( + origin.lat, origin.lon, + destination.lat, destination.lon + ); + totalDistance += distance; + } + }); + return Math.round(totalDistance*1.07); + } + + // Get statistics by year + getStatsByYear() { + const yearStats = {}; + this.flights.forEach(flight => { + const year = flight.year; + if (!yearStats[year]) { + yearStats[year] = { distance: 0, count: 0 }; + } + + const origin = this.locationMap[flight.origin]; + const destination = this.locationMap[flight.destination]; + + if (origin && destination) { + const distance = this.calculateDistance( + origin.lat, origin.lon, + destination.lat, destination.lon + ); + yearStats[year].distance += distance; + yearStats[year].count += 1; + } + }); + return yearStats; + } + + // Get statistics by companion + getStatsByCompanion() { + const companionStats = {}; + this.flights.forEach(flight => { + if (flight.travelers) { + flight.travelers.forEach(traveler => { + if (traveler !== "Patrick") { // Exclude self + if (!companionStats[traveler]) { + companionStats[traveler] = { distance: 0, count: 0 }; + } + + const origin = this.locationMap[flight.origin]; + const destination = this.locationMap[flight.destination]; + + if (origin && destination) { + const distance = this.calculateDistance( + origin.lat, origin.lon, + destination.lat, destination.lon + ); + companionStats[traveler].distance += distance; + companionStats[traveler].count += 1; + } + } + }); + } + }); + return companionStats; + } + + // Get cumulative distance data over time + getCumulativeDistanceData() { + const sortedFlights = this.flights + .map(flight => { + const origin = this.locationMap[flight.origin]; + const destination = this.locationMap[flight.destination]; + if (origin && destination) { + return { + ...flight, + distance: this.calculateDistance( + origin.lat, origin.lon, + destination.lat, destination.lon + ) + }; + } + return null; + }) + .filter(flight => flight !== null) + .sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + if (a.month !== b.month) return a.month - b.month; + return (a.day || 1) - (b.day || 1); + }); + + let cumulativeDistance = 0; + const data = []; + + sortedFlights.forEach(flight => { + cumulativeDistance += flight.distance; + const dateStr = flight.day ? + `${flight.year}-${String(flight.month).padStart(2, '0')}-${String(flight.day).padStart(2, '0')}` : + `${flight.year}-${String(flight.month).padStart(2, '0')}-01`; + data.push({ + date: dateStr, + cumulative: Math.round(cumulativeDistance) + }); + }); + + return data; + } + + // Get statistics by month across all years + getStatsByMonth() { + const monthStats = {}; + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + + this.flights.forEach(flight => { + const monthIndex = flight.month - 1; + const monthName = monthNames[monthIndex]; + + if (!monthStats[monthName]) { + monthStats[monthName] = { distance: 0, count: 0 }; + } + + const origin = this.locationMap[flight.origin]; + const destination = this.locationMap[flight.destination]; + + if (origin && destination) { + const distance = this.calculateDistance( + origin.lat, origin.lon, + destination.lat, destination.lon + ); + monthStats[monthName].distance += distance; + monthStats[monthName].count += 1; + } + }); + + // Ensure all months are present + monthNames.forEach(month => { + if (!monthStats[month]) { + monthStats[month] = { distance: 0, count: 0 }; + } + }); + + return monthStats; + } + + // Get airport visit counts over time + getAirportTimelineData() { + const airportStats = {}; + const sortedFlights = this.flights.sort((a, b) => { + if (a.year !== b.year) return a.year - b.year; + return a.month - b.month; + }); + + sortedFlights.forEach(flight => { + // Count both origin and destination + [flight.origin, flight.destination].forEach(airport => { + if (!airportStats[airport]) { + airportStats[airport] = { count: 0, firstYear: flight.year }; + } + airportStats[airport].count += 1; + }); + }); + + // Get top 10 airports by visit count + const topAirports = Object.entries(airportStats) + .sort(([,a], [,b]) => b.count - a.count) + .slice(0, 10); + + return topAirports; + } + + // Get unique countries visited + getUniqueCountries() { + const countries = new Set(); + this.flights.forEach(flight => { + countries.add(flight.origin); + countries.add(flight.destination); + }); + return countries.size; + } + + // Get top destinations + getTopDestinations() { + const destinations = {}; + this.flights.forEach(flight => { + const dest = flight.destination; + destinations[dest] = (destinations[dest] || 0) + 1; + }); + + return Object.entries(destinations) + .sort(([,a], [,b]) => b - a) + .slice(0, 10); + } + + // Get top airlines + getTopAirlines() { + const airlines = {}; + this.flights.forEach(flight => { + const airline = flight.airline; + airlines[airline] = (airlines[airline] || 0) + 1; + }); + + return Object.entries(airlines) + .sort(([,a], [,b]) => b - a) + .slice(0, 10); + } + + // Update the UI with calculated statistics + updateUI() { + const totalDistance = this.calculateTotalDistance(); + const totalFlights = this.flights.length; + const countriesVisited = this.getUniqueCountries(); + const years = new Set(this.flights.map(f => f.year)).size; + + document.getElementById('total-distance').textContent = totalDistance.toLocaleString(); + document.getElementById('total-flights').textContent = totalFlights; + document.getElementById('countries-visited').textContent = countriesVisited; + document.getElementById('years-traveled').textContent = years; + + this.createYearChart(); + this.createCompanionChart(); + this.createCumulativeChart(); + this.createMonthlyChart(); + this.createAirportsTimelineChart(); + this.updateTopDestinations(); + this.updateTopAirlines(); + } + + createYearChart() { + const yearStats = this.getStatsByYear(); + const years = Object.keys(yearStats).sort(); + const distances = years.map(year => Math.round(yearStats[year].distance)); + const flightCounts = years.map(year => yearStats[year].count); + + const ctx = document.getElementById('year-chart').getContext('2d'); + new Chart(ctx, { + type: 'bar', + data: { + labels: years, + datasets: [ + { + label: 'Distance (km)', + data: distances, + backgroundColor: 'rgba(37, 99, 235, 0.8)', + borderColor: 'rgba(37, 99, 235, 1)', + borderWidth: 1, + borderRadius: 4, + yAxisID: 'y' + }, + { + label: 'Number of Flights', + data: flightCounts, + backgroundColor: 'rgba(16, 185, 129, 0.8)', + borderColor: 'rgba(16, 185, 129, 1)', + borderWidth: 1, + borderRadius: 4, + yAxisID: 'y1' + } + ] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: true, + position: 'top', + labels: { + color: '#6b7280', + font: { + family: 'Inter' + }, + usePointStyle: true, + padding: 20 + } + }, + tooltip: { + mode: 'index', + intersect: false, + callbacks: { + label: function(context) { + if (context.dataset.label === 'Distance (km)') { + return `Distance: ${context.parsed.y.toLocaleString()} km`; + } else { + return `Flights: ${context.parsed.y}`; + } + } + } + } + }, + scales: { + y: { + type: 'linear', + display: true, + position: 'left', + beginAtZero: true, + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + }, + title: { + display: true, + text: 'Distance (km)', + color: '#6b7280', + font: { + family: 'Inter' + } + } + }, + y1: { + type: 'linear', + display: true, + position: 'right', + beginAtZero: true, + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + drawOnChartArea: false + }, + title: { + display: true, + text: 'Number of Flights', + color: '#6b7280', + font: { + family: 'Inter' + } + } + }, + x: { + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + display: false + } + } + } + } + }); + } + + createCompanionChart() { + const companionStats = this.getStatsByCompanion(); + const companions = Object.keys(companionStats); + const distances = companions.map(companion => Math.round(companionStats[companion].distance)); + + const ctx = document.getElementById('companion-chart').getContext('2d'); + new Chart(ctx, { + type: 'doughnut', + data: { + labels: companions, + datasets: [{ + data: distances, + backgroundColor: [ + 'rgba(37, 99, 235, 0.8)', + 'rgba(99, 102, 241, 0.8)', + 'rgba(139, 92, 246, 0.8)', + 'rgba(16, 185, 129, 0.8)', + 'rgba(245, 158, 11, 0.8)', + 'rgba(239, 68, 68, 0.8)', + 'rgba(107, 114, 128, 0.8)', + 'rgba(156, 163, 175, 0.8)' + ], + borderColor: [ + 'rgba(37, 99, 235, 1)', + 'rgba(99, 102, 241, 1)', + 'rgba(139, 92, 246, 1)', + 'rgba(16, 185, 129, 1)', + 'rgba(245, 158, 11, 1)', + 'rgba(239, 68, 68, 1)', + 'rgba(107, 114, 128, 1)', + 'rgba(156, 163, 175, 1)' + ], + borderWidth: 2 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + position: 'bottom', + labels: { + color: '#6b7280', + font: { + family: 'Inter' + }, + padding: 20 + } + } + }, + cutout: '60%' + } + }); + } + + createCumulativeChart() { + const cumulativeData = this.getCumulativeDistanceData(); + const labels = cumulativeData.map(d => d.date); + const distances = cumulativeData.map(d => d.cumulative); + + const ctx = document.getElementById('cumulative-chart').getContext('2d'); + new Chart(ctx, { + type: 'line', + data: { + labels: labels, + datasets: [{ + label: 'Cumulative Distance (km)', + data: distances, + borderColor: 'rgba(37, 99, 235, 1)', + backgroundColor: 'rgba(37, 99, 235, 0.1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointBackgroundColor: 'rgba(37, 99, 235, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + pointRadius: 4, + pointHoverRadius: 6 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + callbacks: { + label: function(context) { + return `Total Distance: ${context.parsed.y.toLocaleString()} km`; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + }, + callback: function(value) { + return (value/1000).toFixed(0) + 'K'; + } + }, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + } + }, + x: { + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + }, + maxTicksLimit: 10 + }, + grid: { + display: false + } + } + } + } + }); + } + + createMonthlyChart() { + const monthStats = this.getStatsByMonth(); + const monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']; + const distances = monthNames.map(month => Math.round(monthStats[month].distance)); + + const ctx = document.getElementById('monthly-chart').getContext('2d'); + new Chart(ctx, { + type: 'bar', + data: { + labels: monthNames, + datasets: [{ + label: 'Distance (km)', + data: distances, + backgroundColor: [ + 'rgba(37, 99, 235, 0.8)', 'rgba(99, 102, 241, 0.8)', 'rgba(139, 92, 246, 0.8)', + 'rgba(16, 185, 129, 0.8)', 'rgba(5, 150, 105, 0.8)', 'rgba(245, 158, 11, 0.8)', + 'rgba(251, 191, 36, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(220, 38, 127, 0.8)', + 'rgba(107, 114, 128, 0.8)', 'rgba(75, 85, 99, 0.8)', 'rgba(55, 65, 81, 0.8)' + ], + borderColor: [ + 'rgba(37, 99, 235, 1)', 'rgba(99, 102, 241, 1)', 'rgba(139, 92, 246, 1)', + 'rgba(16, 185, 129, 1)', 'rgba(5, 150, 105, 1)', 'rgba(245, 158, 11, 1)', + 'rgba(251, 191, 36, 1)', 'rgba(239, 68, 68, 1)', 'rgba(220, 38, 127, 1)', + 'rgba(107, 114, 128, 1)', 'rgba(75, 85, 99, 1)', 'rgba(55, 65, 81, 1)' + ], + borderWidth: 1, + borderRadius: 4 + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + callbacks: { + label: function(context) { + return `Distance: ${context.parsed.y.toLocaleString()} km`; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + } + }, + x: { + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + display: false + } + } + } + } + }); + } + + createAirportsTimelineChart() { + const airportData = this.getAirportTimelineData(); + const airports = airportData.map(([airport]) => airport); + const counts = airportData.map(([, data]) => data.count); + + const ctx = document.getElementById('airports-timeline-chart').getContext('2d'); + new Chart(ctx, { + type: 'bar', + data: { + labels: airports, + datasets: [{ + label: 'Visits', + data: counts, + backgroundColor: 'rgba(37, 99, 235, 0.8)', + borderColor: 'rgba(37, 99, 235, 1)', + borderWidth: 1, + borderRadius: 4 + }] + }, + options: { + indexAxis: 'y', + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + callbacks: { + label: function(context) { + return `Visits: ${context.parsed.x}`; + } + } + } + }, + scales: { + x: { + beginAtZero: true, + ticks: { + color: '#6b7280', + font: { + family: 'Inter' + } + }, + grid: { + color: 'rgba(0, 0, 0, 0.1)' + } + }, + y: { + ticks: { + color: '#6b7280', + font: { + family: 'Inter', + size: 11 + } + }, + grid: { + display: false + } + } + } + } + }); + } + + updateTopDestinations() { + const topDestinations = this.getTopDestinations(); + const container = document.getElementById('top-destinations'); + container.innerHTML = ''; + + topDestinations.forEach(([destination, count]) => { + const item = document.createElement('div'); + item.className = 'destination-item'; + item.innerHTML = ` + ${destination} + ${count} + `; + container.appendChild(item); + }); + } + + updateTopAirlines() { + const topAirlines = this.getTopAirlines(); + const container = document.getElementById('top-airlines'); + container.innerHTML = ''; + + topAirlines.forEach(([airline, count]) => { + const item = document.createElement('div'); + item.className = 'airline-item'; + item.innerHTML = ` + ${airline} + ${count} + `; + container.appendChild(item); + }); + } + + init() { + this.updateUI(); + } +} + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM loaded, checking for data...'); + + if (typeof locationsData !== 'undefined' && typeof flightRoutesData !== 'undefined') { + console.log('Data found, initializing calculator...'); + console.log('Locations:', locationsData.length); + console.log('Flights:', flightRoutesData.length); + + try { + new FlightStatsCalculator(locationsData, flightRoutesData); + console.log('FlightStatsCalculator initialized successfully'); + } catch (error) { + console.error('Error initializing FlightStatsCalculator:', error); + } + } else { + console.error('Required data is missing:', { + locationsData: typeof locationsData, + flightRoutesData: typeof flightRoutesData + }); + } + + // Check if Chart.js is loaded + if (typeof Chart === 'undefined') { + console.error('Chart.js is not loaded!'); + } else { + console.log('Chart.js is loaded successfully'); + } +}); \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..35861cf --- /dev/null +++ b/js/main.js @@ -0,0 +1,78 @@ +// Smooth scrolling for navigation links +document.querySelectorAll('a[href^="#"]').forEach(anchor => { + anchor.addEventListener('click', function (e) { + e.preventDefault(); + const target = document.querySelector(this.getAttribute('href')); + if (target) { + target.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + }); +}); + +// Header scroll effect +const header = document.querySelector('.header'); +let lastScroll = 0; + +window.addEventListener('scroll', () => { + const currentScroll = window.pageYOffset; + + if (currentScroll <= 0) { + header.classList.remove('scroll-up'); + return; + } + + if (currentScroll > lastScroll && !header.classList.contains('scroll-down')) { + // Scrolling down + header.classList.remove('scroll-up'); + header.classList.add('scroll-down'); + } else if (currentScroll < lastScroll && header.classList.contains('scroll-down')) { + // Scrolling up + header.classList.remove('scroll-down'); + header.classList.add('scroll-up'); + } + + lastScroll = currentScroll; +}); + +// Intersection Observer for fade-in animations +const observerOptions = { + root: null, + threshold: 0.1, + rootMargin: '0px' +}; + +const observer = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + entry.target.classList.add('fade-in'); + observer.unobserve(entry.target); + } + }); +}, observerOptions); + +// Observe all sections +document.querySelectorAll('section').forEach(section => { + section.classList.add('fade-in-section'); + observer.observe(section); +}); + +// Mobile menu toggle (if needed in the future) +function setupMobileMenu() { + const menuButton = document.querySelector('.mobile-menu-button'); + const navLinks = document.querySelector('.nav-links'); + + if (menuButton && navLinks) { + menuButton.addEventListener('click', () => { + navLinks.classList.toggle('active'); + menuButton.classList.toggle('active'); + }); + } +} + +// Initialize when DOM is loaded +document.addEventListener('DOMContentLoaded', () => { + setupMobileMenu(); +}); \ No newline at end of file