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 App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
// const db = new PouchDB('dyomedea', { auto_compaction: true, revs_limit: 10 });
// initDb(db);
dispatch({ action: 'initDb' });
//dispatch({ action: 'getGpxesForViewport', params: { type: 'tech' } });
/**
*
* @returns The root app component
*/
const App: React.FC = () => {
const [scope, setScope] = useAtom(scopeAtom);
console.log(`App, scope: ${JSON.stringify(scope)}`);
return (
<IonApp>
<IonContent fullscreen={true}>
<IonApp>
<LiveMap
scope={scope}
setScope={debounce(setScope, 1000)}
numberOfTiledLayers={5}
slippyGraphics={[
<CurrentLocation key='currentLocation' />,
<Gpxes key='gpxes' />,
]}
/>
</IonApp>
</IonContent>
<Explorer/>
<IonHeader className='ion-no-border' translucent={true}>
<IonToolbar>
<IonButtons slot='start'>
<Back />
<Forward />
</IonButtons>
<IonButtons slot='end'>
<GpxImport />
<MapChooser />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonFooter className='ion-no-border'>
<IonToolbar>
<IonButtons>
<GetLocation />
</IonButtons>
</IonToolbar>
</IonFooter>
</IonApp>
);
};
export default App;

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