Skip to content

Postwires

Primary Menu
  • Home
  • CSS
  • Documentation
  • HTML
  • Java
  • Javascript
  • Questions
  • React js
  • React Native
Watch Video
  • Home
  • Uncategorized
  • global map system
  • Uncategorized

global map system

john October 31, 2025
import 'react-native-get-random-values';  // ✅ must be first
import React from 'react';
import { StyleSheet, StatusBar } from 'react-native';
import MapScreen from './components/MapScreen';
import {SafeAreaView} from 'react-native-safe-area-context'

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <StatusBar barStyle="dark-content" />
      <MapScreen />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#f2f6fb' },
});

//package.json
{
  "name": "test",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "lint": "eslint .",
    "start": "react-native start",
    "test": "jest"
  },
  "dependencies": {
    "@react-native-community/geolocation": "^3.4.0",
    "@react-native/new-app-screen": "0.82.1",
    "react": "19.1.1",
    "react-native": "0.82.1",
    "react-native-get-random-values": "^2.0.0",
    "react-native-maps": "^1.26.18",
    "react-native-safe-area-context": "^5.5.2",
    "react-native-vector-icons": "^10.3.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@babel/preset-env": "^7.25.3",
    "@babel/runtime": "^7.25.0",
    "@react-native-community/cli": "20.0.0",
    "@react-native-community/cli-platform-android": "20.0.0",
    "@react-native-community/cli-platform-ios": "20.0.0",
    "@react-native/babel-preset": "0.82.1",
    "@react-native/eslint-config": "0.82.1",
    "@react-native/metro-config": "0.82.1",
    "@react-native/typescript-config": "0.82.1",
    "@types/jest": "^29.5.13",
    "@types/react": "^19.1.1",
    "@types/react-test-renderer": "^19.1.0",
    "eslint": "^8.19.0",
    "jest": "^29.6.3",
    "prettier": "2.8.8",
    "react-test-renderer": "19.1.1",
    "typescript": "^5.8.3"
  },
  "engines": {
    "node": ">=20"
  }
}

//// components/MapScreen.js
import React, { useEffect, useRef, useState } from 'react';
import {
  View,
  StyleSheet,
  Dimensions,
  Animated,
  Platform,
  PermissionsAndroid,
  TouchableOpacity,
  Text,
  Alert,
  TextInput,
  ActivityIndicator,
  Keyboard,
} from 'react-native';
import MapView, { Marker, Circle, PROVIDER_GOOGLE, Polyline } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation';
import Icon from 'react-native-vector-icons/Ionicons';
import RouteCard from './RouteCard';
import FabGroup from './FabGroup';
import SearchBar from './SearchBar';

const { width, height } = Dimensions.get('window');

// ---------- Helpers ----------
function haversineDistance(a, b) {
  const R = 6371000;
  const toRad = (deg) => (deg * Math.PI) / 180;
  const dLat = toRad(b.latitude - a.latitude);
  const dLon = toRad(b.longitude - a.longitude);
  const lat1 = toRad(a.latitude);
  const lat2 = toRad(b.latitude);
  const sinHalfLat = Math.sin(dLat / 2);
  const sinHalfLon = Math.sin(dLon / 2);
  const aa = sinHalfLat * sinHalfLat + Math.cos(lat1) * Math.cos(lat2) * sinHalfLon * sinHalfLon;
  const c = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa));
  return R * c;
}

function decodePolyline(encoded) {
  let points = [];
  let index = 0, len = encoded.length;
  let lat = 0, lng = 0;
  while (index < len) {
    let b, shift = 0, result = 0;
    do {
      b = encoded.charCodeAt(index++) - 63;
      result |= (b & 0x1f) << shift;
      shift += 5;
    } while (b >= 0x20);
    const dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
    lat += dlat;
    shift = 0;
    result = 0;
    do {
      b = encoded.charCodeAt(index++) - 63;
      result |= (b & 0x1f) << shift;
      shift += 5;
    } while (b >= 0x20);
    const dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
    lng += dlng;
    points.push({ latitude: lat / 1e5, longitude: lng / 1e5 });
  }
  return points;
}

