npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-native-tpl-maps-view

v2.2.5

Published

TPL Maps React Native Package

Readme

React Native TPL Maps

react-native-tpl-maps-view lets you use the TPL Maps SDK as a native map view in React Native apps on Android and iOS.

The package connects to TPL Maps servers, draws the map, and handles gestures and camera movement. From JavaScript, you can add markers, draw routes, search for places, and more.


Table of Contents


Requirements

| Item | Supported version | |---|---| | Android | minSdkVersion 21 or higher | | iOS | 12.0 or higher (15.1+ recommended, this is the version we tested) | | React | 18.2.0 | | React Native | 0.71.7 or higher | | New Architecture (Fabric) | Supported, tested with newArchEnabled=true |


Installation

npm install react-native-tpl-maps-view

If you want to follow the navigation example in this guide, also install:

npm install @react-navigation/native @react-navigation/native-stack react-native-screens react-native-safe-area-context

[!NOTE] If Metro cannot find prop-types, install it yourself with npm install prop-types. The package uses it internally but does not list it as a dependency yet.


Keep Your API Key Safe

Never write your real API key directly in your source code or commit it to Git. The examples in this guide read the key from a separate config file:

// config.ts  (add this file to .gitignore)
export const TPL_MAPS_API_KEY = 'YOUR_TPL_MAPS_API_KEY';

You can also use a tool like react-native-config to load the key from a .env file.

You can get your API key from api.tplmaps.com.


Android Setup

1. Add the TPL Maps Maven repository

Open android/build.gradle (the root one) and add the repositories inside the existing buildscript { } block:

buildscript {
    ext {
        // ...
    }
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        // ...
    }

    allprojects {
        repositories {
            jcenter()
            google()
            maven { url 'https://jitpack.io' }
            maven { url 'https://api1.tplmaps.com/artifactory/example-repo-local/' }
        }
    }
}

[!TIP] This setup is tested with React Native 0.77 and the New Architecture, using:

  • Android Gradle Plugin 8.5.1
  • NDK 27.2.12479018
  • Kotlin 1.9.24
  • Gradle 8.10.2

If you are updating your tools for the New Architecture, use these versions.

2. Fix the native library conflict

The TPL Maps SDK comes with its own copy of libc++_shared.so. React Native has one too, and the two can clash during the build.

To fix this, open android/app/build.gradle and add the following inside the android { } block:

android {
    // ...
    packaging {
        jniLibs {
            useLegacyPackaging = false
            keepDebugSymbols += "**/libc++_shared.so"
        }
        pickFirsts += [
            "lib/arm64-v8a/libc++_shared.so",
            "lib/armeabi-v7a/libc++_shared.so",
            "lib/x86_64/libc++_shared.so",
            "lib/x86/libc++_shared.so"
        ]
    }
}

3. Add your API key

Open android/app/src/main/AndroidManifest.xml and add these tags inside <application>:

<meta-data
    android:name="@string/metadata_name_api_key"
    android:value="YOUR_TPL_MAPS_API_KEY" />

<meta-data
    android:name="@string/metadata_name_origin"
    android:value="https://yourapp.com/" />

[!NOTE] You do not need to create the @string/metadata_name_api_key and @string/metadata_name_origin resources yourself. They come from the TPL Maps native SDK (com.tpl.maps.sdk:maps) once Gradle downloads it from the repository in step 1. Just replace the android:value with your own key and origin.

You also do not need to add any permissions. This package already declares INTERNET, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_NETWORK_STATE, and the OpenGL ES 2.0 feature. Android merges them into your app automatically.

4. Register the native package

In android/app/src/main/java/.../MainApplication.kt:

import com.awesomeproject3.MyPackage

// ...

override fun getPackages(): List<ReactPackage> =
    PackageList(this).packages.apply {
        add(MyPackage())
    }

5. Turn on the New Architecture

In android/gradle.properties:

newArchEnabled=true

6. Ask for location permission at runtime

The package only declares the location permissions. On Android 6 and above, you must still ask the user for permission while the app is running. Until the user says yes, the "my location" button, setZoomOnCurrentLocation(), and EnableLocationButton() will not work.

The native SDK does not ask for this permission for you. Ask for it yourself, usually right before you need it:

