Enhance documentation and type definitions for location tracking and property components
This commit is contained in:
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { MapView, Marker, useLocationTracking, Polyline } from '@lynkedup/map-sdk';
|
||||
|
||||
export default function App() {
|
||||
const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking();
|
||||
const { isTracking, path, startTracking, stopTracking, clear } = useLocationTracking(/* opts?: TrackingOptions, geolocation?: GeolocationAPI */);
|
||||
|
||||
return (
|
||||
<MapView style={{ flex: 1 }} initialRegion={{ latitude: 37.78825, longitude: -122.4324, latitudeDelta: 0.0922, longitudeDelta: 0.0421 }}>
|
||||
@@ -27,6 +27,8 @@ export default function App() {
|
||||
</MapView>
|
||||
);
|
||||
}
|
||||
|
||||
> Tip: `useLocationTracking()` accepts an optional `TrackingOptions` object and an optional `GeolocationAPI` (handy for testing with a mocked geolocation provider). Ensure you request platform location permissions before starting tracking.
|
||||
```
|
||||
|
||||
Tracking notes:
|
||||
@@ -103,11 +105,14 @@ export default function PropertiesScreen() {
|
||||
|
||||
Props summary:
|
||||
|
||||
- `apiUrl?: string` — POST endpoint used for property searches (defaults to the demo host).
|
||||
- `initialRegion?: Region` — starting region.
|
||||
- `radius?: number` — search radius in meters (default: 500).
|
||||
- `showSearch?: boolean` — render search bar (default: true).
|
||||
- `onSelectProperty?: (item) => void` — callback for property selection.
|
||||
| Prop | Type | Default | Description |
|
||||
| ---- | ---- | ------- | ----------- |
|
||||
| `apiUrl` | `string` | demo host | POST endpoint used for property searches. Should accept `{ location, radius }` and return `{ success, properties, location }`.
|
||||
| `initialRegion` | `Region` | `DEFAULT_REGION` | Starting region for the map.
|
||||
| `radius` | `number` | `500` | Search radius in meters.
|
||||
| `showSearch` | `boolean` | `true` | Show or hide the built-in search bar.
|
||||
| `onSelectProperty` | `(item: PropertyItem) => void` | — | Callback invoked when a property is selected.
|
||||
| `mapProps` | `Partial<RNMapViewProps>` | — | Props forwarded to the underlying `react-native-maps` view (e.g., `mapType`, `showsMyLocationButton`).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -2,32 +2,44 @@ import React from 'react';
|
||||
import { View, Text, StyleSheet } from 'react-native';
|
||||
import { Marker } from 'react-native-maps';
|
||||
|
||||
/**
|
||||
* Minimal property item shape used by the SDK components.
|
||||
*/
|
||||
export type PropertyItem = {
|
||||
id: string | number;
|
||||
location?: { lat: number; lng: number };
|
||||
price?: number;
|
||||
price_display?: string;
|
||||
address?: string;
|
||||
bedrooms?: number;
|
||||
bathrooms?: number;
|
||||
polygon?: number[][]; // [[lat, lng], ...]
|
||||
};
|
||||
|
||||
/**
|
||||
* Simple price formatter that returns human-readable price strings (K/M abbreviations).
|
||||
*/
|
||||
export const formatPrice = (value?: number | string) => {
|
||||
if (!value) return '';
|
||||
if (value === null || value === undefined || value === '') return '';
|
||||
const num = typeof value === 'string' ? Number(value) : value;
|
||||
if (!num) return String(value);
|
||||
if (!num && num !== 0) return String(value);
|
||||
if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
|
||||
if (num >= 1000) return `${Math.round(num / 1000)}K`;
|
||||
return num.toString();
|
||||
};
|
||||
|
||||
const PriceMarker: React.FC<{
|
||||
export type PriceMarkerProps = {
|
||||
item: PropertyItem;
|
||||
selected?: boolean;
|
||||
onPress?: (item: PropertyItem) => void;
|
||||
}> = ({ item, selected, onPress }) => {
|
||||
if (!item?.location) return null;
|
||||
};
|
||||
|
||||
return (
|
||||
<Marker
|
||||
coordinate={{ latitude: item.location.lat, longitude: item.location.lng }}
|
||||
/**
|
||||
* A small marker component that displays a price label and forwards press events.
|
||||
*/
|
||||
const PriceMarker: React.FC<PriceMarkerProps> = ({ item, selected, onPress }) => {
|
||||
coordinate = {{ latitude: item.location.lat, longitude: item.location.lng }
|
||||
}
|
||||
onPress = {(e) => {
|
||||
e.stopPropagation();
|
||||
onPress && onPress(item);
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
StyleSheet,
|
||||
Platform,
|
||||
Text,
|
||||
FlatList,
|
||||
Image,
|
||||
@@ -13,21 +12,32 @@ import {
|
||||
TouchableOpacity,
|
||||
} from 'react-native';
|
||||
import ClusteredMapView from 'react-native-map-clustering';
|
||||
import { Polygon } from 'react-native-maps';
|
||||
import Geolocation from '@react-native-community/geolocation';
|
||||
import RNMapView, { Region, Polygon, LatLng } from 'react-native-maps';
|
||||
import type { MapViewProps as RNMapViewProps } from 'react-native-maps';
|
||||
import PriceMarker, { formatPrice, PropertyItem } from './PriceMarker';
|
||||
|
||||
const { width } = Dimensions.get('window');
|
||||
|
||||
/**
|
||||
* Props for the `PropertyMap` high-level component.
|
||||
*/
|
||||
export type PropertyMapProps = {
|
||||
/** API endpoint used for POST property searches. Should accept `{ location, radius }` and return `{ success, properties, location }` */
|
||||
apiUrl?: string;
|
||||
initialRegion?: any;
|
||||
/** Initial map region */
|
||||
initialRegion?: Region;
|
||||
/** Search radius in meters (default: 500) */
|
||||
radius?: number;
|
||||
/** Show/hide the search bar */
|
||||
showSearch?: boolean;
|
||||
/** Callback when a property/item is selected */
|
||||
onSelectProperty?: (item: PropertyItem) => void;
|
||||
/** Optional props forwarded to the underlying map view */
|
||||
mapProps?: Partial<RNMapViewProps>;
|
||||
};
|
||||
|
||||
export const DEFAULT_REGION = {
|
||||
/** Default region used when none is provided */
|
||||
export const DEFAULT_REGION: Region = {
|
||||
latitude: 25.48131,
|
||||
longitude: 81.854249,
|
||||
latitudeDelta: 0.02,
|
||||
@@ -35,20 +45,24 @@ export const DEFAULT_REGION = {
|
||||
};
|
||||
|
||||
export const getPolygonCoords = (polygon?: number[][]) => {
|
||||
if (!polygon) return [];
|
||||
if (!polygon) return [] as LatLng[];
|
||||
return polygon.map(([lat, lng]) => ({ latitude: lat, longitude: lng }));
|
||||
};
|
||||
|
||||
/**
|
||||
* `PropertyMap` — a ready-to-use map screen for property searches. Includes clustering, polygon rendering and a bottom property card.
|
||||
*/
|
||||
const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
apiUrl = 'http://64.227.108.180:5000/property-search',
|
||||
initialRegion = DEFAULT_REGION,
|
||||
radius = 500,
|
||||
showSearch = true,
|
||||
onSelectProperty,
|
||||
mapProps = {},
|
||||
}) => {
|
||||
const mapRef = useRef<any>(null);
|
||||
const mapRef = useRef<RNMapView | null>(null);
|
||||
|
||||
const [region, setRegion] = useState<any>(initialRegion);
|
||||
const [region, setRegion] = useState<Region>(initialRegion);
|
||||
const [properties, setProperties] = useState<PropertyItem[]>([]);
|
||||
const [selectedItem, setSelectedItem] = useState<PropertyItem | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -70,12 +84,16 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
|
||||
const requestLocation = async () => {
|
||||
try {
|
||||
Geolocation.getCurrentPosition((pos) => {
|
||||
// prefer bundled community geolocation if available
|
||||
const geo = (global.navigator && (global.navigator as any).geolocation) || null;
|
||||
if (geo?.getCurrentPosition) {
|
||||
geo.getCurrentPosition((pos: any) => {
|
||||
const { latitude, longitude } = pos.coords;
|
||||
const newRegion = { latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 };
|
||||
const newRegion = { latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 } as Region;
|
||||
setRegion(newRegion);
|
||||
mapRef.current?.animateToRegion?.(newRegion, 1000);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
@@ -96,13 +114,13 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
const json = await response.json();
|
||||
|
||||
if (json?.success && json?.location?.coordinates?.lat && json?.location?.coordinates?.lng) {
|
||||
const newRegion = {
|
||||
const newRegion: Region = {
|
||||
latitude: json.location.coordinates.lat,
|
||||
longitude: json.location.coordinates.lng,
|
||||
latitudeDelta: 0.02,
|
||||
longitudeDelta: 0.02,
|
||||
};
|
||||
setProperties(json.properties || []);
|
||||
setProperties((json.properties as PropertyItem[]) || []);
|
||||
setRegion(newRegion);
|
||||
mapRef.current?.animateToRegion?.(newRegion, 1000);
|
||||
}
|
||||
@@ -149,7 +167,7 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
ref={mapRef}
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
region={region}
|
||||
onRegionChangeComplete={(r: any) => {
|
||||
onRegionChangeComplete={(r: Region) => {
|
||||
if (!r?.longitudeDelta) return;
|
||||
setRegion(r);
|
||||
}}
|
||||
@@ -157,10 +175,7 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
animationEnabled
|
||||
clusterPressMaxZoom={16}
|
||||
spiralEnabled={false}
|
||||
>
|
||||
{properties.map((item) =>
|
||||
(item as any).polygon ? (
|
||||
<Polygon
|
||||
{...mapProps}
|
||||
key={`poly-${item.id}`}
|
||||
coordinates={getPolygonCoords((item as any).polygon)}
|
||||
strokeColor={selectedItem?.id === item.id ? '#F4C430' : '#8B0000'}
|
||||
@@ -175,13 +190,16 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
))}
|
||||
</ClusteredMapView>
|
||||
|
||||
{loading && (
|
||||
{
|
||||
loading && (
|
||||
<View style={styles.loader}>
|
||||
<ActivityIndicator size="large" />
|
||||
</View>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
{selectedItem && (
|
||||
{
|
||||
selectedItem && (
|
||||
<Animated.View style={[styles.popupContainer, { transform: [{ translateY: popupTranslateY }] }]}>
|
||||
<FlatList
|
||||
horizontal
|
||||
@@ -202,7 +220,8 @@ const PropertyMap: React.FC<PropertyMapProps> = ({
|
||||
)}
|
||||
/>
|
||||
</Animated.View>
|
||||
)}
|
||||
)
|
||||
}
|
||||
</View >
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,22 +1,49 @@
|
||||
import { useRef, useState, useCallback } from 'react';
|
||||
import type { LatLng } from 'react-native-maps';
|
||||
|
||||
/**
|
||||
* Options for location tracking.
|
||||
* - distanceFilter: minimum change in meters to receive updates.
|
||||
* - interval / fastestInterval: Android/JS timers (ms).
|
||||
* - accuracy: 'high' enables high accuracy via GPS.
|
||||
*/
|
||||
export type TrackingOptions = {
|
||||
distanceFilter?: number; // meters
|
||||
interval?: number; // ms
|
||||
fastestInterval?: number; // ms
|
||||
distanceFilter?: number;
|
||||
interval?: number;
|
||||
fastestInterval?: number;
|
||||
accuracy?: 'high' | 'balanced' | 'low';
|
||||
};
|
||||
|
||||
export function useLocationTracking(opts: TrackingOptions = {}) {
|
||||
/**
|
||||
* Minimal Geolocation API interface (compatible with browser and @react-native-community/geolocation).
|
||||
*/
|
||||
export type GeolocationAPI = {
|
||||
watchPosition: (
|
||||
success: (pos: { coords: { latitude: number; longitude: number; accuracy?: number } }) => void,
|
||||
error?: (err: any) => void,
|
||||
options?: { enableHighAccuracy?: boolean; distanceFilter?: number; interval?: number }
|
||||
) => number | string;
|
||||
clearWatch: (id: number | string) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to track user's location path.
|
||||
*
|
||||
* @param opts TrackingOptions - config for watchPosition
|
||||
* @param geolocation optional custom geolocation implementation (useful for testing)
|
||||
* @returns object with tracking state and control functions
|
||||
*/
|
||||
export function useLocationTracking(opts: TrackingOptions = {}, geolocation?: GeolocationAPI) {
|
||||
const [isTracking, setIsTracking] = useState(false);
|
||||
const [path, setPath] = useState<LatLng[]>([]);
|
||||
const [current, setCurrent] = useState<LatLng | null>(null);
|
||||
const watchIdRef = useRef<number | null>(null);
|
||||
const watchIdRef = useRef<number | string | null>(null);
|
||||
|
||||
const onPosition = useCallback((pos: any) => {
|
||||
const lat = pos.coords?.latitude;
|
||||
const lng = pos.coords?.longitude;
|
||||
const geo = geolocation ?? (global.navigator && (global.navigator as any).geolocation) as GeolocationAPI | undefined;
|
||||
|
||||
const onPosition = useCallback((pos: { coords: { latitude: number; longitude: number } }) => {
|
||||
const lat = pos?.coords?.latitude;
|
||||
const lng = pos?.coords?.longitude;
|
||||
if (typeof lat === 'number' && typeof lng === 'number') {
|
||||
const point = { latitude: lat, longitude: lng } as LatLng;
|
||||
setPath((p) => [...p, point]);
|
||||
@@ -26,33 +53,33 @@ export function useLocationTracking(opts: TrackingOptions = {}) {
|
||||
|
||||
const startTracking = useCallback(() => {
|
||||
if (isTracking) return;
|
||||
if (!global.navigator?.geolocation?.watchPosition) {
|
||||
if (!geo || !geo.watchPosition) {
|
||||
console.warn('[useLocationTracking] Geolocation.watchPosition not available.');
|
||||
return;
|
||||
}
|
||||
|
||||
const id = global.navigator.geolocation.watchPosition(
|
||||
const id = geo.watchPosition(
|
||||
onPosition,
|
||||
(err: any) => console.warn('geolocation error', err),
|
||||
{
|
||||
enableHighAccuracy: opts.accuracy === 'high',
|
||||
distanceFilter: opts.distanceFilter ?? 0,
|
||||
interval: opts.interval
|
||||
interval: opts.interval,
|
||||
}
|
||||
);
|
||||
|
||||
watchIdRef.current = id as unknown as number;
|
||||
watchIdRef.current = id;
|
||||
setIsTracking(true);
|
||||
}, [isTracking, onPosition, opts.accuracy, opts.distanceFilter, opts.interval]);
|
||||
}, [isTracking, onPosition, geo, opts.accuracy, opts.distanceFilter, opts.interval]);
|
||||
|
||||
const stopTracking = useCallback(() => {
|
||||
const id = watchIdRef.current;
|
||||
if (id != null && global.navigator?.geolocation?.clearWatch) {
|
||||
global.navigator.geolocation.clearWatch(id);
|
||||
if (id != null && geo && geo.clearWatch) {
|
||||
geo.clearWatch(id);
|
||||
}
|
||||
watchIdRef.current = null;
|
||||
setIsTracking(false);
|
||||
}, []);
|
||||
}, [geo]);
|
||||
|
||||
const clear = useCallback(() => {
|
||||
setPath([]);
|
||||
|
||||
@@ -3,9 +3,11 @@ export type { MapHandle } from './MapView';
|
||||
export { Marker, Polyline, PROVIDER_GOOGLE } from 'react-native-maps';
|
||||
export { CameraPresets } from './cameraPresets';
|
||||
export { useLocationTracking } from './hooks/useLocationTracking';
|
||||
export type { TrackingOptions, GeolocationAPI } from './hooks/useLocationTracking';
|
||||
|
||||
// High-level components
|
||||
export { default as PropertyMap } from './components/PropertyMap';
|
||||
export type { PropertyMapProps } from './components/PropertyMap';
|
||||
export { default as PriceMarker } from './components/PriceMarker';
|
||||
export { formatPrice } from './components/PriceMarker';
|
||||
export type { PropertyItem } from './components/PriceMarker';
|
||||
export type { PropertyItem, PriceMarkerProps } from './components/PriceMarker';
|
||||
|
||||
Reference in New Issue
Block a user