Restarting from scratch

This commit is contained in:
Eric van der Vlist 2022-10-17 10:08:00 +02:00
parent ed951208e8
commit a4bd11aab0
88 changed files with 1714 additions and 17027 deletions

View File

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

10083
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,59 +3,29 @@
"version": "0.0.1",
"private": true,
"dependencies": {
"@awesome-cordova-plugins/core": "^5.45.0",
"@awesome-cordova-plugins/geolocation": "^5.45.0",
"@capacitor-community/background-geolocation": "^1.2.4",
"@capacitor-community/file-opener": "^1.0.1",
"@capacitor/android": "^4.2.0",
"@capacitor/app": "4.0.1",
"@capacitor/cli": "^4.2.0",
"@capacitor/core": "^4.2.0",
"@capacitor/filesystem": "^4.1.2",
"@capacitor/core": "4.3.0",
"@capacitor/haptics": "4.0.1",
"@capacitor/keyboard": "4.0.1",
"@capacitor/status-bar": "4.0.1",
"@chatscope/chat-ui-kit-react": "^1.9.7",
"@chatscope/chat-ui-kit-styles": "^1.4.0",
"@ionic/react": "^6.2.6",
"@ionic/react-router": "^6.2.6",
"@openpgp/asmcrypto.js": "^2.3.2",
"@reduxjs/toolkit": "^1.8.5",
"@ionic/react": "^6.0.0",
"@ionic/react-router": "^6.0.0",
"@testing-library/jest-dom": "^5.11.9",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"@trapezedev/configure": "^5.0.6",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^12.6.3",
"@types/jest": "^26.0.20",
"@types/node": "^12.19.15",
"@types/pouchdb": "^6.4.0",
"@types/react": "^18.0.18",
"@types/react": "^18.0.17",
"@types/react-dom": "^18.0.6",
"@types/react-router": "^5.1.11",
"@types/react-router-dom": "^5.1.7",
"buffer": "^6.0.3",
"cordova-plugin-geolocation": "^4.1.0",
"crypto-browserify": "^3.12.0",
"docuri": "^4.2.2",
"font-gis": "^1.0.5",
"git": "^0.1.5",
"ionicons": "^6.0.3",
"isomorphic-xml2js": "^0.1.3",
"lodash": "^4.17.21",
"pouchdb": "^7.3.0",
"pouchdb-browser": "^7.3.0",
"pouchdb-find": "^7.3.0",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-dom": "^18.2.0",
"react-localization": "^1.0.19",
"react-pouchdb": "^2.1.0",
"react-redux": "^8.0.2",
"react-router": "^6.3.0",
"react-router-dom": "^6.3.0",
"react-router": "^5.2.0",
"react-router-dom": "^5.2.0",
"react-scripts": "^5.0.0",
"status": "^0.0.13",
"stream-browserify": "^3.0.0",
"typescript": "^4.8.3",
"typescript": "^4.1.3",
"web-vitals": "^0.2.4",
"workbox-background-sync": "^5.1.4",
"workbox-broadcast-update": "^5.1.4",
@ -95,8 +65,7 @@
]
},
"devDependencies": {
"@types/lodash": "^4.14.184"
"@capacitor/cli": "4.3.0"
},
"description": "An Ionic project",
"postinstall": "npx patch-package"
"description": "An Ionic project"
}

View File

@ -1,15 +0,0 @@
diff --git a/node_modules/localized-strings/lib/LocalizedStrings.js b/node_modules/localized-strings/lib/LocalizedStrings.js
index eefa6c8..e0a3cae 100644
--- a/node_modules/localized-strings/lib/LocalizedStrings.js
+++ b/node_modules/localized-strings/lib/LocalizedStrings.js
@@ -192,8 +192,8 @@ var LocalizedStrings = function () {
if (_this4._opts.logsEnabled) {
console.log("\uD83D\uDEA7 \uD83D\uDC77 key '" + key + "' not found in localizedStrings for language " + _this4._language + " \uD83D\uDEA7");
}
- } else if (typeof strings[key] !== "string") {
- // It's an object
+ } else if (typeof strings[key] !== "string" && defaultStrings[key].$$typeof === undefined) {
+ // It's an object (and it's not a React component)
_this4._fallbackValues(defaultStrings[key], strings[key]);
}
});

View File