import { PermissionsAndroid, Platform } from 'react-native';

async function requestLocationPermission() {
  if (Platform.OS !== 'android') return true;
  const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
  return granted === PermissionsAndroid.RESULTS.GRANTED;
}

[!NOTE] On iOS you don't need to do this. The native SDK asks for permission by itself as soon as the map appears, using the text you set in the iOS setup below.


iOS Setup

1. Set the deployment target

In ios/Podfile:

platform :ios, '15.1'

2. Install pods

Autolinking adds react-native-tpl-maps-view, which depends on the native TPLMaps pod (version 4.3.4). If CocoaPods cannot find it, add it yourself above use_native_modules!:

target 'YourApp' do
  pod 'TPLMaps'
  config = use_native_modules!
  # ...
end

Then run:

cd ios && pod install

3. Add your TPL Maps credentials file

  1. Download tplservices.plist from api.tplmaps.com.
  2. Drag it into your app target in Xcode.
  3. Tick Copy items if needed.
  4. Make sure your app target is ticked under Target Membership, so the file is included in the app.

[!WARNING] tplservices.plist contains your credentials. Do not commit it to a public repository.

4. Add a location usage message

In ios/<YourApp>/Info.plist, give NSLocationWhenInUseUsageDescription a real message. It's easy to leave this empty while testing, but Apple will reject your app if it is empty.

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to show nearby places on the map.</string>

Events

On both platforms, every event is sent through one JavaScript event name: onReady.

  • On Android it comes from CustomEventEmitterModule.
  • On iOS it comes from CustomEventEmitter.

Listen once, then check event.type to see what happened:

import { NativeModules, NativeEventEmitter, Platform } from 'react-native';

const emitter =
  Platform.OS === 'android'
    ? new NativeEventEmitter(NativeModules.CustomEventEmitterModule)
    : new NativeEventEmitter(NativeModules.CustomEventEmitter);

const listener = emitter.addListener('onReady', (event) => {
  console.log(event.type, event);
});

// later: listener.remove();

| event.type | When it fires | Data you get | Platforms | |---|---|---|---| | onMapReady | The map has loaded and is ready to use. It fires once right away with empty data, then again a moment later with real coordinates. | { type, data: "lat;lng" } | Android, iOS | | onMapChange | The camera stops moving (after a pan, a zoom, or a SetCameraAnimation / setZoomOnCurrentLocation call). | { type, data: "lat;lng" } | Android, iOS | | onMarkerClick | The user taps any marker. | { type, lat, lng, title, desc } (fields are at the top level, there is no data field) | Android, iOS | | onLocationClick | The user taps the native "my location" button. | { type } (Android sends nothing extra, iOS also sends a data label) | Android, iOS | | onMapClick | The user taps the map or a point of interest. | { type, data: "lat;lng" } | Android, iOS | | onPolyLineClick | The user taps a drawn route. | { type, data: "id;order;properties" } | Android only |

[!IMPORTANT] Watch the coordinate order.

Event data always comes as "lat;lng". Every setter function in the API Reference below (AddMarker, AddCustomMarkerWithBase64, SetCameraAnimation, SetPolyLine, ...) expects the opposite order, "lng;lat".

When you take a coordinate from an event and pass it to one of these functions, swap it. This is the most common mistake with this package. A marker with swapped coordinates usually lands somewhere in the ocean, so it's not always easy to spot.


Usage

The example below is a complete, working app that puts all the setup above together. It covers the things most map apps need:

  • Show the user's location with a marker. The position is read from onMapReady / onMapChange and placed with SetMarkerAtPositionWithBase64, so the same pin moves instead of adding a new one every time.
  • Show several branches with custom markers. A branches array is drawn with AddCustomMarkerWithBase64.
  • React to a marker tap. onMarkerClick is matched to a branch, a route is drawn to it, and its details are shown.
  • Go to another screen and come back. Tapping the bottom bar opens SecondScreen with the selected branch. Going back returns to the map.
  • Let the user pick a spot by moving the map. A second screen (AddressPicker) finds the address under the center pin once the map stops moving.

The example is split into these files:

index.js
App.tsx
MainScreen.tsx
BranchLocator.tsx
AddressPicker.tsx
SecondScreen.tsx
config.ts

