├── .firebaserc ├── .github └── workflows │ ├── firebase-hosting-merge.yml │ ├── firebase-hosting-pull-request.yml │ ├── format.yml │ └── npm-publish.yml ├── .gitignore ├── README.md ├── api ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── rollup.config.ts ├── src │ └── index.ts └── tsconfig.json ├── firebase.json ├── public ├── 404.html ├── altitude │ ├── feet.html │ └── meters.html ├── compat.html ├── css.js ├── cycling_cadence │ └── rpm.html ├── cycling_power │ └── w.html ├── cycling_speed │ ├── 700c-kph.html │ └── 700c-mph.html ├── datetime │ └── luxon.html ├── distance │ ├── distance.js │ ├── km.html │ └── miles.html ├── fallout.html ├── favicon.ico ├── fonts │ ├── chalet.ttf │ ├── chalet.woff │ └── pricedown.woff ├── generic.html ├── googlemaps.html ├── gta.html ├── heading │ ├── deg.html │ └── nsew.html ├── heart_rate │ └── bpm.html ├── inclination │ ├── inclination.js │ └── slope.html ├── index.html ├── indicator.js ├── leaflet.html ├── mapbox.html ├── neighborhood.html ├── power │ ├── power.js │ └── wattage.html ├── speed │ ├── kmh.html │ ├── kph.html │ ├── minperkm.html │ └── mph.html ├── streetview │ └── google.html ├── tags │ └── closest.html └── weather │ ├── feels_like │ ├── C.html │ └── F.html │ ├── rain.html │ └── temperature │ ├── C.html │ └── F.html └── web-editor ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── google_preview.html ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── App.test.js ├── component │ ├── Appbar.jsx │ ├── BorderRadiusPicker.jsx │ ├── ColorPickerToggle.jsx │ ├── CopyIconTextField.jsx │ ├── CountryPicker.tsx │ ├── ExclusiveToggle.jsx │ ├── FontPicker.jsx │ ├── GoogleMapsStyleDialog.jsx │ ├── IndicatorSetting.jsx │ ├── MultipleSelect.jsx │ ├── NavigationDrawer.jsx │ ├── NumberTextField.jsx │ ├── OverlayExportPanel.jsx │ ├── OverlayPreview.jsx │ ├── PullKeyInput.jsx │ ├── TextOverlayPreview.jsx │ ├── TextSettings.jsx │ ├── ZoomSlider.jsx │ ├── google-maps │ │ ├── GoogleAPIKeyDialog.jsx │ │ ├── GoogleMapsContainer.jsx │ │ ├── GoogleMapsSettings.jsx │ │ └── GoogleStreetViewSettings.jsx │ └── mapbox │ │ ├── AttributionDialog.jsx │ │ ├── MapboxContainer.jsx │ │ ├── MapboxSettings.jsx │ │ └── StyleIDHelperDialog.jsx ├── firebase.js ├── fonts │ ├── Hanson-Bold.ttf │ ├── OskariG2-Book.ttf │ └── OskariG2-SemiBold.ttf ├── hooks │ ├── useIndicatorStyle.js │ ├── usePullKey.js │ └── useStyle.js ├── images │ ├── altitude.svg │ ├── background.png │ ├── clock.svg │ ├── cortex.png │ ├── discord.png │ ├── distance.svg │ ├── gcp_key_steps │ │ ├── step1.png │ │ ├── step2.png │ │ ├── step3.png │ │ └── step4.png │ ├── google_maps.jpg │ ├── heading.svg │ ├── inclination.svg │ ├── mapbox.jpg │ ├── mph.svg │ ├── neighborhood.svg │ ├── streetview.png │ └── temperature.svg ├── index.css ├── index.js ├── logo.svg ├── map-style-basic-v8.json ├── reportWebVitals.js ├── screen │ ├── AltitudeEditor.jsx │ ├── ClockEditor.jsx │ ├── CyclingCadenceEditor.jsx │ ├── DistanceEditor.jsx │ ├── GoogleMapsEditor.jsx │ ├── GoogleStreetViewEditor.jsx │ ├── HeadingEditor.jsx │ ├── HeartRateEditor.jsx │ ├── Home.jsx │ ├── InclinationEditor.jsx │ ├── MapboxEditor.jsx │ ├── NeighborhoodEditor.jsx │ ├── SpeedEditor.jsx │ └── WeatherEditor.jsx ├── setupTests.js ├── theme │ ├── editorTheme.jsx │ └── homeTheme.jsx └── utility.js └── tsconfig.json /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "rtirl-a1d7f" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-merge.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on merge 5 | 'on': 6 | push: 7 | branches: 8 | - main 9 | jobs: 10 | build_and_deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: FirebaseExtended/action-hosting-deploy@v0 15 | with: 16 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 17 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_RTIRL_A1D7F }}' 18 | channelId: live 19 | projectId: rtirl-a1d7f 20 | -------------------------------------------------------------------------------- /.github/workflows/firebase-hosting-pull-request.yml: -------------------------------------------------------------------------------- 1 | # This file was auto-generated by the Firebase CLI 2 | # https://github.com/firebase/firebase-tools 3 | 4 | name: Deploy to Firebase Hosting on PR 5 | 'on': pull_request 6 | jobs: 7 | build_and_preview: 8 | if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}' 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: FirebaseExtended/action-hosting-deploy@v0 13 | with: 14 | repoToken: '${{ secrets.GITHUB_TOKEN }}' 15 | firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_RTIRL_A1D7F }}' 16 | projectId: rtirl-a1d7f 17 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | paths: 6 | - "web-editor/**" 7 | branches: 8 | - main 9 | pull_request: 10 | paths: 11 | - "web-editor/**" 12 | branches: 13 | - main 14 | 15 | jobs: 16 | lint: 17 | name: Validate format 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Check out Git repository 22 | uses: actions/checkout@v2 23 | 24 | - name: Set up Node.js 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: 16 28 | 29 | - name: Install Node.js dependencies 30 | run: npm ci 31 | working-directory: web-editor 32 | 33 | - name: Run prettier 34 | run: npx prettier --check src 35 | working-directory: web-editor 36 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish API to NPM 2 | on: 3 | push: 4 | paths: 5 | - "api/**" 6 | branches: 7 | - main 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Publish if version has been updated 15 | uses: pascalgn/npm-publish-action@1.3.8 16 | with: 17 | create_tag: false 18 | workspace: ./api/ 19 | env: 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # Miscellaneous macOS 69 | .DS_Store 70 | -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ -------------------------------------------------------------------------------- /api/README.md: -------------------------------------------------------------------------------- 1 | # RealtimeIRL Client API 2 | 3 | ![npm](https://img.shields.io/npm/v/@rtirl/api) 4 | ![npm](https://img.shields.io/npm/dw/@rtirl/api) 5 | 6 | An API to access RealtimeIRL data. 7 | 8 | ## Installation 9 | 10 | ```bash 11 | npm install @rtirl/api 12 | ``` 13 | 14 | Alternatively, use it in the browser: 15 | 16 | ```html 17 | 18 | ``` 19 | 20 | ## Usage 21 | 22 | ```javascript 23 | import * as RealtimeIRL from '@rtirl/api'; 24 | 25 | RealtimeIRL.forPullKey(YOUR_PULL_KEY).addLocationListener(function ({ 26 | latitude, 27 | longitude, 28 | }) { 29 | // do something with latitude/longitude 30 | }); 31 | ``` 32 | 33 | ```javascript 34 | import * as RealtimeIRL from '@rtirl/api'; 35 | 36 | // muxfd = 158394109 37 | RealtimeIRL.forStreamer("twitch", "158394109").addLocationListener(function ( 38 | location 39 | ) { 40 | // do something with location.latitude/location.longitude 41 | }); 42 | ``` 43 | 44 | See `src/index.ts` for all available functions and types. 45 | 46 | ## Polling-based API 47 | 48 | If you are interested in pulling the data in JSON format, the following endpoint is available: 49 | 50 | ``` 51 | GET https://rtirl.com/api/pull?key= 52 | ``` 53 | 54 | The schema of the response depends on what fields have been pushed by the client. A sample output can be viewed [here](https://rtirl.com/api/pull?key=t0fucprufql69bcx). 55 | 56 | ``` 57 | { 58 | "accuracy":5.408999919891357, // meters 59 | "altitude":{ 60 | "EGM96":3.5973978207728656, // meters 61 | "WGS84":-29.197977916731165 // meters 62 | }, 63 | "heading":206.37741088867188, // degrees 64 | "location":{ 65 | "latitude":40.7047389, // degrees 66 | "longitude":-74.0171302 // degrees 67 | }, 68 | "reportedAt":1629924573000, // milliseconds since epoch 69 | "speed":0.6116824746131897, // meters per second 70 | "updatedAt":1629924573283 // milliseconds since epoch 71 | } 72 | ``` 73 | 74 | Note that the polling API may update less frequently than the push API (~once per five seconds). If you wish to have realtime updates delivered as fast as possible, please use the push API. This helps us save on server costs. 75 | 76 | ## Contributing 77 | 78 | Contributions are always welcome! The documentation is currently quite sparse. Please feel free to send pull requests to improve them. 79 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rtirl/api", 3 | "version": "1.2.1", 4 | "main": "lib/index.js", 5 | "browser": "lib/index.min.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc && rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript", 9 | "prepare": "npm run build" 10 | }, 11 | "files": [ 12 | "lib/**/*" 13 | ], 14 | "exports": { 15 | ".": "./lib" 16 | }, 17 | "author": "muxfdz@gmail.com", 18 | "license": "MIT", 19 | "dependencies": { 20 | "firebase": "^10.12.2" 21 | }, 22 | "devDependencies": { 23 | "@rollup/plugin-commonjs": "^26.0.1", 24 | "@rollup/plugin-node-resolve": "^15.2.3", 25 | "@rollup/plugin-typescript": "^11.1.6", 26 | "prettier": "^3.3.2", 27 | "rollup": "^4.18.0", 28 | "rollup-plugin-terser": "^7.0.2", 29 | "typescript": "^5.4.5" 30 | }, 31 | "description": "Client API library for rtirl.com" 32 | } 33 | -------------------------------------------------------------------------------- /api/rollup.config.ts: -------------------------------------------------------------------------------- 1 | import resolve from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "@rollup/plugin-typescript"; 4 | import pkg from "./package.json" assert { type: "json" }; 5 | import { terser } from "rollup-plugin-terser"; 6 | 7 | export default [ 8 | { 9 | input: "src/index.ts", 10 | output: { 11 | name: "RealtimeIRL", 12 | file: pkg.browser, 13 | format: "iife", 14 | sourcemap: true, 15 | }, 16 | plugins: [resolve(), commonjs(), typescript(), terser()], 17 | }, 18 | ]; 19 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "declaration": true, 6 | "moduleResolution": "node", 7 | "outDir": "./lib", 8 | "strict": true, 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true 11 | }, 12 | "include": ["src", "rollup.config.ts"] 13 | } 14 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": [ 3 | { 4 | "site": "rtirl-overlays", 5 | "public": "public", 6 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 7 | "headers": [ 8 | { 9 | "source": "**/*", 10 | "headers": [ 11 | { 12 | "key": "Access-Control-Allow-Origin", 13 | "value": "*" 14 | }, 15 | { 16 | "key": "X-Frame-Options", 17 | "value": "ALLOW-FROM https://streamelements.com/" 18 | } 19 | ] 20 | } 21 | ] 22 | }, 23 | { 24 | "site": "rtirl-editor", 25 | "public": "web-editor/build", 26 | "predeploy": "npm --prefix web-editor ci && npm --prefix web-editor run build", 27 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 28 | "rewrites": [ 29 | { 30 | "source": "**", 31 | "destination": "/index.html" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 28 | 29 | 30 | 31 |

404

32 | 33 |

34 | The page requested was not found, maybe you meant 35 | RealtimeIRL or 36 | RealtimeIRL Overlay Editor 37 |

