Starting again...

This commit is contained in:
Eric van der Vlist 2022-11-22 08:19:52 +01:00
parent 1c403ae819
commit e75b2f670c
99 changed files with 104 additions and 5791 deletions

View File

@ -1,8 +1,8 @@
import { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.dyomedea.dyomedea2',
appName: 'dyomedea2',
appId: 'io.ionic.starter',
appName: 'dyomedeaOl',
webDir: 'build',
bundledWebRuntime: false
};

View File

@ -1,5 +1,5 @@
{
"name": "dyomedea",
"name": "dyomedeaOl",
"integrations": {
"capacitor": {}
},

View File

@ -1,55 +1,45 @@
{
"name": "dyomedea",
"version": "0.0.2",
"name": "dyomedeaOl",
"version": "0.0.1",
"private": true,
"dependencies": {
"@awesome-cordova-plugins/geolocation": "^6.2.0",
"@capacitor/android": "^4.5.0",
"@capacitor/app": "^4.1.1",
"@capacitor/core": "^4.5.0",
"@capacitor/haptics": "^4.1.0",
"@capacitor/keyboard": "^4.1.0",
"@capacitor/status-bar": "^4.1.0",
"@ionic/react": "^6.3.7",
"@ionic/react-router": "^6.3.7",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.4.3",
"@types/jest": "^29.2.3",
"@types/node": "^18.11.9",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"@types/react-router": "^5.1.19",
"@types/react-router-dom": "^5.3.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",
"@capacitor/app": "4.1.1",
"@capacitor/core": "4.5.0",
"@capacitor/haptics": "4.1.0",
"@capacitor/keyboard": "4.1.0",
"@capacitor/status-bar": "4.1.0",
"@ionic/react": "^6.0.0",
"@ionic/react-router": "^6.0.0",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^12.6.3",
"@types/jest": "^26.0.20",
"@types/node": "^12.19.15",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/react-router": "^5.1.11",
"@types/react-router-dom": "^5.1.7",
"history": "^4.9.0",
"ionicons": "^6.0.3",
"react": "^18.2.0",
"react-cool-dimensions": "^2.0.7",
"react-dom": "^18.2.0",
"react-localization": "^1.0.19",
"react-router": "^6.4.3",
"react-router-dom": "^6.4.3",
"react-scripts": "^5.0.1",
"typedoc": "^0.23.21",
"typescript": "^4.9.3",
"web-vitals": "^3.1.0",
"workbox-background-sync": "^6.5.4",
"workbox-broadcast-update": "^6.5.4",
"workbox-cacheable-response": "^6.5.4",
"workbox-core": "^6.5.4",
"workbox-expiration": "^6.5.4",
"workbox-navigation-preload": "^6.5.4",
"workbox-precaching": "^6.5.4",
"workbox-range-requests": "^6.5.4",
"workbox-routing": "^6.5.4",
"workbox-strategies": "^6.5.4",
"workbox-streams": "^6.5.4"
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.0",
"typescript": "^4.1.3",
"web-vitals": "^0.2.4",
"workbox-background-sync": "^5.1.4",
"workbox-broadcast-update": "^5.1.4",
"workbox-cacheable-response": "^5.1.4",
"workbox-core": "^5.1.4",
"workbox-expiration": "^5.1.4",
"workbox-google-analytics": "^5.1.4",
"workbox-navigation-preload": "^5.1.4",
"workbox-precaching": "^5.1.4",
"workbox-range-requests": "^5.1.4",
"workbox-routing": "^5.1.4",
"workbox-strategies": "^5.1.4",
"workbox-streams": "^5.1.4"
},
"scripts": {
"start": "react-scripts start",
@ -76,10 +66,7 @@
]
},
"devDependencies": {
"@capacitor/cli": "^4.5.0",
"@ionic/cli": "^6.20.4",
"@types/lodash": "^4.14.189",
"@types/pouchdb": "^6.4.0"
"@capacitor/cli": "4.5.0"
},
"description": "An Ionic project"
}

View File

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

View File

@ -3,6 +3,6 @@ import { render } from '@testing-library/react';
import App from './App';
test('renders without crashing', () => {
// const { baseElement } = render(<App />);
// expect(baseElement).toBeDefined();
const { baseElement } = render(<App />);
expect(baseElement).toBeDefined();
});

View File