index.js

import { AppRegistry } from 'react-native';
import App from './App';
import { name as appName } from './app.json';

AppRegistry.registerComponent(appName, () => App);

App.tsx

import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

import MainScreen from './MainScreen';
import BranchLocator from './BranchLocator';
import AddressPicker from './AddressPicker';
import SecondScreen from './SecondScreen';

const Stack = createNativeStackNavigator();

const App: React.FC = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Main" component={MainScreen} />
        <Stack.Screen name="BranchLocator" component={BranchLocator} />
        <Stack.Screen name="AddressPicker" component={AddressPicker} />
        <Stack.Screen name="SecondScreen" component={SecondScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
};

export default App;

MainScreen.tsx

Two buttons that open the two demo screens.

import React from 'react';
import { View, Button, StyleSheet } from 'react-native';
import { NavigationProp, ParamListBase, useNavigation } from '@react-navigation/native';

const MainScreen: React.FC = () => {
  const navigation = useNavigation<NavigationProp<ParamListBase>>();

  return (
    <View style={styles.container}>
      <Button title="Open Map" onPress={() => navigation.navigate('BranchLocator')} />
      <View style={styles.spacer} />
      <Button title="Pick My Location" onPress={() => navigation.navigate('AddressPicker')} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center' },
  spacer: { height: 12 },
});

export default MainScreen;

BranchLocator.tsx

This screen:

  1. Shows the map.
  2. Asks for location permission.
  3. Places a marker that follows the user's position.
  4. Shows branches as custom markers.
  5. Draws a route to the branch the user taps.

It also includes a sizing fix for Fabric. Without it, the native map sometimes doesn't get its real size on the first layout, and stays blank.

import React, { useEffect, useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  Platform,
  PermissionsAndroid,
  TouchableOpacity,
  NativeModules,
  NativeEventEmitter,
  PixelRatio,
  useWindowDimensions,
} from 'react-native';
import { NavigationProp, ParamListBase, useNavigation } from '@react-navigation/native';
import MyViewManager, {
  AddCustomMarkerWithBase64,
  SetMarkerAtPositionWithBase64,
  RemoveAllMarkers,
  RemoveAllPolyLines,
  SetPolyLine,
  EnableLocationButton,
  setZoomOnCurrentLocation,
} from 'react-native-tpl-maps-view';
import { TPL_MAPS_API_KEY } from './config';

type Branch = { id: string; title: string; desc: string; latitude: number; longitude: number };

const branches: Branch[] = [
  { id: '1', title: 'Branch 1', desc: 'Main Boulevard', latitude: 24.899987, longitude: 67.163350 },
  { id: '2', title: 'Branch 2', desc: 'Airport Road', latitude: 24.860360, longitude: 67.001331 },
  { id: '3', title: 'Branch 3', desc: 'Highway Exit 4', latitude: 29.344599, longitude: 71.695158 },
];

// Small pin icons as base64 images.
// Replace these with your own (for example, a different icon for the user and for branches).
const branchMarkerBase64 =
  'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAFxSURBVEiJnZUvbkJBEIe/JSQgOEErwZIaisPVVCAwHKKOhGNQzwG4QQUnAAUhRZEKkLU4DDAVDMljuv9gk0nevvnN79uX3Z3nRITYcM61gHegpQGw0JiKyCJqICLeAKrAJ3ACJBAn1VSDPgHzBrCJGNvYAI0sAFAC5neYX2MOlHIAQ0/xEZgAHxoTfWd1wygAqAEHU/QLtD0LaWuuqD0AtRig41lVN3IQuh59p6gpmUP1auZbEfkiMDS3jXlYwIuZf4fMI5obDwvYm/lzBsBqbjws4Meuxjn3FHLWnP3qWw+zaU3+b9oUKHs2uKw5q2+m7sHaU7QE3rgc45o+Lz26dc5FG3gKi70n1psGOYAKsIuYhGIHVHKbXf8BQD+7mypkdof57K52Xeg15wzzM55elQQoZJwBGEc9EoAqsIqYr4j8zZIAhdS5XH9rvgfqyfqUQCE9D6CXVZsjUsioYD7KrXNanBzOuTJw/Td0ReSYU/cHfjZLwpHI7BkAAAAASUVORK5CYII=';
