Restarting from scratch
This commit is contained in:
parent
ed951208e8
commit
a4bd11aab0
|
@ -1,8 +1,8 @@
|
|||
import { CapacitorConfig } from '@capacitor/cli';
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.dyomedea.dyomedea',
|
||||
appName: 'dyomedea',
|
||||
appId: 'com.dyomedea.dyomedea2',
|
||||
appName: 'dyomedea2',
|
||||
webDir: 'build',
|
||||
bundledWebRuntime: false
|
||||
};
|
||||
|
|
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
|
@ -3,59 +3,29 @@
|
|||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@awesome-cordova-plugins/core": "^5.45.0",
|
||||
"@awesome-cordova-plugins/geolocation": "^5.45.0",
|
||||
"@capacitor-community/background-geolocation": "^1.2.4",
|
||||
"@capacitor-community/file-opener": "^1.0.1",
|
||||
"@capacitor/android": "^4.2.0",
|
||||
"@capacitor/app": "4.0.1",
|
||||
"@capacitor/cli": "^4.2.0",
|
||||
"@capacitor/core": "^4.2.0",
|
||||
"@capacitor/filesystem": "^4.1.2",
|
||||
"@capacitor/core": "4.3.0",
|
||||
"@capacitor/haptics": "4.0.1",
|
||||
"@capacitor/keyboard": "4.0.1",
|
||||
"@capacitor/status-bar": "4.0.1",
|
||||
"@chatscope/chat-ui-kit-react": "^1.9.7",
|
||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
||||
"@ionic/react": "^6.2.6",
|
||||
"@ionic/react-router": "^6.2.6",
|
||||
"@openpgp/asmcrypto.js": "^2.3.2",
|
||||
"@reduxjs/toolkit": "^1.8.5",
|
||||
"@ionic/react": "^6.0.0",
|
||||
"@ionic/react-router": "^6.0.0",
|
||||
"@testing-library/jest-dom": "^5.11.9",
|
||||
"@testing-library/react": "^11.2.7",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@trapezedev/configure": "^5.0.6",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^12.6.3",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^12.19.15",
|
||||
"@types/pouchdb": "^6.4.0",
|
||||
"@types/react": "^18.0.18",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"@types/react-router": "^5.1.11",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"buffer": "^6.0.3",
|
||||
"cordova-plugin-geolocation": "^4.1.0",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"docuri": "^4.2.2",
|
||||
"font-gis": "^1.0.5",
|
||||
"git": "^0.1.5",
|
||||
"ionicons": "^6.0.3",
|
||||
"isomorphic-xml2js": "^0.1.3",
|
||||
"lodash": "^4.17.21",
|
||||
"pouchdb": "^7.3.0",
|
||||
"pouchdb-browser": "^7.3.0",
|
||||
"pouchdb-find": "^7.3.0",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-localization": "^1.0.19",
|
||||
"react-pouchdb": "^2.1.0",
|
||||
"react-redux": "^8.0.2",
|
||||
"react-router": "^6.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"status": "^0.0.13",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"typescript": "^4.8.3",
|
||||
"typescript": "^4.1.3",
|
||||
"web-vitals": "^0.2.4",
|
||||
"workbox-background-sync": "^5.1.4",
|
||||
"workbox-broadcast-update": "^5.1.4",
|
||||
|
@ -95,8 +65,7 @@
|
|||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/lodash": "^4.14.184"
|
||||
"@capacitor/cli": "4.3.0"
|
||||
},
|
||||
"description": "An Ionic project",
|
||||
"postinstall": "npx patch-package"
|
||||
"description": "An Ionic project"
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
diff --git a/node_modules/localized-strings/lib/LocalizedStrings.js b/node_modules/localized-strings/lib/LocalizedStrings.js
|
||||
index eefa6c8..e0a3cae 100644
|
||||
--- a/node_modules/localized-strings/lib/LocalizedStrings.js
|
||||
+++ b/node_modules/localized-strings/lib/LocalizedStrings.js
|
||||
@@ -192,8 +192,8 @@ var LocalizedStrings = function () {
|
||||
if (_this4._opts.logsEnabled) {
|
||||
console.log("\uD83D\uDEA7 \uD83D\uDC77 key '" + key + "' not found in localizedStrings for language " + _this4._language + " \uD83D\uDEA7");
|
||||
}
|
||||
- } else if (typeof strings[key] !== "string") {
|
||||
- // It's an object
|
||||
+ } else if (typeof strings[key] !== "string" && defaultStrings[key].$$typeof === undefined) {
|
||||
+ // It's an object (and it's not a React component)
|
||||
_this4._fallbackValues(defaultStrings[key], strings[key]);
|
||||
}
|
||||
});
|
|
@ -1,44 +0,0 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1000"
|
||||
height="2000"
|
||||
viewBox="0 0 250 500"
|
||||
>
|
||||
<g>
|
||||
<circle
|
||||
cx="100"
|
||||
cy="100"
|
||||
r="10"
|
||||
fill='green'
|
||||
stroke='black'
|
||||
stroke-width="1"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>
|
||||
<circle
|
||||
cx="200"
|
||||
cy="200"
|
||||
r="10"
|
||||
fill='green'
|
||||
stroke='black'
|
||||
stroke-width="1"
|
||||
vector-effect="non-scaling-size"
|
||||
/>
|
||||
|
||||
<path
|
||||
d="M 50, 50 l 0.0001 0"
|
||||
stroke-width="10"
|
||||
stroke-linecap="round"
|
||||
stroke="green"
|
||||
vector-effect="non-scaling-stroke"
|
||||
/>
|
||||
|
||||
|
||||
</g>
|
||||
</svg>
|
||||
<!--
|
||||
|
||||
<svg width="921" height="1301"><g transform="scale(256) translate(6.916599514166666,8.07269355334303))"><circle cx="0.7154276391666667" cy="0.6137091783430285" r="10px" fill="green" vector-effect="non-scaling-size"></circle></g></svg>
|
||||
|
||||
<svg width="921" height="1301"><g transform="scale(256) translate(6.916599514166666,8.07269355334303))"><path d="M 0.7154276391666667 0.6137091783430285 l 0.0001 0" vector-effect="non-scaling-stroke" stroke-width="50" stroke-linecap="round" stroke="green"></path></g></svg>
|
||||
|
||||
-->
|
Before Width: | Height: | Size: 1.1 KiB |
|
@ -16,11 +16,7 @@
|
|||
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
|
||||
<link
|
||||
rel="shortcut icon"
|
||||
type="image/png"
|
||||
href="%PUBLIC_URL%/assets/icon/favicon.png"
|
||||
/>
|
||||
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/assets/icon/favicon.png" />
|
||||
|
||||
<!-- add to homescreen for ios -->
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
@ -31,4 +27,5 @@
|
|||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"short_name": "Dyomedea",
|
||||
"name": "Dyomedea",
|
||||
"short_name": "Ionic App",
|
||||
"name": "My Ionic App",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/icon/favicon.png",
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import App from './App';
|
||||
|
||||
test('renders without crashing', () => {
|
||||
const { baseElement } = render(<App />);
|
||||
expect(baseElement).toBeDefined();
|
||||
});
|
67
src/App.tsx
67
src/App.tsx
|
@ -1,9 +1,7 @@
|
|||
import { IonApp, setupIonicReact } from '@ionic/react';
|
||||
|
||||
import store from './store/index';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Suspense } from 'react';
|
||||
import { PouchDB } from 'react-pouchdb';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import Home from './pages/Home';
|
||||
|
||||
/* Core CSS required for Ionic components to work properly */
|
||||
import '@ionic/react/css/core.css';
|
||||
|
@ -24,50 +22,21 @@ import '@ionic/react/css/display.css';
|
|||
/* Theme variables */
|
||||
import './theme/variables.css';
|
||||
|
||||
import Map from './components/map/map';
|
||||
|
||||
setupIonicReact();
|
||||
|
||||
// See https://stackoverflow.com/questions/71538643/property-wakelock-does-not-exist-on-type-navigator
|
||||
const requestWakeLock = async () => {
|
||||
const anyNav: any = navigator;
|
||||
if ('wakeLock' in navigator) {
|
||||
try {
|
||||
const wakeLock = await anyNav['wakeLock'].request('screen');
|
||||
} catch (err: any) {
|
||||
// The wake lock request fails - usually system-related, such as low battery.
|
||||
console.log(`Wake lock request failed: ${err.name}, ${err.message}`);
|
||||
}
|
||||
} else {
|
||||
console.log('No wake lock support here...');
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.hidden) {
|
||||
console.log('Application hidden');
|
||||
} else {
|
||||
console.log('Application visible');
|
||||
requestWakeLock();
|
||||
}
|
||||
};
|
||||
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange, false);
|
||||
requestWakeLock();
|
||||
|
||||
const App: React.FC = () => {
|
||||
return (
|
||||
<IonApp>
|
||||
<Provider store={store}>
|
||||
<PouchDB name='dyomedea' auto_compaction={true} revs_limit={10}>
|
||||
<Suspense fallback='loading...'>
|
||||
<Map />
|
||||
</Suspense>
|
||||
</PouchDB>
|
||||
</Provider>
|
||||
</IonApp>
|
||||
);
|
||||
};
|
||||
const App: React.FC = () => (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonRouterOutlet>
|
||||
<Route exact path="/home">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route exact path="/">
|
||||
<Redirect to="/home" />
|
||||
</Route>
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
);
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
.container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.container strong {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #8c8c8c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
import './ExploreContainer.css';
|
||||
|
||||
interface ContainerProps { }
|
||||
|
||||
const ExploreContainer: React.FC<ContainerProps> = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<strong>Ready to create an app?</strong>
|
||||
<p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreContainer;
|
|
@ -1,12 +0,0 @@
|
|||
import react, { Fragment } from 'react';
|
||||
|
||||
interface FormProps {
|
||||
context: any;
|
||||
validate: (ctx: any) => {};
|
||||
}
|
||||
|
||||
const Form: react.FC<{}> = () => {
|
||||
return <Fragment />;
|
||||
};
|
||||
|
||||
export default Form;
|
|
@ -1,93 +0,0 @@
|
|||
import React, { useId } from 'react';
|
||||
|
||||
import { geoPoint, lat2tile, lon2tile, Point } from '../../lib/geo';
|
||||
|
||||
import { tileProviders } from './tile';
|
||||
|
||||
interface AvatarForLocationProps {
|
||||
location: geoPoint;
|
||||
zoom?: number;
|
||||
tileProvider?: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
const AvatarForLocation: React.FC<AvatarForLocationProps> = (
|
||||
props: AvatarForLocationProps
|
||||
) => {
|
||||
const size = props.size !== undefined ? props.size : 42;
|
||||
const zoom = props.zoom ? Math.round(props.zoom) : 16;
|
||||
const tileProvider = props.tileProvider ? props.tileProvider : 'osm';
|
||||
const location = props.location;
|
||||
|
||||
const tilesLocation: Point = {
|
||||
x: lon2tile(location.lon, zoom),
|
||||
y: lat2tile(location.lat, zoom),
|
||||
};
|
||||
|
||||
const locationWithinTile: Point = {
|
||||
x: 256 * (tilesLocation.x - Math.floor(tilesLocation.x)),
|
||||
y: 256 * (tilesLocation.y - Math.floor(tilesLocation.y)),
|
||||
};
|
||||
|
||||
const eastTileNeeded = locationWithinTile.x > 256 - size / 2;
|
||||
const westTileNeeded = locationWithinTile.x < size / 2;
|
||||
const southTileNeeded = locationWithinTile.y > 256 - size / 2;
|
||||
const northTileNeeded = locationWithinTile.y < size / 2;
|
||||
|
||||
const getImage = (stepX: number, stepY: number) => (
|
||||
<image
|
||||
href={tileProviders[tileProvider].getTileUrl(
|
||||
zoom,
|
||||
Math.floor(tilesLocation.x) + stepX,
|
||||
Math.floor(tilesLocation.y) + stepY
|
||||
)}
|
||||
height='256'
|
||||
width={256}
|
||||
x={-locationWithinTile.x + size / 2 + stepX * 256}
|
||||
y={-locationWithinTile.y + size / 2 + stepY * 256}
|
||||
/>
|
||||
);
|
||||
|
||||
var images = [getImage(0, 0)];
|
||||
if (eastTileNeeded) {
|
||||
images.push(getImage(1, 0));
|
||||
}
|
||||
if (westTileNeeded) {
|
||||
images.push(getImage(-1, 0));
|
||||
}
|
||||
if (southTileNeeded) {
|
||||
images.push(getImage(0, 1));
|
||||
}
|
||||
if (northTileNeeded) {
|
||||
images.push(getImage(0, -1));
|
||||
}
|
||||
if (northTileNeeded && eastTileNeeded) {
|
||||
images.push(getImage(1, -1));
|
||||
}
|
||||
if (northTileNeeded && westTileNeeded) {
|
||||
images.push(getImage(-1, -1));
|
||||
}
|
||||
if (southTileNeeded && eastTileNeeded) {
|
||||
images.push(getImage(1, 1));
|
||||
}
|
||||
if (southTileNeeded && westTileNeeded) {
|
||||
images.push(getImage(-1, 1));
|
||||
}
|
||||
|
||||
console.log(`${images.length} images: ${images}`);
|
||||
|
||||
const id = useId();
|
||||
|
||||
return (
|
||||
<svg width={size} height={size}>
|
||||
<defs>
|
||||
<clipPath id={`cut-off-${id}`}>
|
||||
<circle cx={size / 2} cy={size / 2} r={size / 2} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<g clip-path={`url(#cut-off-${id})`}>{[images]}</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default AvatarForLocation;
|
|
@ -1,33 +0,0 @@
|
|||
import React
|
||||
|
||||
|
||||
|
||||
from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import { MapState } from '../../store/map';
|
||||
|
||||
const CurrentLocation: React.FC<{}> = () => {
|
||||
const scale = useSelector(
|
||||
(state: { map: MapState }) => state.map.whiteboard.scale
|
||||
);
|
||||
const CurrentLocationState = useSelector(
|
||||
(state: { map: MapState }) => state.map.currentLocation
|
||||
);
|
||||
|
||||
return (
|
||||
<circle
|
||||
cx={CurrentLocationState.whiteboard.x}
|
||||
cy={CurrentLocationState.whiteboard.y}
|
||||
r={6 / scale}
|
||||
fill='blue'
|
||||
opacity='90%'
|
||||
stroke='white'
|
||||
strokeWidth={3/scale}
|
||||
strokeOpacity='100%'
|
||||
></circle>
|
||||
);
|
||||
};
|
||||
|
||||
export default CurrentLocation;
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { useFind } from 'react-pouchdb';
|
||||
|
||||
import { lat2tile, lon2tile } from '../../lib/geo';
|
||||
import { zoom0 } from '../../store/map';
|
||||
|
||||
const Gpx: React.FC<{ gpx: any }> = (props) => {
|
||||
var trkpts: any[] = [];
|
||||
|
||||
trkpts = useFind({
|
||||
selector: {
|
||||
type: 'trkpt',
|
||||
gpx: props.gpx._id,
|
||||
},
|
||||
// sort: ['trkpt.time'],
|
||||
// use_index: 'type-trkpt-gpx-time3',
|
||||
});
|
||||
|
||||
console.log(
|
||||
`rendering gpx, subtype:"${props.gpx.subtype}", ${trkpts.length} points`
|
||||
);
|
||||
|
||||
trkpts.sort((first: any, second: any) =>
|
||||
first.trkpt.time.localeCompare(second.trkpt.time)
|
||||
);
|
||||
|
||||
const clickHandler = (event: any) => {
|
||||
console.log(`gpx (${JSON.stringify(props.gpx)}) clicked.`);
|
||||
};
|
||||
|
||||
// console.log(`gpx: ${JSON.stringify(props.gpx)}`);
|
||||
|
||||
const d = trkpts.reduce((previous: string, current: any, index: number) => {
|
||||
const action = index === 0 ? 'M' : index === 1 ? 'L' : '';
|
||||
const trkpt = current.trkpt;
|
||||
return `${previous} ${action} ${lon2tile(trkpt.$.lon, zoom0)}, ${lat2tile(
|
||||
trkpt.$.lat,
|
||||
zoom0
|
||||
)}`;
|
||||
}, '');
|
||||
|
||||
return (
|
||||
<path d={d} className={`track ${props.gpx.subtype}`} pointerEvents='none' />
|
||||
);
|
||||
};
|
||||
|
||||
export default Gpx;
|
|
@ -1,111 +0,0 @@
|
|||
import { IonButton, IonIcon, isPlatform } from '@ionic/react';
|
||||
import { cloudDownload } from 'ionicons/icons';
|
||||
import React, { Fragment, useRef } from 'react';
|
||||
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
|
||||
import { useDB } from 'react-pouchdb';
|
||||
import { FileOpener } from '@capacitor-community/file-opener';
|
||||
import { getGpxAsXmlString } from '../../db/gpx';
|
||||
|
||||
const GpxExport: React.FC<{ gpx: any }> = (props: { gpx: any }) => {
|
||||
const db = useDB();
|
||||
|
||||
const isCapacitor = isPlatform('capacitor');
|
||||
console.log(`isPlatform('capacitor'): ${isPlatform('capacitor')}`);
|
||||
|
||||
const hiddenLinkElement = useRef<HTMLAnchorElement>(null);
|
||||
|
||||
var downloadBaseName: string;
|
||||
|
||||
if (props.gpx.metadata.name !== undefined) {
|
||||
downloadBaseName = props.gpx.metadata.name.substr(
|
||||
0,
|
||||
props.gpx.metadata.name.lastIndexOf('.')
|
||||
);
|
||||
} else if (props.gpx.gpx.metadata.name !== undefined) {
|
||||
downloadBaseName = props.gpx.gpx.metadata.name;
|
||||
} else {
|
||||
downloadBaseName = `track-${props.gpx.metadata.lastModified}`;
|
||||
}
|
||||
|
||||
const downloadExt = 'gpx';
|
||||
|
||||
const downloadName = `${downloadBaseName}.${downloadExt}`;
|
||||
|
||||
const getNewFilename: any = async (
|
||||
filename: string,
|
||||
extension: string,
|
||||
i = 0
|
||||
) => {
|
||||
const filenameToTest =
|
||||
i === 0 ? `${filename}.${extension}` : `${filename}(${i}).${extension}`;
|
||||
try {
|
||||
await Filesystem.stat({
|
||||
path: filenameToTest,
|
||||
directory: Directory.Documents,
|
||||
});
|
||||
return getNewFilename(filename, extension, i + 1);
|
||||
} catch {
|
||||
return filenameToTest;
|
||||
}
|
||||
};
|
||||
|
||||
const download = async (event: any) => {
|
||||
event.preventDefault();
|
||||
console.log('download()');
|
||||
const gpxAsXml = await getGpxAsXmlString(db, props.gpx._id);
|
||||
console.log(`gpxAsXml: ${gpxAsXml}`);
|
||||
if (isCapacitor) {
|
||||
const filename = await getNewFilename(
|
||||
`../Download/${downloadBaseName}`,
|
||||
downloadExt
|
||||
);
|
||||
console.log(`filename: ${filename}`);
|
||||
const fileUrl = await Filesystem.writeFile({
|
||||
path: filename,
|
||||
data: gpxAsXml,
|
||||
directory: Directory.Documents,
|
||||
encoding: Encoding.UTF8,
|
||||
recursive: true,
|
||||
});
|
||||
console.log(`fileUrl: ${fileUrl.uri}`);
|
||||
await FileOpener.open({
|
||||
filePath: fileUrl.uri,
|
||||
contentType: 'application/gpx+xml',
|
||||
openWithDefault: false,
|
||||
});
|
||||
} else {
|
||||
const blob = new Blob([gpxAsXml], {
|
||||
type: 'application/gpx+xml',
|
||||
});
|
||||
const fileDownloadUrl = URL.createObjectURL(blob);
|
||||
console.log(`fileDownloadUrl: ${fileDownloadUrl}`);
|
||||
console.log(
|
||||
`hiddenLinkElement.current, href: ${
|
||||
hiddenLinkElement.current!.href
|
||||
}, click: ${hiddenLinkElement.current!.click}`
|
||||
);
|
||||
hiddenLinkElement.current!.href = fileDownloadUrl;
|
||||
hiddenLinkElement.current?.click();
|
||||
console.log(
|
||||
`hiddenLinkElement.current, href: ${
|
||||
hiddenLinkElement.current!.href
|
||||
}, click: ${hiddenLinkElement.current!.click}`
|
||||
);
|
||||
URL.revokeObjectURL(fileDownloadUrl);
|
||||
hiddenLinkElement.current!.href = '';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<IonButton id='gpx-export-button' onClick={download}>
|
||||
<IonIcon icon={cloudDownload} title='Export' slot='icon-only' />
|
||||
</IonButton>
|
||||
<a className='hidden' download={downloadName} ref={hiddenLinkElement}>
|
||||
download
|
||||
</a>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default GpxExport;
|
|
@ -1,221 +0,0 @@
|
|||
import React, { Fragment, useRef, useState } from 'react';
|
||||
|
||||
import { useDB, useFind } from 'react-pouchdb';
|
||||
|
||||
import '../../theme/get-location.css';
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonIcon,
|
||||
IonModal,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import {
|
||||
recordingOutline,
|
||||
recording,
|
||||
closeCircle,
|
||||
pauseCircle,
|
||||
stop,
|
||||
} from 'ionicons/icons';
|
||||
import {
|
||||
startBackgroundGeolocation,
|
||||
stopBackgroundGeolocation,
|
||||
} from '../../lib/background-geolocation';
|
||||
import { appendTrkpt, deleteCurrent, saveCurrent } from '../../db/gpx';
|
||||
import { enterAnimation, leaveAnimation } from '../../lib/animation';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { mapActions } from '../../store/map';
|
||||
import { SettingsState } from '../../store/settings';
|
||||
|
||||
import i18n, { setI18nLanguage } from '../../i18n/index';
|
||||
|
||||
declare global {
|
||||
var $lastValidLocationTime: number;
|
||||
}
|
||||
|
||||
const GpxRecord: React.FC<{}> = () => {
|
||||
const language = useSelector(
|
||||
(state: { settings: SettingsState }) => state.settings.language
|
||||
);
|
||||
|
||||
setI18nLanguage(language);
|
||||
|
||||
const db = useDB();
|
||||
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
|
||||
const [watcher_id, setWatcher_id] = useState();
|
||||
|
||||
const geolocationsSettingsState = useSelector(
|
||||
(state: { settings: SettingsState }) => state.settings.geolocation
|
||||
);
|
||||
|
||||
const gpxes = useFind({
|
||||
selector: {
|
||||
type: 'gpx',
|
||||
subtype: 'current',
|
||||
},
|
||||
});
|
||||
|
||||
const hasCurrentTrack = gpxes.length > 0;
|
||||
|
||||
console.log(
|
||||
`GpxRecord, hasCurrentTrack:${hasCurrentTrack}, gpxes: ${JSON.stringify(
|
||||
gpxes
|
||||
)}`
|
||||
);
|
||||
|
||||
const modal = useRef<HTMLIonModalElement>(null);
|
||||
|
||||
const dismiss = () => {
|
||||
modal.current?.dismiss();
|
||||
};
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const newLocationHandler = (location: any) => {
|
||||
console.log(
|
||||
`Location filtering, elapsed time: ${
|
||||
location.time - globalThis.$lastValidLocationTime
|
||||
}`
|
||||
);
|
||||
if (
|
||||
location.time - globalThis.$lastValidLocationTime >
|
||||
geolocationsSettingsState.minTimeInterval
|
||||
) {
|
||||
globalThis.$lastValidLocationTime = location.time;
|
||||
appendTrkpt(db, {
|
||||
$: {
|
||||
lat: location.latitude,
|
||||
lon: location.longitude,
|
||||
},
|
||||
ele: location.altitude,
|
||||
time: new Date(location.time).toISOString(),
|
||||
extensions: {
|
||||
speed: location.speed,
|
||||
accuracy: location.accuracy,
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
mapActions.setCurrent({
|
||||
lat: location.latitude,
|
||||
lon: location.longitude,
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
mapActions.setCenter({
|
||||
lat: location.latitude,
|
||||
lon: location.longitude,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const startRecording = () => {
|
||||
globalThis.$lastValidLocationTime = 0;
|
||||
startBackgroundGeolocation(
|
||||
newLocationHandler,
|
||||
geolocationsSettingsState.minDistance
|
||||
).then((result) => {
|
||||
setWatcher_id(result);
|
||||
});
|
||||
setIsRecording(true);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const pauseRecording = () => {
|
||||
if (isRecording) {
|
||||
stopBackgroundGeolocation(watcher_id);
|
||||
}
|
||||
setIsRecording(false);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
const stopRecording = () => {
|
||||
saveCurrent(db);
|
||||
pauseRecording();
|
||||
};
|
||||
|
||||
const deleteRecording = () => {
|
||||
deleteCurrent(db);
|
||||
pauseRecording();
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<IonButton id='open-RecordDialog'>
|
||||
{isRecording && (
|
||||
<IonIcon slot='icon-only' icon={recording} style={{ color: 'red' }} />
|
||||
)}
|
||||
{!isRecording && <IonIcon slot='icon-only' icon={recordingOutline} />}
|
||||
</IonButton>
|
||||
<IonModal
|
||||
ref={modal}
|
||||
trigger='open-RecordDialog'
|
||||
enterAnimation={enterAnimation}
|
||||
leaveAnimation={leaveAnimation}
|
||||
>
|
||||
<IonToolbar>
|
||||
<IonTitle>{i18n.trackRecording}</IonTitle>
|
||||
<IonButtons slot='end'>
|
||||
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
<IonContent>
|
||||
{!isRecording && (
|
||||
<IonButton
|
||||
expand='block'
|
||||
color='primary'
|
||||
size='large'
|
||||
onClick={startRecording}
|
||||
>
|
||||
<IonIcon slot='start' icon={recording}></IonIcon>
|
||||
{hasCurrentTrack ? (
|
||||
<span>{i18n.resumeRecording}</span>
|
||||
) : (
|
||||
<span>{i18n.startRecording}</span>
|
||||
)}
|
||||
</IonButton>
|
||||
)}
|
||||
{isRecording && (
|
||||
<IonButton
|
||||
expand='block'
|
||||
color='primary'
|
||||
size='large'
|
||||
onClick={pauseRecording}
|
||||
>
|
||||
<IonIcon slot='start' icon={pauseCircle}></IonIcon>
|
||||
{i18n.pauseRecording}
|
||||
</IonButton>
|
||||
)}
|
||||
{hasCurrentTrack && (
|
||||
<>
|
||||
<IonButton
|
||||
expand='block'
|
||||
color='danger'
|
||||
size='large'
|
||||
onClick={stopRecording}
|
||||
>
|
||||
<IonIcon slot='start' icon={stop}></IonIcon>
|
||||
{i18n.stopRecording}
|
||||
</IonButton>
|
||||
<IonButton
|
||||
expand='block'
|
||||
color='danger'
|
||||
size='large'
|
||||
onClick={deleteRecording}
|
||||
>
|
||||
<IonIcon slot='start' icon={closeCircle}></IonIcon>
|
||||
{i18n.cancelRecording}
|
||||
</IonButton>
|
||||
</>
|
||||
)}
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default GpxRecord;
|
|
@ -1,186 +0,0 @@
|
|||
import {
|
||||
IonModal,
|
||||
IonToolbar,
|
||||
IonTitle,
|
||||
IonButtons,
|
||||
IonButton,
|
||||
IonContent,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonItem,
|
||||
IonSpinner,
|
||||
} from '@ionic/react';
|
||||
import React, { Fragment, useRef, useState } from 'react';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import i18n from '../../i18n';
|
||||
import { geoPoint } from '../../lib/geo';
|
||||
import { MapState } from '../../store/map';
|
||||
import OsmNotesChat from './OsmNotesChat';
|
||||
|
||||
const LocationInfo: React.FC<{}> = () => {
|
||||
const scope = useSelector((state: { map: MapState }) => state.map.scope);
|
||||
|
||||
const modal = useRef<HTMLIonModalElement>(null);
|
||||
|
||||
const dismiss = () => {
|
||||
modal.current?.dismiss();
|
||||
};
|
||||
|
||||
const [elevation, setElevation] = useState(undefined);
|
||||
const [address, setAddress] = useState<{ display_name: any } | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [notes, setNotes] = useState<{ features: any } | undefined>(undefined);
|
||||
|
||||
const findElevation = async () => {
|
||||
setElevation(undefined);
|
||||
const response = await fetch(
|
||||
// `https://api.opentopodata.org/v1/mapzen?locations=${scope.center.lat},${scope.center.lon}`,
|
||||
`https://api.open-meteo.com/v1/elevation?latitude=${scope.center.lat}&longitude=${scope.center.lon}`,
|
||||
{}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(`elevation: ${JSON.stringify(data)}`);
|
||||
setElevation(data.elevation[0]);
|
||||
};
|
||||
|
||||
const findAddress = async () => {
|
||||
setAddress(undefined);
|
||||
const response = await fetch(
|
||||
`https://nominatim.openstreetmap.org/reverse?lat=${
|
||||
scope.center.lat
|
||||
}&lon=${
|
||||
scope.center.lon
|
||||
}&format=jsonv2&addressdetails=1&extratags=1&namedetails=1&accept-language=${i18n.getLanguage()}`,
|
||||
{}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(`address: ${JSON.stringify(data)}`);
|
||||
setAddress(data);
|
||||
};
|
||||
const roughDistance = (a: geoPoint, b: geoPoint): number => {
|
||||
const pseudoDistanceInDegrees = Math.sqrt(
|
||||
(a.lat - b.lat) ** 2 + (a.lon - b.lon) ** 2
|
||||
);
|
||||
const metresPerDegree =
|
||||
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
|
||||
return pseudoDistanceInDegrees * metresPerDegree;
|
||||
};
|
||||
|
||||
const findNotes = async () => {
|
||||
setNotes(undefined);
|
||||
const metresPerDegree =
|
||||
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
|
||||
const deltaDegrees = 5000 / metresPerDegree;
|
||||
const response = await fetch(
|
||||
`https://api.openstreetmap.org/api/0.6/notes.json?bbox=${
|
||||
scope.center.lon - deltaDegrees
|
||||
},${scope.center.lat - deltaDegrees},${scope.center.lon + deltaDegrees},${
|
||||
scope.center.lat + deltaDegrees
|
||||
}`,
|
||||
{}
|
||||
);
|
||||
const data = await response.json();
|
||||
console.log(`notes: ${JSON.stringify(data)}`);
|
||||
data.features.sort(
|
||||
(first: any, second: any) =>
|
||||
roughDistance(scope.center, {
|
||||
lon: first.geometry.coordinates[0],
|
||||
lat: first.geometry.coordinates[1],
|
||||
}) -
|
||||
roughDistance(scope.center, {
|
||||
lon: second.geometry.coordinates[0],
|
||||
lat: second.geometry.coordinates[1],
|
||||
})
|
||||
);
|
||||
setNotes(data);
|
||||
};
|
||||
|
||||
const ionModalWillPresentHandler = async () => {
|
||||
findElevation();
|
||||
findAddress();
|
||||
findNotes();
|
||||
};
|
||||
|
||||
const localDate = (isoDate: string) => {
|
||||
const date = new Date(isoDate);
|
||||
const format = new Intl.DateTimeFormat(i18n.getLanguage(), {
|
||||
dateStyle: 'full',
|
||||
timeStyle: 'long',
|
||||
});
|
||||
return format.format(date);
|
||||
};
|
||||
|
||||
const osmNotesChat =
|
||||
notes !== undefined && notes.features.length ? (
|
||||
<OsmNotesChat notes={notes} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
|
||||
return (
|
||||
<IonModal
|
||||
trigger='information-request'
|
||||
ref={modal}
|
||||
onIonModalWillPresent={ionModalWillPresentHandler}
|
||||
>
|
||||
<IonToolbar>
|
||||
<IonTitle>{i18n.locationInfo!.title}</IonTitle>
|
||||
<IonButtons slot='end'>
|
||||
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
|
||||
</IonButtons>
|
||||
</IonToolbar>
|
||||
<IonContent>
|
||||
<IonList lines='full' class='ion-no-margin'>
|
||||
<IonListHeader lines='full'>
|
||||
<IonLabel>{i18n.locationInfo!.location}</IonLabel>
|
||||
</IonListHeader>
|
||||
<IonItem>
|
||||
<IonLabel>{i18n.locationInfo!.lat}</IonLabel>
|
||||
{scope.center.lat} N
|
||||
</IonItem>
|
||||
<IonItem>
|
||||
<IonLabel>{i18n.locationInfo!.lon}</IonLabel>
|
||||
{scope.center.lon} E
|
||||
</IonItem>
|
||||
<IonItem>
|
||||
<IonLabel>{i18n.locationInfo!.elevation}</IonLabel>
|
||||
{elevation === undefined && <IonSpinner name='lines' />}
|
||||
{elevation !== undefined && <>{elevation} m</>}
|
||||
</IonItem>
|
||||
</IonList>
|
||||
<IonList lines='full' class='ion-no-margin'>
|
||||
<IonListHeader lines='full'>
|
||||
<IonLabel>{i18n.locationInfo!.address}</IonLabel>
|
||||
</IonListHeader>
|
||||
<IonItem>
|
||||
<IonLabel>{i18n.locationInfo!.display_name}</IonLabel>
|
||||
{address === undefined && <IonSpinner name='lines' />}
|
||||
{address !== undefined &&
|
||||
address.display_name
|
||||
.split(',')
|
||||
.map((value: string, index: number) => (
|
||||
<Fragment key={index}>
|
||||
{value}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
</IonItem>
|
||||
</IonList>
|
||||
<IonList lines='full' class='ion-no-margin'>
|
||||
<IonListHeader lines='full'>
|
||||
<IonLabel>
|
||||
{i18n.locationInfo!.notes}
|
||||
{notes === undefined && <IonSpinner name='lines' />}
|
||||
</IonLabel>
|
||||
</IonListHeader>
|
||||
{osmNotesChat}
|
||||
</IonList>
|
||||
</IonContent>
|
||||
</IonModal>
|
||||
);
|
||||
};
|
||||
|
||||
export default LocationInfo;
|
|
@ -1,143 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
MainContainer,
|
||||
Sidebar,
|
||||
ConversationList,
|
||||
Conversation,
|
||||
ConversationHeader,
|
||||
ChatContainer,
|
||||
MessageList,
|
||||
Message,
|
||||
Avatar,
|
||||
} from '@chatscope/chat-ui-kit-react';
|
||||
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
|
||||
|
||||
import { useSelector } from 'react-redux';
|
||||
import i18n from '../../i18n';
|
||||
import { geoPoint } from '../../lib/geo';
|
||||
import { MapState } from '../../store/map';
|
||||
import AvatarForLocation from './AvatarForLocation';
|
||||
interface OsmNotesChatProps {
|
||||
notes: { features: any };
|
||||
}
|
||||
|
||||
const OsmNotesChat: React.FC<OsmNotesChatProps> = (
|
||||
props: OsmNotesChatProps
|
||||
) => {
|
||||
const scope = useSelector((state: { map: MapState }) => state.map.scope);
|
||||
|
||||
const [noteIndex, setNoteIndex] = useState(0);
|
||||
const conversationClickHandlerFactory = (index: number) => (event: any) => {
|
||||
setNoteIndex(index);
|
||||
};
|
||||
|
||||
const roughDistance = (a: geoPoint, b: geoPoint): number => {
|
||||
const pseudoDistanceInDegrees = Math.sqrt(
|
||||
(a.lat - b.lat) ** 2 + (a.lon - b.lon) ** 2
|
||||
);
|
||||
const metresPerDegree =
|
||||
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
|
||||
return pseudoDistanceInDegrees * metresPerDegree;
|
||||
};
|
||||
|
||||
const AvatarForFeature: React.FC<{
|
||||
feature: any;
|
||||
size: number;
|
||||
as: any;
|
||||
}> = (props: { feature: any; size: number; as: any }) => {
|
||||
const distance = roughDistance(scope.center, {
|
||||
lon: props.feature.geometry.coordinates[0],
|
||||
lat: props.feature.geometry.coordinates[1],
|
||||
});
|
||||
return (
|
||||
<Avatar size={props.size <= 50 ? 'sm' : 'lg'}>
|
||||
<AvatarForLocation
|
||||
location={{
|
||||
lon: props.feature.geometry.coordinates[0],
|
||||
lat: props.feature.geometry.coordinates[1],
|
||||
}}
|
||||
size={props.size}
|
||||
/>
|
||||
</Avatar>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<MainContainer responsive>
|
||||
<Sidebar position='left' id='chat-sidebar'>
|
||||
<ConversationList>
|
||||
{props.notes
|
||||
.features!.slice(0, 5)
|
||||
.map((feature: any, index: number) => (
|
||||
<Conversation
|
||||
onClick={conversationClickHandlerFactory(index)}
|
||||
key={feature.properties.id}
|
||||
name={i18n.locationInfo!.at!(
|
||||
roughDistance(scope.center, {
|
||||
lon: feature.geometry.coordinates[0],
|
||||
lat: feature.geometry.coordinates[1],
|
||||
})
|
||||
)}
|
||||
info={feature.properties.status}
|
||||
>
|
||||
<AvatarForFeature feature={feature} size={42} as={Avatar} />
|
||||
</Conversation>
|
||||
))}
|
||||
</ConversationList>
|
||||
</Sidebar>
|
||||
<ChatContainer>
|
||||
<ConversationHeader>
|
||||
<AvatarForFeature
|
||||
feature={props.notes.features[noteIndex]}
|
||||
size={68}
|
||||
as={Avatar}
|
||||
/>
|
||||
<ConversationHeader.Content>
|
||||
<span
|
||||
style={{
|
||||
alignSelf: 'flex-center',
|
||||
}}
|
||||
>
|
||||
{i18n.locationInfo!.at!(
|
||||
roughDistance(scope.center, {
|
||||
lon: props.notes.features[noteIndex].geometry
|
||||
.coordinates[0],
|
||||
lat: props.notes.features[noteIndex].geometry
|
||||
.coordinates[1],
|
||||
})
|
||||
)}
|
||||
</span>
|
||||
</ConversationHeader.Content>
|
||||
</ConversationHeader>
|
||||
<MessageList>
|
||||
{noteIndex < props.notes.features.length &&
|
||||
props.notes.features[noteIndex].properties.comments.map(
|
||||
(comment: any, index: number) => (
|
||||
<Message
|
||||
key={`${props.notes.features[noteIndex].properties.id}/${index}`}
|
||||
model={{
|
||||
direction: 'incoming',
|
||||
position: 'single',
|
||||
sender: comment.user,
|
||||
sentTime: comment.date,
|
||||
payload: comment.html,
|
||||
}}
|
||||
>
|
||||
<Message.Header
|
||||
sender={comment.user}
|
||||
sentTime={comment.date}
|
||||
/>
|
||||
<Message.Footer sentTime={comment.action} />
|
||||
<Avatar>{comment.user ? comment.user : '??'}</Avatar>
|
||||
</Message>
|
||||
)
|
||||
)}
|
||||
</MessageList>
|
||||
</ChatContainer>
|
||||
</MainContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default OsmNotesChat;
|
|
@ -1,146 +0,0 @@
|
|||
import _ from 'lodash';
|
||||
import React, { useRef } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { informationCircleOutline } from 'ionicons/icons';
|
||||
import { Point } from '../../lib/geo';
|
||||
import { MapState } from '../../store/map';
|
||||
import '../../theme/map.css';
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonModal,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import i18n from '../../i18n';
|
||||
import eventBus from '../../lib/pubsub';
|
||||
|
||||
export const getSTile = (lat: number, zoom: number) => {
|
||||
/**
|
||||
* The horizontal distance represented by each square tile, measured along the parallel at a given latitude, is given by:
|
||||
* Stile = C ∙ cos(latitude) / 2 zoomlevel
|
||||
* As tiles are 256-pixels wide, the horizontal distance represented by one pixel is:
|
||||
* Spixel = Stile / 256 = C ∙ cos(latitude) / 2 (zoomlevel + 8)
|
||||
* where C means the equatorial circumference of the Earth (40 075 016.686 m ≈ 2π ∙ 6 378 137.000 m for the reference geoid used by OpenStreetMap).
|
||||
* see https://wiki.openstreetmap.org/wiki/Zoom_levels
|
||||
*/
|
||||
return (40075016.686 * Math.cos((lat * Math.PI) / 180)) / 2 ** zoom;
|
||||
};
|
||||
|
||||
const Reticle: React.FC<{}> = () => {
|
||||
const windowState = useSelector(
|
||||
(state: { map: MapState }) => state.map.window
|
||||
);
|
||||
const scope = useSelector((state: { map: MapState }) => state.map.scope);
|
||||
const radius = Math.min(windowState.width, windowState.height) / 4;
|
||||
const center: Point = {
|
||||
x: windowState.width / 2,
|
||||
y: windowState.height / 2,
|
||||
};
|
||||
|
||||
const sTile = getSTile(scope.center.lat, scope.zoom);
|
||||
|
||||
const radiusMeters = (sTile * radius) / 256;
|
||||
const radiusOrder = Math.floor(Math.log10(radiusMeters));
|
||||
const radiusRoughUnit = radiusMeters / 10 ** radiusOrder;
|
||||
var radiusBaseUnit, radiusBaseSubUnit;
|
||||
if (radiusRoughUnit <= 2) {
|
||||
radiusBaseUnit = 1;
|
||||
radiusBaseSubUnit = 0.2;
|
||||
} else if (radiusRoughUnit <= 5) {
|
||||
radiusBaseUnit = 2;
|
||||
radiusBaseSubUnit = 0.5;
|
||||
} else {
|
||||
radiusBaseUnit = 5;
|
||||
radiusBaseSubUnit = 1;
|
||||
}
|
||||
|
||||
const nbSubUnits = Math.floor(
|
||||
radiusMeters / (radiusBaseSubUnit * 10 ** radiusOrder)
|
||||
);
|
||||
const nbUnits = Math.floor(
|
||||
radiusMeters / (radiusBaseUnit * 10 ** radiusOrder)
|
||||
);
|
||||
|
||||
const radiusUnit = radiusOrder < 3 ? 'm' : 'km';
|
||||
const radiusUnitValue =
|
||||
radiusOrder < 3
|
||||
? radiusBaseUnit * 10 ** radiusOrder
|
||||
: radiusBaseUnit * 10 ** (radiusOrder - 3);
|
||||
|
||||
const subUnitPxStep = ((radiusBaseSubUnit * 10 ** radiusOrder) / sTile) * 256;
|
||||
const unitPxStep = ((radiusBaseUnit * 10 ** radiusOrder) / sTile) * 256;
|
||||
|
||||
console.log(
|
||||
`radiusMeters: ${radiusMeters}, radiusOrder: ${radiusOrder}, radiusUnit: ${radiusBaseUnit}, radiusSubUnit: ${radiusBaseSubUnit}, nbSubUnits: ${nbSubUnits}`
|
||||
);
|
||||
|
||||
return (
|
||||
<g className='reticle' pointerEvents='visible' id='information-request'>
|
||||
<circle
|
||||
cx={center.x}
|
||||
cy={center.y}
|
||||
r={unitPxStep * nbUnits}
|
||||
fill='transparent'
|
||||
stroke='black'
|
||||
strokeWidth={1}
|
||||
/>
|
||||
<path
|
||||
d={`M ${center.x - radius},${center.y}
|
||||
h ${radius - 3}
|
||||
m ${6},0
|
||||
h ${radius - 3}
|
||||
M ${center.x},${center.y - radius}
|
||||
v ${radius - 3}
|
||||
m 0,${6}
|
||||
v ${radius - 3}`}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
<g id='reticle-east'>
|
||||
<path
|
||||
stroke='red'
|
||||
className='reticle'
|
||||
strokeWidth={0.5}
|
||||
d={_.range(1, nbSubUnits + 1)
|
||||
.map(
|
||||
(i) => `M ${center.x + i * subUnitPxStep},${center.y - 10} v 20`
|
||||
)
|
||||
.join(' ')}
|
||||
/>
|
||||
<path
|
||||
stroke='red'
|
||||
className='reticle'
|
||||
d={_.range(1, nbUnits + 1)
|
||||
.map((i) => `M ${center.x + i * unitPxStep},${center.y - 20} v 40`)
|
||||
.join(' ')}
|
||||
strokeWidth={0.5}
|
||||
/>
|
||||
</g>
|
||||
<use
|
||||
stroke='red'
|
||||
className='reticle'
|
||||
href='#reticle-east'
|
||||
transform={`rotate(90, ${center.x}, ${center.y})`}
|
||||
/>
|
||||
<use
|
||||
className='reticle'
|
||||
href='#reticle-east'
|
||||
transform={`rotate(180, ${center.x}, ${center.y})`}
|
||||
/>
|
||||
<use
|
||||
className='reticle'
|
||||
href='#reticle-east'
|
||||
transform={`rotate(-90, ${center.x}, ${center.y})`}
|
||||
/>
|
||||
<text
|
||||
x={center.x + unitPxStep * nbUnits - 15}
|
||||
y={center.y - 10}
|
||||
cursor='pointer'
|
||||
>
|
||||
{`${radiusUnitValue * nbUnits} ${radiusUnit}`}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
|
||||
export default Reticle;
|
|
@ -1,210 +0,0 @@
|
|||
import React, { Fragment, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { useDB } from 'react-pouchdb';
|
||||
|
||||
import {
|
||||
IonButton,
|
||||
IonButtons,
|
||||
IonContent,
|
||||
IonIcon,
|
||||
IonInput,
|
||||
IonItem,
|
||||
IonLabel,
|
||||
IonList,
|
||||
IonListHeader,
|
||||
IonModal,
|
||||
IonSelect,
|
||||
IonSelectOption,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
import { options } from 'ionicons/icons';
|
||||
import { enterAnimation, leaveAnimation } from '../../lib/animation';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { settingsActions, SettingsState } from '../../store/settings';
|
||||
import uri from '../../lib/ids';
|
||||
import _ from 'lodash';
|
||||
import i18n, { setI18nLanguage } from '../../i18n/index';
|
||||
|
||||
const Settings: React.FC<{}> = () => {
|
||||
const dispatch = useDispatch();
|
||||
const db = useDB();
|
||||
|
||||
useEffect(() => {
|
||||
const initFromDb = async () => {
|
||||
try {
|
||||
const settingsFromDb = await db.get(uri('settings', {}));
|
||||
console.log(
|
||||
`settingsFromDb: ${JSON.stringify(settingsFromDb.settings)}`
|
||||
);
|
||||
dispatch(settingsActions.saveSettings(settingsFromDb.settings));
|
||||
} catch {}
|
||||
};
|
||||
initFromDb();
|
||||
}, [db, dispatch]);
|
||||
|
||||
const settingsState = useSelector(
|
||||
(state: { settings: SettingsState }) => state.settings
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!settingsState.default) {
|
||||
const settingsFromRedux = async () => {
|
||||
const id = uri('settings', {});
|
||||
var settingsFromDb;
|
||||
try {
|
||||
settingsFromDb = await db.get(id);
|
||||
} catch (error) {
|
||||
console.log(
|
||||
`Error getting settings from db: ${JSON.stringify(error)}`
|
||||
);
|
||||
settingsFromDb = { _id: id, settings: {} };
|
||||
}
|
||||
if (!_.isEqual(settingsState, settingsFromDb.settings)) {
|
||||
console.log(`settingsFromRedux: ${JSON.stringify(settingsState)}`);
|
||||
settingsFromDb.settings = settingsState;
|
||||
db.put(settingsFromDb);
|
||||
}
|
||||
};
|
||||
settingsFromRedux();
|
||||
}
|
||||
setLocalLanguage(settingsState.language);
|
||||
}, [db, settingsState]);
|
||||
|
||||
const [localLanguage, setLocalLanguage] = useState(settingsState.language);
|
||||
|
||||
const languageSelect = useRef<HTMLIonSelectElement>(null);
|
||||
const pseudo = useRef<HTMLIonInputElement>(null);
|
||||
const minTimeInterval = useRef<HTMLIonInputElement>(null);
|
||||
const minDistance = useRef<HTMLIonInputElement>(null);
|
||||
|
||||
const modal = useRef<HTMLIonModalElement>(null);
|
||||
|
||||
const save = () => {
|
||||
dispatch(
|
||||
settingsActions.saveSettings({
|
||||
user: {
|
||||
pseudo: pseudo.current?.value,
|
||||
},
|