38 | 39 | 40 | -------------------------------------------------------------------------------- /public/altitude/feet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 ft
9 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /public/altitude/meters.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 m
9 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /public/compat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | 16 | 17 | 29 | 30 | 31 |
32 |
33 |
47 | 143 | 144 | 145 | -------------------------------------------------------------------------------- /public/css.js: -------------------------------------------------------------------------------- 1 | const css = new URLSearchParams(window.location.search).get("css"); 2 | const font = new URLSearchParams(window.location.search).get("font"); 3 | 4 | if (font) { 5 | const decodedFont = atob(font); 6 | const importFontFamily = document.createElement("link"); 7 | importFontFamily.href = decodedFont; 8 | importFontFamily.type = "text/css"; 9 | importFontFamily.rel = "stylesheet"; 10 | 11 | // load google font 12 | document.getElementsByTagName("head")[0].appendChild(importFontFamily); 13 | } 14 | 15 | // migration nessessary for old users 16 | if (css) { 17 | document.body.setAttribute("style", css); 18 | const decodedCSS = atob(css); 19 | document.getElementsByTagName("body")[0].style = decodedCSS; 20 | } 21 | -------------------------------------------------------------------------------- /public/cycling_cadence/rpm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 rpm
9 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /public/cycling_power/w.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
0 watts
11 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /public/cycling_speed/700c-kph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 km/h
9 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/cycling_speed/700c-mph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 mph
9 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /public/datetime/luxon.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | 12 | 13 | 14 |
15 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /public/distance/distance.js: -------------------------------------------------------------------------------- 1 | var key; 2 | 3 | var oldsessionID; 4 | var totalDistance = 0.0; 5 | var gps = { old: { latitude: 0.0, longitude: 0.0 }, new: { latitude: 0.0, longitude: 0.0 } }; 6 | 7 | 8 | // Get user options 9 | var params = new URLSearchParams(window.location.search); 10 | key = params.get('key') || ''; 11 | 12 | RealtimeIRL.forPullKey(key).addLocationListener( 13 | function ({ latitude, longitude }) { 14 | gps.new.latitude = latitude; 15 | gps.new.longitude = longitude; 16 | 17 | if (oldsessionID === undefined) { 18 | document.getElementById("text").innerText = '0.0' + unit; 19 | } else { 20 | // We have new gps points. Let's calculate the delta distance using previously saved gps points. 21 | delta = distanceInKmBetweenEarthCoordinates(gps.new.latitude, gps.new.longitude, gps.old.latitude, gps.old.longitude); 22 | totalDistance = totalDistance + delta; 23 | totalStr = (totalDistance * unitMultiplier).toFixed(1) + unit; 24 | document.getElementById("text").innerText = totalStr; 25 | //shifting new points to old for next update 26 | gps.old.latitude = latitude; 27 | gps.old.longitude = longitude; 28 | 29 | // Note that because of GPS drift, different gps points will keep comming even if 30 | // the subject is stationary. Each new gps point will be considered as subject is moving 31 | // and it will get added to the total distance. Each addition will be tiny but it will 32 | // addup over time and can become visible. So, at the end the shown distance might look 33 | // sligtly more than expected. 34 | } 35 | } 36 | ); 37 | 38 | RealtimeIRL.forPullKey(key).addSessionIdListener( 39 | function (sessionId) { 40 | if (sessionId != oldsessionID) { 41 | oldsessionID = sessionId; 42 | resetVars(); 43 | } 44 | } 45 | ); 46 | 47 | function degreesToRadians(degrees) { 48 | return degrees * Math.PI / 180; 49 | } 50 | 51 | function distanceInKmBetweenEarthCoordinates(lat1, lon1, lat2, lon2) { 52 | var earthRadiusKm = 6371; 53 | 54 | var dLat = degreesToRadians(lat2 - lat1); 55 | var dLon = degreesToRadians(lon2 - lon1); 56 | 57 | lat1 = degreesToRadians(lat1); 58 | lat2 = degreesToRadians(lat2); 59 | 60 | var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 61 | Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); 62 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 63 | return earthRadiusKm * c; 64 | } 65 | 66 | function resetVars() { 67 | // New session. Reset total distance 68 | totalDistance = 0; 69 | // Set starting point to the current point 70 | gps.old.latitude = gps.new.latitude; 71 | gps.old.longitude = gps.new.longitude; 72 | } -------------------------------------------------------------------------------- /public/distance/km.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Distance in km 9 | 10 | 11 |
Waiting for stream to start...
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/distance/miles.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Distance in miles 9 | 10 | 11 |
Waiting for stream to start...
12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/chalet.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/public/fonts/chalet.ttf -------------------------------------------------------------------------------- /public/fonts/chalet.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/public/fonts/chalet.woff -------------------------------------------------------------------------------- /public/fonts/pricedown.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/public/fonts/pricedown.woff -------------------------------------------------------------------------------- /public/generic.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |
16 |
29 |
30 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /public/googlemaps.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |
16 |
30 |
31 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /public/heading/deg.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0°
9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/heading/nsew.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/heart_rate/bpm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 bpm
9 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /public/inclination/inclination.js: -------------------------------------------------------------------------------- 1 | const unit = '%'; 2 | var key; 3 | var inclination = 0.0; 4 | var gps = { old: { latitude: 0.0, longitude: 0.0, altitude: 0.0, inclination: 0.0 }, new: { latitude: 0.0, longitude: 0.0, altitude: 0.0, inclination: 0.0 } }; 5 | 6 | // Get user options 7 | var params = new URLSearchParams(window.location.search); 8 | key = params.get('key') || ''; 9 | 10 | RealtimeIRL.forPullKey(key).addListener( 11 | function (data) { 12 | gps.new.latitude = data.location.latitude; 13 | gps.new.longitude = data.location.longitude; 14 | gps.new.altitude = data.altitude.EGM96; 15 | 16 | // We have new gps points. Let's calculate the delta distance using previously saved gps points (IN METERS) 17 | delta = distanceInKmBetweenEarthCoordinates(gps.new.latitude, gps.new.longitude, gps.old.latitude, gps.old.longitude) * 1000; 18 | 19 | if (delta !== 0) { 20 | gps.new.inclination = ((gps.new.altitude - gps.old.altitude) / delta * 100); 21 | } else { 22 | gps.new.inclination = 0; 23 | } 24 | 25 | // Ease-in inclination, no sudden jumps 26 | inclination = (gps.new.inclination + gps.old.inclination) / 2; 27 | 28 | gps.old.inclination = gps.new.inclination; 29 | 30 | // "Fix" errors 31 | if ((inclination > 40) || (inclination < -40) || isNaN(inclination)){ 32 | inclination = 0.0; 33 | } 34 | 35 | document.getElementById("text").innerText = inclination.toFixed(1) + unit; 36 | 37 | // Shifting new points to old for next update 38 | gps.old.latitude = gps.new.latitude; 39 | gps.old.longitude = gps.new.longitude; 40 | gps.old.altitude = gps.new.altitude; 41 | } 42 | ); 43 | 44 | function degreesToRadians(degrees) { 45 | return degrees * Math.PI / 180; 46 | } 47 | 48 | function distanceInKmBetweenEarthCoordinates(lat1, lon1, lat2, lon2) { 49 | var earthRadiusKm = 6371; 50 | 51 | var dLat = degreesToRadians(lat2 - lat1); 52 | var dLon = degreesToRadians(lon2 - lon1); 53 | 54 | lat1 = degreesToRadians(lat1); 55 | lat2 = degreesToRadians(lat2); 56 | 57 | var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 58 | Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); 59 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 60 | return earthRadiusKm * c; 61 | } -------------------------------------------------------------------------------- /public/inclination/slope.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Slope percentage 9 | 10 | 11 |
Init
12 | 13 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RealtimeIRL Overlays 6 | 7 | 11 | 12 | 13 | You probably want the overlay editor. https://editor.rtirl.com/ 14 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /public/indicator.js: -------------------------------------------------------------------------------- 1 | var params = new URLSearchParams(window.location.search); 2 | if (params.get("indicatorStyle")) { 3 | var indicatorStyle = atob(params.get("indicatorStyle")); 4 | indicatorStyle = JSON.parse(indicatorStyle); 5 | console.log(indicatorStyle); 6 | console.log(indicatorStyle.backgroundColor); 7 | document.getElementById("marker").style.backgroundColor = indicatorStyle.backgroundColor; 8 | document.getElementById("marker").style.boxShadow = "0 0 10px " + indicatorStyle.backgroundColor; 9 | document.getElementById("marker").style.width = `${indicatorStyle.width}px`; 10 | document.getElementById("marker").style.height = `${indicatorStyle.height}px`; 11 | } -------------------------------------------------------------------------------- /public/leaflet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | 16 | 17 | 18 | 19 | 31 | 32 | 33 |
34 |
35 |
49 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /public/mapbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 |
17 |
30 |
31 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /public/neighborhood.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /public/power/power.js: -------------------------------------------------------------------------------- 1 | var key; 2 | var inclination = 0.0; 3 | var wattage = 0; 4 | var timeDelta = 0.0; 5 | var target; 6 | var gauge; 7 | var powerLossPercentage = 3; //Not all of the power that you produce when cycling is transferred directly to the wheels. 3% default. 8 | var gps = { old: { latitude: 0.0, longitude: 0.0, altitude: 0.0, speed: 0.0, time: 0, wattage: 0 }, new: { latitude: 0.0, longitude: 0.0, altitude: 0.0, speed: 0.0, time: 0, wattage: 0 } }; 9 | var timer; 10 | 11 | // Get user options 12 | var params = new URLSearchParams(window.location.search); 13 | key = params.get('key') || ''; 14 | var mRider = params.get('ridermass') || 80; 15 | var mBike = params.get('bikemass') || 11; 16 | var maxPower = params.get('maxpower') || 400; 17 | 18 | // Canvas Options 19 | 20 | var opts = { 21 | angle: 0.07, // The span of the gauge arc 22 | lineWidth: 0.53, // The line thickness 23 | radiusScale: 1, // Relative radius 24 | pointer: { 25 | length: 0.68, // // Relative to gauge radius 26 | strokeWidth: 0.051, // The thickness 27 | color: '#000000' // Fill color 28 | }, 29 | limitMax: true, // If false, max value increases automatically if value > maxValue 30 | limitMin: true, // If true, the min value of the gauge will be fixed 31 | colorStart: '#6FADCF', // Colors 32 | colorStop: '#8FC0DA', // just experiment with them 33 | strokeColor: '#E0E0E0', // to see which ones work best for you 34 | generateGradient: true, 35 | highDpiSupport: true, // High resolution support 36 | percentColors: [[0.0, "#a9d70b" ], [0.50, "#f9c802"], [1.0, "#ff0000"]], 37 | }; 38 | 39 | RealtimeIRL.forPullKey(key).addListener( 40 | function (data) { 41 | if (gps.new.latitude == 0){ //only run once 42 | target = document.getElementById('foo'); 43 | gauge = new Gauge(target).setOptions(opts); 44 | gauge.maxValue = 400; 45 | gauge.animationSpeed = 100; 46 | } 47 | gps.new.latitude = data.location.latitude; 48 | gps.new.longitude = data.location.longitude; 49 | gps.new.altitude = data.altitude.EGM96; 50 | gps.new.speed = data.speed; 51 | gps.new.time = data.updatedAt; 52 | 53 | clearTimeout(timer); 54 | 55 | // We have new gps points. Let's calculate the delta distance using previously saved gps points (IN METERS) 56 | delta = distanceInKmBetweenEarthCoordinates(gps.new.latitude, gps.new.longitude, gps.old.latitude, gps.old.longitude) * 1000; 57 | 58 | timeDelta = timeDifference(gps.new.time, gps.old.time); 59 | 60 | // Now calculate the slope percentage, based on altitude change and distance travelled 61 | inclination = ((gps.new.altitude - gps.old.altitude) / delta * 100); 62 | 63 | // "Fix" errors 64 | if ((inclination > 40) || (inclination < -40)){ 65 | inclination = 0.0; 66 | } 67 | 68 | // Power = Force * distance / time 69 | gps.new.wattage = (forceOfGravity(inclination, mRider, mBike) + forceOfRollingResistance(inclination, mRider, mBike) + forceOfDrag(delta/timeDelta)) * (delta / timeDelta) / (1 - powerLossPercentage/100); //P=(Fg+Fr+Fa)×v/(1−loss) 70 | 71 | // Ease-in wattage, no sudden jumps 72 | wattage = (gps.new.wattage + gps.old.wattage) / 2; 73 | 74 | gps.old.wattage = gps.new.wattage; 75 | 76 | if (wattage > 0 && wattage <= maxPower) { 77 | document.getElementById("text").innerText = wattage.toFixed(0) + "W"; 78 | gauge.set(wattage.toFixed(0)); 79 | } 80 | else if (wattage > maxPower){ 81 | document.getElementById("text").innerText = maxPower + "W+"; 82 | gauge.set(maxPower); 83 | } 84 | else { 85 | document.getElementById("text").innerText = 0 + "W"; 86 | gauge.set(0); 87 | } 88 | 89 | // Shifting new points to old for next update 90 | gps.old.latitude = gps.new.latitude; 91 | gps.old.longitude = gps.new.longitude; 92 | gps.old.altitude = gps.new.altitude; 93 | gps.old.speed = gps.new.speed; 94 | gps.old.time = gps.new.time; 95 | 96 | timer = setTimeout(() => { 97 | document.getElementById("text").innerText = 0 + "W"; 98 | gauge.set(0); 99 | }, 10000); 100 | } 101 | ); 102 | 103 | function degreesToRadians(degrees) { 104 | return degrees * Math.PI / 180; 105 | } 106 | 107 | function distanceInKmBetweenEarthCoordinates(lat1, lon1, lat2, lon2) { 108 | var earthRadiusKm = 6371; 109 | 110 | var dLat = degreesToRadians(lat2 - lat1); 111 | var dLon = degreesToRadians(lon2 - lon1); 112 | 113 | lat1 = degreesToRadians(lat1); 114 | lat2 = degreesToRadians(lat2); 115 | 116 | var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + 117 | Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); 118 | var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); 119 | return earthRadiusKm * c; 120 | } 121 | 122 | function forceOfGravity(slope, massRider, massBike){ 123 | var g = 9.81; 124 | return (g * Math.sin(Math.atan(slope/100)) * (massRider + massBike)); 125 | } 126 | 127 | function forceOfRollingResistance(slope, massRider, massBike){ 128 | var g = 9.81; 129 | var resistance = 0.006; //default tarmac resistance 130 | return (g * Math.cos(Math.atan(slope/100)) * (massRider + massBike) * resistance); 131 | } 132 | 133 | function forceOfDrag(speed){ 134 | return 0.5 * 0.33 * speed * speed * 1.19; //Fa=0.5×Cd×A×ρ×(v+w). Not accounting for wind (for now). Air density = 1.19, aprox. Lower if higher altitudes 135 | 2 136 | } 137 | 138 | function timeDifference(date1, date2) { 139 | var difference = date1 - date2; 140 | return difference/1000; //in seconds, float 141 | } -------------------------------------------------------------------------------- /public/power/wattage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Cycling Power Meter 8 | 34 | 35 | 36 |
37 | 38 |
Init
39 |
40 | 41 | -------------------------------------------------------------------------------- /public/speed/kmh.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 km/h
9 | 25 | 26 | -------------------------------------------------------------------------------- /public/speed/kph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 km/h
9 | 25 | 26 | -------------------------------------------------------------------------------- /public/speed/minperkm.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0:00 /km
9 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /public/speed/mph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0 mph
9 | 25 | 26 | -------------------------------------------------------------------------------- /public/streetview/google.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 13 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/tags/closest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 15 | 16 | 17 | 29 | 30 | 31 |
32 |
33 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /public/weather/feels_like/C.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0°C
9 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/weather/feels_like/F.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
0°F
9 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /public/weather/rain.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 |
15 |
16 |
29 |
30 | 134 | 135 | 136 | -------------------------------------------------------------------------------- /public/weather/temperature/C.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0°C
10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/weather/temperature/F.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
0°F
10 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /web-editor/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /web-editor/README.md: -------------------------------------------------------------------------------- 1 | # RTIRL Overlay Web App 2 | 3 | The goal of this web app is to replace [text instruction page](https://overlays.rtirl.com/) and to make the process 4 | of configuring and stylizing overlay elements easier for [https://rtirl.com/](https://rtirl.com/) 5 | 6 | To view/use app hosted, visit [https://editor.rtirl.com/](https://editor.rtirl.com/) 7 | -------------------------------------------------------------------------------- /web-editor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.9.0", 7 | "@emotion/styled": "^11.8.1", 8 | "@mui/icons-material": "^5.6.2", 9 | "@mui/material": "^5.6.2", 10 | "@mui/system": "^5.8.0", 11 | "@samuelmeuli/font-manager": "^1.4.0", 12 | "@testing-library/jest-dom": "^5.16.4", 13 | "@testing-library/react": "^13.1.1", 14 | "@testing-library/user-event": "^13.5.0", 15 | "@types/jest": "^27.4.1", 16 | "@types/node": "^17.0.25", 17 | "@types/react": "^18.0.6", 18 | "@types/react-dom": "^18.0.2", 19 | "firebase": "^9.8.3", 20 | "immutable": "^4.0.0", 21 | "leaflet": "^1.8.0", 22 | "luxon": "^2.5.2", 23 | "mapbox-gl": "^2.8.1", 24 | "react": "^18.0.0", 25 | "react-color": "^2.19.3", 26 | "react-dom": "^18.0.0", 27 | "react-leaflet": "^4.0.0", 28 | "react-map-gl": "^7.0.10", 29 | "react-router-dom": "^6.3.0", 30 | "react-scripts": "5.0.1", 31 | "typescript": "^4.6.3", 32 | "web-vitals": "^2.1.4" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | "defaults", 49 | "not ie 11" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "prettier": "^2.6.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /web-editor/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/public/favicon.ico -------------------------------------------------------------------------------- /web-editor/public/google_preview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 14 | 15 | 16 |
17 |
18 |
32 |
33 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /web-editor/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | RealtimeIRL Overlay Editor 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /web-editor/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "RTIRL Editor", 3 | "name": "RealtimeIRL Overlay Editor", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /web-editor/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web-editor/src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | -------------------------------------------------------------------------------- /web-editor/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | test("renders learn react link", () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /web-editor/src/component/Appbar.jsx: -------------------------------------------------------------------------------- 1 | import AppBar from "@mui/material/AppBar"; 2 | import Toolbar from "@mui/material/Toolbar"; 3 | import Typography from "@mui/material/Typography"; 4 | import { Box, Tooltip, Link } from "@mui/material"; 5 | import * as React from "react"; 6 | import { useLocation } from "react-router-dom"; 7 | 8 | export const EditorAppbar = () => { 9 | const location = useLocation(); 10 | // Do not render the app bar on the home screen 11 | if (location.pathname === "/") { 12 | return <>; 13 | } 14 | return ( 15 | 16 | 17 | 23 | 24 | RealtimeIRL Overlay Editor 25 | 26 | 27 | 28 | 29 | 30 | Home 31 | 32 | 33 | 34 | 35 | 36 | 40 | window.open("https://discord.gg/uWuzfEUBTX", "_blank") 41 | } 42 | > 43 | Discord 44 | 45 | 46 | 47 | 48 | 49 | 53 | window.open("https://github.com/muxable/rtirl-obs", "_blank") 54 | } 55 | > 56 | GitHub 57 | 58 | 59 | 60 | 61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /web-editor/src/component/BorderRadiusPicker.jsx: -------------------------------------------------------------------------------- 1 | import { Stack } from "@mui/material"; 2 | import React from "react"; 3 | import BorderStyleIcon from "@mui/icons-material/BorderStyle"; 4 | import { NumberTextField } from "./NumberTextField"; 5 | import { clamp } from "../utility"; 6 | 7 | function BorderRadiusPicker({ textDivCSS, setTextDivCSS }) { 8 | return ( 9 | <> 10 | 11 | { 14 | setTextDivCSS({ 15 | ...textDivCSS, 16 | border_top_left_radius: clamp(border_top_left_radius, 0, 100), 17 | }); 18 | }} 19 | endAdornmentUnit="%" 20 | prefixIcon={} 21 | tooltipTitle="Top Left Border Radius" 22 | /> 23 | { 26 | setTextDivCSS({ 27 | ...textDivCSS, 28 | border_top_right_radius: clamp(border_top_right_radius, 0, 100), 29 | }); 30 | }} 31 | endAdornmentUnit="%" 32 | prefixIcon={} 33 | tooltipTitle="Top Right Border Radius" 34 | /> 35 | 36 | 37 | 38 | { 41 | setTextDivCSS({ 42 | ...textDivCSS, 43 | border_bottom_left_radius: clamp( 44 | border_bottom_left_radius, 45 | 0, 46 | 100 47 | ), 48 | }); 49 | }} 50 | endAdornmentUnit="%" 51 | prefixIcon={} 52 | tooltipTitle="Bottom Left Border Radius" 53 | /> 54 | { 57 | setTextDivCSS({ 58 | ...textDivCSS, 59 | border_bottom_right_radius: clamp( 60 | border_bottom_right_radius, 61 | 0, 62 | 100 63 | ), 64 | }); 65 | }} 66 | endAdornmentUnit="%" 67 | prefixIcon={} 68 | tooltipTitle="Bottom Right Border Radius" 69 | /> 70 | 71 | 72 | ); 73 | } 74 | 75 | export default BorderRadiusPicker; 76 | -------------------------------------------------------------------------------- /web-editor/src/component/ColorPickerToggle.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { SketchPicker } from "react-color"; 3 | 4 | class ColorPickerToggle extends React.Component { 5 | state = { 6 | displayColorPicker: false, 7 | }; 8 | 9 | handleClick = () => { 10 | this.setState({ displayColorPicker: !this.state.displayColorPicker }); 11 | }; 12 | 13 | handleClose = () => { 14 | this.setState({ displayColorPicker: false }); 15 | }; 16 | 17 | handleChange = (color) => { 18 | this.props.setColor(color.rgb); 19 | }; 20 | 21 | render() { 22 | return ( 23 | <> 24 |
31 |
42 |
43 | {this.state.displayColorPicker ? ( 44 |
45 |
55 | 59 |
60 | ) : null} 61 | 62 | ); 63 | } 64 | } 65 | 66 | export default ColorPickerToggle; 67 | -------------------------------------------------------------------------------- /web-editor/src/component/CopyIconTextField.jsx: -------------------------------------------------------------------------------- 1 | import CheckIcon from "@mui/icons-material/Check"; 2 | import ContentCopyIcon from "@mui/icons-material/ContentCopy"; 3 | import { IconButton, InputAdornment, TextField, Tooltip } from "@mui/material"; 4 | import { getAnalytics, logEvent } from "firebase/analytics"; 5 | import { useEffect, useState } from "react"; 6 | import { firebaseApp } from "../firebase"; 7 | 8 | export const CopyIconTextField = ({ value, multiline, row, label, type }) => { 9 | const analytics = getAnalytics(firebaseApp); 10 | const [copied, setCopied] = useState(false); 11 | 12 | useEffect(() => { 13 | if (copied) { 14 | setTimeout(() => setCopied(false), 1500); 15 | } 16 | }, [copied]); 17 | 18 | return ( 19 | 35 | 36 | { 38 | navigator.clipboard.writeText(value); 39 | setCopied(true); 40 | if (type) { 41 | logEvent(analytics, "export_string", { 42 | type: type, 43 | }); 44 | } 45 | }} 46 | > 47 | {copied ? : } 48 | 49 | 50 | 51 | ), 52 | }} 53 | > 54 | ); 55 | }; 56 | -------------------------------------------------------------------------------- /web-editor/src/component/CountryPicker.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MenuItem, Select, SelectChangeEvent } from "@mui/material"; 3 | 4 | // https://github.com/lipis/flag-icons/issues/510 5 | const languageToCountry: { [key: string]: string } = { 6 | aa: "dj", 7 | af: "za", 8 | ak: "gh", 9 | sq: "al", 10 | am: "et", 11 | ar: "aa", 12 | hy: "am", 13 | ay: "wh", 14 | az: "az", 15 | bm: "ml", 16 | be: "by", 17 | bn: "bd", 18 | bi: "vu", 19 | bs: "ba", 20 | bg: "bg", 21 | my: "mm", 22 | ca: "ad", 23 | zh: "cn", 24 | "zh-hans": "cn", 25 | "zh-hant": "cn", 26 | hr: "hr", 27 | cs: "cz", 28 | da: "dk", 29 | dv: "mv", 30 | nl: "nl", 31 | dz: "bt", 32 | en: "gb", 33 | et: "ee", 34 | ee: "ew", 35 | fj: "fj", 36 | fil: "ph", 37 | fi: "fi", 38 | fr: "fr", 39 | ff: "ff", 40 | gaa: "gh", 41 | ka: "ge", 42 | de: "de", 43 | el: "gr", 44 | gn: "gx", 45 | gu: "in", 46 | ht: "ht", 47 | ha: "ha", 48 | he: "il", 49 | hi: "in", 50 | ho: "pg", 51 | hu: "hu", 52 | is: "is", 53 | ig: "ng", 54 | id: "id", 55 | ga: "ie", 56 | it: "it", 57 | ja: "jp", 58 | kr: "ne", 59 | kk: "kz", 60 | km: "kh", 61 | kmb: "ao", 62 | rw: "rw", 63 | kg: "cg", 64 | ko: "kr", 65 | kj: "ao", 66 | ku: "iq", 67 | ky: "kg", 68 | lo: "la", 69 | la: "va", 70 | lv: "lv", 71 | ln: "cg", 72 | lt: "lt", 73 | lu: "cd", 74 | lb: "lu", 75 | mk: "mk", 76 | mg: "mg", 77 | ms: "my", 78 | mt: "mt", 79 | mi: "nz", 80 | mh: "mh", 81 | mn: "mn", 82 | mos: "bf", 83 | ne: "np", 84 | nd: "zw", 85 | nso: "za", 86 | no: "no", 87 | nb: "no", 88 | nn: "no", 89 | ny: "mw", 90 | pap: "aw", 91 | ps: "af", 92 | fa: "ir", 93 | pl: "pl", 94 | pt: "pt", 95 | pa: "in", 96 | qu: "wh", 97 | ro: "ro", 98 | rm: "ch", 99 | rn: "bi", 100 | ru: "ru", 101 | sg: "cf", 102 | sr: "rs", 103 | srr: "sn", 104 | sn: "zw", 105 | si: "lk", 106 | sk: "sk", 107 | sl: "si", 108 | so: "so", 109 | snk: "sn", 110 | nr: "za", 111 | st: "ls", 112 | es: "es", 113 | sw: "sw", 114 | ss: "sz", 115 | sv: "se", 116 | tl: "ph", 117 | tg: "tj", 118 | ta: "lk", 119 | te: "in", 120 | tet: "tl", 121 | th: "th", 122 | ti: "er", 123 | tpi: "pg", 124 | ts: "za", 125 | tn: "bw", 126 | tr: "tr", 127 | tk: "tm", 128 | uk: "ua", 129 | umb: "ao", 130 | ur: "pk", 131 | uz: "uz", 132 | ve: "za", 133 | vi: "vn", 134 | cy: "gb", 135 | wo: "sn", 136 | xh: "za", 137 | yo: "yo", 138 | zu: "za", 139 | }; 140 | 141 | function getFlagEmoji(countryCode: string) { 142 | const codePoints = countryCode 143 | .toUpperCase() 144 | .split("") 145 | .map((char) => 127397 + char.charCodeAt(0)); 146 | return String.fromCodePoint(...codePoints); 147 | } 148 | 149 | function CountryPicker({ 150 | countries, 151 | lang, 152 | setLang, 153 | }: { 154 | countries: string[]; 155 | lang: string; 156 | setLang: any; 157 | mapboxMapRef: any; 158 | }) { 159 | const handleLanguageChange = (event: SelectChangeEvent) => { 160 | const newLang = event.target.value as string; 161 | setLang(newLang); 162 | }; 163 | 164 | return ( 165 | 191 | ); 192 | } 193 | 194 | export default CountryPicker; 195 | -------------------------------------------------------------------------------- /web-editor/src/component/ExclusiveToggle.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from "@mui/material"; 2 | import ToggleButton from "@mui/material/ToggleButton"; 3 | import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; 4 | import * as React from "react"; 5 | 6 | function ExclusiveToggle({ name, selectedOption, onOptionChange, options }) { 7 | return ( 8 | 9 | {name} 10 | { 15 | if (selectedOption) { 16 | onOptionChange(selectedOption); 17 | } 18 | }} 19 | > 20 | {options.map((option, _) => ( 21 | 22 | {option.name} 23 | 24 | ))} 25 | 26 | 27 | ); 28 | } 29 | 30 | export default ExclusiveToggle; 31 | -------------------------------------------------------------------------------- /web-editor/src/component/FontPicker.jsx: -------------------------------------------------------------------------------- 1 | import { FormControl, FormHelperText, MenuItem, Select } from "@mui/material"; 2 | import { 3 | FontManager, 4 | FONT_FAMILY_DEFAULT, 5 | OPTIONS_DEFAULTS, 6 | } from "@samuelmeuli/font-manager"; 7 | import React from "react"; 8 | 9 | function getFontId(fontFamily) { 10 | return fontFamily.replace(/\s+/g, "-").toLowerCase(); 11 | } 12 | 13 | /** 14 | * Based on the no longer mantained https://github.com/samuelmeuli/font-picker-react 15 | */ 16 | export default class FontPicker extends React.PureComponent { 17 | fontManager; 18 | 19 | static defaultProps = { 20 | activeFontFamily: FONT_FAMILY_DEFAULT, 21 | onChange: () => {}, 22 | pickerId: OPTIONS_DEFAULTS.pickerId, 23 | families: OPTIONS_DEFAULTS.families, 24 | categories: OPTIONS_DEFAULTS.categories, 25 | scripts: OPTIONS_DEFAULTS.scripts, 26 | variants: OPTIONS_DEFAULTS.variants, 27 | filter: OPTIONS_DEFAULTS.filter, 28 | limit: OPTIONS_DEFAULTS.limit, 29 | sort: "popularity", 30 | }; 31 | 32 | state = { 33 | loadingStatus: "loading", 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | 39 | const { 40 | apiKey, 41 | activeFontFamily, 42 | pickerId, 43 | families, 44 | categories, 45 | scripts, 46 | variants, 47 | filter, 48 | limit, 49 | sort, 50 | onChange, 51 | } = this.props; 52 | 53 | const options = { 54 | pickerId, 55 | families, 56 | categories, 57 | scripts, 58 | variants, 59 | filter, 60 | limit, 61 | sort, 62 | }; 63 | 64 | this.fontManager = new FontManager( 65 | apiKey, 66 | activeFontFamily, 67 | options, 68 | onChange 69 | ); 70 | } 71 | 72 | componentDidMount = () => { 73 | this.fontManager 74 | .init() 75 | .then(() => { 76 | this.setState({ 77 | loadingStatus: "finished", 78 | }); 79 | }) 80 | .catch((err) => { 81 | this.setState({ 82 | loadingStatus: "error", 83 | }); 84 | }); 85 | }; 86 | 87 | componentDidUpdate = (prevProps) => { 88 | const { activeFontFamily, onChange } = this.props; 89 | 90 | if (activeFontFamily !== prevProps.activeFontFamily) { 91 | this.setActiveFontFamily(activeFontFamily); 92 | } 93 | 94 | if (onChange !== prevProps.onChange) { 95 | this.fontManager.setOnChange(onChange); 96 | } 97 | }; 98 | 99 | onSelection = (e) => { 100 | const target = e.target; 101 | const activeFontFamily = target.value; 102 | if (!activeFontFamily) { 103 | throw Error(`Missing font family in clicked font button`); 104 | } 105 | this.setActiveFontFamily(activeFontFamily); 106 | }; 107 | 108 | setActiveFontFamily = (activeFontFamily) => { 109 | this.fontManager.setActiveFont(activeFontFamily); 110 | }; 111 | 112 | generateFontList = (fonts) => { 113 | const { activeFontFamily } = this.props; 114 | const { loadingStatus } = this.state; 115 | 116 | if (loadingStatus !== "finished") { 117 | return
; 118 | } 119 | 120 | const fontList = fonts.map((font) => { 121 | const isActive = font.family === activeFontFamily; 122 | const fontId = getFontId(font.family); 123 | return ( 124 | 130 | {font.family} 131 | 132 | ); 133 | }); 134 | 135 | return fontList; 136 | }; 137 | 138 | render = () => { 139 | const { activeFontFamily } = this.props; 140 | const { loadingStatus } = this.state; 141 | 142 | const fonts = Array.from(this.fontManager.getFonts().values()); 143 | fonts.sort((font1, font2) => font1.family.localeCompare(font2.family)); 144 | 145 | return ( 146 | 147 | 161 | {loadingStatus === "error" && ( 162 | An error ocurred loading the fonts. 163 | )} 164 | 165 | ); 166 | }; 167 | } 168 | -------------------------------------------------------------------------------- /web-editor/src/component/GoogleMapsStyleDialog.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogActions, 4 | DialogContent, 5 | DialogTitle, 6 | Link, 7 | List, 8 | ListItem, 9 | } from "@mui/material"; 10 | import * as React from "react"; 11 | 12 | export const GoogleMapsStyleDialog = ({ open, setOpen }) => { 13 | return ( 14 |
15 | setOpen(false)}> 16 | setOpen(false)}> 17 | About Google Maps Styles 18 | 19 | 20 | In order to preview and generate this overlay, you need to provide a 21 | style string, you can generate one in these sites: 22 |
23 | 24 | 25 | 26 | Style with Google 27 | 28 | 29 | 30 | 31 | Style with Snazzymaps 32 | 33 | 34 | 35 | You can use [] to use the default style.
36 | {"Please review the "} 37 | 41 | Google attribution requirements 42 | 43 | . 44 |
45 | 46 | 47 |
48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /web-editor/src/component/IndicatorSetting.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Stack, 3 | Accordion, 4 | AccordionSummary, 5 | AccordionDetails, 6 | } from "@mui/material"; 7 | import ColorPickerToggle from "./ColorPickerToggle"; 8 | import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; 9 | import ColorLensIcon from "@mui/icons-material/ColorLens"; 10 | import { NumberTextField } from "./NumberTextField"; 11 | import BorderOuterIcon from "@mui/icons-material/BorderOuter"; 12 | import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; 13 | 14 | export const IndicatorSetting = ({ indicatorStyle, setIndicatorStyle }) => { 15 | return ( 16 | 21 | } 23 | aria-controls="panel1a-content" 24 | id="panel1a-header" 25 | > 26 | Indicator Style 27 | 28 | 29 | 30 | 31 | 32 | 33 | { 36 | setIndicatorStyle({ 37 | ...indicatorStyle, 38 | backgroundColor: `rgba(${color.r}, ${color.g}, ${color.b}, ${color.a})`, 39 | }); 40 | }} 41 | /> 42 | 43 | { 46 | setIndicatorStyle({ 47 | ...indicatorStyle, 48 | width, 49 | }); 50 | }} 51 | endAdornmentUnit="px" 52 | prefixIcon={} 53 | tooltipTitle="Width" 54 | /> 55 | { 58 | setIndicatorStyle({ 59 | ...indicatorStyle, 60 | height, 61 | }); 62 | }} 63 | endAdornmentUnit="px" 64 | prefixIcon={ 65 | 66 | } 67 | tooltipTitle="Height" 68 | /> 69 | { 72 | setIndicatorStyle({ 73 | ...indicatorStyle, 74 | borderRadius, 75 | }); 76 | }} 77 | endAdornmentUnit="%" 78 | prefixIcon={} 79 | tooltipTitle="Border Radius" 80 | /> 81 | 82 | 83 | 84 | 85 | ); 86 | }; 87 | -------------------------------------------------------------------------------- /web-editor/src/component/MultipleSelect.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Chip, 4 | FormControl, 5 | InputLabel, 6 | Select, 7 | MenuItem, 8 | Input, 9 | } from "@mui/material"; 10 | import * as React from "react"; 11 | 12 | function MultipleSelect({ selected, setSelected, inputLabel, fullSet }) { 13 | const handleSelectChange = (event) => { 14 | setSelected([...event.target.value]); 15 | }; 16 | 17 | const rest = fullSet.filter((item) => !selected.includes(item)); 18 | 19 | return ( 20 | 21 | 22 | {inputLabel} 23 | 50 | 51 | 52 | ); 53 | } 54 | 55 | export default MultipleSelect; 56 | -------------------------------------------------------------------------------- /web-editor/src/component/NavigationDrawer.jsx: -------------------------------------------------------------------------------- 1 | import AccessTimeIcon from "@mui/icons-material/AccessTime"; 2 | import ExploreIcon from "@mui/icons-material/Explore"; 3 | import HeightIcon from "@mui/icons-material/Height"; 4 | import HomeIcon from "@mui/icons-material/Home"; 5 | import MapIcon from "@mui/icons-material/Map"; 6 | import MyLocationIcon from "@mui/icons-material/MyLocation"; 7 | import SpeedIcon from "@mui/icons-material/Speed"; 8 | import StraightenIcon from "@mui/icons-material/Straighten"; 9 | import ThermostatIcon from "@mui/icons-material/Thermostat"; 10 | import Box from "@mui/material/Box"; 11 | import Divider from "@mui/material/Divider"; 12 | import List from "@mui/material/List"; 13 | import ListItem from "@mui/material/ListItem"; 14 | import ListItemIcon from "@mui/material/ListItemIcon"; 15 | import ListItemText from "@mui/material/ListItemText"; 16 | import SwipeableDrawer from "@mui/material/SwipeableDrawer"; 17 | import * as React from "react"; 18 | import { Link } from "react-router-dom"; 19 | import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; 20 | 21 | export const NavigationDrawer = ({ open, setOpen }) => { 22 | const mapActions = [ 23 | { icon: , name: "Home", link: "/" }, 24 | { icon: , name: "Mapbox", link: "/mapbox" }, 25 | { icon: , name: "Google Maps", link: "/googlemap" }, 26 | { 27 | icon: , 28 | name: "Google Street View", 29 | link: "/googlestreetview", 30 | }, 31 | ]; 32 | 33 | const actions = [ 34 | { icon: , name: "Neighborhood" }, 35 | { icon: , name: "Clock" }, 36 | { icon: , name: "Weather" }, 37 | { icon: , name: "Speed" }, 38 | { icon: , name: "Heading" }, 39 | { icon: , name: "Distance" }, 40 | { icon: , name: "Altitude" }, 41 | { 42 | icon: , 43 | name: "Inclination", 44 | }, 45 | ]; 46 | 47 | return ( 48 | setOpen(false)} 51 | onOpen={() => setOpen(true)} 52 | > 53 | 59 | 60 | {mapActions.map((action, index) => ( 61 | 66 | 67 | {action.icon} 68 | 69 | 70 | 71 | ))} 72 | 73 | 74 | 75 | 76 | 77 | {actions.map((action, index) => ( 78 | 83 | 84 | {action.icon} 85 | 86 | 87 | 88 | ))} 89 | 90 | 91 | 92 | ); 93 | }; 94 | -------------------------------------------------------------------------------- /web-editor/src/component/NumberTextField.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Tooltip, Input, InputAdornment } from "@mui/material"; 3 | 4 | export const NumberTextField = ({ 5 | value, 6 | setValue, 7 | endAdornmentUnit, 8 | prefixIcon, 9 | tooltipTitle, 10 | }) => { 11 | return ( 12 | { 17 | setValue(e.target.value); 18 | }} 19 | endAdornment={ 20 | {endAdornmentUnit} 21 | } 22 | startAdornment={ 23 | 24 | {prefixIcon} 25 | 26 | } 27 | /> 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /web-editor/src/component/OverlayExportPanel.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Stack } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import * as React from "react"; 4 | import { CopyIconTextField } from "./CopyIconTextField"; 5 | 6 | function OverlayExportPanel({ 7 | isExportable, 8 | url, 9 | streamElementExportable, 10 | iFrameTag, 11 | textDivCSS, 12 | type, 13 | }) { 14 | const isToExportMap = type.includes("map"); 15 | 16 | var properties; 17 | if (textDivCSS) { 18 | properties = [ 19 | `color: ${textDivCSS.textColor}`, 20 | `font-size: ${textDivCSS.fontSize}px`, 21 | `font-family: ${textDivCSS.fontFamily}`, 22 | `font-weight: ${textDivCSS.isBold ? "bold" : "normal"}`, 23 | `font-style: ${textDivCSS.isItalic ? "italic" : "normal"}`, 24 | `transform: rotate(${textDivCSS.rotation}deg)`, 25 | `background-color: ${textDivCSS.backgroundColor}`, 26 | `border-color: ${textDivCSS.borderColor}`, 27 | `border-width: ${textDivCSS.borderWidth}px`, 28 | "border-style: solid", 29 | `text-align: ${textDivCSS.textAlign}`, 30 | `border-radius: ${textDivCSS.border_top_left_radius}% ${textDivCSS.border_top_right_radius}% ${textDivCSS.border_bottom_right_radius}% ${textDivCSS.border_bottom_left_radius}%`, 31 | `padding: ${textDivCSS.padding}px`, 32 | `-webkit-text-stroke-width: ${textDivCSS.strokeWidth}px`, 33 | `-webkit-text-stroke-color: ${textDivCSS.strokeColor}`, 34 | `text-shadow: ${textDivCSS.hShadow}px ${textDivCSS.vShadow}px ${textDivCSS.blurRadius}px ${textDivCSS.shadowColor}`, 35 | ].join(";\n"); 36 | const fontLink = `https://fonts.googleapis.com/css2?family=${textDivCSS.fontFamily}&display=swap`; 37 | const encodedProperties = btoa(properties); 38 | const encodedFontLink = btoa(fontLink); 39 | 40 | url += `&css=${encodedProperties}&font=${encodedFontLink}`; 41 | } 42 | 43 | const getCSS = () => { 44 | const css = `@import url('https://fonts.googleapis.com/css2?family=${textDivCSS.fontFamily}&display=swap'); 45 | body {\n${properties}\n}`; 46 | 47 | return ( 48 | 49 | ); 50 | }; 51 | 52 | console.log("url", url); 53 | 54 | return ( 55 | 62 | 94 | 95 | ); 96 | } 97 | 98 | export default OverlayExportPanel; 99 | -------------------------------------------------------------------------------- /web-editor/src/component/OverlayPreview.jsx: -------------------------------------------------------------------------------- 1 | import { CardContent } from "@mui/material"; 2 | import Card from "@mui/material/Card"; 3 | import CardActions from "@mui/material/CardActions"; 4 | import CardMedia from "@mui/material/CardMedia"; 5 | import Typography from "@mui/material/Typography"; 6 | import * as React from "react"; 7 | import { Link } from "react-router-dom"; 8 | 9 | export const OverlayPreview = ({ name, route, href, image, text }) => { 10 | const content = ( 11 | 12 | {image && ( 13 | 21 | )} 22 | {text && ( 23 | 33 | 34 | {text} 35 | 36 | 37 | )} 38 | 39 | 40 | {name} 41 | 42 | 43 | 44 | ); 45 | 46 | if (href != null) { 47 | return ( 48 | 49 | {content} 50 | 51 | ); 52 | } 53 | 54 | return ( 55 | 56 | {content} 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /web-editor/src/component/PullKeyInput.jsx: -------------------------------------------------------------------------------- 1 | import KeyIcon from "@mui/icons-material/Key"; 2 | import { Box, InputAdornment } from "@mui/material"; 3 | import TextField from "@mui/material/TextField"; 4 | import * as React from "react"; 5 | 6 | function PullKeyInput({ pullKey, onKeyChange }) { 7 | return ( 8 | 15 | e.key === "Enter" && e.preventDefault()} 25 | onChange={(e) => { 26 | onKeyChange(e.target.value); 27 | }} 28 | InputProps={{ 29 | startAdornment: ( 30 | 31 | 32 | 33 | ), 34 | }} 35 | sx={{ 36 | width: "95%", 37 | }} 38 | /> 39 | 40 | ); 41 | } 42 | 43 | export default PullKeyInput; 44 | -------------------------------------------------------------------------------- /web-editor/src/component/TextOverlayPreview.jsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material"; 2 | import * as React from "react"; 3 | 4 | function TextOverlayPreview({ textDivCSS, text }) { 5 | return ( 6 | 7 |
16 |
34 | {text} 35 |
36 |
37 |
38 | ); 39 | } 40 | 41 | export default TextOverlayPreview; 42 | -------------------------------------------------------------------------------- /web-editor/src/component/ZoomSlider.jsx: -------------------------------------------------------------------------------- 1 | import ZoomInIcon from "@mui/icons-material/ZoomIn"; 2 | import Box from "@mui/material/Box"; 3 | import Grid from "@mui/material/Grid"; 4 | import MuiInput from "@mui/material/Input"; 5 | import Slider from "@mui/material/Slider"; 6 | import Typography from "@mui/material/Typography"; 7 | import * as React from "react"; 8 | export const ZoomSlider = ({ 9 | zoomValue, 10 | minZoomLevel, 11 | maxZoomLevel, 12 | onZoomChange, 13 | }) => { 14 | const handleSliderChange = (_, newValue) => { 15 | onZoomChange(newValue); 16 | }; 17 | const handleInputChange = (event) => { 18 | onZoomChange(event.target.value === "" ? "" : Number(event.target.value)); 19 | }; 20 | const handleBlur = () => { 21 | if (zoomValue < minZoomLevel) { 22 | onZoomChange(minZoomLevel); 23 | } else if (zoomValue > maxZoomLevel) { 24 | onZoomChange(maxZoomLevel); 25 | } 26 | }; 27 | 28 | return ( 29 | 30 | 35 | Zoom level 36 | 37 | 38 | 39 | 40 | 41 | 42 | 56 | 57 | 58 | 71 | 72 | 73 | 74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /web-editor/src/component/google-maps/GoogleAPIKeyDialog.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { 3 | Box, 4 | Stepper, 5 | Step, 6 | StepLabel, 7 | Button, 8 | Dialog, 9 | DialogActions, 10 | DialogContent, 11 | DialogTitle, 12 | Typography, 13 | Card, 14 | CardMedia, 15 | CardContent, 16 | } from "@mui/material"; 17 | 18 | export const GoogleAPIKeyDialog = ({ open, setOpen }) => { 19 | const [activeStep, setActiveStep] = React.useState(0); 20 | 21 | const handleNext = () => { 22 | setActiveStep((prevActiveStep) => prevActiveStep + 1); 23 | }; 24 | 25 | const handleBack = () => { 26 | setActiveStep((prevActiveStep) => prevActiveStep - 1); 27 | }; 28 | 29 | const steps = [ 30 | { 31 | title: "Create GCP Project", 32 | description: 33 | "Create a new project in Google Cloud Platform by clicking the Create Project button. Name your project or leave it as default and click Create.", 34 | }, 35 | { 36 | title: "Create Credentials", 37 | description: 38 | "Search Google maps platform on the search bar and click Create Credentials and select API Key.", 39 | }, 40 | { 41 | title: "Restrict API Key", 42 | description: 43 | "Select HTTP referrer option. Click on Add Item and input your map URL.", 44 | }, 45 | { 46 | title: "Enable Billing", 47 | description: 48 | "Search Billing on the search bar and configure your setting. Billing is required to use the API Key. You can learn more about billing in the Google Cloud Platform Pricing Guide.", 49 | }, 50 | ]; 51 | 52 | return ( 53 |
54 | setOpen(false)}> 55 | setOpen(false)}> 56 | About Google Maps API Key 57 | 58 | 59 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 79 | 80 | 81 | 88 | 89 | 90 | 91 |
92 | ); 93 | }; 94 | 95 | function SimpleCard({ steps, activeStep }) { 96 | return ( 97 | <> 98 | 99 | 106 | 107 | {steps[activeStep].description} 108 | 109 | 110 | 111 | ); 112 | } 113 | 114 | function HorizontalLinearStepper({ steps, activeStep }) { 115 | return ( 116 | 117 | 118 | {steps.map((label, index) => { 119 | return ( 120 | 121 | {label.title} 122 | 123 | ); 124 | })} 125 | 126 | 127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /web-editor/src/component/google-maps/GoogleMapsContainer.jsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const containerStyle = { 4 | width: "100%", 5 | height: "100%", 6 | }; 7 | 8 | export const GoogleMapsContainer = ({ 9 | canPreview, 10 | mapStyle, 11 | apiKey, 12 | zoom, 13 | indicatorStyle, 14 | }) => { 15 | console.log("indicatorStyle", indicatorStyle); 16 | const uri = `google_preview.html?api_key=${apiKey}&style=${mapStyle}&zoom=${zoom}`; 17 | 18 | return ( 19 |
28 | {canPreview ? ( 29 | <> 30 | 36 |
47 | 48 | ) : ( 49 |
58 |

Unable to preview, please verify the data provided

59 |
60 | )} 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /web-editor/src/component/google-maps/GoogleStreetViewSettings.jsx: -------------------------------------------------------------------------------- 1 | import KeyIcon from "@mui/icons-material/Key"; 2 | import { InputAdornment } from "@mui/material"; 3 | import Box from "@mui/material/Box"; 4 | import Divider from "@mui/material/Divider"; 5 | import Stack from "@mui/material/Stack"; 6 | import TextField from "@mui/material/TextField"; 7 | import Typography from "@mui/material/Typography"; 8 | import React from "react"; 9 | import PullKeyInput from "../PullKeyInput"; 10 | 11 | export const GoogleStreetViewSettings = ({ 12 | pullKey, 13 | onPullKeyChange, 14 | apiKey, 15 | onApiKeyChange, 16 | }) => { 17 | return ( 18 | 29 | } 31 | spacing={2} 32 | textAlign="left" 33 | > 34 | 35 | Settings 36 | 37 | 38 | 39 | 40 | 41 | e.key === "Enter" && e.preventDefault()} 51 | onChange={(e) => onApiKeyChange(e.target.value)} 52 | InputProps={{ 53 | startAdornment: ( 54 | 55 | 56 | 57 | ), 58 | }} 59 | /> 60 | 61 | 62 | 63 | ); 64 | }; 65 | -------------------------------------------------------------------------------- /web-editor/src/component/mapbox/AttributionDialog.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogActions, 4 | DialogContent, 5 | DialogTitle, 6 | Link, 7 | Typography, 8 | } from "@mui/material"; 9 | import * as React from "react"; 10 | 11 | export const AttributionDialog = ({ open, setOpen }) => { 12 | return ( 13 |
14 | setOpen(false)}> 15 | setOpen(false)}> 16 | About Map Attribution 17 | 18 | 19 | 20 | Please be aware that if you disable the map attribution, you might 21 | need to add attribution manually to your stream. 22 | 23 | 24 | Please review the{" "} 25 | 26 | Mapbox attribution requirements 27 | 28 | . 29 | 30 | 31 | Muxable does not provide legal support if you are contacted 32 | regarding an attribution violation. 33 | 34 | 35 | 36 | 37 | 38 |
39 | ); 40 | }; 41 | -------------------------------------------------------------------------------- /web-editor/src/component/mapbox/MapboxContainer.jsx: -------------------------------------------------------------------------------- 1 | import "mapbox-gl/dist/mapbox-gl.css"; 2 | import * as React from "react"; 3 | import MapGL from "react-map-gl"; 4 | 5 | export default function MapboxContainer({ 6 | mapStyle, 7 | apiKey, 8 | canPreview, 9 | zoom, 10 | setZoom, 11 | fullscreen, 12 | indicatorStyle, 13 | }) { 14 | const mapRef = React.useRef(); 15 | const [viewState, setViewState] = React.useState({ 16 | longitude: -100, 17 | latitude: 40, 18 | }); 19 | 20 | React.useEffect(() => { 21 | if (mapRef.current) { 22 | mapRef.current.resize(); 23 | } 24 | }, [fullscreen]); 25 | 26 | return ( 27 |
36 | {canPreview ? ( 37 | <> 38 | setZoom(evt.viewState.zoom)} 46 | {...viewState} 47 | mapStyle={mapStyle} 48 | styleDiffing 49 | onMove={(evt) => 50 | setViewState({ 51 | latitude: evt.viewState.latitude, 52 | longitude: evt.viewState.longitude, 53 | }) 54 | } 55 | mapboxAccessToken={apiKey} 56 | /> 57 |
68 | 69 | ) : ( 70 |
71 |

Unable to preview, please verify the data provided

72 |
73 | )} 74 |
75 | ); 76 | } 77 | -------------------------------------------------------------------------------- /web-editor/src/component/mapbox/StyleIDHelperDialog.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | Dialog, 3 | DialogActions, 4 | DialogContent, 5 | DialogTitle, 6 | Link, 7 | } from "@mui/material"; 8 | import * as React from "react"; 9 | 10 | export const StyleIDHelperDialog = ({ open, setOpen }) => { 11 | return ( 12 |
13 | setOpen(false)}> 14 | setOpen(false)}> 15 | About Mapbox Style ID 16 | 17 | 18 | In order to preview the map, you need to provide a styleID.
19 | The default for our preview is mapbox/streets-v11 20 |