const userMarkerBase64 = branchMarkerBase64;

const BranchLocator: React.FC = () => {
  const navigation = useNavigation<NavigationProp<ParamListBase>>();
  const { width, height } = useWindowDimensions();

  // Start with no size, then set a real size a moment after the screen appears.
  // This makes Fabric lay out the native view again with real dimensions,
  // which is what makes the map start drawing.
  const [mapStyle, setMapStyle] = useState({});
  const [currentLocation, setCurrentLocation] = useState<{ lat: string; lng: string } | null>(null);
  const [selectedBranch, setSelectedBranch] = useState<Branch | null>(null);

  useEffect(() => {
    const timer = setTimeout(() => setMapStyle({ flex: 1, width: '100%', height: '100%' }), 300);
    return () => clearTimeout(timer);
  }, []);

  const requestLocationPermission = async () => {
    if (Platform.OS !== 'android') return true;
    const granted = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION);
    return granted === PermissionsAndroid.RESULTS.GRANTED;
  };

  const showBranchMarkers = () => {
    branches.forEach((branch) => {
      AddCustomMarkerWithBase64(
        `${branch.longitude};${branch.latitude};0;"${branch.title}";"${branch.desc}"`,
        branchMarkerBase64
      );
    });
  };

  // Event data comes as "lat;lng", so we swap it to "lng;lat" for the setter functions.
  const trackUserMarker = (lat: string, lng: string) => {
    setCurrentLocation({ lat, lng });
    SetMarkerAtPositionWithBase64(`${lng};${lat};0;"You are here";""`, userMarkerBase64);
  };

  const routeToBranch = (branch: Branch, from: { lat: string; lng: string }) => {
    RemoveAllPolyLines();
    SetPolyLine(`${from.lat};${from.lng}`, [`${branch.latitude};${branch.longitude}`], '#1D8548', '2', TPL_MAPS_API_KEY);
  };

  // The map is created again every time this screen opens (including when you
  // come back to it), so onMapReady fires again each time. That's why markers
  // and routes are drawn here, and not only once when the screen first loads.
  const handleEvent = (event: any, hasLocationPermission: boolean) => {
    if (event.type === 'onMapReady') {
      RemoveAllMarkers();
      RemoveAllPolyLines();
      showBranchMarkers();
      if (event.data) {
        const [lat, lng] = event.data.split(';');
        trackUserMarker(lat, lng);
      }
      if (hasLocationPermission) {
        EnableLocationButton(true);
        setZoomOnCurrentLocation();
      }
    } else if (event.type === 'onMapChange') {
      if (event.data) {
        const [lat, lng] = event.data.split(';');
        trackUserMarker(lat, lng);
      }
    } else if (event.type === 'onMarkerClick') {
      const branch = branches.find((b) => b.title === event.title);
      if (!branch) return; // The user tapped their own "You are here" marker.
      setSelectedBranch(branch);
      if (currentLocation) routeToBranch(branch, currentLocation);
    }
  };

  useEffect(() => {
    let listener: { remove: () => void } | undefined;

    requestLocationPermission().then((hasLocationPermission) => {
      const emitter =
        Platform.OS === 'android'
          ? new NativeEventEmitter(NativeModules.CustomEventEmitterModule)
          : new NativeEventEmitter(NativeModules.CustomEventEmitter);

      listener = emitter.addListener('onReady', (event) => handleEvent(event, hasLocationPermission));
    });

    return () => listener?.remove();
  }, []);

  return (
    <View style={{ flex: 1 }}>
      {Platform.OS === 'android' && (
        <MyViewManager
          style={mapStyle}
          customWidth={PixelRatio.getPixelSizeForLayoutSize(width)}
          customHeight={PixelRatio.getPixelSizeForLayoutSize(height)}
        />
      )}
      {Platform.OS === 'ios' && <MyViewManager style={{ flex: 1 }} />}

      <TouchableOpacity
        style={styles.footer}
        onPress={() => navigation.navigate('SecondScreen', { branch: selectedBranch })}
      >
        <Text style={styles.footerTitle}>{selectedBranch ? selectedBranch.title : 'Tap a branch marker'}</Text>
        <Text style={styles.footerDescription}>
          {selectedBranch ? selectedBranch.desc : 'Then tap here to see full details'}
        </Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  footer: {
    position: 'absolute',
    bottom: 10,
    left: 10,
    right: 10,
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 10,
  },
  footerTitle: { fontWeight: 'bold', color: 'black' },
  footerDescription: { color: 'black' },
});

