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

expo-osm-sdk

v2.1.1

Published

OpenStreetMap component for React Native with Expo

Readme

expo-osm-sdk

npm version License: MIT TypeScript Expo

Native OpenStreetMap SDK for Expo — display maps, markers, shapes, and track location on iOS and Android.

v2.0.0 — Focused core SDK. Routing (OSRM) and search (Nominatim) features have been removed. See the migration guide if you were using them.


Features

  • 🗺️ Native map powered by MapLibre GL Native (iOS & Android)
  • 📍 Markers with custom icons, drag support, and info windows
  • 📐 Shapes — polylines, polygons, circles, custom overlays
  • 📷 Camera controls — zoom, pan, pitch, bearing, animateToLocation, fitToMarkers
  • 📌 User location — display with signature purple dot, follow mode, and color customization
  • 🛰️ Location tracking hook (useLocationTracking)
  • 🏠 Geofencing hooks (useGeofencing, useSingleGeofence) with enter/exit/dwell events
  • 🧭 Navigation controls UI component (zoom, compass, pitch toggle)
  • 🌐 Web — safe fallback when MapLibre GL JS is not installed
  • 📦 TypeScript — full type definitions included

Requirements

| Requirement | Version | |---|---| | Expo SDK | ≥ 49 | | React Native | ≥ 0.72 | | React | ≥ 18 | | iOS | ≥ 11 | | Android API | ≥ 21 | | Node.js | ≥ 16 |


Installation

npm install expo-osm-sdk

Expo Config Plugin (recommended)

Add to your app.json:

{
  "expo": {
    "plugins": [
      ["expo-osm-sdk/plugin", {
        "locationPermissionText": "This app uses your location to display it on the map"
      }]
    ]
  }
}

Then rebuild your development client:

npx expo run:ios
# or
npx expo run:android

Note: This SDK requires a development build. It does not work in Expo Go.


Quick Start

import React from 'react';
import { StyleSheet, View } from 'react-native';
import { OSMView } from 'expo-osm-sdk';

export default function App() {
  return (
    <View style={styles.container}>
      <OSMView
        style={styles.map}
        initialCenter={{ latitude: 40.7128, longitude: -74.0060 }}
        initialZoom={12}
        onMapReady={() => console.log('Map is ready')}
        onPress={(coordinate) => console.log('Tapped:', coordinate)}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1 },
  map: { flex: 1 },
});

API Reference

OSMView

The main map component.

import { OSMView, OSMViewRef } from 'expo-osm-sdk';
import { useRef } from 'react';

const mapRef = useRef<OSMViewRef>(null);

<OSMView
  ref={mapRef}
  style={{ flex: 1 }}

  // Initial camera
  initialCenter={{ latitude: 51.505, longitude: -0.09 }}
  initialZoom={13}
  initialPitch={0}       // tilt in degrees (0–60)
  initialBearing={0}     // rotation in degrees (0–360)

  // Tile source — defaults to Carto Voyager vector tiles
  styleUrl="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
  // or raster:
  tileServerUrl="https://tile.openstreetmap.org/{z}/{x}/{y}.png"

  // Overlays
  markers={[...]}
  polylines={[...]}
  polygons={[...]}
  circles={[...]}

  // Clustering
  clustering={{ enabled: true, radius: 50 }}

  // User location
  showUserLocation={true}
  followUserLocation={false}
  userLocationTintColor="#9C1AFF"

  // Controls
  showsCompass={true}
  showsZoomControls={true}
  rotateEnabled={true}
  scrollEnabled={true}
  zoomEnabled={true}
  pitchEnabled={true}

  // Events
  onMapReady={() => {}}
  onRegionChange={(region) => {}}
  onPress={(coordinate) => {}}
  onLongPress={(coordinate) => {}}
  onMarkerPress={(markerId, coordinate) => {}}
  onUserLocationChange={(coordinate) => {}}
/>

OSMViewRef Methods

| Method | Description | |---|---| | zoomIn() | Zoom in one level | | zoomOut() | Zoom out one level | | setZoom(zoom) | Set zoom level (1–20) | | animateToLocation(lat, lng, zoom?) | Fly to coordinates | | animateToRegion(region, duration?) | Fit to a region | | fitToMarkers(markerIds?, padding?) | Fit all/selected markers in view | | animateCamera(options) | Animate pitch, bearing, zoom together | | setPitch(degrees) | Set camera tilt | | setBearing(degrees) | Set camera rotation | | getPitch() | Get current pitch | | getBearing() | Get current bearing | | getCurrentLocation() | Get user's GPS coordinate | | startLocationTracking() | Start GPS updates | | stopLocationTracking() | Stop GPS updates | | waitForLocation(timeoutSeconds) | Wait for fresh GPS fix | | addMarker(marker) | Add marker at runtime | | removeMarker(markerId) | Remove marker | | updateMarker(markerId, updates) | Update marker properties | | addPolyline(polyline) | Add polyline | | removePolyline(polylineId) | Remove polyline | | addPolygon(polygon) | Add polygon | | removePolygon(polygonId) | Remove polygon | | addCircle(circle) | Add circle | | removeCircle(circleId) | Remove circle | | takeSnapshot(format?, quality?) | Capture map as image |


