Restarting from scratch

This commit is contained in:
Eric van der Vlist 2022-10-17 10:08:00 +02:00
parent ed951208e8
commit a4bd11aab0
88 changed files with 1714 additions and 17027 deletions

View File

@ -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
};

10083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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"
}

View File

@ -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]);
}
});

View File

@ -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

View File

@ -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>

View File

@ -1,6 +1,6 @@
{
"short_name": "Dyomedea",
"name": "Dyomedea",
"short_name": "Ionic App",
"name": "My Ionic App",
"icons": [
{
"src": "assets/icon/favicon.png",

8
src/App.test.tsx Normal file
View File

@ -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();
});

View File

@ -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;

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1 +0,0 @@
declare module 'avatar-initials';

Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

View File

@ -1 +0,0 @@
declare module 'font-gis';

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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({});
};

View File

@ -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)}`);
*/
};

View File

@ -1 +0,0 @@
declare module 'react-pouchdb';

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
};

View File

@ -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;

View File

@ -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();

View File

@ -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');
};

View File

@ -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
src/lib/docuri.d.ts vendored
View File

@ -1 +0,0 @@
declare module 'docuri';

View File

@ -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)));
}

View File

@ -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.

View File

@ -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.

View File

@ -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"
}
}

View File

@ -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;
}
}

View File

@ -1,7 +0,0 @@
export default class Copyright {
constructor(object) {
this.author = object.author;
this.year = object.year;
this.license = object.license;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}
}

View File

@ -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 };

View File

@ -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));
}
}
}

View File

@ -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({});
});
});

View File

@ -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;

View File

@ -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
src/pages/Home.css Normal file
View File

25
src/pages/Home.tsx Normal file
View File

@ -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;

15
src/reportWebVitals.ts Normal file
View File

@ -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;

View File

@ -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;

View File

@ -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);
});
});

View File

@ -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;

View File

@ -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;

View File

@ -1,10 +0,0 @@
.get-position {
position: absolute;
display: block;
height: 40px;
width: 40px;
bottom: 10px;
margin-left: 50%;
transform: translateX(-20px);
}

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,6 +0,0 @@
.background {
position: fixed;
width: 4032px;
height: 2268px;
transform-origin: top left;
}

View File

@ -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;
}

View File

@ -1,10 +0,0 @@
.slippy {
position: fixed;
width: 100%;
height: 100%;
z-index: 10;
}
.huge {
position: fixed;
}

View File

@ -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;

View File

@ -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" />

View File

@ -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"
]
}