export default BranchLocator;

How this example works

  • User location marker: This package has no getCurrentPosition() function. The only way to get the user's position in JavaScript is from the data field of onMapReady or onMapChange, because the native map is the one tracking GPS. We use SetMarkerAtPositionWithBase64 (not AddCustomMarkerWithBase64) so the same pin moves around instead of adding a new pin on every update.
  • Why we ask for permission first: EnableLocationButton and setZoomOnCurrentLocation only work after permission is given. On iOS the SDK asks by itself, so the hasLocationPermission check only matters inside the onMapReady part above.
  • Marker tap to route: Tapping a branch marker fires onMarkerClick. Its title is matched to the branches array (it's the same text used to create the marker), then SetPolyLine draws a route from the user's last known location to that branch.
  • Tapping a second branch on iOS: routeToBranch calls RemoveAllPolyLines() before drawing the new route. On Android this clears the old route. On iOS it does nothing (see Known Platform Differences), so older routes stay on the map next to the new one.
  • Leaving the screen and coming back: On both Android and iOS, the native code resets its "map ready" flag when the map is removed (which happens when you leave this screen). So onMapReady fires again when you come back, and the markers are drawn again.

AddressPicker.tsx

A "move the map to pick a location" screen. The selected spot is always whatever is under the pin in the center of the screen. When the user stops moving the map, the app looks up the address for that spot and shows it in a text field.

import React, { useEffect, useRef, useState } from 'react';
import {
  View,
  Text,
  TextInput,
  Button,
  StyleSheet,
  Platform,
  NativeModules,
  NativeEventEmitter,
  PixelRatio,
  useWindowDimensions,
} from 'react-native';
import { NavigationProp, ParamListBase, useNavigation } from '@react-navigation/native';
import MyViewManager, { getSearchPointData } from 'react-native-tpl-maps-view';
import { TPL_MAPS_API_KEY } from './config';

const AddressPicker: React.FC = () => {
  const navigation = useNavigation<NavigationProp<ParamListBase>>();
  const { width, height } = useWindowDimensions();

  const [mapStyle, setMapStyle] = useState({});
  const [address, setAddress] = useState('Drag the map to choose a location');
  const [isLoading, setIsLoading] = useState(false);

  // onMapChange can fire many times while the user is still dragging,
  // not only once when the map stops. Without this timer, every small
  // movement would send a new network request to look up the address.
  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);

  useEffect(() => {
    const timer = setTimeout(() => setMapStyle({ flex: 1, width: '100%', height: '100%' }), 300);
    return () => clearTimeout(timer);
  }, []);

  const reverseGeocode = async (lat: string, lng: string) => {
    setIsLoading(true);
    try {
      const result = await getSearchPointData('', lat, lng, TPL_MAPS_API_KEY);
      const place = result?.data?.[0];
      setAddress(place ? place.compound_address_parents ?? place.name : 'No address found here');
    } catch (error) {
      setAddress('Could not look up this address');
    } finally {
      setIsLoading(false);
    }
  };

  // Event data comes as "lat;lng". getSearchPointData takes lat and lng as
  // separate values, so no swapping is needed here (unlike the marker
  // functions, which expect one combined "lng;lat" string).
  const handleCenterChanged = (data: string) => {
    const [lat, lng] = data.split(';');
    if (debounceRef.current) clearTimeout(debounceRef.current);
    debounceRef.current = setTimeout(() => reverseGeocode(lat, lng), 600);
  };

  useEffect(() => {
    const emitter =
      Platform.OS === 'android'
        ? new NativeEventEmitter(NativeModules.CustomEventEmitterModule)
        : new NativeEventEmitter(NativeModules.CustomEventEmitter);

    const listener = emitter.addListener('onReady', (event) => {
      if ((event.type === 'onMapReady' || event.type === 'onMapChange') && event.data) {
        handleCenterChanged(event.data);
      }
    });

    return () => {
      listener.remove();
      if (debounceRef.current) clearTimeout(debounceRef.current);
    };
  }, []);

  return (
    <View style={{ flex: 1 }}>
      {Platform.OS === 'android' && (
        <MyViewManager
          style={mapStyle}
          customWidth={PixelRatio.getPixelSizeForLayoutSize(width)}
          customHeight={PixelRatio.getPixelSizeForLayoutSize(height)}
        />
      )}
      {Platform.OS === 'ios' && <MyViewManager style={{ flex: 1 }} />}

      {/* Fixed center pin. The map moves under it, the pin never moves. */}
      <View style={styles.centerPin} pointerEvents="none">
        <Text style={styles.centerPinText}>📍</Text>
      </View>

      <View style={styles.panel}>
        <TextInput style={styles.addressInput} editable={false} value={isLoading ? 'Looking up address…' : address} />
        <Button title="Use This Location" onPress={() => navigation.goBack()} />
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  centerPin: {
    position: 'absolute',
    top: '50%',
    left: '50%',
    marginLeft: -12,
    marginTop: -24,
  },
  centerPinText: { fontSize: 24 },
  panel: {
    position: 'absolute',
    bottom: 10,
    left: 10,
    right: 10,
    backgroundColor: 'white',
    borderRadius: 10,
    padding: 10,
  },
  addressInput: { color: 'black', marginBottom: 8 },
});