@ -1,44 +0,0 @@
<svg
xmlns="http://www.w3.org/2000/svg"
width="1000"
height="2000"
viewBox="0 0 250 500"
>
<g>
<circle
cx="100"
cy="100"
r="10"
fill='green'
stroke='black'
stroke-width="1"
vector-effect="non-scaling-stroke"
/>
<circle
cx="200"
cy="200"
r="10"
fill='green'
stroke='black'
stroke-width="1"
vector-effect="non-scaling-size"
/>
<path
d="M 50, 50 l 0.0001 0"
stroke-width="10"
stroke-linecap="round"
stroke="green"
vector-effect="non-scaling-stroke"
/>
</g>
</svg>
<!--
<svg width="921" height="1301"><g transform="scale(256) translate(6.916599514166666,8.07269355334303))"><circle cx="0.7154276391666667" cy="0.6137091783430285" r="10px" fill="green" vector-effect="non-scaling-size"></circle></g></svg>
<svg width="921" height="1301"><g transform="scale(256) translate(6.916599514166666,8.07269355334303))"><path d="M 0.7154276391666667 0.6137091783430285 l 0.0001 0" vector-effect="non-scaling-stroke" stroke-width="50" stroke-linecap="round" stroke="green"></path></g></svg>
-->

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -16,11 +16,7 @@
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link
rel="shortcut icon"
type="image/png"
href="%PUBLIC_URL%/assets/icon/favicon.png"
/>
<link rel="shortcut icon" type="image/png" href="%PUBLIC_URL%/assets/icon/favicon.png" />
<!-- add to homescreen for ios -->
<meta name="apple-mobile-web-app-capable" content="yes" />
@ -31,4 +27,5 @@
<body>
<div id="root"></div>
</body>
</html>

View File

@ -1,6 +1,6 @@
{
"short_name": "Dyomedea",
"name": "Dyomedea",
"short_name": "Ionic App",
"name": "My Ionic App",
"icons": [
{
"src": "assets/icon/favicon.png",

8
src/App.test.tsx Normal file
View File

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

View File

@ -1,9 +1,7 @@
import { IonApp, setupIonicReact } from '@ionic/react';
import store from './store/index';
import { Provider } from 'react-redux';
import { Suspense } from 'react';
import { PouchDB } from 'react-pouchdb';
import { Redirect, Route } from 'react-router-dom';
import { IonApp, IonRouterOutlet, setupIonicReact } from '@ionic/react';
import { IonReactRouter } from '@ionic/react-router';
import Home from './pages/Home';
/* Core CSS required for Ionic components to work properly */
import '@ionic/react/css/core.css';
@ -24,50 +22,21 @@ import '@ionic/react/css/display.css';
/* Theme variables */
import './theme/variables.css';
import Map from './components/map/map';
setupIonicReact();
// See https://stackoverflow.com/questions/71538643/property-wakelock-does-not-exist-on-type-navigator
const requestWakeLock = async () => {
const anyNav: any = navigator;
if ('wakeLock' in navigator) {
try {
const wakeLock = await anyNav['wakeLock'].request('screen');
} catch (err: any) {
// The wake lock request fails - usually system-related, such as low battery.
console.log(`Wake lock request failed: ${err.name}, ${err.message}`);
}
} else {
console.log('No wake lock support here...');
}
};
const handleVisibilityChange = () => {
if (document.hidden) {
console.log('Application hidden');
} else {
console.log('Application visible');
requestWakeLock();
}
};
// See https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API
document.addEventListener('visibilitychange', handleVisibilityChange, false);
requestWakeLock();
const App: React.FC = () => {
return (
<IonApp>
<Provider store={store}>
<PouchDB name='dyomedea' auto_compaction={true} revs_limit={10}>
<Suspense fallback='loading...'>
<Map />
</Suspense>
</PouchDB>
</Provider>
</IonApp>
);
};
const App: React.FC = () => (
<IonApp>
<IonReactRouter>
<IonRouterOutlet>
<Route exact path="/home">
<Home />
</Route>
<Route exact path="/">
<Redirect to="/home" />
</Route>
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
);
export default App;

View File

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

View File

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

View File

@ -1,12 +0,0 @@
import react, { Fragment } from 'react';
interface FormProps {
context: any;
validate: (ctx: any) => {};
}
const Form: react.FC<{}> = () => {
return <Fragment />;
};
export default Form;

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;

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;

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;

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;

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;

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;

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;

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;

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