Starting again...
This commit is contained in:
parent
1c403ae819
commit
e75b2f670c
|
@ -1,8 +1,8 @@
|
||||||
import { CapacitorConfig } from '@capacitor/cli';
|
import { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
const config: CapacitorConfig = {
|
const config: CapacitorConfig = {
|
||||||
appId: 'com.dyomedea.dyomedea2',
|
appId: 'io.ionic.starter',
|
||||||
appName: 'dyomedea2',
|
appName: 'dyomedeaOl',
|
||||||
webDir: 'build',
|
webDir: 'build',
|
||||||
bundledWebRuntime: false
|
bundledWebRuntime: false
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "dyomedea",
|
"name": "dyomedeaOl",
|
||||||
"integrations": {
|
"integrations": {
|
||||||
"capacitor": {}
|
"capacitor": {}
|
||||||
},
|
},
|
||||||
|
|
89
package.json
89
package.json
|
@ -1,55 +1,45 @@
|
||||||
{
|
{
|
||||||
"name": "dyomedea",
|
"name": "dyomedeaOl",
|
||||||
"version": "0.0.2",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@awesome-cordova-plugins/geolocation": "^6.2.0",
|
"@capacitor/app": "4.1.1",
|
||||||
"@capacitor/android": "^4.5.0",
|
"@capacitor/core": "4.5.0",
|
||||||
"@capacitor/app": "^4.1.1",
|
"@capacitor/haptics": "4.1.0",
|
||||||
"@capacitor/core": "^4.5.0",
|
"@capacitor/keyboard": "4.1.0",
|
||||||
"@capacitor/haptics": "^4.1.0",
|
"@capacitor/status-bar": "4.1.0",
|
||||||
"@capacitor/keyboard": "^4.1.0",
|
"@ionic/react": "^6.0.0",
|
||||||
"@capacitor/status-bar": "^4.1.0",
|
"@ionic/react-router": "^6.0.0",
|
||||||
"@ionic/react": "^6.3.7",
|
"@testing-library/jest-dom": "^5.11.9",
|
||||||
"@ionic/react-router": "^6.3.7",
|
"@testing-library/react": "^13.3.0",
|
||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/user-event": "^12.6.3",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@types/jest": "^26.0.20",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@types/node": "^12.19.15",
|
||||||
"@types/jest": "^29.2.3",
|
"@types/react": "^18.0.17",
|
||||||
"@types/node": "^18.11.9",
|
"@types/react-dom": "^18.0.6",
|
||||||
"@types/react": "^18.0.25",
|
"@types/react-router": "^5.1.11",
|
||||||
"@types/react-dom": "^18.0.9",
|
"@types/react-router-dom": "^5.1.7",
|
||||||
"@types/react-router": "^5.1.19",
|
"history": "^4.9.0",
|
||||||
"@types/react-router-dom": "^5.3.3",
|
"ionicons": "^6.0.3",
|
||||||
"ionicons": "^6.0.4",
|
|
||||||
"isomorphic-xml2js": "^0.1.3",
|
|
||||||
"jotai": "^1.10.0",
|
|
||||||
"jotai-location": "^0.2.0",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"pouchdb": "^7.3.1",
|
|
||||||
"pouchdb-browser": "^7.3.1",
|
|
||||||
"pouchdb-find": "^7.3.1",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-cool-dimensions": "^2.0.7",
|
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-localization": "^1.0.19",
|
"react-router": "^5.2.0",
|
||||||
"react-router": "^6.4.3",
|
"react-router-dom": "^5.2.0",
|
||||||
"react-router-dom": "^6.4.3",
|
"react-scripts": "^5.0.0",
|
||||||
"react-scripts": "^5.0.1",
|
"typescript": "^4.1.3",
|
||||||
"typedoc": "^0.23.21",
|
"web-vitals": "^0.2.4",
|
||||||
"typescript": "^4.9.3",
|
"workbox-background-sync": "^5.1.4",
|
||||||
"web-vitals": "^3.1.0",
|
"workbox-broadcast-update": "^5.1.4",
|
||||||
"workbox-background-sync": "^6.5.4",
|
"workbox-cacheable-response": "^5.1.4",
|
||||||
"workbox-broadcast-update": "^6.5.4",
|
"workbox-core": "^5.1.4",
|
||||||
"workbox-cacheable-response": "^6.5.4",
|
"workbox-expiration": "^5.1.4",
|
||||||
"workbox-core": "^6.5.4",
|
"workbox-google-analytics": "^5.1.4",
|
||||||
"workbox-expiration": "^6.5.4",
|
"workbox-navigation-preload": "^5.1.4",
|
||||||
"workbox-navigation-preload": "^6.5.4",
|
"workbox-precaching": "^5.1.4",
|
||||||
"workbox-precaching": "^6.5.4",
|
"workbox-range-requests": "^5.1.4",
|
||||||
"workbox-range-requests": "^6.5.4",
|
"workbox-routing": "^5.1.4",
|
||||||
"workbox-routing": "^6.5.4",
|
"workbox-strategies": "^5.1.4",
|
||||||
"workbox-strategies": "^6.5.4",
|
"workbox-streams": "^5.1.4"
|
||||||
"workbox-streams": "^6.5.4"
|
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "react-scripts start",
|
"start": "react-scripts start",
|
||||||
|
@ -76,10 +66,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/cli": "^4.5.0",
|
"@capacitor/cli": "4.5.0"
|
||||||
"@ionic/cli": "^6.20.4",
|
|
||||||
"@types/lodash": "^4.14.189",
|
|
||||||
"@types/pouchdb": "^6.4.0"
|
|
||||||
},
|
},
|
||||||
"description": "An Ionic project"
|
"description": "An Ionic project"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
<svg width="1000" height="1000" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g transform="scale(256) translate(-110000, -110000) ">
|
|
||||||
<path d=" M 110000,110000 L 111000, 111000" pointer-events="none" style="
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
stroke-width: 2px
|
|
||||||
|
|
||||||
fill: transparent;
|
|
||||||
stroke: black;
|
|
||||||
"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
<!--
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
stroke-width: 2
|
|
||||||
-->
|
|
||||||
<!--
|
|
||||||
stroke-width: 0.00781211;
|
|
||||||
-->
|
|
Before Width: | Height: | Size: 428 B |
|
@ -3,6 +3,6 @@ import { render } from '@testing-library/react';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
test('renders without crashing', () => {
|
test('renders without crashing', () => {
|
||||||
// const { baseElement } = render(<App />);
|
const { baseElement } = render(<App />);
|
||||||
// expect(baseElement).toBeDefined();
|
expect(baseElement).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
154
src/App.tsx
154
src/App.tsx
|
@ -1,12 +1,7 @@
|
||||||
import {
|
import { Redirect, Route } from 'react-router-dom';
|
||||||
IonApp,
|
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
|
||||||
IonButtons,
|
import { IonReactRouter } from '@ionic/react-router';
|
||||||
IonContent,
|
import Home from './pages/Home';
|
||||||
IonFooter,
|
|
||||||
IonHeader,
|
|
||||||
IonToolbar,
|
|
||||||
setupIonicReact,
|
|
||||||
} from '@ionic/react';
|
|
||||||
|
|
||||||
/* Core CSS required for Ionic components to work properly */
|
/* Core CSS required for Ionic components to work properly */
|
||||||
import '@ionic/react/css/core.css';
|
import '@ionic/react/css/core.css';
|
||||||
|
@ -27,136 +22,21 @@ import '@ionic/react/css/display.css';
|
||||||
/* Theme variables */
|
/* Theme variables */
|
||||||
import './theme/variables.css';
|
import './theme/variables.css';
|
||||||
|
|
||||||
import dispatch from './workers/dispatcher-main';
|
|
||||||
|
|
||||||
import LiveMap from './components/map/LiveMap';
|
|
||||||
|
|
||||||
import { atom, useAtom } from 'jotai';
|
|
||||||
import { atomWithHash } from 'jotai-location';
|
|
||||||
import { MapScope } from './components/map/types';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import GetLocation from './components/buttons/GetLocation';
|
|
||||||
import { geoPoint } from './components/map/types';
|
|
||||||
import Back from './components/buttons/Back';
|
|
||||||
import Forward from './components/buttons/Forward';
|
|
||||||
import CurrentLocation from './components/map/CurrentLocation';
|
|
||||||
import GpxImport from './components/dialogs/GpxImport';
|
|
||||||
import Gpxes from './components/map/Gpxes';
|
|
||||||
import MapChooser from './components/dialogs/MapChooser';
|
|
||||||
import Explorer from './components/dialogs/Explorer';
|
|
||||||
// import { initDb } from './db';
|
|
||||||
// import PouchDB from 'pouchdb';
|
|
||||||
// import PouchDBFind from 'pouchdb-find';
|
|
||||||
// PouchDB.plugin(PouchDBFind);
|
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
// See https://stackoverflow.com/questions/71538643/property-wakelock-does-not-exist-on-type-navigator
|
const App: React.FC = () => (
|
||||||
const requestWakeLock = async () => {
|
<IonApp>
|
||||||
const anyNav: any = navigator;
|
<IonReactRouter>
|
||||||
if ('wakeLock' in navigator) {
|
<IonRouterOutlet>
|
||||||
try {
|
<Route exact path="/home">
|
||||||
const wakeLock = await anyNav['wakeLock'].request('screen');
|
<Home />
|
||||||
} catch (err: any) {
|
</Route>
|
||||||
// The wake lock request fails - usually system-related, such as low battery.
|
<Route exact path="/">
|
||||||
console.log(`Wake lock request failed: ${err.name}, ${err.message}`);
|
<Redirect to="/home" />
|
||||||
}
|
</Route>
|
||||||
} else {
|
</IonRouterOutlet>
|
||||||
console.log('No wake lock support here...');
|
</IonReactRouter>
|
||||||
}
|
</IonApp>
|
||||||
};
|
|
||||||
|
|
||||||
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 initialScope: MapScope = {
|
|
||||||
center: { lat: -37.8403508, lon: 77.5539501 },
|
|
||||||
zoom: 13,
|
|
||||||
tileProvider: 'osm',
|
|
||||||
};
|
|
||||||
const scopeAtom = atomWithHash('scope', initialScope);
|
|
||||||
|
|
||||||
export const setCenterAtom = atom(null, (get, set, center: geoPoint) => {
|
|
||||||
const previousScope = get(scopeAtom);
|
|
||||||
const newScope: MapScope = {
|
|
||||||
...previousScope,
|
|
||||||
center: center,
|
|
||||||
};
|
|
||||||
set(scopeAtom, newScope);
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tileProviderAtom = atom(
|
|
||||||
(get) => get(scopeAtom).tileProvider,
|
|
||||||
(get, set, tileProvider: string) => {
|
|
||||||
const previousScope = get(scopeAtom);
|
|
||||||
const newScope: MapScope = {
|
|
||||||
...previousScope,
|
|
||||||
tileProvider,
|
|
||||||
};
|
|
||||||
set(scopeAtom, newScope);
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// const db = new PouchDB('dyomedea', { auto_compaction: true, revs_limit: 10 });
|
|
||||||
// initDb(db);
|
|
||||||
|
|
||||||
dispatch({ action: 'initDb' });
|
|
||||||
//dispatch({ action: 'getGpxesForViewport', params: { type: 'tech' } });
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns The root app component
|
|
||||||
*/
|
|
||||||
const App: React.FC = () => {
|
|
||||||
const [scope, setScope] = useAtom(scopeAtom);
|
|
||||||
|
|
||||||
console.log(`App, scope: ${JSON.stringify(scope)}`);
|
|
||||||
return (
|
|
||||||
<IonApp>
|
|
||||||
<IonContent fullscreen={true}>
|
|
||||||
<IonApp>
|
|
||||||
<LiveMap
|
|
||||||
scope={scope}
|
|
||||||
setScope={debounce(setScope, 1000)}
|
|
||||||
numberOfTiledLayers={5}
|
|
||||||
slippyGraphics={[
|
|
||||||
<CurrentLocation key='currentLocation' />,
|
|
||||||
<Gpxes key='gpxes' />,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</IonApp>
|
|
||||||
</IonContent>
|
|
||||||
<Explorer/>
|
|
||||||
<IonHeader className='ion-no-border' translucent={true}>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonButtons slot='start'>
|
|
||||||
<Back />
|
|
||||||
<Forward />
|
|
||||||
</IonButtons>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<GpxImport />
|
|
||||||
<MapChooser />
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonHeader>
|
|
||||||
<IonFooter className='ion-no-border'>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonButtons>
|
|
||||||
<GetLocation />
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
</IonFooter>
|
|
||||||
</IonApp>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
.container {
|
||||||
|
text-align: center;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container strong {
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container p {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: #8c8c8c;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import './ExploreContainer.css';
|
||||||
|
|
||||||
|
interface ContainerProps { }
|
||||||
|
|
||||||
|
const ExploreContainer: React.FC<ContainerProps> = () => {
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<strong>Ready to create an app?</strong>
|
||||||
|
<p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExploreContainer;
|
|
@ -1,26 +0,0 @@
|
||||||
import react from 'react';
|
|
||||||
import { IonButton, IonIcon } from '@ionic/react';
|
|
||||||
import { arrowBackCircleOutline } from 'ionicons/icons';
|
|
||||||
import './BackForward.css';
|
|
||||||
|
|
||||||
export interface BackProperties {}
|
|
||||||
|
|
||||||
export const Back: react.FC<BackProperties> = (props: BackProperties) => {
|
|
||||||
const onClickHandler = (event: any) => {
|
|
||||||
window.history.back();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonButton
|
|
||||||
onClick={onClickHandler}
|
|
||||||
size='large'
|
|
||||||
className='back-forward'
|
|
||||||
shape='round'
|
|
||||||
fill='solid'
|
|
||||||
>
|
|
||||||
<IonIcon slot='icon-only' icon={arrowBackCircleOutline} />
|
|
||||||
</IonButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Back;
|
|
|
@ -1,5 +0,0 @@
|
||||||
ion-button.back-forward {
|
|
||||||
--opacity: 0.6;
|
|
||||||
--ion-background-color: white;
|
|
||||||
--ionicon-stroke-width: 48px;
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import react from 'react';
|
|
||||||
import { IonButton, IonIcon } from '@ionic/react';
|
|
||||||
import { arrowForwardCircleOutline } from 'ionicons/icons';
|
|
||||||
import './BackForward.css';
|
|
||||||
|
|
||||||
export interface ForwardProperties {}
|
|
||||||
|
|
||||||
export const Forward: react.FC<ForwardProperties> = (props: ForwardProperties) => {
|
|
||||||
const onClickHandler = (event: any) => {
|
|
||||||
window.history.forward();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonButton
|
|
||||||
onClick={onClickHandler}
|
|
||||||
size='large'
|
|
||||||
className='back-forward'
|
|
||||||
shape='round'
|
|
||||||
fill='solid'
|
|
||||||
>
|
|
||||||
<IonIcon slot='icon-only' icon={arrowForwardCircleOutline} />
|
|
||||||
</IonButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Forward;
|
|
|
@ -1,6 +0,0 @@
|
||||||
ion-button.get-location {
|
|
||||||
--opacity: 0.6;
|
|
||||||
--ion-background-color: white;
|
|
||||||
margin-left: calc(50% - 20px);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Geolocation } from '@awesome-cordova-plugins/geolocation';
|
|
||||||
|
|
||||||
import './GetLocation.css';
|
|
||||||
import { IonButton, IonIcon } from '@ionic/react';
|
|
||||||
import { locateOutline } from 'ionicons/icons';
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
import { setCenterAtom } from '../../App';
|
|
||||||
import { locationAtom } from '../map/CurrentLocation';
|
|
||||||
|
|
||||||
const GetLocation: React.FC<{}> = () => {
|
|
||||||
const [, setCenter] = useAtom(setCenterAtom);
|
|
||||||
const [, setLocation] = useAtom(locationAtom);
|
|
||||||
|
|
||||||
const onClickHandler = (event: any) => {
|
|
||||||
console.log('Click handler');
|
|
||||||
Geolocation.getCurrentPosition().then((position) => {
|
|
||||||
console.log(
|
|
||||||
`Click handler, position: ${position.coords.latitude}, ${position.coords.longitude}`
|
|
||||||
);
|
|
||||||
setCenter({
|
|
||||||
lat: position.coords.latitude,
|
|
||||||
lon: position.coords.longitude,
|
|
||||||
});
|
|
||||||
setLocation({
|
|
||||||
lat: position.coords.latitude,
|
|
||||||
lon: position.coords.longitude,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonButton
|
|
||||||
onClick={onClickHandler}
|
|
||||||
className='get-location get-position'
|
|
||||||
shape='round'
|
|
||||||
size='small'
|
|
||||||
fill='solid'
|
|
||||||
>
|
|
||||||
<IonIcon slot='icon-only' icon={locateOutline} color='white' />
|
|
||||||
</IonButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GetLocation;
|
|
|
@ -1,50 +0,0 @@
|
||||||
import {
|
|
||||||
IonModal,
|
|
||||||
IonToolbar,
|
|
||||||
IonTitle,
|
|
||||||
IonButtons,
|
|
||||||
IonButton,
|
|
||||||
IonContent,
|
|
||||||
IonList,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import react, { useRef, useState } from 'react';
|
|
||||||
import { atom, useAtom } from 'jotai';
|
|
||||||
import i18n from '../../i18n';
|
|
||||||
|
|
||||||
import cssDialog from './dialogs.module.css';
|
|
||||||
|
|
||||||
export interface ExplorerProperties {}
|
|
||||||
|
|
||||||
export const isOpenAtom = atom(false);
|
|
||||||
|
|
||||||
export const Explorer: react.FC<ExplorerProperties> = (
|
|
||||||
props: ExplorerProperties
|
|
||||||
) => {
|
|
||||||
const modal = useRef<HTMLIonModalElement>(null);
|
|
||||||
const [isOpen, setIsOpen] = useAtom(isOpenAtom);
|
|
||||||
|
|
||||||
const dismiss = () => {
|
|
||||||
setIsOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IonModal
|
|
||||||
ref={modal}
|
|
||||||
className={cssDialog.modal}
|
|
||||||
isOpen={isOpen}
|
|
||||||
onDidDismiss={dismiss}
|
|
||||||
>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{i18n.explorer.nearBy}</IonTitle>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<IonButton onClick={dismiss}>{i18n.common.close}</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
<IonContent>
|
|
||||||
<IonList></IonList>
|
|
||||||
</IonContent>
|
|
||||||
</IonModal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Explorer;
|
|
|
@ -1,8 +0,0 @@
|
||||||
.inputFile {
|
|
||||||
width: 0.1px;
|
|
||||||
height: 0.1px;
|
|
||||||
opacity: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
import GPX from '../../lib/gpx-parser-builder/src/gpx';
|
|
||||||
|
|
||||||
import css from './GpxImport.module.css';
|
|
||||||
import { IonIcon, IonItem } from '@ionic/react';
|
|
||||||
import { cloudUpload } from 'ionicons/icons';
|
|
||||||
import { findStartTime } from '../../lib/gpx';
|
|
||||||
import dispatch from '../../workers/dispatcher-main';
|
|
||||||
import { intToGpxId } from '../../lib/ids';
|
|
||||||
|
|
||||||
const GpxImport: React.FC<{}> = () => {
|
|
||||||
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);
|
|
||||||
console.log(`gpx: ${JSON.stringify(gpx)}`);
|
|
||||||
const startTime = new Date(findStartTime(gpx)!);
|
|
||||||
dispatch({
|
|
||||||
action: 'pruneAndSaveImportedGpx',
|
|
||||||
params: {
|
|
||||||
id: { gpx: intToGpxId(startTime.valueOf()) },
|
|
||||||
gpx: gpx,
|
|
||||||
tech: {
|
|
||||||
lastModified: new Date(file.lastModified).toISOString(),
|
|
||||||
importDate: new Date().toISOString(),
|
|
||||||
name: file.name,
|
|
||||||
size: file.size,
|
|
||||||
type: file.type,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
// 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={css.inputFile}
|
|
||||||
accept='.gpx'
|
|
||||||
onChange={onChangeHandler}
|
|
||||||
/>
|
|
||||||
<label htmlFor='gpx-import'>
|
|
||||||
<IonIcon slot='icon-only' icon={cloudUpload} title='import' />
|
|
||||||
</label>
|
|
||||||
</IonItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default GpxImport;
|
|
|
@ -1,78 +0,0 @@
|
||||||
import react, { useRef } from 'react';
|
|
||||||
|
|
||||||
import {
|
|
||||||
IonButton,
|
|
||||||
IonButtons,
|
|
||||||
IonContent,
|
|
||||||
IonIcon,
|
|
||||||
IonItem,
|
|
||||||
IonLabel,
|
|
||||||
IonList,
|
|
||||||
IonModal,
|
|
||||||
IonRadio,
|
|
||||||
IonRadioGroup,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import { layersOutline } from 'ionicons/icons';
|
|
||||||
import { nonFakeTileProviders } from '../map/tile-providers';
|
|
||||||
import { tileProviderAtom } from '../../App';
|
|
||||||
import i18n from '../../i18n/index';
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
|
|
||||||
import cssDialog from './dialogs.module.css';
|
|
||||||
|
|
||||||
export interface MapChooserProperties {}
|
|
||||||
|
|
||||||
export const MapChooser: react.FC<MapChooserProperties> = (
|
|
||||||
props: MapChooserProperties
|
|
||||||
) => {
|
|
||||||
const modal = useRef<HTMLIonModalElement>(null);
|
|
||||||
|
|
||||||
const dismiss = () => {
|
|
||||||
modal.current?.dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
const [tileProvider, setTileProvider] = useAtom(tileProviderAtom);
|
|
||||||
|
|
||||||
const changeHandler = (event: any) => {
|
|
||||||
setTileProvider(event.detail.value);
|
|
||||||
dismiss();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IonButton id='open-TileServerChooser'>
|
|
||||||
<IonIcon slot='icon-only' icon={layersOutline} />
|
|
||||||
</IonButton>
|
|
||||||
<IonModal
|
|
||||||
trigger='open-TileServerChooser'
|
|
||||||
ref={modal}
|
|
||||||
className={cssDialog.modal}
|
|
||||||
>
|
|
||||||
<IonToolbar>
|
|
||||||
<IonTitle>{i18n.mapChooser.chooseYourMap}</IonTitle>
|
|
||||||
<IonButtons slot='end'>
|
|
||||||
<IonButton onClick={() => dismiss()}>{i18n.common.close}</IonButton>
|
|
||||||
</IonButtons>
|
|
||||||
</IonToolbar>
|
|
||||||
<IonContent>
|
|
||||||
<IonList>
|
|
||||||
<IonRadioGroup value={tileProvider} onIonChange={changeHandler}>
|
|
||||||
{Object.keys(nonFakeTileProviders).map((provider) => {
|
|
||||||
return (
|
|
||||||
<IonItem key={provider}>
|
|
||||||
<IonLabel> {nonFakeTileProviders[provider].name}</IonLabel>
|
|
||||||
<IonRadio slot='start' value={provider} />
|
|
||||||
</IonItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</IonRadioGroup>
|
|
||||||
</IonList>
|
|
||||||
</IonContent>
|
|
||||||
</IonModal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapChooser;
|
|
|
@ -1,15 +0,0 @@
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal ion-toolbar {
|
|
||||||
--background: rgba(14, 116, 144, 0.7);
|
|
||||||
--color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal ion-content {
|
|
||||||
background-color: rgba(255, 255, 255, 0.7);
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { atom, useAtom } from 'jotai';
|
|
||||||
import react from 'react';
|
|
||||||
import Marker from './Marker';
|
|
||||||
import { geoPoint, Rectangle, TileKeyObject } from './types';
|
|
||||||
|
|
||||||
export interface CurrentLocationProperties {
|
|
||||||
keyObject?: TileKeyObject;
|
|
||||||
zoom?: number;
|
|
||||||
viewPort?: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialLocation: geoPoint | null = null;
|
|
||||||
export const locationAtom = atom<geoPoint | null>(initialLocation);
|
|
||||||
|
|
||||||
export const CurrentLocation: react.FC<CurrentLocationProperties> = (
|
|
||||||
props: CurrentLocationProperties
|
|
||||||
) => {
|
|
||||||
const [location] = useAtom(locationAtom);
|
|
||||||
console.log(
|
|
||||||
`Rendering CurrentLocation, location:${JSON.stringify(location)}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return location !== null ? (
|
|
||||||
<Marker
|
|
||||||
key='currentLocation'
|
|
||||||
id='currentLocation'
|
|
||||||
coordinates={location}
|
|
||||||
icon={
|
|
||||||
<circle
|
|
||||||
cx={0}
|
|
||||||
cy={0}
|
|
||||||
r={8}
|
|
||||||
fill='blue'
|
|
||||||
opacity='90%'
|
|
||||||
stroke='white'
|
|
||||||
strokeWidth={3}
|
|
||||||
strokeOpacity='100%'
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
keyObject={props.keyObject}
|
|
||||||
zoom={props.zoom}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
/>
|
|
||||||
) : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CurrentLocation;
|
|
|
@ -1,47 +0,0 @@
|
||||||
import react, { startTransition, useEffect, useState } from 'react';
|
|
||||||
import dispatch from '../../workers/dispatcher-main';
|
|
||||||
import Trk from './Trk';
|
|
||||||
import { TileKeyObject, Rectangle } from './types';
|
|
||||||
import getUri from '../../lib/ids';
|
|
||||||
|
|
||||||
export interface GpxProperties {
|
|
||||||
id: string;
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
zoom: number;
|
|
||||||
viewPort: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Gpx: react.FC<GpxProperties> = (props: GpxProperties) => {
|
|
||||||
const [gpx, setGpx] = useState<any>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const getGpx = async () => {
|
|
||||||
const gpx = await dispatch({
|
|
||||||
action: 'getGpx',
|
|
||||||
params: {
|
|
||||||
id: props.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(`<Gpx>, gpx: ${JSON.stringify(gpx)}`);
|
|
||||||
startTransition(() => setGpx(gpx));
|
|
||||||
};
|
|
||||||
getGpx();
|
|
||||||
}, [props.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g>
|
|
||||||
{gpx
|
|
||||||
.filter((row: any) => row.doc.type === 'trk')
|
|
||||||
.map((row: any) => (
|
|
||||||
<Trk
|
|
||||||
key={row.id}
|
|
||||||
id={row.id}
|
|
||||||
keyObject={props.keyObject}
|
|
||||||
zoom={props.zoom}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Gpx;
|
|
|
@ -1,51 +0,0 @@
|
||||||
import react, { startTransition, useEffect, useState } from 'react';
|
|
||||||
import dispatch from '../../workers/dispatcher-main';
|
|
||||||
import Gpx from './Gpx';
|
|
||||||
import { TileKeyObject, Rectangle } from './types';
|
|
||||||
|
|
||||||
export interface GpxesProperties {
|
|
||||||
keyObject?: TileKeyObject;
|
|
||||||
zoom?: number;
|
|
||||||
viewPort?: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Gpxes: react.FC<GpxesProperties> = (props: GpxesProperties) => {
|
|
||||||
const [visibleGpxes, setVisibleGpxes] = useState<any>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const getVisibleGpxes = async () => {
|
|
||||||
const gpxes = await dispatch({
|
|
||||||
action: 'getGpxesForViewport',
|
|
||||||
params: {
|
|
||||||
viewport: props.viewPort,
|
|
||||||
zoomLevel: props.keyObject?.zoomLevel,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(`Gpxes, visibles: ${JSON.stringify(gpxes)}`);
|
|
||||||
startTransition(() => setVisibleGpxes(gpxes));
|
|
||||||
};
|
|
||||||
if (props.viewPort !== undefined) {
|
|
||||||
getVisibleGpxes();
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
props.viewPort?.bottomRight.x,
|
|
||||||
props.viewPort?.bottomRight.y,
|
|
||||||
props.viewPort?.topLeft.x,
|
|
||||||
props.viewPort?.topLeft.y,
|
|
||||||
props.keyObject?.zoomLevel,
|
|
||||||
]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{visibleGpxes.map((id: string) => (
|
|
||||||
<Gpx
|
|
||||||
key={id}
|
|
||||||
id={id}
|
|
||||||
keyObject={props.keyObject!}
|
|
||||||
zoom={props.zoom!}
|
|
||||||
viewPort={props.viewPort!}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Gpxes;
|
|
|
@ -1,5 +0,0 @@
|
||||||
.handler {
|
|
||||||
position: fixed;
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
|
@ -1,243 +0,0 @@
|
||||||
import { createEvent, fireEvent, render, screen } from '@testing-library/react';
|
|
||||||
import { Handlers } from './Handlers';
|
|
||||||
import { Transformation } from './LiveMap';
|
|
||||||
import { Point } from './types';
|
|
||||||
describe('The Handlers component ', () => {
|
|
||||||
test('is just an empty div', () => {
|
|
||||||
const transformMap = (t: Transformation) => {};
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
// screen.debug();
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
// screen.debug();
|
|
||||||
expect(handlers).toMatchInlineSnapshot(`
|
|
||||||
<div
|
|
||||||
class="handler"
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
/* test('handle mouseDown / mouseMove events', () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseDown(handlers, {
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toMatchInlineSnapshot(`
|
|
||||||
Arguments [
|
|
||||||
Object {
|
|
||||||
"x": 10,
|
|
||||||
"y": 30,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('does *not* handle mouseMove events not preceded by a mouseDown', () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('A mouseUp event disable further mouseMove events', () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseDown(handlers, {
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseUp(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('A mouseLeave event disable further mouseMove events', () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(handlers, createEvent.mouseOver(handlers));
|
|
||||||
fireEvent(handlers, createEvent.mouseEnter(handlers));
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseDown(handlers, {
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(handlers, createEvent.mouseOut(handlers));
|
|
||||||
fireEvent(handlers, createEvent.mouseLeave(handlers));
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throttle mouseMove events', () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseDown(handlers, {
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 30,
|
|
||||||
clientY: 60,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toMatchInlineSnapshot(`
|
|
||||||
Arguments [
|
|
||||||
Object {
|
|
||||||
"x": 10,
|
|
||||||
"y": 30,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('throttle mouseMove events', async () => {
|
|
||||||
var transformMapParams: any;
|
|
||||||
function transformMap(
|
|
||||||
deltaShift: Point | null,
|
|
||||||
deltaZoom: number | null,
|
|
||||||
zoomCenter: Point | null
|
|
||||||
) {
|
|
||||||
console.log(`transformMap${JSON.stringify(arguments)}`);
|
|
||||||
transformMapParams = arguments;
|
|
||||||
}
|
|
||||||
render(<Handlers transformMap={transformMap} />);
|
|
||||||
const handlers = screen.getByRole('presentation');
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseDown(handlers, {
|
|
||||||
clientX: 10,
|
|
||||||
clientY: 20,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 20,
|
|
||||||
clientY: 50,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await new Promise((r) => setTimeout(r, 500));
|
|
||||||
fireEvent(
|
|
||||||
handlers,
|
|
||||||
createEvent.mouseMove(handlers, {
|
|
||||||
clientX: 5,
|
|
||||||
clientY: 5,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(transformMapParams).toMatchInlineSnapshot(`
|
|
||||||
Arguments [
|
|
||||||
Object {
|
|
||||||
"x": -15,
|
|
||||||
"y": -45,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
}); */
|
|
||||||
});
|
|
|
@ -1,327 +0,0 @@
|
||||||
import react, { useRef } from 'react';
|
|
||||||
|
|
||||||
import { Point } from './types';
|
|
||||||
import './Handler.css';
|
|
||||||
import { handlersConfig } from './config';
|
|
||||||
import { Transformation } from './LiveMap';
|
|
||||||
import { isOpenAtom } from '../dialogs/Explorer';
|
|
||||||
import cache from '../../lib/cache';
|
|
||||||
import { useAtom } from 'jotai';
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export interface HandlersProperties {
|
|
||||||
/** The transformation to apply on the `<LiveMap>` parent */
|
|
||||||
transformMap: (t: Transformation) => void;
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param props
|
|
||||||
* @returns A div with the following handlers
|
|
||||||
* * mouseLeave, mouseDown and mouseUp to track the mouse state
|
|
||||||
* * mouseMove to shift the map if the mouse is down
|
|
||||||
* * doubleClick to zoom in
|
|
||||||
* * wheel to zoom in and out
|
|
||||||
* * touchStart, touchEnd and touchCancel to track touch state
|
|
||||||
* * touchMove to shift the map (single finger) or shift and zoom (two fingers).
|
|
||||||
*
|
|
||||||
* Communication with the parent `<LiveMap>` is done through the transformMap {@link components/map/LiveMap!Transformation} property.
|
|
||||||
*/
|
|
||||||
export const Handlers: react.FC<HandlersProperties> = (
|
|
||||||
props: HandlersProperties
|
|
||||||
) => {
|
|
||||||
const genericHandler = (event: any) => {
|
|
||||||
// console.log(`Log - Event: ${event.type}`);
|
|
||||||
// if (event.clientX !== undefined) {
|
|
||||||
// console.log(
|
|
||||||
// `Mouse : ${event.clientX}, ${event.clientY}, target: ${event.target}`
|
|
||||||
// );
|
|
||||||
// console.log(
|
|
||||||
// `mouseState: ' ${JSON.stringify(mouseState)} (+${
|
|
||||||
// Date.now() - mouseState.timestamp
|
|
||||||
// }ms) `
|
|
||||||
// );
|
|
||||||
// return;
|
|
||||||
//}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Mouse handlers
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
const initialMouseState = {
|
|
||||||
down: false,
|
|
||||||
starting: { x: -1, y: -1 },
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseState = useRef(initialMouseState);
|
|
||||||
|
|
||||||
const mouseLeaveHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
mouseState.current = initialMouseState;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseDownHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
mouseState.current = {
|
|
||||||
down: true,
|
|
||||||
starting: { x: event.clientX, y: event.clientY },
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseUpHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
mouseState.current = initialMouseState;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mouseMoveHandler = (event: any) => {
|
|
||||||
if (
|
|
||||||
mouseState.current.down &&
|
|
||||||
Date.now() - mouseState.current.timestamp >
|
|
||||||
handlersConfig.mouseMoveThrottleDelay /* &&
|
|
||||||
(event.clientX - mouseState.current.starting.x) ** 2 +
|
|
||||||
(event.clientY - mouseState.current.starting.y) ** 2 >
|
|
||||||
100 */
|
|
||||||
) {
|
|
||||||
genericHandler(event);
|
|
||||||
if (mouseState.current.down) {
|
|
||||||
props.transformMap({
|
|
||||||
deltaShift: {
|
|
||||||
x: event.clientX - mouseState.current.starting.x,
|
|
||||||
y: event.clientY - mouseState.current.starting.y,
|
|
||||||
},
|
|
||||||
deltaZoom: null,
|
|
||||||
zoomCenter: null,
|
|
||||||
});
|
|
||||||
mouseState.current = {
|
|
||||||
down: true,
|
|
||||||
starting: {
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
},
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Double click
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const doubleClickHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
props.transformMap({
|
|
||||||
deltaShift: null,
|
|
||||||
deltaZoom: Math.SQRT2,
|
|
||||||
zoomCenter: {
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Wheel handler
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
const initialWheelState = {
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wheelState = useRef(initialWheelState);
|
|
||||||
|
|
||||||
const wheelEventHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
if (
|
|
||||||
event.deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
|
|
||||||
Date.now() - wheelState.current.timestamp >
|
|
||||||
handlersConfig.wheelThrottleDelay
|
|
||||||
) {
|
|
||||||
props.transformMap({
|
|
||||||
deltaShift: null,
|
|
||||||
deltaZoom: event.deltaY < 0 ? Math.SQRT2 : Math.SQRT1_2,
|
|
||||||
zoomCenter: {
|
|
||||||
x: event.clientX,
|
|
||||||
y: event.clientY,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
wheelState.current = {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* Touch handlers
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
interface TouchState {
|
|
||||||
state: 'up' | 'pointer' | 'double';
|
|
||||||
touches: Point[];
|
|
||||||
distance: number;
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialTouchState: TouchState = {
|
|
||||||
state: 'up',
|
|
||||||
touches: [],
|
|
||||||
distance: -1,
|
|
||||||
timestamp: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchState = useRef<TouchState>(initialTouchState);
|
|
||||||
|
|
||||||
const touchCancelHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
touchState.current = initialTouchState;
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchEndHandler = touchCancelHandler;
|
|
||||||
|
|
||||||
const touchStartHandler = (event: any) => {
|
|
||||||
genericHandler(event);
|
|
||||||
if (event.touches.length === 2) {
|
|
||||||
touchState.current = {
|
|
||||||
state: 'double',
|
|
||||||
touches: [
|
|
||||||
{ x: event.touches[0].screenX, y: event.touches[0].screenY },
|
|
||||||
{ x: event.touches[1].screenX, y: event.touches[1].screenY },
|
|
||||||
],
|
|
||||||
distance: Math.sqrt(
|
|
||||||
(event.touches[0].screenX - event.touches[1].screenX) ** 2 +
|
|
||||||
(event.touches[0].screenY - event.touches[1].screenY) ** 2
|
|
||||||
),
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
} else if (event.touches.length === 1) {
|
|
||||||
touchState.current = {
|
|
||||||
state: 'pointer',
|
|
||||||
touches: [{ x: event.touches[0].screenX, y: event.touches[0].screenY }],
|
|
||||||
timestamp: Date.now(),
|
|
||||||
distance: -1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const touchMoveHandler = (event: any) => {
|
|
||||||
if (
|
|
||||||
touchState.current.state === 'double' &&
|
|
||||||
Date.now() - touchState.current.timestamp >
|
|
||||||
handlersConfig.doubleTouchMoveThrottleDelay
|
|
||||||
) {
|
|
||||||
if (event.touches.length === 2) {
|
|
||||||
genericHandler(event);
|
|
||||||
const newDistance = Math.sqrt(
|
|
||||||
(event.touches[0].screenX - event.touches[1].screenX) ** 2 +
|
|
||||||
(event.touches[0].screenY - event.touches[1].screenY) ** 2
|
|
||||||
);
|
|
||||||
const factor = newDistance / touchState.current.distance;
|
|
||||||
// console.log(`+++++++++ ZOOM Factor is ${factor} ++++++++++`);
|
|
||||||
const previousCenter = {
|
|
||||||
x:
|
|
||||||
(touchState.current.touches[0].x +
|
|
||||||
touchState.current.touches[1].x) /
|
|
||||||
2,
|
|
||||||
y:
|
|
||||||
(touchState.current.touches[0].y +
|
|
||||||
touchState.current.touches[1].y) /
|
|
||||||
2,
|
|
||||||
};
|
|
||||||
const currentCenter = {
|
|
||||||
x: (event.touches[0].screenX + event.touches[1].screenX) / 2,
|
|
||||||
y: (event.touches[0].screenY + event.touches[1].screenY) / 2,
|
|
||||||
};
|
|
||||||
props.transformMap({
|
|
||||||
deltaShift: {
|
|
||||||
x: currentCenter.x - previousCenter.x,
|
|
||||||
y: currentCenter.y - previousCenter.y,
|
|
||||||
},
|
|
||||||
deltaZoom: factor,
|
|
||||||
zoomCenter: {
|
|
||||||
x: (currentCenter.x + previousCenter.x) / 2,
|
|
||||||
y: (currentCenter.y + previousCenter.y) / 2,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
touchState.current = {
|
|
||||||
state: 'double',
|
|
||||||
touches: [
|
|
||||||
{ x: event.touches[0].screenX, y: event.touches[0].screenY },
|
|
||||||
{ x: event.touches[1].screenX, y: event.touches[1].screenY },
|
|
||||||
],
|
|
||||||
distance: newDistance,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
touchState.current.state === 'pointer' &&
|
|
||||||
Date.now() - touchState.current.timestamp >
|
|
||||||
handlersConfig.singleTouchMoveThrottleDelay
|
|
||||||
) {
|
|
||||||
if (event.touches.length === 1) {
|
|
||||||
genericHandler(event);
|
|
||||||
props.transformMap({
|
|
||||||
deltaShift: {
|
|
||||||
x: event.touches[0].screenX - touchState.current.touches[0].x,
|
|
||||||
y: event.touches[0].screenY - touchState.current.touches[0].y,
|
|
||||||
},
|
|
||||||
deltaZoom: null,
|
|
||||||
zoomCenter: null,
|
|
||||||
});
|
|
||||||
touchState.current = {
|
|
||||||
state: 'pointer',
|
|
||||||
touches: [
|
|
||||||
{
|
|
||||||
x: event.touches[0].screenX,
|
|
||||||
y: event.touches[0].screenY,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
timestamp: Date.now(),
|
|
||||||
distance: -1,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const [, setIsOpen] = useAtom(isOpenAtom);
|
|
||||||
|
|
||||||
const contextMenuHandler = (event: any) => {
|
|
||||||
console.log(event);
|
|
||||||
event.preventDefault();
|
|
||||||
console.log(cache.map({ cacheId: 'points' }));
|
|
||||||
|
|
||||||
setIsOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className='handler'
|
|
||||||
role='presentation'
|
|
||||||
onMouseDown={mouseDownHandler}
|
|
||||||
onMouseMove={mouseMoveHandler}
|
|
||||||
onMouseUp={mouseUpHandler}
|
|
||||||
onMouseLeave={mouseLeaveHandler}
|
|
||||||
onDoubleClick={doubleClickHandler}
|
|
||||||
onWheel={wheelEventHandler}
|
|
||||||
onTouchEnd={touchEndHandler}
|
|
||||||
onTouchCancel={touchCancelHandler}
|
|
||||||
onTouchStart={touchStartHandler}
|
|
||||||
onTouchMove={touchMoveHandler}
|
|
||||||
onContextMenu={contextMenuHandler}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Handlers;
|
|
|
@ -1,241 +0,0 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import LayerStack from './LayerStack';
|
|
||||||
import { CoordinateSystem } from './LiveMap';
|
|
||||||
|
|
||||||
describe('The LayerStack component', () => {
|
|
||||||
const coordinateSystem:CoordinateSystem= {
|
|
||||||
shift:{x:0,y:0},
|
|
||||||
zoom:1,
|
|
||||||
}
|
|
||||||
test('generates four empty layers and a populated one', () => {
|
|
||||||
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
|
|
||||||
render(
|
|
||||||
<LayerStack
|
|
||||||
numberOfTiledLayers={5}
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
|
|
||||||
coordinateSystem= {coordinateSystem}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const svg = screen.getByTestId('layer-stack');
|
|
||||||
expect(svg).toMatchInlineSnapshot(`
|
|
||||||
<svg
|
|
||||||
data-testid="layer-stack"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="translate(0, 0) scale(1)"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="scale(1024) translate(-194.25, -83.25)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(512) translate(-388.5, -166.5)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(64) translate(-3108, -1332)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(128) translate(-1554, -666)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(256) translate(-777, -333)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates two empty layers and a populated one', () => {
|
|
||||||
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
|
|
||||||
render(
|
|
||||||
<LayerStack
|
|
||||||
numberOfTiledLayers={3}
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
|
|
||||||
coordinateSystem= {coordinateSystem}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const svg = screen.getByTestId('layer-stack');
|
|
||||||
expect(svg).toMatchInlineSnapshot(`
|
|
||||||
<svg
|
|
||||||
data-testid="layer-stack"
|
|
||||||
height="100%"
|
|
||||||
width="100%"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="translate(0, 0) scale(1)"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="scale(512) translate(-388.5, -166.5)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(128) translate(-1554, -666)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(256) translate(-777, -333)"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* test('populates a new layer when zoomed in', () => {
|
|
||||||
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
|
|
||||||
render(
|
|
||||||
<LayerStack
|
|
||||||
numberOfTiledLayers={3}
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 9, x: 777, y: 333 }}
|
|
||||||
coordinateSystem= {coordinateSystem}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
const { result } = renderHook(() => [
|
|
||||||
useAtom(coordinateSystemAtom),
|
|
||||||
useAtom(relativeCoordinateSystemAtom),
|
|
||||||
]);
|
|
||||||
act(() => {
|
|
||||||
result.current[0][1]({
|
|
||||||
shift: {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
},
|
|
||||||
zoom: 1,
|
|
||||||
} as any);
|
|
||||||
result.current[1][1]({
|
|
||||||
deltaShift: null,
|
|
||||||
zoomCenter: { x: 0, y: 0 },
|
|
||||||
deltaZoom: 2,
|
|
||||||
} as any);
|
|
||||||
});
|
|
||||||
const svg = screen.getByTestId('layer-stack');
|
|
||||||
expect(svg).toMatchInlineSnapshot(`
|
|
||||||
<svg
|
|
||||||
data-testid="layer-stack"
|
|
||||||
height="768"
|
|
||||||
width="1024"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="translate(0, 0) scale(2)"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="scale(128) translate(-388.5, -166.5)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(512) translate(-1554, -666)"
|
|
||||||
/>
|
|
||||||
<g
|
|
||||||
transform="scale(256) translate(-777, -333)"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="translate(777, 333)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/265/333.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(778, 333)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/266/333.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(779, 333)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/267/333.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(780, 333)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/268/333.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(777, 334)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/265/334.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(778, 334)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/266/334.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(779, 334)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/267/334.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(780, 334)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/268/334.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(777, 335)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/265/335.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(778, 335)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/266/335.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(779, 335)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/267/335.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(780, 335)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/9/268/335.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
}); */
|
|
||||||
});
|
|
|
@ -1,146 +0,0 @@
|
||||||
import react, {
|
|
||||||
cloneElement,
|
|
||||||
JSXElementConstructor,
|
|
||||||
ReactComponentElement,
|
|
||||||
ReactElement,
|
|
||||||
ReactNode,
|
|
||||||
useRef,
|
|
||||||
} from 'react';
|
|
||||||
|
|
||||||
import { Rectangle, TileKeyObject } from './types';
|
|
||||||
|
|
||||||
import { CoordinateSystem } from './LiveMap';
|
|
||||||
import { range } from 'lodash';
|
|
||||||
import tileUri from './uris';
|
|
||||||
import TiledLayer from './TiledLayer';
|
|
||||||
import { ReactComponentOrElement } from '@ionic/react';
|
|
||||||
import { KeyObject } from 'crypto';
|
|
||||||
import useViewport from './use-viewport';
|
|
||||||
|
|
||||||
export interface LayerStackProperties {
|
|
||||||
/**
|
|
||||||
* A key identifying the top left tile
|
|
||||||
*/
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
/**
|
|
||||||
* Number of {@link components/map/TiledLayer!TiledLayer}.
|
|
||||||
*/
|
|
||||||
numberOfTiledLayers?: number;
|
|
||||||
/**
|
|
||||||
* The coordinates system
|
|
||||||
*/
|
|
||||||
coordinateSystem: CoordinateSystem;
|
|
||||||
/** Slippy graphics are non scalable SVG snippets defined by geo locations */
|
|
||||||
slippyGraphics?: ReactElement<any, string | JSXElementConstructor<any>>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param props
|
|
||||||
* @returns A stack of layers embedded in an SVG element
|
|
||||||
*
|
|
||||||
* This component does the conversion between the {@link components/map/LiveMap!CoordinateSystem} and
|
|
||||||
* the `<`{@link components/map/TiledLayer!TiledLayer}`>`
|
|
||||||
* components which units are in tiles.
|
|
||||||
*
|
|
||||||
* When more then one {@link components/map/TiledLayer!TiledLayer} is required, the tiled layer identified by the keyObject is considered active and new tiles are added
|
|
||||||
* as needed and the other layers are used as backups while the tiles are loading.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const LayerStack: react.FC<LayerStackProperties> = (
|
|
||||||
props: LayerStackProperties
|
|
||||||
) => {
|
|
||||||
console.log(
|
|
||||||
`LayerStack rendering, coordinateSystem: ${JSON.stringify(
|
|
||||||
props.coordinateSystem
|
|
||||||
)}, slippyGraphics: ${props.slippyGraphics}`
|
|
||||||
);
|
|
||||||
const g = useRef<SVGGElement>(null);
|
|
||||||
|
|
||||||
const viewPort = useViewport({
|
|
||||||
coordinateSystem: props.coordinateSystem,
|
|
||||||
keyObject: props.keyObject,
|
|
||||||
svgElement: g,
|
|
||||||
});
|
|
||||||
|
|
||||||
const numberOfTiledLayers =
|
|
||||||
props.numberOfTiledLayers === undefined ? 1 : props.numberOfTiledLayers;
|
|
||||||
|
|
||||||
const activeTiledLayer = Math.floor(numberOfTiledLayers / 2);
|
|
||||||
|
|
||||||
const getTiledLayer = (i: number, viewPort?: Rectangle) => {
|
|
||||||
const relativeZoomLevel = i - activeTiledLayer;
|
|
||||||
const zoom = 2 ** relativeZoomLevel;
|
|
||||||
const origin = {
|
|
||||||
x: props.keyObject.x * zoom,
|
|
||||||
y: props.keyObject.y * zoom,
|
|
||||||
};
|
|
||||||
const keyObject = {
|
|
||||||
provider: props.keyObject.provider,
|
|
||||||
zoomLevel: props.keyObject.zoomLevel + relativeZoomLevel,
|
|
||||||
x: Math.floor(origin.x),
|
|
||||||
y: Math.floor(origin.y),
|
|
||||||
};
|
|
||||||
const shift = {
|
|
||||||
x: -origin.x,
|
|
||||||
y: -origin.y,
|
|
||||||
};
|
|
||||||
const key = tileUri({
|
|
||||||
provider: keyObject.provider,
|
|
||||||
zoomLevel: keyObject.zoomLevel,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TiledLayer
|
|
||||||
key={key}
|
|
||||||
keyObject={keyObject}
|
|
||||||
shift={shift}
|
|
||||||
zoom={256 / zoom}
|
|
||||||
coordinateSystem={props.coordinateSystem}
|
|
||||||
viewPort={viewPort}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
// console.log(`tiledLayers: ${JSON.stringify(tiledLayers)}`);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<svg width='100%' height='100%' data-testid='layer-stack'>
|
|
||||||
<g
|
|
||||||
transform={`translate(${props.coordinateSystem.shift.x}, ${props.coordinateSystem.shift.y}) scale(${props.coordinateSystem.zoom})`}
|
|
||||||
key='tiles'
|
|
||||||
ref={g}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
// Tiled layers with less detail
|
|
||||||
range(0, activeTiledLayer).map((index) => getTiledLayer(index))
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// Tiled layers with more details
|
|
||||||
range(numberOfTiledLayers - 1, activeTiledLayer, -1).map((index) =>
|
|
||||||
getTiledLayer(index)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
// And the active one
|
|
||||||
getTiledLayer(activeTiledLayer, viewPort)
|
|
||||||
}
|
|
||||||
{props.slippyGraphics !== undefined ? (
|
|
||||||
// Slippy graphics (if needed)
|
|
||||||
<>
|
|
||||||
{props.slippyGraphics.map((slippyGraphic) =>
|
|
||||||
cloneElement(slippyGraphic, {
|
|
||||||
keyObject: props.keyObject,
|
|
||||||
zoom: props.coordinateSystem.zoom,
|
|
||||||
viewPort: viewPort,
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
{console.log('LayerStack: adding slippyGraphics')},
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LayerStack;
|
|
|
@ -1,155 +0,0 @@
|
||||||
import react, {
|
|
||||||
JSXElementConstructor,
|
|
||||||
ReactElement,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import useDimensions from 'react-cool-dimensions';
|
|
||||||
|
|
||||||
import { MapScope, Point } from './types';
|
|
||||||
import Map from './Map';
|
|
||||||
import Handlers from './Handlers';
|
|
||||||
import { tileProviders } from './tile-providers';
|
|
||||||
import { lon2tile, lat2tile, tile2lat, tile2long } from '../../lib/geo';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Definition of a coordinate system
|
|
||||||
*
|
|
||||||
* The coordinate system is shifted and zoomed (from the viewport origin)
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export interface CoordinateSystem {
|
|
||||||
/** Zoom relative to the origin */
|
|
||||||
zoom: number;
|
|
||||||
/** Origin's shift (in pixels) */
|
|
||||||
shift: Point;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Description of coordinates system transformation
|
|
||||||
*/
|
|
||||||
export interface Transformation {
|
|
||||||
/** New translation to apply */
|
|
||||||
deltaShift: Point | null;
|
|
||||||
/** Zoom factor to apply */
|
|
||||||
deltaZoom: number | null;
|
|
||||||
/** Center of the new zoom to apply */
|
|
||||||
zoomCenter: Point | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LiveMapProperties {
|
|
||||||
/** The initial map's scope */
|
|
||||||
scope: MapScope;
|
|
||||||
/** The number of tiled layers (default to 1) */
|
|
||||||
numberOfTiledLayers?: number;
|
|
||||||
/** If provided, a function to call when the scope is updated. */
|
|
||||||
setScope?: (scope: MapScope) => void;
|
|
||||||
/** Slippy graphics are non scalable SVG snippets defined by geo locations */
|
|
||||||
slippyGraphics?: ReactElement<any, string | JSXElementConstructor<any>>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param props
|
|
||||||
* @returns A `<LiveMap>` component.
|
|
||||||
*
|
|
||||||
* A `<LiveMap>` is a wrapper around a {@link components/map/Map!Map} component which updates the `<Map>`'s scope according to user's mouse, wheel and touch events.
|
|
||||||
*
|
|
||||||
* To do so, `<LiveMap>` embeds a `<Map>` component together with a {@link components/map/Handlers!Handlers} component which listens to user's event.
|
|
||||||
*
|
|
||||||
* The main task of `<LiveMap>` components is thus to translate {@link Transformation}s delivered by `<Handler>` in pixels into geographical coordinates.
|
|
||||||
*/
|
|
||||||
export const LiveMap: react.FC<LiveMapProperties> = (
|
|
||||||
props: LiveMapProperties
|
|
||||||
) => {
|
|
||||||
const [scope, setScope] = useState(props.scope);
|
|
||||||
useEffect(() => {
|
|
||||||
setScope(props.scope);
|
|
||||||
}, [props.scope]);
|
|
||||||
console.log(
|
|
||||||
`LiveMap rendering, scope: ${JSON.stringify(scope)}, slippyGraphics: ${
|
|
||||||
props.slippyGraphics
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
const { observe, width, height } = useDimensions<HTMLDivElement>();
|
|
||||||
const transform = (t: Transformation) => {
|
|
||||||
const deltaZoom = t.deltaZoom === null ? 1 : t.deltaZoom;
|
|
||||||
const deltaZoomLevel = Math.log2(deltaZoom);
|
|
||||||
|
|
||||||
const tileProvider = tileProviders[scope.tileProvider];
|
|
||||||
|
|
||||||
const tilesZoom = Math.min(
|
|
||||||
Math.max(Math.round(scope.zoom), tileProvider.minZoom),
|
|
||||||
tileProvider.maxZoom
|
|
||||||
);
|
|
||||||
const softZoom = scope.zoom - tilesZoom;
|
|
||||||
const relativeScale = 2 ** softZoom;
|
|
||||||
const visibleTileSize = tileProvider.tileSize * relativeScale;
|
|
||||||
|
|
||||||
// Values in pixels
|
|
||||||
const actualDeltaShift =
|
|
||||||
t.deltaShift === null ? { x: 0, y: 0 } : t.deltaShift;
|
|
||||||
const actualZoomCenter =
|
|
||||||
t.zoomCenter === null ? { x: 0, y: 0 } : t.zoomCenter;
|
|
||||||
|
|
||||||
// Values in tiles (for the current zoom level)
|
|
||||||
const actualDeltaShiftTiles = {
|
|
||||||
x: actualDeltaShift.x / visibleTileSize,
|
|
||||||
y: actualDeltaShift.y / visibleTileSize,
|
|
||||||
};
|
|
||||||
const actualZoomCenterTiles = {
|
|
||||||
x: actualZoomCenter.x / visibleTileSize,
|
|
||||||
y: actualZoomCenter.y / visibleTileSize,
|
|
||||||
};
|
|
||||||
const tilesCenter: Point = {
|
|
||||||
x: lon2tile(scope.center.lon, tilesZoom),
|
|
||||||
y: lat2tile(scope.center.lat, tilesZoom),
|
|
||||||
};
|
|
||||||
|
|
||||||
const newTilesCenter = {
|
|
||||||
x:
|
|
||||||
tilesCenter.x -
|
|
||||||
actualDeltaShiftTiles.x -
|
|
||||||
(width / 2 / visibleTileSize - actualZoomCenterTiles.x) *
|
|
||||||
(deltaZoom - 1),
|
|
||||||
y:
|
|
||||||
tilesCenter.y -
|
|
||||||
actualDeltaShiftTiles.y -
|
|
||||||
(height / 2 / visibleTileSize - actualZoomCenterTiles.y) *
|
|
||||||
(deltaZoom - 1),
|
|
||||||
};
|
|
||||||
|
|
||||||
const newScope: MapScope = {
|
|
||||||
center: {
|
|
||||||
lat: tile2lat(newTilesCenter.y, tilesZoom),
|
|
||||||
lon: tile2long(newTilesCenter.x, tilesZoom),
|
|
||||||
},
|
|
||||||
zoom: deltaZoomLevel + scope.zoom,
|
|
||||||
tileProvider: scope.tileProvider,
|
|
||||||
};
|
|
||||||
// console.log(
|
|
||||||
// `LiveMap transform: ${JSON.stringify(t)}, ${JSON.stringify(
|
|
||||||
// scope
|
|
||||||
// )} -> ${JSON.stringify(newScope)}, delta lat: ${
|
|
||||||
// newScope.center.lat - scope.center.lat
|
|
||||||
// }, delta lon: ${newScope.center.lon - scope.center.lon}`
|
|
||||||
// );
|
|
||||||
setScope(newScope);
|
|
||||||
if (props.setScope !== undefined) {
|
|
||||||
props.setScope(newScope);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '100%', height: '100%' }} ref={observe}>
|
|
||||||
<Handlers transformMap={transform} />
|
|
||||||
<Map
|
|
||||||
scope={scope}
|
|
||||||
numberOfTiledLayers={props.numberOfTiledLayers}
|
|
||||||
slippyGraphics={props.slippyGraphics}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LiveMap;
|
|
|
@ -1,77 +0,0 @@
|
||||||
import react, { JSXElementConstructor, ReactElement, ReactNode } from 'react';
|
|
||||||
import useDimensions from 'react-cool-dimensions';
|
|
||||||
|
|
||||||
import { Point, MapScope } from './types';
|
|
||||||
import Marker from './Marker';
|
|
||||||
|
|
||||||
import LayerStack from './LayerStack';
|
|
||||||
import { tileProviders } from './tile-providers';
|
|
||||||
import { lon2tile, lat2tile } from '../../lib/geo';
|
|
||||||
|
|
||||||
export interface MapProperties {
|
|
||||||
scope: MapScope;
|
|
||||||
numberOfTiledLayers?: number;
|
|
||||||
/** Slippy graphics are non scalable SVG snippets defined by geo locations */
|
|
||||||
slippyGraphics?: ReactElement<any, string | JSXElementConstructor<any>>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @returns A `<Map>` component
|
|
||||||
*
|
|
||||||
* `<Map>` components display the map specified by their {@link MapProperties}'s scope.
|
|
||||||
*
|
|
||||||
* They can be driven by {@link components/map/LiveMap!LiveMap} component to react to user's event.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const Map: react.FC<MapProperties> = (props: MapProperties) => {
|
|
||||||
console.log(
|
|
||||||
`Map rendering, scope: ${JSON.stringify(props.scope)}, slippyGraphics: ${
|
|
||||||
props.slippyGraphics
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
|
|
||||||
const { observe, width, height } = useDimensions<HTMLDivElement>();
|
|
||||||
|
|
||||||
const tileProvider = tileProviders[props.scope.tileProvider];
|
|
||||||
|
|
||||||
const tilesZoom = Math.min(
|
|
||||||
Math.max(Math.round(props.scope.zoom), tileProvider.minZoom),
|
|
||||||
tileProvider.maxZoom
|
|
||||||
);
|
|
||||||
const tilesCenter: Point = {
|
|
||||||
x: lon2tile(props.scope.center.lon, tilesZoom),
|
|
||||||
y: lat2tile(props.scope.center.lat, tilesZoom),
|
|
||||||
};
|
|
||||||
const softZoom = props.scope.zoom - tilesZoom;
|
|
||||||
const relativeScale = 2 ** softZoom;
|
|
||||||
const visibleTileSize = tileProvider.tileSize * relativeScale;
|
|
||||||
const nbTilesLeft = width / 2 / visibleTileSize;
|
|
||||||
const nbTilesTop = height / 2 / visibleTileSize;
|
|
||||||
const firstTileLeft = Math.floor(tilesCenter.x - nbTilesLeft);
|
|
||||||
const firstTileTop = Math.floor(tilesCenter.y - nbTilesTop);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={{ width: '100%', height: '100%' }} ref={observe}>
|
|
||||||
<LayerStack
|
|
||||||
numberOfTiledLayers={props.numberOfTiledLayers}
|
|
||||||
keyObject={{
|
|
||||||
provider: props.scope.tileProvider,
|
|
||||||
zoomLevel: tilesZoom,
|
|
||||||
x: firstTileLeft,
|
|
||||||
y: firstTileTop,
|
|
||||||
}}
|
|
||||||
coordinateSystem={{
|
|
||||||
shift: {
|
|
||||||
x: -((tilesCenter.x - nbTilesLeft) % 1) * visibleTileSize,
|
|
||||||
y: -((tilesCenter.y - nbTilesTop) % 1) * visibleTileSize,
|
|
||||||
},
|
|
||||||
zoom: relativeScale,
|
|
||||||
}}
|
|
||||||
slippyGraphics={props.slippyGraphics}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Map;
|
|
|
@ -1,72 +0,0 @@
|
||||||
import react, { ReactNode, useEffect } from 'react';
|
|
||||||
import { lat2tile, lon2tile } from '../../lib/geo';
|
|
||||||
import cache from '../../lib/cache';
|
|
||||||
import { geoPoint, Rectangle, TileKeyObject } from './types';
|
|
||||||
|
|
||||||
export interface MarkerProperties {
|
|
||||||
id: string;
|
|
||||||
coordinates: geoPoint;
|
|
||||||
icon: ReactNode;
|
|
||||||
keyObject?: TileKeyObject;
|
|
||||||
zoom?: number;
|
|
||||||
viewPort?: Rectangle;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Marker: react.FC<MarkerProperties> = (props: MarkerProperties) => {
|
|
||||||
console.log(`Rendering Marker`);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(`Marker onMount, id:${props.id}`);
|
|
||||||
return () => {
|
|
||||||
console.log(`Marker onUnmount, id:${props.id}`);
|
|
||||||
cache.delete({ cacheId: 'points', key: props.id });
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (
|
|
||||||
props.keyObject === undefined ||
|
|
||||||
props.zoom === undefined ||
|
|
||||||
props.viewPort === undefined
|
|
||||||
) {
|
|
||||||
console.log(`Marker props undefined: ${JSON.stringify(props)} `);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const x0 = lon2tile(props.coordinates.lon, 0);
|
|
||||||
const y0 = lat2tile(props.coordinates.lat, 0);
|
|
||||||
const x = x0 * 2 ** props.keyObject.zoomLevel;
|
|
||||||
const y = y0 * 2 ** props.keyObject.zoomLevel;
|
|
||||||
if (
|
|
||||||
x < props.viewPort.topLeft.x ||
|
|
||||||
x > props.viewPort.bottomRight.x + 1 ||
|
|
||||||
y < props.viewPort.topLeft.y ||
|
|
||||||
y > props.viewPort.bottomRight.y + 1
|
|
||||||
) {
|
|
||||||
cache.delete({ cacheId: 'points', key: props.id });
|
|
||||||
console.log(
|
|
||||||
`Marker ${x}, ${y} out of viewport: ${JSON.stringify(props.viewPort)} `
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
cache.set({
|
|
||||||
cacheId: 'points',
|
|
||||||
key: props.id,
|
|
||||||
value: {
|
|
||||||
coordinates: props.coordinates,
|
|
||||||
point: { x: x0, y: y0 },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
<g
|
|
||||||
transform={`translate(${(x - props.keyObject.x) * 256}, ${
|
|
||||||
(y - props.keyObject.y) * 256
|
|
||||||
}) scale(${1 / props.zoom})`}
|
|
||||||
className={props.className}
|
|
||||||
>
|
|
||||||
{props.icon}
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Marker;
|
|
|
@ -1,56 +0,0 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import Tile from './Tile';
|
|
||||||
import { TileKeyObject } from './types';
|
|
||||||
|
|
||||||
describe('The Tile component ', () => {
|
|
||||||
const keyObject: TileKeyObject = {
|
|
||||||
provider: 'fake',
|
|
||||||
zoomLevel: 10,
|
|
||||||
x: 1,
|
|
||||||
y: 2,
|
|
||||||
};
|
|
||||||
test('Is initially empty', () => {
|
|
||||||
const { baseElement } = render(
|
|
||||||
<svg>
|
|
||||||
<Tile keyObject={keyObject} delay={10000} />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(1, 2)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('Gets its image immediately with a fake URL', () => {
|
|
||||||
const { baseElement } = render(
|
|
||||||
<svg>
|
|
||||||
<Tile keyObject={keyObject} />
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(1, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,68 +0,0 @@
|
||||||
import react, { memo, useEffect, useRef } from 'react';
|
|
||||||
import { isEqual } from 'lodash';
|
|
||||||
import { TileKeyObject } from './types';
|
|
||||||
import { getTileUrl } from './tile-providers';
|
|
||||||
|
|
||||||
export interface TileProperties {
|
|
||||||
/** The image's source URL */
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
/** A delay to add (for test/debug purposes) */
|
|
||||||
delay?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param props
|
|
||||||
* @returns A tile
|
|
||||||
*
|
|
||||||
* Tile components are containers for images.
|
|
||||||
*
|
|
||||||
* They return an empty `<g/>` element immediately so that the rendering can proceed
|
|
||||||
* and append an `<image/>` element whenever the image is loaded.
|
|
||||||
*
|
|
||||||
* They are designed to be part of {@link components/map/TiledLayer!TiledLayer} components in SVG `<g/>` elements
|
|
||||||
* in which the unit is the tile size. In this coordinate system their size is thus always equal to 1.
|
|
||||||
*/
|
|
||||||
export const Tile: react.FC<TileProperties> = memo((props: TileProperties) => {
|
|
||||||
const g = useRef<SVGGElement>(null);
|
|
||||||
|
|
||||||
const timeout = (ms: number) => {
|
|
||||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
||||||
};
|
|
||||||
// console.log(`Rendering tile: ${JSON.stringify(props)}`);
|
|
||||||
useEffect(() => {
|
|
||||||
const loadImage = async () => {
|
|
||||||
// console.log(`Pre loading: ${props.href}`);
|
|
||||||
const href = getTileUrl(props.keyObject);
|
|
||||||
const image = new Image(1, 1);
|
|
||||||
image.loading = 'eager';
|
|
||||||
// @ts-ignore
|
|
||||||
image.setAttribute('href', href);
|
|
||||||
if (!image.complete) {
|
|
||||||
await image.decode();
|
|
||||||
}
|
|
||||||
if (props.delay !== undefined) {
|
|
||||||
await timeout(props.delay);
|
|
||||||
}
|
|
||||||
const svgImage = document.createElementNS(
|
|
||||||
'http://www.w3.org/2000/svg',
|
|
||||||
'image'
|
|
||||||
) as unknown as SVGImageElement;
|
|
||||||
svgImage.setAttribute('width', '1');
|
|
||||||
svgImage.setAttribute('height', '1');
|
|
||||||
// @ts-ignore
|
|
||||||
svgImage.setAttribute('href', href);
|
|
||||||
g.current?.replaceChildren(svgImage);
|
|
||||||
};
|
|
||||||
loadImage();
|
|
||||||
}, [props.keyObject]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g
|
|
||||||
ref={g}
|
|
||||||
transform={`translate(${props.keyObject.x}, ${props.keyObject.y})`}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}, isEqual);
|
|
||||||
|
|
||||||
export default Tile;
|
|
|
@ -1,309 +0,0 @@
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
import TileSet from './TileSet';
|
|
||||||
|
|
||||||
describe('The TiledLayer component ', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.cacheForTileSet = new Map();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('exposes the tiles needed per its viewport', () => {
|
|
||||||
const { baseElement } = render(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
|
|
||||||
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(1, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(2, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/2/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(3, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/3/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('adds more tiles when its viewport is updated without removing the previous ones', () => {
|
|
||||||
const { baseElement, rerender } = render(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
|
|
||||||
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
rerender(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
|
|
||||||
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(1, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(2, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/2/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(3, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/3/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 0)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/0.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/1.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('is not reinitialized if its key isObject updated', () => {
|
|
||||||
const { baseElement, rerender } = render(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
|
|
||||||
viewPort={{ topLeft: { x: 1, y: 2 }, bottomRight: { x: 3, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
rerender(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 4, y: 10 }}
|
|
||||||
viewPort={{ topLeft: { x: 5, y: 0 }, bottomRight: { x: 5, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(1, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(2, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/2/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(3, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/3/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 0)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/0.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/1.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(5, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/5/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('Also works with negative coordinates', () => {
|
|
||||||
const { baseElement } = render(
|
|
||||||
<svg>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 10, x: 5, y: 8 }}
|
|
||||||
viewPort={{ topLeft: { x: -3, y: -1 }, bottomRight: { x: -2, y: 2 } }}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
// screen.debug();
|
|
||||||
expect(baseElement).toMatchInlineSnapshot(`
|
|
||||||
<body>
|
|
||||||
<div>
|
|
||||||
<svg>
|
|
||||||
<g
|
|
||||||
transform="translate(-3, -1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1021/1023.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-2, -1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1022/1023.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-3, 0)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1021/0.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-2, 0)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1022/0.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-3, 1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1021/1.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-2, 1)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1022/1.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-3, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1021/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="translate(-2, 2)"
|
|
||||||
>
|
|
||||||
<image
|
|
||||||
height="1"
|
|
||||||
href="https://fakeurl/10/1022/2.png"
|
|
||||||
width="1"
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,123 +0,0 @@
|
||||||
import react, { memo } from 'react';
|
|
||||||
import { isEqual, range } from 'lodash';
|
|
||||||
|
|
||||||
import { Rectangle, TileKeyObject } from './types';
|
|
||||||
import tileUri from './uris';
|
|
||||||
import Tile from './Tile';
|
|
||||||
import { tileSetConfig } from './config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @hidden
|
|
||||||
*/
|
|
||||||
export const thisIsAModule = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var cacheForTileSet: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
//export {};
|
|
||||||
|
|
||||||
globalThis.cacheForTileSet = new Map();
|
|
||||||
|
|
||||||
export interface TileSetProperties {
|
|
||||||
/** A partial Tile key object specifying the provider and zoom level */
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
/** The current viewport expressed in tiles coordinates */
|
|
||||||
viewPort?: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A lazily loaded set of tiles.
|
|
||||||
*
|
|
||||||
* This component is rather dumb and is mainly a sparse array of tiles.
|
|
||||||
*
|
|
||||||
* New tiles are added to the array when the viewport is updated and they stay in the array until
|
|
||||||
* the component is destroyed.
|
|
||||||
*
|
|
||||||
* This component has no need to know the number nor the size of its tiles: tiles can be added when needed and
|
|
||||||
* its unit is the tile size (the parent component needs to transform its enclosing SVG group to adapt its units)
|
|
||||||
*
|
|
||||||
* The `globalThis.cacheForTiledLayer` global variable is used as a cache to store tiles without being subject
|
|
||||||
* to re-initialization when components are unmounted/remounted.
|
|
||||||
*
|
|
||||||
* This cache is a map of map, the first key identifying the `<TiledLayer` and the second one for `<Tile>`s.
|
|
||||||
*
|
|
||||||
* Idea stolen [on the web](https://dev.to/tiagof/react-re-mounting-vs-re-rendering-lnh)
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const TileSet: react.FC<TileSetProperties> = memo(
|
|
||||||
(props: TileSetProperties) => {
|
|
||||||
// console.log(`Rendering TiledLayer: ${JSON.stringify(props)}`);
|
|
||||||
|
|
||||||
const key = tileUri({
|
|
||||||
provider: props.keyObject.provider,
|
|
||||||
zoomLevel: props.keyObject.zoomLevel,
|
|
||||||
});
|
|
||||||
|
|
||||||
const tiles: any = globalThis.cacheForTileSet.get(key) ?? new Map();
|
|
||||||
if (props.viewPort !== undefined) {
|
|
||||||
range(props.viewPort.topLeft.y, props.viewPort.bottomRight.y + 1).forEach(
|
|
||||||
(row) => {
|
|
||||||
range(
|
|
||||||
props.viewPort!.topLeft.x,
|
|
||||||
props.viewPort!.bottomRight.x + 1
|
|
||||||
).forEach((col) => {
|
|
||||||
const keyObject = {
|
|
||||||
provider: props.keyObject.provider,
|
|
||||||
zoomLevel: props.keyObject.zoomLevel,
|
|
||||||
x: props.keyObject.x + col,
|
|
||||||
y: props.keyObject.y + row,
|
|
||||||
};
|
|
||||||
const key = tileUri(keyObject);
|
|
||||||
if (!tiles.has(key)) {
|
|
||||||
tiles.set(
|
|
||||||
key,
|
|
||||||
<Tile
|
|
||||||
key={key}
|
|
||||||
keyObject={{
|
|
||||||
provider: props.keyObject.provider,
|
|
||||||
zoomLevel: props.keyObject.zoomLevel,
|
|
||||||
x: col,
|
|
||||||
y: row,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const tile = tiles.get(key);
|
|
||||||
tiles.delete(key);
|
|
||||||
tiles.set(key, tile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
if (tiles.size > tileSetConfig.cacheSizePerLayer) {
|
|
||||||
const oldestTileKeys = [...tiles.keys()];
|
|
||||||
oldestTileKeys.splice(-tileSetConfig.cacheSizePerLayer);
|
|
||||||
oldestTileKeys.forEach((tileKey) => {
|
|
||||||
tiles.delete(tileKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (globalThis.cacheForTileSet.has(key)) {
|
|
||||||
globalThis.cacheForTileSet.delete(key);
|
|
||||||
}
|
|
||||||
globalThis.cacheForTileSet.set(key, tiles);
|
|
||||||
if (globalThis.cacheForTileSet > tileSetConfig.numberOfCachedLayers) {
|
|
||||||
const oldestCachedLayerKeys = [...globalThis.cacheForTileSet.keys()];
|
|
||||||
oldestCachedLayerKeys.slice(-tileSetConfig.numberOfCachedLayers);
|
|
||||||
oldestCachedLayerKeys.forEach((cachedLayerKey) => {
|
|
||||||
globalThis.cacheForTileSet.delete(cachedLayerKey);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{Array.from(tiles.values())}</>;
|
|
||||||
},
|
|
||||||
isEqual
|
|
||||||
);
|
|
||||||
|
|
||||||
export default TileSet;
|
|
|
@ -1,61 +0,0 @@
|
||||||
import { render, screen } from '@testing-library/react';
|
|
||||||
import { CoordinateSystem } from './LiveMap';
|
|
||||||
import TiledLayer from './TiledLayer';
|
|
||||||
|
|
||||||
describe('The TiledLayer component', () => {
|
|
||||||
const coordinateSystem: CoordinateSystem = {
|
|
||||||
shift: { x: 0, y: 0 },
|
|
||||||
zoom: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.cacheForTileSet = new Map();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('generates an empty layer if inactive', () => {
|
|
||||||
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
|
|
||||||
render(
|
|
||||||
<svg data-testid='tiled-layer'>
|
|
||||||
<TiledLayer
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 5, x: 2, y: 3 }}
|
|
||||||
shift={{ x: 0.5, y: 0.25 }}
|
|
||||||
zoom={4}
|
|
||||||
coordinateSystem={coordinateSystem}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
const svg = screen.getByTestId('tiled-layer');
|
|
||||||
expect(svg).toMatchInlineSnapshot(`
|
|
||||||
<svg
|
|
||||||
data-testid="tiled-layer"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="scale(4) translate(0.5, 0.25)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('generates a populated layer if active', () => {
|
|
||||||
// const { result } = renderHook(() => useAtom(tiledLayersAtom));
|
|
||||||
render(
|
|
||||||
<svg data-testid='tiled-layer'>
|
|
||||||
<TiledLayer
|
|
||||||
keyObject={{ provider: 'fake', zoomLevel: 5, x: 2, y: 3 }}
|
|
||||||
shift={{ x: 0, y: 0 }}
|
|
||||||
zoom={1}
|
|
||||||
coordinateSystem={coordinateSystem}
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
const svg = screen.getByTestId('tiled-layer');
|
|
||||||
expect(svg).toMatchInlineSnapshot(`
|
|
||||||
<svg
|
|
||||||
data-testid="tiled-layer"
|
|
||||||
>
|
|
||||||
<g
|
|
||||||
transform="scale(1) translate(0, 0)"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,71 +0,0 @@
|
||||||
import react, {
|
|
||||||
cloneElement,
|
|
||||||
JSXElementConstructor,
|
|
||||||
ReactElement,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import TileSet from './TileSet';
|
|
||||||
import { Point, Rectangle, TileKeyObject } from './types';
|
|
||||||
import { CoordinateSystem } from './LiveMap';
|
|
||||||
import useViewport from './use-viewport';
|
|
||||||
|
|
||||||
export interface TiledLayerProperties {
|
|
||||||
/**
|
|
||||||
* A key identifying the top left tile
|
|
||||||
*/
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
/**
|
|
||||||
* The translation to apply.
|
|
||||||
*/
|
|
||||||
shift: Point;
|
|
||||||
/**
|
|
||||||
* The zoom to apply. If equal to 256 (the tile size), the layer is considered active and should add tiles which are in its viewport
|
|
||||||
*/
|
|
||||||
zoom: number;
|
|
||||||
/**
|
|
||||||
* The coordinate system
|
|
||||||
*/
|
|
||||||
coordinateSystem: CoordinateSystem;
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
viewPort?: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param props
|
|
||||||
* @returns A layer of tiles.
|
|
||||||
* This component wraps a `<TileSet>` in an SVG `<g>` element taking care of the scale and translation.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export const TiledLayer: react.FC<TiledLayerProperties> = (
|
|
||||||
props: TiledLayerProperties
|
|
||||||
) => {
|
|
||||||
console.log(
|
|
||||||
`Rendering TiledLayer, zoom: ${props.zoom}, viewPort: ${JSON.stringify(
|
|
||||||
props.viewPort
|
|
||||||
)}`
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<g
|
|
||||||
transform={`scale(${props.zoom}) translate(${props.shift.x}, ${props.shift.y})`}
|
|
||||||
>
|
|
||||||
<TileSet
|
|
||||||
keyObject={{
|
|
||||||
provider: props.keyObject.provider,
|
|
||||||
zoomLevel: props.keyObject.zoomLevel,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
}}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
/>
|
|
||||||
</g>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TiledLayer;
|
|
|
@ -1,47 +0,0 @@
|
||||||
import react, { startTransition, useEffect, useState } from 'react';
|
|
||||||
import dispatch from '../../workers/dispatcher-main';
|
|
||||||
import { TileKeyObject, Rectangle } from './types';
|
|
||||||
import getUri from '../../lib/ids';
|
|
||||||
import Trkseg from './Trkseg';
|
|
||||||
|
|
||||||
export interface TrkProperties {
|
|
||||||
id: string;
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
zoom: number;
|
|
||||||
viewPort: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Trk: react.FC<TrkProperties> = (props: TrkProperties) => {
|
|
||||||
const [trk, setTrk] = useState<any>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const getTrk = async () => {
|
|
||||||
const trk = await dispatch({
|
|
||||||
action: 'getTrk',
|
|
||||||
params: {
|
|
||||||
id: props.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(`<Trk>, gpx: ${JSON.stringify(trk)}`);
|
|
||||||
startTransition(() => setTrk(trk));
|
|
||||||
};
|
|
||||||
getTrk();
|
|
||||||
}, [props.id]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<g>
|
|
||||||
{trk
|
|
||||||
.filter((row: any) => row.doc.type === 'trkseg')
|
|
||||||
.map((row: any) => (
|
|
||||||
<Trkseg
|
|
||||||
key={row.id}
|
|
||||||
id={row.id}
|
|
||||||
keyObject={props.keyObject}
|
|
||||||
zoom={props.zoom}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</g>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Trk;
|
|
|
@ -1,15 +0,0 @@
|
||||||
.track {
|
|
||||||
fill: transparent;
|
|
||||||
stroke-width: 2px;
|
|
||||||
stroke-linecap: round;
|
|
||||||
stroke-linejoin: round;
|
|
||||||
stroke: rgba(10, 1, 51, 0.8);
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.start {
|
|
||||||
color: green;
|
|
||||||
}
|
|
||||||
.finish {
|
|
||||||
color: red;
|
|
||||||
}
|
|
|
@ -1,92 +0,0 @@
|
||||||
import react, { startTransition, useEffect, useState } from 'react';
|
|
||||||
import { lon2tile, lat2tile } from '../../lib/geo';
|
|
||||||
import dispatch from '../../workers/dispatcher-main';
|
|
||||||
import { TileKeyObject, Rectangle, Point, geoPoint } from './types';
|
|
||||||
|
|
||||||
import css from './Trkseg.module.css';
|
|
||||||
import { ReactComponent as Start } from '../../icons/flag-start-b-svgrepo-com.svg';
|
|
||||||
import { ReactComponent as Finish } from '../../icons/flag-finish-b-o-svgrepo-com.svg';
|
|
||||||
import Marker from './Marker';
|
|
||||||
|
|
||||||
export interface TrksegProperties {
|
|
||||||
id: string;
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
zoom: number;
|
|
||||||
viewPort: Rectangle;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Trkseg: react.FC<TrksegProperties> = (props: TrksegProperties) => {
|
|
||||||
const [trkseg, setTrkseg] = useState<any>([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const getTrkseg = async () => {
|
|
||||||
const trk = await dispatch({
|
|
||||||
action: 'getTrkseg',
|
|
||||||
params: {
|
|
||||||
id: props.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
console.log(`<Trkseg>, gpx: ${JSON.stringify(trk)}`);
|
|
||||||
startTransition(() => setTrkseg(trk));
|
|
||||||
};
|
|
||||||
getTrkseg();
|
|
||||||
}, [props.id]);
|
|
||||||
|
|
||||||
const d = trkseg
|
|
||||||
.slice(1)
|
|
||||||
.reduce((previous: string, current: any, index: number) => {
|
|
||||||
const action = index === 0 ? 'M' : index === 1 ? 'L' : '';
|
|
||||||
const trkpt = current.doc.doc;
|
|
||||||
return `${previous} ${action} ${
|
|
||||||
(lon2tile(trkpt.$.lon, props.keyObject.zoomLevel) - props.keyObject.x) *
|
|
||||||
256
|
|
||||||
}, ${
|
|
||||||
(lat2tile(trkpt.$.lat, props.keyObject.zoomLevel) - props.keyObject.y) *
|
|
||||||
256
|
|
||||||
}`;
|
|
||||||
}, '');
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{trkseg[1] !== undefined ? (
|
|
||||||
<>
|
|
||||||
<Marker
|
|
||||||
id={`${props.id}/start`}
|
|
||||||
coordinates={trkseg[1].doc.doc.$ as geoPoint}
|
|
||||||
key='start'
|
|
||||||
keyObject={props.keyObject}
|
|
||||||
icon={
|
|
||||||
<g
|
|
||||||
className={css.start}
|
|
||||||
transform=' scale(.5) translate(-50, -100)'
|
|
||||||
>
|
|
||||||
<Start />
|
|
||||||
</g>
|
|
||||||
}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
zoom={props.zoom}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<path d={d} pointerEvents='none' className={css.track} />
|
|
||||||
<Marker
|
|
||||||
id={`${props.id}/finish`}
|
|
||||||
coordinates={trkseg.at(-1).doc.doc.$ as geoPoint}
|
|
||||||
key='finish'
|
|
||||||
keyObject={props.keyObject}
|
|
||||||
icon={
|
|
||||||
<g
|
|
||||||
className={css.finish}
|
|
||||||
transform=' scale(.5) translate(-50, -100)'
|
|
||||||
>
|
|
||||||
<Finish />
|
|
||||||
</g>
|
|
||||||
}
|
|
||||||
viewPort={props.viewPort}
|
|
||||||
zoom={props.zoom}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Trkseg;
|
|
|
@ -1,16 +0,0 @@
|
||||||
/**The handlers configuration */
|
|
||||||
export const handlersConfig = {
|
|
||||||
/**Controls the activity of the mouse mouse event */
|
|
||||||
mouseMoveThrottleDelay: 50,
|
|
||||||
/**Controls the activity of the wheel event */
|
|
||||||
wheelThrottleDelay: 100,
|
|
||||||
/** Controls the activity of the single touch move event */
|
|
||||||
singleTouchMoveThrottleDelay: 50,
|
|
||||||
/** Controls the activity of the double touch move event */
|
|
||||||
doubleTouchMoveThrottleDelay: 100,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tileSetConfig = {
|
|
||||||
cacheSizePerLayer: 1000,
|
|
||||||
numberOfCachedLayers: 20,
|
|
||||||
};
|
|
|
@ -1,158 +0,0 @@
|
||||||
import { TileKeyObject } from './types';
|
|
||||||
|
|
||||||
export interface TileProvider {
|
|
||||||
name: string;
|
|
||||||
minZoom: number;
|
|
||||||
maxZoom: number;
|
|
||||||
tileSize: 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 type TileProviders = {
|
|
||||||
[key: string]: TileProvider;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tileProviders: TileProviders = {
|
|
||||||
osm: {
|
|
||||||
name: 'Open Street Map',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 19,
|
|
||||||
tileSize: 256,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png',
|
|
||||||
},
|
|
||||||
osmfr: {
|
|
||||||
name: 'Open Street Map France',
|
|
||||||
minZoom: 0,
|
|
||||||
tileSize: 256,
|
|
||||||
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,
|
|
||||||
tileSize: 256,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://' +
|
|
||||||
getRandomItem(abc) +
|
|
||||||
'.tile.opentopomap.org/' +
|
|
||||||
zoom +
|
|
||||||
'/' +
|
|
||||||
x +
|
|
||||||
'/' +
|
|
||||||
y +
|
|
||||||
'.png',
|
|
||||||
},
|
|
||||||
cyclosm: {
|
|
||||||
name: 'CyclOSM',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 19,
|
|
||||||
tileSize: 256,
|
|
||||||
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,
|
|
||||||
tileSize: 256,
|
|
||||||
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',
|
|
||||||
// },
|
|
||||||
|
|
||||||
fake: {
|
|
||||||
name: 'Fake provider',
|
|
||||||
minZoom: 0,
|
|
||||||
maxZoom: 20,
|
|
||||||
tileSize: 256,
|
|
||||||
getTileUrl: (zoom: number, x: number, y: number) =>
|
|
||||||
'https://fakeurl/' + zoom + '/' + x + '/' + y + '.png',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const { fake, ...nonFakeTileProviders } = tileProviders;
|
|
||||||
|
|
||||||
const mod = (n: number, m: number) => {
|
|
||||||
const jsMod = n % m;
|
|
||||||
return jsMod >= 0 ? jsMod : jsMod + m;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param keyObject The tile identifier
|
|
||||||
* @returns The `<Tile>`'s URL 'as a string)
|
|
||||||
*/
|
|
||||||
export const getTileUrl = (keyObject: TileKeyObject) => {
|
|
||||||
const nbTiles = 2 ** keyObject.zoomLevel;
|
|
||||||
const x = mod(keyObject.x, nbTiles);
|
|
||||||
const y = mod(keyObject.y, nbTiles);
|
|
||||||
return tileProviders[keyObject.provider].getTileUrl(
|
|
||||||
keyObject.zoomLevel,
|
|
||||||
x,
|
|
||||||
y
|
|
||||||
);
|
|
||||||
};
|
|
||||||
export default getTileUrl;
|
|
|
@ -1,53 +0,0 @@
|
||||||
import tileProviders, { TileProviders } from './tile-providers';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An identifier for tiles (can also be used for tile layers)
|
|
||||||
*/
|
|
||||||
export interface TileKeyObject {
|
|
||||||
/**A tile provider id ('osm', 'otm', ...) */
|
|
||||||
provider: keyof TileProviders;
|
|
||||||
/**The zoom level (integer) */
|
|
||||||
zoomLevel: number;
|
|
||||||
/**The X coordinate (integer)*/
|
|
||||||
x: number;
|
|
||||||
/**The Y coordinate (integer) */
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A point identified by its longitude and latitude
|
|
||||||
*/
|
|
||||||
export interface geoPoint {
|
|
||||||
lon: number;
|
|
||||||
lat: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A map scope.
|
|
||||||
*
|
|
||||||
* This object contains what's needed to identify the state of the map
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export interface MapScope {
|
|
||||||
center: geoPoint;
|
|
||||||
zoom: number;
|
|
||||||
tileProvider: keyof TileProviders;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A point
|
|
||||||
*/
|
|
||||||
export interface Point {
|
|
||||||
/**The X coordinate (integer)*/
|
|
||||||
x: number;
|
|
||||||
/**The Y coordinate (integer) */
|
|
||||||
y: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A rectangle
|
|
||||||
*/
|
|
||||||
export interface Rectangle {
|
|
||||||
topLeft: Point;
|
|
||||||
bottomRight: Point;
|
|
||||||
}
|
|
|
@ -1,25 +0,0 @@
|
||||||
import tileUri from './uris';
|
|
||||||
describe('Test that', () => {
|
|
||||||
test('uri generation is working', () => {
|
|
||||||
expect(tileUri({ provider: 'osm', zoomLevel: 16, x: 25, y: 52 })).toEqual(
|
|
||||||
'tile/osm/16/25/52'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('uri parsing works', () => {
|
|
||||||
expect(tileUri('tile/otm/5/28/3')).toEqual({
|
|
||||||
provider: 'otm',
|
|
||||||
zoomLevel: 5,
|
|
||||||
x: 28,
|
|
||||||
y: 3,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('x and y are optional', () => {
|
|
||||||
expect(tileUri({ provider: 'osm', zoomLevel: 16 })).toEqual('tile/osm/16//');
|
|
||||||
});
|
|
||||||
test('uri parsing works', () => {
|
|
||||||
expect(tileUri('tile/otm/5')).toEqual({
|
|
||||||
provider: 'otm',
|
|
||||||
zoomLevel: 5,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,26 +0,0 @@
|
||||||
import { route } from '../../lib/docuri';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A [docuri](https://github.com/jo/docuri) route for {@link components/map/types!TileKeyObject}
|
|
||||||
*
|
|
||||||
* TODO: update docuri (or write a wrapper) to support datatyping (and formats).
|
|
||||||
*/
|
|
||||||
export const tileUri = (rte: any) => {
|
|
||||||
const r = route('tile/:provider/:zoomLevel(/:x/:y)')(rte);
|
|
||||||
if (typeof r === 'object') {
|
|
||||||
return r.x === undefined
|
|
||||||
? {
|
|
||||||
provider: r.provider,
|
|
||||||
zoomLevel: parseInt(r.zoomLevel),
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
provider: r.provider,
|
|
||||||
zoomLevel: parseInt(r.zoomLevel),
|
|
||||||
x: parseInt(r.x),
|
|
||||||
y: parseInt(r.y),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default tileUri;
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { RefObject, useEffect, useRef, useState } from 'react';
|
|
||||||
import { CoordinateSystem } from './LiveMap';
|
|
||||||
import { Rectangle, TileKeyObject } from './types';
|
|
||||||
|
|
||||||
const useViewport = (props: {
|
|
||||||
keyObject: TileKeyObject;
|
|
||||||
coordinateSystem: CoordinateSystem;
|
|
||||||
svgElement: RefObject<SVGGElement>;
|
|
||||||
}) => {
|
|
||||||
const { keyObject, coordinateSystem, svgElement } = props;
|
|
||||||
const [viewPort, setViewPort] = useState<Rectangle>();
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
svgElement.current !== null &&
|
|
||||||
svgElement.current.ownerSVGElement !== null &&
|
|
||||||
svgElement.current.ownerSVGElement.parentElement !== null
|
|
||||||
) {
|
|
||||||
const nearerHTMLParent = svgElement.current.ownerSVGElement.parentElement;
|
|
||||||
setViewPort({
|
|
||||||
topLeft: {
|
|
||||||
x:
|
|
||||||
keyObject.x +
|
|
||||||
Math.floor(-coordinateSystem.shift.x / coordinateSystem.zoom / 256),
|
|
||||||
y:
|
|
||||||
keyObject.y +
|
|
||||||
Math.floor(-coordinateSystem.shift.y / coordinateSystem.zoom / 256),
|
|
||||||
},
|
|
||||||
bottomRight: {
|
|
||||||
x:
|
|
||||||
keyObject.x +
|
|
||||||
Math.ceil(
|
|
||||||
(-coordinateSystem.shift.x + nearerHTMLParent.offsetWidth) /
|
|
||||||
coordinateSystem.zoom /
|
|
||||||
256
|
|
||||||
) -
|
|
||||||
1,
|
|
||||||
y:
|
|
||||||
keyObject.y +
|
|
||||||
Math.ceil(
|
|
||||||
(-coordinateSystem.shift.y + nearerHTMLParent.offsetHeight) /
|
|
||||||
coordinateSystem.zoom /
|
|
||||||
256
|
|
||||||
) -
|
|
||||||
1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [props.keyObject, props.coordinateSystem]);
|
|
||||||
return viewPort;
|
|
||||||
};
|
|
||||||
export default useViewport;
|
|
|
@ -1,86 +0,0 @@
|
||||||
import { initDb } from '.';
|
|
||||||
import { existsGpx, putNewGpx } from './gpx';
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The gpx module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new Gpx when required', async () => {
|
|
||||||
await putNewGpx({ gpx: 0 });
|
|
||||||
expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/0000000000000000',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
$: {
|
|
||||||
creator: 'dyomedea version 0.000002',
|
|
||||||
version: '1.1',
|
|
||||||
xmlns: 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:dyo': 'http://xmlns.dyomedea.com/',
|
|
||||||
'xmlns:gpxtpx':
|
|
||||||
'http://www.garmin.com/xmlschemas/TrackPointExtension/v1',
|
|
||||||
'xmlns:gpxx': 'http://www.garmin.com/xmlschemas/GpxExtensions/v3',
|
|
||||||
'xmlns:wptx1':
|
|
||||||
'http://www.garmin.com/xmlschemas/WaypointExtension/v1',
|
|
||||||
'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',
|
|
||||||
},
|
|
||||||
extensions: undefined,
|
|
||||||
metadata: {
|
|
||||||
author: undefined,
|
|
||||||
bounds: undefined,
|
|
||||||
copyright: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
keywords: undefined,
|
|
||||||
link: undefined,
|
|
||||||
name: undefined,
|
|
||||||
time: '1970-01-01T00:00:00.000Z',
|
|
||||||
},
|
|
||||||
rte: undefined,
|
|
||||||
trk: undefined,
|
|
||||||
wpt: undefined,
|
|
||||||
},
|
|
||||||
type: 'gpx',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id if needed', async () => {
|
|
||||||
const id = await putNewGpx();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('The gpx module with a real db', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await initDb({});
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(async () => {
|
|
||||||
await db.destroy();
|
|
||||||
db = undefined;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
|
|
||||||
test("existsGpx returns false if the GPX doesn't exist", async () => {
|
|
||||||
const exists = await existsGpx({ gpx: 1 });
|
|
||||||
expect(exists).toBeFalsy();
|
|
||||||
});
|
|
||||||
test('existsGpx returns false if the GPX exists', async () => {
|
|
||||||
const id = { gpx: 1 };
|
|
||||||
await putNewGpx(id);
|
|
||||||
const exists = await existsGpx(id);
|
|
||||||
expect(exists).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
269
src/db/gpx.ts
269
src/db/gpx.ts
|
@ -1,269 +0,0 @@
|
||||||
import { PureComponent } from 'react';
|
|
||||||
import { Point, Rectangle } from '../components/map/types';
|
|
||||||
import { lat2tile, lon2tile, rectanglesIntersect } from '../lib/geo';
|
|
||||||
import getUri, { intToGpxId } from '../lib/ids';
|
|
||||||
import { get, getDocsByType, getFamily, put, putAll } from './lib';
|
|
||||||
|
|
||||||
const emptyGpx: Gpx = {
|
|
||||||
$: {
|
|
||||||
version: '1.1',
|
|
||||||
creator: 'dyomedea version 0.000002',
|
|
||||||
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',
|
|
||||||
'xmlns:dyo': 'http://xmlns.dyomedea.com/',
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
name: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
author: undefined,
|
|
||||||
copyright: undefined,
|
|
||||||
link: undefined,
|
|
||||||
time: undefined,
|
|
||||||
keywords: undefined,
|
|
||||||
bounds: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
},
|
|
||||||
wpt: undefined,
|
|
||||||
rte: undefined,
|
|
||||||
trk: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewGpx = async (
|
|
||||||
id: IdGpx = { gpx: intToGpxId(Date.now()) }
|
|
||||||
) => {
|
|
||||||
const uri = getUri('gpx', id);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'gpx',
|
|
||||||
(gpx) => {
|
|
||||||
(gpx.metadata ??= {}).time = new Date(Date.now()).toISOString();
|
|
||||||
return gpx;
|
|
||||||
},
|
|
||||||
emptyGpx
|
|
||||||
);
|
|
||||||
return id;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const existsGpx = async (id: IdGpx) => {
|
|
||||||
const uri = getUri('gpx', id);
|
|
||||||
try {
|
|
||||||
await get(uri);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const prune = (id: any, object: any, docs: any[]) => {
|
|
||||||
if (typeof object === 'object') {
|
|
||||||
for (const key in object) {
|
|
||||||
if (
|
|
||||||
key === 'wpt' ||
|
|
||||||
key === 'rte' ||
|
|
||||||
key === 'rtept' ||
|
|
||||||
key === 'trk' ||
|
|
||||||
key === 'trkseg' ||
|
|
||||||
key === 'trkpt'
|
|
||||||
) {
|
|
||||||
const subObjects = object[key];
|
|
||||||
for (const index in subObjects) {
|
|
||||||
const subId = { ...id };
|
|
||||||
subId[key] = index;
|
|
||||||
docs.push({
|
|
||||||
_id: getUri(key, subId),
|
|
||||||
type: key,
|
|
||||||
doc: subObjects[index],
|
|
||||||
});
|
|
||||||
prune(subId, subObjects[index], docs);
|
|
||||||
}
|
|
||||||
object[key] = undefined;
|
|
||||||
} else prune(id, object[key], docs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const extensionsFromObject = (
|
|
||||||
object: any,
|
|
||||||
extensions = {
|
|
||||||
viewport: { topLeft: <Point>{}, bottomRight: <Point>{} },
|
|
||||||
bbox: {
|
|
||||||
minLon: <number | undefined>undefined,
|
|
||||||
minLat: <number | undefined>undefined,
|
|
||||||
maxLon: <number | undefined>undefined,
|
|
||||||
maxLat: <number | undefined>undefined,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
) => {
|
|
||||||
if (typeof object === 'object') {
|
|
||||||
if ('$' in object) {
|
|
||||||
const attributes = object.$;
|
|
||||||
if ('lat' in attributes) {
|
|
||||||
const lat = +attributes.lat;
|
|
||||||
if (
|
|
||||||
extensions.bbox.minLat === undefined ||
|
|
||||||
lat < extensions.bbox.minLat
|
|
||||||
) {
|
|
||||||
extensions.bbox.minLat = lat;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
extensions.bbox.maxLat === undefined ||
|
|
||||||
lat > extensions.bbox.maxLat
|
|
||||||
) {
|
|
||||||
extensions.bbox.maxLat = lat;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ('lon' in attributes) {
|
|
||||||
const lon = +attributes.lon;
|
|
||||||
if (
|
|
||||||
extensions.bbox.minLon === undefined ||
|
|
||||||
lon < extensions.bbox.minLon
|
|
||||||
) {
|
|
||||||
extensions.bbox.minLon = lon;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
extensions.bbox.maxLon === undefined ||
|
|
||||||
lon > extensions.bbox.maxLon
|
|
||||||
) {
|
|
||||||
extensions.bbox.maxLon = lon;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const key in object) {
|
|
||||||
extensionsFromObject(object[key], extensions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return extensions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const extensionsFromGpx = (gpx: Gpx) => {
|
|
||||||
const extensions = { ...gpx.extensions, ...extensionsFromObject(gpx) };
|
|
||||||
gpx.extensions = undefined;
|
|
||||||
if (
|
|
||||||
extensions.bbox.maxLat !== undefined &&
|
|
||||||
extensions.bbox.minLon !== undefined
|
|
||||||
) {
|
|
||||||
extensions.viewport.topLeft = {
|
|
||||||
x: lon2tile(extensions.bbox.minLon, 0),
|
|
||||||
y: lat2tile(extensions.bbox.maxLat, 0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
extensions.bbox.minLat !== undefined &&
|
|
||||||
extensions.bbox.maxLon !== undefined
|
|
||||||
) {
|
|
||||||
extensions.viewport.bottomRight = {
|
|
||||||
x: lon2tile(extensions.bbox.maxLon, 0),
|
|
||||||
y: lat2tile(extensions.bbox.minLat, 0),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const pruneAndSaveImportedGpx = async (params: any) => {
|
|
||||||
const { id, gpx, extensions } = params;
|
|
||||||
let docs: any[] = [
|
|
||||||
{ _id: getUri('gpx', id), type: 'gpx', doc: gpx },
|
|
||||||
{
|
|
||||||
_id: getUri('extensions', id),
|
|
||||||
type: 'extensions',
|
|
||||||
doc: { ...extensions, ...extensionsFromGpx(gpx) },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
prune(id, gpx, docs);
|
|
||||||
console.log(JSON.stringify(docs));
|
|
||||||
try {
|
|
||||||
const result = await putAll(docs);
|
|
||||||
console.log(JSON.stringify(result));
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`error: ${err}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGpxesForViewport = async (params: any) => {
|
|
||||||
const { viewport, zoomLevel } = params;
|
|
||||||
const zoomedViewport: Rectangle = {
|
|
||||||
topLeft: {
|
|
||||||
x: viewport.topLeft.x / 2 ** zoomLevel,
|
|
||||||
y: viewport.topLeft.y / 2 ** zoomLevel,
|
|
||||||
},
|
|
||||||
bottomRight: {
|
|
||||||
x: (viewport.bottomRight.x + 1) / 2 ** zoomLevel,
|
|
||||||
y: (viewport.bottomRight.y + 1) / 2 ** zoomLevel,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const allExtensions = await getDocsByType('extensions');
|
|
||||||
console.log(
|
|
||||||
`getGpxesForViewport, allExtensions: ${JSON.stringify(allExtensions)}`
|
|
||||||
);
|
|
||||||
return allExtensions
|
|
||||||
.filter((extensions: any) => {
|
|
||||||
return rectanglesIntersect(zoomedViewport, extensions.doc.viewport);
|
|
||||||
})
|
|
||||||
.map((extensions: any) =>
|
|
||||||
getUri('gpx', getUri('extensions', extensions._id))
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const appendToArray = (target: any, key: string, value: any) => {
|
|
||||||
if (!(key in target)) {
|
|
||||||
target[key] = <any>[];
|
|
||||||
}
|
|
||||||
target[key].push(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFullGpx = async (params: any) => {
|
|
||||||
const { id } = params;
|
|
||||||
const docs = await getFamily(id, { include_docs: true });
|
|
||||||
let target: any[];
|
|
||||||
let gpx: Gpx | undefined = undefined;
|
|
||||||
docs.rows.forEach((row: any) => {
|
|
||||||
// level 0
|
|
||||||
if (row.doc.type === 'gpx') {
|
|
||||||
target = [row.doc.doc];
|
|
||||||
gpx = row.doc.doc;
|
|
||||||
}
|
|
||||||
//level 1
|
|
||||||
if (
|
|
||||||
row.doc.type === 'wpt' ||
|
|
||||||
row.doc.type === 'rte' ||
|
|
||||||
row.doc.type === 'trk' ||
|
|
||||||
row.doc.type === 'extensions'
|
|
||||||
) {
|
|
||||||
target.splice(1);
|
|
||||||
appendToArray(target.at(-1), row.doc.type, row.doc.doc);
|
|
||||||
target.push(row.doc.doc);
|
|
||||||
}
|
|
||||||
// level 2
|
|
||||||
if (row.doc.type === 'rtept' || row.doc.type === 'trkseg') {
|
|
||||||
target.splice(2);
|
|
||||||
appendToArray(target.at(-1), row.doc.type, row.doc.doc);
|
|
||||||
target.push(row.doc.doc);
|
|
||||||
}
|
|
||||||
// level 3
|
|
||||||
if (row.doc.type === 'trkpt') {
|
|
||||||
appendToArray(target.at(-1), row.doc.type, row.doc.doc);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return gpx;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getGpx = async (params: any) => {
|
|
||||||
const { id } = params;
|
|
||||||
const docs = await getFamily(id, { include_docs: true });
|
|
||||||
console.log(`getGpx, uri: ${id} docs: ${JSON.stringify(docs)}`);
|
|
||||||
return docs.rows.filter(
|
|
||||||
(row: any) =>
|
|
||||||
row.doc.type === 'gpx' ||
|
|
||||||
row.doc.type === 'wpt' ||
|
|
||||||
row.doc.type === 'rte' ||
|
|
||||||
row.doc.type === 'trk' ||
|
|
||||||
row.doc.type === 'extensions'
|
|
||||||
);
|
|
||||||
};
|
|
117
src/db/index.ts
117
src/db/index.ts
|
@ -1,117 +0,0 @@
|
||||||
import _ from 'lodash';
|
|
||||||
import PouchDB from 'pouchdb';
|
|
||||||
import PouchDBFind from 'pouchdb-find';
|
|
||||||
import uri from '../lib/ids';
|
|
||||||
|
|
||||||
PouchDB.plugin(PouchDBFind);
|
|
||||||
|
|
||||||
const dbDefinitionId = uri('dbdef', {});
|
|
||||||
|
|
||||||
const currentDbDefinition = {
|
|
||||||
_id: dbDefinitionId,
|
|
||||||
type: dbDefinitionId,
|
|
||||||
def: { version: '0.000001' },
|
|
||||||
};
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const initDb = async (params: any) => {
|
|
||||||
if (globalThis.db === undefined) {
|
|
||||||
globalThis.db = new PouchDB('dyomedea', {
|
|
||||||
auto_compaction: true,
|
|
||||||
revs_limit: 10,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const db = globalThis.db;
|
|
||||||
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',
|
|
||||||
def: {
|
|
||||||
fields: [{ type: '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 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.dbReady = true;
|
|
||||||
|
|
||||||
/* const indexes = await db.getIndexes();
|
|
||||||
console.log(`indexes: ${JSON.stringify(indexes)}`);
|
|
||||||
|
|
||||||
const explain1 = await db.explain({
|
|
||||||
selector: {
|
|
||||||
type: 'trkpt',
|
|
||||||
gpx: 'xxxx',
|
|
||||||
},
|
|
||||||
// sort: ['trkpt.time'],
|
|
||||||
// use_index: 'type-trkpt-gpx-time',
|
|
||||||
});
|
|
||||||
console.log(`explain1: ${JSON.stringify(explain1)}`);
|
|
||||||
const explain2 = await db.explain({
|
|
||||||
selector: {
|
|
||||||
type: 'gpx',
|
|
||||||
},
|
|
||||||
// sort: ['trkpt.time'],
|
|
||||||
// use_index: 'type-trkpt-gpx-time',
|
|
||||||
});
|
|
||||||
console.log(`explain2: ${JSON.stringify(explain2)}`);
|
|
||||||
*/
|
|
||||||
};
|
|
|
@ -1,299 +0,0 @@
|
||||||
import { initDb } from '.';
|
|
||||||
import uri from '../lib/ids';
|
|
||||||
import { getDocsByType, getFamily } from './lib';
|
|
||||||
import { putNewRte } from './rte';
|
|
||||||
import { putNewRtept } from './rtept';
|
|
||||||
import { putNewTrk } from './trk';
|
|
||||||
import { putNewTrkpt } from './trkpt';
|
|
||||||
import { putNewTrkseg } from './trkseg';
|
|
||||||
import { putNewWpt } from './wpt';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('getFamily', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await initDb({});
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(async () => {
|
|
||||||
await db.destroy();
|
|
||||||
db = undefined;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('returns two rows after a gpx and a track have been inserted.', async () => {
|
|
||||||
await putNewTrk();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-4c114f3ae0073151e4082ff1d220c2a4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 4,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('also returns the docs if required.', async () => {
|
|
||||||
await putNewTrk();
|
|
||||||
const allDocs: any = await getFamily('gpx/', {
|
|
||||||
include_docs: true,
|
|
||||||
});
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"doc": Object {
|
|
||||||
"_id": "gpx/4320000000000000",
|
|
||||||
"_rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
"doc": Object {
|
|
||||||
"$": Object {
|
|
||||||
"creator": "dyomedea version 0.000002",
|
|
||||||
"version": "1.1",
|
|
||||||
"xmlns": "http://www.topografix.com/GPX/1/1",
|
|
||||||
"xmlns:dyo": "http://xmlns.dyomedea.com/",
|
|
||||||
"xmlns:gpxtpx": "http://www.garmin.com/xmlschemas/TrackPointExtension/v1",
|
|
||||||
"xmlns:gpxx": "http://www.garmin.com/xmlschemas/GpxExtensions/v3",
|
|
||||||
"xmlns:wptx1": "http://www.garmin.com/xmlschemas/WaypointExtension/v1",
|
|
||||||
"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",
|
|
||||||
},
|
|
||||||
"metadata": Object {
|
|
||||||
"time": "1970-01-01T00:00:00.000Z",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"type": "gpx",
|
|
||||||
},
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"doc": Object {
|
|
||||||
"_id": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"_rev": "1-4c114f3ae0073151e4082ff1d220c2a4",
|
|
||||||
"doc": Object {
|
|
||||||
"number": 0,
|
|
||||||
},
|
|
||||||
"type": "trk",
|
|
||||||
},
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-4c114f3ae0073151e4082ff1d220c2a4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 4,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('returns three rows after a gpx and a track segment have been inserted.', async () => {
|
|
||||||
await putNewTrkseg();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-4c114f3ae0073151e4082ff1d220c2a4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-68d7de0569de570229ea9f9e1a0b13cb",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 5,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
test('returns four rows after a gpx and a track point have been inserted.', async () => {
|
|
||||||
await putNewTrkpt();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-4c114f3ae0073151e4082ff1d220c2a4",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-68d7de0569de570229ea9f9e1a0b13cb",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/3trk/000000/000000/000000",
|
|
||||||
"key": "gpx/4320000000000000/3trk/000000/000000/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-7d917d1f3505fe0e3092161694904b53",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 6,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns two rows after a gpx and a waypoint have been inserted.', async () => {
|
|
||||||
await putNewWpt();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/1wpt/000000",
|
|
||||||
"key": "gpx/4320000000000000/1wpt/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-c6793365fd0dd56236ab8734a41c7ae7",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 4,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns two rows after a gpx and a route have been inserted.', async () => {
|
|
||||||
await putNewRte();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/2rte/000000",
|
|
||||||
"key": "gpx/4320000000000000/2rte/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-2ca14f512a9c83f5a239389e580befce",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 4,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('returns three rows after a gpx and a route point have been inserted.', async () => {
|
|
||||||
await putNewRtept();
|
|
||||||
const allDocs: any = await getFamily('gpx/');
|
|
||||||
expect(allDocs).toMatchInlineSnapshot(`
|
|
||||||
Object {
|
|
||||||
"offset": 0,
|
|
||||||
"rows": Array [
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000",
|
|
||||||
"key": "gpx/4320000000000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-49baa096ec0c89962f2cafd3ff50b80b",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/2rte/000000",
|
|
||||||
"key": "gpx/4320000000000000/2rte/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-2ca14f512a9c83f5a239389e580befce",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Object {
|
|
||||||
"id": "gpx/4320000000000000/2rte/000000/000000",
|
|
||||||
"key": "gpx/4320000000000000/2rte/000000/000000",
|
|
||||||
"value": Object {
|
|
||||||
"rev": "1-0f4064d20f6bfac3888a7758851fbac5",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
"total_rows": 5,
|
|
||||||
}
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getDocsByType', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await initDb({});
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(async () => {
|
|
||||||
await db.destroy();
|
|
||||||
db = undefined;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('gets the rte amongst other docs', async () => {
|
|
||||||
await putNewRtept();
|
|
||||||
const rtes = await getDocsByType('rte');
|
|
||||||
expect(rtes).toMatchInlineSnapshot(`
|
|
||||||
Array [
|
|
||||||
Object {
|
|
||||||
"_id": "gpx/4320000000000000/2rte/000000",
|
|
||||||
"_rev": "1-2ca14f512a9c83f5a239389e580befce",
|
|
||||||
"doc": Object {
|
|
||||||
"number": 0,
|
|
||||||
},
|
|
||||||
"type": "rte",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,52 +0,0 @@
|
||||||
import { cloneDeep } from 'lodash';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const put = async (
|
|
||||||
_id: string,
|
|
||||||
type: string,
|
|
||||||
update: (doc: any) => any,
|
|
||||||
defaultDoc: any
|
|
||||||
) => {
|
|
||||||
var current;
|
|
||||||
try {
|
|
||||||
current = await db.get(_id);
|
|
||||||
} catch {
|
|
||||||
current = { _rev: undefined, doc: cloneDeep(defaultDoc) };
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
db.put({ _id, _rev: current._rev, type, doc: update(current.doc) });
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.name === 'conflict') {
|
|
||||||
await put(_id, type, update, defaultDoc);
|
|
||||||
} else {
|
|
||||||
console.error(
|
|
||||||
`put(${_id}, ${JSON.stringify(
|
|
||||||
update(current.doc)
|
|
||||||
)}), error: ${JSON.stringify(error)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getFamily = async (key: string, options: any = {}) => {
|
|
||||||
return await db.allDocs({
|
|
||||||
startkey: key,
|
|
||||||
endkey: key + '\ufff0',
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const get = async (id: string) => {
|
|
||||||
await db.get(id);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putAll = async (docs: any[]) => {
|
|
||||||
return await db.bulkDocs(docs);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getDocsByType = async (type: string) => {
|
|
||||||
return (await db.find({ selector: { type: type } })).docs;
|
|
||||||
};
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { putNewRte } from './rte';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The rte module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new rte when required', async () => {
|
|
||||||
putNewRte({ gpx: 4320000000000000, rte: 25 });
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/4320000000000000/2rte/000025',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
link: undefined,
|
|
||||||
name: undefined,
|
|
||||||
number: 0,
|
|
||||||
rtept: undefined,
|
|
||||||
src: undefined,
|
|
||||||
type: undefined,
|
|
||||||
},
|
|
||||||
type: 'rte',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the trk if needed', async () => {
|
|
||||||
const id = await putNewRte({ gpx: 0 });
|
|
||||||
expect(id).toEqual({ gpx: 0, rte: 0 });
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and trk if needed', async () => {
|
|
||||||
const id = await putNewRte();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000, rte: 0 });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,33 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { putNewGpx } from './gpx';
|
|
||||||
import { put } from './lib';
|
|
||||||
|
|
||||||
export const emptyRte: Rte = {
|
|
||||||
name: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
src: undefined,
|
|
||||||
link: undefined,
|
|
||||||
number: 0,
|
|
||||||
type: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
rtept: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewRte = async (id?: IdRte | IdGpx) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('rte' in finalId)) {
|
|
||||||
const gpxId = await putNewGpx(id);
|
|
||||||
finalId = { ...gpxId, rte: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('rte', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'rte',
|
|
||||||
(rte) => {
|
|
||||||
return rte;
|
|
||||||
},
|
|
||||||
emptyRte
|
|
||||||
);
|
|
||||||
return finalId as IdRte;
|
|
||||||
};
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { putNewRtept } from './rtept';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The rtept module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new rtept when required', async () => {
|
|
||||||
putNewRtept({ gpx: 0, rte: 0, rtept: 0 });
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/0000000000000000/2rte/000000/000000',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
ele: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
fix: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
link: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
name: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
src: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
time: undefined,
|
|
||||||
type: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
},
|
|
||||||
type: 'rtept',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the rtept if needed', async () => {
|
|
||||||
const id = await putNewRtept({ gpx: 0 });
|
|
||||||
expect(id).toEqual({ gpx: 0, rte: 0, rtept: 0 });
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and rte if needed', async () => {
|
|
||||||
const id = await putNewRtept();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000, rte: 0, rtept: 0 });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,44 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { put } from './lib';
|
|
||||||
import { putNewRte } from './rte';
|
|
||||||
|
|
||||||
export const emptyRtept: Wpt = {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ele: undefined,
|
|
||||||
time: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
name: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
src: undefined,
|
|
||||||
link: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
type: undefined,
|
|
||||||
fix: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewRtept = async (id?: IdGpx | IdRte | IdRtept) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('rtept' in finalId)) {
|
|
||||||
const rteId = await putNewRte(id);
|
|
||||||
finalId = { ...rteId, rtept: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('rtept', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'rtept',
|
|
||||||
(rtept) => {
|
|
||||||
return rtept;
|
|
||||||
},
|
|
||||||
emptyRtept
|
|
||||||
);
|
|
||||||
return finalId as IdRtept;
|
|
||||||
};
|
|
|
@ -1,47 +0,0 @@
|
||||||
import { putNewTrk } from './trk';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The trk module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new trk when required', async () => {
|
|
||||||
putNewTrk({ gpx: 1, trk: 2 });
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/0000000000000001/3trk/000002',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
link: undefined,
|
|
||||||
name: undefined,
|
|
||||||
number: 0,
|
|
||||||
src: undefined,
|
|
||||||
trkseg: undefined,
|
|
||||||
type: undefined,
|
|
||||||
},
|
|
||||||
type: 'trk',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the trk if needed', async () => {
|
|
||||||
const id = await putNewTrk({ gpx: 2 });
|
|
||||||
expect(id).toEqual({ gpx: 2, trk: 0});
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and trk if needed', async () => {
|
|
||||||
const id = await putNewTrk();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000, trk: 0});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,42 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { putNewGpx } from './gpx';
|
|
||||||
import { getFamily, put } from './lib';
|
|
||||||
|
|
||||||
export const emptyTrk: Trk = {
|
|
||||||
name: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
src: undefined,
|
|
||||||
link: undefined,
|
|
||||||
number: 0,
|
|
||||||
type: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
trkseg: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewTrk = async (id?: IdTrk | IdGpx) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('trk' in finalId)) {
|
|
||||||
const gpxId = await putNewGpx(id);
|
|
||||||
finalId = { ...gpxId, trk: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('trk', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'trk',
|
|
||||||
(trk) => {
|
|
||||||
return trk;
|
|
||||||
},
|
|
||||||
emptyTrk
|
|
||||||
);
|
|
||||||
return finalId as IdTrk;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTrk = async (params: any) => {
|
|
||||||
const { id } = params;
|
|
||||||
const docs = await getFamily(id, { include_docs: true });
|
|
||||||
console.log(`getTrk, uri: ${id} docs: ${JSON.stringify(docs)}`);
|
|
||||||
return docs.rows.filter(
|
|
||||||
(row: any) => row.doc.type === 'trk' || row.doc.type === 'trkseg'
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,79 +0,0 @@
|
||||||
import { putNewTrkpt } from './trkpt';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The trkpt module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new trkpt when required', async () => {
|
|
||||||
putNewTrkpt({
|
|
||||||
gpx: 1,
|
|
||||||
trk: 2,
|
|
||||||
trkseg: 3,
|
|
||||||
trkpt: 4,
|
|
||||||
});
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/0000000000000001/3trk/000002/000003/000004',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
ele: undefined,
|
|
||||||
extensions: {
|
|
||||||
'dyo:accuracy': undefined,
|
|
||||||
'dyo:batterylevel': undefined,
|
|
||||||
'dyo:course': undefined,
|
|
||||||
'dyo:speed': undefined,
|
|
||||||
'dyo:useragent': undefined,
|
|
||||||
},
|
|
||||||
fix: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
link: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
name: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
src: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
time: undefined,
|
|
||||||
type: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
},
|
|
||||||
type: 'trkpt',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the trk if needed', async () => {
|
|
||||||
const id = await putNewTrkpt({ gpx: 5 });
|
|
||||||
expect(id).toEqual({
|
|
||||||
gpx: 5,
|
|
||||||
trk: 0,
|
|
||||||
trkseg: 0,
|
|
||||||
trkpt: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and trk if needed', async () => {
|
|
||||||
const id = await putNewTrkpt();
|
|
||||||
expect(id).toEqual({
|
|
||||||
gpx: 4320000000000000,
|
|
||||||
trk: 0,
|
|
||||||
trkseg: 0,
|
|
||||||
trkpt: 0,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,50 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { put } from './lib';
|
|
||||||
import { putNewTrkseg } from './trkseg';
|
|
||||||
|
|
||||||
const emptyTrkpt: Wpt = {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ele: undefined,
|
|
||||||
time: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
name: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
src: undefined,
|
|
||||||
link: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
type: undefined,
|
|
||||||
fix: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
extensions: {
|
|
||||||
'dyo:speed': undefined,
|
|
||||||
'dyo:course': undefined,
|
|
||||||
'dyo:accuracy': undefined,
|
|
||||||
'dyo:batterylevel': undefined,
|
|
||||||
'dyo:useragent': undefined,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewTrkpt = async (id?: IdTrk | IdGpx | IdTrkseg | IdTrkpt) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('trkpt' in finalId)) {
|
|
||||||
const trksegId = await putNewTrkseg(id);
|
|
||||||
finalId = { ...trksegId, trkpt: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('trkpt', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'trkpt',
|
|
||||||
(trkpt) => {
|
|
||||||
return trkpt;
|
|
||||||
},
|
|
||||||
emptyTrkpt
|
|
||||||
);
|
|
||||||
return finalId;
|
|
||||||
};
|
|
|
@ -1,40 +0,0 @@
|
||||||
import { putNewTrkseg } from './trkseg';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The trkseg module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new trk when required', async () => {
|
|
||||||
putNewTrkseg({ gpx: 1234567890123456, trk: 123456, trkseg: 5 });
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/1234567890123456/3trk/123456/000005',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
trkpt: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
},
|
|
||||||
type: 'trkseg',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the trk if needed', async () => {
|
|
||||||
const id = await putNewTrkseg({ gpx: 1 });
|
|
||||||
expect(id).toEqual({ gpx: 1, trk: 0, trkseg: 0 });
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and trk if needed', async () => {
|
|
||||||
const id = await putNewTrkseg();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000, trk: 0, trkseg: 0 });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,33 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { getFamily, put } from './lib';
|
|
||||||
import { putNewTrk } from './trk';
|
|
||||||
|
|
||||||
const emptyTrkseg: Trkseg = {
|
|
||||||
trkpt: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewTrkseg = async (id?: IdTrk | IdGpx | IdTrkseg) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('trkseg' in finalId)) {
|
|
||||||
const trkId = await putNewTrk(id);
|
|
||||||
finalId = { ...trkId, trkseg: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('trkseg', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'trkseg',
|
|
||||||
(trkseg) => {
|
|
||||||
return trkseg;
|
|
||||||
},
|
|
||||||
emptyTrkseg
|
|
||||||
);
|
|
||||||
return finalId as IdTrkseg;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTrkseg = async (params: any) => {
|
|
||||||
const { id } = params;
|
|
||||||
const docs = await getFamily(id, { include_docs: true });
|
|
||||||
console.log(`getTrkseg, uri: ${id} docs: ${JSON.stringify(docs)}`);
|
|
||||||
return docs.rows;
|
|
||||||
};
|
|
|
@ -1,119 +0,0 @@
|
||||||
interface Gpx {
|
|
||||||
$: Gpx_;
|
|
||||||
metadata?: Metadata;
|
|
||||||
wpt?: Wpt[];
|
|
||||||
rte?: Rte[];
|
|
||||||
trk?: Trk[];
|
|
||||||
extensions?: Extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Gpx_ {
|
|
||||||
version: '1.1';
|
|
||||||
creator: string;
|
|
||||||
xmlns: 'http://www.topografix.com/GPX/1/1';
|
|
||||||
'xmlns:xsi'?: 'http://www.w3.org/2001/XMLSchema-instance';
|
|
||||||
'xsi:schemaLocation'?: string;
|
|
||||||
'xmlns:gpxx'?: string;
|
|
||||||
'xmlns:wptx1'?: string;
|
|
||||||
'xmlns:gpxtpx'?: string;
|
|
||||||
'xmlns:dyo'?: 'http://xmlns.dyomedea.com/';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Metadata {
|
|
||||||
name?: string;
|
|
||||||
desc?: string;
|
|
||||||
author?: string;
|
|
||||||
copyright?: string;
|
|
||||||
link?: Link[];
|
|
||||||
time?: string;
|
|
||||||
keywords?: string;
|
|
||||||
bounds?: Bounds;
|
|
||||||
extensions?: Extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Bounds {
|
|
||||||
$: Bounds_;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Bounds_ {
|
|
||||||
minlat: number;
|
|
||||||
minlon: number;
|
|
||||||
maxlat: number;
|
|
||||||
maxlon: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Extensions {
|
|
||||||
'dyo:speed'?: number;
|
|
||||||
'dyo:course'?: number;
|
|
||||||
'dyo:accuracy'?: number;
|
|
||||||
'dyo:batterylevel'?: number;
|
|
||||||
'dyo:useragent'?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Trk {
|
|
||||||
name?: string;
|
|
||||||
cmt?: string;
|
|
||||||
desc?: string;
|
|
||||||
src?: string;
|
|
||||||
link?: Link[];
|
|
||||||
number?: number;
|
|
||||||
type?: string;
|
|
||||||
extensions?: Extensions;
|
|
||||||
trkseg?: Trkseg[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Link {
|
|
||||||
$: Link_;
|
|
||||||
text?: string;
|
|
||||||
type?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Link_ {
|
|
||||||
href: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Trkseg {
|
|
||||||
trkpt?: Wpt[];
|
|
||||||
extensions?: Extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Wpt {
|
|
||||||
$: Wpt_;
|
|
||||||
ele?: number;
|
|
||||||
time?: string;
|
|
||||||
magvar?: number;
|
|
||||||
geoidheight?: number;
|
|
||||||
name?: string;
|
|
||||||
cmt?: string;
|
|
||||||
desc?: string;
|
|
||||||
src?: string;
|
|
||||||
link?: Link;
|
|
||||||
sym?: string;
|
|
||||||
type?: string;
|
|
||||||
fix?: string;
|
|
||||||
sat?: number;
|
|
||||||
hdop?: number;
|
|
||||||
vdop?: number;
|
|
||||||
pdop?: number;
|
|
||||||
ageofdgpsdata?: number;
|
|
||||||
dgpsid?: number;
|
|
||||||
extensions?: Extensions;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Wpt_ {
|
|
||||||
lat: number;
|
|
||||||
lon: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Rte {
|
|
||||||
name?: string;
|
|
||||||
cmt?: string;
|
|
||||||
desc?: string;
|
|
||||||
src?: string;
|
|
||||||
link?: Link[];
|
|
||||||
number?: number;
|
|
||||||
type?: string;
|
|
||||||
extensions?: Extensions;
|
|
||||||
rtept?: Wpt[];
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,58 +0,0 @@
|
||||||
import { putNewWpt } from './wpt';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: any;
|
|
||||||
var dbReady: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const originalDb = globalThis.db;
|
|
||||||
const originalDateNow = globalThis.Date.now;
|
|
||||||
|
|
||||||
describe('The wpt module', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
globalThis.db = { put: jest.fn() };
|
|
||||||
globalThis.Date.now = () => 0;
|
|
||||||
});
|
|
||||||
afterEach(() => {
|
|
||||||
globalThis.db = originalDb;
|
|
||||||
globalThis.Date.now = originalDateNow;
|
|
||||||
});
|
|
||||||
test('db.put() a new wpt when required', async () => {
|
|
||||||
putNewWpt({ gpx: 1, wpt: 2 });
|
|
||||||
await expect(globalThis.db.put).toBeCalledWith({
|
|
||||||
_id: 'gpx/0000000000000001/1wpt/000002',
|
|
||||||
_rev: undefined,
|
|
||||||
doc: {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
ele: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
fix: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
link: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
name: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
src: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
time: undefined,
|
|
||||||
type: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
},
|
|
||||||
type: 'wpt',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('db.put() generates an id for the wpt if needed', async () => {
|
|
||||||
const id = await putNewWpt({ gpx: 1 });
|
|
||||||
expect(id).toEqual({ gpx: 1, wpt: 0 });
|
|
||||||
});
|
|
||||||
test('db.put() generates ids for both gpx and trk if needed', async () => {
|
|
||||||
const id = await putNewWpt();
|
|
||||||
expect(id).toEqual({ gpx: 4320000000000000, wpt: 0 });
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,44 +0,0 @@
|
||||||
import getUri from '../lib/ids';
|
|
||||||
import { putNewGpx } from './gpx';
|
|
||||||
import { put } from './lib';
|
|
||||||
|
|
||||||
export const emptyWpt: Wpt = {
|
|
||||||
$: { lat: 0, lon: 0 },
|
|
||||||
ele: undefined,
|
|
||||||
time: undefined,
|
|
||||||
magvar: undefined,
|
|
||||||
geoidheight: undefined,
|
|
||||||
name: undefined,
|
|
||||||
cmt: undefined,
|
|
||||||
desc: undefined,
|
|
||||||
src: undefined,
|
|
||||||
link: undefined,
|
|
||||||
sym: undefined,
|
|
||||||
type: undefined,
|
|
||||||
fix: undefined,
|
|
||||||
sat: undefined,
|
|
||||||
hdop: undefined,
|
|
||||||
vdop: undefined,
|
|
||||||
pdop: undefined,
|
|
||||||
ageofdgpsdata: undefined,
|
|
||||||
dgpsid: undefined,
|
|
||||||
extensions: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const putNewWpt = async (id?: IdGpx | IdWpt) => {
|
|
||||||
let finalId = { ...id };
|
|
||||||
if (!('wpt' in finalId)) {
|
|
||||||
const gpxId = await putNewGpx(id);
|
|
||||||
finalId = { ...gpxId, wpt: 0 };
|
|
||||||
}
|
|
||||||
const uri = getUri('wpt', finalId);
|
|
||||||
await put(
|
|
||||||
uri,
|
|
||||||
'wpt',
|
|
||||||
(wpt) => {
|
|
||||||
return wpt;
|
|
||||||
},
|
|
||||||
emptyWpt
|
|
||||||
);
|
|
||||||
return finalId as IdWpt;
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
import LocalizedStrings from 'react-localization';
|
|
||||||
|
|
||||||
const strings = new LocalizedStrings({
|
|
||||||
en: {
|
|
||||||
colonize: (input: string): any => strings.formatString('{0}:', input),
|
|
||||||
|
|
||||||
common: { save: 'Save', cancel: 'Cancel', close: 'Close' },
|
|
||||||
|
|
||||||
mapChooser: {
|
|
||||||
chooseYourMap: 'Choose your map',
|
|
||||||
},
|
|
||||||
|
|
||||||
explorer: {
|
|
||||||
nearBy: 'Nearby',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
fr: {
|
|
||||||
colonize: (input: string): any => strings.formatString('{0} :', input),
|
|
||||||
|
|
||||||
common: {
|
|
||||||
save: 'Sauvegarder',
|
|
||||||
cancel: 'Annuler',
|
|
||||||
close: 'Fermer',
|
|
||||||
},
|
|
||||||
|
|
||||||
mapChooser: {
|
|
||||||
chooseYourMap: 'Choisissez votre carte',
|
|
||||||
},
|
|
||||||
|
|
||||||
explorer: {
|
|
||||||
nearBy: "Près d'ici",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export default strings;
|
|
||||||
|
|
||||||
export const setI18nLanguage = (language: string) => {
|
|
||||||
if (language === undefined || language === 'auto') {
|
|
||||||
strings.setLanguage(strings.getInterfaceLanguage());
|
|
||||||
} else {
|
|
||||||
strings.setLanguage(language);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1 +0,0 @@
|
||||||
<svg width="100px" height="100px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--gis" preserveAspectRatio="xMidYMid meet"><path d="M50 0a3.5 3.5 0 0 0-3.5 3.5v80A3.5 3.5 0 0 0 50 87a3.5 3.5 0 0 0 3.5-3.5V47h43a3.5 3.5 0 0 0 3.5-3.5v-40A3.5 3.5 0 0 0 96.5 0h-46a3.5 3.5 0 0 0-.254.01A3.5 3.5 0 0 0 50 0zm13.8 7h9.8v7.43h9.8V7H93v7.43h-9.6v9.799H93v9.8h-9.6V40h-9.8v-5.97h-9.8V40H54v-5.97h9.8v-9.801H54v-9.8h9.8V7zm0 7.43v9.799h9.8v-9.8h-9.8zm9.8 9.799v9.8h9.8v-9.8h-9.8z" fill="currentColor"></path><path d="M38.004 76.792C27.41 78.29 20 81.872 20 87.143C20 94.243 32.381 100 50 100s30-5.756 30-12.857c0-5.272-7.41-8.853-18.003-10.35l-1.468 2.499C68.514 80.399 74 82.728 74 85.429c0 3.787-10.745 6.857-24 6.857s-24-3.07-24-6.857c-.001-2.692 5.45-5.018 13.459-6.13c-.484-.836-.97-1.67-1.455-2.507z" fill="currentColor"></path></svg>
|
|
Before Width: | Height: | Size: 937 B |
|
@ -1 +0,0 @@
|
||||||
<svg width="100px" height="100px" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--gis" preserveAspectRatio="xMidYMid meet"><path d="M50 0a3.5 3.5 0 0 0-3.5 3.5v80A3.5 3.5 0 0 0 50 87a3.5 3.5 0 0 0 3.5-3.5V47h43a3.5 3.5 0 0 0 3.5-3.5v-40A3.5 3.5 0 0 0 96.5 0h-46a3.5 3.5 0 0 0-.254.01A3.5 3.5 0 0 0 50 0z" fill="currentColor"></path><path d="M38.004 76.792C27.41 78.29 20 81.872 20 87.143C20 94.243 32.381 100 50 100s30-5.756 30-12.857c0-5.272-7.41-8.853-18.003-10.35l-1.468 2.499C68.514 80.399 74 82.728 74 85.429c0 3.787-10.745 6.857-24 6.857s-24-3.07-24-6.857c-.001-2.692 5.45-5.018 13.459-6.13c-.484-.836-.97-1.67-1.455-2.507z" fill="currentColor"></path></svg>
|
|
Before Width: | Height: | Size: 770 B |
|
@ -1,61 +0,0 @@
|
||||||
/**
|
|
||||||
* @hidden
|
|
||||||
*/
|
|
||||||
export const thisIsAModule = true;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var _allCaches: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis._allCaches = new Map();
|
|
||||||
|
|
||||||
const cache = {
|
|
||||||
set: (params: any) => {
|
|
||||||
const { cacheId, key, value } = params;
|
|
||||||
if (!_allCaches.has(cacheId)) {
|
|
||||||
_allCaches.set(cacheId, new Map());
|
|
||||||
}
|
|
||||||
const k = _allCaches.get(cacheId);
|
|
||||||
k.set(key, value);
|
|
||||||
},
|
|
||||||
get: (params: any) => {
|
|
||||||
const { cacheId, key } = params;
|
|
||||||
if (!_allCaches.has(cacheId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const k = _allCaches.get(cacheId);
|
|
||||||
if (!k.has(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const value = k.get(key);
|
|
||||||
k.delete(key);
|
|
||||||
k.set(key, value);
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
delete: (params: any) => {
|
|
||||||
const { cacheId, key } = params;
|
|
||||||
if (!_allCaches.has(cacheId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const k = _allCaches.get(cacheId);
|
|
||||||
if (!k.has(key)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const value = k.get(key);
|
|
||||||
k.delete(key);
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
map: (params: any) => {
|
|
||||||
const { cacheId } = params;
|
|
||||||
if (!_allCaches.has(cacheId)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return _allCaches.get(cacheId);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default cache;
|
|
|
@ -1,137 +0,0 @@
|
||||||
/*
|
|
||||||
* DocURI: Rich document ids for CouchDB.
|
|
||||||
*
|
|
||||||
* Copyright (c) 2014 null2 GmbH Berlin
|
|
||||||
* Licensed under the MIT license.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// type/id/subtype/index/version
|
|
||||||
|
|
||||||
var docuri = module.exports = exports = {};
|
|
||||||
|
|
||||||
// Cached regular expressions for matching named param parts and splatted parts
|
|
||||||
// of route strings.
|
|
||||||
// http://backbonejs.org/docs/backbone.html#section-158
|
|
||||||
var optionalParam = /\((.*?)\)/g;
|
|
||||||
var namedParam = /(\(\?)?:\w+/g;
|
|
||||||
var splatParam = /\*\w+/g;
|
|
||||||
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
|
|
||||||
var paramKeys = /[*:]\w+/g;
|
|
||||||
|
|
||||||
// Convert a route string into a regular expression,
|
|
||||||
// with named regular expressions for named arguments.
|
|
||||||
// http://backbonejs.org/docs/backbone.html#section-165
|
|
||||||
function routeToRegExp(src) {
|
|
||||||
var keys = [], match;
|
|
||||||
|
|
||||||
while ( ( match = paramKeys.exec( src ) ) !== null )
|
|
||||||
{
|
|
||||||
keys.push( match[0] );
|
|
||||||
}
|
|
||||||
|
|
||||||
var route = src.replace(escapeRegExp, '\\$&')
|
|
||||||
.replace(optionalParam, '(?:$1)?')
|
|
||||||
.replace(namedParam, function(match, optional) {
|
|
||||||
|
|
||||||
return optional ? match : '([^/?]+)';
|
|
||||||
})
|
|
||||||
.replace(splatParam, '([^?]*?)');
|
|
||||||
|
|
||||||
keys = keys.reduce(function(memo, key) {
|
|
||||||
var value = '\\' + key;
|
|
||||||
|
|
||||||
memo[key] = new RegExp(value + '(\\/|\\)|\\(|$)');
|
|
||||||
|
|
||||||
return memo;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
return {
|
|
||||||
src: src,
|
|
||||||
exp: new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$'),
|
|
||||||
keys: keys
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a route and a DocURI return an object of extracted parameters.
|
|
||||||
// Unmatched DocURIs will be treated as false.
|
|
||||||
// http://backbonejs.org/docs/backbone.html#section-166
|
|
||||||
function extractParameters(route, fragment, coding) {
|
|
||||||
var params = route.exp.exec(fragment);
|
|
||||||
|
|
||||||
if (!params) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
params = params.slice(1);
|
|
||||||
|
|
||||||
return Object.keys(route.keys).reduce(function(memo, key, i) {
|
|
||||||
var param = params[i];
|
|
||||||
|
|
||||||
if (param) {
|
|
||||||
|
|
||||||
var k = key.substr(1);
|
|
||||||
var decoder = (coding[k] ?? {decoder:decodeURIComponent}).decoder;
|
|
||||||
|
|
||||||
if (key[0] === '*') {
|
|
||||||
param = param.split('/').map(decoder);
|
|
||||||
} else {
|
|
||||||
param = decoder(param);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
memo[key.substr(1)] = param;
|
|
||||||
}
|
|
||||||
|
|
||||||
return memo;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert named parameters from object.
|
|
||||||
function insertParameters(route, obj, coding) {
|
|
||||||
var str = route.src;
|
|
||||||
|
|
||||||
Object.keys(route.keys).forEach(function(key) {
|
|
||||||
var k = key.substr(1);
|
|
||||||
var value = (obj[k] !== undefined) ? obj[k] : '';
|
|
||||||
|
|
||||||
var encoder = (coding[k] ?? {encoder:encodeURIComponent}).encoder;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value = value.map(encoder).join('/');
|
|
||||||
} else {
|
|
||||||
value = encoder(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
str = str.replace(route.keys[key], value + '$1');
|
|
||||||
});
|
|
||||||
|
|
||||||
// massage optional parameter
|
|
||||||
return str
|
|
||||||
.replace(/\(\/\)/g, '')
|
|
||||||
.replace(/[)(]/g, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
docuri.route = function(route, coding={}) {
|
|
||||||
route = routeToRegExp(route);
|
|
||||||
|
|
||||||
return function(source, target) {
|
|
||||||
source = source || {};
|
|
||||||
|
|
||||||
if (target) {
|
|
||||||
source = extractParameters(route, source, coding);
|
|
||||||
Object.keys(target).forEach(function(key) {
|
|
||||||
source[key] = target[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof source === 'object') {
|
|
||||||
return insertParameters(route, source, coding);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof source === 'string') {
|
|
||||||
return extractParameters(route, source, coding);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Rectangle } from '../components/map/types';
|
|
||||||
|
|
||||||
// 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)));
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rectanglesIntersect = (r1: Rectangle, r2: Rectangle) =>
|
|
||||||
!(
|
|
||||||
r2.topLeft.x > r1.bottomRight.x ||
|
|
||||||
r2.bottomRight.x < r1.topLeft.x ||
|
|
||||||
r2.topLeft.y > r1.bottomRight.y ||
|
|
||||||
r2.bottomRight.y < r1.topLeft.y
|
|
||||||
);
|
|
|
@ -1,21 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2017 Zheng-Xiang Ke
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
|
@ -1,91 +0,0 @@
|
||||||
# gpx-parser-builder
|
|
||||||
A simple gpx parser and builder between GPX string and JavaScript object. It is dependent on [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
|
|
||||||
|
|
||||||
[![npm](https://img.shields.io/npm/dt/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
|
|
||||||
[![GitHub stars](https://img.shields.io/github/stars/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/stargazers)
|
|
||||||
[![GitHub forks](https://img.shields.io/github/forks/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/network)
|
|
||||||
[![npm](https://img.shields.io/npm/v/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
|
|
||||||
[![GitHub license](https://img.shields.io/github/license/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/blob/master/LICENSE)
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
gpx-parser-builder is written with ECMAScript 6. You can leverage [Babel](https://babeljs.io/) and [Webpack](https://webpack.js.org/) to make all browsers available.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install gpx-parser-builder --save
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version
|
|
||||||
|
|
||||||
v1.0.0+ is a breaking change for v0.2.2-. v1.0.0+ fully supports gpx files including waypoints, routes, and tracks. Every gpx type is 1-1 corresponding to a JavaScript class.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
import GPX from 'gpx-parser-builder';
|
|
||||||
|
|
||||||
// Parse gpx
|
|
||||||
const gpx = GPX.parse('GPX_STRING');
|
|
||||||
|
|
||||||
window.console.dir(gpx.metadata);
|
|
||||||
window.console.dir(gpx.wpt);
|
|
||||||
window.console.dir(gpx.trk);
|
|
||||||
|
|
||||||
// Build gpx
|
|
||||||
window.console.log(gpx.toString());
|
|
||||||
```
|
|
||||||
|
|
||||||
Get more details about usage with the unit tests.
|
|
||||||
|
|
||||||
### GPX
|
|
||||||
|
|
||||||
The GPX JavaScript object.
|
|
||||||
|
|
||||||
`constructor(object)`
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
const gpx = new Gpx({$:{...}, metadat: {...}, wpt:[{...},{...}]}, trk: {...}, rte: {...})
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Member Variables
|
|
||||||
|
|
||||||
`$` the attributes for the gpx element. Default value:
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
'version': '1.1',
|
|
||||||
'creator': 'gpx-parser-builder',
|
|
||||||
'xmlns': 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
||||||
'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`metadata` the metadata for the gpx.
|
|
||||||
|
|
||||||
`wpt` array of waypoints. It is corresponded to `<wpt>`. The type of all elements in `wpt` is `Waypoint`;
|
|
||||||
|
|
||||||
`rte` array of routes. It is corresponded to `<rte>`. The type of all elements in `rte` is `Route`;
|
|
||||||
|
|
||||||
`trk` array of tracks. It is corresponded to `<trk>`. The type of all elements in `trk` is `Track`;
|
|
||||||
|
|
||||||
#### Static Methods
|
|
||||||
|
|
||||||
`parse(gpxString)` parse gpx string to Gpx object. return `null` if parsing failed.
|
|
||||||
|
|
||||||
#### Member Methods
|
|
||||||
|
|
||||||
`toString(options)` GPX object to gpx string. The options is for [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
|
|
||||||
|
|
||||||
## Save as GPX file in the frontend
|
|
||||||
|
|
||||||
You can leverage [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) or [FileSaver.js](https://github.com/eligrey/FileSaver.js) to save as GPX file. ⚠️Not all borwsers support the above file techniques. ⚠️️️
|
|
||||||
|
|
||||||
## Author
|
|
||||||
|
|
||||||
Zheng-Xiang Ke, kf99916@gmail.com
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
gpx-parser-builder is available under the MIT license. See the LICENSE file for more info.
|
|
|
@ -1,36 +0,0 @@
|
||||||
{
|
|
||||||
"name": "gpx-parser-builder",
|
|
||||||
"version": "1.0.2",
|
|
||||||
"description": "A simple gpx parser and builder between GPX string and JavaScript object",
|
|
||||||
"main": "./src/gpx.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "mocha --require @babel/register test/**/*.js"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/kf99916/gpx-parser-builder.git"
|
|
||||||
},
|
|
||||||
"keywords": [
|
|
||||||
"gpx",
|
|
||||||
"parser",
|
|
||||||
"builder"
|
|
||||||
],
|
|
||||||
"author": "Zheng-Xiang Ke",
|
|
||||||
"license": "MIT",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/kf99916/gpx-parser-builder/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/kf99916/gpx-parser-builder",
|
|
||||||
"files": [
|
|
||||||
"src"
|
|
||||||
],
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "~7.7",
|
|
||||||
"@babel/preset-env": "~7.7",
|
|
||||||
"@babel/register": "~7.7",
|
|
||||||
"mocha": "~6.2"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"isomorphic-xml2js": "~0.1"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default class Bounds {
|
|
||||||
constructor(object) {
|
|
||||||
this.minlat = object.minlat;
|
|
||||||
this.minlon = object.minlon;
|
|
||||||
this.maxlat = object.maxlat;
|
|
||||||
this.maxlon = object.maxlon;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
export default class Copyright {
|
|
||||||
constructor(object) {
|
|
||||||
this.author = object.author;
|
|
||||||
this.year = object.year;
|
|
||||||
this.license = object.license;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
declare module 'gpx-parser-builder' {
|
|
||||||
class GPX {
|
|
||||||
static parse(gpxString: any): any;
|
|
||||||
constructor(object: any);
|
|
||||||
$: any;
|
|
||||||
extensions: any;
|
|
||||||
metadata: any;
|
|
||||||
wpt: any;
|
|
||||||
rte: any;
|
|
||||||
trk: any;
|
|
||||||
toString(options: any): string;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
import * as xml2js from 'isomorphic-xml2js';
|
|
||||||
import Metadata from './metadata';
|
|
||||||
import Waypoint from './waypoint';
|
|
||||||
import Route from './route';
|
|
||||||
import Track from './track';
|
|
||||||
import {removeEmpty, allDatesToISOString} from './utils';
|
|
||||||
|
|
||||||
const defaultAttributes = {
|
|
||||||
version: '1.1',
|
|
||||||
creator: 'gpx-parser-builder',
|
|
||||||
xmlns: 'http://www.topografix.com/GPX/1/1',
|
|
||||||
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
||||||
'xsi:schemaLocation':
|
|
||||||
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class GPX {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = Object.assign({}, defaultAttributes, object.$ || object.attributes || {});
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
|
|
||||||
if (object.metadata) {
|
|
||||||
this.metadata = new Metadata(object.metadata);
|
|
||||||
}
|
|
||||||
if (object.wpt) {
|
|
||||||
if (!Array.isArray(object.wpt)) {
|
|
||||||
object.wpt = [object.wpt];
|
|
||||||
}
|
|
||||||
this.wpt = object.wpt.map(wpt => new Waypoint(wpt));
|
|
||||||
}
|
|
||||||
if (object.rte) {
|
|
||||||
if (!Array.isArray(object.rte)) {
|
|
||||||
object.rte = [object.rte];
|
|
||||||
}
|
|
||||||
this.rte = object.rte.map(rte => new Route(rte));
|
|
||||||
}
|
|
||||||
if (object.trk) {
|
|
||||||
if (!Array.isArray(object.trk)) {
|
|
||||||
object.trk = [object.trk];
|
|
||||||
}
|
|
||||||
this.trk = object.trk.map(trk => new Track(trk));
|
|
||||||
}
|
|
||||||
|
|
||||||
removeEmpty(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
static parse(gpxString) {
|
|
||||||
let gpx;
|
|
||||||
xml2js.parseString(gpxString, {
|
|
||||||
explicitArray: false
|
|
||||||
}, (err, xml) => {
|
|
||||||
if (err) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!xml.gpx) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
gpx = new GPX({
|
|
||||||
attributes: xml.gpx.$,
|
|
||||||
metadata: xml.gpx.metadata,
|
|
||||||
wpt: xml.gpx.wpt,
|
|
||||||
rte: xml.gpx.rte,
|
|
||||||
trk: xml.gpx.trk
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return gpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(options) {
|
|
||||||
options = options || {};
|
|
||||||
options.rootName = 'gpx';
|
|
||||||
|
|
||||||
const builder = new xml2js.Builder(options), gpx = new GPX(this);
|
|
||||||
allDatesToISOString(gpx);
|
|
||||||
return builder.buildObject(gpx);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,8 +0,0 @@
|
||||||
export default class Link {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = {};
|
|
||||||
this.$.href = object.$.href || object.href;
|
|
||||||
this.text = object.text;
|
|
||||||
this.type = object.type;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +0,0 @@
|
||||||
import Copyright from './copyright';
|
|
||||||
import Link from './link';
|
|
||||||
import Person from './person';
|
|
||||||
import Bounds from './bounds';
|
|
||||||
|
|
||||||
export default class Metadata {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.time = object.time ? new Date(object.time) : new Date();
|
|
||||||
this.keywords = object.keywords;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.author) {
|
|
||||||
this.author = new Person(object.author);
|
|
||||||
}
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
if (object.bounds) {
|
|
||||||
this.bounds = new Bounds(object.bounds);
|
|
||||||
}
|
|
||||||
if (object.copyright) {
|
|
||||||
this.copyright = new Copyright(object.copyright);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Person {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.email = object.email;
|
|
||||||
if (object.link) {
|
|
||||||
this.link = new Link(object.link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import Waypoint from './waypoint';
|
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Route {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.number = object.number;
|
|
||||||
this.type = object.type;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
this.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (object.rtept) {
|
|
||||||
if (!Array.isArray(object.rtept)) {
|
|
||||||
this.rtept = [object.rtept];
|
|
||||||
}
|
|
||||||
this.rtept = object.rtept.map(pt => new Waypoint(pt));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
import Waypoint from './waypoint';
|
|
||||||
|
|
||||||
export default class TrackSegment {
|
|
||||||
constructor(object) {
|
|
||||||
if (object.trkpt) {
|
|
||||||
if (!Array.isArray(object.trkpt)) {
|
|
||||||
object.trkpt = [object.trkpt];
|
|
||||||
}
|
|
||||||
this.trkpt = object.trkpt.map(pt => new Waypoint(pt));
|
|
||||||
}
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import TrackSegment from './track-segment';
|
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Track {
|
|
||||||
constructor(object) {
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.number = object.number;
|
|
||||||
this.type = object.type;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
if (object.trkseg) {
|
|
||||||
if (!Array.isArray(object.trkseg)) {
|
|
||||||
object.trkseg = [object.trkseg];
|
|
||||||
}
|
|
||||||
this.trkseg = object.trkseg.map(seg => new TrackSegment(seg));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
function removeEmpty(obj) {
|
|
||||||
Object.entries(obj).forEach(([key, val]) => {
|
|
||||||
if (val && val instanceof Object) {
|
|
||||||
removeEmpty(val);
|
|
||||||
} else if (val == null) {
|
|
||||||
delete obj[key];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function allDatesToISOString(obj) {
|
|
||||||
Object.entries(obj).forEach(([key, val]) => {
|
|
||||||
if (val) {
|
|
||||||
if (val instanceof Date) {
|
|
||||||
obj[key] = val.toISOString().split('.')[0] + 'Z';
|
|
||||||
} else if (val instanceof Object) {
|
|
||||||
allDatesToISOString(val);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export { removeEmpty, allDatesToISOString };
|
|
|
@ -1,32 +0,0 @@
|
||||||
import Link from './link';
|
|
||||||
|
|
||||||
export default class Waypoint {
|
|
||||||
constructor(object) {
|
|
||||||
this.$ = {};
|
|
||||||
this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1;
|
|
||||||
this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1;
|
|
||||||
this.ele = object.ele;
|
|
||||||
this.time = object.time ? new Date(object.time) : new Date();
|
|
||||||
this.magvar = object.magvar;
|
|
||||||
this.geoidheight = object.geoidheight;
|
|
||||||
this.name = object.name;
|
|
||||||
this.cmt = object.cmt;
|
|
||||||
this.desc = object.desc;
|
|
||||||
this.src = object.src;
|
|
||||||
this.sym = object.sym;
|
|
||||||
this.type = object.type;
|
|
||||||
this.sat = object.sat;
|
|
||||||
this.hdop = object.hdop;
|
|
||||||
this.vdop = object.vdop;
|
|
||||||
this.pdop = object.pdop;
|
|
||||||
this.ageofdgpsdata = object.ageofdgpsdata;
|
|
||||||
this.dgpsid = object.dgpsid;
|
|
||||||
this.extensions = object.extensions;
|
|
||||||
if (object.link) {
|
|
||||||
if (!Array.isArray(object.link)) {
|
|
||||||
object.link = [object.link];
|
|
||||||
}
|
|
||||||
this.link = object.link.map(l => new Link(l));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { assert } from 'console';
|
|
||||||
import { findStartTime } from './gpx';
|
|
||||||
|
|
||||||
describe('findStartTime', () => {
|
|
||||||
test('to be undefined for a string', () => {
|
|
||||||
const start = findStartTime('');
|
|
||||||
expect(start).toBeUndefined();
|
|
||||||
});
|
|
||||||
test('to be undefined for an object without time key', () => {
|
|
||||||
const start = findStartTime({ foo: 'foo', bar: 'bar' });
|
|
||||||
expect(start).toBeUndefined();
|
|
||||||
});
|
|
||||||
test('to be the time value for an object with a time key', () => {
|
|
||||||
const start = findStartTime({ foo: 'foo', time: 'bar' });
|
|
||||||
expect(start).toEqual('bar');
|
|
||||||
});
|
|
||||||
test('to be the lowest time value for an object with several time keys', () => {
|
|
||||||
const start = findStartTime({
|
|
||||||
foo: { time: 'foo' },
|
|
||||||
time: 'bar',
|
|
||||||
bar: { time: 'a' },
|
|
||||||
});
|
|
||||||
expect(start).toEqual('a');
|
|
||||||
});
|
|
||||||
test('to be the lowest time value for an array with several objects with time keys', () => {
|
|
||||||
const start = findStartTime({
|
|
||||||
foos: [{ time: 'foo' }, { time: '0' }],
|
|
||||||
time: 'bar',
|
|
||||||
bar: { time: 'a' },
|
|
||||||
});
|
|
||||||
expect(start).toEqual('0');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,19 +0,0 @@
|
||||||
const min = (s1?: string, s2?: string) => {
|
|
||||||
return s1! < s2! ? s1 : s2;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const findStartTime = (x: any, startTime?: string) => {
|
|
||||||
if (typeof x === 'object') {
|
|
||||||
let newStartTime = startTime;
|
|
||||||
|
|
||||||
for (const key in x) {
|
|
||||||
if (key === 'time') {
|
|
||||||
newStartTime = min(newStartTime, x[key]);
|
|
||||||
} else {
|
|
||||||
newStartTime = findStartTime(x[key], newStartTime);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return newStartTime;
|
|
||||||
}
|
|
||||||
else return startTime;
|
|
||||||
};
|
|
|
@ -1,120 +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/3trk/:trk');
|
|
||||||
expect(gpx({ gpx: 10, trk: 0 })).toBe('gpx/10/3trk/0');
|
|
||||||
});
|
|
||||||
test(', using the two levels (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx/3trk/:trk');
|
|
||||||
expect(gpx('gpx/10/3trk/0')).toMatchObject({ gpx: '10', trk: '0' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking a multilevel route with optional part', () => {
|
|
||||||
test(', using the two levels', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/3trk/:trk)');
|
|
||||||
expect(gpx({ gpx: 10, trk: 0 })).toBe('gpx/10/3trk/0');
|
|
||||||
});
|
|
||||||
test(', using the two levels (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/3trk/:trk)');
|
|
||||||
expect(gpx('gpx/10/3trk/0')).toMatchObject({ gpx: '10', trk: '0' });
|
|
||||||
});
|
|
||||||
test(', using only one level', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/3trk/:trk)');
|
|
||||||
expect(gpx({ gpx: 10 })).toBe('gpx/10/3trk/'); //Unfortunately !
|
|
||||||
});
|
|
||||||
test(', using only one level (vive-versa)', () => {
|
|
||||||
const gpx = route('gpx/:gpx(/3trk/:trk)');
|
|
||||||
expect(gpx('gpx/10')).toMatchObject({ gpx: '10' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking gpx ids', () => {
|
|
||||||
const id = {
|
|
||||||
gpx: 1234567890123456,
|
|
||||||
};
|
|
||||||
const key = 'gpx/1234567890123456';
|
|
||||||
test(', vice', () => {
|
|
||||||
const gpx = uri('gpx', id);
|
|
||||||
expect(gpx).toBe(key);
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const gpx = uri('gpx', key);
|
|
||||||
expect(gpx).toMatchObject(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trk ids', () => {
|
|
||||||
const id = {
|
|
||||||
gpx: 1234567890123456,
|
|
||||||
trk: 123456,
|
|
||||||
};
|
|
||||||
const key = 'gpx/1234567890123456/3trk/123456';
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trk', id);
|
|
||||||
expect(rte).toBe(key);
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trk', key);
|
|
||||||
expect(rte).toMatchObject(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trkseg ids', () => {
|
|
||||||
const id = {
|
|
||||||
gpx: 111,
|
|
||||||
trk: 0,
|
|
||||||
trkseg: 3,
|
|
||||||
};
|
|
||||||
const key = 'gpx/0000000000000111/3trk/000000/000003';
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trkseg', id);
|
|
||||||
expect(rte).toBe(key);
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trkseg', key);
|
|
||||||
expect(rte).toMatchObject(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking trkpt ids', () => {
|
|
||||||
const id = {
|
|
||||||
gpx: 25,
|
|
||||||
trk: 8,
|
|
||||||
trkseg: 0,
|
|
||||||
trkpt: 155,
|
|
||||||
};
|
|
||||||
const key = 'gpx/0000000000000025/3trk/000008/000000/000155';
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('trkpt', id);
|
|
||||||
expect(rte).toBe(key);
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('trkpt', key);
|
|
||||||
expect(rte).toMatchObject(id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Checking settings id', () => {
|
|
||||||
test(', vice', () => {
|
|
||||||
const rte = uri('settings', {});
|
|
||||||
expect(rte).toBe('settings');
|
|
||||||
});
|
|
||||||
test(', and versa', () => {
|
|
||||||
const rte = uri('settings', 'settings');
|
|
||||||
expect(rte).toMatchObject({});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { route } from './docuri';
|
|
||||||
|
|
||||||
const integerType = (n: number) => {
|
|
||||||
return {
|
|
||||||
encoder: (v: number) => v.toString().padStart(n, '0'),
|
|
||||||
decoder: parseInt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const bigIntType = (n: number) => {
|
|
||||||
return {
|
|
||||||
encoder: (v: number) => v.toString().padStart(n, '0'),
|
|
||||||
decoder: BigInt,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const coding = {
|
|
||||||
gpx: integerType(16),
|
|
||||||
wpt: integerType(6),
|
|
||||||
rte: integerType(6),
|
|
||||||
rtept: integerType(6),
|
|
||||||
trk: integerType(6),
|
|
||||||
trkseg: integerType(6),
|
|
||||||
trkpt: integerType(6),
|
|
||||||
};
|
|
||||||
|
|
||||||
const routes = {
|
|
||||||
dbdef: route('dbdef', coding),
|
|
||||||
settings: route('settings', coding),
|
|
||||||
gpx: route('gpx/:gpx', coding),
|
|
||||||
wpt: route('gpx/:gpx/1wpt/:wpt', coding),
|
|
||||||
rte: route('gpx/:gpx/2rte/:rte', coding),
|
|
||||||
rtept: route('gpx/:gpx/2rte/:rte/:rtept', coding),
|
|
||||||
trk: route('gpx/:gpx/3trk/:trk', coding),
|
|
||||||
trkseg: route('gpx/:gpx/3trk/:trk/:trkseg', coding),
|
|
||||||
trkpt: route('gpx/:gpx/3trk/:trk/:trkseg/:trkpt', coding),
|
|
||||||
extensions: route('gpx/:gpx/4extensions', coding),
|
|
||||||
};
|
|
||||||
|
|
||||||
type RouteKey = keyof typeof routes;
|
|
||||||
|
|
||||||
const uri = (type: RouteKey, param: any) => {
|
|
||||||
return routes[type](param);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default uri;
|
|
||||||
|
|
||||||
const minDate = -8640000000000000;
|
|
||||||
const halfMinDate = minDate / 2;
|
|
||||||
|
|
||||||
export const intToGpxId = (i: number) => Math.round(i / 2) - halfMinDate;
|
|
|
@ -1,34 +0,0 @@
|
||||||
interface IdGpx {
|
|
||||||
gpx: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdTrk {
|
|
||||||
gpx: number;
|
|
||||||
trk: number;
|
|
||||||
}
|
|
||||||
interface IdTrkseg {
|
|
||||||
gpx: number;
|
|
||||||
trk: number;
|
|
||||||
trkseg: number;
|
|
||||||
}
|
|
||||||
interface IdTrkpt {
|
|
||||||
gpx: number;
|
|
||||||
trk: number;
|
|
||||||
trkseg: number;
|
|
||||||
trkpt: number;
|
|
||||||
}
|
|
||||||
interface IdWpt {
|
|
||||||
gpx: number;
|
|
||||||
wpt: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdRte {
|
|
||||||
gpx: number;
|
|
||||||
rte: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdRtept {
|
|
||||||
gpx: number;
|
|
||||||
rte: number;
|
|
||||||
rtept: number;
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'docuri';
|
|
|
@ -1 +0,0 @@
|
||||||
declare module 'gpx-parser-builder';
|
|
|
@ -1,10 +1,5 @@
|
||||||
import {
|
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
|
||||||
IonContent,
|
import ExploreContainer from '../components/ExploreContainer';
|
||||||
IonHeader,
|
|
||||||
IonPage,
|
|
||||||
IonTitle,
|
|
||||||
IonToolbar,
|
|
||||||
} from '@ionic/react';
|
|
||||||
import './Home.css';
|
import './Home.css';
|
||||||
|
|
||||||
const Home: React.FC = () => {
|
const Home: React.FC = () => {
|
||||||
|
@ -16,11 +11,12 @@ const Home: React.FC = () => {
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
<IonContent fullscreen>
|
<IonContent fullscreen>
|
||||||
<IonHeader collapse='condense'>
|
<IonHeader collapse="condense">
|
||||||
<IonToolbar>
|
<IonToolbar>
|
||||||
<IonTitle size='large'>Blank</IonTitle>
|
<IonTitle size="large">Blank</IonTitle>
|
||||||
</IonToolbar>
|
</IonToolbar>
|
||||||
</IonHeader>
|
</IonHeader>
|
||||||
|
<ExploreContainer />
|
||||||
</IonContent>
|
</IonContent>
|
||||||
</IonPage>
|
</IonPage>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,14 +1,8 @@
|
||||||
/* Ionic Variables and Theming. For more info, please see:
|
/* Ionic Variables and Theming. For more info, please see:
|
||||||
http://ionicframework.com/docs/theming/ */
|
http://ionicframework.com/docs/theming/ */
|
||||||
|
|
||||||
|
|
||||||
/** Ionic CSS Variables **/
|
/** Ionic CSS Variables **/
|
||||||
:root {
|
:root {
|
||||||
|
|
||||||
/** Transparent background so that underlying tiles and whiteboard can be seen **/
|
|
||||||
--ion-background-color: transparent;
|
|
||||||
|
|
||||||
|
|
||||||
/** primary **/
|
/** primary **/
|
||||||
--ion-color-primary: #3880ff;
|
--ion-color-primary: #3880ff;
|
||||||
--ion-color-primary-rgb: 56, 128, 255;
|
--ion-color-primary-rgb: 56, 128, 255;
|
||||||
|
|
|
@ -1,37 +0,0 @@
|
||||||
import dispatch, { init, worker } from './dispatcher-main';
|
|
||||||
|
|
||||||
jest.mock('./get-worker', () => ({
|
|
||||||
getWorker: () => ({
|
|
||||||
port: {
|
|
||||||
postMessage: jest.fn(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('The dispatcher-main', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
test('should create a new shared web worker', () => {
|
|
||||||
expect(worker).toBeDefined();
|
|
||||||
});
|
|
||||||
test('should create a onmessage function', () => {
|
|
||||||
expect(worker.port.onmessage).toBeDefined();
|
|
||||||
});
|
|
||||||
test('should return a promise if no callback is provided', () => {
|
|
||||||
expect(dispatch('ping')).toBeInstanceOf(Promise);
|
|
||||||
});
|
|
||||||
test('should forward the message', () => {
|
|
||||||
dispatch('ping');
|
|
||||||
expect(worker.port.postMessage).toBeCalledWith({ id: 0, payload: 'ping' });
|
|
||||||
});
|
|
||||||
test('should return the response', () => {
|
|
||||||
var response;
|
|
||||||
const callback = (error, success) => {
|
|
||||||
response = success;
|
|
||||||
};
|
|
||||||
dispatch('ping', callback);
|
|
||||||
worker.port.onmessage({ data: { id: 0, payload: 'pong' } });
|
|
||||||
expect(response).toEqual('pong');
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,51 +0,0 @@
|
||||||
import { getWorker } from './get-worker';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var dispatcherQueue: { index: number; queue: Map<number, any> };
|
|
||||||
}
|
|
||||||
|
|
||||||
export var worker: any;
|
|
||||||
|
|
||||||
export const init = () => {
|
|
||||||
globalThis.dispatcherQueue = { index: 0, queue: new Map() };
|
|
||||||
|
|
||||||
worker = getWorker();
|
|
||||||
|
|
||||||
console.log(`worker: ${worker}`);
|
|
||||||
|
|
||||||
worker.onmessage = (event: any) => {
|
|
||||||
const { id, payload } = event.data;
|
|
||||||
dispatcherQueue.queue.get(id)(null, payload);
|
|
||||||
dispatcherQueue.queue.delete(id);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const dispatch = (
|
|
||||||
payload: any,
|
|
||||||
callBack?: (error: any, result: any) => void
|
|
||||||
) => {
|
|
||||||
if (worker === undefined) {
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
if (callBack === undefined) {
|
|
||||||
/** If a callback function is not provided, return a promise */
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
dispatch(payload, (error, result) => {
|
|
||||||
if (error) {
|
|
||||||
reject(error);
|
|
||||||
} else {
|
|
||||||
resolve(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/** Otherwise, use the callback function */
|
|
||||||
dispatcherQueue.queue.set(dispatcherQueue.index, callBack);
|
|
||||||
const message = {
|
|
||||||
id: dispatcherQueue.index++,
|
|
||||||
payload: payload,
|
|
||||||
};
|
|
||||||
worker.postMessage(message);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default dispatch;
|
|
|
@ -1,33 +0,0 @@
|
||||||
import worker from './dispatcher-worker';
|
|
||||||
|
|
||||||
jest.mock('../db', () => ({
|
|
||||||
initDb: () => 'called initDb',
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('The dispatcher-worker ', () => {
|
|
||||||
let port;
|
|
||||||
beforeEach(() => {
|
|
||||||
port = {
|
|
||||||
postMessage: jest.fn(),
|
|
||||||
};
|
|
||||||
worker.onconnect({ ports: [port] });
|
|
||||||
});
|
|
||||||
test('creates a onmessage function', () => {
|
|
||||||
expect(port.onmessage).toBeDefined();
|
|
||||||
expect(port.postMessage).not.toBeCalled();
|
|
||||||
});
|
|
||||||
test('receives a ping and sends back an unknownAction', async () => {
|
|
||||||
await port.onmessage({ data: { id: 5, payload: { action: 'ping' } } });
|
|
||||||
expect(port.postMessage).toBeCalledWith({
|
|
||||||
id: 5,
|
|
||||||
payload: 'unknownAction',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
test('calls initDb when required', async () => {
|
|
||||||
await port.onmessage({ data: { id: 5, payload: { action: 'initDb' } } });
|
|
||||||
expect(port.postMessage).toBeCalledWith({
|
|
||||||
id: 5,
|
|
||||||
payload: 'called initDb',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,39 +0,0 @@
|
||||||
import { initDb } from '../db';
|
|
||||||
import {
|
|
||||||
putNewGpx,
|
|
||||||
existsGpx,
|
|
||||||
pruneAndSaveImportedGpx,
|
|
||||||
getGpxesForViewport,
|
|
||||||
getGpx,
|
|
||||||
} from '../db/gpx';
|
|
||||||
import { getTrk, putNewTrk } from '../db/trk';
|
|
||||||
import { getTrkseg } from '../db/trkseg';
|
|
||||||
|
|
||||||
const self = globalThis as unknown as WorkerGlobalScope;
|
|
||||||
|
|
||||||
const actions = {
|
|
||||||
initDb,
|
|
||||||
putNewGpx,
|
|
||||||
putNewTrk,
|
|
||||||
existsGpx,
|
|
||||||
pruneAndSaveImportedGpx,
|
|
||||||
getGpxesForViewport,
|
|
||||||
getGpx,
|
|
||||||
getTrk,
|
|
||||||
getTrkseg,
|
|
||||||
};
|
|
||||||
|
|
||||||
onmessage = async function (e) {
|
|
||||||
console.log(`Worker received ${JSON.stringify(e.data)}`);
|
|
||||||
const { id, payload } = e.data;
|
|
||||||
console.log(`payload.action in actions: ${payload.action in actions}`);
|
|
||||||
var returnValue: any = 'unknownAction';
|
|
||||||
if (payload.action in actions) {
|
|
||||||
returnValue = await actions[<keyof typeof actions>payload.action](
|
|
||||||
payload.params
|
|
||||||
);
|
|
||||||
}
|
|
||||||
postMessage({ id: id, payload: returnValue });
|
|
||||||
};
|
|
||||||
|
|
||||||
export default self;
|
|
|
@ -1,4 +0,0 @@
|
||||||
export const getWorker = () =>
|
|
||||||
new Worker(new URL('./dispatcher-worker', import.meta.url));
|
|
||||||
|
|
||||||
export default getWorker;
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2020",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": [
|
||||||
"dom",
|
"dom",
|
||||||
"dom.iterable",
|
"dom.iterable",
|
||||||
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://typedoc.org/schema.json",
|
|
||||||
"entryPoints": ["./src"],
|
|
||||||
"exclude": ["**/*.test.+(tsx|ts)"],
|
|
||||||
"entryPointStrategy": "expand",
|
|
||||||
"out": "doc"
|
|
||||||
}
|
|
Loading…
Reference in New Issue