export default AddressPicker;

How this example works

  • Why the pin doesn't move: The pin is a normal View placed in the center of the screen. It has nothing to do with the map functions. The map moves underneath it, and the selected spot is always the point under the pin. This is the same point that onMapChange gives you in data.
  • Why we use a timer: This package has no "drag ended" event (see Events). onMapChange is the only camera event, and it can fire more than once per drag. The short timer in JavaScript waits until the map stops before looking up the address.

SecondScreen.tsx

This screen reads the branch sent from BranchLocator with navigation.navigate('SecondScreen', { branch }). It shows a full round trip: tap a marker, open a details screen with that marker's data, then go back. Going back opens the map again and redraws its markers, as explained above.

import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import { useNavigation, useRoute, NavigationProp, ParamListBase, RouteProp } from '@react-navigation/native';

type SecondScreenParams = { branch?: { title: string; desc: string } | null };

const SecondScreen: React.FC = () => {
  const navigation = useNavigation<NavigationProp<ParamListBase>>();
  const route = useRoute<RouteProp<Record<string, SecondScreenParams>, string>>();
  const branch = route.params?.branch;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>{branch ? branch.title : 'Second Screen'}</Text>
      <Text style={styles.description}>
        {branch ? branch.desc : 'You navigated here from the map screen without selecting a branch.'}
      </Text>
      <TouchableOpacity style={styles.backButton} onPress={() => navigation.goBack()}>
        <Text style={styles.backButtonText}>← Go Back</Text>
      </TouchableOpacity>
    </View>
  );
};

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 20 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 12 },
  description: { fontSize: 16, color: '#666', textAlign: 'center', marginBottom: 30 },
  backButton: { backgroundColor: '#D40000', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 8 },
  backButtonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});

export default SecondScreen;

Common Recipes

Two things almost every map app needs, taken out of the full example above so they are easy to find and copy.

Draw a route from one point to another

SetPolyLine(currentLatLng, destinationLatLng, color, width, apiKey)

This sends your starting point and one or more destinations to the TPL Maps routing service. It gets back the best route and draws it on the map.

  • Both currentLatLng and every item in destinationLatLng use "lat;lng" order. This is the opposite of most other setter functions in this package (see the coordinate order note).
  • destinationLatLng can have more than one stop. The route will be planned through all of them.
import { SetPolyLine, RemoveAllPolyLines } from 'react-native-tpl-maps-view';
import { TPL_MAPS_API_KEY } from './config';

// A route from one point to a single destination.
RemoveAllPolyLines(); // Clear any old route first (see the iOS note below).
SetPolyLine('24.820186;67.103118', ['24.801976;67.031951'], '#1D8548', '2', TPL_MAPS_API_KEY);