function sumDistancesAlongPath(points, startIndex = 0) {
  let sum = 0;
  for (let i = startIndex; i < points.length - 1; i++) {
    sum += haversineDistance(points[i], points[i + 1]);
  }
  return sum;
}

function findNearestPointIndex(points, loc) {
  let minD = Infinity;
  let idx = 0;
  for (let i = 0; i < points.length; i++) {
    const d = haversineDistance(points[i], loc);
    if (d < minD) {
      minD = d;
      idx = i;
    }
  }
  return idx;
}

// ---------- Component ----------
export default function MapScreen() {
  const [coords, setCoords] = useState(null);
  const [accuracy, setAccuracy] = useState(null);
  const [following, setFollowing] = useState(true);

  // start / dest points
  const [startPoint, setStartPoint] = useState(null);
  const [destination, setDestination] = useState(null);

  // route state (polyline + info)
  const [routeCoords, setRouteCoords] = useState([]);
  const [routeInfo, setRouteInfo] = useState(null);
  const [remainingMeters, setRemainingMeters] = useState(null);

  // panel + inputs
  const [panelOpen, setPanelOpen] = useState(false);
  const [startQuery, setStartQuery] = useState('');
  const [destQuery, setDestQuery] = useState('');
  const [loadingStart, setLoadingStart] = useState(false);
  const [loadingDest, setLoadingDest] = useState(false);

  // mode + signals
  const [mode, setMode] = useState('driving'); // driving | walking
  const [clearSignalCounter, setClearSignalCounter] = useState(0);

  // indicates whether user pressed Go to start navigation
  const [routeStarted, setRouteStarted] = useState(false);

  const mapRef = useRef(null);
  const markerAnim = useRef(new Animated.Value(0)).current;
  const watchIdRef = useRef(null);

  const GOOGLE_API_KEY = 'AIzaSyA1o2H3l50BEe0J3HtKMU7kVpyhb40j5oE'; // move to env in production

  // watch device location
  useEffect(() => {
    async function requestPermission() {
      if (Platform.OS === 'android') {
        try {
          const granted = await PermissionsAndroid.request(
            PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
            {
              title: 'Location permission',
              message: 'App needs access to your location for navigation',
              buttonNeutral: 'Ask Me Later',
              buttonNegative: 'Cancel',
              buttonPositive: 'OK',
            }
          );
          if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
            Alert.alert('Location permission denied', 'App will not be able to get your location.');
          }
        } catch (err) {
          console.warn(err);
        }
      }
    }
    requestPermission();

    watchIdRef.current = Geolocation.watchPosition(
      (pos) => {
        const c = pos.coords;
        const newCoords = { latitude: c.latitude, longitude: c.longitude, heading: c.heading || 0 };
        setCoords(newCoords);
        setAccuracy(c.accuracy);

        Animated.sequence([
          Animated.timing(markerAnim, { toValue: 1, duration: 150, useNativeDriver: true }),
          Animated.timing(markerAnim, { toValue: 0, duration: 150, useNativeDriver: true })
        ]).start();

        if (following && mapRef.current) {
          try {
            mapRef.current.animateToRegion(
              { latitude: c.latitude, longitude: c.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 },
              400
            );
          } catch (e) {}
        }

        if (routeCoords && routeCoords.length > 0) {
          const nearestIdx = findNearestPointIndex(routeCoords, newCoords);
          const remainAlongPath = sumDistancesAlongPath(routeCoords, nearestIdx);
          setRemainingMeters(Math.round(remainAlongPath));
        }
      },
      (err) => { console.warn(err); },
      { enableHighAccuracy: true, distanceFilter: 1, interval: 2000, fastestInterval: 1000 }
    );

    return () => {
      if (watchIdRef.current != null) Geolocation.clearWatch(watchIdRef.current);
    };
  }, [following, routeCoords]);

  const recenter = () => {
    if (coords && mapRef.current) {
      mapRef.current.animateToRegion(
        { latitude: coords.latitude, longitude: coords.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 },
        400
      );
      setFollowing(true);
    }
  };

  // Nominatim geocode helper (single-result)
  async function geocodeQuery(q) {
    if (!q || !q.trim()) return null;
    const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q.trim())}&limit=1`;
    try {
      const res = await fetch(url, {
        headers: {
          'User-Agent': 'my-geolocation-app/1.0 (contact: myemail@example.com)',
          'Accept-Language': 'en',
        },
      });
      const text = await res.text();
      const data = JSON.parse(text || '[]');
      if (data && data.length > 0) {
        const { lat, lon, display_name } = data[0];
        const latN = parseFloat(lat), lonN = parseFloat(lon);
        if (!isNaN(latN) && !isNaN(lonN)) {
          return { latitude: latN, longitude: lonN, label: display_name };
        }
      }
      return null;
    } catch (e) {
      console.warn('geocode error', e);
      return null;
    }
  }

  // panel search handlers (start / dest fields)
  const onSearchStart = async () => {
    if (!startQuery.trim()) return;
    Keyboard.dismiss();
    setLoadingStart(true);
    const res = await geocodeQuery(startQuery);
    setLoadingStart(false);
    if (res) {
      setStartPoint(res);
      if (mapRef.current) mapRef.current.animateToRegion({ latitude: res.latitude, longitude: res.longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 400);
      // if dest already exists and routeStarted true or both points present -> auto-fetch if user expects immediate route
      if (destination && routeStarted) await fetchRoute(res, destination, mode);
    } else {
      Alert.alert('No results', 'Could not find a location for the Start query.');
    }
  };

  const onSearchDest = async () => {
    if (!destQuery.trim()) return;
    Keyboard.dismiss();
    setLoadingDest(true);
    const res = await geocodeQuery(destQuery);
    setLoadingDest(false);
    if (res) {
      setDestination(res);
      if (mapRef.current) mapRef.current.animateToRegion({ latitude: res.latitude, longitude: res.longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 400);
      // if user already pressed Go (routeStarted) and both points available -> fetch
      if (startPoint && routeStarted) {
        await fetchRoute(startPoint, res, mode);
      } else if (!startPoint && coords && routeStarted) {
        // origin is current location
        await fetchRoute(coords, res, mode);
      }
    } else {
      Alert.alert('No results', 'Could not find a location for the Destination query.');
    }
  };

  // fetch route via Google Directions API
  const fetchRoute = async (origin, dest, travelMode = 'driving') => {
    try {
      if (!origin || !dest) return;
      const originParam = `${origin.latitude},${origin.longitude}`;
      const destParam = `${dest.latitude},${dest.longitude}`;
      const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${originParam}&destination=${destParam}&mode=${travelMode}&key=${GOOGLE_API_KEY}`;
      const res = await fetch(url);
      const json = await res.json();
      if (json.status !== 'OK') {
        console.warn('Directions error', json);
        Alert.alert('Route error', `Directions API: ${json.status}`);
        setRouteCoords([]);
        setRouteInfo(null);
        return;
      }
      const r = json.routes[0];
      const points = decodePolyline(r.overview_polyline.points);
      setRouteCoords(points);

      let distanceMeters = 0, durationSec = 0;
      if (r.legs && r.legs.length > 0) {
        r.legs.forEach((leg) => {
          if (leg.distance && leg.distance.value) distanceMeters += leg.distance.value;
          if (leg.duration && leg.duration.value) durationSec += leg.duration.value;
        });
      }

      setRouteInfo({
        distanceMeters,
        durationSec,
        distanceText: r.legs && r.legs.length > 0 ? r.legs[0].distance.text : `${(distanceMeters/1000).toFixed(1)} km`,
        durationText: r.legs && r.legs.length > 0 ? r.legs[0].duration.text : `${Math.round(durationSec/60)} min`
      });

      const nearestIdx = findNearestPointIndex(points, origin);
      const remainAlongPath = sumDistancesAlongPath(points, nearestIdx);
      setRemainingMeters(Math.round(remainAlongPath));
    } catch (e) {
      console.warn('fetchRoute error', e);
      Alert.alert('Route error', e.message || 'Could not fetch route');
      setRouteCoords([]);
      setRouteInfo(null);
    }
  };

  // Go pressed (inside RouteCard)
  const onGoPressed = async () => {
    if (!destination) {
      Alert.alert('No destination', 'Please set a destination first.');
      return;
    }
    const origin = startPoint || coords;
    if (!origin) {
      Alert.alert('No start', 'Current location not available yet.');
      return;
    }
    // mark that route has started (this will reveal mode icons etc.)
    setRouteStarted(true);
    await fetchRoute(origin, destination, mode);
  };

  // clear everything
  const clearMap = () => {
    setStartPoint(null);
    setDestination(null);
    setRouteCoords([]);
    setRouteInfo(null);
    setRemainingMeters(null);
    setPanelOpen(false);
    setStartQuery('');
    setDestQuery('');
    setMode('driving');
    setFollowing(true);
    setRouteStarted(false);
    setClearSignalCounter((c) => c + 1);
    if (coords && mapRef.current) {
      mapRef.current.animateToRegion({ latitude: coords.latitude, longitude: coords.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 }, 300);
    }
  };

  // mode change (only visible after routeStarted)
  const changeMode = async (newMode) => {
    setMode(newMode);
    if (!routeStarted) return;
    // if route already started and both points exist, refetch
    const origin = startPoint || coords;
    if (origin && destination) {
      await fetchRoute(origin, destination, newMode);
    }
  };

  // compute straight-line distance to show before route is fetched
  const computeStraightDistanceText = () => {
    const origin = startPoint || coords;
    if (!origin || !destination) return '-';
    const meters = Math.round(haversineDistance(origin, destination));
    if (meters >= 1000) return `${(meters / 1000).toFixed(1)} km`;
    return `${meters} m`;
  };

  const formatDistance = (m) => {
    if (m == null) return '-';
    if (m >= 1000) return `${(m / 1000).toFixed(1)} km`;
    return `${Math.round(m)} m`;
  };
  const formatDuration = (sec) => {
    if (!sec && sec !== 0) return '-';
    const mins = Math.round(sec / 60);
    if (mins >= 60) {
      const h = Math.floor(mins / 60);
      const rem = mins % 60;
      return `${h}h ${rem}m`;
    }
    return `${mins} min`;
  };

  // prepare props for RouteCard
  const routeCardProps = {
    destinationLabel: destination ? destination.label : null,
    // before route started show straight-line distance; after started show actual route distance
    totalDistanceText: routeStarted && routeInfo ? routeInfo.distanceText : computeStraightDistanceText(),
    totalDurationText: routeStarted && routeInfo ? routeInfo.durationText : '-',
    remainingDistanceText: formatDistance(remainingMeters),
    mode,
    showDetails: routeStarted && !!routeInfo, // show full details only after route started AND routeInfo present
    onGoPress: onGoPressed,
  };

  return (
    <View style={styles.container}>
      {/* keep original SearchBar at top */}
      <SearchBar onPlaceSelected={({ latitude, longitude, label }) => {
        const dest = { latitude, longitude, label: label || 'Destination' };
        setDestination(dest);
        setRouteCoords([]);
        setRouteInfo(null);
        setRemainingMeters(null);
        setFollowing(false);
        // center on destination
        if (mapRef.current) mapRef.current.animateToRegion({ latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 500);
        // only auto-route if a custom startPoint exists and routeStarted is true
        if (startPoint && routeStarted) {
          fetchRoute(startPoint, dest, mode);
        }
      }} clearSignal={clearSignalCounter} />

      <MapView
        ref={mapRef}
        style={styles.map}
        provider={Platform.OS === 'android' ? PROVIDER_GOOGLE : null}
        showsUserLocation={false}
        showsMyLocationButton={false}
        loadingEnabled={true}
        initialRegion={{ latitude: 20.5937, longitude: 78.9629, latitudeDelta: 20, longitudeDelta: 20 }}
      >
        {coords && (
          <>
            <Marker coordinate={{ latitude: coords.latitude, longitude: coords.longitude }}>
              <Animated.View style={[styles.blueDot, { transform: [{ scale: markerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 1.35] }) }] }]} />
            </Marker>
            <Circle center={{ latitude: coords.latitude, longitude: coords.longitude }} radius={accuracy || 20}
              strokeColor={'rgba(0,122,255,0.6)'} fillColor={'rgba(0,122,255,0.15)'} />
          </>
        )}

        {startPoint && (
          <Marker coordinate={{ latitude: startPoint.latitude, longitude: startPoint.longitude }} pinColor="green" title={startPoint.label} />
        )}

        {destination && (
          <Marker coordinate={{ latitude: destination.latitude, longitude: destination.longitude }} pinColor="red" title={destination.label} />
        )}

        {routeCoords && routeCoords.length > 0 && (
          <Polyline
            key={mode}
            coordinates={routeCoords}
            strokeWidth={5}
            strokeColor={'rgba(0,122,255,0.9)'}
            lineCap="round"
            lineJoin="round"
            lineDashPattern={mode === 'walking' ? [10, 6] : undefined}
          />
        )}
      </MapView>

      {/* Arrow icon - moved to RIGHT side above other bottom icons */}
      <TouchableOpacity style={styles.arrowIconRight} onPress={() => setPanelOpen(s => !s)}>
        <Icon name="swap-horizontal-outline" size={26} color="#111" />
      </TouchableOpacity>

      {/* Mode icons - visible only after Go (routeStarted) */}
      {routeStarted && (
        <View style={styles.modeRow}>
          <TouchableOpacity style={[styles.iconBtn, mode === 'driving' ? styles.iconActive : null]} onPress={() => changeMode('driving')}>
            <Icon name="car-outline" size={20} color={mode === 'driving' ? '#fff' : '#007AFF'} />
          </TouchableOpacity>
          <TouchableOpacity style={[styles.iconBtn, mode === 'walking' ? styles.iconActive : null]} onPress={() => changeMode('walking')}>
            <Icon name="walk-outline" size={20} color={mode === 'walking' ? '#fff' : '#007AFF'} />
          </TouchableOpacity>
        </View>
      )}

      {/* Top panel with Start / Destination inputs (opens from right arrow) */}
      {panelOpen && (
        <View style={styles.panel}>
          <View style={styles.inputRow}>
            <TextInput
              style={styles.input}
              placeholder="Start (type address or place)..."
              value={startQuery}
              onChangeText={setStartQuery}
              returnKeyType="search"
              onSubmitEditing={onSearchStart}
            />
            <TouchableOpacity style={styles.iconBtnSmall} onPress={onSearchStart}>
              {loadingStart ? <ActivityIndicator size="small" /> : <Icon name="search" size={18} color="#007AFF" />}
            </TouchableOpacity>
            <TouchableOpacity style={styles.iconBtnSmall} onPress={() => {
              if (coords) {
                setStartPoint({ latitude: coords.latitude, longitude: coords.longitude, label: 'Current location' });
                setStartQuery('Current location');
                Keyboard.dismiss();
                if (destination && routeStarted) fetchRoute({ latitude: coords.latitude, longitude: coords.longitude }, destination, mode);
              } else {
                Alert.alert('No current location', 'Waiting for device location.');
              }
            }}>
              <Icon name="locate-outline" size={20} color="#007AFF" />
            </TouchableOpacity>
          </View>

          <View style={styles.inputRow}>
            <TextInput
              style={styles.input}
              placeholder="Destination (type address or place)..."
              value={destQuery}
              onChangeText={setDestQuery}
              returnKeyType="search"
              onSubmitEditing={onSearchDest}
            />
            <TouchableOpacity style={styles.iconBtnSmall} onPress={onSearchDest}>
              {loadingDest ? <ActivityIndicator size="small" /> : <Icon name="search" size={18} color="#007AFF" />}
            </TouchableOpacity>
          </View>
        </View>
      )}

      {/* RouteCard visible when destination exists (shows straight distance until Go pressed) */}
      {destination && (
        <View style={styles.routeCardWrap}>
          <RouteCard
            destinationLabel={routeCardProps.destinationLabel}
            totalDistanceText={routeCardProps.totalDistanceText}
            totalDurationText={routeCardProps.totalDurationText}
            remainingDistanceText={routeCardProps.remainingDistanceText}
            mode={routeCardProps.mode}
            showDetails={routeCardProps.showDetails}
            onGoPress={routeCardProps.onGoPress}
          />
        </View>
      )}

      {/* Clear button at bottom (red) */}
      <TouchableOpacity style={styles.clearBottomBtn} onPress={clearMap}>
        <Text style={styles.clearBottomText}>Clear</Text>
      </TouchableOpacity>

      <FabGroup following={following} onToggleFollow={() => setFollowing((v) => !v)} onRecenter={recenter} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { width, height },

  blueDot: {
    width: 18, height: 18, borderRadius: 9, backgroundColor: '#007AFF', borderWidth: 3, borderColor: 'white',
    shadowColor: '#007AFF', shadowOpacity: 0.6, shadowRadius: 6, elevation: 4
  },

  arrowIconRight: {
    position: 'absolute',
    right: 20,
    bottom: 280, // above other bottom icons
    zIndex: 95,
    backgroundColor: 'orange',
    padding: 8,
    borderRadius: 15,
    shadowColor: '#000',
    shadowOpacity: 0.12,
    shadowRadius: 4,
    elevation: 6
  },

  modeRow: {
    position: 'absolute', right: 16, bottom: 500, zIndex: 90, flexDirection: 'column', alignItems: 'center',
  },
  iconBtn: {
    backgroundColor: 'white', padding: 10, borderRadius: 26, marginBottom: 8, shadowColor: '#000', shadowOpacity: 0.12,
    shadowRadius: 4, elevation: 6, minWidth: 46, alignItems: 'center', justifyContent: 'center'
  },
  iconActive: { backgroundColor: '#007AFF' },

  panel: {
    position: 'absolute', left: 12, right: 12, top: 10, zIndex: 95, backgroundColor: 'white', borderRadius: 10,
    padding: 8, elevation: 8, shadowColor: '#000', shadowOpacity: 0.12, shadowRadius: 6
  },
  inputRow: { flexDirection: 'row', alignItems: 'center', marginVertical: 6 },
  input: { flex: 1, height: 44, paddingHorizontal: 10, borderRadius: 6, borderWidth: 1, borderColor: '#eee' },
  iconBtnSmall: { paddingHorizontal: 8 },

  goFab: {
    position: 'absolute', right: 16, bottom: 140, zIndex: 80, width: 64, height: 64, borderRadius: 32, backgroundColor: '#007AFF',
    alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.18, shadowRadius: 6
  },

  routeCardWrap: {
    position: 'absolute', left: 16, right: 16, bottom:0, zIndex: 80,
  },

  clearBottomBtn: {
    position: 'absolute',
    right: 16,
    bottom: 10,
    zIndex: 95,
    backgroundColor: '#D9534F',
    paddingHorizontal: 16,
    paddingVertical: 10,
    borderRadius: 999,
    elevation: 8,
    shadowColor: '#000',
    shadowOpacity: 0.2,
    shadowRadius: 6,
  },
  clearBottomText: { color: '#fff', fontWeight: '700' },
});

