From 67bc16e194dbb9d7c50cae38662811d9e879060c Mon Sep 17 00:00:00 2001 From: Mansi Date: Tue, 30 Dec 2025 23:53:08 +0530 Subject: [PATCH] Add map clustering functionality and integrate Google Maps API key --- android/app/src/main/AndroidManifest.xml | 6 + package-lock.json | 50 +++++ package.json | 1 + src/screens/Map.tsx | 260 ++++++++++++++++++----- yarn.lock | 34 ++- 5 files changed, 300 insertions(+), 51 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 1a78b88..7280671 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ + + + + diff --git a/package-lock.json b/package-lock.json index ae7b60b..3b035e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "lynkeduppro-login-sdk": "^0.1.9", "react": "19.1.1", "react-native": "0.82.1", + "react-native-map-clustering": "^4.0.0", "react-native-maps": "^1.26.20", "react-native-permissions": "^5.4.4", "react-native-safe-area-context": "^5.5.2", @@ -2651,6 +2652,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mapbox/geo-viewport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@mapbox/geo-viewport/-/geo-viewport-0.4.1.tgz", + "integrity": "sha512-5g6eM3EOSl7+0p0VY+vHWEYjUlNzof936VKHTi/NuJVABjbYe8D2NAVJ0qt5C9Np4glUlhKFepgAgQ0OEybrjQ==", + "license": "BSD-2-Clause", + "dependencies": { + "@mapbox/sphericalmercator": "~1.1.0" + } + }, "node_modules/@mapbox/polyline": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", @@ -2662,6 +2672,17 @@ "polyline": "bin/polyline.bin.js" } }, + "node_modules/@mapbox/sphericalmercator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz", + "integrity": "sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA==", + "bin": { + "bbox": "bin/bbox.js", + "to4326": "bin/to4326.js", + "to900913": "bin/to900913.js", + "xyz": "bin/xyz.js" + } + }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", @@ -8379,6 +8400,12 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", + "license": "ISC" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -10192,6 +10219,20 @@ } } }, + "node_modules/react-native-map-clustering": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-4.0.0.tgz", + "integrity": "sha512-+YNh4frhZIHQReURxYGHNy9MJ50GYWpW6psoBEjvTG6vb33eYu00GmO8Pu/9VwMB1YL5lOxZ9+sJClJ8Mz1Bxw==", + "license": "MIT", + "dependencies": { + "@mapbox/geo-viewport": "^0.4.1", + "supercluster": "^8.0.0" + }, + "peerDependencies": { + "react-native": "*", + "react-native-maps": "*" + } + }, "node_modules/react-native-maps": { "version": "1.26.20", "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz", @@ -11494,6 +11535,15 @@ ], "license": "MIT" }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "license": "ISC", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", diff --git a/package.json b/package.json index a636ab5..510e724 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "lynkeduppro-login-sdk": "^0.1.9", "react": "19.1.1", "react-native": "0.82.1", + "react-native-map-clustering": "^4.0.0", "react-native-maps": "^1.26.20", "react-native-permissions": "^5.4.4", "react-native-safe-area-context": "^5.5.2", diff --git a/src/screens/Map.tsx b/src/screens/Map.tsx index 94eefbc..9dc2355 100644 --- a/src/screens/Map.tsx +++ b/src/screens/Map.tsx @@ -1,60 +1,102 @@ import React, { useEffect, useRef, useState } from 'react'; -import { View, StyleSheet, PermissionsAndroid, Platform } from 'react-native'; -import MapView, { Marker, MapType, Region } from 'react-native-maps'; +import { + View, + StyleSheet, + PermissionsAndroid, + Platform, + Text, + TouchableOpacity, + Dimensions, +} from 'react-native'; + +import MapView from 'react-native-map-clustering'; +import { Marker, MapType, Region } from 'react-native-maps'; import Geolocation from '@react-native-community/geolocation'; +const { height } = Dimensions.get('window'); + +/* ---------------- CONSTANTS ---------------- */ + const DEFAULT_REGION: Region = { - latitude: 21.1702, - longitude: 72.8311, - latitudeDelta: 0.05, - longitudeDelta: 0.05, + latitude: 47.6062, + longitude: -122.3321, + latitudeDelta: 0.15, + longitudeDelta: 0.15, }; -const Map: React.FC = () => { - const mapRef = useRef(null); - const [mapType, setMapType] = useState('standard'); - const [region, setRegion] = useState(DEFAULT_REGION); +/* ---------------- MOCK DATA ---------------- */ + +const generateProperties = (count = 2000) => + Array.from({ length: count }).map((_, i) => ({ + id: `${i}`, + lat: 47.5 + Math.random() * 0.25, + lng: -122.45 + Math.random() * 0.3, + price: Math.floor(300000 + Math.random() * 4000000), + beds: Math.floor(1 + Math.random() * 5), + baths: Math.floor(1 + Math.random() * 4), + })); + +const properties = generateProperties(2000); + +/* ---------------- HELPERS ---------------- */ + +const formatPrice = (value: number) => { + if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; + if (value >= 1000) return `${Math.round(value / 1000)}K`; + return value.toString(); +}; + +/* ---------------- PRICE MARKER (FIXED) ---------------- */ + +const PriceMarker = ({ item, onPress }: any) => { + const [tracks, setTracks] = useState(true); useEffect(() => { - requestPermissionAndLocate(); + const t = setTimeout(() => setTracks(false), 400); + return () => clearTimeout(t); }, []); - const requestPermissionAndLocate = async () => { + return ( + + + {formatPrice(item.price)} + + + ); +}; + +/* ---------------- MAIN SCREEN ---------------- */ + +const MapScreen: React.FC = () => { + const mapRef = useRef(null); + + const [mapType, setMapType] = useState('standard'); + const [selectedItem, setSelectedItem] = useState(null); + + /* ---- current location ---- */ + useEffect(() => { + locateUser(); + }, []); + + const locateUser = async () => { if (Platform.OS === 'android') { const granted = await PermissionsAndroid.request( PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION ); if (granted !== PermissionsAndroid.RESULTS.GRANTED) return; } - locateUser(); - }; - const locateUser = () => { - Geolocation.getCurrentPosition( - pos => { - const { latitude, longitude } = pos.coords; - - const currentRegion: Region = { - latitude, - longitude, - latitudeDelta: 0.01, - longitudeDelta: 0.01, - }; - - setRegion(currentRegion); - - // ✅ Redirect to current location - mapRef.current?.animateToRegion(currentRegion, 1000); - }, - err => console.log(err), - { enableHighAccuracy: true } - ); - }; - - const onRegionChangeComplete = (r: Region) => { - setRegion(r); - - setMapType(r.latitudeDelta < 0.008 ? 'satellite' : 'standard'); + Geolocation.getCurrentPosition(pos => { + const { latitude, longitude } = pos.coords; + mapRef.current?.animateToRegion( + { latitude, longitude, latitudeDelta: 0.05, longitudeDelta: 0.05 }, + 1000 + ); + }); }; return ( @@ -63,24 +105,142 @@ const Map: React.FC = () => { ref={mapRef} style={StyleSheet.absoluteFillObject} initialRegion={DEFAULT_REGION} - mapType={mapType} showsUserLocation - onRegionChangeComplete={onRegionChangeComplete} + animationEnabled + clusterPressMaxZoom={16} + spiralEnabled={false} + mapType={mapType} + onPress={() => setSelectedItem(null)} + onRegionChangeComplete={r => + setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard') + } + + /* ---- CLUSTER BUBBLE ---- */ + renderCluster={(cluster, onPress) => { + const { geometry, properties } = cluster; + + const avgPrice = + properties.point_count * 900000 / properties.point_count; + + return ( + + + + {formatPrice(avgPrice)} + + + + ); + }} > - + {properties.map(item => ( + setSelectedItem(item)} + /> + ))} + + {/* -------- BOTTOM POPUP -------- */} + {selectedItem && ( + + setSelectedItem(null)} + > + + + + + ${formatPrice(selectedItem.price)} + + + + {selectedItem.beds} Beds • {selectedItem.baths} Baths + + + + View Details + + + )} ); }; +/* ---------------- STYLES ---------------- */ + const styles = StyleSheet.create({ container: { flex: 1 }, + + pin: { + backgroundColor: '#8B0000', + paddingHorizontal: 10, + paddingVertical: 6, + borderRadius: 18, + }, + pinText: { + color: '#fff', + fontWeight: '600', + fontSize: 12, + }, + + clusterBubble: { + backgroundColor: '#8B0000', + paddingHorizontal: 14, + paddingVertical: 8, + borderRadius: 24, + borderWidth: 2, + borderColor: '#fff', + }, + clusterText: { + color: '#fff', + fontWeight: 'bold', + fontSize: 13, + }, + + popup: { + position: 'absolute', + bottom: 20, + left: 16, + right: 16, + backgroundColor: '#fff', + borderRadius: 16, + padding: 16, + elevation: 10, + }, + close: { + position: 'absolute', + right: 12, + top: 12, + }, + price: { + fontSize: 22, + fontWeight: 'bold', + }, + details: { + marginVertical: 6, + color: '#666', + }, + btn: { + marginTop: 12, + backgroundColor: '#8B0000', + padding: 12, + borderRadius: 10, + alignItems: 'center', + }, + btnText: { + color: '#fff', + fontWeight: '600', + }, }); -export default Map; +export default MapScreen; diff --git a/yarn.lock b/yarn.lock index a151d4e..146af32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1344,6 +1344,13 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@mapbox/geo-viewport@^0.4.1": + version "0.4.1" + resolved "https://registry.npmjs.org/@mapbox/geo-viewport/-/geo-viewport-0.4.1.tgz" + integrity sha512-5g6eM3EOSl7+0p0VY+vHWEYjUlNzof936VKHTi/NuJVABjbYe8D2NAVJ0qt5C9Np4glUlhKFepgAgQ0OEybrjQ== + dependencies: + "@mapbox/sphericalmercator" "~1.1.0" + "@mapbox/polyline@^1.2.1": version "1.2.1" resolved "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz" @@ -1351,6 +1358,11 @@ dependencies: meow "^9.0.0" +"@mapbox/sphericalmercator@~1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz" + integrity sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA== + "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": version "5.1.1-v1" resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" @@ -4579,6 +4591,11 @@ jsonfile@^4.0.0: object.assign "^4.1.4" object.values "^1.1.6" +kdbush@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz" + integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA== + keyv@^4.5.3: version "4.5.4" resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" @@ -5520,7 +5537,15 @@ react-is@^19.1.1: resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz" integrity sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA== -react-native-maps@^1.26.20: +react-native-map-clustering@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-4.0.0.tgz" + integrity sha512-+YNh4frhZIHQReURxYGHNy9MJ50GYWpW6psoBEjvTG6vb33eYu00GmO8Pu/9VwMB1YL5lOxZ9+sJClJ8Mz1Bxw== + dependencies: + "@mapbox/geo-viewport" "^0.4.1" + supercluster "^8.0.0" + +react-native-maps@*, react-native-maps@^1.26.20: version "1.26.20" resolved "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz" integrity sha512-kWibDz6wLLQ0685gOEFz5jdzm4miD7PMeVdtZV7ilgftDcusC2iy7SueBJpHF0LKCoOSa1BEUiKqpx1dBMSNpA== @@ -6246,6 +6271,13 @@ strnum@^1.1.1: resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz" integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== +supercluster@^8.0.0: + version "8.0.1" + resolved "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz" + integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ== + dependencies: + kdbush "^4.0.2" + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"