diff --git a/README.md b/README.md index 6a37550..db888b1 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,11 @@ import Geolocation from '@react-native-community/geolocation'; Geolocation.getCurrentPosition(info => console.log(info)); ``` -Check out the [example project](example) for more examples. +Check out the [example project](example) for more examples. The example app +also provides a **Logging** section that records `watchPosition` updates into +timestamped CSV files inside `/storage/Android/data//files/` (or the +platform-equivalent app directory) and offers a shortcut to clear previously +recorded logs on the device. ## Methods diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..84f7855 --- /dev/null +++ b/example/index.js @@ -0,0 +1 @@ +import './index.tsx'; diff --git a/example/index.tsx b/example/index.tsx index 879f181..0d2d7e7 100644 --- a/example/index.tsx +++ b/example/index.tsx @@ -24,6 +24,7 @@ function ExampleApp() { + ); diff --git a/example/package.json b/example/package.json index 69e214f..f27f42e 100644 --- a/example/package.json +++ b/example/package.json @@ -16,6 +16,7 @@ "react": "18.2.0", "react-native": "0.74.2", "react-native-background-timer": "^2.4.1", + "react-native-fs": "^2.20.0", "react-native-safe-area-context": "4.10.5", "react-native-screens": "3.32.0" }, @@ -28,4 +29,4 @@ "preset": "react-native" }, "packageManager": "yarn@3.6.4" -} \ No newline at end of file +} diff --git a/example/src/components/WatchOptionsForm.tsx b/example/src/components/WatchOptionsForm.tsx new file mode 100644 index 0000000..737e28d --- /dev/null +++ b/example/src/components/WatchOptionsForm.tsx @@ -0,0 +1,228 @@ +/** + * Copyright (c) React Native Community + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +'use strict'; + +import React from 'react'; +import { + StyleSheet, + View, + Text, + Switch, + TextInput, + Platform, +} from 'react-native'; +import type { GeolocationOptions } from '@react-native-community/geolocation'; + +export const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes +export const DEFAULT_DISTANCE_FILTER_M = 100; + +export type WatchOptionFormValues = { + enableHighAccuracy: boolean; + timeout: string; + maximumAge: string; + distanceFilter: string; + useSignificantChanges: boolean; + interval: string; + fastestInterval: string; +}; + +export const initialWatchOptionValues: WatchOptionFormValues = { + enableHighAccuracy: false, + timeout: '', + maximumAge: '', + distanceFilter: '', + useSignificantChanges: false, + interval: '', + fastestInterval: '', +}; + +const parseNumber = (value: string) => { + if (value.trim().length === 0) { + return undefined; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +}; + +export const buildWatchOptions = ( + values: WatchOptionFormValues +): GeolocationOptions => { + const options: GeolocationOptions = {}; + + if (values.enableHighAccuracy) { + options.enableHighAccuracy = true; + } + + const timeoutValue = parseNumber(values.timeout); + if (timeoutValue !== undefined) { + options.timeout = timeoutValue; + } + + const maximumAgeValue = parseNumber(values.maximumAge); + if (maximumAgeValue !== undefined) { + options.maximumAge = maximumAgeValue; + } + + const distanceFilterValue = parseNumber(values.distanceFilter); + if (distanceFilterValue !== undefined) { + options.distanceFilter = distanceFilterValue; + } + + if (Platform.OS === 'ios') { + options.useSignificantChanges = values.useSignificantChanges; + } + + if (Platform.OS === 'android') { + const intervalValue = parseNumber(values.interval); + if (intervalValue !== undefined) { + options.interval = intervalValue; + } + + const fastestIntervalValue = parseNumber(values.fastestInterval); + if (fastestIntervalValue !== undefined) { + options.fastestInterval = fastestIntervalValue; + } + } + + return options; +}; + +export const buildCurrentPositionOptions = ( + values: WatchOptionFormValues +): GeolocationOptions => { + const options: GeolocationOptions = {}; + + if (values.enableHighAccuracy) { + options.enableHighAccuracy = true; + } + + const timeoutValue = parseNumber(values.timeout); + if (timeoutValue !== undefined) { + options.timeout = timeoutValue; + } + + const maximumAgeValue = parseNumber(values.maximumAge); + if (maximumAgeValue !== undefined) { + options.maximumAge = maximumAgeValue; + } + + return options; +}; + +type Props = { + values: WatchOptionFormValues; + onChange: ( + field: T, + value: WatchOptionFormValues[T] + ) => void; +}; + +export function WatchOptionsForm({ values, onChange }: Props) { + return ( + <> + + High accuracy (default: off) + onChange('enableHighAccuracy', next)} + /> + + + + Timeout (ms · default: {DEFAULT_TIMEOUT_MS}) + + onChange('timeout', next)} + placeholder={`${DEFAULT_TIMEOUT_MS}`} + /> + + + Maximum age (ms · default: Infinity) + onChange('maximumAge', next)} + placeholder="Infinity" + /> + + + + Distance filter (m · default: {DEFAULT_DISTANCE_FILTER_M}) + + onChange('distanceFilter', next)} + placeholder={`${DEFAULT_DISTANCE_FILTER_M}`} + /> + + {Platform.OS === 'ios' && ( + + Use significant changes (default: false) + onChange('useSignificantChanges', next)} + /> + + )} + {Platform.OS === 'android' && ( + <> + + Interval (ms · default: system) + onChange('interval', next)} + placeholder="System" + /> + + + Fastest interval (ms · default: system) + onChange('fastestInterval', next)} + placeholder="System" + /> + + + )} + + ); +} + +const styles = StyleSheet.create({ + row: { + marginBottom: 12, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + label: { + flex: 1, + }, + input: { + borderWidth: 1, + borderColor: '#ccc', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 4, + minWidth: 100, + textAlign: 'right', + }, +}); diff --git a/example/src/examples/WatchPosition.tsx b/example/src/examples/WatchPosition.tsx index 6653406..20c85ce 100644 --- a/example/src/examples/WatchPosition.tsx +++ b/example/src/examples/WatchPosition.tsx @@ -11,17 +11,43 @@ import React, { useState, useEffect } from 'react'; import { StyleSheet, Text, View, Alert, Button } from 'react-native'; -import Geolocation from '@react-native-community/geolocation'; +import Geolocation, { type GeolocationResponse } from '@react-native-community/geolocation'; +import { + WatchOptionsForm, + buildCurrentPositionOptions, + buildWatchOptions, + initialWatchOptionValues, + type WatchOptionFormValues, +} from '../components/WatchOptionsForm'; export default function WatchPositionExample() { + const [formValues, setFormValues] = + useState(initialWatchOptionValues); + const [position, setPosition] = + useState(null); + const [subscriptionId, setSubscriptionId] = useState(null); + const watchPosition = () => { try { + const currentOptions = buildCurrentPositionOptions(formValues); + console.log('watchPosition.getCurrentPositionOptions', currentOptions); + Geolocation.getCurrentPosition( + (nextPosition) => { + setPosition(nextPosition); + }, + (error) => Alert.alert('GetCurrentPosition Error', JSON.stringify(error)), + currentOptions + ); + + const watchOptions = buildWatchOptions(formValues); + console.log('watchPosition.startOptions', watchOptions); const watchID = Geolocation.watchPosition( - (position) => { - console.log('watchPosition', JSON.stringify(position)); - setPosition(JSON.stringify(position)); + (nextPosition) => { + console.log('watchPosition', JSON.stringify(nextPosition)); + setPosition(nextPosition); }, - (error) => Alert.alert('WatchPosition Error', JSON.stringify(error)) + (error) => Alert.alert('WatchPosition Error', JSON.stringify(error)), + watchOptions ); setSubscriptionId(watchID); } catch (error) { @@ -35,8 +61,6 @@ export default function WatchPositionExample() { setPosition(null); }; - const [position, setPosition] = useState(null); - const [subscriptionId, setSubscriptionId] = useState(null); useEffect(() => { return () => { clearWatch(); @@ -46,10 +70,22 @@ export default function WatchPositionExample() { return ( + + setFormValues((prev) => ({ ...prev, [field]: value })) + } + /> Last position: - {position || 'unknown'} + {position ? JSON.stringify(position) : 'unknown'} + {position && ( + + Position timestamp:{' '} + {new Date(position.timestamp).toLocaleTimeString()} + + )} {subscriptionId !== null ? (