@ -1,12 +1,7 @@
import {
IonApp,
IonButtons,
IonContent,
IonFooter,
IonHeader,
IonToolbar,
setupIonicReact,
} from '@ionic/react';
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
@ -27,136 +22,21 @@ import '@ionic/react/css/display.css';
/* Theme variables */
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();
// See https://stackoverflow.com/questions/71538643/property-wakelock-does-not-exist-on-type-navigator
const requestWakeLock = async () => {
const anyNav: any = navigator;
if ('wakeLock' in navigator) {
try {
const wakeLock = await anyNav['wakeLock'].request('screen');
} catch (err: any) {
// The wake lock request fails - usually system-related, such as low battery.
console.log(`Wake lock request failed: ${err.name}, ${err.message}`);
}
} else {
console.log('No wake lock support here...');
}
};
const handleVisibilityChange = () => {
if (document.hidden) {
console.log('Application hidden');
} else {
console.log('Application visible');
requestWakeLock();
}
};
// See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
document.addEventListener('visibilitychange', handleVisibilityChange, false);
requestWakeLock();
const 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 (
const App: React.FC = () => (
<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>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
};
export default App;

View File

@ -0,0 +1,24 @@
.container {
text-align: center;
position: absolute;
left: 0;
right: 0;
top: 50%;
transform: translateY(-50%);
}
.container strong {
font-size: 20px;
line-height: 26px;
}
.container p {
font-size: 16px;
line-height: 22px;
color: #8c8c8c;
margin: 0;
}
.container a {
text-decoration: none;
}

View File

@ -0,0 +1,14 @@
import './ExploreContainer.css';
interface ContainerProps { }
const ExploreContainer: React.FC<ContainerProps> = () => {
return (
<div className="container">
<strong>Ready to create an app?</strong>
<p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
</div>
);
};
export default ExploreContainer;

View File

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

View File

@ -1,5 +0,0 @@
ion-button.back-forward {
--opacity: 0.6;
--ion-background-color: white;
--ionicon-stroke-width: 48px;
}

View File

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

View File

@ -1,6 +0,0 @@
ion-button.get-location {
--opacity: 0.6;
--ion-background-color: white;
margin-left: calc(50% - 20px);
}

View File

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

View File

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

View File

@ -1,8 +0,0 @@
.inputFile {
width: 0.1px;
height: 0.1px;
opacity: 0;
overflow: hidden;
position: absolute;
z-index: -1;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +0,0 @@
.handler {
position: fixed;
height: 100%;
width: 100%;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

119
src/db/types.d.ts vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2017 Zheng-Xiang Ke
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -1,91 +0,0 @@
# gpx-parser-builder
A simple gpx parser and builder between GPX string and JavaScript object. It is dependent on [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
[![npm](https://img.shields.io/npm/dt/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
[![GitHub stars](https://img.shields.io/github/stars/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/stargazers)
[![GitHub forks](https://img.shields.io/github/forks/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/network)
[![npm](https://img.shields.io/npm/v/gpx-parser-builder.svg)](https://www.npmjs.com/package/gpx-parser-builder)
[![GitHub license](https://img.shields.io/github/license/kf99916/gpx-parser-builder.svg)](https://github.com/kf99916/gpx-parser-builder/blob/master/LICENSE)
## Requirements
gpx-parser-builder is written with ECMAScript 6. You can leverage [Babel](https://babeljs.io/) and [Webpack](https://webpack.js.org/) to make all browsers available.
## Installation
```bash
npm install gpx-parser-builder --save
```
## Version
v1.0.0+ is a breaking change for v0.2.2-. v1.0.0+ fully supports gpx files including waypoints, routes, and tracks. Every gpx type is 1-1 corresponding to a JavaScript class.
## Usage
```javascript
import GPX from 'gpx-parser-builder';
// Parse gpx
const gpx = GPX.parse('GPX_STRING');
window.console.dir(gpx.metadata);
window.console.dir(gpx.wpt);
window.console.dir(gpx.trk);
// Build gpx
window.console.log(gpx.toString());
```
Get more details about usage with the unit tests.
### GPX
The GPX JavaScript object.
`constructor(object)`
```javascript
const gpx = new Gpx({$:{...}, metadat: {...}, wpt:[{...},{...}]}, trk: {...}, rte: {...})
```
#### Member Variables
`$` the attributes for the gpx element. Default value:
```javascript
{
'version': '1.1',
'creator': 'gpx-parser-builder',
'xmlns': 'http://www.topografix.com/GPX/1/1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation': 'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
}
```
`metadata` the metadata for the gpx.
`wpt` array of waypoints. It is corresponded to `<wpt>`. The type of all elements in `wpt` is `Waypoint`;
`rte` array of routes. It is corresponded to `<rte>`. The type of all elements in `rte` is `Route`;
`trk` array of tracks. It is corresponded to `<trk>`. The type of all elements in `trk` is `Track`;
#### Static Methods
`parse(gpxString)` parse gpx string to Gpx object. return `null` if parsing failed.
#### Member Methods
`toString(options)` GPX object to gpx string. The options is for [isomorphic-xml2js](https://github.com/RikkiGibson/isomorphic-xml2js).
## Save as GPX file in the frontend
You can leverage [StreamSaver.js](https://github.com/jimmywarting/StreamSaver.js) or [FileSaver.js](https://github.com/eligrey/FileSaver.js) to save as GPX file. ⚠Not all borwsers support the above file techniques. ⚠️️️
## Author
Zheng-Xiang Ke, kf99916@gmail.com
## License
gpx-parser-builder is available under the MIT license. See the LICENSE file for more info.

View File

@ -1,36 +0,0 @@
{
"name": "gpx-parser-builder",
"version": "1.0.2",
"description": "A simple gpx parser and builder between GPX string and JavaScript object",
"main": "./src/gpx.js",
"scripts": {
"test": "mocha --require @babel/register test/**/*.js"
},
"repository": {
"type": "git",
"url": "git+https://github.com/kf99916/gpx-parser-builder.git"
},
"keywords": [
"gpx",
"parser",
"builder"
],
"author": "Zheng-Xiang Ke",
"license": "MIT",
"bugs": {
"url": "https://github.com/kf99916/gpx-parser-builder/issues"
},
"homepage": "https://github.com/kf99916/gpx-parser-builder",
"files": [
"src"
],
"devDependencies": {
"@babel/core": "~7.7",
"@babel/preset-env": "~7.7",
"@babel/register": "~7.7",
"mocha": "~6.2"
},
"dependencies": {
"isomorphic-xml2js": "~0.1"
}
}

View File

@ -1,8 +0,0 @@
export default class Bounds {
constructor(object) {
this.minlat = object.minlat;
this.minlon = object.minlon;
this.maxlat = object.maxlat;
this.maxlon = object.maxlon;
}
}

View File

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

View File

@ -1,13 +0,0 @@
declare module 'gpx-parser-builder' {
class GPX {
static parse(gpxString: any): any;
constructor(object: any);
$: any;
extensions: any;
metadata: any;
wpt: any;
rte: any;
trk: any;
toString(options: any): string;
}
}

View File

@ -1,79 +0,0 @@
import * as xml2js from 'isomorphic-xml2js';
import Metadata from './metadata';
import Waypoint from './waypoint';
import Route from './route';
import Track from './track';
import {removeEmpty, allDatesToISOString} from './utils';
const defaultAttributes = {
version: '1.1',
creator: 'gpx-parser-builder',
xmlns: 'http://www.topografix.com/GPX/1/1',
'xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
'xsi:schemaLocation':
'http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd'
}
export default class GPX {
constructor(object) {
this.$ = Object.assign({}, defaultAttributes, object.$ || object.attributes || {});
this.extensions = object.extensions;
if (object.metadata) {
this.metadata = new Metadata(object.metadata);
}
if (object.wpt) {
if (!Array.isArray(object.wpt)) {
object.wpt = [object.wpt];
}
this.wpt = object.wpt.map(wpt => new Waypoint(wpt));
}
if (object.rte) {
if (!Array.isArray(object.rte)) {
object.rte = [object.rte];
}
this.rte = object.rte.map(rte => new Route(rte));
}
if (object.trk) {
if (!Array.isArray(object.trk)) {
object.trk = [object.trk];
}
this.trk = object.trk.map(trk => new Track(trk));
}
removeEmpty(this);
}
static parse(gpxString) {
let gpx;
xml2js.parseString(gpxString, {
explicitArray: false
}, (err, xml) => {
if (err) {
return;
}
if (!xml.gpx) {
return;
}
gpx = new GPX({
attributes: xml.gpx.$,
metadata: xml.gpx.metadata,
wpt: xml.gpx.wpt,
rte: xml.gpx.rte,
trk: xml.gpx.trk
});
});
return gpx;
}
toString(options) {
options = options || {};
options.rootName = 'gpx';
const builder = new xml2js.Builder(options), gpx = new GPX(this);
allDatesToISOString(gpx);
return builder.buildObject(gpx);
}
}

View File

@ -1,8 +0,0 @@
export default class Link {
constructor(object) {
this.$ = {};
this.$.href = object.$.href || object.href;
this.text = object.text;
this.type = object.type;
}
}

View File

@ -1,29 +0,0 @@
import Copyright from './copyright';
import Link from './link';
import Person from './person';
import Bounds from './bounds';
export default class Metadata {
constructor(object) {
this.name = object.name;
this.desc = object.desc;
this.time = object.time ? new Date(object.time) : new Date();
this.keywords = object.keywords;
this.extensions = object.extensions;
if (object.author) {
this.author = new Person(object.author);
}
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.bounds) {
this.bounds = new Bounds(object.bounds);
}
if (object.copyright) {
this.copyright = new Copyright(object.copyright);
}
}
}

View File

@ -1,11 +0,0 @@
import Link from './link';
export default class Person {
constructor(object) {
this.name = object.name;
this.email = object.email;
if (object.link) {
this.link = new Link(object.link);
}
}
}

View File

@ -1,27 +0,0 @@
import Waypoint from './waypoint';
import Link from './link';
export default class Route {
constructor(object) {
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.number = object.number;
this.type = object.type;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
this.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.rtept) {
if (!Array.isArray(object.rtept)) {
this.rtept = [object.rtept];
}
this.rtept = object.rtept.map(pt => new Waypoint(pt));
}
}
}

View File

@ -1,13 +0,0 @@
import Waypoint from './waypoint';
export default class TrackSegment {
constructor(object) {
if (object.trkpt) {
if (!Array.isArray(object.trkpt)) {
object.trkpt = [object.trkpt];
}
this.trkpt = object.trkpt.map(pt => new Waypoint(pt));
}
this.extensions = object.extensions;
}
}

View File

@ -1,26 +0,0 @@
import TrackSegment from './track-segment';
import Link from './link';
export default class Track {
constructor(object) {
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.number = object.number;
this.type = object.type;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
if (object.trkseg) {
if (!Array.isArray(object.trkseg)) {
object.trkseg = [object.trkseg];
}
this.trkseg = object.trkseg.map(seg => new TrackSegment(seg));
}
}
}

View File

@ -1,23 +0,0 @@
function removeEmpty(obj) {
Object.entries(obj).forEach(([key, val]) => {
if (val && val instanceof Object) {
removeEmpty(val);
} else if (val == null) {
delete obj[key];
}
});
}
function allDatesToISOString(obj) {
Object.entries(obj).forEach(([key, val]) => {
if (val) {
if (val instanceof Date) {
obj[key] = val.toISOString().split('.')[0] + 'Z';
} else if (val instanceof Object) {
allDatesToISOString(val);
}
}
});
}
export { removeEmpty, allDatesToISOString };

View File

@ -1,32 +0,0 @@
import Link from './link';
export default class Waypoint {
constructor(object) {
this.$ = {};
this.$.lat = object.$.lat === 0 || object.lat === 0 ? 0 : object.$.lat || object.lat || -1;
this.$.lon = object.$.lon === 0 || object.lon === 0 ? 0 : object.$.lon || object.lon || -1;
this.ele = object.ele;
this.time = object.time ? new Date(object.time) : new Date();
this.magvar = object.magvar;
this.geoidheight = object.geoidheight;
this.name = object.name;
this.cmt = object.cmt;
this.desc = object.desc;
this.src = object.src;
this.sym = object.sym;
this.type = object.type;
this.sat = object.sat;
this.hdop = object.hdop;
this.vdop = object.vdop;
this.pdop = object.pdop;
this.ageofdgpsdata = object.ageofdgpsdata;
this.dgpsid = object.dgpsid;
this.extensions = object.extensions;
if (object.link) {
if (!Array.isArray(object.link)) {
object.link = [object.link];
}
this.link = object.link.map(l => new Link(l));
}
}
}

View File

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

View File

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

View File

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

View File

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

34
src/lib/types.d.ts vendored
View File

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

View File

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

View File

@ -1 +0,0 @@
declare module 'gpx-parser-builder';

View File

@ -1,10 +1,5 @@
import {
IonContent,
IonHeader,
IonPage,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { IonContent, IonHeader, IonPage, IonTitle, IonToolbar } from '@ionic/react';
import ExploreContainer from '../components/ExploreContainer';
import './Home.css';
const Home: React.FC = () => {
@ -16,11 +11,12 @@ const Home: React.FC = () => {
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse='condense'>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size='large'>Blank</IonTitle>
<IonTitle size="large">Blank</IonTitle>
</IonToolbar>
</IonHeader>
<ExploreContainer />
</IonContent>
</IonPage>
);

View File

@ -1,14 +1,8 @@
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
/** Ionic CSS Variables **/
:root {
/** Transparent background so that underlying tiles and whiteboard can be seen **/
--ion-background-color: transparent;
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +0,0 @@
export const getWorker = () =>
new Worker(new URL('./dispatcher-worker', import.meta.url));
export default getWorker;

View File

@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es2020",
"target": "es5",
"lib": [
"dom",
"dom.iterable",

View File

@ -1,7 +0,0 @@
{
"$schema": "https://typedoc.org/schema.json",
"entryPoints": ["./src"],
"exclude": ["**/*.test.+(tsx|ts)"],
"entryPointStrategy": "expand",
"out": "doc"
}