Restarting from scratch
This commit is contained in:
parent
ed951208e8
commit
a4bd11aab0
|
@ -1,8 +1,8 @@
|
||||||
import { CapacitorConfig } from '@capacitor/cli';
|
import { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'com.dyomedea.dyomedea',
|
appId: 'com.dyomedea.dyomedea2',
|
||||||
appName: 'dyomedea',
|
appName: 'dyomedea2',
|
||||||
webDir: 'build',
|
webDir: 'build',
|
||||||
bundledWebRuntime: false
|
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",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"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/app": "4.0.1",
|
||||||
"@capacitor/cli": "^4.2.0",
|
"@capacitor/core": "4.3.0",
|
||||||
"@capacitor/core": "^4.2.0",
|
|
||||||
"@capacitor/filesystem": "^4.1.2",
|
|
||||||
"@capacitor/haptics": "4.0.1",
|
"@capacitor/haptics": "4.0.1",
|
||||||
"@capacitor/keyboard": "4.0.1",
|
"@capacitor/keyboard": "4.0.1",
|
||||||
"@capacitor/status-bar": "4.0.1",
|
"@capacitor/status-bar": "4.0.1",
|
||||||
"@chatscope/chat-ui-kit-react": "^1.9.7",
|
"@ionic/react": "^6.0.0",
|
||||||
"@chatscope/chat-ui-kit-styles": "^1.4.0",
|
"@ionic/react-router": "^6.0.0",
|
||||||
"@ionic/react": "^6.2.6",
|
|
||||||
"@ionic/react-router": "^6.2.6",
|
|
||||||
"@openpgp/asmcrypto.js": "^2.3.2",
|
|
||||||
"@reduxjs/toolkit": "^1.8.5",
|
|
||||||
"@testing-library/jest-dom": "^5.11.9",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.6.3",
|
||||||
"@trapezedev/configure": "^5.0.6",
|
|
||||||
"@types/jest": "^26.0.20",
|
"@types/jest": "^26.0.20",
|
||||||
"@types/node": "^12.19.15",
|
"@types/node": "^12.19.15",
|
||||||
"@types/pouchdb": "^6.4.0",
|
"@types/react": "^18.0.17",
|
||||||
"@types/react": "^18.0.18",
|
|
||||||
"@types/react-dom": "^18.0.6",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react-router": "^5.1.11",
|
"@types/react-router": "^5.1.11",
|
||||||
"@types/react-router-dom": "^5.1.7",
|
"@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",
|
"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": "^18.2.0",
|
||||||
"react-app-polyfill": "^3.0.0",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-localization": "^1.0.19",
|
"react-router": "^5.2.0",
|
||||||
"react-pouchdb": "^2.1.0",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-redux": "^8.0.2",
|
|
||||||
"react-router": "^6.3.0",
|
|
||||||
"react-router-dom": "^6.3.0",
|
|
||||||
"react-scripts": "^5.0.0",
|
"react-scripts": "^5.0.0",
|
||||||
"status": "^0.0.13",
|
"typescript": "^4.1.3",
|
||||||
"stream-browserify": "^3.0.0",
|
|
||||||
"typescript": "^4.8.3",
|
|
||||||
"web-vitals": "^0.2.4",
|
"web-vitals": "^0.2.4",
|
||||||
"workbox-background-sync": "^5.1.4",
|
"workbox-background-sync": "^5.1.4",
|
||||||
"workbox-broadcast-update": "^5.1.4",
|
"workbox-broadcast-update": "^5.1.4",
|
||||||
|
@ -95,8 +65,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/lodash": "^4.14.184"
|
"@capacitor/cli": "4.3.0"
|
||||||
},
|
},
|
||||||
"description": "An Ionic project",
|
"description": "An Ionic project"
|
||||||
"postinstall": "npx patch-package"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||||
|
|
||||||
<link
|
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/assets/icon/favicon.png" />
|
||||||
rel="shortcut icon"
|
|
||||||
type="image/png"
|
|
||||||
href="%PUBLIC_URL%/assets/icon/favicon.png"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- add to homescreen for ios -->
|
<!-- add to homescreen for ios -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||||
|
@ -31,4 +27,5 @@
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"short_name": "Dyomedea",
|
"short_name": "Ionic App",
|
||||||
"name": "Dyomedea",
|
"name": "My Ionic App",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "assets/icon/favicon.png",
|
"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();
|
||||||
|
});
|
63
src/App.tsx
63
src/App.tsx
|
@ -1,9 +1,7 @@
|
||||||
import { IonApp, setupIonicReact } from '@ionic/react';
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
|
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
|
||||||
import store from './store/index';
|
import { IonReactRouter } from '@ionic/react-router';
|
||||||
import { Provider } from 'react-redux';
|
import Home from './pages/Home';
|
||||||
import { Suspense } from 'react';
|
|
||||||
import { PouchDB } from 'react-pouchdb';
|
|
||||||
|
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
import '@ionic/react/css/core.css';
|
import '@ionic/react/css/core.css';
|
||||||
|
@ -24,50 +22,21 @@ import '@ionic/react/css/display.css';
|
||||||
/* Theme variables */
|
/* Theme variables */
|
||||||
import './theme/variables.css';
|
import './theme/variables.css';
|
||||||
|
|
||||||
import Map from './components/map/map';
|
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
// See https://stackoverflow.com/questions/71538643/property-wakelock-does-not-exist-on-type-navigator
|
const App: React.FC = () => (
|
||||||
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>
|
<IonApp>
|
||||||
<Provider store={store}>
|
<IonReactRouter>
|
||||||
<PouchDB name='dyomedea' auto_compaction={true} revs_limit={10}>
|
<IonRouterOutlet>
|
||||||
<Suspense fallback='loading...'>
|
<Route exact path="/home">
|
||||||
<Map />
|
<Home />
|
||||||
</Suspense>
|
</Route>
|
||||||
</PouchDB>
|
<Route exact path="/">
|
||||||
</Provider>
|
<Redirect to="/home" />
|
||||||
|
</Route>
|
||||||
|
</IonRouterOutlet>
|
||||||
|
</IonReactRouter>
|
||||||
</IonApp>
|
</IonApp>
|
||||||
);
|
);
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
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,
|
|
||||||
},
|
|
||||||
geolocation: {
|
|
||||||
minTimeInterval: minTimeInterval.current?.value,
|
|
||||||
minDistance: minDistance.current?.value,
|
|
||||||
},
|
|
||||||
language: languageSelect.current?.value,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
setI18nLanguage(localLanguage);
|
|
||||||
|
|
||||||
const dismiss = () => {
|
|
||||||
modal.current?.dismiss();
|
|
||||||
setLocalLanguage(settingsState.language);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<IonButton id='open-SettingsDialog'>
|
|
||||||
<IonIcon slot='icon-only' icon={options} />
|
|
||||||
</IonButton>
|
|
||||||
<IonModal
|
|
||||||
ref={modal}
|
|
||||||
trigger='open-SettingsDialog'
|
|
||||||
enterAnimation={enterAnimation}
|
|
||||||
leaveAnimation={leaveAnimation}
|
|
||||||
className='full-height'
|
|
||||||
>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{i18n.settings}</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.language}</IonLabel>
|
|
||||||
</IonListHeader>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>{i18n.colonize(i18n.language)} </IonLabel>
|
|
||||||
<IonSelect
|
|
||||||
placeholder={i18n.languageSelect.placeHolder}
|
|
||||||
ref={languageSelect}
|
|
||||||
value={localLanguage}
|
|
||||||
interface='popover'
|
|
||||||
onIonChange={(ev) => setLocalLanguage(ev.detail.value)}
|
|
||||||
>
|
|
||||||
{i18n.languageSelect.choices.map((choice) => (
|
|
||||||
<IonSelectOption key={choice.value} value={choice.value}>
|
|
||||||
{choice.label}
|
|
||||||
</IonSelectOption>
|
|
||||||
))}
|
|
||||||
</IonSelect>
|
|
||||||
</IonItem>
|
|
||||||
</IonList>
|
|
||||||
<IonList lines='full' class='ion-no-margin'>
|
|
||||||
<IonListHeader lines='full'>
|
|
||||||
<IonLabel>{i18n.user}</IonLabel>
|
|
||||||
</IonListHeader>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>{i18n.colonize(i18n.pseudo)} </IonLabel>
|
|
||||||
<IonInput
|
|
||||||
ref={pseudo}
|
|
||||||
value={settingsState.user.pseudo}
|
|
||||||
autocomplete='nickname'
|
|
||||||
></IonInput>
|
|
||||||
</IonItem>
|
|
||||||
</IonList>
|
|
||||||
<IonList lines='full' class='ion-no-margin'>
|
|
||||||
<IonListHeader lines='full'>
|
|
||||||
<IonLabel>{i18n.geolocation}</IonLabel>
|
|
||||||
</IonListHeader>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>{i18n.colonize(i18n.minTimeInt)}</IonLabel>
|
|
||||||
<IonInput
|
|
||||||
value={settingsState.geolocation.minTimeInterval}
|
|
||||||
ref={minTimeInterval}
|
|
||||||
inputMode='numeric'
|
|
||||||
type='number'
|
|
||||||
></IonInput>
|
|
||||||
</IonItem>
|
|
||||||
<IonItem>
|
|
||||||
<IonLabel>{i18n.colonize(i18n.minDist)}</IonLabel>
|
|
||||||
<IonInput
|
|
||||||
value={settingsState.geolocation.minDistance}
|
|
||||||
ref={minDistance}
|
|
||||||
inputMode='numeric'
|
|
||||||
type='number'
|
|
||||||
></IonInput>
|
|
||||||
</IonItem>
|
|
||||||
</IonList>
|
|
||||||
<IonItem>
|
|
||||||
<IonToolbar class='secondary'>
|
|
||||||
<IonButtons slot='secondary'>
|
|
||||||
<IonButton
|
|
||||||
shape='round'
|
|
||||||
fill='solid'
|
|
||||||
color='primary'
|
|
||||||
onClick={() => save()}
|
|
||||||
>
|
|
||||||
{i18n.save}
|
|
||||||
</IonButton>
|
|
||||||
<IonButton
|
|
||||||
shape='round'
|
|
||||||
fill='outline'
|
|
||||||
color='primary'
|
|
||||||
onClick={() => dismiss()}
|
|
||||||
>
|
|
||||||
{i18n.cancel}{' '}
|
|
||||||
</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonItem>
|
|
||||||
</IonContent>
|
|
||||||
</IonModal>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Settings;
|
|
|
@ -1,13 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { IonButton, IonIcon } from '@ionic/react';
|
|
||||||
import { layersOutline } from 'ionicons/icons';
|
|
||||||
const TileServerChooserButton: React.FC<{}> = () => {
|
|
||||||
return (
|
|
||||||
<IonButton id='open-TileServerChooser'>
|
|
||||||
<IonIcon slot='icon-only' icon={layersOutline} />
|
|
||||||
</IonButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TileServerChooserButton;
|
|
|
@ -1,77 +0,0 @@
|
||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonButtons,
|
|
||||||
IonContent,
|
|
||||||
IonItem,
|
|
||||||
IonLabel,
|
|
||||||
IonList,
|
|
||||||
IonModal,
|
|
||||||
IonRadio,
|
|
||||||
IonRadioGroup,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import React, { useRef } from 'react';
|
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
|
||||||
import { enterAnimation, leaveAnimation } from '../../lib/animation';
|
|
||||||
import { mapActions, MapState } from '../../store/map';
|
|
||||||
import { tileProviders } from './tile';
|
|
||||||
import i18n, { setI18nLanguage } from '../../i18n/index';
|
|
||||||
import { SettingsState } from '../../store/settings';
|
|
||||||
|
|
||||||
const TileServerChooserDialog: React.FC<{}> = () => {
|
|
||||||
const tileProvider = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.scope.tileProvider
|
|
||||||
) as keyof typeof tileProviders;
|
|
||||||
|
|
||||||
const language = useSelector(
|
|
||||||
(state: { settings: SettingsState }) => state.settings.language
|
|
||||||
);
|
|
||||||
|
|
||||||
setI18nLanguage(language);
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const modal = useRef<HTMLIonModalElement>(null);
|
|
||||||
|
|
||||||
const dismiss = () => {
|
|
||||||
modal.current?.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeHandler = (event: any) => {
|
|
||||||
dispatch(mapActions.setTileProvider(event.detail.value));
|
|
||||||
dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonModal
|
|
||||||
ref={modal}
|
|
||||||
trigger='open-TileServerChooser'
|
|
||||||
enterAnimation={enterAnimation}
|
|
||||||
leaveAnimation={leaveAnimation}
|
|
||||||
>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{i18n.chooseYourMap}</IonTitle>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
<IonContent>
|
|
||||||
<IonList>
|
|
||||||
<IonRadioGroup value={tileProvider} onIonChange={changeHandler}>
|
|
||||||
{Object.keys(tileProviders).map((provider) => {
|
|
||||||
return (
|
|
||||||
<IonItem key={provider}>
|
|
||||||
<IonLabel> {tileProviders[provider].name}</IonLabel>
|
|
||||||
<IonRadio slot='start' value={provider} />
|
|
||||||
</IonItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</IonRadioGroup>
|
|
||||||
</IonList>
|
|
||||||
</IonContent>
|
|
||||||
</IonModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TileServerChooserDialog;
|
|
|
@ -1,102 +0,0 @@
|
||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonButtons,
|
|
||||||
IonCard,
|
|
||||||
IonCardContent,
|
|
||||||
IonCardHeader,
|
|
||||||
IonCardTitle,
|
|
||||||
IonContent,
|
|
||||||
IonIcon,
|
|
||||||
IonItem,
|
|
||||||
IonList,
|
|
||||||
IonModal,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { trash } from 'ionicons/icons';
|
|
||||||
import React, { Fragment, useRef } from 'react';
|
|
||||||
import { useFind, useDB } from 'react-pouchdb';
|
|
||||||
import { deleteGps } from '../../db/gpx';
|
|
||||||
import { enterAnimation, leaveAnimation } from '../../lib/animation';
|
|
||||||
import phoneRoute from '../../theme/icons/font-gis/svg/routing/uEB08-phone-route-nons.svg';
|
|
||||||
import GpxImport from './gpx-import';
|
|
||||||
import GpxExport from './GpxExport';
|
|
||||||
import i18n, { setI18nLanguage } from '../../i18n/index';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { SettingsState } from '../../store/settings';
|
|
||||||
|
|
||||||
const TrackBrowser: React.FC<{}> = () => {
|
|
||||||
const gpxes = useFind({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const db = useDB();
|
|
||||||
|
|
||||||
const modal = useRef<HTMLIonModalElement>(null);
|
|
||||||
|
|
||||||
const dismiss = () => {
|
|
||||||
modal.current?.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
const language = useSelector(
|
|
||||||
(state: { settings: SettingsState }) => state.settings.language
|
|
||||||
);
|
|
||||||
|
|
||||||
setI18nLanguage(language);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Fragment>
|
|
||||||
<IonButton id='track-browser-button'>
|
|
||||||
<img src={phoneRoute} alt='GPS tracks' width='24px' />
|
|
||||||
</IonButton>
|
|
||||||
<IonModal
|
|
||||||
ref={modal}
|
|
||||||
className='full-height'
|
|
||||||
trigger='track-browser-button'
|
|
||||||
enterAnimation={enterAnimation}
|
|
||||||
leaveAnimation={leaveAnimation}
|
|
||||||
>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{i18n.tracks}</IonTitle>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<GpxImport />
|
|
||||||
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
<IonContent>
|
|
||||||
{gpxes.map((gpx: any) => (
|
|
||||||
<IonCard key={gpx._id}>
|
|
||||||
<IonCardHeader>
|
|
||||||
<IonItem>
|
|
||||||
<IonCardTitle>{gpx.gpx.metadata.name}</IonCardTitle>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<GpxExport gpx={gpx} />
|
|
||||||
<IonButton
|
|
||||||
onClick={() => {
|
|
||||||
deleteGps(db, { _id: gpx._id, _rev: gpx._rev });
|
|
||||||
}}
|
|
||||||
color='danger'
|
|
||||||
>
|
|
||||||
<IonIcon slot='icon-only' icon={trash} />
|
|
||||||
</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonItem>
|
|
||||||
<IonCardContent>
|
|
||||||
<IonList>
|
|
||||||
<IonItem>{gpx.gpx.metadata.time}</IonItem>
|
|
||||||
<IonItem>{gpx.gpx.metadata.desc}</IonItem>
|
|
||||||
</IonList>
|
|
||||||
</IonCardContent>
|
|
||||||
</IonCardHeader>
|
|
||||||
</IonCard>
|
|
||||||
))}
|
|
||||||
</IonContent>
|
|
||||||
</IonModal>
|
|
||||||
</Fragment>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TrackBrowser;
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'avatar-initials';
|
|
Binary file not shown.
Before Width: | Height: | Size: 546 B |
|
@ -1 +0,0 @@
|
||||||
declare module 'font-gis';
|
|
|
@ -1,43 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Geolocation } from '@awesome-cordova-plugins/geolocation';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
|
|
||||||
import '../../theme/get-location.css';
|
|
||||||
import { IonButton, IonIcon } from '@ionic/react';
|
|
||||||
import { locateOutline } from 'ionicons/icons';
|
|
||||||
|
|
||||||
const GetLocation: React.FC<{}> = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const onClickHandler = (event: any) => {
|
|
||||||
console.log('Click handler');
|
|
||||||
Geolocation.getCurrentPosition().then((position) => {
|
|
||||||
console.log(
|
|
||||||
`Click handler, position: ${position.coords.latitude}, ${position.coords.longitude}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
mapActions.setCenter({
|
|
||||||
lat: position.coords.latitude,
|
|
||||||
lon: position.coords.longitude,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
mapActions.setCurrent({
|
|
||||||
lat: position.coords.latitude,
|
|
||||||
lon: position.coords.longitude,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonButton onClick={onClickHandler} className='get-location' shape='round' size='small' fill='solid'>
|
|
||||||
<IonIcon slot='icon-only' icon={locateOutline} color='white' />
|
|
||||||
</IonButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GetLocation;
|
|
|
@ -1,58 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useDB } from 'react-pouchdb';
|
|
||||||
|
|
||||||
import GPX from '../../lib/gpx-parser-builder';
|
|
||||||
|
|
||||||
import '../../theme/get-location.css';
|
|
||||||
import { IonIcon, IonItem } from '@ionic/react';
|
|
||||||
import { cloudUpload } from 'ionicons/icons';
|
|
||||||
import { pushGpx } from '../../db/gpx';
|
|
||||||
|
|
||||||
const GpxImport: React.FC<{}> = () => {
|
|
||||||
const db = useDB();
|
|
||||||
|
|
||||||
const onChangeHandler = (event: any) => {
|
|
||||||
console.log('On change handler');
|
|
||||||
const file: File = event.target.files[0];
|
|
||||||
const fileReader = new FileReader();
|
|
||||||
fileReader.readAsText(file);
|
|
||||||
|
|
||||||
fileReader.addEventListener(
|
|
||||||
'load',
|
|
||||||
() => {
|
|
||||||
// this will then display a text file
|
|
||||||
console.log(fileReader.result);
|
|
||||||
const gpx = GPX.parse(fileReader.result);
|
|
||||||
pushGpx(db, {
|
|
||||||
gpx,
|
|
||||||
metadata: {
|
|
||||||
lastModified: new Date(file.lastModified).toISOString(),
|
|
||||||
importDate: new Date().toISOString(),
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonItem>
|
|
||||||
<input
|
|
||||||
type='file'
|
|
||||||
id='gpx-import'
|
|
||||||
className='input-file'
|
|
||||||
accept='.gpx'
|
|
||||||
onChange={onChangeHandler}
|
|
||||||
/>
|
|
||||||
<label htmlFor='gpx-import'>
|
|
||||||
<IonIcon slot='icon-only' icon={cloudUpload} title="import" />
|
|
||||||
</label>
|
|
||||||
</IonItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GpxImport;
|
|
|
@ -1,20 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { useFind } from 'react-pouchdb';
|
|
||||||
|
|
||||||
import Gpx from './Gpx';
|
|
||||||
|
|
||||||
const Gpxes: React.FC<{}> = () => {
|
|
||||||
const gpxes = useFind({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return gpxes.map((gpx: any) => {
|
|
||||||
console.log('doc');
|
|
||||||
return <Gpx key={gpx._id} gpx={gpx} />;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Gpxes;
|
|
|
@ -1,90 +0,0 @@
|
||||||
import react, { useMemo, useEffect, useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { useDB } from 'react-pouchdb';
|
|
||||||
|
|
||||||
import Layer from '../slippy/layer';
|
|
||||||
import Slippy from '../slippy/slippy';
|
|
||||||
import TiledMap from './tiled-map';
|
|
||||||
import GetLocation from './get-location';
|
|
||||||
import Whiteboard from './whiteboard';
|
|
||||||
import CurrentLocation from './CurrentLocation';
|
|
||||||
import {
|
|
||||||
IonApp,
|
|
||||||
IonButtons,
|
|
||||||
IonContent,
|
|
||||||
IonFooter,
|
|
||||||
IonHeader,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
|
|
||||||
import Gpxes from './gpxes';
|
|
||||||
import GpxRecord from './GpxRecord';
|
|
||||||
import { initDb } from '../../db';
|
|
||||||
import TileServerChooserButton from './TileServerChooserButton';
|
|
||||||
import TileServerChooserDialog from './TileServerChooserDialog';
|
|
||||||
import TrackBrowser from './TracksBrowser';
|
|
||||||
import Settings from './Settings';
|
|
||||||
import Reticle from './Reticle';
|
|
||||||
import LocationInfo from './LocationInfo';
|
|
||||||
|
|
||||||
const Map: react.FC<{}> = () => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const resizeHandler = () => {
|
|
||||||
dispatch(mapActions.resize());
|
|
||||||
};
|
|
||||||
const debouncedResizeHandler = useMemo(
|
|
||||||
() => _.debounce(resizeHandler, 500),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const db = useDB();
|
|
||||||
|
|
||||||
const [dbReady, setDbReady] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
window.addEventListener('resize', debouncedResizeHandler);
|
|
||||||
initDb(db, setDbReady);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IonContent fullscreen={true}>
|
|
||||||
<IonApp>
|
|
||||||
<TileServerChooserDialog />
|
|
||||||
<LocationInfo />
|
|
||||||
<Slippy>
|
|
||||||
<Whiteboard fixedChildren={<Reticle />}>
|
|
||||||
<CurrentLocation />
|
|
||||||
dbReady && <Gpxes />
|
|
||||||
</Whiteboard>
|
|
||||||
<Layer>
|
|
||||||
<TiledMap />
|
|
||||||
</Layer>
|
|
||||||
</Slippy>
|
|
||||||
</IonApp>
|
|
||||||
</IonContent>
|
|
||||||
<IonHeader className='ion-no-border' translucent={true}>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<TrackBrowser />
|
|
||||||
<GpxRecord />
|
|
||||||
<TileServerChooserButton />
|
|
||||||
<Settings />
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
<IonFooter className='ion-no-border'>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonButtons>
|
|
||||||
<GetLocation />
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonFooter>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Map;
|
|
|
@ -1,153 +0,0 @@
|
||||||
import React, { Fragment } from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { MapState } from '../../store/map';
|
|
||||||
|
|
||||||
export interface TileProvider {
|
|
||||||
name: string;
|
|
||||||
minZoom: number;
|
|
||||||
maxZoom: number;
|
|
||||||
getTileUrl: { (zoom: number, x: number, y: number): string };
|
|
||||||
}
|
|
||||||
|
|
||||||
const getRandomItem = (items: any[]) => {
|
|
||||||
const idx = Math.floor(Math.random() * items.length);
|
|
||||||
return items[idx];
|
|
||||||
};
|
|
||||||
|
|
||||||
const abc = ['a', 'b', 'c'];
|
|
||||||
|
|
||||||
export const tileProviders: any = {
|
|
||||||
osm: {
|
|
||||||
name: 'Open Street Map',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 19,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png',
|
|
||||||
},
|
|
||||||
osmfr: {
|
|
||||||
name: 'Open Street Map France',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 20,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://' +
|
|
||||||
getRandomItem(abc) +
|
|
||||||
'.tile.openstreetmap.fr/osmfr/' +
|
|
||||||
zoom +
|
|
||||||
'/' +
|
|
||||||
x +
|
|
||||||
'/' +
|
|
||||||
y +
|
|
||||||
'.png',
|
|
||||||
},
|
|
||||||
otm: {
|
|
||||||
name: 'Open Topo Map',
|
|
||||||
minZoom: 2,
|
|
||||||
maxZoom: 17,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://' +
|
|
||||||
getRandomItem(abc) +
|
|
||||||
'.tile.opentopomap.org/' +
|
|
||||||
zoom +
|
|
||||||
'/' +
|
|
||||||
x +
|
|
||||||
'/' +
|
|
||||||
y +
|
|
||||||
'.png',
|
|
||||||
},
|
|
||||||
cyclosm: {
|
|
||||||
name: 'CyclOSM',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 19,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://' +
|
|
||||||
getRandomItem(abc) +
|
|
||||||
'.tile-cyclosm.openstreetmap.fr/cyclosm/' +
|
|
||||||
zoom +
|
|
||||||
'/' +
|
|
||||||
x +
|
|
||||||
'/' +
|
|
||||||
y +
|
|
||||||
'.png',
|
|
||||||
},
|
|
||||||
//https://b.tile.openstreetmap.fr/openriverboatmap/20/535762/382966.png
|
|
||||||
openriverboatmap: {
|
|
||||||
name: 'Open River Boat Map',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 20,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://' +
|
|
||||||
getRandomItem(abc) +
|
|
||||||
'.tile.openstreetmap.fr/openriverboatmap/' +
|
|
||||||
zoom +
|
|
||||||
'/' +
|
|
||||||
x +
|
|
||||||
'/' +
|
|
||||||
y +
|
|
||||||
'.png',
|
|
||||||
},
|
|
||||||
|
|
||||||
// cyclosmlite: {
|
|
||||||
// name: 'CyclOSM lite',
|
|
||||||
// minZoom: 0,
|
|
||||||
// maxZoom: 19,
|
|
||||||
// getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
// 'https://' +
|
|
||||||
// getRandomItem(abc) +
|
|
||||||
// '.tile-cyclosm.openstreetmap.fr/cyclosm-lite/' +
|
|
||||||
// zoom +
|
|
||||||
// '/' +
|
|
||||||
// x +
|
|
||||||
// '/' +
|
|
||||||
// y +
|
|
||||||
// '.png',
|
|
||||||
// },
|
|
||||||
// esrisat: {
|
|
||||||
// name: 'ESRI Satellite',
|
|
||||||
// minZoom: 0,
|
|
||||||
// maxZoom: 19,
|
|
||||||
// getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
// 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/' +
|
|
||||||
// zoom +
|
|
||||||
// '/' +
|
|
||||||
// x +
|
|
||||||
// '/' +
|
|
||||||
// y +
|
|
||||||
// '.jpg',
|
|
||||||
// },
|
|
||||||
};
|
|
||||||
|
|
||||||
const Tile: React.FC<{
|
|
||||||
ix: number;
|
|
||||||
iy: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
zoom: number;
|
|
||||||
}> = (props: {
|
|
||||||
ix: number;
|
|
||||||
iy: number;
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
zoom: number;
|
|
||||||
}) => {
|
|
||||||
const tileProvider = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.scope.tileProvider
|
|
||||||
) as keyof typeof tileProviders;
|
|
||||||
|
|
||||||
const x = props.x % 2 ** props.zoom;
|
|
||||||
const canRender = props.y >= 0 && props.y <= 2 ** props.zoom - 1;
|
|
||||||
|
|
||||||
if (canRender) {
|
|
||||||
return (
|
|
||||||
canRender && (
|
|
||||||
<img
|
|
||||||
src={tileProviders[tileProvider].getTileUrl(props.zoom, x, props.y)}
|
|
||||||
className='tile'
|
|
||||||
alt=''
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <Fragment />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Tile;
|
|
|
@ -1,36 +0,0 @@
|
||||||
import react from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { MapState } from '../../store/map';
|
|
||||||
import Tile from './tile';
|
|
||||||
|
|
||||||
import '../../theme/map.css';
|
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
export const tileSize = 256;
|
|
||||||
|
|
||||||
const TiledMap: react.FC<{}> = () => {
|
|
||||||
|
|
||||||
const tilesState = useSelector((state: { map: MapState }) => state.map.tiles);
|
|
||||||
console.log(`tilesState: ${JSON.stringify(tilesState)}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='tiles'>
|
|
||||||
{_.range(tilesState.nb.y).map((iy) => (
|
|
||||||
<div key={'y' + iy} className='tilesRow'>
|
|
||||||
{_.range(tilesState.nb.y).map((ix) => (
|
|
||||||
<Tile
|
|
||||||
key={'x' + ix + 'y' + iy}
|
|
||||||
iy={iy}
|
|
||||||
ix={ix}
|
|
||||||
x={tilesState.first.x + ix}
|
|
||||||
y={tilesState.first.y + iy}
|
|
||||||
zoom={tilesState.zoom}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TiledMap;
|
|
|
@ -1,34 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { MapState } from '../../store/map';
|
|
||||||
|
|
||||||
interface WhiteboardProps {
|
|
||||||
children?: any;
|
|
||||||
fixedChildren?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Whiteboard: React.FC<WhiteboardProps> = (props: WhiteboardProps) => {
|
|
||||||
const whiteBoardState = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.whiteboard
|
|
||||||
);
|
|
||||||
const windowState = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.window
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
width={windowState.width}
|
|
||||||
height={windowState.height}
|
|
||||||
className='whiteboard'
|
|
||||||
>
|
|
||||||
{props.fixedChildren}
|
|
||||||
<g
|
|
||||||
transform={`scale(${whiteBoardState.scale}) translate(${whiteBoardState.translation.x},${whiteBoardState.translation.y})`}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Whiteboard;
|
|
|
@ -1,130 +0,0 @@
|
||||||
import react, { useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
|
|
||||||
interface DoubleTouchHandlerProps {
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DoubleTouchHandler: react.FC<DoubleTouchHandlerProps> = (
|
|
||||||
props: DoubleTouchHandlerProps
|
|
||||||
) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const initialTouchState = {
|
|
||||||
state: 'up',
|
|
||||||
touches: [
|
|
||||||
{ x: -1, y: -1 },
|
|
||||||
{ x: -1, y: -1 },
|
|
||||||
],
|
|
||||||
distance: -1,
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [touchState, setTouchState] = useState(initialTouchState);
|
|
||||||
|
|
||||||
console.log('DoubleTouchHandler, touchState: ' + JSON.stringify(touchState));
|
|
||||||
|
|
||||||
const genericHandler = (event: any) => {
|
|
||||||
console.log('Log - Event: ' + event.type);
|
|
||||||
if (event.type.startsWith('touch')) {
|
|
||||||
if (event.touches.length > 1) {
|
|
||||||
console.log(
|
|
||||||
`${event.touches.length} touches, (${event.touches[0].pageX}, ${event.touches[0].pageY}), (${event.touches[1].pageX}, ${event.touches[1].pageY})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('touchState: ' + JSON.stringify(touchState));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const touchCancelHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
setTouchState(initialTouchState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchStartHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
if (event.touches.length === 2) {
|
|
||||||
setTouchState({
|
|
||||||
state: 'double',
|
|
||||||
touches: [
|
|
||||||
{ x: event.touches[0].pageX, y: event.touches[0].pageY },
|
|
||||||
{ x: event.touches[1].pageX, y: event.touches[1].pageY },
|
|
||||||
],
|
|
||||||
distance: Math.sqrt(
|
|
||||||
(event.touches[0].pageX - event.touches[1].pageX) ** 2 +
|
|
||||||
(event.touches[0].pageY - event.touches[1].pageY) ** 2
|
|
||||||
),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchEndHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
setTouchState(initialTouchState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchMoveHandler = (event: any) => {
|
|
||||||
if (
|
|
||||||
(touchState.state === 'double') &&
|
|
||||||
((Date.now() - touchState.timestamp) > 50)
|
|
||||||
) {
|
|
||||||
if (event.touches.length === 2) {
|
|
||||||
genericHandler(event);
|
|
||||||
const newDistance = Math.sqrt(
|
|
||||||
(event.touches[0].pageX - event.touches[1].pageX) ** 2 +
|
|
||||||
(event.touches[0].pageY - event.touches[1].pageY) ** 2
|
|
||||||
);
|
|
||||||
const factor = newDistance / touchState.distance;
|
|
||||||
console.log(`+++++++++ ZOOM Factor is ${factor} ++++++++++`);
|
|
||||||
setTouchState({
|
|
||||||
state: 'double',
|
|
||||||
touches: [
|
|
||||||
{ x: event.touches[0].pageX, y: event.touches[0].pageY },
|
|
||||||
{ x: event.touches[1].pageX, y: event.touches[1].pageY },
|
|
||||||
],
|
|
||||||
distance: newDistance,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
const previousCenter = {
|
|
||||||
x: (touchState.touches[0].x + touchState.touches[1].x) / 2,
|
|
||||||
y: (touchState.touches[0].y + touchState.touches[1].y) / 2,
|
|
||||||
};
|
|
||||||
const currentCenter = {
|
|
||||||
x: (event.touches[0].pageX + event.touches[1].pageX) / 2,
|
|
||||||
y: (event.touches[0].pageY + event.touches[1].pageY) / 2,
|
|
||||||
};
|
|
||||||
dispatch(
|
|
||||||
mapActions.scale({
|
|
||||||
factor: factor,
|
|
||||||
center: currentCenter,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
mapActions.shift({
|
|
||||||
x: currentCenter.x - previousCenter.x,
|
|
||||||
y: currentCenter.y - previousCenter.y,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='viewport double-touch-handler'
|
|
||||||
onTouchStart={touchStartHandler}
|
|
||||||
onTouchMove={touchMoveHandler}
|
|
||||||
onTouchEnd={touchEndHandler}
|
|
||||||
onTouchCancel={touchCancelHandler}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DoubleTouchHandler;
|
|
|
@ -1,30 +0,0 @@
|
||||||
import react from 'react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { MapState } from '../../store/map';
|
|
||||||
|
|
||||||
import '../../theme/layer.css';
|
|
||||||
|
|
||||||
|
|
||||||
const Layer: react.FC<{
|
|
||||||
children?: JSX.Element;
|
|
||||||
}> = (props: { children?: JSX.Element }) => {
|
|
||||||
const slippyState = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.slippy
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`--- Rendering layer, slippyState: ${JSON.stringify(slippyState)} ---`
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='background'
|
|
||||||
style={{
|
|
||||||
transform: `translate(${slippyState.translation.x}px, ${slippyState.translation.y}px) scale(${slippyState.scale})`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Layer;
|
|
|
@ -1,118 +0,0 @@
|
||||||
import react, { useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
|
|
||||||
|
|
||||||
interface MouseHandlerProps {
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const MouseHandler: react.FC<MouseHandlerProps> = (
|
|
||||||
props: MouseHandlerProps
|
|
||||||
) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const initialMouseState = {
|
|
||||||
down: false,
|
|
||||||
starting: { x: -1, y: -1 },
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [mouseState, setMouseState] = useState(initialMouseState);
|
|
||||||
|
|
||||||
console.log('MouseHandler, mouseState: ' + JSON.stringify(mouseState));
|
|
||||||
|
|
||||||
const genericHandler = (event: any) => {
|
|
||||||
console.log(`Log - Event: ${event.type}`);
|
|
||||||
if (event.pageX !== undefined) {
|
|
||||||
console.log(`Mouse : ${event.pageX}, ${event.pageY}, target: ${event.target}`);
|
|
||||||
console.log(
|
|
||||||
`mouseState: ' ${JSON.stringify(mouseState)} (+${
|
|
||||||
Date.now() - mouseState.timestamp
|
|
||||||
}ms) `
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseLeaveHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
// throtteledMouseMoveHandler.cancel();
|
|
||||||
setMouseState(initialMouseState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseDownHandler = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
genericHandler(event);
|
|
||||||
setMouseState({
|
|
||||||
down: true,
|
|
||||||
starting: { x: event.pageX, y: event.pageY },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseUpHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
// event.preventDefault();
|
|
||||||
setMouseState(initialMouseState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseMoveHandler = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
if (mouseState.down && Date.now() - mouseState.timestamp > 50) {
|
|
||||||
genericHandler(event);
|
|
||||||
console.log(
|
|
||||||
`dispatch ${JSON.stringify({
|
|
||||||
x: event.pageX - mouseState.starting.x,
|
|
||||||
y: event.pageY - mouseState.starting.y,
|
|
||||||
})}`
|
|
||||||
);
|
|
||||||
dispatch(
|
|
||||||
mapActions.shift({
|
|
||||||
x: event.pageX - mouseState.starting.x,
|
|
||||||
y: event.pageY - mouseState.starting.y,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
setMouseState({
|
|
||||||
down: true,
|
|
||||||
starting: {
|
|
||||||
x: event.pageX,
|
|
||||||
y: event.pageY,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// const throtteledMouseMoveHandler = useCallback(
|
|
||||||
// _.throttle(mouseMoveHandler, 50),
|
|
||||||
// [mouseState.down, mouseState.starting.x, mouseState.starting.y]
|
|
||||||
// );
|
|
||||||
|
|
||||||
const doubleClickHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
dispatch(
|
|
||||||
mapActions.scale({
|
|
||||||
factor: Math.SQRT2,
|
|
||||||
center: { x: event.pageX, y: event.pageY },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='viewport mouse-handler'
|
|
||||||
onMouseDown={mouseDownHandler}
|
|
||||||
onMouseMove={mouseMoveHandler}
|
|
||||||
onMouseUp={mouseUpHandler}
|
|
||||||
onMouseLeave={mouseLeaveHandler}
|
|
||||||
onDoubleClick={doubleClickHandler}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MouseHandler;
|
|
|
@ -1,100 +0,0 @@
|
||||||
import react, { useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
|
|
||||||
interface SingleTouchHandlerProps {
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const SingleTouchHandler: react.FC<SingleTouchHandlerProps> = (
|
|
||||||
props: SingleTouchHandlerProps
|
|
||||||
) => {
|
|
||||||
const initialTouchState = {
|
|
||||||
state: 'up',
|
|
||||||
touch: { x: -1, y: -1 },
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const [touchState, setTouchState] = useState(initialTouchState);
|
|
||||||
|
|
||||||
console.log('SingleTouchHandler, touchState: ' + JSON.stringify(touchState));
|
|
||||||
|
|
||||||
const genericHandler = (event: any) => {
|
|
||||||
console.log('Log - Event: ' + event.type);
|
|
||||||
if (event.type.startsWith('touch')) {
|
|
||||||
if (event.touches.length > 0) {
|
|
||||||
console.log(
|
|
||||||
`Touch1 : (${event.touches[0].pageX}, ${event.touches[0].pageY})`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
console.log('touchState: ' + JSON.stringify(touchState));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchCancelHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
setTouchState(initialTouchState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchStartHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
// event.preventDefault();
|
|
||||||
if (event.touches.length === 1) {
|
|
||||||
setTouchState({
|
|
||||||
state: 'pointer',
|
|
||||||
touch: { x: event.touches[0].pageX, y: event.touches[0].pageY },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchEndHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
// event.preventDefault();
|
|
||||||
setTouchState(initialTouchState);
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchMoveHandler = (event: any) => {
|
|
||||||
// event.preventDefault();
|
|
||||||
if (
|
|
||||||
touchState.state === 'pointer' &&
|
|
||||||
Date.now() - touchState.timestamp > 50
|
|
||||||
) {
|
|
||||||
if (event.touches.length === 1) {
|
|
||||||
genericHandler(event);
|
|
||||||
dispatch(
|
|
||||||
mapActions.shift({
|
|
||||||
x: event.touches[0].pageX - touchState.touch.x,
|
|
||||||
y: event.touches[0].pageY - touchState.touch.y,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setTouchState({
|
|
||||||
state: 'pointer',
|
|
||||||
touch: {
|
|
||||||
x: event.touches[0].pageX,
|
|
||||||
y: event.touches[0].pageY,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='viewport single-touch-handler'
|
|
||||||
onTouchStart={touchStartHandler}
|
|
||||||
onTouchMove={touchMoveHandler}
|
|
||||||
onTouchEnd={touchEndHandler}
|
|
||||||
onTouchCancel={touchCancelHandler}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SingleTouchHandler;
|
|
|
@ -1,40 +0,0 @@
|
||||||
import react from 'react';
|
|
||||||
|
|
||||||
import MouseHandler from './mouse-handler';
|
|
||||||
import SingleTouchHandler from './single-touch-handler';
|
|
||||||
import DoubleTouchHandler from './double-touch-handler';
|
|
||||||
import WheelHandler from './wheel-handler';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { MapState } from '../../store/map';
|
|
||||||
|
|
||||||
import '../../theme/slippy.css';
|
|
||||||
|
|
||||||
interface SlippyProps {
|
|
||||||
children?: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Slippy: react.FC<SlippyProps> = (props: SlippyProps) => {
|
|
||||||
//console.log(`--- Rendering viewport, props: ${JSON.stringify(props)} ---`);
|
|
||||||
|
|
||||||
const slippyState = useSelector(
|
|
||||||
(state: { map: MapState }) => state.map.slippy
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log(`slippyState: ${JSON.stringify(slippyState)}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='slippy'>
|
|
||||||
<MouseHandler>
|
|
||||||
<SingleTouchHandler>
|
|
||||||
<DoubleTouchHandler>
|
|
||||||
<WheelHandler>
|
|
||||||
{props.children}
|
|
||||||
</WheelHandler>
|
|
||||||
</DoubleTouchHandler>
|
|
||||||
</SingleTouchHandler>
|
|
||||||
</MouseHandler>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Slippy;
|
|
|
@ -1,62 +0,0 @@
|
||||||
import react, { useState } from 'react';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
|
|
||||||
import { mapActions } from '../../store/map';
|
|
||||||
|
|
||||||
|
|
||||||
interface WheelHandlerProps {
|
|
||||||
children: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const WheelHandler: react.FC<WheelHandlerProps> = (
|
|
||||||
props: WheelHandlerProps
|
|
||||||
) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
|
|
||||||
const initialWheelState = {
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const [wheelState, setWheelState] = useState(initialWheelState);
|
|
||||||
|
|
||||||
console.log('WheelHandler, wheelState: ' + JSON.stringify(wheelState));
|
|
||||||
|
|
||||||
const genericHandler = (event: any) => {
|
|
||||||
console.log(`Log - Event: ${event.type}`);
|
|
||||||
if (event.deltaY !== undefined) {
|
|
||||||
console.log(`Wheel : ${event.deltaY}, ${event.deltaMode}`);
|
|
||||||
console.log(
|
|
||||||
`wheelState: ' ${JSON.stringify(wheelState)} (+${
|
|
||||||
Date.now() - wheelState.timestamp
|
|
||||||
}ms) `
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const wheelEventHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
if (
|
|
||||||
event.deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
|
|
||||||
Date.now() - wheelState.timestamp > 100
|
|
||||||
) {
|
|
||||||
dispatch(
|
|
||||||
mapActions.scale({
|
|
||||||
factor: event.deltaY < 0 ? Math.SQRT2 : Math.SQRT1_2,
|
|
||||||
center: { x: event.pageX, y: event.pageY },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
setWheelState({
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='viewport wheel-handler' onWheel={wheelEventHandler}>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WheelHandler;
|
|
|
@ -1,101 +0,0 @@
|
||||||
import { initDb } from '.';
|
|
||||||
import PouchDB from 'pouchdb';
|
|
||||||
import { appendTrkpt, deleteCurrent, saveCurrent } from './gpx';
|
|
||||||
|
|
||||||
import PouchDBFind from 'pouchdb-find';
|
|
||||||
import { mkdtempSync, rmdirSync } from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
import { tmpdir } from 'os';
|
|
||||||
PouchDB.plugin(PouchDBFind);
|
|
||||||
//PouchDB.plugin(require('pouchdb-find'));
|
|
||||||
|
|
||||||
// export class PouchService {
|
|
||||||
// constructor() {
|
|
||||||
// PouchDB.plugin(PouchDBFind);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// jest.useFakeTimers();
|
|
||||||
|
|
||||||
const tmpDir = mkdtempSync(join(tmpdir(), 'dyomedea-testDB'));
|
|
||||||
|
|
||||||
var db: PouchDB.Database;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
db = new PouchDB(tmpDir);
|
|
||||||
await initDb(db, () => {});
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(async () => {
|
|
||||||
// await db.close();
|
|
||||||
await db.destroy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking that trkpts are beeing inserted', () => {
|
|
||||||
test('and that we have two documents after the first call', async () => {
|
|
||||||
await appendTrkpt(db, {});
|
|
||||||
const results = await db.find({ selector: {} });
|
|
||||||
const docs = results.docs;
|
|
||||||
// console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(2);
|
|
||||||
});
|
|
||||||
test('and that we have three documents after the second call', async () => {
|
|
||||||
await appendTrkpt(db, {});
|
|
||||||
await appendTrkpt(db, {});
|
|
||||||
const results = await db.find({ selector: {} });
|
|
||||||
const docs = results.docs;
|
|
||||||
console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(3);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking that saveCurrent() is working as expected', () => {
|
|
||||||
test(', that we still have two documents after saving.', async () => {
|
|
||||||
await appendTrkpt(db, {});
|
|
||||||
await saveCurrent(db);
|
|
||||||
const results = await db.find({
|
|
||||||
selector: {},
|
|
||||||
});
|
|
||||||
const docs = results.docs;
|
|
||||||
// console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(2);
|
|
||||||
});
|
|
||||||
test("and that we don't have current tracks after saving.", async () => {
|
|
||||||
await appendTrkpt(db, {});
|
|
||||||
await saveCurrent(db);
|
|
||||||
const results = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
subtype: 'current',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const docs = results.docs;
|
|
||||||
// console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking that deleteCurrent() is working as expected', () => {
|
|
||||||
test(', that we have no more documents after deleting.', async () => {
|
|
||||||
await appendTrkpt(db, { time: '' });
|
|
||||||
await deleteCurrent(db);
|
|
||||||
const results = await db.find({
|
|
||||||
selector: {},
|
|
||||||
});
|
|
||||||
const docs = results.docs;
|
|
||||||
// console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(0);
|
|
||||||
});
|
|
||||||
test(', that we have two documents from a saved track after deleting the current one.', async () => {
|
|
||||||
await appendTrkpt(db, { time: '' });
|
|
||||||
await saveCurrent(db);
|
|
||||||
await appendTrkpt(db, { time: '' });
|
|
||||||
await deleteCurrent(db);
|
|
||||||
const results = await db.find({
|
|
||||||
selector: {},
|
|
||||||
});
|
|
||||||
const docs = results.docs;
|
|
||||||
// console.log(`docs: ${JSON.stringify(docs)}`);
|
|
||||||
expect(docs.length).toBe(2);
|
|
||||||
});
|
|
||||||
});
|
|
196
src/db/gpx.ts
196
src/db/gpx.ts
|
@ -1,196 +0,0 @@
|
||||||
import {
|
|
||||||
Sha256,
|
|
||||||
string_to_bytes,
|
|
||||||
bytes_to_base64,
|
|
||||||
} from '@openpgp/asmcrypto.js';
|
|
||||||
import GPX from '../lib/gpx-parser-builder';
|
|
||||||
|
|
||||||
export const pushGpx = async (db: any, payload: any) => {
|
|
||||||
const gpxString = JSON.stringify(payload.gpx);
|
|
||||||
const sha = new Sha256();
|
|
||||||
const result = sha.process(string_to_bytes(gpxString)).finish().result;
|
|
||||||
var _id;
|
|
||||||
if (result === null) {
|
|
||||||
console.log(`Can't hash`);
|
|
||||||
_id = crypto.randomUUID();
|
|
||||||
} else {
|
|
||||||
_id = bytes_to_base64(result);
|
|
||||||
console.log(`Digest: ${_id}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await db.get(_id);
|
|
||||||
alert('This file has alerady been imported.');
|
|
||||||
return;
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
var gpx = JSON.parse(gpxString);
|
|
||||||
var points: any[] = [];
|
|
||||||
|
|
||||||
const prune = (object: any) => {
|
|
||||||
for (var key in object) {
|
|
||||||
if (key === 'trkpt') {
|
|
||||||
points.push(...object[key]);
|
|
||||||
object[key] = [];
|
|
||||||
} else {
|
|
||||||
if (typeof object[key] === 'object') {
|
|
||||||
prune(object[key]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
prune(gpx);
|
|
||||||
|
|
||||||
const doc = { ...payload, _id, type: 'gpx', subtype: 'other', gpx };
|
|
||||||
console.log(JSON.stringify(doc));
|
|
||||||
await db.put(doc);
|
|
||||||
|
|
||||||
for (var point in points) {
|
|
||||||
const docPoint = { type: 'trkpt', gpx: _id, trkpt: points[point] };
|
|
||||||
console.log(JSON.stringify(docPoint));
|
|
||||||
await db.post(docPoint);
|
|
||||||
console.log(JSON.stringify(docPoint));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialGpx = {
|
|
||||||
type: 'gpx',
|
|
||||||
subtype: 'current',
|
|
||||||
gpx: {
|
|
||||||
$: {
|
|
||||||
version: '1.1',
|
|
||||||
creator: 'dyomedea version 0.000001',
|
|
||||||
xmlns: 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
||||||
'xsi:schemaLocation':
|
|
||||||
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www8.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/WaypointExtension/v1 http://www8.garmin.com/xmlschemas/WaypointExtensionv1.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd',
|
|
||||||
'xmlns:gpxx': 'http://www.garmin.com/xmlschemas/GpxExtensions/v3',
|
|
||||||
'xmlns:wptx1': 'http://www.garmin.com/xmlschemas/WaypointExtension/v1',
|
|
||||||
'xmlns:gpxtpx': 'http://www.garmin.com/xmlschemas/TrackPointExtension/v1',
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
name: 'Tbd',
|
|
||||||
desc: 'Tbd',
|
|
||||||
time: '2022-08-27T21:35:01.000Z',
|
|
||||||
},
|
|
||||||
trk: [
|
|
||||||
{
|
|
||||||
name: 'Tbd',
|
|
||||||
trkseg: [
|
|
||||||
{
|
|
||||||
trkpt: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
lastModified: '2022-08-27T21:35:02.000Z',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const appendTrkpt = async (db: any, trkpt: any) => {
|
|
||||||
const currents = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
subtype: 'current',
|
|
||||||
},
|
|
||||||
fields: ['_id'],
|
|
||||||
});
|
|
||||||
console.log(`appendTrkpt - db.find() : ${JSON.stringify(currents)}`);
|
|
||||||
|
|
||||||
var currentId;
|
|
||||||
if (currents.docs.length > 0) {
|
|
||||||
currentId = currents.docs[0]._id;
|
|
||||||
} else {
|
|
||||||
initialGpx.metadata.lastModified = trkpt.time;
|
|
||||||
initialGpx.gpx.metadata.time = trkpt.time;
|
|
||||||
const response = await db.post(initialGpx);
|
|
||||||
currentId = response.id;
|
|
||||||
}
|
|
||||||
console.log(`currentId: ${currentId}`);
|
|
||||||
const docPoint = { type: 'trkpt', gpx: currentId, trkpt: trkpt };
|
|
||||||
await db.post(docPoint);
|
|
||||||
console.log(JSON.stringify(docPoint));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const saveCurrent = async (db: any) => {
|
|
||||||
const currents = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
subtype: 'current',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(`saveCurrent - db.find() : ${JSON.stringify(currents)}`);
|
|
||||||
if (currents.docs.length > 0) {
|
|
||||||
const doc = currents.docs[0];
|
|
||||||
doc.subtype = 'other';
|
|
||||||
await db.put(doc);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteGps = async (
|
|
||||||
db: any,
|
|
||||||
gps: { _id: string; _rev: string }
|
|
||||||
) => {
|
|
||||||
console.log(`Deleting document ${JSON.stringify(gps)}`);
|
|
||||||
await db.put({ _deleted: true, ...gps });
|
|
||||||
console.log(`done, id: ${gps}`);
|
|
||||||
const currentTrkpts = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'trkpt',
|
|
||||||
gpx: gps._id,
|
|
||||||
},
|
|
||||||
fields: ['_id', '_rev'],
|
|
||||||
});
|
|
||||||
console.log(`deleteGps - db.find(trkpts) : ${JSON.stringify(currentTrkpts)}`);
|
|
||||||
const trkpts: { _id: string; _rev: string }[] = currentTrkpts.docs;
|
|
||||||
for (let j = 0; j < trkpts.length; j++) {
|
|
||||||
await db.put({ _deleted: true, ...trkpts[j] });
|
|
||||||
}
|
|
||||||
await await db.compact();
|
|
||||||
await db.viewCleanup();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteCurrent = async (db: any) => {
|
|
||||||
const currents = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
subtype: 'current',
|
|
||||||
},
|
|
||||||
fields: ['_id', '_rev'],
|
|
||||||
});
|
|
||||||
console.log(`deleteCurrent - db.find(gpx) : ${JSON.stringify(currents)}`);
|
|
||||||
const docs: { _id: string; _rev: string }[] = currents.docs;
|
|
||||||
for (let i = 0; i < docs.length; i++) {
|
|
||||||
console.log(`Deleting document ${JSON.stringify(docs[i])}`);
|
|
||||||
await deleteGps(db, docs[i]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGpx = async (db: any, gpxId: string) => {
|
|
||||||
console.log(`getGpx(db, "${gpxId}")`);
|
|
||||||
var gpxResult = await db.get(gpxId);
|
|
||||||
var gpx = gpxResult.gpx;
|
|
||||||
var trkptResults = await db.find({
|
|
||||||
selector: {
|
|
||||||
type: 'trkpt',
|
|
||||||
gpx: gpxId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
var trkpts = trkptResults.docs;
|
|
||||||
trkpts.sort((first: any, second: any) =>
|
|
||||||
first.trkpt.time.localeCompare(second.trkpt.time)
|
|
||||||
);
|
|
||||||
trkpts.map((trkptDoc: any) => {
|
|
||||||
gpx.trk[0].trkseg[0].trkpt.push(trkptDoc.trkpt);
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
return gpx;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGpxAsXmlString = async (db: any, gpxId: string) => {
|
|
||||||
const gpx = new GPX(await getGpx(db, gpxId));
|
|
||||||
return gpx.toString({});
|
|
||||||
};
|
|
107
src/db/index.ts
107
src/db/index.ts
|
@ -1,107 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import uri from '../lib/ids';
|
|
||||||
|
|
||||||
const dbDefinitionId = uri('dbdef', {});
|
|
||||||
|
|
||||||
const currentDbDefinition = {
|
|
||||||
_id: dbDefinitionId,
|
|
||||||
type: dbDefinitionId,
|
|
||||||
def: { version: '0.000001' },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initDb = async (db: any, setDbReady: any) => {
|
|
||||||
var previousDbDefinition = {
|
|
||||||
_id: dbDefinitionId,
|
|
||||||
type: dbDefinitionId,
|
|
||||||
def: { version: '0' },
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
previousDbDefinition = await db.get(dbDefinitionId);
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.status !== 404) {
|
|
||||||
console.log(
|
|
||||||
`Unexpected error fetching db definition: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousDbDefinition.def.version < currentDbDefinition.def.version) {
|
|
||||||
previousDbDefinition.def = currentDbDefinition.def;
|
|
||||||
db.put(previousDbDefinition);
|
|
||||||
// TODO: support migrations
|
|
||||||
}
|
|
||||||
await await db.compact();
|
|
||||||
await db.viewCleanup();
|
|
||||||
|
|
||||||
// WARNING: defs must use the canonical form and be identical to what will be returned by db.getIndexes
|
|
||||||
const requiredIndexes: any = [
|
|
||||||
{
|
|
||||||
name: 'type-subtype',
|
|
||||||
def: {
|
|
||||||
fields: [{ type: 'asc' }, { subtype: 'asc' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'type-trkpt-gpx-time',
|
|
||||||
def: {
|
|
||||||
fields: [{ type: 'asc' }, { gpx: 'asc' }, { 'trkpt.time': 'asc' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const existingIndexes = (await db.getIndexes()).indexes;
|
|
||||||
|
|
||||||
const pruneIndex = ({ name, def }: any) => ({ name, def });
|
|
||||||
|
|
||||||
const isSameIndex = (idx1: any, idx2: any) => {
|
|
||||||
return _.isEqual(pruneIndex(idx1), pruneIndex(idx2));
|
|
||||||
};
|
|
||||||
|
|
||||||
const findIndex = (targetIndexes: any, index: any) =>
|
|
||||||
targetIndexes.find((targetIndex: any) => {
|
|
||||||
return isSameIndex(targetIndex, index);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (var index of existingIndexes) {
|
|
||||||
if (index.type === 'json') {
|
|
||||||
// Non system indexes
|
|
||||||
// console.log(`Checking existing index :${JSON.stringify(index)}`);
|
|
||||||
if (!findIndex(requiredIndexes, index)) {
|
|
||||||
// console.log(`db.deleteIndex(${JSON.stringify(index)})`);
|
|
||||||
await db.deleteIndex(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (index of requiredIndexes) {
|
|
||||||
if (!findIndex(existingIndexes, index)) {
|
|
||||||
// console.log(`db.createIndex(${JSON.stringify(index)})`);
|
|
||||||
await db.createIndex({ name: index.name, ...index.def });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDbReady(true);
|
|
||||||
|
|
||||||
/* const indexes = await db.getIndexes();
|
|
||||||
console.log(`indexes: ${JSON.stringify(indexes)}`);
|
|
||||||
|
|
||||||
const explain1 = await db.explain({
|
|
||||||
selector: {
|
|
||||||
type: 'trkpt',
|
|
||||||
gpx: 'xxxx',
|
|
||||||
},
|
|
||||||
// sort: ['trkpt.time'],
|
|
||||||
// use_index: 'type-trkpt-gpx-time',
|
|
||||||
});
|
|
||||||
console.log(`explain1: ${JSON.stringify(explain1)}`);
|
|
||||||
const explain2 = await db.explain({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
},
|
|
||||||
// sort: ['trkpt.time'],
|
|
||||||
// use_index: 'type-trkpt-gpx-time',
|
|
||||||
});
|
|
||||||
console.log(`explain2: ${JSON.stringify(explain2)}`);
|
|
||||||
*/
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'react-pouchdb';
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,147 +0,0 @@
|
||||||
import LocalizedStrings from 'react-localization';
|
|
||||||
|
|
||||||
const strings = new LocalizedStrings({
|
|
||||||
en: {
|
|
||||||
colonize: (input: string): any => strings.formatString('{0}:', input),
|
|
||||||
|
|
||||||
save: 'Save',
|
|
||||||
cancel: 'Cancel',
|
|
||||||
close: 'Close',
|
|
||||||
settings: 'Settings',
|
|
||||||
language: 'Language',
|
|
||||||
user: 'User',
|
|
||||||
pseudo: 'Pseudo',
|
|
||||||
geolocation: 'Geolocation',
|
|
||||||
minTimeInt: 'Minimal time interval (ms)',
|
|
||||||
minDist: 'Minimal distance (m)',
|
|
||||||
|
|
||||||
trackRecording: 'Track recording',
|
|
||||||
resumeRecording: 'Resume recording',
|
|
||||||
startRecording: 'Start recording',
|
|
||||||
pauseRecording: 'Pause',
|
|
||||||
// stopRecording: 'stop',
|
|
||||||
// cancelRecording: 'cancel',
|
|
||||||
stopRecording: (
|
|
||||||
<>
|
|
||||||
Stop recording
|
|
||||||
<br />
|
|
||||||
(and save track)
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cancelRecording: (
|
|
||||||
<>
|
|
||||||
Cancel recording
|
|
||||||
<br />
|
|
||||||
(and clear track)
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
|
|
||||||
chooseYourMap: 'Choose your map',
|
|
||||||
|
|
||||||
languageSelect: {
|
|
||||||
placeHolder: 'Select your language',
|
|
||||||
choices: [
|
|
||||||
{ value: 'auto', label: 'Automatic' },
|
|
||||||
{ value: 'en', label: 'English' },
|
|
||||||
{ value: 'fr', label: 'Français' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
tracks: 'Tracks',
|
|
||||||
|
|
||||||
locationInfo: {
|
|
||||||
title: 'Here',
|
|
||||||
location: 'Location',
|
|
||||||
lat: 'Latitude: ',
|
|
||||||
lon: 'Longitude: ',
|
|
||||||
elevation: 'Elevation:',
|
|
||||||
address: 'Address',
|
|
||||||
display_name: 'Full address:',
|
|
||||||
notes: 'Notes',
|
|
||||||
at: (distance: number) => `At ${Math.round(distance)}m...`,
|
|
||||||
created: 'Created on ',
|
|
||||||
status: {
|
|
||||||
opened: 'opened',
|
|
||||||
closed: 'closed',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
colonize: (input: string): any => strings.formatString('{0} :', input),
|
|
||||||
|
|
||||||
save: 'Sauvegarder',
|
|
||||||
cancel: 'Annuler',
|
|
||||||
close: 'Fermer',
|
|
||||||
settings: 'Paramètres',
|
|
||||||
language: 'Langue',
|
|
||||||
user: 'Utilisateur',
|
|
||||||
pseudo: 'Pseudo',
|
|
||||||
geolocation: 'Géolocalisation',
|
|
||||||
minTimeInt: 'Interval minimal (ms)',
|
|
||||||
minDist: 'Distance minimale (m)',
|
|
||||||
|
|
||||||
trackRecording: 'Enregistrement de trace',
|
|
||||||
resumeRecording: (
|
|
||||||
<>
|
|
||||||
Reprendre
|
|
||||||
<br />
|
|
||||||
l'enregistrement
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
startRecording: (
|
|
||||||
<>
|
|
||||||
Démarre
|
|
||||||
<br />
|
|
||||||
l'enregistrement
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
pauseRecording: 'Pause',
|
|
||||||
stopRecording: (
|
|
||||||
<>
|
|
||||||
Arrêter
|
|
||||||
<br />
|
|
||||||
(et sauvegarder)
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
cancelRecording: (
|
|
||||||
<>
|
|
||||||
Annuler
|
|
||||||
<br />
|
|
||||||
(et effacer)
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
|
|
||||||
chooseYourMap: 'Choisissez votre carte',
|
|
||||||
|
|
||||||
languageSelect: {
|
|
||||||
placeHolder: 'Selectionner votre langue',
|
|
||||||
choices: [
|
|
||||||
{ value: 'auto', label: 'Automatique' },
|
|
||||||
{ value: 'fr', label: 'Français' },
|
|
||||||
{ value: 'en', label: 'English' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
tracks: 'Traces',
|
|
||||||
|
|
||||||
locationInfo: {
|
|
||||||
title: 'Ici',
|
|
||||||
location: 'Position',
|
|
||||||
lat: 'Latitude : ',
|
|
||||||
lon: 'Longitude : ',
|
|
||||||
elevation: 'Altitude :',
|
|
||||||
address: 'Addresse',
|
|
||||||
display_name: 'Adresse complète :',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default strings;
|
|
||||||
|
|
||||||
export const setI18nLanguage = (language: string) => {
|
|
||||||
if (language === undefined || language === 'auto') {
|
|
||||||
strings.setLanguage(strings.getInterfaceLanguage());
|
|
||||||
} else {
|
|
||||||
strings.setLanguage(language);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,50 +0,0 @@
|
||||||
import LocalizedStrings from 'react-localization';
|
|
||||||
import { stopRecursion } from 'localized-strings/lib/StopRecursion';
|
|
||||||
|
|
||||||
const strings = new LocalizedStrings({
|
|
||||||
en: {
|
|
||||||
internationalTileProviders: {
|
|
||||||
osm: {
|
|
||||||
name: 'Open Street Map',
|
|
||||||
},
|
|
||||||
otm: {
|
|
||||||
name: 'Open Topo Map',
|
|
||||||
},
|
|
||||||
CyclOSM: {
|
|
||||||
name: 'CyclOSM',
|
|
||||||
title: 'CyclOSM: OpenStreetMap-based bicycle map',
|
|
||||||
description: (
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
<a href='https://github.com/cyclosm/cyclosm-cartocss-style/'>
|
|
||||||
CyclOSM
|
|
||||||
</a>{' '}
|
|
||||||
is a bicycle-oriented map built on top of{' '}
|
|
||||||
<a href='https://www.openstreetmap.org/'>OpenStreetMap</a> data.
|
|
||||||
It aims at providing a beautiful and practical map for cyclists,
|
|
||||||
no matter their cycling habits or abilities.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
In urban areas, it renders the main different types of cycle
|
|
||||||
tracks and lanes, on each side of the road, for helping you draw
|
|
||||||
your bike to work route. It also features essential POIs as well
|
|
||||||
as bicycle parking spots or spots shared with motorbikes, specific
|
|
||||||
infrastructure (elevators / ramps), road speeds or surfaces to
|
|
||||||
avoid streets with pavings, bumpers and bike boxes, etc.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
The same map also lets you visualize main bicycle touring routes
|
|
||||||
as well as essential POIs when touring (emergency services,
|
|
||||||
shelters, tourism, shops).
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
localizedTileProviders: stopRecursion({}),
|
|
||||||
},
|
|
||||||
|
|
||||||
fr: {},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default strings;
|
|
|
@ -1,6 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
import * as serviceWorkerRegistration from './serviceWorkerRegistration';
|
||||||
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container!);
|
const root = createRoot(container!);
|
||||||
|
@ -10,4 +12,12 @@ root.render(
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If you want your app to work offline and load faster, you can change
|
||||||
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
// Learn more about service workers: https://cra.link/PWA
|
||||||
|
serviceWorkerRegistration.unregister();
|
||||||
|
|
||||||
|
// If you want to start measuring performance in your app, pass a function
|
||||||
|
// to log results (for example: reportWebVitals(console.log))
|
||||||
|
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
|
||||||
|
reportWebVitals();
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { createAnimation } from '@ionic/react';
|
|
||||||
|
|
||||||
export const enterAnimation = (baseEl: HTMLElement) => {
|
|
||||||
const root = baseEl.shadowRoot;
|
|
||||||
|
|
||||||
const backdropAnimation = createAnimation()
|
|
||||||
.addElement(root?.querySelector('ion-backdrop')!)
|
|
||||||
.fromTo('opacity', '0.01', 'var(--backdrop-opacity)');
|
|
||||||
|
|
||||||
const wrapperAnimation = createAnimation()
|
|
||||||
.addElement(root?.querySelector('.modal-wrapper')!)
|
|
||||||
.keyframes([
|
|
||||||
{ offset: 0, opacity: '0', transform: 'scale(0)' },
|
|
||||||
{ offset: 1, opacity: '0.99', transform: 'scale(1)' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
return createAnimation()
|
|
||||||
.addElement(baseEl)
|
|
||||||
.easing('ease-out')
|
|
||||||
.duration(500)
|
|
||||||
.addAnimation([backdropAnimation, wrapperAnimation]);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const leaveAnimation = (baseEl: HTMLElement) => {
|
|
||||||
return enterAnimation(baseEl).direction('reverse');
|
|
||||||
};
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { BackgroundGeolocationPlugin } from '@capacitor-community/background-geolocation';
|
|
||||||
import { registerPlugin } from '@capacitor/core';
|
|
||||||
|
|
||||||
const BackgroundGeolocation = registerPlugin<BackgroundGeolocationPlugin>(
|
|
||||||
'BackgroundGeolocation'
|
|
||||||
);
|
|
||||||
|
|
||||||
const backgroundGeolocationConfig = {
|
|
||||||
// If the "backgroundMessage" option is defined, the watcher will
|
|
||||||
// provide location updates whether the app is in the background or the
|
|
||||||
// foreground. If it is not defined, location updates are only
|
|
||||||
// guaranteed in the foreground. This is true on both platforms.
|
|
||||||
// On Android, a notification must be shown to continue receiving
|
|
||||||
// location updates in the background. This option specifies the text of
|
|
||||||
// that notification.
|
|
||||||
backgroundMessage: 'Cancel to prevent battery drain.',
|
|
||||||
|
|
||||||
// The title of the notification mentioned above. Defaults to "Using
|
|
||||||
// your location".
|
|
||||||
backgroundTitle: 'Tracking You.',
|
|
||||||
|
|
||||||
// Whether permissions should be requested from the user automatically,
|
|
||||||
// if they are not already granted. Defaults to "true".
|
|
||||||
requestPermissions: true,
|
|
||||||
|
|
||||||
// If "true", stale locations may be delivered while the device
|
|
||||||
// obtains a GPS fix. You are responsible for checking the "time"
|
|
||||||
// property. If "false", locations are guaranteed to be up to date.
|
|
||||||
// Defaults to "false".
|
|
||||||
stale: false,
|
|
||||||
|
|
||||||
// The minimum number of metres between subsequent locations. Defaults
|
|
||||||
// to 0.
|
|
||||||
distanceFilter: 10,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const startBackgroundGeolocation = async (
|
|
||||||
newLocationHandler: any,
|
|
||||||
distanceFilter: number
|
|
||||||
) => {
|
|
||||||
backgroundGeolocationConfig.distanceFilter = distanceFilter;
|
|
||||||
const locationHandler = (location: any, error: any) => {
|
|
||||||
console.log('com.dyomedea.dyomedea LOG', ' - Callback');
|
|
||||||
if (error) {
|
|
||||||
if (error.code === 'NOT_AUTHORIZED') {
|
|
||||||
if (
|
|
||||||
window.confirm(
|
|
||||||
'This app needs your location, ' +
|
|
||||||
'but does not have permission.\n\n' +
|
|
||||||
'Open settings now?'
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// It can be useful to direct the user to their device's
|
|
||||||
// settings when location permissions have been denied. The
|
|
||||||
// plugin provides the 'openSettings' method to do exactly
|
|
||||||
// this.
|
|
||||||
BackgroundGeolocation.openSettings();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return console.error('com.dyomedea.dyomedea LOG', ' - error: ', error);
|
|
||||||
}
|
|
||||||
console.log(location);
|
|
||||||
if (location !== undefined) {
|
|
||||||
newLocationHandler(location);
|
|
||||||
}
|
|
||||||
|
|
||||||
return console.log('com.dyomedea.dyomedea LOG', ' - location: ', location);
|
|
||||||
};
|
|
||||||
|
|
||||||
var watcher_id;
|
|
||||||
|
|
||||||
console.log('com.dyomedea.dyomedea LOG', ' - Adding the watcher');
|
|
||||||
await BackgroundGeolocation.addWatcher(
|
|
||||||
backgroundGeolocationConfig,
|
|
||||||
locationHandler
|
|
||||||
)
|
|
||||||
.then(function after_the_watcher_has_been_added(id) {
|
|
||||||
// When a watcher is no longer needed, it should be removed by calling
|
|
||||||
// 'removeWatcher' with an object containing its ID.
|
|
||||||
console.log('com.dyomedea.dyomedea LOG', ' - Watcher added');
|
|
||||||
watcher_id = id;
|
|
||||||
/*BackgroundGeolocation.removeWatcher({
|
|
||||||
id: watcher_id,
|
|
||||||
}); */
|
|
||||||
})
|
|
||||||
.catch((reason) => {
|
|
||||||
console.error('com.dyomedea.dyomedea LOG', ' - reason: ', reason);
|
|
||||||
});
|
|
||||||
return watcher_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const stopBackgroundGeolocation = (watcher_id: any) => {
|
|
||||||
BackgroundGeolocation.removeWatcher({
|
|
||||||
id: watcher_id,
|
|
||||||
});
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'docuri';
|
|
|
@ -1,35 +0,0 @@
|
||||||
export interface Point {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface geoPoint {
|
|
||||||
lon: number;
|
|
||||||
lat: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// cf https://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#ECMAScript_(JavaScript/ActionScript,_etc.)
|
|
||||||
export const lon2tile = (lon: number, zoom: number) => {
|
|
||||||
return ((Number(lon) + 180) / 360) * Math.pow(2, zoom);
|
|
||||||
};
|
|
||||||
export const lat2tile = (lat: number, zoom: number) => {
|
|
||||||
return (
|
|
||||||
((1 -
|
|
||||||
Math.log(
|
|
||||||
Math.tan((Number(lat) * Math.PI) / 180) +
|
|
||||||
1 / Math.cos((Number(lat) * Math.PI) / 180)
|
|
||||||
) /
|
|
||||||
Math.PI) /
|
|
||||||
2) *
|
|
||||||
Math.pow(2, zoom)
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export function tile2long(x: number, z: number) {
|
|
||||||
return (x / Math.pow(2, z)) * 360 - 180;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tile2lat(y: number, z: number) {
|
|
||||||
var n = Math.PI - (2 * Math.PI * y) / Math.pow(2, z);
|
|
||||||
return (180 / Math.PI) * Math.atan(0.5 * (Math.exp(n) - Math.exp(-n)));
|
|
||||||
}
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017 Zheng-Xiang Ke
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,91 +0,0 @@
|
||||||
# gpx-parser-builder
|
|
||||||
A simple gpx parser and builder between GPX string and JavaScript object. It is dependent on [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
|
|
||||||
|
|
||||||
[![npm](https://img.shields.io/npm/dt/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
|
|
||||||
[![GitHub stars](https://img.shields.io/github/stars/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/stargazers)
|
|
||||||
[![GitHub forks](https://img.shields.io/github/forks/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/network)
|
|
||||||
[![npm](https://img.shields.io/npm/v/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
|
|
||||||
[![GitHub license](https://img.shields.io/github/license/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/blob/master/LICENSE)
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
gpx-parser-builder is written with ECMAScript 6. You can leverage [Babel](https://babeljs.io/) and [Webpack](https://webpack.js.org/) to make all browsers available.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install gpx-parser-builder --save
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version
|
|
||||||
|
|
||||||
v1.0.0+ is a breaking change for v0.2.2-. v1.0.0+ fully supports gpx files including waypoints, routes, and tracks. Every gpx type is 1-1 corresponding to a JavaScript class.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import GPX from 'gpx-parser-builder';
|
|
||||||
|
|
||||||
// Parse gpx
|
|
||||||
const gpx = GPX.parse('GPX_STRING');
|
|
||||||
|
|
||||||
window.console.dir(gpx.metadata);
|
|
||||||
window.console.dir(gpx.wpt);
|
|
||||||
window.console.dir(gpx.trk);
|
|
||||||
|
|
||||||
// Build gpx
|
|
||||||
window.console.log(gpx.toString());
|
|
||||||
```
|
|
||||||
|
|
||||||
Get more details about usage with the unit tests.
|
|
||||||
|
|
||||||
### GPX
|
|
||||||
|
|
||||||
The GPX JavaScript object.
|
|
||||||
|
|
||||||
`constructor(object)`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const gpx = new Gpx({$:{...}, metadat: {...}, wpt:[{...},{...}]}, trk: {...}, rte: {...})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Member Variables
|
|
||||||
|
|
||||||
`$` the attributes for the gpx element. Default value:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
'version': '1.1',
|
|
||||||
'creator': 'gpx-parser-builder',
|
|
||||||
'xmlns': 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
||||||
'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`metadata` the metadata for the gpx.
|
|
||||||
|
|
||||||
`wpt` array of waypoints. It is corresponded to `<wpt>`. The type of all elements in `wpt` is `Waypoint`;
|
|
||||||
|
|
||||||
`rte` array of routes. It is corresponded to `<rte>`. The type of all elements in `rte` is `Route`;
|
|
||||||
|
|
||||||
`trk` array of tracks. It is corresponded to `<trk>`. The type of all elements in `trk` is `Track`;
|
|
||||||
|
|
||||||
#### Static Methods
|
|
||||||
|
|
||||||
`parse(gpxString)` parse gpx string to Gpx object. return `null` if parsing failed.
|
|
||||||
|
|
||||||
#### Member Methods
|
|
||||||
|
|
||||||
`toString(options)` GPX object to gpx string. The options is for [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
|
|
||||||
|
|
||||||
## Save as GPX file in the frontend
|
|
||||||
|
|
||||||
You can leverage [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) or [FileSaver.js](https://github.com/eligrey/FileSaver.js) to save as GPX file. ⚠️Not all borwsers support the above file techniques. ⚠️️️
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Zheng-Xiang Ke, kf99916@gmail.com
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
gpx-parser-builder is available under the MIT license. See the LICENSE file for more info.
|
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"name": "gpx-parser-builder",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"description": "A simple gpx parser and builder between GPX string and JavaScript object",
|
|
||||||
"main": "./src/gpx.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "mocha --require @babel/register test/**/*.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/kf99916/gpx-parser-builder.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"gpx",
|
|
||||||
"parser",
|
|
||||||
"builder"
|
|
||||||
],
|
|
||||||
"author": "Zheng-Xiang Ke",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/kf99916/gpx-parser-builder/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/kf99916/gpx-parser-builder",
|
|
||||||
"files": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "~7.7",
|
|
||||||
"@babel/preset-env": "~7.7",
|
|
||||||
"@babel/register": "~7.7",
|
|
||||||
"mocha": "~6.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"isomorphic-xml2js": "~0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default class Bounds {
|
|
||||||
constructor(object) {
|
|
||||||
this.minlat = object.minlat;
|
|
||||||
this.minlon = object.minlon;
|
|
||||||
this.maxlat = object.maxlat;
|
|
||||||
this.maxlon = object.maxlon;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export default class Copyright {
|
|
||||||
constructor(object) {
|
|
||||||
this.author = object.author;
|
|
||||||
this.year = object.year;
|
|
||||||
this.license = object.license;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
declare module 'gpx-parser-builder' {
|
|
||||||
class GPX {
|
|
||||||
static parse(gpxString: any): any;
|
|
||||||
constructor(object: any);
|
|
||||||
$: any;
|
|
||||||
extensions: any;
|
|
||||||
metadata: any;
|
|
||||||
wpt: any;
|
|
||||||
rte: any;
|
|
||||||
trk: any;
|
|
||||||
toString(options: any): string;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import * as xml2js from 'isomorphic-xml2js';
|
|
||||||
import Metadata from './metadata';
|
|
||||||
import Waypoint from './waypoint';
|
|
||||||
import Route from './route';
|
|
||||||
import Track from './track';
|
|
||||||
import {removeEmpty, allDatesToISOString} from './utils';
|
|
||||||
|
|
||||||
const defaultAttributes = {
|
|
||||||
version: '1.1',
|
|
||||||
creator: 'gpx-parser-builder',
|
|
||||||
xmlns: 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
||||||
'xsi:schemaLocation':
|
|
||||||
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GPX {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = Object.assign({}, defaultAttributes, object.$ || object.attributes || {});
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
|
|
||||||
if (object.metadata) {
|
|
||||||
this.metadata = new Metadata(object.metadata);
|
|
||||||
}
|
|
||||||
if (object.wpt) {
|
|
||||||
if (!Array.isArray(object.wpt)) {
|
|
||||||
object.wpt = [object.wpt];
|
|
||||||
}
|
|
||||||
this.wpt = object.wpt.map(wpt => new Waypoint(wpt));
|
|
||||||
}
|
|
||||||
if (object.rte) {
|
|
||||||
if (!Array.isArray(object.rte)) {
|
|
||||||
object.rte = [object.rte];
|
|
||||||
}
|
|
||||||
this.rte = object.rte.map(rte => new Route(rte));
|
|
||||||
}
|
|
||||||
if (object.trk) {
|
|
||||||
if (!Array.isArray(object.trk)) {
|
|
||||||
object.trk = [object.trk];
|
|
||||||
}
|
|
||||||
this.trk = object.trk.map(trk => new Track(trk));
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEmpty(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(gpxString) {
|
|
||||||
let gpx;
|
|
||||||
xml2js.parseString(gpxString, {
|
|
||||||
explicitArray: false
|
|
||||||
}, (err, xml) => {
|
|
||||||
if (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!xml.gpx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpx = new GPX({
|
|
||||||
attributes: xml.gpx.$,
|
|
||||||
metadata: xml.gpx.metadata,
|
|
||||||
wpt: xml.gpx.wpt,
|
|
||||||
rte: xml.gpx.rte,
|
|
||||||
trk: xml.gpx.trk
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return gpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(options) {
|
|
||||||
options = options || {};
|
|
||||||
options.rootName = 'gpx';
|
|
||||||
|
|
||||||
const builder = new xml2js.Builder(options), gpx = new GPX(this);
|
|
||||||
allDatesToISOString(gpx);
|
|
||||||
return builder.buildObject(gpx);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default class Link {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = {};
|
|
||||||
this.$.href = object.$.href || object.href;
|
|
||||||
this.text = object.text;
|
|
||||||
this.type = object.type;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import Copyright from './copyright';
|
|
||||||
import Link from './link';
|
|
||||||
import Person from './person';
|
|
||||||
import Bounds from './bounds';
|
|
||||||
|
|
||||||
export default class Metadata {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.time = object.time ? new Date(object.time) : new Date();
|
|
||||||
this.keywords = object.keywords;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.author) {
|
|
||||||
this.author = new Person(object.author);
|
|
||||||
}
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
if (object.bounds) {
|
|
||||||
this.bounds = new Bounds(object.bounds);
|
|
||||||
}
|
|
||||||
if (object.copyright) {
|
|
||||||
this.copyright = new Copyright(object.copyright);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Person {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.email = object.email;
|
|
||||||
if (object.link) {
|
|
||||||
this.link = new Link(object.link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import Waypoint from './waypoint';
|
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Route {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.number = object.number;
|
|
||||||
this.type = object.type;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
this.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.rtept) {
|
|
||||||
if (!Array.isArray(object.rtept)) {
|
|
||||||
this.rtept = [object.rtept];
|
|
||||||
}
|
|
||||||
this.rtept = object.rtept.map(pt => new Waypoint(pt));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import Waypoint from './waypoint';
|
|
||||||
|
|
||||||
export default class TrackSegment {
|
|
||||||
constructor(object) {
|
|
||||||
if (object.trkpt) {
|
|
||||||
if (!Array.isArray(object.trkpt)) {
|
|
||||||
object.trkpt = [object.trkpt];
|
|
||||||
}
|
|
||||||
this.trkpt = object.trkpt.map(pt => new Waypoint(pt));
|
|
||||||
}
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import TrackSegment from './track-segment';
|
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Track {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.number = object.number;
|
|
||||||
this.type = object.type;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
if (object.trkseg) {
|
|
||||||
if (!Array.isArray(object.trkseg)) {
|
|
||||||
object.trkseg = [object.trkseg];
|
|
||||||
}
|
|
||||||
this.trkseg = object.trkseg.map(seg => new TrackSegment(seg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
function removeEmpty(obj) {
|
|
||||||
Object.entries(obj).forEach(([key, val]) => {
|
|
||||||
if (val && val instanceof Object) {
|
|
||||||
removeEmpty(val);
|
|
||||||
} else if (val == null) {
|
|
||||||
delete obj[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function allDatesToISOString(obj) {
|
|
||||||
Object.entries(obj).forEach(([key, val]) => {
|
|
||||||
if (val) {
|
|
||||||
if (val instanceof Date) {
|
|
||||||
obj[key] = val.toISOString().split('.')[0] + 'Z';
|
|
||||||
} else if (val instanceof Object) {
|
|
||||||
allDatesToISOString(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { removeEmpty, allDatesToISOString };
|
|
|
@ -1,32 +0,0 @@
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Waypoint {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = {};
|
|
||||||
this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1;
|
|
||||||
this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1;
|
|
||||||
this.ele = object.ele;
|
|
||||||
this.time = object.time ? new Date(object.time) : new Date();
|
|
||||||
this.magvar = object.magvar;
|
|
||||||
this.geoidheight = object.geoidheight;
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.sym = object.sym;
|
|
||||||
this.type = object.type;
|
|
||||||
this.sat = object.sat;
|
|
||||||
this.hdop = object.hdop;
|
|
||||||
this.vdop = object.vdop;
|
|
||||||
this.pdop = object.pdop;
|
|
||||||
this.ageofdgpsdata = object.ageofdgpsdata;
|
|
||||||
this.dgpsid = object.dgpsid;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,116 +0,0 @@
|
||||||
import { route } from 'docuri';
|
|
||||||
import uri from './ids';
|
|
||||||
|
|
||||||
describe('Checking some DocURI features', () => {
|
|
||||||
test(', basic route', () => {
|
|
||||||
const gpx = route('gpx/:id');
|
|
||||||
expect(gpx({ id: 10 })).toBe('gpx/10');
|
|
||||||
});
|
|
||||||
test(', basic route (vice-versa', () => {
|
|
||||||
const gpx = route('gpx/:id');
|
|
||||||
expect(gpx('gpx/10')).toMatchObject({ id: '10' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking a multilevel route', () => {
|
|
||||||
test(', using the two levels', () => {
|
|
||||||
const gpx = route('gpx/:gpx/trk/:trk');
|
|
||||||
expect(gpx({ gpx: 10, trk: 0 })).toBe('gpx/10/trk/0');
|
|
||||||
});
|
|
||||||
test(', using the two levels (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx/trk/:trk');
|
|
||||||
expect(gpx('gpx/10/trk/0')).toMatchObject({ gpx: '10', trk: '0' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking a multilevel route with optional part', () => {
|
|
||||||
test(', using the two levels', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/trk/:trk)');
|
|
||||||
expect(gpx({ gpx: 10, trk: 0 })).toBe('gpx/10/trk/0');
|
|
||||||
});
|
|
||||||
test(', using the two levels (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/trk/:trk)');
|
|
||||||
expect(gpx('gpx/10/trk/0')).toMatchObject({ gpx: '10', trk: '0' });
|
|
||||||
});
|
|
||||||
test(', using only one level', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/trk/:trk)');
|
|
||||||
expect(gpx({ gpx: 10 })).toBe('gpx/10/trk/'); //Unfortunately !
|
|
||||||
});
|
|
||||||
test(', using only one level (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/trk/:trk)');
|
|
||||||
expect(gpx('gpx/10')).toMatchObject({ gpx: '10' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking gpx ids', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const gpx = uri('gpx', { gpx: 'id' });
|
|
||||||
expect(gpx).toBe('gpx/id');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const gpx = uri('gpx', 'gpx/id');
|
|
||||||
expect(gpx).toMatchObject({ gpx: 'id' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trk ids', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trk', { gpx: 'gpxid', trk: 'trkid' });
|
|
||||||
expect(rte).toBe('gpx/gpxid/trk/trkid');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trk', 'gpx/gpxid/trk/trkid');
|
|
||||||
expect(rte).toMatchObject({ gpx: 'gpxid', trk: 'trkid' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trkseg ids', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trkseg', {
|
|
||||||
gpx: 'gpxid',
|
|
||||||
trk: 'trkid',
|
|
||||||
trkseg: 'trksegid',
|
|
||||||
});
|
|
||||||
expect(rte).toBe('gpx/gpxid/trk/trkid/trksegid');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trkseg', 'gpx/gpxid/trk/trkid/trksegid');
|
|
||||||
expect(rte).toMatchObject({
|
|
||||||
gpx: 'gpxid',
|
|
||||||
trk: 'trkid',
|
|
||||||
trkseg: 'trksegid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trkpt ids', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trkpt', {
|
|
||||||
gpx: 'gpxid',
|
|
||||||
trk: 'trkid',
|
|
||||||
trkseg: 'trksegid',
|
|
||||||
trkpt: 'trkptid',
|
|
||||||
});
|
|
||||||
expect(rte).toBe('gpx/gpxid/trk/trkid/trksegid/trkptid');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trkpt', 'gpx/gpxid/trk/trkid/trksegid/trkptid');
|
|
||||||
expect(rte).toMatchObject({
|
|
||||||
gpx: 'gpxid',
|
|
||||||
trk: 'trkid',
|
|
||||||
trkseg: 'trksegid',
|
|
||||||
trkpt: 'trkptid',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking settings id', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('settings', {});
|
|
||||||
expect(rte).toBe('settings');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('settings', 'settings');
|
|
||||||
expect(rte).toMatchObject({});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,21 +0,0 @@
|
||||||
import { route } from 'docuri';
|
|
||||||
|
|
||||||
const routes = {
|
|
||||||
dbdef: route('dbdef'),
|
|
||||||
settings: route('settings'),
|
|
||||||
gpx: route('gpx/:gpx'),
|
|
||||||
trk: route('gpx/:gpx/trk/:trk'),
|
|
||||||
trkseg: route('gpx/:gpx/trk/:trk/:trkseg'),
|
|
||||||
trkpt: route('gpx/:gpx/trk/:trk/:trkseg/:trkpt'),
|
|
||||||
wpt: route('gpx/:gpx/wpt/:wpt'),
|
|
||||||
rte: route('gpx/:gpx/rte/:rte'),
|
|
||||||
rtept: route('gpx/:gpx/rte/:rte/:rtept'),
|
|
||||||
};
|
|
||||||
|
|
||||||
type RouteKey = keyof typeof routes;
|
|
||||||
|
|
||||||
const uri = (type: RouteKey, param: any) => {
|
|
||||||
return routes[type](param);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default uri;
|
|
|
@ -1,25 +0,0 @@
|
||||||
/**
|
|
||||||
*
|
|
||||||
* See https://www.pluralsight.com/guides/how-to-communicate-between-independent-components-in-reactjs (and many similar pages)
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const eventBus = {
|
|
||||||
on(event: string, callback: any) {
|
|
||||||
// ...
|
|
||||||
document.addEventListener(event, (e) => callback(e));
|
|
||||||
},
|
|
||||||
|
|
||||||
dispatch(event: string, data: any) {
|
|
||||||
// ...
|
|
||||||
document.dispatchEvent(new CustomEvent(event, { detail: data }));
|
|
||||||
},
|
|
||||||
|
|
||||||
remove(event: string, callback: (e: Event) => void) {
|
|
||||||
// ...
|
|
||||||
document.removeEventListener(event, callback);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default eventBus;
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||||
|
import ExploreContainer from '../components/ExploreContainer';
|
||||||
|
import './Home.css';
|
||||||
|
|
||||||
|
const Home: React.FC = () => {
|
||||||
|
return (
|
||||||
|
<IonPage>
|
||||||
|
<IonHeader>
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle>Blank</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<IonContent fullscreen>
|
||||||
|
<IonHeader collapse="condense">
|
||||||
|
<IonToolbar>
|
||||||
|
<IonTitle size="large">Blank</IonTitle>
|
||||||
|
</IonToolbar>
|
||||||
|
</IonHeader>
|
||||||
|
<ExploreContainer />
|
||||||
|
</IonContent>
|
||||||
|
</IonPage>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { ReportHandler } from 'web-vitals';
|
||||||
|
|
||||||
|
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
|
||||||
|
if (onPerfEntry && onPerfEntry instanceof Function) {
|
||||||
|
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
|
||||||
|
getCLS(onPerfEntry);
|
||||||
|
getFID(onPerfEntry);
|
||||||
|
getFCP(onPerfEntry);
|
||||||
|
getLCP(onPerfEntry);
|
||||||
|
getTTFB(onPerfEntry);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default reportWebVitals;
|
|
@ -1,10 +0,0 @@
|
||||||
import { configureStore } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import mapReducer from './map';
|
|
||||||
import settingsReducer from './settings';
|
|
||||||
|
|
||||||
const store = configureStore({
|
|
||||||
reducer: { map: mapReducer, settings: settingsReducer },
|
|
||||||
});
|
|
||||||
|
|
||||||
export default store;
|
|
|
@ -1,69 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import { initialMapState, reevaluateState } from './map';
|
|
||||||
|
|
||||||
//interface CustomMatchers<R = unknown> {
|
|
||||||
// isDeepEqual(received: any, target: any): R;
|
|
||||||
//}
|
|
||||||
|
|
||||||
//declare global {
|
|
||||||
// namespace jest {
|
|
||||||
// interface Expect extends CustomMatchers {}
|
|
||||||
// interface Matchers<R> extends CustomMatchers<R> {}
|
|
||||||
// interface InverseAsymmetricMatchers extends CustomMatchers {}
|
|
||||||
// }
|
|
||||||
//}
|
|
||||||
|
|
||||||
expect.extend({
|
|
||||||
isDeepEqual: (received: any, target: any) => {
|
|
||||||
const pass = _.isEqual(received, target);
|
|
||||||
if (pass) {
|
|
||||||
return { message: () => '', pass: true };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
message: () =>
|
|
||||||
`${JSON.stringify(received)} instead of\n ${JSON.stringify(target)}`,
|
|
||||||
pass: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
isAlmostDeepEqual: (received: any, target: any, delta: number) => {
|
|
||||||
const pass = _.isEqualWith(received, target, (r: any, t: any) => {
|
|
||||||
if (typeof r !== 'number' || typeof t !== 'number') {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
console.log(`r: ${r}, t:${t}, ${Math.abs(r - t) <= delta}`);
|
|
||||||
return Math.abs(r - t) <= delta;
|
|
||||||
});
|
|
||||||
if (pass) {
|
|
||||||
return { message: () => '', pass: true };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
message: () =>
|
|
||||||
`${JSON.stringify(received)} instead of\n ${JSON.stringify(target)}`,
|
|
||||||
pass: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Our isAlmostDeepEqual matcher', () => {
|
|
||||||
test('compares correctly two numbers that are almost equals', () => {
|
|
||||||
expect({ x: 1.001 }).isAlmostDeepEqual({ x: 1 }, 1e-2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Map store methods', () => {
|
|
||||||
test('initialize its state', () => {
|
|
||||||
const state = _.cloneDeep(initialMapState);
|
|
||||||
expect(state.tiles.nb.x).not.toBe(0);
|
|
||||||
});
|
|
||||||
test('reevaluateState keeps the same values', () => {
|
|
||||||
const state = _.cloneDeep(initialMapState);
|
|
||||||
reevaluateState(state);
|
|
||||||
expect(state).isAlmostDeepEqual(initialMapState, 1e-7);
|
|
||||||
});
|
|
||||||
test('reevaluateState computes the right longitude after a shift 50 pixels left', () => {
|
|
||||||
const state = _.cloneDeep(initialMapState);
|
|
||||||
state.slippy.translation.x = state.slippy.translation.x - 50;
|
|
||||||
reevaluateState(state);
|
|
||||||
expect(state.scope.center.lon).not.toBe(77.5539501);
|
|
||||||
});
|
|
||||||
});
|
|
281
src/store/map.ts
281
src/store/map.ts
|
@ -1,281 +0,0 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import { tileProviders } from '../components/map/tile';
|
|
||||||
import { tileSize } from '../components/map/tiled-map';
|
|
||||||
import {
|
|
||||||
geoPoint,
|
|
||||||
Point,
|
|
||||||
lon2tile,
|
|
||||||
lat2tile,
|
|
||||||
tile2lat,
|
|
||||||
tile2long,
|
|
||||||
} from '../lib/geo';
|
|
||||||
|
|
||||||
export const zoom0 = 18;
|
|
||||||
|
|
||||||
// Top level properties (the other properties can be derived from them)
|
|
||||||
|
|
||||||
// The map itself
|
|
||||||
export interface MapScope {
|
|
||||||
center: geoPoint;
|
|
||||||
zoom: number;
|
|
||||||
tileProvider: string;
|
|
||||||
}
|
|
||||||
var initialMapScope: MapScope = {
|
|
||||||
center: { lat: -37.8403508, lon: 77.5539501 },
|
|
||||||
zoom: 15,
|
|
||||||
tileProvider: 'osm',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Derived properties
|
|
||||||
|
|
||||||
// Properties needed to render the tiled map
|
|
||||||
export interface TilesDescription {
|
|
||||||
nb: Point;
|
|
||||||
first: Point;
|
|
||||||
zoom: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properties needed to render the slippy viewport
|
|
||||||
export interface SlippyState {
|
|
||||||
scale: number;
|
|
||||||
translation: Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Properties needed to render the SVG whiteboard
|
|
||||||
export interface WhiteboardState {
|
|
||||||
scale: number;
|
|
||||||
translation: Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current location
|
|
||||||
|
|
||||||
export interface CurrentLocationState {
|
|
||||||
geo: geoPoint;
|
|
||||||
whiteboard: Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Window
|
|
||||||
|
|
||||||
export interface WindowState {
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Global state
|
|
||||||
export interface MapState {
|
|
||||||
scope: MapScope;
|
|
||||||
tiles: TilesDescription;
|
|
||||||
slippy: SlippyState;
|
|
||||||
whiteboard: WhiteboardState;
|
|
||||||
currentLocation: CurrentLocationState;
|
|
||||||
window: WindowState;
|
|
||||||
}
|
|
||||||
export var initialMapState: MapState = {
|
|
||||||
scope: initialMapScope,
|
|
||||||
slippy: {
|
|
||||||
scale: 1,
|
|
||||||
translation: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
tiles: {
|
|
||||||
first: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
nb: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
zoom: 0,
|
|
||||||
},
|
|
||||||
whiteboard: {
|
|
||||||
scale: 0,
|
|
||||||
translation: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
currentLocation: {
|
|
||||||
geo: {
|
|
||||||
lat: 0,
|
|
||||||
lon: 0,
|
|
||||||
},
|
|
||||||
whiteboard: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
window: {
|
|
||||||
height: window.innerHeight,
|
|
||||||
width: window.innerWidth
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const evaluateWhiteboardViewBox = (
|
|
||||||
state: MapState,
|
|
||||||
visibleTileSize: number
|
|
||||||
) => {
|
|
||||||
// Update the whiteboard SVG viewBox
|
|
||||||
|
|
||||||
const scaleFactor = 2 ** (state.scope.zoom - zoom0);
|
|
||||||
|
|
||||||
state.whiteboard.scale = scaleFactor * tileSize;
|
|
||||||
state.whiteboard.translation.x =
|
|
||||||
(-state.tiles.first.x * visibleTileSize + state.slippy.translation.x) /
|
|
||||||
state.whiteboard.scale;
|
|
||||||
state.whiteboard.translation.y =
|
|
||||||
(-state.tiles.first.y * visibleTileSize + state.slippy.translation.y) /
|
|
||||||
state.whiteboard.scale;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const evaluateStateFromScope = (state: MapState) => {
|
|
||||||
console.log('<<<<<<<<<<<< evaluateStateFromScope');
|
|
||||||
state.tiles.zoom = _.round(state.scope.zoom);
|
|
||||||
if (state.tiles.zoom < tileProviders[state.scope.tileProvider].minZoom) {
|
|
||||||
state.tiles.zoom = tileProviders[state.scope.tileProvider].minZoom;
|
|
||||||
}
|
|
||||||
if (state.tiles.zoom > tileProviders[state.scope.tileProvider].maxZoom) {
|
|
||||||
state.tiles.zoom = tileProviders[state.scope.tileProvider].maxZoom;
|
|
||||||
}
|
|
||||||
|
|
||||||
const softZoom = state.scope.zoom - state.tiles.zoom;
|
|
||||||
|
|
||||||
state.slippy.scale = 2 ** softZoom;
|
|
||||||
|
|
||||||
const visibleTileSize = tileSize * state.slippy.scale;
|
|
||||||
|
|
||||||
state.tiles.nb.x = _.ceil(window.innerWidth / visibleTileSize + 4);
|
|
||||||
state.tiles.nb.y = _.ceil(window.innerHeight / visibleTileSize + 4);
|
|
||||||
|
|
||||||
const tilesCenter: Point = {
|
|
||||||
x: lon2tile(state.scope.center.lon, state.tiles.zoom),
|
|
||||||
y: lat2tile(state.scope.center.lat, state.tiles.zoom),
|
|
||||||
};
|
|
||||||
|
|
||||||
state.tiles.first.x = _.floor(tilesCenter.x - state.tiles.nb.x / 2);
|
|
||||||
state.tiles.first.y = _.floor(tilesCenter.y - state.tiles.nb.y / 2);
|
|
||||||
|
|
||||||
const tilesCenterTargetLocation: Point = {
|
|
||||||
x: window.innerWidth / 2,
|
|
||||||
y: window.innerHeight / 2,
|
|
||||||
};
|
|
||||||
const tilesCenterActualLocation: Point = {
|
|
||||||
x: (tilesCenter.x - state.tiles.first.x) * visibleTileSize,
|
|
||||||
y: (tilesCenter.y - state.tiles.first.y) * visibleTileSize,
|
|
||||||
};
|
|
||||||
|
|
||||||
state.slippy.translation.x =
|
|
||||||
tilesCenterTargetLocation.x - tilesCenterActualLocation.x;
|
|
||||||
state.slippy.translation.y =
|
|
||||||
tilesCenterTargetLocation.y - tilesCenterActualLocation.y;
|
|
||||||
|
|
||||||
evaluateWhiteboardViewBox(state, visibleTileSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
evaluateStateFromScope(initialMapState);
|
|
||||||
|
|
||||||
export const reevaluateState = (state: MapState) => {
|
|
||||||
// Update the scope (center and zoom level)
|
|
||||||
const centerPX = {
|
|
||||||
x: window.innerWidth / 2,
|
|
||||||
y: window.innerHeight / 2,
|
|
||||||
};
|
|
||||||
const visibleTileSize = tileSize * state.slippy.scale;
|
|
||||||
const centerTiles = {
|
|
||||||
x:
|
|
||||||
state.tiles.first.x +
|
|
||||||
(centerPX.x - state.slippy.translation.x) / visibleTileSize,
|
|
||||||
y:
|
|
||||||
state.tiles.first.y +
|
|
||||||
(centerPX.y - state.slippy.translation.y) / visibleTileSize,
|
|
||||||
};
|
|
||||||
state.scope.center.lat = tile2lat(centerTiles.y, state.tiles.zoom);
|
|
||||||
state.scope.center.lon = tile2long(centerTiles.x, state.tiles.zoom);
|
|
||||||
|
|
||||||
state.scope.zoom = state.tiles.zoom + Math.log2(state.slippy.scale);
|
|
||||||
|
|
||||||
// Check if the state must be reevaluated
|
|
||||||
|
|
||||||
if (
|
|
||||||
-state.slippy.translation.x < visibleTileSize ||
|
|
||||||
-state.slippy.translation.y < visibleTileSize ||
|
|
||||||
-state.slippy.translation.x >
|
|
||||||
(state.tiles.nb.x - 1) * visibleTileSize - window.innerWidth ||
|
|
||||||
-state.slippy.translation.y >
|
|
||||||
(state.tiles.nb.y - 1) * visibleTileSize - window.innerHeight ||
|
|
||||||
(state.slippy.scale > Math.SQRT2 &&
|
|
||||||
state.tiles.zoom + 1 <=
|
|
||||||
tileProviders[state.scope.tileProvider].maxZoom) ||
|
|
||||||
(state.slippy.scale < Math.SQRT1_2 &&
|
|
||||||
state.tiles.zoom - 1 >= tileProviders[state.scope.tileProvider].minZoom)
|
|
||||||
) {
|
|
||||||
evaluateStateFromScope(state);
|
|
||||||
} else {
|
|
||||||
evaluateWhiteboardViewBox(state, visibleTileSize);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapSlice = createSlice({
|
|
||||||
name: 'map',
|
|
||||||
initialState: initialMapState,
|
|
||||||
reducers: {
|
|
||||||
setTileProvider: (state, action) => {
|
|
||||||
state.scope.tileProvider = action.payload;
|
|
||||||
if (
|
|
||||||
state.tiles.zoom < tileProviders[state.scope.tileProvider].minZoom ||
|
|
||||||
state.tiles.zoom > tileProviders[state.scope.tileProvider].maxZoom
|
|
||||||
) {
|
|
||||||
evaluateStateFromScope(state);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
resize: (state) => {
|
|
||||||
state.window.height = window.innerHeight;
|
|
||||||
state.window.width = window.innerWidth;
|
|
||||||
evaluateStateFromScope(state);
|
|
||||||
},
|
|
||||||
setCenter: (state, action) => {
|
|
||||||
state.scope.center.lat = action.payload.lat;
|
|
||||||
state.scope.center.lon = action.payload.lon;
|
|
||||||
evaluateStateFromScope(state);
|
|
||||||
},
|
|
||||||
setCurrent: (state, action) => {
|
|
||||||
state.currentLocation.geo.lat = action.payload.lat;
|
|
||||||
state.currentLocation.geo.lon = action.payload.lon;
|
|
||||||
state.currentLocation.whiteboard.x = lon2tile(
|
|
||||||
state.currentLocation.geo.lon,
|
|
||||||
zoom0
|
|
||||||
);
|
|
||||||
state.currentLocation.whiteboard.y = lat2tile(
|
|
||||||
state.currentLocation.geo.lat,
|
|
||||||
zoom0
|
|
||||||
);
|
|
||||||
},
|
|
||||||
shift: (state, action) => {
|
|
||||||
state.slippy.translation.x =
|
|
||||||
state.slippy.translation.x + action.payload.x;
|
|
||||||
state.slippy.translation.y =
|
|
||||||
state.slippy.translation.y + action.payload.y;
|
|
||||||
reevaluateState(state);
|
|
||||||
},
|
|
||||||
scale: (state, action) => {
|
|
||||||
state.slippy.scale = state.slippy.scale * action.payload.factor;
|
|
||||||
state.slippy.translation.x =
|
|
||||||
state.slippy.translation.x +
|
|
||||||
(state.slippy.translation.x - action.payload.center.x) *
|
|
||||||
(action.payload.factor - 1);
|
|
||||||
state.slippy.translation.y =
|
|
||||||
state.slippy.translation.y +
|
|
||||||
(state.slippy.translation.y - action.payload.center.y) *
|
|
||||||
(action.payload.factor - 1);
|
|
||||||
|
|
||||||
reevaluateState(state);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mapActions = mapSlice.actions;
|
|
||||||
|
|
||||||
export default mapSlice.reducer;
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
export interface SettingsState {
|
|
||||||
default?: boolean;
|
|
||||||
user: {
|
|
||||||
pseudo: string;
|
|
||||||
};
|
|
||||||
geolocation: {
|
|
||||||
minTimeInterval: number;
|
|
||||||
minDistance: number;
|
|
||||||
};
|
|
||||||
language: 'auto'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initialSettingsState: SettingsState = {
|
|
||||||
default: true,
|
|
||||||
user: {
|
|
||||||
pseudo: 'user',
|
|
||||||
},
|
|
||||||
geolocation: {
|
|
||||||
minTimeInterval: 15000,
|
|
||||||
minDistance: 10,
|
|
||||||
},
|
|
||||||
language: 'auto',
|
|
||||||
};
|
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
|
||||||
name: 'settings',
|
|
||||||
initialState: initialSettingsState,
|
|
||||||
reducers: {
|
|
||||||
saveSettings: (state, action) => {
|
|
||||||
return action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const settingsActions = settingsSlice.actions;
|
|
||||||
|
|
||||||
export default settingsSlice.reducer;
|
|
|
@ -1,10 +0,0 @@
|
||||||
|
|
||||||
.get-position {
|
|
||||||
position: absolute;
|
|
||||||
display: block;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
bottom: 10px;
|
|
||||||
margin-left: 50%;
|
|
||||||
transform: translateX(-20px);
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
|
||||||
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
||||||
xmlns:math="http://www.w3.org/2005/xpath-functions/math"
|
|
||||||
xmlns:xd="http://www.oxygenxml.com/ns/doc/xsl" exclude-result-prefixes="xs math xd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg" xpath-default-namespace="http://www.w3.org/2000/svg"
|
|
||||||
version="3.0">
|
|
||||||
<xd:doc scope="stylesheet">
|
|
||||||
<xd:desc>
|
|
||||||
<xd:p><xd:b>Created on:</xd:b> Sep 27, 2022</xd:p>
|
|
||||||
<xd:p><xd:b>Author:</xd:b> vdv</xd:p>
|
|
||||||
<xd:p/>
|
|
||||||
</xd:desc>
|
|
||||||
</xd:doc>
|
|
||||||
|
|
||||||
<xsl:template match="@* | node()">
|
|
||||||
<xsl:copy>
|
|
||||||
<xsl:apply-templates select="@* | node()"/>
|
|
||||||
</xsl:copy>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<xsl:template match="*" priority="1">
|
|
||||||
<xsl:element name="{local-name()}" namespace="http://www.w3.org/2000/svg">
|
|
||||||
<xsl:apply-templates select="@* | node()"/>
|
|
||||||
</xsl:element>
|
|
||||||
</xsl:template>
|
|
||||||
|
|
||||||
<xsl:template match="*[namespace-uri() != 'http://www.w3.org/2000/svg']" priority="2"/>
|
|
||||||
|
|
||||||
<xsl:template match="@*[namespace-uri() != '']" priority="2"/>
|
|
||||||
|
|
||||||
|
|
||||||
</xsl:stylesheet>
|
|
|
@ -1,21 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100" id="svg4460">
|
|
||||||
<path style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.45134997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:188.97599792;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" d="M 22.5 0 C 19.738691 0.00027608953 17.500276 2.2386906 17.5 5 L 17.5 95 C 17.500276 97.761309 19.738691 99.999724 22.5 100 L 77.5 100 C 80.261309 99.999724 82.499724 97.761309 82.5 95 L 82.5 5 C 82.499724 2.2386906 80.261309 0.00027608953 77.5 0 L 22.5 0 z M 27.5 10 L 72.771484 10 L 72.771484 86 L 57.341797 86 C 58.580391 85.448966 59.8144 84.804893 60.994141 83.974609 L 60.998047 83.970703 L 61.001953 83.96875 C 61.72283 83.457013 62.435251 82.86605 63.09375 82.15625 L 60.228516 79.041016 C 59.81405 79.487772 59.324496 79.900903 58.78125 80.287109 L 58.779297 80.287109 C 57.351568 81.291697 55.694654 82.048612 53.935547 82.699219 L 54.976562 86 L 50.691406 86 L 50.203125 83.869141 C 47.655524 84.554832 45.050384 85.076819 42.414062 85.519531 L 42.482422 86 L 27.5 86 L 27.5 10 z M 56 13.990234 C 50.659618 13.990234 46.285156 18.352611 46.285156 23.677734 C 46.285156 25.741003 46.943198 27.659873 48.058594 29.236328 L 54.814453 40.914062 C 55.760615 42.150123 56.391017 41.916033 57.177734 40.849609 L 64.628906 28.167969 C 64.779447 27.895655 64.897013 27.60664 65 27.310547 C 65.459945 26.187167 65.714844 24.960316 65.714844 23.677734 C 65.714844 18.352611 61.340382 13.990234 56 13.990234 z M 56 18.529297 C 58.875642 18.529297 61.162109 20.810309 61.162109 23.677734 C 61.162109 26.54516 58.875642 28.826172 56 28.826172 C 53.124365 28.826172 50.837891 26.54516 50.837891 23.677734 C 50.837891 20.810309 53.124365 18.529297 56 18.529297 z M 55.083984 43.818359 C 52.331426 44.159847 49.556949 44.647301 46.8125 45.429688 L 47.845703 49.681641 C 50.33888 48.970886 52.924484 48.511129 55.548828 48.185547 L 55.083984 43.818359 z M 42.703125 46.900391 C 41.899176 47.258443 41.099432 47.670605 40.320312 48.15625 L 40.318359 48.160156 L 40.3125 48.162109 C 39.221777 48.849029 38.072394 49.720017 37.134766 50.986328 C 36.454385 51.904007 35.899497 53.06454 35.769531 54.4375 L 39.810547 54.884766 C 39.841087 54.562134 40.003987 54.138357 40.300781 53.738281 L 40.302734 53.738281 L 40.302734 53.736328 C 40.781568 53.08964 41.498964 52.500208 42.351562 51.962891 L 42.353516 51.962891 C 42.949584 51.591924 43.58544 51.261899 44.248047 50.966797 L 42.703125 46.900391 z M 40.623047 56.84375 L 38.003906 60.203125 C 38.624675 60.770477 39.275568 61.20552 39.904297 61.570312 L 39.912109 61.574219 L 39.921875 61.580078 C 42.009749 62.767146 44.160579 63.437732 46.15625 64.074219 L 47.304688 59.855469 C 45.30999 59.219242 43.428554 58.604712 41.816406 57.689453 C 41.355333 57.421336 40.949774 57.142362 40.623047 56.84375 z M 51.199219 61.041016 L 50.080078 65.267578 L 50.609375 65.431641 L 51.259766 65.638672 C 53.41277 66.338602 55.474793 67.087486 57.318359 68.123047 L 59.189453 64.220703 C 56.959894 62.968325 54.645722 62.15036 52.416016 61.425781 L 52.410156 61.423828 L 51.742188 61.210938 L 51.199219 61.041016 z M 62.990234 67.148438 L 60.119141 70.257812 C 60.797859 70.993066 61.305407 71.877723 61.552734 72.791016 L 61.554688 72.796875 L 61.554688 72.802734 C 61.849699 73.869603 61.851853 75.12486 61.621094 76.392578 L 65.605469 77.242188 C 65.931226 75.452628 65.983971 73.487852 65.449219 71.548828 C 64.983707 69.833728 64.09394 68.344078 62.990234 67.148438 z M 45 90.410156 L 55 90.410156 L 55 95 L 45 95 L 45 90.410156 z " id="path4492"/>
|
|
||||||
|
|
||||||
<defs id="defs4462">
|
|
||||||
<marker orient="auto" refY="0.0" refX="0.0" id="TriangleOutL" style="overflow:visible">
|
|
||||||
<path id="path974" d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z " style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1" transform="scale(0.15)"/>
|
|
||||||
</marker>
|
|
||||||
<marker style="overflow:visible" id="TriangleOutL-4" refX="0" refY="0" orient="auto">
|
|
||||||
<path transform="scale(0.15)" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1" d="M 5.77,0 -2.88,5 V -5 Z" id="path974-8"/>
|
|
||||||
</marker>
|
|
||||||
<marker orient="auto" refY="0" refX="0" id="TriangleOutL-4-4" style="overflow:visible">
|
|
||||||
<path id="path974-8-6" d="M 5.77,0 -2.88,5 V -5 Z" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1" transform="scale(0.15)"/>
|
|
||||||
</marker>
|
|
||||||
<marker orient="auto" refY="0" refX="0" id="TriangleOutL-4-3" style="overflow:visible">
|
|
||||||
<path id="path974-8-4" d="M 5.77,0 -2.88,5 V -5 Z" style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1" transform="scale(0.15)"/>
|
|
||||||
</marker>
|
|
||||||
</defs>
|
|
||||||
<metadata id="metadata4465">
|
|
||||||
|
|
||||||
</metadata>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 5.8 KiB |
|
@ -1,112 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
version="1.1"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
id="svg4460"
|
|
||||||
sodipodi:docname="uEB08-phone-route.svg"
|
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
|
||||||
<path
|
|
||||||
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#000080;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:3.45134997;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:188.97599792;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
|
|
||||||
d="M 22.5 0 C 19.738691 0.00027608953 17.500276 2.2386906 17.5 5 L 17.5 95 C 17.500276 97.761309 19.738691 99.999724 22.5 100 L 77.5 100 C 80.261309 99.999724 82.499724 97.761309 82.5 95 L 82.5 5 C 82.499724 2.2386906 80.261309 0.00027608953 77.5 0 L 22.5 0 z M 27.5 10 L 72.771484 10 L 72.771484 86 L 57.341797 86 C 58.580391 85.448966 59.8144 84.804893 60.994141 83.974609 L 60.998047 83.970703 L 61.001953 83.96875 C 61.72283 83.457013 62.435251 82.86605 63.09375 82.15625 L 60.228516 79.041016 C 59.81405 79.487772 59.324496 79.900903 58.78125 80.287109 L 58.779297 80.287109 C 57.351568 81.291697 55.694654 82.048612 53.935547 82.699219 L 54.976562 86 L 50.691406 86 L 50.203125 83.869141 C 47.655524 84.554832 45.050384 85.076819 42.414062 85.519531 L 42.482422 86 L 27.5 86 L 27.5 10 z M 56 13.990234 C 50.659618 13.990234 46.285156 18.352611 46.285156 23.677734 C 46.285156 25.741003 46.943198 27.659873 48.058594 29.236328 L 54.814453 40.914062 C 55.760615 42.150123 56.391017 41.916033 57.177734 40.849609 L 64.628906 28.167969 C 64.779447 27.895655 64.897013 27.60664 65 27.310547 C 65.459945 26.187167 65.714844 24.960316 65.714844 23.677734 C 65.714844 18.352611 61.340382 13.990234 56 13.990234 z M 56 18.529297 C 58.875642 18.529297 61.162109 20.810309 61.162109 23.677734 C 61.162109 26.54516 58.875642 28.826172 56 28.826172 C 53.124365 28.826172 50.837891 26.54516 50.837891 23.677734 C 50.837891 20.810309 53.124365 18.529297 56 18.529297 z M 55.083984 43.818359 C 52.331426 44.159847 49.556949 44.647301 46.8125 45.429688 L 47.845703 49.681641 C 50.33888 48.970886 52.924484 48.511129 55.548828 48.185547 L 55.083984 43.818359 z M 42.703125 46.900391 C 41.899176 47.258443 41.099432 47.670605 40.320312 48.15625 L 40.318359 48.160156 L 40.3125 48.162109 C 39.221777 48.849029 38.072394 49.720017 37.134766 50.986328 C 36.454385 51.904007 35.899497 53.06454 35.769531 54.4375 L 39.810547 54.884766 C 39.841087 54.562134 40.003987 54.138357 40.300781 53.738281 L 40.302734 53.738281 L 40.302734 53.736328 C 40.781568 53.08964 41.498964 52.500208 42.351562 51.962891 L 42.353516 51.962891 C 42.949584 51.591924 43.58544 51.261899 44.248047 50.966797 L 42.703125 46.900391 z M 40.623047 56.84375 L 38.003906 60.203125 C 38.624675 60.770477 39.275568 61.20552 39.904297 61.570312 L 39.912109 61.574219 L 39.921875 61.580078 C 42.009749 62.767146 44.160579 63.437732 46.15625 64.074219 L 47.304688 59.855469 C 45.30999 59.219242 43.428554 58.604712 41.816406 57.689453 C 41.355333 57.421336 40.949774 57.142362 40.623047 56.84375 z M 51.199219 61.041016 L 50.080078 65.267578 L 50.609375 65.431641 L 51.259766 65.638672 C 53.41277 66.338602 55.474793 67.087486 57.318359 68.123047 L 59.189453 64.220703 C 56.959894 62.968325 54.645722 62.15036 52.416016 61.425781 L 52.410156 61.423828 L 51.742188 61.210938 L 51.199219 61.041016 z M 62.990234 67.148438 L 60.119141 70.257812 C 60.797859 70.993066 61.305407 71.877723 61.552734 72.791016 L 61.554688 72.796875 L 61.554688 72.802734 C 61.849699 73.869603 61.851853 75.12486 61.621094 76.392578 L 65.605469 77.242188 C 65.931226 75.452628 65.983971 73.487852 65.449219 71.548828 C 64.983707 69.833728 64.09394 68.344078 62.990234 67.148438 z M 45 90.410156 L 55 90.410156 L 55 95 L 45 95 L 45 90.410156 z "
|
|
||||||
id="path4492" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1177"
|
|
||||||
id="namedview11"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="3.6"
|
|
||||||
inkscape:cx="8.4145021"
|
|
||||||
inkscape:cy="84.274158"
|
|
||||||
inkscape:window-x="1272"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg4460"
|
|
||||||
inkscape:document-rotation="0" />
|
|
||||||
<defs
|
|
||||||
id="defs4462">
|
|
||||||
<marker
|
|
||||||
inkscape:stockid="TriangleOutL"
|
|
||||||
orient="auto"
|
|
||||||
refY="0.0"
|
|
||||||
refX="0.0"
|
|
||||||
id="TriangleOutL"
|
|
||||||
style="overflow:visible"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
id="path974"
|
|
||||||
d="M 5.77,0.0 L -2.88,5.0 L -2.88,-5.0 L 5.77,0.0 z "
|
|
||||||
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
|
||||||
transform="scale(0.15)" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
inkscape:isstock="true"
|
|
||||||
style="overflow:visible"
|
|
||||||
id="TriangleOutL-4"
|
|
||||||
refX="0"
|
|
||||||
refY="0"
|
|
||||||
orient="auto"
|
|
||||||
inkscape:stockid="TriangleOutL">
|
|
||||||
<path
|
|
||||||
transform="scale(0.15)"
|
|
||||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
|
||||||
d="M 5.77,0 -2.88,5 V -5 Z"
|
|
||||||
id="path974-8" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
inkscape:stockid="TriangleOutL"
|
|
||||||
orient="auto"
|
|
||||||
refY="0"
|
|
||||||
refX="0"
|
|
||||||
id="TriangleOutL-4-4"
|
|
||||||
style="overflow:visible"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
id="path974-8-6"
|
|
||||||
d="M 5.77,0 -2.88,5 V -5 Z"
|
|
||||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
|
||||||
transform="scale(0.15)" />
|
|
||||||
</marker>
|
|
||||||
<marker
|
|
||||||
inkscape:stockid="TriangleOutL"
|
|
||||||
orient="auto"
|
|
||||||
refY="0"
|
|
||||||
refX="0"
|
|
||||||
id="TriangleOutL-4-3"
|
|
||||||
style="overflow:visible"
|
|
||||||
inkscape:isstock="true">
|
|
||||||
<path
|
|
||||||
id="path974-8-4"
|
|
||||||
d="M 5.77,0 -2.88,5 V -5 Z"
|
|
||||||
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1"
|
|
||||||
transform="scale(0.15)" />
|
|
||||||
</marker>
|
|
||||||
</defs>
|
|
||||||
<metadata
|
|
||||||
id="metadata4465">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 7.6 KiB |
|
@ -1,42 +0,0 @@
|
||||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
|
||||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
|
||||||
viewBox="0 0 297 297" style="enable-background:new 0 0 297 297;" xml:space="preserve">
|
|
||||||
<path d="M148.5,0C66.653,0,0.067,66.616,0.067,148.499C0.067,230.383,66.653,297,148.5,297s148.433-66.617,148.433-148.501
|
|
||||||
C296.933,66.616,230.347,0,148.5,0z M158.597,276.411v-61.274c0-5.575-4.521-10.097-10.097-10.097s-10.097,4.521-10.097,10.097
|
|
||||||
v61.274c-62.68-4.908-112.845-55.102-117.747-117.814h61.207c5.575,0,10.097-4.521,10.097-10.097s-4.522-10.097-10.097-10.097
|
|
||||||
H20.656C25.558,75.69,75.723,25.497,138.403,20.589v61.274c0,5.575,4.521,10.097,10.097,10.097s10.097-4.521,10.097-10.097V20.589
|
|
||||||
c62.681,4.908,112.846,55.102,117.747,117.814h-61.207c-5.575,0-10.097,4.521-10.097,10.097s4.521,10.097,10.097,10.097h61.207
|
|
||||||
C271.441,221.31,221.276,271.503,158.597,276.411z"/>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 1.3 KiB |
|
@ -1,6 +0,0 @@
|
||||||
.background {
|
|
||||||
position: fixed;
|
|
||||||
width: 4032px;
|
|
||||||
height: 2268px;
|
|
||||||
transform-origin: top left;
|
|
||||||
}
|
|
|
@ -1,104 +0,0 @@
|
||||||
.tilesRow {
|
|
||||||
height: 256px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tile {
|
|
||||||
height: 256px;
|
|
||||||
width: 256px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-button.get-location {
|
|
||||||
--opacity: 0.6;
|
|
||||||
--ion-background-color: white;
|
|
||||||
margin-left: calc(50% - 20px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.whiteboard {
|
|
||||||
position: fixed;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-file {
|
|
||||||
width: 0.1px;
|
|
||||||
height: 0.1px;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
path.track {
|
|
||||||
fill: transparent;
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
path {
|
|
||||||
stroke: rgba(10, 1, 51, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
path.current {
|
|
||||||
stroke: rgba(2, 71, 20, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle path,
|
|
||||||
path.reticle,
|
|
||||||
use.reticle,
|
|
||||||
.reticle use {
|
|
||||||
stroke: red;
|
|
||||||
}
|
|
||||||
|
|
||||||
.reticle text {
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-modal {
|
|
||||||
--height: 100%;
|
|
||||||
--border-radius: 16px;
|
|
||||||
--box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1),
|
|
||||||
0 4px 6px -4px rgb(0 0 0 / 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-modal.full-height {
|
|
||||||
--height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ion-modal::part(backdrop) {
|
|
||||||
background: rgba(209, 213, 219);
|
|
||||||
opacity: 1;
|
|
||||||
} */
|
|
||||||
|
|
||||||
ion-modal ion-toolbar {
|
|
||||||
--background: rgba(14, 116, 144, 0.7);
|
|
||||||
--color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-modal.full-height ion-toolbar {
|
|
||||||
--background: rgba(14, 116, 144, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-modal ion-toolbar.secondary {
|
|
||||||
--background: inherit;
|
|
||||||
--color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
ion-modal ion-content {
|
|
||||||
background-color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 576px) {
|
|
||||||
#chat-sidebar {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.cs-conversation-header__avatar {
|
|
||||||
height: auto !important;
|
|
||||||
width:auto !important;
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
.slippy {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.huge {
|
|
||||||
position: fixed;
|
|
||||||
}
|
|
|
@ -1,14 +1,8 @@
|
||||||
/* Ionic Variables and Theming. For more info, please see:
|
/* Ionic Variables and Theming. For more info, please see:
|
||||||
http://ionicframework.com/docs/theming/ */
|
http://ionicframework.com/docs/theming/ */
|
||||||
|
|
||||||
|
|
||||||
/** Ionic CSS Variables **/
|
/** Ionic CSS Variables **/
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
/** Transparent background so that underlying tiles and whiteboard can be seen **/
|
|
||||||
--ion-background-color: transparent;
|
|
||||||
|
|
||||||
|
|
||||||
/** primary **/
|
/** primary **/
|
||||||
--ion-color-primary: #3880ff;
|
--ion-color-primary: #3880ff;
|
||||||
--ion-color-primary-rgb: 56, 128, 255;
|
--ion-color-primary-rgb: 56, 128, 255;
|
||||||
|
|
|
@ -1,16 +0,0 @@
|
||||||
platforms:
|
|
||||||
android:
|
|
||||||
manifest:
|
|
||||||
- file: AndroidManifest.xml
|
|
||||||
target: manifest
|
|
||||||
delete: //uses-permission
|
|
||||||
- file: AndroidManifest.xml
|
|
||||||
target: manifest
|
|
||||||
inject: |
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_LOCATION_EXTRA_COMMANDS" />
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
|
|
@ -1,7 +1,11 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": [
|
||||||
|
"dom",
|
||||||
|
"dom.iterable",
|
||||||
|
"esnext"
|
||||||
|
],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
@ -17,10 +21,6 @@
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src",
|
"src"
|
||||||
"node_modules/gpx-parser-builder/src/gpx-parser-builder.d.ts"
|
]
|
||||||
],
|
|
||||||
"paths": {
|
|
||||||
"crypto": ["node_modules/crypto-browserify"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue