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 |
30 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/public/googlemaps.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 |
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 |
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 |
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 |
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 |
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 |
43 | {this.state.displayColorPicker ? (
44 |
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 |
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 |
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 | }
28 | disableUnderline
29 | renderValue={(select) => (
30 |
31 | {selected.map((value) => (
32 | e.stopPropagation()}
34 | key={value}
35 | label={value}
36 | onDelete={() => {
37 | setSelected(selected.filter((item) => item !== value));
38 | }}
39 | />
40 | ))}
41 |
42 | )}
43 | >
44 | {rest.map((element) => (
45 |
48 | ))}
49 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------