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