// Or a route through several stops in one call.
SetPolyLine(
  '24.820186;67.103118',
  ['24.801976;67.031951', '24.883838;67.177270', '24.846263;67.055640'],
  '#1D8548',
  '2',
  TPL_MAPS_API_KEY
);

[!WARNING] RemoveAllPolyLines() does nothing on iOS (see Known Platform Differences). On iOS, each new SetPolyLine call draws a new route on top of the old one instead of replacing it.

Move the camera to any point

SetCameraAnimation(lngLat, zoom)

This smoothly moves the camera to a point at the zoom level you choose.

  • Unlike SetPolyLine above, it uses "lng;lat" order (the same as the marker functions). Double check the order whenever you switch between the two.
  • zoom is a string, from about "10" (whole city) to "18" (street level).
  • On iOS, zoom is ignored and the camera always zooms to level 15.
import { SetCameraAnimation } from 'react-native-tpl-maps-view';

// Center the camera on a point, zoomed in to street level.
SetCameraAnimation('67.125973;24.820649', '15');

// A wider, city level view of the same point.
SetCameraAnimation('67.125973;24.820649', '11');

To center on the device's own location instead of a fixed point, use setZoomOnCurrentLocation() (see the user location marker in BranchLocator.tsx above). It takes no arguments because the native SDK already knows where the device is.


API Reference

All coordinates use "lng;lat" order unless a note says otherwise.

Markers

AddMarker(lngLat: string)

Adds a simple marker.

AddMarker('67.125973;24.820649');

AddCustomMarker(lngLatWithImageName: string)

Adds a marker with an image from your app's resources.

  • Android format: "lng;lat;imageName;width;height;bearing;title;desc"
  • iOS: works the same as AddMarker for now (the image name is ignored). Use AddCustomMarkerWithBase64 on iOS if you need a custom icon.

AddCustomMarkerWithBase64(lngLatMeta: string, base64Image: string)

Adds a marker with a base64 image.

  • lngLatMeta format: "lng;lat;bearing;title;description"
AddCustomMarkerWithBase64('67.1234;24.8765;0;"Title";"Description"', base64String);

SetMarkerAtPositionWithBase64(lngLatMeta: string, base64Image: string)

Same as AddCustomMarkerWithBase64, but uses one single marker. Each new call moves that marker instead of adding a new one.

RemoveAllMarkers()

Removes all markers.

[!NOTE] Marked as not working in the current iOS native code.

Camera

SetCameraAnimation(lngLat: string, zoom: string)

Smoothly moves the camera to a point.

SetCameraAnimation('67.125973;24.820649', '15');

[!NOTE] zoom is ignored on iOS (fixed at 15.0 in the native code).

EnableLocationButton(isEnable: boolean)

Shows or hides the native "my location" button.

setZoomOnCurrentLocation()

Moves the camera to the device's current location.

CameraCallback()

Returns a Promise. Android only. Calling it on iOS throws an error because the native module does not exist there.

Routes

SetPolyLine(currentLatLng: string, destinationLatLng: string[], color: string, width: string, apiKey: string)

Gets the best route from the TPL Maps routing service and draws it.

[!IMPORTANT] currentLatLng and every destination use "lat;lng" order (the reverse of the other functions).

SetPolyLine('24.820186;67.103118', ['24.801976;67.031951'], '#1D8548', '2', TPL_MAPS_API_KEY);

RemoveAllPolyLines()

Removes all drawn routes.

[!NOTE] Marked as not working in the current iOS native code.

Search

getSearchPointData(query: string, lat: string, lng: string, apiKey: string)

Finds the address of a point (reverse geocoding). Returns a Promise with the API response as JSON.

getSearchByQuery(query: string, apiKey: string)

Searches for places by text.

getOptimizedSearch(query: string, lat: string, lng: string, apiKey: string)

Searches for places by text, with results sorted by how close they are to the given point.


Maintainers

TPL Maps

| Name | Role | Email | |---|---|---| | Bilal Sipra | Head of Information Technology (IT) | [email protected] | | Daniyal Ahmed Khan | Manager & Team Lead (Mobile Development) | [email protected] | | Zaeem Ehsanullah | Assistant Manager (Mobile Application Development) | [email protected] |


Contributing

Found a bug or have a problem? Please email [email protected].


License

MIT