Restarting from scratch

This commit is contained in:
vdv committed 2022-10-17 10:08:00 +02:00
1 parent ed951208e8
commit a4bd11aab0
88 files changed
+1714 -17027

No files matched your search

+24
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;
}
+14
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;
-12
View File
@@ -1,12 +0,0 @@
import react, { Fragment } from 'react';
interface FormProps {
context: any;
validate: (ctx: any) => {};
}
const Form: react.FC<{}> = () => {
return <Fragment />;
};
export default Form;
-93
View File
@@ -1,93 +0,0 @@
import React, { useId } from 'react';
import { geoPoint, lat2tile, lon2tile, Point } from '../../lib/geo';
import { tileProviders } from './tile';
interface AvatarForLocationProps {
location: geoPoint;
zoom?: number;
tileProvider?: string;
size?: number;
}
const AvatarForLocation: React.FC<AvatarForLocationProps> = (
props: AvatarForLocationProps
) => {
const size = props.size !== undefined ? props.size : 42;
const zoom = props.zoom ? Math.round(props.zoom) : 16;
const tileProvider = props.tileProvider ? props.tileProvider : 'osm';
const location = props.location;
const tilesLocation: Point = {
x: lon2tile(location.lon, zoom),
y: lat2tile(location.lat, zoom),
};
const locationWithinTile: Point = {
x: 256 * (tilesLocation.x - Math.floor(tilesLocation.x)),
y: 256 * (tilesLocation.y - Math.floor(tilesLocation.y)),
};
const eastTileNeeded = locationWithinTile.x > 256 - size / 2;
const westTileNeeded = locationWithinTile.x < size / 2;
const southTileNeeded = locationWithinTile.y > 256 - size / 2;
const northTileNeeded = locationWithinTile.y < size / 2;
const getImage = (stepX: number, stepY: number) => (
<image
href={tileProviders[tileProvider].getTileUrl(
zoom,
Math.floor(tilesLocation.x) + stepX,
Math.floor(tilesLocation.y) + stepY
)}
height='256'
width={256}
x={-locationWithinTile.x + size / 2 + stepX * 256}
y={-locationWithinTile.y + size / 2 + stepY * 256}
/>
);
var images = [getImage(0, 0)];
if (eastTileNeeded) {
images.push(getImage(1, 0));
}
if (westTileNeeded) {
images.push(getImage(-1, 0));
}
if (southTileNeeded) {
images.push(getImage(0, 1));
}
if (northTileNeeded) {
images.push(getImage(0, -1));
}
if (northTileNeeded && eastTileNeeded) {
images.push(getImage(1, -1));
}
if (northTileNeeded && westTileNeeded) {
images.push(getImage(-1, -1));
}
if (southTileNeeded && eastTileNeeded) {
images.push(getImage(1, 1));
}
if (southTileNeeded && westTileNeeded) {
images.push(getImage(-1, 1));
}
console.log(`${images.length} images: ${images}`);
const id = useId();
return (
<svg width={size} height={size}>
<defs>
<clipPath id={`cut-off-${id}`}>
<circle cx={size / 2} cy={size / 2} r={size / 2} />
</clipPath>
</defs>
<g clip-path={`url(#cut-off-${id})`}>{[images]}</g>
</svg>
);
};
export default AvatarForLocation;
-33
View File
@@ -1,33 +0,0 @@
import React
from 'react';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
const CurrentLocation: React.FC<{}> = () => {
const scale = useSelector(
(state: { map: MapState }) => state.map.whiteboard.scale
);
const CurrentLocationState = useSelector(
(state: { map: MapState }) => state.map.currentLocation
);
return (
<circle
cx={CurrentLocationState.whiteboard.x}
cy={CurrentLocationState.whiteboard.y}
r={6 / scale}
fill='blue'
opacity='90%'
stroke='white'
strokeWidth={3/scale}
strokeOpacity='100%'
></circle>
);
};
export default CurrentLocation;
-48
View File
@@ -1,48 +0,0 @@
import React from 'react';
import { useFind } from 'react-pouchdb';
import { lat2tile, lon2tile } from '../../lib/geo';
import { zoom0 } from '../../store/map';
const Gpx: React.FC<{ gpx: any }> = (props) => {
var trkpts: any[] = [];
trkpts = useFind({
selector: {
type: 'trkpt',
gpx: props.gpx._id,
},
// sort: ['trkpt.time'],
// use_index: 'type-trkpt-gpx-time3',
});
console.log(
`rendering gpx, subtype:"${props.gpx.subtype}", ${trkpts.length} points`
);
trkpts.sort((first: any, second: any) =>
first.trkpt.time.localeCompare(second.trkpt.time)
);
const clickHandler = (event: any) => {
console.log(`gpx (${JSON.stringify(props.gpx)}) clicked.`);
};
// console.log(`gpx: ${JSON.stringify(props.gpx)}`);
const d = trkpts.reduce((previous: string, current: any, index: number) => {
const action = index === 0 ? 'M' : index === 1 ? 'L' : '';
const trkpt = current.trkpt;
return `${previous} ${action} ${lon2tile(trkpt.$.lon, zoom0)}, ${lat2tile(
trkpt.$.lat,
zoom0
)}`;
}, '');
return (
<path d={d} className={`track ${props.gpx.subtype}`} pointerEvents='none' />
);
};
export default Gpx;
-111
View File
@@ -1,111 +0,0 @@
import { IonButton, IonIcon, isPlatform } from '@ionic/react';
import { cloudDownload } from 'ionicons/icons';
import React, { Fragment, useRef } from 'react';
import { Filesystem, Directory, Encoding } from '@capacitor/filesystem';
import { useDB } from 'react-pouchdb';
import { FileOpener } from '@capacitor-community/file-opener';
import { getGpxAsXmlString } from '../../db/gpx';
const GpxExport: React.FC<{ gpx: any }> = (props: { gpx: any }) => {
const db = useDB();
const isCapacitor = isPlatform('capacitor');
console.log(`isPlatform('capacitor'): ${isPlatform('capacitor')}`);
const hiddenLinkElement = useRef<HTMLAnchorElement>(null);
var downloadBaseName: string;
if (props.gpx.metadata.name !== undefined) {
downloadBaseName = props.gpx.metadata.name.substr(
0,
props.gpx.metadata.name.lastIndexOf('.')
);
} else if (props.gpx.gpx.metadata.name !== undefined) {
downloadBaseName = props.gpx.gpx.metadata.name;
} else {
downloadBaseName = `track-${props.gpx.metadata.lastModified}`;
}
const downloadExt = 'gpx';
const downloadName = `${downloadBaseName}.${downloadExt}`;
const getNewFilename: any = async (
filename: string,
extension: string,
i = 0
) => {
const filenameToTest =
i === 0 ? `${filename}.${extension}` : `${filename}(${i}).${extension}`;
try {
await Filesystem.stat({
path: filenameToTest,
directory: Directory.Documents,
});
return getNewFilename(filename, extension, i + 1);
} catch {
return filenameToTest;
}
};
const download = async (event: any) => {
event.preventDefault();
console.log('download()');
const gpxAsXml = await getGpxAsXmlString(db, props.gpx._id);
console.log(`gpxAsXml: ${gpxAsXml}`);
if (isCapacitor) {
const filename = await getNewFilename(
`../Download/${downloadBaseName}`,
downloadExt
);
console.log(`filename: ${filename}`);
const fileUrl = await Filesystem.writeFile({
path: filename,
data: gpxAsXml,
directory: Directory.Documents,
encoding: Encoding.UTF8,
recursive: true,
});
console.log(`fileUrl: ${fileUrl.uri}`);
await FileOpener.open({
filePath: fileUrl.uri,
contentType: 'application/gpx+xml',
openWithDefault: false,
});
} else {
const blob = new Blob([gpxAsXml], {
type: 'application/gpx+xml',
});
const fileDownloadUrl = URL.createObjectURL(blob);
console.log(`fileDownloadUrl: ${fileDownloadUrl}`);
console.log(
`hiddenLinkElement.current, href: ${
hiddenLinkElement.current!.href
}, click: ${hiddenLinkElement.current!.click}`
);
hiddenLinkElement.current!.href = fileDownloadUrl;
hiddenLinkElement.current?.click();
console.log(
`hiddenLinkElement.current, href: ${
hiddenLinkElement.current!.href
}, click: ${hiddenLinkElement.current!.click}`
);
URL.revokeObjectURL(fileDownloadUrl);
hiddenLinkElement.current!.href = '';
}
};
return (
<Fragment>
<IonButton id='gpx-export-button' onClick={download}>
<IonIcon icon={cloudDownload} title='Export' slot='icon-only' />
</IonButton>
<a className='hidden' download={downloadName} ref={hiddenLinkElement}>
download
</a>
</Fragment>
);
};
export default GpxExport;
-221
View File
@@ -1,221 +0,0 @@
import React, { Fragment, useRef, useState } from 'react';
import { useDB, useFind } from 'react-pouchdb';
import '../../theme/get-location.css';
import {
IonButton,
IonButtons,
IonContent,
IonIcon,
IonModal,
IonTitle,
IonToolbar,
} from '@ionic/react';
import {
recordingOutline,
recording,
closeCircle,
pauseCircle,
stop,
} from 'ionicons/icons';
import {
startBackgroundGeolocation,
stopBackgroundGeolocation,
} from '../../lib/background-geolocation';
import { appendTrkpt, deleteCurrent, saveCurrent } from '../../db/gpx';
import { enterAnimation, leaveAnimation } from '../../lib/animation';
import { useDispatch, useSelector } from 'react-redux';
import { mapActions } from '../../store/map';
import { SettingsState } from '../../store/settings';
import i18n, { setI18nLanguage } from '../../i18n/index';
declare global {
var $lastValidLocationTime: number;
}
const GpxRecord: React.FC<{}> = () => {
const language = useSelector(
(state: { settings: SettingsState }) => state.settings.language
);
setI18nLanguage(language);
const db = useDB();
const [isRecording, setIsRecording] = useState(false);
const [watcher_id, setWatcher_id] = useState();
const geolocationsSettingsState = useSelector(
(state: { settings: SettingsState }) => state.settings.geolocation
);
const gpxes = useFind({
selector: {
type: 'gpx',
subtype: 'current',
},
});
const hasCurrentTrack = gpxes.length > 0;
console.log(
`GpxRecord, hasCurrentTrack:${hasCurrentTrack}, gpxes: ${JSON.stringify(
gpxes
)}`
);
const modal = useRef<HTMLIonModalElement>(null);
const dismiss = () => {
modal.current?.dismiss();
};
const dispatch = useDispatch();
const newLocationHandler = (location: any) => {
console.log(
`Location filtering, elapsed time: ${
location.time - globalThis.$lastValidLocationTime
}`
);
if (
location.time - globalThis.$lastValidLocationTime >
geolocationsSettingsState.minTimeInterval
) {
globalThis.$lastValidLocationTime = location.time;
appendTrkpt(db, {
$: {
lat: location.latitude,
lon: location.longitude,
},
ele: location.altitude,
time: new Date(location.time).toISOString(),
extensions: {
speed: location.speed,
accuracy: location.accuracy,
},
});
dispatch(
mapActions.setCurrent({
lat: location.latitude,
lon: location.longitude,
})
);
dispatch(
mapActions.setCenter({
lat: location.latitude,
lon: location.longitude,
})
);
}
};
const startRecording = () => {
globalThis.$lastValidLocationTime = 0;
startBackgroundGeolocation(
newLocationHandler,
geolocationsSettingsState.minDistance
).then((result) => {
setWatcher_id(result);
});
setIsRecording(true);
dismiss();
};
const pauseRecording = () => {
if (isRecording) {
stopBackgroundGeolocation(watcher_id);
}
setIsRecording(false);
dismiss();
};
const stopRecording = () => {
saveCurrent(db);
pauseRecording();
};
const deleteRecording = () => {
deleteCurrent(db);
pauseRecording();
};
return (
<Fragment>
<IonButton id='open-RecordDialog'>
{isRecording && (
<IonIcon slot='icon-only' icon={recording} style={{ color: 'red' }} />
)}
{!isRecording && <IonIcon slot='icon-only' icon={recordingOutline} />}
</IonButton>
<IonModal
ref={modal}
trigger='open-RecordDialog'
enterAnimation={enterAnimation}
leaveAnimation={leaveAnimation}
>
<IonToolbar>
<IonTitle>{i18n.trackRecording}</IonTitle>
<IonButtons slot='end'>
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
</IonButtons>
</IonToolbar>
<IonContent>
{!isRecording && (
<IonButton
expand='block'
color='primary'
size='large'
onClick={startRecording}
>
<IonIcon slot='start' icon={recording}></IonIcon>
{hasCurrentTrack ? (
<span>{i18n.resumeRecording}</span>
) : (
<span>{i18n.startRecording}</span>
)}
</IonButton>
)}
{isRecording && (
<IonButton
expand='block'
color='primary'
size='large'
onClick={pauseRecording}
>
<IonIcon slot='start' icon={pauseCircle}></IonIcon>
{i18n.pauseRecording}
</IonButton>
)}
{hasCurrentTrack && (
<>
<IonButton
expand='block'
color='danger'
size='large'
onClick={stopRecording}
>
<IonIcon slot='start' icon={stop}></IonIcon>
{i18n.stopRecording}
</IonButton>
<IonButton
expand='block'
color='danger'
size='large'
onClick={deleteRecording}
>
<IonIcon slot='start' icon={closeCircle}></IonIcon>
{i18n.cancelRecording}
</IonButton>
</>
)}
</IonContent>
</IonModal>
</Fragment>
);
};
export default GpxRecord;
-186
View File
@@ -1,186 +0,0 @@
import {
IonModal,
IonToolbar,
IonTitle,
IonButtons,
IonButton,
IonContent,
IonLabel,
IonList,
IonListHeader,
IonItem,
IonSpinner,
} from '@ionic/react';
import React, { Fragment, useRef, useState } from 'react';
import { useSelector } from 'react-redux';
import i18n from '../../i18n';
import { geoPoint } from '../../lib/geo';
import { MapState } from '../../store/map';
import OsmNotesChat from './OsmNotesChat';
const LocationInfo: React.FC<{}> = () => {
const scope = useSelector((state: { map: MapState }) => state.map.scope);
const modal = useRef<HTMLIonModalElement>(null);
const dismiss = () => {
modal.current?.dismiss();
};
const [elevation, setElevation] = useState(undefined);
const [address, setAddress] = useState<{ display_name: any } | undefined>(
undefined
);
const [notes, setNotes] = useState<{ features: any } | undefined>(undefined);
const findElevation = async () => {
setElevation(undefined);
const response = await fetch(
// `https://api.opentopodata.org/v1/mapzen?locations=${scope.center.lat},${scope.center.lon}`,
`https://api.open-meteo.com/v1/elevation?latitude=${scope.center.lat}&longitude=${scope.center.lon}`,
{}
);
const data = await response.json();
console.log(`elevation: ${JSON.stringify(data)}`);
setElevation(data.elevation[0]);
};
const findAddress = async () => {
setAddress(undefined);
const response = await fetch(
`https://nominatim.openstreetmap.org/reverse?lat=${
scope.center.lat
}&lon=${
scope.center.lon
}&format=jsonv2&addressdetails=1&extratags=1&namedetails=1&accept-language=${i18n.getLanguage()}`,
{}
);
const data = await response.json();
console.log(`address: ${JSON.stringify(data)}`);
setAddress(data);
};
const roughDistance = (a: geoPoint, b: geoPoint): number => {
const pseudoDistanceInDegrees = Math.sqrt(
(a.lat - b.lat) ** 2 + (a.lon - b.lon) ** 2
);
const metresPerDegree =
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
return pseudoDistanceInDegrees * metresPerDegree;
};
const findNotes = async () => {
setNotes(undefined);
const metresPerDegree =
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
const deltaDegrees = 5000 / metresPerDegree;
const response = await fetch(
`https://api.openstreetmap.org/api/0.6/notes.json?bbox=${
scope.center.lon - deltaDegrees
},${scope.center.lat - deltaDegrees},${scope.center.lon + deltaDegrees},${
scope.center.lat + deltaDegrees
}`,
{}
);
const data = await response.json();
console.log(`notes: ${JSON.stringify(data)}`);
data.features.sort(
(first: any, second: any) =>
roughDistance(scope.center, {
lon: first.geometry.coordinates[0],
lat: first.geometry.coordinates[1],
}) -
roughDistance(scope.center, {
lon: second.geometry.coordinates[0],
lat: second.geometry.coordinates[1],
})
);
setNotes(data);
};
const ionModalWillPresentHandler = async () => {
findElevation();
findAddress();
findNotes();
};
const localDate = (isoDate: string) => {
const date = new Date(isoDate);
const format = new Intl.DateTimeFormat(i18n.getLanguage(), {
dateStyle: 'full',
timeStyle: 'long',
});
return format.format(date);
};
const osmNotesChat =
notes !== undefined && notes.features.length ? (
<OsmNotesChat notes={notes} />
) : (
<></>
);
return (
<IonModal
trigger='information-request'
ref={modal}
onIonModalWillPresent={ionModalWillPresentHandler}
>
<IonToolbar>
<IonTitle>{i18n.locationInfo!.title}</IonTitle>
<IonButtons slot='end'>
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
</IonButtons>
</IonToolbar>
<IonContent>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>{i18n.locationInfo!.location}</IonLabel>
</IonListHeader>
<IonItem>
<IonLabel>{i18n.locationInfo!.lat}</IonLabel>
{scope.center.lat} N
</IonItem>
<IonItem>
<IonLabel>{i18n.locationInfo!.lon}</IonLabel>
{scope.center.lon} E
</IonItem>
<IonItem>
<IonLabel>{i18n.locationInfo!.elevation}</IonLabel>
{elevation === undefined && <IonSpinner name='lines' />}
{elevation !== undefined && <>{elevation} m</>}
</IonItem>
</IonList>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>{i18n.locationInfo!.address}</IonLabel>
</IonListHeader>
<IonItem>
<IonLabel>{i18n.locationInfo!.display_name}</IonLabel>
{address === undefined && <IonSpinner name='lines' />}
{address !== undefined &&
address.display_name
.split(',')
.map((value: string, index: number) => (
<Fragment key={index}>
{value}
<br />
</Fragment>
))}
</IonItem>
</IonList>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>
{i18n.locationInfo!.notes}
{notes === undefined && <IonSpinner name='lines' />}
</IonLabel>
</IonListHeader>
{osmNotesChat}
</IonList>
</IonContent>
</IonModal>
);
};
export default LocationInfo;
-143
View File
@@ -1,143 +0,0 @@
import React, { useState } from 'react';
import {
MainContainer,
Sidebar,
ConversationList,
Conversation,
ConversationHeader,
ChatContainer,
MessageList,
Message,
Avatar,
} from '@chatscope/chat-ui-kit-react';
import '@chatscope/chat-ui-kit-styles/dist/default/styles.min.css';
import { useSelector } from 'react-redux';
import i18n from '../../i18n';
import { geoPoint } from '../../lib/geo';
import { MapState } from '../../store/map';
import AvatarForLocation from './AvatarForLocation';
interface OsmNotesChatProps {
notes: { features: any };
}
const OsmNotesChat: React.FC<OsmNotesChatProps> = (
props: OsmNotesChatProps
) => {
const scope = useSelector((state: { map: MapState }) => state.map.scope);
const [noteIndex, setNoteIndex] = useState(0);
const conversationClickHandlerFactory = (index: number) => (event: any) => {
setNoteIndex(index);
};
const roughDistance = (a: geoPoint, b: geoPoint): number => {
const pseudoDistanceInDegrees = Math.sqrt(
(a.lat - b.lat) ** 2 + (a.lon - b.lon) ** 2
);
const metresPerDegree =
111111 * Math.cos((scope.center.lat * Math.PI) / 180);
return pseudoDistanceInDegrees * metresPerDegree;
};
const AvatarForFeature: React.FC<{
feature: any;
size: number;
as: any;
}> = (props: { feature: any; size: number; as: any }) => {
const distance = roughDistance(scope.center, {
lon: props.feature.geometry.coordinates[0],
lat: props.feature.geometry.coordinates[1],
});
return (
<Avatar size={props.size <= 50 ? 'sm' : 'lg'}>
<AvatarForLocation
location={{
lon: props.feature.geometry.coordinates[0],
lat: props.feature.geometry.coordinates[1],
}}
size={props.size}
/>
</Avatar>
);
};
return (
<div style={{ position: 'relative' }}>
<MainContainer responsive>
<Sidebar position='left' id='chat-sidebar'>
<ConversationList>
{props.notes
.features!.slice(0, 5)
.map((feature: any, index: number) => (
<Conversation
onClick={conversationClickHandlerFactory(index)}
key={feature.properties.id}
name={i18n.locationInfo!.at!(
roughDistance(scope.center, {
lon: feature.geometry.coordinates[0],
lat: feature.geometry.coordinates[1],
})
)}
info={feature.properties.status}
>
<AvatarForFeature feature={feature} size={42} as={Avatar} />
</Conversation>
))}
</ConversationList>
</Sidebar>
<ChatContainer>
<ConversationHeader>
<AvatarForFeature
feature={props.notes.features[noteIndex]}
size={68}
as={Avatar}
/>
<ConversationHeader.Content>
<span
style={{
alignSelf: 'flex-center',
}}
>
{i18n.locationInfo!.at!(
roughDistance(scope.center, {
lon: props.notes.features[noteIndex].geometry
.coordinates[0],
lat: props.notes.features[noteIndex].geometry
.coordinates[1],
})
)}
</span>
</ConversationHeader.Content>
</ConversationHeader>
<MessageList>
{noteIndex < props.notes.features.length &&
props.notes.features[noteIndex].properties.comments.map(
(comment: any, index: number) => (
<Message
key={`${props.notes.features[noteIndex].properties.id}/${index}`}
model={{
direction: 'incoming',
position: 'single',
sender: comment.user,
sentTime: comment.date,
payload: comment.html,
}}
>
<Message.Header
sender={comment.user}
sentTime={comment.date}
/>
<Message.Footer sentTime={comment.action} />
<Avatar>{comment.user ? comment.user : '??'}</Avatar>
</Message>
)
)}
</MessageList>
</ChatContainer>
</MainContainer>
</div>
);
};
export default OsmNotesChat;
-146
View File
@@ -1,146 +0,0 @@
import _ from 'lodash';
import React, { useRef } from 'react';
import { useSelector } from 'react-redux';
import { informationCircleOutline } from 'ionicons/icons';
import { Point } from '../../lib/geo';
import { MapState } from '../../store/map';
import '../../theme/map.css';
import {
IonButton,
IonButtons,
IonModal,
IonTitle,
IonToolbar,
} from '@ionic/react';
import i18n from '../../i18n';
import eventBus from '../../lib/pubsub';
export const getSTile = (lat: number, zoom: number) => {
/**
* The horizontal distance represented by each square tile, measured along the parallel at a given latitude, is given by:
* Stile = C ∙ cos(latitude) / 2 zoomlevel
* As tiles are 256-pixels wide, the horizontal distance represented by one pixel is:
* Spixel = Stile / 256 = C ∙ cos(latitude) / 2 (zoomlevel + 8)
* where C means the equatorial circumference of the Earth (40 075 016.686 m ≈ 2π ∙ 6 378 137.000 m for the reference geoid used by OpenStreetMap).
* see https://wiki.openstreetmap.org/wiki/Zoom_levels
*/
return (40075016.686 * Math.cos((lat * Math.PI) / 180)) / 2 ** zoom;
};
const Reticle: React.FC<{}> = () => {
const windowState = useSelector(
(state: { map: MapState }) => state.map.window
);
const scope = useSelector((state: { map: MapState }) => state.map.scope);
const radius = Math.min(windowState.width, windowState.height) / 4;
const center: Point = {
x: windowState.width / 2,
y: windowState.height / 2,
};
const sTile = getSTile(scope.center.lat, scope.zoom);
const radiusMeters = (sTile * radius) / 256;
const radiusOrder = Math.floor(Math.log10(radiusMeters));
const radiusRoughUnit = radiusMeters / 10 ** radiusOrder;
var radiusBaseUnit, radiusBaseSubUnit;
if (radiusRoughUnit <= 2) {
radiusBaseUnit = 1;
radiusBaseSubUnit = 0.2;
} else if (radiusRoughUnit <= 5) {
radiusBaseUnit = 2;
radiusBaseSubUnit = 0.5;
} else {
radiusBaseUnit = 5;
radiusBaseSubUnit = 1;
}
const nbSubUnits = Math.floor(
radiusMeters / (radiusBaseSubUnit * 10 ** radiusOrder)
);
const nbUnits = Math.floor(
radiusMeters / (radiusBaseUnit * 10 ** radiusOrder)
);
const radiusUnit = radiusOrder < 3 ? 'm' : 'km';
const radiusUnitValue =
radiusOrder < 3
? radiusBaseUnit * 10 ** radiusOrder
: radiusBaseUnit * 10 ** (radiusOrder - 3);
const subUnitPxStep = ((radiusBaseSubUnit * 10 ** radiusOrder) / sTile) * 256;
const unitPxStep = ((radiusBaseUnit * 10 ** radiusOrder) / sTile) * 256;
console.log(
`radiusMeters: ${radiusMeters}, radiusOrder: ${radiusOrder}, radiusUnit: ${radiusBaseUnit}, radiusSubUnit: ${radiusBaseSubUnit}, nbSubUnits: ${nbSubUnits}`
);
return (
<g className='reticle' pointerEvents='visible' id='information-request'>
<circle
cx={center.x}
cy={center.y}
r={unitPxStep * nbUnits}
fill='transparent'
stroke='black'
strokeWidth={1}
/>
<path
d={`M ${center.x - radius},${center.y}
h ${radius - 3}
m ${6},0
h ${radius - 3}
M ${center.x},${center.y - radius}
v ${radius - 3}
m 0,${6}
v ${radius - 3}`}
strokeWidth={0.5}
/>
<g id='reticle-east'>
<path
stroke='red'
className='reticle'
strokeWidth={0.5}
d={_.range(1, nbSubUnits + 1)
.map(
(i) => `M ${center.x + i * subUnitPxStep},${center.y - 10} v 20`
)
.join(' ')}
/>
<path
stroke='red'
className='reticle'
d={_.range(1, nbUnits + 1)
.map((i) => `M ${center.x + i * unitPxStep},${center.y - 20} v 40`)
.join(' ')}
strokeWidth={0.5}
/>
</g>
<use
stroke='red'
className='reticle'
href='#reticle-east'
transform={`rotate(90, ${center.x}, ${center.y})`}
/>
<use
className='reticle'
href='#reticle-east'
transform={`rotate(180, ${center.x}, ${center.y})`}
/>
<use
className='reticle'
href='#reticle-east'
transform={`rotate(-90, ${center.x}, ${center.y})`}
/>
<text
x={center.x + unitPxStep * nbUnits - 15}
y={center.y - 10}
cursor='pointer'
>
{`${radiusUnitValue * nbUnits} ${radiusUnit}`}
</text>
</g>
);
};
export default Reticle;
-210
View File
@@ -1,210 +0,0 @@
import React, { Fragment, useEffect, useRef, useState } from 'react';
import { useDB } from 'react-pouchdb';
import {
IonButton,
IonButtons,
IonContent,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonList,
IonListHeader,
IonModal,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { options } from 'ionicons/icons';
import { enterAnimation, leaveAnimation } from '../../lib/animation';
import { useDispatch, useSelector } from 'react-redux';
import { settingsActions, SettingsState } from '../../store/settings';
import uri from '../../lib/ids';
import _ from 'lodash';
import i18n, { setI18nLanguage } from '../../i18n/index';
const Settings: React.FC<{}> = () => {
const dispatch = useDispatch();
const db = useDB();
useEffect(() => {
const initFromDb = async () => {
try {
const settingsFromDb = await db.get(uri('settings', {}));
console.log(
`settingsFromDb: ${JSON.stringify(settingsFromDb.settings)}`
);
dispatch(settingsActions.saveSettings(settingsFromDb.settings));
} catch {}
};
initFromDb();
}, [db, dispatch]);
const settingsState = useSelector(
(state: { settings: SettingsState }) => state.settings
);
useEffect(() => {
if (!settingsState.default) {
const settingsFromRedux = async () => {
const id = uri('settings', {});
var settingsFromDb;
try {
settingsFromDb = await db.get(id);
} catch (error) {
console.log(
`Error getting settings from db: ${JSON.stringify(error)}`
);
settingsFromDb = { _id: id, settings: {} };
}
if (!_.isEqual(settingsState, settingsFromDb.settings)) {
console.log(`settingsFromRedux: ${JSON.stringify(settingsState)}`);
settingsFromDb.settings = settingsState;
db.put(settingsFromDb);
}
};
settingsFromRedux();
}
setLocalLanguage(settingsState.language);
}, [db, settingsState]);
const [localLanguage, setLocalLanguage] = useState(settingsState.language);
const languageSelect = useRef<HTMLIonSelectElement>(null);
const pseudo = useRef<HTMLIonInputElement>(null);
const minTimeInterval = useRef<HTMLIonInputElement>(null);
const minDistance = useRef<HTMLIonInputElement>(null);
const modal = useRef<HTMLIonModalElement>(null);
const save = () => {
dispatch(
settingsActions.saveSettings({
user: {
pseudo: pseudo.current?.value,
},
geolocation: {
minTimeInterval: minTimeInterval.current?.value,
minDistance: minDistance.current?.value,
},
language: languageSelect.current?.value,
})
);
dismiss();
};
setI18nLanguage(localLanguage);
const dismiss = () => {
modal.current?.dismiss();
setLocalLanguage(settingsState.language);
};
return (
<Fragment>
<IonButton id='open-SettingsDialog'>
<IonIcon slot='icon-only' icon={options} />
</IonButton>
<IonModal
ref={modal}
trigger='open-SettingsDialog'
enterAnimation={enterAnimation}
leaveAnimation={leaveAnimation}
className='full-height'
>
<IonToolbar>
<IonTitle>{i18n.settings}</IonTitle>
<IonButtons slot='end'>
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
</IonButtons>
</IonToolbar>
<IonContent>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>{i18n.language}</IonLabel>
</IonListHeader>
<IonItem>
<IonLabel>{i18n.colonize(i18n.language)} </IonLabel>
<IonSelect
placeholder={i18n.languageSelect.placeHolder}
ref={languageSelect}
value={localLanguage}
interface='popover'
onIonChange={(ev) => setLocalLanguage(ev.detail.value)}
>
{i18n.languageSelect.choices.map((choice) => (
<IonSelectOption key={choice.value} value={choice.value}>
{choice.label}
</IonSelectOption>
))}
</IonSelect>
</IonItem>
</IonList>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>{i18n.user}</IonLabel>
</IonListHeader>
<IonItem>
<IonLabel>{i18n.colonize(i18n.pseudo)} </IonLabel>
<IonInput
ref={pseudo}
value={settingsState.user.pseudo}
autocomplete='nickname'
></IonInput>
</IonItem>
</IonList>
<IonList lines='full' class='ion-no-margin'>
<IonListHeader lines='full'>
<IonLabel>{i18n.geolocation}</IonLabel>
</IonListHeader>
<IonItem>
<IonLabel>{i18n.colonize(i18n.minTimeInt)}</IonLabel>
<IonInput
value={settingsState.geolocation.minTimeInterval}
ref={minTimeInterval}
inputMode='numeric'
type='number'
></IonInput>
</IonItem>
<IonItem>
<IonLabel>{i18n.colonize(i18n.minDist)}</IonLabel>
<IonInput
value={settingsState.geolocation.minDistance}
ref={minDistance}
inputMode='numeric'
type='number'
></IonInput>
</IonItem>
</IonList>
<IonItem>
<IonToolbar class='secondary'>
<IonButtons slot='secondary'>
<IonButton
shape='round'
fill='solid'
color='primary'
onClick={() => save()}
>
{i18n.save}
</IonButton>
<IonButton
shape='round'
fill='outline'
color='primary'
onClick={() => dismiss()}
>
{i18n.cancel}{' '}
</IonButton>
</IonButtons>
</IonToolbar>
</IonItem>
</IonContent>
</IonModal>
</Fragment>
);
};
export default Settings;
@@ -1,13 +0,0 @@
import React from 'react';
import { IonButton, IonIcon } from '@ionic/react';
import { layersOutline } from 'ionicons/icons';
const TileServerChooserButton: React.FC<{}> = () => {
return (
<IonButton id='open-TileServerChooser'>
<IonIcon slot='icon-only' icon={layersOutline} />
</IonButton>
);
};
export default TileServerChooserButton;
@@ -1,77 +0,0 @@
import {
IonButton,
IonButtons,
IonContent,
IonItem,
IonLabel,
IonList,
IonModal,
IonRadio,
IonRadioGroup,
IonTitle,
IonToolbar,
} from '@ionic/react';
import React, { useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { enterAnimation, leaveAnimation } from '../../lib/animation';
import { mapActions, MapState } from '../../store/map';
import { tileProviders } from './tile';
import i18n, { setI18nLanguage } from '../../i18n/index';
import { SettingsState } from '../../store/settings';
const TileServerChooserDialog: React.FC<{}> = () => {
const tileProvider = useSelector(
(state: { map: MapState }) => state.map.scope.tileProvider
) as keyof typeof tileProviders;
const language = useSelector(
(state: { settings: SettingsState }) => state.settings.language
);
setI18nLanguage(language);
const dispatch = useDispatch();
const modal = useRef<HTMLIonModalElement>(null);
const dismiss = () => {
modal.current?.dismiss();
};
const changeHandler = (event: any) => {
dispatch(mapActions.setTileProvider(event.detail.value));
dismiss();
};
return (
<IonModal
ref={modal}
trigger='open-TileServerChooser'
enterAnimation={enterAnimation}
leaveAnimation={leaveAnimation}
>
<IonToolbar>
<IonTitle>{i18n.chooseYourMap}</IonTitle>
<IonButtons slot='end'>
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
</IonButtons>
</IonToolbar>
<IonContent>
<IonList>
<IonRadioGroup value={tileProvider} onIonChange={changeHandler}>
{Object.keys(tileProviders).map((provider) => {
return (
<IonItem key={provider}>
<IonLabel> {tileProviders[provider].name}</IonLabel>
<IonRadio slot='start' value={provider} />
</IonItem>
);
})}
</IonRadioGroup>
</IonList>
</IonContent>
</IonModal>
);
};
export default TileServerChooserDialog;
-102
View File
@@ -1,102 +0,0 @@
import {
IonButton,
IonButtons,
IonCard,
IonCardContent,
IonCardHeader,
IonCardTitle,
IonContent,
IonIcon,
IonItem,
IonList,
IonModal,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { trash } from 'ionicons/icons';
import React, { Fragment, useRef } from 'react';
import { useFind, useDB } from 'react-pouchdb';
import { deleteGps } from '../../db/gpx';
import { enterAnimation, leaveAnimation } from '../../lib/animation';
import phoneRoute from '../../theme/icons/font-gis/svg/routing/uEB08-phone-route-nons.svg';
import GpxImport from './gpx-import';
import GpxExport from './GpxExport';
import i18n, { setI18nLanguage } from '../../i18n/index';
import { useSelector } from 'react-redux';
import { SettingsState } from '../../store/settings';
const TrackBrowser: React.FC<{}> = () => {
const gpxes = useFind({
selector: {
type: 'gpx',
},
});
const db = useDB();
const modal = useRef<HTMLIonModalElement>(null);
const dismiss = () => {
modal.current?.dismiss();
};
const language = useSelector(
(state: { settings: SettingsState }) => state.settings.language
);
setI18nLanguage(language);
return (
<Fragment>
<IonButton id='track-browser-button'>
<img src={phoneRoute} alt='GPS tracks' width='24px' />
</IonButton>
<IonModal
ref={modal}
className='full-height'
trigger='track-browser-button'
enterAnimation={enterAnimation}
leaveAnimation={leaveAnimation}
>
<IonToolbar>
<IonTitle>{i18n.tracks}</IonTitle>
<IonButtons slot='end'>
<GpxImport />
<IonButton onClick={() => dismiss()}>{i18n.close}</IonButton>
</IonButtons>
</IonToolbar>
<IonContent>
{gpxes.map((gpx: any) => (
<IonCard key={gpx._id}>
<IonCardHeader>
<IonItem>
<IonCardTitle>{gpx.gpx.metadata.name}</IonCardTitle>
<IonButtons slot='end'>
<GpxExport gpx={gpx} />
<IonButton
onClick={() => {
deleteGps(db, { _id: gpx._id, _rev: gpx._rev });
}}
color='danger'
>
<IonIcon slot='icon-only' icon={trash} />
</IonButton>
</IonButtons>
</IonItem>
<IonCardContent>
<IonList>
<IonItem>{gpx.gpx.metadata.time}</IonItem>
<IonItem>{gpx.gpx.metadata.desc}</IonItem>
</IonList>
</IonCardContent>
</IonCardHeader>
</IonCard>
))}
</IonContent>
</IonModal>
</Fragment>
);
};
export default TrackBrowser;
-1
View File
@@ -1 +0,0 @@
declare module 'avatar-initials';
Binary file not shown.

Before

Width:  |  Height:  |  Size: 546 B

-1
View File
@@ -1 +0,0 @@
declare module 'font-gis';
-43
View File
@@ -1,43 +0,0 @@
import React from 'react';
import { Geolocation } from '@awesome-cordova-plugins/geolocation';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
import '../../theme/get-location.css';
import { IonButton, IonIcon } from '@ionic/react';
import { locateOutline } from 'ionicons/icons';
const GetLocation: React.FC<{}> = () => {
const dispatch = useDispatch();
const onClickHandler = (event: any) => {
console.log('Click handler');
Geolocation.getCurrentPosition().then((position) => {
console.log(
`Click handler, position: ${position.coords.latitude}, ${position.coords.longitude}`
);
dispatch(
mapActions.setCenter({
lat: position.coords.latitude,
lon: position.coords.longitude,
})
);
dispatch(
mapActions.setCurrent({
lat: position.coords.latitude,
lon: position.coords.longitude,
})
);
});
};
return (
<IonButton onClick={onClickHandler} className='get-location' shape='round' size='small' fill='solid'>
<IonIcon slot='icon-only' icon={locateOutline} color='white' />
</IonButton>
);
};
export default GetLocation;
-58
View File
@@ -1,58 +0,0 @@
import React from 'react';
import { useDB } from 'react-pouchdb';
import GPX from '../../lib/gpx-parser-builder';
import '../../theme/get-location.css';
import { IonIcon, IonItem } from '@ionic/react';
import { cloudUpload } from 'ionicons/icons';
import { pushGpx } from '../../db/gpx';
const GpxImport: React.FC<{}> = () => {
const db = useDB();
const onChangeHandler = (event: any) => {
console.log('On change handler');
const file: File = event.target.files[0];
const fileReader = new FileReader();
fileReader.readAsText(file);
fileReader.addEventListener(
'load',
() => {
// this will then display a text file
console.log(fileReader.result);
const gpx = GPX.parse(fileReader.result);
pushGpx(db, {
gpx,
metadata: {
lastModified: new Date(file.lastModified).toISOString(),
importDate: new Date().toISOString(),
name: file.name,
size: file.size,
type: file.type,
},
});
},
false
);
};
return (
<IonItem>
<input
type='file'
id='gpx-import'
className='input-file'
accept='.gpx'
onChange={onChangeHandler}
/>
<label htmlFor='gpx-import'>
<IonIcon slot='icon-only' icon={cloudUpload} title="import" />
</label>
</IonItem>
);
};
export default GpxImport;
-20
View File
@@ -1,20 +0,0 @@
import React from 'react';
import { useFind } from 'react-pouchdb';
import Gpx from './Gpx';
const Gpxes: React.FC<{}> = () => {
const gpxes = useFind({
selector: {
type: 'gpx',
},
});
return gpxes.map((gpx: any) => {
console.log('doc');
return <Gpx key={gpx._id} gpx={gpx} />;
});
};
export default Gpxes;
-90
View File
@@ -1,90 +0,0 @@
import react, { useMemo, useEffect, useState } from 'react';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
import _ from 'lodash';
import { useDB } from 'react-pouchdb';
import Layer from '../slippy/layer';
import Slippy from '../slippy/slippy';
import TiledMap from './tiled-map';
import GetLocation from './get-location';
import Whiteboard from './whiteboard';
import CurrentLocation from './CurrentLocation';
import {
IonApp,
IonButtons,
IonContent,
IonFooter,
IonHeader,
IonToolbar,
} from '@ionic/react';
import Gpxes from './gpxes';
import GpxRecord from './GpxRecord';
import { initDb } from '../../db';
import TileServerChooserButton from './TileServerChooserButton';
import TileServerChooserDialog from './TileServerChooserDialog';
import TrackBrowser from './TracksBrowser';
import Settings from './Settings';
import Reticle from './Reticle';
import LocationInfo from './LocationInfo';
const Map: react.FC<{}> = () => {
const dispatch = useDispatch();
const resizeHandler = () => {
dispatch(mapActions.resize());
};
const debouncedResizeHandler = useMemo(
() => _.debounce(resizeHandler, 500),
[]
);
const db = useDB();
const [dbReady, setDbReady] = useState(false);
useEffect(() => {
window.addEventListener('resize', debouncedResizeHandler);
initDb(db, setDbReady);
}, []);
return (
<>
<IonContent fullscreen={true}>
<IonApp>
<TileServerChooserDialog />
<LocationInfo />
<Slippy>
<Whiteboard fixedChildren={<Reticle />}>
<CurrentLocation />
dbReady && <Gpxes />
</Whiteboard>
<Layer>
<TiledMap />
</Layer>
</Slippy>
</IonApp>
</IonContent>
<IonHeader className='ion-no-border' translucent={true}>
<IonToolbar>
<IonButtons slot='end'>
<TrackBrowser />
<GpxRecord />
<TileServerChooserButton />
<Settings />
</IonButtons>
</IonToolbar>
</IonHeader>
<IonFooter className='ion-no-border'>
<IonToolbar>
<IonButtons>
<GetLocation />
</IonButtons>
</IonToolbar>
</IonFooter>
</>
);
};
export default Map;
-153
View File
@@ -1,153 +0,0 @@
import React, { Fragment } from 'react';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
export interface TileProvider {
name: string;
minZoom: number;
maxZoom: number;
getTileUrl: { (zoom: number, x: number, y: number): string };
}
const getRandomItem = (items: any[]) => {
const idx = Math.floor(Math.random() * items.length);
return items[idx];
};
const abc = ['a', 'b', 'c'];
export const tileProviders: any = {
osm: {
name: 'Open Street Map',
minZoom: 0,
maxZoom: 19,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://tile.openstreetmap.org/' + zoom + '/' + x + '/' + y + '.png',
},
osmfr: {
name: 'Open Street Map France',
minZoom: 0,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.openstreetmap.fr/osmfr/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
otm: {
name: 'Open Topo Map',
minZoom: 2,
maxZoom: 17,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.opentopomap.org/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
cyclosm: {
name: 'CyclOSM',
minZoom: 0,
maxZoom: 19,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile-cyclosm.openstreetmap.fr/cyclosm/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
//https://b.tile.openstreetmap.fr/openriverboatmap/20/535762/382966.png
openriverboatmap: {
name: 'Open River Boat Map',
minZoom: 0,
maxZoom: 20,
getTileUrl: (zoom: number, x: number, y: number) =>
'https://' +
getRandomItem(abc) +
'.tile.openstreetmap.fr/openriverboatmap/' +
zoom +
'/' +
x +
'/' +
y +
'.png',
},
// cyclosmlite: {
// name: 'CyclOSM lite',
// minZoom: 0,
// maxZoom: 19,
// getTileUrl: (zoom: number, x: number, y: number) =>
// 'https://' +
// getRandomItem(abc) +
// '.tile-cyclosm.openstreetmap.fr/cyclosm-lite/' +
// zoom +
// '/' +
// x +
// '/' +
// y +
// '.png',
// },
// esrisat: {
// name: 'ESRI Satellite',
// minZoom: 0,
// maxZoom: 19,
// getTileUrl: (zoom: number, x: number, y: number) =>
// 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/' +
// zoom +
// '/' +
// x +
// '/' +
// y +
// '.jpg',
// },
};
const Tile: React.FC<{
ix: number;
iy: number;
x: number;
y: number;
zoom: number;
}> = (props: {
ix: number;
iy: number;
x: number;
y: number;
zoom: number;
}) => {
const tileProvider = useSelector(
(state: { map: MapState }) => state.map.scope.tileProvider
) as keyof typeof tileProviders;
const x = props.x % 2 ** props.zoom;
const canRender = props.y >= 0 && props.y <= 2 ** props.zoom - 1;
if (canRender) {
return (
canRender && (
<img
src={tileProviders[tileProvider].getTileUrl(props.zoom, x, props.y)}
className='tile'
alt=''
/>
)
);
}
return <Fragment />;
};
export default Tile;
-36
View File
@@ -1,36 +0,0 @@
import react from 'react';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
import Tile from './tile';
import '../../theme/map.css';
import _ from 'lodash';
export const tileSize = 256;
const TiledMap: react.FC<{}> = () => {
const tilesState = useSelector((state: { map: MapState }) => state.map.tiles);
console.log(`tilesState: ${JSON.stringify(tilesState)}`);
return (
<div className='tiles'>
{_.range(tilesState.nb.y).map((iy) => (
<div key={'y' + iy} className='tilesRow'>
{_.range(tilesState.nb.y).map((ix) => (
<Tile
key={'x' + ix + 'y' + iy}
iy={iy}
ix={ix}
x={tilesState.first.x + ix}
y={tilesState.first.y + iy}
zoom={tilesState.zoom}
/>
))}
</div>
))}
</div>
);
};
export default TiledMap;
-34
View File
@@ -1,34 +0,0 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
interface WhiteboardProps {
children?: any;
fixedChildren?: any;
}
const Whiteboard: React.FC<WhiteboardProps> = (props: WhiteboardProps) => {
const whiteBoardState = useSelector(
(state: { map: MapState }) => state.map.whiteboard
);
const windowState = useSelector(
(state: { map: MapState }) => state.map.window
);
return (
<svg
width={windowState.width}
height={windowState.height}
className='whiteboard'
>
{props.fixedChildren}
<g
transform={`scale(${whiteBoardState.scale}) translate(${whiteBoardState.translation.x},${whiteBoardState.translation.y})`}
>
{props.children}
</g>
</svg>
);
};
export default Whiteboard;
@@ -1,130 +0,0 @@
import react, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
interface DoubleTouchHandlerProps {
children: any;
}
const DoubleTouchHandler: react.FC<DoubleTouchHandlerProps> = (
props: DoubleTouchHandlerProps
) => {
const dispatch = useDispatch();
const initialTouchState = {
state: 'up',
touches: [
{ x: -1, y: -1 },
{ x: -1, y: -1 },
],
distance: -1,
timestamp: 0,
};
const [touchState, setTouchState] = useState(initialTouchState);
console.log('DoubleTouchHandler, touchState: ' + JSON.stringify(touchState));
const genericHandler = (event: any) => {
console.log('Log - Event: ' + event.type);
if (event.type.startsWith('touch')) {
if (event.touches.length > 1) {
console.log(
`${event.touches.length} touches, (${event.touches[0].pageX}, ${event.touches[0].pageY}), (${event.touches[1].pageX}, ${event.touches[1].pageY})`
);
}
console.log('touchState: ' + JSON.stringify(touchState));
return;
}
};
const touchCancelHandler = (event: any) => {
genericHandler(event);
setTouchState(initialTouchState);
};
const touchStartHandler = (event: any) => {
genericHandler(event);
if (event.touches.length === 2) {
setTouchState({
state: 'double',
touches: [
{ x: event.touches[0].pageX, y: event.touches[0].pageY },
{ x: event.touches[1].pageX, y: event.touches[1].pageY },
],
distance: Math.sqrt(
(event.touches[0].pageX - event.touches[1].pageX) ** 2 +
(event.touches[0].pageY - event.touches[1].pageY) ** 2
),
timestamp: Date.now(),
});
}
};
const touchEndHandler = (event: any) => {
genericHandler(event);
setTouchState(initialTouchState);
};
const touchMoveHandler = (event: any) => {
if (
(touchState.state === 'double') &&
((Date.now() - touchState.timestamp) > 50)
) {
if (event.touches.length === 2) {
genericHandler(event);
const newDistance = Math.sqrt(
(event.touches[0].pageX - event.touches[1].pageX) ** 2 +
(event.touches[0].pageY - event.touches[1].pageY) ** 2
);
const factor = newDistance / touchState.distance;
console.log(`+++++++++ ZOOM Factor is ${factor} ++++++++++`);
setTouchState({
state: 'double',
touches: [
{ x: event.touches[0].pageX, y: event.touches[0].pageY },
{ x: event.touches[1].pageX, y: event.touches[1].pageY },
],
distance: newDistance,
timestamp: Date.now(),
});
const previousCenter = {
x: (touchState.touches[0].x + touchState.touches[1].x) / 2,
y: (touchState.touches[0].y + touchState.touches[1].y) / 2,
};
const currentCenter = {
x: (event.touches[0].pageX + event.touches[1].pageX) / 2,
y: (event.touches[0].pageY + event.touches[1].pageY) / 2,
};
dispatch(
mapActions.scale({
factor: factor,
center: currentCenter,
})
);
dispatch(
mapActions.shift({
x: currentCenter.x - previousCenter.x,
y: currentCenter.y - previousCenter.y,
})
);
}
}
};
return (
<div
className='viewport double-touch-handler'
onTouchStart={touchStartHandler}
onTouchMove={touchMoveHandler}
onTouchEnd={touchEndHandler}
onTouchCancel={touchCancelHandler}
>
{props.children}
</div>
);
};
export default DoubleTouchHandler;
-30
View File
@@ -1,30 +0,0 @@
import react from 'react';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
import '../../theme/layer.css';
const Layer: react.FC<{
children?: JSX.Element;
}> = (props: { children?: JSX.Element }) => {
const slippyState = useSelector(
(state: { map: MapState }) => state.map.slippy
);
console.log(
`--- Rendering layer, slippyState: ${JSON.stringify(slippyState)} ---`
);
return (
<div
className='background'
style={{
transform: `translate(${slippyState.translation.x}px, ${slippyState.translation.y}px) scale(${slippyState.scale})`,
}}
>
{props.children}
</div>
);
};
export default Layer;
-118
View File
@@ -1,118 +0,0 @@
import react, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
interface MouseHandlerProps {
children: any;
}
const MouseHandler: react.FC<MouseHandlerProps> = (
props: MouseHandlerProps
) => {
const dispatch = useDispatch();
const initialMouseState = {
down: false,
starting: { x: -1, y: -1 },
timestamp: 0,
};
const [mouseState, setMouseState] = useState(initialMouseState);
console.log('MouseHandler, mouseState: ' + JSON.stringify(mouseState));
const genericHandler = (event: any) => {
console.log(`Log - Event: ${event.type}`);
if (event.pageX !== undefined) {
console.log(`Mouse : ${event.pageX}, ${event.pageY}, target: ${event.target}`);
console.log(
`mouseState: ' ${JSON.stringify(mouseState)} (+${
Date.now() - mouseState.timestamp
}ms) `
);
return;
}
};
const mouseLeaveHandler = (event: any) => {
genericHandler(event);
// throtteledMouseMoveHandler.cancel();
setMouseState(initialMouseState);
};
const mouseDownHandler = (event: any) => {
// event.preventDefault();
genericHandler(event);
setMouseState({
down: true,
starting: { x: event.pageX, y: event.pageY },
timestamp: Date.now(),
});
};
const mouseUpHandler = (event: any) => {
genericHandler(event);
// event.preventDefault();
setMouseState(initialMouseState);
};
const mouseMoveHandler = (event: any) => {
// event.preventDefault();
if (mouseState.down && Date.now() - mouseState.timestamp > 50) {
genericHandler(event);
console.log(
`dispatch ${JSON.stringify({
x: event.pageX - mouseState.starting.x,
y: event.pageY - mouseState.starting.y,
})}`
);
dispatch(
mapActions.shift({
x: event.pageX - mouseState.starting.x,
y: event.pageY - mouseState.starting.y,
})
);
setMouseState({
down: true,
starting: {
x: event.pageX,
y: event.pageY,
},
timestamp: Date.now(),
});
}
};
// const throtteledMouseMoveHandler = useCallback(
// _.throttle(mouseMoveHandler, 50),
// [mouseState.down, mouseState.starting.x, mouseState.starting.y]
// );
const doubleClickHandler = (event: any) => {
genericHandler(event);
dispatch(
mapActions.scale({
factor: Math.SQRT2,
center: { x: event.pageX, y: event.pageY },
})
);
};
return (
<div
className='viewport mouse-handler'
onMouseDown={mouseDownHandler}
onMouseMove={mouseMoveHandler}
onMouseUp={mouseUpHandler}
onMouseLeave={mouseLeaveHandler}
onDoubleClick={doubleClickHandler}
>
{props.children}
</div>
);
};
export default MouseHandler;
@@ -1,100 +0,0 @@
import react, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
interface SingleTouchHandlerProps {
children: any;
}
const SingleTouchHandler: react.FC<SingleTouchHandlerProps> = (
props: SingleTouchHandlerProps
) => {
const initialTouchState = {
state: 'up',
touch: { x: -1, y: -1 },
timestamp: 0,
};
const dispatch = useDispatch();
const [touchState, setTouchState] = useState(initialTouchState);
console.log('SingleTouchHandler, touchState: ' + JSON.stringify(touchState));
const genericHandler = (event: any) => {
console.log('Log - Event: ' + event.type);
if (event.type.startsWith('touch')) {
if (event.touches.length > 0) {
console.log(
`Touch1 : (${event.touches[0].pageX}, ${event.touches[0].pageY})`
);
}
console.log('touchState: ' + JSON.stringify(touchState));
return;
}
};
const touchCancelHandler = (event: any) => {
genericHandler(event);
setTouchState(initialTouchState);
};
const touchStartHandler = (event: any) => {
genericHandler(event);
// event.preventDefault();
if (event.touches.length === 1) {
setTouchState({
state: 'pointer',
touch: { x: event.touches[0].pageX, y: event.touches[0].pageY },
timestamp: Date.now(),
});
}
};
const touchEndHandler = (event: any) => {
genericHandler(event);
// event.preventDefault();
setTouchState(initialTouchState);
};
const touchMoveHandler = (event: any) => {
// event.preventDefault();
if (
touchState.state === 'pointer' &&
Date.now() - touchState.timestamp > 50
) {
if (event.touches.length === 1) {
genericHandler(event);
dispatch(
mapActions.shift({
x: event.touches[0].pageX - touchState.touch.x,
y: event.touches[0].pageY - touchState.touch.y,
})
);
setTouchState({
state: 'pointer',
touch: {
x: event.touches[0].pageX,
y: event.touches[0].pageY,
},
timestamp: Date.now(),
});
}
}
};
return (
<div
className='viewport single-touch-handler'
onTouchStart={touchStartHandler}
onTouchMove={touchMoveHandler}
onTouchEnd={touchEndHandler}
onTouchCancel={touchCancelHandler}
>
{props.children}
</div>
);
};
export default SingleTouchHandler;
-40
View File
@@ -1,40 +0,0 @@
import react from 'react';
import MouseHandler from './mouse-handler';
import SingleTouchHandler from './single-touch-handler';
import DoubleTouchHandler from './double-touch-handler';
import WheelHandler from './wheel-handler';
import { useSelector } from 'react-redux';
import { MapState } from '../../store/map';
import '../../theme/slippy.css';
interface SlippyProps {
children?: any;
}
const Slippy: react.FC<SlippyProps> = (props: SlippyProps) => {
//console.log(`--- Rendering viewport, props: ${JSON.stringify(props)} ---`);
const slippyState = useSelector(
(state: { map: MapState }) => state.map.slippy
);
console.log(`slippyState: ${JSON.stringify(slippyState)}`);
return (
<div className='slippy'>
<MouseHandler>
<SingleTouchHandler>
<DoubleTouchHandler>
<WheelHandler>
{props.children}
</WheelHandler>
</DoubleTouchHandler>
</SingleTouchHandler>
</MouseHandler>
</div>
);
};
export default Slippy;
-62
View File
@@ -1,62 +0,0 @@
import react, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { mapActions } from '../../store/map';
interface WheelHandlerProps {
children: any;
}
const WheelHandler: react.FC<WheelHandlerProps> = (
props: WheelHandlerProps
) => {
const dispatch = useDispatch();
const initialWheelState = {
timestamp: 0,
};
const [wheelState, setWheelState] = useState(initialWheelState);
console.log('WheelHandler, wheelState: ' + JSON.stringify(wheelState));
const genericHandler = (event: any) => {
console.log(`Log - Event: ${event.type}`);
if (event.deltaY !== undefined) {
console.log(`Wheel : ${event.deltaY}, ${event.deltaMode}`);
console.log(
`wheelState: ' ${JSON.stringify(wheelState)} (+${
Date.now() - wheelState.timestamp
}ms) `
);
return;
}
};
const wheelEventHandler = (event: any) => {
genericHandler(event);
if (
event.deltaMode === WheelEvent.DOM_DELTA_PIXEL &&
Date.now() - wheelState.timestamp > 100
) {
dispatch(
mapActions.scale({
factor: event.deltaY < 0 ? Math.SQRT2 : Math.SQRT1_2,
center: { x: event.pageX, y: event.pageY },
})
);
setWheelState({
timestamp: Date.now(),
});
}
};
return (
<div className='viewport wheel-handler' onWheel={wheelEventHandler}>
{props.children}
</div>
);
};
export default WheelHandler;