Restarting from scratch
This commit is contained in:
1 parent
ed951208e8
commit
a4bd11aab0
88 files changed
+1714
-17027
No files matched your search
@@ -0,0 +1,24 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.container strong {
|
||||
font-size: 20px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
.container p {
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
color: #8c8c8c;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container a {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import './ExploreContainer.css';
|
||||
|
||||
interface ContainerProps { }
|
||||
|
||||
const ExploreContainer: React.FC<ContainerProps> = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<strong>Ready to create an app?</strong>
|
||||
<p>Start with Ionic <a target="_blank" rel="noopener noreferrer" href="https://ionicframework.com/docs/components">UI Components</a></p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExploreContainer;
|
||||
@@ -1,12 +0,0 @@
|
||||
import react, { Fragment } from 'react';
|
||||
|
||||
interface FormProps {
|
||||
context: any;
|
||||
validate: (ctx: any) => {};
|
||||
}
|
||||
|
||||
const Form: react.FC<{}> = () => {
|
||||
return <Fragment />;
|
||||
};
|
||||
|
||||
export default Form;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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
@@ -1 +0,0 @@
|
||||
declare module 'avatar-initials';
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 546 B |
Vendored
-1
@@ -1 +0,0 @@
|
||||
declare module 'font-gis';
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
Reference in new issue
Block a user