Markers

import { OSMView } from 'expo-osm-sdk';
import type { MarkerConfig } from 'expo-osm-sdk';

const markers: MarkerConfig[] = [
  {
    id: 'marker-1',
    coordinate: { latitude: 40.7128, longitude: -74.0060 },
    title: 'New York',
    description: 'The Big Apple',
    icon: {
      uri: 'https://example.com/pin.png',
      size: 40,
      anchor: { x: 0.5, y: 1.0 },  // bottom-center
    },
    draggable: true,
    opacity: 1,
  },
];

<OSMView
  markers={markers}
  onMarkerPress={(id, coord) => console.log('Marker pressed:', id)}
  onMarkerDragEnd={(id, coord) => console.log('Dragged to:', coord)}
/>

Shapes

import type { PolylineConfig, PolygonConfig, CircleConfig } from 'expo-osm-sdk';

const polylines: PolylineConfig[] = [{
  id: 'route',
  coordinates: [
    { latitude: 40.7128, longitude: -74.0060 },
    { latitude: 40.7580, longitude: -73.9855 },
  ],
  strokeColor: '#007AFF',
  strokeWidth: 4,
  strokeOpacity: 0.9,
}];

const polygons: PolygonConfig[] = [{
  id: 'area',
  coordinates: [
    { latitude: 40.70, longitude: -74.02 },
    { latitude: 40.73, longitude: -74.02 },
    { latitude: 40.73, longitude: -73.98 },
    { latitude: 40.70, longitude: -73.98 },
  ],
  fillColor: '#FF0000',
  fillOpacity: 0.2,
  strokeColor: '#FF0000',
  strokeWidth: 2,
}];

const circles: CircleConfig[] = [{
  id: 'zone',
  center: { latitude: 40.7128, longitude: -74.0060 },
  radius: 500,  // meters
  fillColor: '#0000FF',
  fillOpacity: 0.15,
  strokeColor: '#0000FF',
  strokeWidth: 2,
}];

<OSMView
  polylines={polylines}
  polygons={polygons}
  circles={circles}
/>

User Location

<OSMView
  showUserLocation={true}
  followUserLocation={true}
  userLocationTintColor="#9C1AFF"
  userLocationAccuracyFillColor="rgba(156, 26, 255, 0.2)"
  userLocationAccuracyBorderColor="#9C1AFF"
  onUserLocationChange={(coord) => {
    console.log('User is at:', coord.latitude, coord.longitude);
  }}
/>

Camera Controls

import { useRef } from 'react';
import { OSMView, OSMViewRef } from 'expo-osm-sdk';

const mapRef = useRef<OSMViewRef>(null);

// Fly to a location
await mapRef.current?.animateToLocation(48.8566, 2.3522, 14);

// 3D navigation view
await mapRef.current?.animateCamera({
  latitude: 48.8566,
  longitude: 2.3522,
  zoom: 16,
  pitch: 45,
  bearing: 90,
  duration: 1000,
});

// Fit all markers in view
await mapRef.current?.fitToMarkers(undefined, 80);

NavigationControls Component

A ready-made controls overlay for zoom, compass, and pitch:

import { NavigationControls } from 'expo-osm-sdk';

<NavigationControls
  onZoomIn={() => mapRef.current?.zoomIn()}
  onZoomOut={() => mapRef.current?.zoomOut()}
  onResetBearing={() => mapRef.current?.setBearing(0)}
  onResetPitch={() => mapRef.current?.setPitch(0)}
  getBearing={() => mapRef.current?.getBearing() ?? Promise.resolve(0)}
  getPitch={() => mapRef.current?.getPitch() ?? Promise.resolve(0)}
  style={{ position: 'absolute', right: 16, bottom: 120 }}
/>

LocationButton Component

A button that flies the camera to the user's current location:

import { LocationButton } from 'expo-osm-sdk';

<LocationButton
  getCurrentLocation={() => mapRef.current!.getCurrentLocation()}
  onLocationFound={(loc) => {
    mapRef.current?.animateToLocation(loc.latitude, loc.longitude, 15);
  }}
  style={{ position: 'absolute', right: 16, bottom: 60 }}
