Enhance documentation and type definitions for location tracking and property components

This commit is contained in:
2026-02-04 23:36:51 +05:30
parent d1e383baf5
commit 986266348a
5 changed files with 163 additions and 98 deletions

View File

@@ -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`).
---

View File

@@ -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);

View File

@@ -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 >
);
};

View File

@@ -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([]);

View File

@@ -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';