21 | Other options are:
22 | mapbox/outdoors-v11
23 | mapbox/light-v10
24 | mapbox/dark-v10
25 | adoucett/cjf5k84bp0p7t2rmiwvwikhyn
26 |
27 | We can only preview styleID's that are either public or accesible with 28 | your Mapbox API access token.
29 | Supply your own styleID in the format of:
30 | account/styleID 31 |
32 |
33 | A note on the map watermarks: it's tempting to crop them out 34 | with OBS, however most mapping providers require displaying 35 | attribution, even when used in non-interactive video. Verify you are 36 | meeting the relevant attribution requirements for your mapping 37 | provider. Muxable does not provide legal support if you are contacted 38 | regarding an attribution violation. 39 |
40 |
41 | Please review the
42 | 43 | Mapbox attribution requirements 44 | 45 | . 46 |
47 | 48 | 49 |
50 |
51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /web-editor/src/firebase.js: -------------------------------------------------------------------------------- 1 | import { initializeApp } from "firebase/app"; 2 | 3 | const firebaseConfig = { 4 | apiKey: "AIzaSyBPLQyZHIWvRazDdhm2-ymNf-a_1wqts4c", 5 | authDomain: "rtirl-a1d7f.firebaseapp.com", 6 | databaseURL: "https://rtirl-a1d7f-default-rtdb.firebaseio.com", 7 | projectId: "rtirl-a1d7f", 8 | storageBucket: "rtirl-a1d7f.appspot.com", 9 | messagingSenderId: "684852107701", 10 | appId: "1:684852107701:web:ddcd7410021f2f8a9a61fc", 11 | measurementId: "G-TQNVW5X4GN", 12 | }; 13 | 14 | export const firebaseApp = initializeApp(firebaseConfig); 15 | -------------------------------------------------------------------------------- /web-editor/src/fonts/Hanson-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/fonts/Hanson-Bold.ttf -------------------------------------------------------------------------------- /web-editor/src/fonts/OskariG2-Book.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/fonts/OskariG2-Book.ttf -------------------------------------------------------------------------------- /web-editor/src/fonts/OskariG2-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/fonts/OskariG2-SemiBold.ttf -------------------------------------------------------------------------------- /web-editor/src/hooks/useIndicatorStyle.js: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | export default function useIndicatorStyle() { 4 | // background-color: cyan; 5 | // height: 12px; 6 | // width: 12px; 7 | // position: absolute; 8 | // border-radius: 50%; 9 | // top: 119px; 10 | // left: 144px; 11 | // box-shadow: 0 0 10px cyan; 12 | 13 | const [indicatorStyle, setIndicatorStyle] = useState({ 14 | height: 12, 15 | width: 12, 16 | borderRadius: 50, 17 | backgroundColor: "cyan", 18 | }); 19 | 20 | return [indicatorStyle, setIndicatorStyle]; 21 | } 22 | -------------------------------------------------------------------------------- /web-editor/src/hooks/usePullKey.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const PULL_KEY_CHARSET = "abcdefghijklmnopqrstuvwxyz0123456789"; 4 | 5 | export default function usePullKey(initialPullKey) { 6 | const [pullKey, setPullKey] = React.useState({ 7 | value: initialPullKey, 8 | valid: validatePullKey(initialPullKey), 9 | }); 10 | 11 | const onPullKeyChange = (pullKey) => { 12 | setPullKey({ value: pullKey, valid: validatePullKey(pullKey) }); 13 | }; 14 | 15 | return [pullKey, onPullKeyChange]; 16 | } 17 | 18 | function validatePullKey(key) { 19 | key = key.trim(); 20 | if (key.length !== 16) { 21 | return false; 22 | } 23 | let checksum = 13; 24 | for (let i = 0; i < 15; i++) { 25 | const index = PULL_KEY_CHARSET.indexOf(key[i]); 26 | checksum += index; 27 | } 28 | const checksumKey = PULL_KEY_CHARSET.charAt( 29 | checksum % PULL_KEY_CHARSET.length 30 | ); 31 | const lastChar = key[key.length - 1]; 32 | return checksumKey === lastChar; 33 | } 34 | -------------------------------------------------------------------------------- /web-editor/src/hooks/useStyle.js: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export default function useStyle() { 4 | const [textDivCSS, setTextDivCSS] = React.useState({ 5 | textColor: "rgba(223, 251, 38, 1)", 6 | fontFamily: "Open Sans", 7 | rotation: 0, 8 | fontSize: 30, 9 | isBold: false, 10 | isItalic: false, 11 | backgroundColor: "rgba(255, 255, 255, 0)", 12 | borderColor: "rgba(255, 255, 255, 1)", 13 | borderWidth: 0, 14 | borderStyle: "solid", 15 | padding: 0, 16 | border_top_left_radius: 0, 17 | border_top_right_radius: 0, 18 | border_bottom_left_radius: 0, 19 | border_bottom_right_radius: 0, 20 | textAlign: "left", 21 | strokeColor: "rgba(255, 255, 255, 1)", 22 | strokeWidth: 0, 23 | hShadow: 0, 24 | vShadow: 0, 25 | blurRadius: 0, 26 | shadowColor: "rgba(255, 255, 255, 1)", 27 | }); 28 | 29 | return [textDivCSS, setTextDivCSS]; 30 | } 31 | -------------------------------------------------------------------------------- /web-editor/src/images/altitude.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/background.png -------------------------------------------------------------------------------- /web-editor/src/images/clock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/cortex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/cortex.png -------------------------------------------------------------------------------- /web-editor/src/images/discord.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/discord.png -------------------------------------------------------------------------------- /web-editor/src/images/distance.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/gcp_key_steps/step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/gcp_key_steps/step1.png -------------------------------------------------------------------------------- /web-editor/src/images/gcp_key_steps/step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/gcp_key_steps/step2.png -------------------------------------------------------------------------------- /web-editor/src/images/gcp_key_steps/step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/gcp_key_steps/step3.png -------------------------------------------------------------------------------- /web-editor/src/images/gcp_key_steps/step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/gcp_key_steps/step4.png -------------------------------------------------------------------------------- /web-editor/src/images/google_maps.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/google_maps.jpg -------------------------------------------------------------------------------- /web-editor/src/images/heading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/inclination.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web-editor/src/images/mapbox.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/mapbox.jpg -------------------------------------------------------------------------------- /web-editor/src/images/mph.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/neighborhood.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/images/streetview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/muxable/rtirl-obs/6c335fc68cab056ca0d646c7c916631ae96ab5b6/web-editor/src/images/streetview.png -------------------------------------------------------------------------------- /web-editor/src/images/temperature.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | background-color: black; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /web-editor/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import reportWebVitals from "./reportWebVitals"; 6 | import { BrowserRouter } from "react-router-dom"; 7 | 8 | const root = ReactDOM.createRoot(document.getElementById("root")); 9 | root.render( 10 | 11 | 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want to start measuring performance in your app, pass a function 18 | // to log results (for example: reportWebVitals(console.log)) 19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 20 | reportWebVitals(); 21 | -------------------------------------------------------------------------------- /web-editor/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /web-editor/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /web-editor/src/screen/AltitudeEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Typography } from "@mui/material"; 2 | import { useState } from "react"; 3 | import ExclusiveToggle from "../component/ExclusiveToggle"; 4 | import OverlayExportPanel from "../component/OverlayExportPanel"; 5 | import PullKeyInput from "../component/PullKeyInput"; 6 | import TextOverlayPreview from "../component/TextOverlayPreview"; 7 | import { TextSettings } from "../component/TextSettings"; 8 | import { scrollbarStyles } from "../theme/editorTheme"; 9 | 10 | const unitOptions = [ 11 | { name: "Feet", value: "feet" }, 12 | { name: "Meters", value: "meters" }, 13 | ]; 14 | 15 | function AltitudeEditor({ 16 | pullKey, 17 | onPullKeyChange, 18 | textStyle, 19 | onTextStyleChange, 20 | }) { 21 | const [units, setUnits] = useState("feet"); 22 | const url = `https://overlays.rtirl.com/altitude/${units}.html?key=${pullKey.value}`; 23 | 24 | return ( 25 | 26 | 36 | 46 | 47 | 48 | 49 | 50 | Export 51 | 52 | 60 | 61 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export default AltitudeEditor; 83 | -------------------------------------------------------------------------------- /web-editor/src/screen/ClockEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, MenuItem, Select } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import { DateTime } from "luxon"; 4 | import { useState } from "react"; 5 | import CountryPicker from "../component/CountryPicker"; 6 | import OverlayExportPanel from "../component/OverlayExportPanel"; 7 | import PullKeyInput from "../component/PullKeyInput"; 8 | import TextOverlayPreview from "../component/TextOverlayPreview"; 9 | import { TextSettings } from "../component/TextSettings"; 10 | import { scrollbarStyles } from "../theme/editorTheme"; 11 | 12 | function ClockEditor({ 13 | pullKey, 14 | onPullKeyChange, 15 | textStyle, 16 | onTextStyleChange, 17 | }) { 18 | const [format, setFormat] = useState("tt"); 19 | const [lang, setLang] = useState("en"); 20 | 21 | const url = `https://overlays.rtirl.com/datetime/luxon.html?key=${pullKey.value}&lang=${lang}&format=${format}`; 22 | 23 | const standaloneToken = [ 24 | { token: "D", hint: "Date" }, 25 | { token: "DD", hint: "Date" }, 26 | { token: "DDD", hint: "Date" }, 27 | { token: "DDDD", hint: "Date" }, 28 | { token: "t", hint: "Time (12-hour)" }, 29 | { token: "tt", hint: "Time (12-hour)" }, 30 | { token: "ttt", hint: "Time (12-hour)" }, 31 | { token: "tttt", hint: "Time (12-hour)" }, 32 | { token: "T", hint: "Time (24-hour)" }, 33 | { token: "TT", hint: "Time (24-hour)" }, 34 | { token: "TTT", hint: "Time (24-hour)" }, 35 | { token: "TTTT", hint: "Time (24-hour)" }, 36 | { token: "f", hint: "Date and time (12-hour)" }, 37 | { token: "ff", hint: "Date and time (12-hour)" }, 38 | { token: "fff", hint: "Date and time (12-hour)" }, 39 | { token: "ffff", hint: "Date and time (12-hour)" }, 40 | { token: "F", hint: "Date and time with seconds (12-hour)" }, 41 | { token: "FF", hint: "Date and time with seconds (12-hour)" }, 42 | { token: "FFF", hint: "Date and time with seconds (12-hour)" }, 43 | { token: "FFFF", hint: "Date and time with seconds (12-hour)" }, 44 | { token: "ss", hint: "Seconds" }, 45 | { token: "mm", hint: "Minutes" }, 46 | { token: "hh", hint: "Hours (12-hour)" }, 47 | { token: "HH", hint: "Hours (24-hour)" }, 48 | { token: "ZZZZ", hint: "Time zone" }, 49 | { token: "ZZZZZ", hint: "Full time zone" }, 50 | { token: "a", hint: "AM/PM" }, 51 | { token: "d", hint: "Day of the month" }, 52 | { token: "dd", hint: "Day of the month with zero" }, 53 | { token: "ccc", hint: "Day of the week" }, 54 | { token: "cccc", hint: "Full day of the week" }, 55 | { token: "L", hint: "Month number" }, 56 | { token: "LL", hint: "Month number with zero" }, 57 | { token: "LLL", hint: "Month" }, 58 | { token: "LLLL", hint: "Full month" }, 59 | { token: "y", hint: "Year" }, 60 | { token: "yy", hint: "Year (two digits)" }, 61 | ]; 62 | 63 | const luxonCountries = [ 64 | "en", 65 | "es", 66 | "fr", 67 | "it", 68 | "de", 69 | "nl", 70 | "pt", 71 | "ru", 72 | "ja", 73 | "zh", 74 | "ko", 75 | "el", 76 | "tr", 77 | "ar", 78 | "he", 79 | "id", 80 | "th", 81 | "vi", 82 | "pl", 83 | "sv", 84 | "hu", 85 | "fi", 86 | "da", 87 | "no", 88 | "is", 89 | "cs", 90 | "sk", 91 | "sl", 92 | "hr", 93 | "bg", 94 | "ro", 95 | "lt", 96 | "lv", 97 | "et", 98 | ]; 99 | 100 | const time = DateTime.now().setLocale(lang); 101 | 102 | return ( 103 | 104 | 114 | 124 | 125 | 126 | 127 | 128 | Export 129 | 130 | 137 | 138 | 139 | 140 | Format 141 | 142 | 175 | 176 | 177 | 178 | Language 179 | 180 | 185 | 186 | 190 | 191 | 192 | 193 | 194 | 198 | 199 | 200 | 201 | ); 202 | } 203 | 204 | export default ClockEditor; 205 | -------------------------------------------------------------------------------- /web-editor/src/screen/CyclingCadenceEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Typography } from "@mui/material"; 2 | import OverlayExportPanel from "../component/OverlayExportPanel"; 3 | import PullKeyInput from "../component/PullKeyInput"; 4 | import TextOverlayPreview from "../component/TextOverlayPreview"; 5 | import { TextSettings } from "../component/TextSettings"; 6 | import { scrollbarStyles } from "../theme/editorTheme"; 7 | 8 | function CyclingCadenceEditor({ 9 | pullKey, 10 | onPullKeyChange, 11 | textStyle, 12 | onTextStyleChange, 13 | }) { 14 | const url = `https://overlays.rtirl.com/cycling_cadence/rpm.html?key=${pullKey.value}`; 15 | 16 | return ( 17 | 18 | 28 | 38 | 39 | 40 | 41 | 42 | Export 43 | 44 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default CyclingCadenceEditor; 68 | -------------------------------------------------------------------------------- /web-editor/src/screen/DistanceEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import { useState } from "react"; 4 | import ExclusiveToggle from "../component/ExclusiveToggle"; 5 | import OverlayExportPanel from "../component/OverlayExportPanel"; 6 | import PullKeyInput from "../component/PullKeyInput"; 7 | import TextOverlayPreview from "../component/TextOverlayPreview"; 8 | import { TextSettings } from "../component/TextSettings"; 9 | import { scrollbarStyles } from "../theme/editorTheme"; 10 | 11 | const unitOptions = [ 12 | { name: "Miles", value: "miles" }, 13 | { name: "Kilometers", value: "km" }, 14 | ]; 15 | 16 | function DistanceEditor({ 17 | pullKey, 18 | onPullKeyChange, 19 | textStyle, 20 | onTextStyleChange, 21 | }) { 22 | const [units, setUnits] = useState("miles"); 23 | const url = `https://overlays.rtirl.com/distance/${units}.html?key=${pullKey.value}`; 24 | 25 | return ( 26 | 27 | 37 | 47 | 48 | 49 | 50 | Export 51 | 52 | 60 | 61 | 67 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | } 81 | 82 | export default DistanceEditor; 83 | -------------------------------------------------------------------------------- /web-editor/src/screen/GoogleMapsEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Stack } from "@mui/material"; 2 | import * as React from "react"; 3 | import { useState } from "react"; 4 | import { GoogleMapsContainer } from "../component/google-maps/GoogleMapsContainer"; 5 | import { GoogleMapsSettings } from "../component/google-maps/GoogleMapsSettings"; 6 | 7 | export default function GoogleMapsEditor({ 8 | pullKey, 9 | onPullKeyChange, 10 | indicatorStyle, 11 | setIndicatorStyle, 12 | }) { 13 | const [mapStyle, setMapStyle] = useState({ value: "", valid: false }); 14 | const [apiKey, setAPIKey] = useState(""); 15 | const [zoom, setZoom] = useState(5); 16 | const [fullscreen, setFullscreen] = useState(false); 17 | const jsonMapStyle = mapStyle.valid ? JSON.parse(mapStyle.value) : {}; 18 | const styleB64 = encodeURIComponent( 19 | window.btoa(JSON.stringify(jsonMapStyle)) 20 | ); 21 | 22 | return ( 23 | 24 | 25 | 39 | 40 | 41 | 42 | 43 | 44 | 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /web-editor/src/screen/GoogleStreetViewEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Stack } from "@mui/material"; 2 | import * as React from "react"; 3 | import { useState } from "react"; 4 | import { GoogleStreetViewSettings } from "../component/google-maps/GoogleStreetViewSettings"; 5 | import OverlayExportPanel from "../component/OverlayExportPanel"; 6 | 7 | export default function GoogleStreetViewEditor({ pullKey, onPullKeyChange }) { 8 | const [apiKey, setAPIKey] = useState(""); 9 | const url = `https://overlays.rtirl.com/streetview/google.html?key=${pullKey.value}&api_key=${apiKey}`; 10 | 11 | return ( 12 | 13 | 14 | 20 | 21 | 22 | 23 | 24 | 25 | {pullKey.valid && apiKey ? ( 26 | 36 | ) : ( 37 |
46 |

Unable to preview, please verify the data provided

47 |
48 | )} 49 | 55 |
56 |
57 |
58 |
59 |
60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /web-editor/src/screen/HeadingEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import { useState } from "react"; 4 | import CountryPicker from "../component/CountryPicker"; 5 | import ExclusiveToggle from "../component/ExclusiveToggle"; 6 | import OverlayExportPanel from "../component/OverlayExportPanel"; 7 | import PullKeyInput from "../component/PullKeyInput"; 8 | import TextOverlayPreview from "../component/TextOverlayPreview"; 9 | import { TextSettings } from "../component/TextSettings"; 10 | import { scrollbarStyles } from "../theme/editorTheme"; 11 | 12 | const headingOptions = [ 13 | { name: "deg\u00B0", value: "deg" }, 14 | { name: "NSEW", value: "nsew" }, 15 | ]; 16 | 17 | function HeadingEditor({ 18 | pullKey, 19 | onPullKeyChange, 20 | textStyle, 21 | onTextStyleChange, 22 | }) { 23 | const [units, setUnits] = useState("deg"); 24 | const [lang, setLang] = useState("en"); 25 | const url = `https://overlays.rtirl.com/heading/${units}.html?key=${pullKey.value}&lang=${lang}`; 26 | 27 | const headingCountries = ["en", "es", "sv", "tr"]; 28 | 29 | return ( 30 | 31 | 41 | 51 | 52 | 53 | 54 | Export 55 | 56 | 63 | 64 | 70 | 71 | 72 | Language 73 | 74 | 79 | 80 | 84 | 85 | 86 | 87 | 88 | 92 | 93 | 94 | 95 | ); 96 | } 97 | 98 | export default HeadingEditor; 99 | -------------------------------------------------------------------------------- /web-editor/src/screen/HeartRateEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Typography } from "@mui/material"; 2 | import PullKeyInput from "../component/PullKeyInput"; 3 | import OverlayExportPanel from "../component/OverlayExportPanel"; 4 | import { TextSettings } from "../component/TextSettings"; 5 | import TextOverlayPreview from "../component/TextOverlayPreview"; 6 | import { scrollbarStyles } from "../theme/editorTheme"; 7 | 8 | function HeartRateEditor({ 9 | pullKey, 10 | onPullKeyChange, 11 | textStyle, 12 | onTextStyleChange, 13 | }) { 14 | const url = `https://overlays.rtirl.com/heart_rate/bpm.html?key=${pullKey.value}`; 15 | 16 | return ( 17 | 18 | 28 | 38 | 39 | 40 | 41 | 42 | Export 43 | 44 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default HeartRateEditor; 68 | -------------------------------------------------------------------------------- /web-editor/src/screen/InclinationEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import OverlayExportPanel from "../component/OverlayExportPanel"; 4 | import PullKeyInput from "../component/PullKeyInput"; 5 | import TextOverlayPreview from "../component/TextOverlayPreview"; 6 | import { TextSettings } from "../component/TextSettings"; 7 | import { scrollbarStyles } from "../theme/editorTheme"; 8 | 9 | function InclinationEditor({ 10 | pullKey, 11 | onPullKeyChange, 12 | textStyle, 13 | onTextStyleChange, 14 | }) { 15 | const url = `https://overlays.rtirl.com/inclination/slope.html?key=${pullKey.value}`; 16 | 17 | return ( 18 | 19 | 29 | 39 | 40 | 41 | 42 | Export 43 | 44 | 51 | 52 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | ); 65 | } 66 | 67 | export default InclinationEditor; 68 | -------------------------------------------------------------------------------- /web-editor/src/screen/MapboxEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Stack } from "@mui/material"; 2 | import * as React from "react"; 3 | import { useEffect, useState } from "react"; 4 | import MapboxContainer from "../component/mapbox/MapboxContainer"; 5 | import MapboxSettings from "../component/mapbox/MapboxSettings"; 6 | import { scrollbarStyles } from "../theme/editorTheme"; 7 | 8 | const mapboxMapStyleJsonCache = []; 9 | 10 | export default function MapboxEditor({ 11 | pullKey, 12 | onPullKeyChange, 13 | indicatorStyle, 14 | setIndicatorStyle, 15 | }) { 16 | const [mapStyle, setMapStyle] = useState(null); 17 | const [apiKey, setAPIKey] = useState( 18 | "pk.eyJ1Ijoia2V2bW8zMTQiLCJhIjoiY2w0MW1qaTh3MG80dzNjcXRndmJ0a2JieiJ9.Y_xABmAqvD-qZeed8MabxQ" 19 | ); 20 | const [mapLibrary, setMapLibrary] = useState("leaflet"); 21 | const [styleId, setStyleID] = useState("mapbox/streets-v11"); 22 | const [zoom, setZoom] = useState(5); 23 | const [fullscreen, setFullscreen] = useState(false); 24 | const [attribution, setAttribution] = useState(false); 25 | const [lang, setLang] = useState("en"); 26 | const [validStyle, setValidStyle] = useState(true); 27 | 28 | useEffect(() => { 29 | fetch(`https://api.mapbox.com/styles/v1/${styleId}?access_token=${apiKey}`) 30 | .then((res) => { 31 | res.status === 200 ? setValidStyle(true) : setValidStyle(false); 32 | return res.json(); 33 | }) 34 | .then((res) => { 35 | mapboxMapStyleJsonCache[styleId] = res; 36 | setMapStyle(res); 37 | }); 38 | }, [styleId, apiKey]); 39 | 40 | return ( 41 | 42 | 52 | 74 | 75 | 76 | 77 | 78 | 79 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /web-editor/src/screen/NeighborhoodEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import { useEffect, useState } from "react"; 4 | import CountryPicker from "../component/CountryPicker"; 5 | import MultipleSelect from "../component/MultipleSelect"; 6 | import OverlayExportPanel from "../component/OverlayExportPanel"; 7 | import PullKeyInput from "../component/PullKeyInput"; 8 | import TextOverlayPreview from "../component/TextOverlayPreview"; 9 | import { TextSettings } from "../component/TextSettings"; 10 | import { scrollbarStyles } from "../theme/editorTheme"; 11 | 12 | function NeighborhoodEditor({ 13 | pullKey, 14 | onPullKeyChange, 15 | textStyle, 16 | onTextStyleChange, 17 | }) { 18 | const [selected, setSelected] = useState(["neighborhood", "place"]); 19 | const [lang, setLang] = useState("en"); 20 | 21 | const [text, setText] = useState("Williamsburg, Brooklyn, NY"); 22 | 23 | const formatURL = () => { 24 | var base = `https://overlays.rtirl.com/neighborhood.html?key=${pullKey.value}&lang=${lang}&format=`; 25 | var formatStr = ""; 26 | /* eslint-disable */ 27 | const mp = { 28 | neighborhood: "${data.neighborhood ? data.neighborhood.text + ', ' : ''}", 29 | region: "${data.region ? data.region.text + ', ' : ''}", 30 | country: "${data.country ? data.country.text + ', ' : ''}", 31 | place: "${data.place ? data.place.text + ', ' : ''}", 32 | postcode: "${data.postcode ? data.postcode.text + ', ' : ''}", 33 | district: "${data.district ? data.district.text + ', ' : ''}", 34 | locality: "${data.locality ? data.locality.text + ', ' : ''}", 35 | poi: "${data.poi ? data.poi.text + ', ' : ''}", 36 | address: "${data.address ? data.address.text + ', ' : ''}", 37 | }; 38 | /* eslint-enable */ 39 | for (let i = 0; i < selected.length; i++) { 40 | var param = selected[i]; 41 | if (i !== selected.length - 1) { 42 | formatStr += encodeURIComponent(mp[param]); 43 | } else { 44 | var last = `\${data.${param} ? data.${param}.text : ''}`; 45 | formatStr += encodeURIComponent(last); 46 | } 47 | } 48 | return base + formatStr; 49 | }; 50 | 51 | useEffect(() => { 52 | const coordinates = [-73.990593, 40.740121]; 53 | const lng = coordinates[0]; 54 | const lat = coordinates[1]; 55 | const context = {}; 56 | fetch( 57 | `https://api.mapbox.com/geocoding/v5/mapbox.places/${lng},${lat}.json?access_token=pk.eyJ1Ijoia2V2bW8zMTQiLCJhIjoiY2w0MW1qaTh3MG80dzNjcXRndmJ0a2JieiJ9.Y_xABmAqvD-qZeed8MabxQ&language=${lang}` 58 | ) 59 | .then((res) => res.json()) 60 | .then((res) => { 61 | console.log(res); 62 | for (var param of [ 63 | "country", 64 | "region", 65 | "postcode", 66 | "district", 67 | "place", 68 | "locality", 69 | "neighborhood", 70 | "address", 71 | "poi", 72 | ]) { 73 | const str = param; 74 | context[param] = res.features.find((feature) => 75 | feature.place_type.includes(str) 76 | ); 77 | } 78 | const ret = []; 79 | for (var p of selected) { 80 | if (context[p]) { 81 | if (p === "country") { 82 | ret.push(context[p]["properties"]["short_code"].toUpperCase()); 83 | } else { 84 | ret.push(context[p]["text"]); 85 | } 86 | } 87 | } 88 | 89 | setText(ret.join(", ")); 90 | }) 91 | .catch((err) => console.log(err)); 92 | }, [selected, lang, setText]); 93 | 94 | const fullSet = [ 95 | "address", 96 | "country", 97 | "region", 98 | "postcode", 99 | "district", 100 | "place", 101 | "locality", 102 | "neighborhood", 103 | "poi", 104 | ]; 105 | 106 | const countries = [ 107 | "en", 108 | "es", 109 | "fr", 110 | "it", 111 | "de", 112 | "nl", 113 | "pt", 114 | "ru", 115 | "ja", 116 | "zh", 117 | "ko", 118 | "el", 119 | "tr", 120 | "ar", 121 | "he", 122 | "id", 123 | "th", 124 | "vi", 125 | "pl", 126 | "sv", 127 | "hu", 128 | "fi", 129 | "da", 130 | "no", 131 | "is", 132 | "cs", 133 | "sk", 134 | "sl", 135 | "hr", 136 | "bg", 137 | "ro", 138 | "lt", 139 | "lv", 140 | "et", 141 | ]; 142 | 143 | return ( 144 | 145 | 155 | 165 | 166 | 167 | 168 | 169 | Export 170 | 171 | 179 | 180 | {/* Select Types */} 181 | 182 | 183 | 184 | {" "} 185 | Format, the order matters{" "} 186 | 187 | 193 | 194 | {/* Select Language */} 195 | 196 | 197 | Language 198 | 199 | 204 | 205 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | ); 218 | } 219 | 220 | export default NeighborhoodEditor; 221 | -------------------------------------------------------------------------------- /web-editor/src/screen/SpeedEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Typography } from "@mui/material"; 2 | import { useState } from "react"; 3 | import ExclusiveToggle from "../component/ExclusiveToggle"; 4 | import OverlayExportPanel from "../component/OverlayExportPanel"; 5 | import PullKeyInput from "../component/PullKeyInput"; 6 | import TextOverlayPreview from "../component/TextOverlayPreview"; 7 | import { TextSettings } from "../component/TextSettings"; 8 | import { scrollbarStyles } from "../theme/editorTheme"; 9 | 10 | const speedOptions = [ 11 | { name: "MPH", value: "mph" }, 12 | { name: "KMH", value: "kmh" }, 13 | { name: "MIN/KM", value: "minperkm" }, 14 | ]; 15 | 16 | function SpeedEditor({ 17 | pullKey, 18 | onPullKeyChange, 19 | textStyle, 20 | onTextStyleChange, 21 | }) { 22 | const [units, setUnits] = useState("mph"); 23 | 24 | const url = `https://overlays.rtirl.com/speed/${units}.html?key=${pullKey.value}`; 25 | 26 | return ( 27 | 28 | 38 | 48 | 49 | 50 | 51 | 52 | Export 53 | 54 | 60 | 61 | 67 | 71 | 72 | 73 | 74 | 75 | 79 | 80 | 81 | 82 | ); 83 | } 84 | 85 | export default SpeedEditor; 86 | -------------------------------------------------------------------------------- /web-editor/src/screen/WeatherEditor.jsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid } from "@mui/material"; 2 | import Typography from "@mui/material/Typography"; 3 | import { useState } from "react"; 4 | import ExclusiveToggle from "../component/ExclusiveToggle"; 5 | import OverlayExportPanel from "../component/OverlayExportPanel"; 6 | import PullKeyInput from "../component/PullKeyInput"; 7 | import TextOverlayPreview from "../component/TextOverlayPreview"; 8 | import { TextSettings } from "../component/TextSettings"; 9 | import { scrollbarStyles } from "../theme/editorTheme"; 10 | 11 | const unitOptions = [ 12 | { name: "Celsius", value: "C" }, 13 | { name: "Fahrenheit", value: "F" }, 14 | ]; 15 | 16 | const modeOptions = [ 17 | { name: "Temperature", value: "temperature" }, 18 | { name: "Feels Like", value: "feels_like" }, 19 | ]; 20 | 21 | function WeatherEditor({ 22 | pullKey, 23 | onPullKeyChange, 24 | textStyle, 25 | onTextStyleChange, 26 | }) { 27 | const [units, setUnits] = useState("C"); 28 | const [mode, setMode] = useState("temperature"); 29 | const url = `https://overlays.rtirl.com/weather/${mode}/${units}.html?key=${pullKey.value}`; 30 | 31 | return ( 32 | 33 | 43 | 53 | 54 | 55 | 56 | 57 | Export 58 | 59 | 66 | 67 | 73 | 79 | 83 | 84 | 85 | 86 | 87 | 91 | 92 | 93 | 94 | ); 95 | } 96 | 97 | export default WeatherEditor; 98 | -------------------------------------------------------------------------------- /web-editor/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | -------------------------------------------------------------------------------- /web-editor/src/theme/editorTheme.jsx: -------------------------------------------------------------------------------- 1 | import { createTheme } from "@mui/material/styles"; 2 | import OskariBook from "../fonts/OskariG2-Book.ttf"; 3 | import OskariSemiBold from "../fonts/OskariG2-SemiBold.ttf"; 4 | 5 | export const scrollbarStyles = { 6 | "&::-webkit-scrollbar": { 7 | width: "0.4em", 8 | }, 9 | "&::-webkit-scrollbar-track": { 10 | boxShadow: "inset 0 0 6px rgba(0,0,0,0.00)", 11 | webkitBoxShadow: "inset 0 0 6px rgba(0,0,0,0.00)", 12 | margin: "0 1em 0 1em", 13 | }, 14 | "&::-webkit-scrollbar-thumb": { 15 | backgroundColor: "rgba(0,0,0,.1)", 16 | outline: "1px solid slategrey", 17 | }, 18 | }; 19 | 20 | const colors = { 21 | primary: { 22 | main: "#262626", 23 | border: "#545454", 24 | contrastText: "white", 25 | }, 26 | secondary: { 27 | main: "#000000", 28 | text: "white", 29 | contrastText: "white", 30 | }, 31 | background: { 32 | default: "black", 33 | contrastText: "white", 34 | }, 35 | text: { 36 | main: "rgba(246, 243, 237, 0.8)", 37 | title: "rgba(246, 243, 237, 1)", 38 | }, 39 | }; 40 | 41 | const editorTheme = createTheme({ 42 | palette: { 43 | mode: "dark", 44 | ...colors, 45 | }, 46 | typography: { 47 | fontFamily: "Oskari G2, Arial", 48 | allVariants: { 49 | color: colors.text.main, 50 | }, 51 | h6: { 52 | color: colors.text.title, 53 | }, 54 | }, 55 | components: { 56 | MuiCssBaseline: { 57 | styleOverrides: ` 58 | @font-face { 59 | font-family: 'Oskari G2'; 60 | src: url(${OskariBook}); 61 | } 62 | 63 | @font-face { 64 | font-family: 'Oskari G2 SemiBold'; 65 | src: url(${OskariSemiBold}); 66 | } 67 | `, 68 | }, 69 | MuiInputAdornment: { 70 | styleOverrides: { 71 | root: { 72 | color: colors.text.main, 73 | }, 74 | }, 75 | }, 76 | MuiTextField: { 77 | styleOverrides: { 78 | root: { 79 | "& label.Mui-focused": { 80 | color: colors.text.title, 81 | }, 82 | "& .MuiInput-underline:after": { 83 | borderBottomColor: colors.text.title, 84 | }, 85 | color: colors.text.main, 86 | }, 87 | }, 88 | }, 89 | MuiAppBar: { 90 | styleOverrides: { 91 | root: { 92 | borderBottom: "1px solid", 93 | borderColor: colors.primary.border, 94 | }, 95 | }, 96 | }, 97 | MuiLink: { 98 | styleOverrides: { 99 | root: { 100 | color: colors.text.main, 101 | }, 102 | underlineAlways: { 103 | textDecoration: "underline", 104 | }, 105 | }, 106 | }, 107 | MuiCheckbox: { 108 | styleOverrides: { 109 | colorPrimary: { 110 | color: colors.text.main, 111 | "&.Mui-checked": { 112 | color: colors.text.main, 113 | }, 114 | }, 115 | }, 116 | }, 117 | }, 118 | }); 119 | 120 | export default editorTheme; 121 | -------------------------------------------------------------------------------- /web-editor/src/theme/homeTheme.jsx: -------------------------------------------------------------------------------- 1 | import { createTheme, responsiveFontSizes } from "@mui/material/styles"; 2 | import HansonBold from "../fonts/Hanson-Bold.ttf"; 3 | import OskariBook from "../fonts/OskariG2-Book.ttf"; 4 | 5 | let homeTheme = createTheme({ 6 | typography: { 7 | fontFamily: "Oskari G2, Arial", 8 | h2: { 9 | fontFamily: "Hanson, Arial", 10 | }, 11 | h4: { 12 | fontFamily: "Hanson, Arial", 13 | }, 14 | h5: { 15 | fontFamily: "Hanson, Arial", 16 | }, 17 | allVariants: { 18 | color: "white", 19 | }, 20 | }, 21 | components: { 22 | MuiCssBaseline: { 23 | styleOverrides: ` 24 | @font-face { 25 | font-family: 'Oskari G2'; 26 | src: url(${OskariBook}); 27 | } 28 | 29 | @font-face { 30 | font-family: 'Hanson'; 31 | src: url(${HansonBold}); 32 | } 33 | `, 34 | }, 35 | }, 36 | 37 | breakpoints: { 38 | values: { 39 | xs: 0, 40 | sm: 425, 41 | md: 768, 42 | lg: 1280, 43 | xl: 1580, 44 | }, 45 | }, 46 | }); 47 | 48 | homeTheme = responsiveFontSizes(homeTheme, { 49 | breakpoints: ["xs", "sm", "md", "lg", "xl"], 50 | factor: 5, 51 | }); 52 | 53 | export default homeTheme; 54 | -------------------------------------------------------------------------------- /web-editor/src/utility.js: -------------------------------------------------------------------------------- 1 | export const clamp = (num, min, max) => Math.min(Math.max(num, min), max); 2 | --------------------------------------------------------------------------------