//FabGroup.js
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';

export default function FabGroup({ following, onToggleFollow, onRecenter }) {
  return (
    <View style={styles.container} pointerEvents="box-none">
      <TouchableOpacity style={styles.fab} onPress={onRecenter}>
        <Text style={styles.icon}>⤴︎</Text>
      </TouchableOpacity>

      <TouchableOpacity style={[styles.fab, following ? styles.active : null]} onPress={onToggleFollow}>
        <Text style={styles.icon}>{following ? '🔒' : '🔓'}</Text>
      </TouchableOpacity>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { position: 'absolute', right: 16, bottom: 20, zIndex: 30, alignItems: 'center' },
  fab: {
    bottom:300,
    width: 52,
    height: 52,
    borderRadius: 26,
    backgroundColor: 'white',
    alignItems: 'center',
    justifyContent: 'center',
    marginBottom: 12,
    shadowColor: '#000',
    shadowOpacity: 0.16,
    shadowRadius: 6,
    elevation: 8
  },
  active: { backgroundColor: '#007AFF' },
  icon: { fontSize: 20 }
});

//

// components/RouteCard.js
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';

export default function RouteCard({
  destinationLabel,
  totalDistanceText,
  totalDurationText,
  remainingDistanceText,
  mode,
  showDetails,    // when true show full details; when false show only destination + straight distance
  onGoPress,      // callback when Go is pressed
}) {
  return (
    <View style={styles.card}>
      <View style={styles.headerRow}>
        <Text style={styles.title} numberOfLines={1}>
          {destinationLabel || 'Destination'}
        </Text>

        {/* Go button sits inside the card */}
        <TouchableOpacity style={styles.goBtn} onPress={onGoPress}>
          <Icon name="navigate-circle" size={30} color="#fff" />
        </TouchableOpacity>
      </View>

      {/* If showDetails is false, show only the straight-line distance (totalDistanceText) */}
      {!showDetails ? (
        <Text style={styles.row}>
          <Text style={styles.label}>Distance:</Text> {totalDistanceText || '-'}
        </Text>
      ) : (
        // Full details
        <>
          <Text style={styles.row}>
            <Text style={styles.label}>Mode:</Text> {mode}
          </Text>
          <Text style={styles.row}>
            <Text style={styles.label}>Route:</Text> {totalDistanceText} • {totalDurationText}
          </Text>
          <Text style={styles.row}>
            <Text style={styles.label}>Remaining:</Text> {remainingDistanceText}
          </Text>
        </>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    position: 'absolute',
    left: 12,
    right: 12,
    bottom: 80,
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 12,
    minWidth: 200,
    shadowColor: '#000',
    shadowOpacity: 0.12,
    shadowRadius: 8,
    elevation: 6,
    flexDirection: 'column'
  },
  headerRow: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'space-between'
  },
  title: { fontWeight: '700', fontSize: 14, marginBottom: 6, flex: 1, marginRight: 8 },
  row: { fontSize: 13, color: '#333', marginTop: 4 },
  label: { fontWeight: '600', color: '#444' },

  goBtn: {
    width: 44,
    height: 44,
    borderRadius: 22,
    backgroundColor: '#007AFF',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

//

// components/SearchBar.js
import React, { useState, useEffect } from 'react';
import {
  View,
  TextInput,
  StyleSheet,
  TouchableOpacity,
  ActivityIndicator,
  Alert,
  Keyboard,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';

export default function SearchBar({ onPlaceSelected, clearSignal }) {
  const [query, setQuery] = useState('');
  const [loading, setLoading] = useState(false);

  // 🔁 When clearSignal changes, reset input box
  useEffect(() => {
    if (clearSignal) {
      setQuery('');
      Keyboard.dismiss();
    }
  }, [clearSignal]);

  const handleSearch = async () => {
    if (!query.trim()) return;
    Keyboard.dismiss(); // ✅ close keyboard when searching

    setLoading(true);
    try {
      const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(
        query.trim()
      )}&limit=5`;

      const res = await fetch(url, {
        headers: {
          'User-Agent': 'my-geolocation-app/1.0 (contact: myemail@example.com)',
          'Accept-Language': 'en',
        },
      });

      const text = await res.text();
      let data = JSON.parse(text || '[]');

      if (data && data.length > 0) {
        // Use the first result
        const { lat, lon, display_name } = data[0];
        const latitude = parseFloat(lat);
        const longitude = parseFloat(lon);
        if (!isNaN(latitude) && !isNaN(longitude)) {
          onPlaceSelected({ latitude, longitude, label: display_name });
        } else {
          Alert.alert('Invalid location returned.');
        }
      } else {
        Alert.alert('No results', 'Could not find any location for your search.');
      }
    } catch (e) {
      console.warn('Search error:', e);
      Alert.alert('Search error', e.message || 'Unknown error');
    } finally {
      setLoading(false);
    }
  };

  return (
    <View style={styles.container}>
      <View style={styles.inputContainer}>
        <TextInput
          placeholder="Enter destination (city, place...)"
          value={query}
          onChangeText={setQuery}
          style={styles.input}
          returnKeyType="search"
          onSubmitEditing={handleSearch}
          blurOnSubmit={true} // ✅ auto dismiss on keyboard search press
        />
        <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
          {loading ? (
            <ActivityIndicator size="small" color="#007AFF" />
          ) : (
            <Icon name="search" size={22} color="#007AFF" />
          )}
        </TouchableOpacity>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { position: 'absolute', top: 10, left: 12, right: 12, zIndex: 40 },
  inputContainer: {
    flexDirection: 'row',
    alignItems: 'center',
    backgroundColor: 'white',
    borderRadius: 10,
    elevation: 6,
    shadowColor: '#000',
    shadowOpacity: 0.1,
    shadowRadius: 4,
  },
  input: {
    flex: 1,
    height: 44,
    paddingHorizontal: 12,
    color: '#222',
    fontSize: 16,
  },
  iconButton: {
    paddingHorizontal: 12,
  },
});

//StatusCard.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';

export default function StatusCard({ coords, accuracy }) {
  return (
    <View style={styles.card} pointerEvents="none">
      <Text style={styles.row}>
        <Text style={styles.label}>Lat:</Text> {coords ? coords.latitude.toFixed(6) : '-'}
      </Text>
      <Text style={styles.row}>
        <Text style={styles.label}>Lng:</Text> {coords ? coords.longitude.toFixed(6) : '-'}
      </Text>
      <Text style={styles.row}>
        <Text style={styles.label}>Accuracy:</Text> {accuracy ? `${Math.round(accuracy)} m` : '-'}
      </Text>
    </View>
  );
}

const styles = StyleSheet.create({
  card: {
    position: 'absolute',
    left: 12,
    bottom: 160,
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 10,
    minWidth: 180,
    shadowColor: '#000',
    shadowOpacity: 0.12,
    shadowRadius: 8,
    elevation: 6
  },
  row: { fontSize: 12, color: '#333' },
  label: { fontWeight: '600', color: '#444' }
});
//
//app/build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"

//mefest


<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <uses-permission android:name="android.permission.INTERNET" />

    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />


    <application
    
      android:name=".MainApplication"
      android:label="@string/app_name"
      android:icon="@mipmap/ic_launcher"
      android:roundIcon="@mipmap/ic_launcher_round"
      android:allowBackup="false"
      android:theme="@style/AppTheme"
      android:usesCleartextTraffic="${usesCleartextTraffic}"
      android:supportsRtl="true">
      <meta-data
  android:name="com.google.android.geo.API_KEY"
  android:value="AIzaSyA1o2H3l50BEe0J3HtKMU7kVpyhb40j5oE" />
      <activity
        android:name=".MainActivity"
        android:label="@string/app_name"
        android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
        android:launchMode="singleTask"
        android:windowSoftInputMode="adjustResize"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
      </activity>
    </application>
</manifest>


About the Author

john

Administrator

Visit Website View All Posts

Continue Reading

Previous: todoapp build with latest stack
Next: jdagj

Related Stories

  • Uncategorized
  • React Native

How To Fix Gradle Error In React Native

john November 2, 2025
  • Uncategorized

jdagj

john October 31, 2025
  • Uncategorized

todoapp build with latest stack

john October 31, 2025

Recent Posts

  • How To Fix Gradle Error In React Native
  • jdagj
  • global map system
  • todoapp build with latest stack
  • weather app build with latest technology

Categories

  • CSS
  • Documentation
  • HTML
  • Java
  • Javascript
  • Questions
  • React js
  • React Native
  • Uncategorized

You may have missed

  • Uncategorized
  • React Native

How To Fix Gradle Error In React Native

john November 2, 2025
  • Uncategorized

jdagj

john October 31, 2025
  • Uncategorized

global map system

john October 31, 2025
  • Uncategorized

todoapp build with latest stack

john October 31, 2025
  • About Author
  • About Us
  • Contact Us
  • Disclaimer
  • Privacy Policy
Copyright © All rights reserved. | MoreNews by AF themes.