/>

useLocationTracking Hook

import { useLocationTracking } from 'expo-osm-sdk';
import { useRef } from 'react';

const mapRef = useRef<OSMViewRef>(null);
const { currentLocation, isTracking, error } = useLocationTracking(mapRef, {
  autoStart: true,
});

useGeofencing Hook

Monitor enter, exit, and dwell events for circle and polygon geofences:

import { useGeofencing } from 'expo-osm-sdk';
import type { Geofence } from 'expo-osm-sdk';

const geofences: Geofence[] = [
  {
    id: 'office',
    name: 'Office',
    type: 'circle',
    center: { latitude: 51.505, longitude: -0.09 },
    radius: 200,  // meters
  },
  {
    id: 'park',
    name: 'Hyde Park',
    type: 'polygon',
    coordinates: [
      { latitude: 51.508, longitude: -0.165 },
      { latitude: 51.513, longitude: -0.165 },
      { latitude: 51.513, longitude: -0.155 },
      { latitude: 51.508, longitude: -0.155 },
    ],
  },
];

const { activeGeofences, isInGeofence, getDwellTime } = useGeofencing(
  mapRef,
  geofences,
  {
    onEnter: (event) => console.log('Entered:', event.geofenceName),
    onExit: (event) => console.log('Exited:', event.geofenceName),
    onDwell: (event) => console.log('Dwelling in:', event.geofenceName),
    checkInterval: 5000,     // ms
    dwellThreshold: 60000,   // ms before dwell event fires
  }
);

console.log('Currently inside:', activeGeofences);
console.log('In office?', isInGeofence('office'));
console.log('Office dwell time (ms):', getDwellTime('office'));

For a single geofence, use useSingleGeofence:

import { useSingleGeofence } from 'expo-osm-sdk';

const { isInside, dwellTime } = useSingleGeofence(mapRef, geofences[0], {
  onEnter: (e) => alert('You arrived!'),
});

Tile Configuration

All built-in presets are available via TILE_CONFIGS. Each entry has a styleUrl (vector) or tileUrl (raster), plus an attribution string you must display in your app per each provider's terms.

| Key | Provider | Type | Production? | API key | |---|---|---|---|---| | openMapTiles | Carto Voyager | Vector | ✅ Recommended | None | | openfreemapLiberty | OpenFreeMap | Vector | ✅ Recommended | None | | openfreemapPositron | OpenFreeMap | Vector | ✅ Recommended | None | | openfreemapBright | OpenFreeMap | Vector | ✅ Recommended | None | | openTopoMap | OpenTopoMap | Raster | ⚠️ Low-traffic only | None | | humanitarian | HOT OSM | Raster | ⚠️ Low-traffic only | None | | openStreetMap | OpenStreetMap | Raster | ❌ Dev/demo only | None |

openStreetMap raster tiles must not be used in production. The OSM tile servers are a shared community resource. Their usage policy explicitly prohibits commercial or high-traffic use. Violating it risks your IP being blocked. Use any of the vector presets above instead — they look better, load faster, and have no such restrictions.

OpenFreeMap is fully open-source. If you use openfreemap* presets in production, consider sponsoring the project or self-hosting for SLA needs.

Basic usage

import { OSMView, TILE_CONFIGS } from 'expo-osm-sdk';

// Vector (recommended for all production apps)
<OSMView styleUrl={TILE_CONFIGS.openMapTiles.styleUrl} />
<OSMView styleUrl={TILE_CONFIGS.openfreemapLiberty.styleUrl} />
<OSMView styleUrl={TILE_CONFIGS.openfreemapPositron.styleUrl} />
<OSMView styleUrl={TILE_CONFIGS.openfreemapBright.styleUrl} />

// Raster (dev/demo/specialist use only)
<OSMView tileServerUrl={TILE_CONFIGS.openTopoMap.tileUrl} />
<OSMView tileServerUrl={TILE_CONFIGS.humanitarian.tileUrl} />

Layer switcher pattern

Let the user switch basemap style at runtime — styleUrl is a reactive prop:

import { useState } from 'react';
import { View, Pressable, Text, StyleSheet } from 'react-native';
import { OSMView, TILE_CONFIGS } from 'expo-osm-sdk';

type LayerKey = 'openMapTiles' | 'openfreemapLiberty' | 'openfreemapPositron' | 'openfreemapBright';

const LAYERS: Record<LayerKey, string> = {
  openMapTiles:        'Voyager',
  openfreemapLiberty:  'Liberty',
  openfreemapPositron: 'Positron',
  openfreemapBright:   'Bright',
};

export default function MapWithLayerSwitcher() {
  const [active, setActive] = useState<LayerKey>('openfreemapLiberty');
  const config = TILE_CONFIGS[active];

  return (
    <View style={{ flex: 1 }}>
      <OSMView
        style={{ flex: 1 }}
        styleUrl={config.styleUrl}
        initialCenter={{ latitude: 20.5937, longitude: 78.9629 }}
        initialZoom={5}
      />

      {/* Layer switcher */}
      <View style={styles.switcher}>
        {(Object.keys(LAYERS) as LayerKey[]).map((key) => (
          <Pressable
            key={key}
            onPress={() => setActive(key)}
            style={[styles.chip, active === key && styles.chipActive]}
          >
            <Text style={[styles.chipText, active === key && styles.chipTextActive]}>
              {LAYERS[key]}
            </Text>
          </Pressable>
        ))}
      </View>

      {/* Attribution — required by each provider's terms */}
      <Text style={styles.attribution}>{config.attribution}</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  switcher:       { flexDirection: 'row', flexWrap: 'wrap', gap: 8, padding: 10 },
  chip:           { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 20, backgroundColor: '#e2e8f0' },
  chipActive:     { backgroundColor: '#0ea5e9' },
  chipText:       { fontSize: 13, color: '#1e293b' },
  chipTextActive: { color: 'white', fontWeight: '600' },
  attribution:    { fontSize: 10, color: '#64748b', textAlign: 'right', paddingHorizontal: 8, paddingBottom: 4 },
});

Geofencing Utilities

Low-level utilities for custom geofence logic:

import {
  calculateDistance,    // meters between two coordinates
  isPointInCircle,
  isPointInPolygon,
  isPointInGeofence,
  distanceToGeofence,
  validateGeofence,
  getGeofenceCenter,
  doGeofencesOverlap,
} from 'expo-osm-sdk';

const dist = calculateDistance(
  { latitude: 51.505, longitude: -0.09 },
  { latitude: 48.857, longitude: 2.352 }
);
console.log(dist, 'meters');

Web Support

On web, OSMView renders a fallback UI by default. For a real interactive map on web, install maplibre-gl:

npm install maplibre-gl

Then configure your bundler to handle MapLibre's CSS (see docs/WEB_SETUP_GUIDE.md).


Platform Behavior

| Environment | Behavior | |---|---| | iOS development build | ✅ Full native map | | Android development build | ✅ Full native map | | Expo Go | ⚠️ Fallback UI — native modules not available | | Web (without maplibre-gl) | ⚠️ Fallback UI | | Web (with maplibre-gl) | ✅ Interactive map via MapLibre GL JS |


Expo SDK Compatibility

| expo-osm-sdk | Expo SDK | React Native | |---|---|---| | 2.x | 49–53+ | 0.72–0.77+ | | 1.x | 49–53 | 0.72–0.76 |


v2.0.0 Migration Guide

v2.0.0 removes the built-in routing and search features. These were out of scope for a map display SDK — they depend on external services (OSRM, Nominatim) and belong in separate, purpose-built packages.

Removed exports

| Removed | Reason | |---|---| | useOSRMRouting | Routing belongs in a routing library | | calculateRoute, calculateSimpleRoute, getRouteEstimate | Routing | | formatDuration, formatRouteDistance, calculateStraightLineDistance | Routing utilities | | useNominatimSearch | Geocoding belongs in a geocoding library | | searchLocations, reverseGeocode, getSuggestions | Geocoding | | calculateDistanceKm, formatDistance (nominatim) | Geocoding utilities | | quickSearch, searchNearby, getAddressFromCoordinates, searchPOI, smartSearch | Search helpers | | SearchBox | App-specific UI component | | Route, RouteStep, NominatimSearchResult, NominatimAddress, etc. | Types for removed features |

What to use instead

For routing:

Draw a route by passing coordinates to polylines directly, or use any routing API (OSRM, Valhalla, GraphHopper, Google Directions, etc.) and pass the resulting coordinates:

// Get coordinates from any routing API, then display them:
<OSMView
  polylines={[{
    id: 'route',
    coordinates: routeCoordinates,  // from your routing provider
    strokeColor: '#007AFF',
    strokeWidth: 4,
  }]}
/>

For geocoding/search:

Use any geocoding library. For Nominatim specifically, the API is simple enough to call directly:

const response = await fetch(
  `https://nominatim.openstreetmap.org/search?q=${query}&format=json&limit=5`,
  { headers: { 'User-Agent': 'YourApp/1.0' } }
);
const results = await response.json();

License

MIT © Saikat Maiti