Geospatial

Build location based experiences within ViroReact using our Geospatial APIs

Geospatial API

The Geospatial API enables location-based augmented reality experiences by leveraging Google's ARCore Geospatial API. This allows you to place virtual content at real-world geographic coordinates (latitude, longitude, altitude) that persists across sessions and can be experienced by multiple users.

Overview

The Geospatial API provides:

  • Earth Tracking: Track the device's position relative to the Earth using GPS and Visual Positioning System (VPS)
  • Geospatial Anchors: Place AR content at specific geographic coordinates
  • Three Anchor Types: WGS84 (absolute altitude), Terrain (relative to ground), and Rooftop (relative to buildings)
  • VPS Availability: Check if enhanced visual positioning is available at a location
  • Cross-Platform: Works on both iOS and Android

Requirements

  • iOS: iOS 12.0+ with ARKit support, ARCore SDK for iOS
  • Android: Android 7.0+ (API 24) with ARCore support
  • Google Cloud: Valid API key with ARCore API enabled
  • Location Services: Device location permissions must be granted
  • Network: Internet connection required for geospatial features

Setup

Google Cloud Setup (Required for All Projects)

Before configuring your app, you need to set up Google Cloud:

  1. Go to Google Cloud Console
  2. Create a new project or select an existing one
  3. Enable the ARCore API:
    • Navigate to "APIs & Services" > "Library"
    • Search for "ARCore API"
    • Click "Enable"
  4. Create an API key:
    • Navigate to "APIs & Services" > "Credentials"
    • Click "Create Credentials" > "API Key"
    • (Optional) Restrict the key to your app's bundle ID/package name for security

Expo Projects

For Expo projects, the plugin handles all native configuration automatically.

1. Configure app.json or app.config.js:

{
  "expo": {
    "plugins": [
      [
        "@reactvision/react-viro",
        {
          "googleCloudApiKey": "YOUR_GOOGLE_CLOUD_API_KEY",
          "geospatialAnchorProvider": "arcore",
          "android": {
            "xRMode": ["AR"]
          }
        }
      ]
    ]
  }
}

2. Rebuild your app:

npx expo prebuild --clean
npx expo run:ios
# or
npx expo run:android

That's it! The Expo plugin automatically configures:

PlatformWhat the plugin adds
iOSGARAPIKey in Info.plist, use_frameworks! :linkage => :dynamic in Podfile, ARCore/CloudAnchors, ARCore/Geospatial, and ARCore/Semantics pods, location permission descriptions
Androidcom.google.android.ar.API_KEY meta-data, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, CAMERA, and INTERNET permissions in AndroidManifest.xml

Bare React Native Projects

For bare React Native projects, you must configure native files manually.

iOS Setup

1. Modify your Podfile:

Add the following to your ios/Podfile:

# Enable dynamic frameworks (required for ARCore)
use_frameworks! :linkage => :dynamic

# Add ARCore pods
pod 'ARCore/CloudAnchors', '~> 1.51.0'
pod 'ARCore/Geospatial', '~> 1.51.0'
pod 'ARCore/Semantics', '~> 1.51.0'  # Required for Scene Semantics support

Important: ARCore SDK requires use_frameworks! with dynamic linkage. This may affect other dependencies in your project.

2. Add API key to Info.plist:

Add the following to your ios/[YourApp]/Info.plist:

<key>GARAPIKey</key>
<string>YOUR_GOOGLE_CLOUD_API_KEY</string>

3. Add required permissions to Info.plist:

<key>NSLocationWhenInUseUsageDescription</key>
<string>This app uses your location to place AR content in the real world.</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>This app uses your location to place AR content in the real world.</string>
<key>NSCameraUsageDescription</key>
<string>This app uses the camera for augmented reality.</string>

4. Install pods:

cd ios && pod install && cd ..

Android Setup

1. Add API key to AndroidManifest.xml:

Add the following inside the <application> tag in android/app/src/main/AndroidManifest.xml:

<meta-data
    android:name="com.google.android.ar.API_KEY"
    android:value="YOUR_GOOGLE_CLOUD_API_KEY" />

2. Add required permissions to AndroidManifest.xml:

Add the following outside the <application> tag:

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

3. (Optional) Override ARCore version in build.gradle:

ViroReact includes ARCore as a dependency, but you can override the version if needed:

dependencies {
    implementation 'com.google.ar:core:1.44.0'
}

4. Rebuild your app:

npx react-native run-android

Basic Usage

import React, { useRef, useEffect, useState } from "react";
import {
  ViroARSceneNavigator,
  ViroARScene,
  ViroBox,
  ViroNode,
  ViroGeospatialPose,
} from "@reactvision/react-viro";

const GeospatialScene = (props: { arSceneNavigator: any }) => {
  const { arSceneNavigator } = props;
  const [pose, setPose] = useState<ViroGeospatialPose | null>(null);
  const [anchorPosition, setAnchorPosition] = useState<number[] | null>(null);

  useEffect(() => {
    // Enable geospatial mode when scene loads
    arSceneNavigator.setGeospatialModeEnabled(true);

    // Poll for geospatial pose
    const interval = setInterval(async () => {
      const result = await arSceneNavigator.getCameraGeospatialPose();
      if (result.success && result.pose) {
        setPose(result.pose);
      }
    }, 1000);

    return () => clearInterval(interval);
  }, []);

  const placeAnchorAtCurrentLocation = async () => {
    if (!pose) return;

    // Create a terrain anchor 1 meter above ground at current location
    const result = await arSceneNavigator.createTerrainAnchor(
      pose.latitude,
      pose.longitude,
      1.0, // 1 meter above terrain
      [0, 0, 0, 1] // Identity quaternion (facing north)
    );

    if (result.success && result.anchor) {
      setAnchorPosition(result.anchor.position);
      console.log("Anchor created:", result.anchor.anchorId);
    }
  };

  return (
    <ViroARScene>
      {anchorPosition && (
        <ViroNode position={anchorPosition}>
          <ViroBox
            position={[0, 0, 0]}
            scale={[0.3, 0.3, 0.3]}
            materials={["redMaterial"]}
          />
        </ViroNode>
      )}
    </ViroARScene>
  );
};

const App = () => {
  return (
    <ViroARSceneNavigator
      initialScene={{ scene: GeospatialScene }}
      geospatialAnchorProvider="arcore"
    />
  );
};

export default App;

API Reference

Props

geospatialAnchorProvider

Enable the Geospatial API provider.

TypeRequiredDefault
"none" | "arcore"No"none"
<ViroARSceneNavigator
  geospatialAnchorProvider="arcore"
  // ... other props
/>

Methods

All methods are available through the arSceneNavigator object passed to your scene.


isGeospatialModeSupported()

Check if geospatial mode is supported on the current device.

Returns: Promise<ViroGeospatialSupportResult>

const checkSupport = async () => {
  const result = await arSceneNavigator.isGeospatialModeSupported();
  if (result.supported) {
    console.log("Geospatial is supported!");
  } else {
    console.log("Not supported:", result.error);
  }
};

setGeospatialModeEnabled(enabled: boolean)

Enable or disable geospatial tracking.

Parameters:

  • enabled: boolean - Whether to enable geospatial mode
// Enable geospatial tracking
arSceneNavigator.setGeospatialModeEnabled(true);

// Disable when done
arSceneNavigator.setGeospatialModeEnabled(false);

getEarthTrackingState()

Get the current Earth tracking state.

Returns: Promise<ViroEarthTrackingStateResult>

const checkTracking = async () => {
  const result = await arSceneNavigator.getEarthTrackingState();
  console.log("Tracking state:", result.state);
  // "Enabled" | "Paused" | "Stopped"
};

getCameraGeospatialPose()

Get the camera's current geospatial pose including location and accuracy metrics.

Returns: Promise<ViroGeospatialPoseResult>

const getPose = async () => {
  const result = await arSceneNavigator.getCameraGeospatialPose();

  if (result.success && result.pose) {
    const { pose } = result;
    console.log(`Location: ${pose.latitude}, ${pose.longitude}`);
    console.log(`Altitude: ${pose.altitude}m`);
    console.log(`Heading: ${pose.heading}°`);
    console.log(`Horizontal accuracy: ${pose.horizontalAccuracy}m`);
    console.log(`Vertical accuracy: ${pose.verticalAccuracy}m`);
  }
};

checkVPSAvailability(latitude: number, longitude: number)

Check if Visual Positioning System (VPS) is available at a location. VPS provides enhanced accuracy in supported areas.

Parameters:

  • latitude: number - Latitude in degrees
  • longitude: number - Longitude in degrees

Returns: Promise<ViroVPSAvailabilityResult>

const checkVPS = async (lat: number, lng: number) => {
  const result = await arSceneNavigator.checkVPSAvailability(lat, lng);

  switch (result.availability) {
    case "Available":
      console.log("VPS available - high accuracy positioning!");
      break;
    case "Unavailable":
      console.log("VPS not available - GPS-only positioning");
      break;
    case "Unknown":
      console.log("Could not determine VPS availability");
      break;
  }
};

createGeospatialAnchor(latitude, longitude, altitude, quaternion?)

Create a WGS84 geospatial anchor at absolute coordinates.

Parameters:

  • latitude: number - Latitude in degrees
  • longitude: number - Longitude in degrees
  • altitude: number - Altitude in meters above WGS84 ellipsoid
  • quaternion: [x, y, z, w] - Orientation in EUS frame (optional, defaults to facing north)

Returns: Promise<ViroCreateGeospatialAnchorResult>

const createAnchor = async () => {
  const result = await arSceneNavigator.createGeospatialAnchor(
    37.7749, // latitude
    -122.4194, // longitude
    10.0, // 10 meters altitude
    [0, 0, 0, 1] // facing north
  );

  if (result.success && result.anchor) {
    console.log("Created anchor:", result.anchor.anchorId);
    console.log("World position:", result.anchor.position);
  }
};

createTerrainAnchor(latitude, longitude, altitudeAboveTerrain, quaternion?)

Create an anchor positioned relative to the terrain surface. The altitude is automatically resolved based on terrain data.

Parameters:

  • latitude: number - Latitude in degrees
  • longitude: number - Longitude in degrees
  • altitudeAboveTerrain: number - Height above terrain in meters
  • quaternion: [x, y, z, w] - Orientation in EUS frame (optional)

Returns: Promise<ViroCreateGeospatialAnchorResult>

const createTerrainAnchor = async () => {
  // Place content 2 meters above the ground
  const result = await arSceneNavigator.createTerrainAnchor(
    37.7749,
    -122.4194,
    2.0 // 2 meters above terrain
  );

  if (result.success) {
    // Position your AR content at result.anchor.position
  }
};

createRooftopAnchor(latitude, longitude, altitudeAboveRooftop, quaternion?)

Create an anchor positioned relative to a building rooftop. Useful for placing content on top of buildings.

Parameters:

  • latitude: number - Latitude in degrees
  • longitude: number - Longitude in degrees
  • altitudeAboveRooftop: number - Height above rooftop in meters
  • quaternion: [x, y, z, w] - Orientation in EUS frame (optional)

Returns: Promise<ViroCreateGeospatialAnchorResult>

const createRooftopAnchor = async () => {
  // Place content 1 meter above the building rooftop
  const result = await arSceneNavigator.createRooftopAnchor(
    37.7749,
    -122.4194,
    1.0
  );

  if (result.success) {
    console.log("Rooftop anchor created at:", result.anchor.position);
  }
};

removeGeospatialAnchor(anchorId: string)

Remove a previously created geospatial anchor.

Parameters:

  • anchorId: string - The ID of the anchor to remove
const removeAnchor = (anchorId: string) => {
  arSceneNavigator.removeGeospatialAnchor(anchorId);
};

Types

// Provider type
type ViroGeospatialAnchorProvider = "none" | "arcore";

// Earth tracking state
type ViroEarthTrackingState = "Enabled" | "Paused" | "Stopped";

// VPS availability
type ViroVPSAvailability = "Available" | "Unavailable" | "Unknown";

// Anchor type
type ViroGeospatialAnchorType = "WGS84" | "Terrain" | "Rooftop";

// Quaternion [x, y, z, w] in East-Up-South frame
type ViroQuaternion = [number, number, number, number];

// Camera geospatial pose
type ViroGeospatialPose = {
  latitude: number; // Degrees
  longitude: number; // Degrees
  altitude: number; // Meters above WGS84 ellipsoid
  heading: number; // Degrees (0 = North, 90 = East)
  quaternion: ViroQuaternion; // Orientation in EUS frame
  horizontalAccuracy: number; // Meters (95% confidence)
  verticalAccuracy: number; // Meters (95% confidence)
  headingAccuracy: number; // Degrees (95% confidence)
  orientationYawAccuracy: number; // Degrees (95% confidence)
};

// Geospatial anchor
type ViroGeospatialAnchor = {
  anchorId: string;
  type: ViroGeospatialAnchorType;
  latitude: number;
  longitude: number;
  altitude: number;
  heading: number;
  position: [number, number, number]; // World coordinates [x, y, z]
};

// Result types
type ViroGeospatialSupportResult = {
  supported: boolean;
  error?: string;
};

type ViroEarthTrackingStateResult = {
  state: ViroEarthTrackingState;
  error?: string;
};

type ViroGeospatialPoseResult = {
  success: boolean;
  pose?: ViroGeospatialPose;
  error?: string;
};

type ViroVPSAvailabilityResult = {
  availability: ViroVPSAvailability;
  error?: string;
};

type ViroCreateGeospatialAnchorResult = {
  success: boolean;
  anchor?: ViroGeospatialAnchor;
  error?: string;
};

Anchor Types

WGS84 Anchors

WGS84 anchors use absolute coordinates on the WGS84 ellipsoid. You specify the exact altitude in meters above the ellipsoid (not above sea level or ground).

Use when:

  • You know the exact altitude from survey data
  • Placing content at known fixed heights
  • Working with data from external sources with WGS84 altitudes

Example:

// Place a marker at a surveyed location
await arSceneNavigator.createGeospatialAnchor(
  37.7749, // lat
  -122.4194, // lng
  15.5 // exact WGS84 altitude in meters
);

Terrain Anchors

Terrain anchors are positioned relative to the ground surface. Google's terrain data is used to resolve the actual altitude.

Use when:

  • Placing content on or near the ground
  • You don't know the exact altitude
  • Creating outdoor experiences at ground level

Example:

// Place a virtual pet on the ground
await arSceneNavigator.createTerrainAnchor(
  37.7749,
  -122.4194,
  0.0 // Directly on terrain
);

// Place a sign 3 meters above ground
await arSceneNavigator.createTerrainAnchor(37.7749, -122.4194, 3.0);

Rooftop Anchors

Rooftop anchors are positioned relative to building rooftops. Useful for content that should appear on top of buildings.

Use when:

  • Placing content on building rooftops
  • Creating city-scale experiences
  • Building visualization applications

Example:

// Place a virtual antenna on a building roof
await arSceneNavigator.createRooftopAnchor(
  37.7749,
  -122.4194,
  5.0 // 5 meters above the rooftop
);

Best Practices

1. Check Support Before Using

useEffect(() => {
  const init = async () => {
    const support = await arSceneNavigator.isGeospatialModeSupported();
    if (!support.supported) {
      Alert.alert(
        "Not Supported",
        "Geospatial AR is not available on this device"
      );
      return;
    }
    arSceneNavigator.setGeospatialModeEnabled(true);
  };
  init();
}, []);

2. Wait for Good Tracking Quality

const waitForGoodTracking = async (): Promise<ViroGeospatialPose | null> => {
  const MAX_ATTEMPTS = 30;
  const REQUIRED_ACCURACY = 5; // meters

  for (let i = 0; i < MAX_ATTEMPTS; i++) {
    const result = await arSceneNavigator.getCameraGeospatialPose();

    if (result.success && result.pose) {
      const { horizontalAccuracy, verticalAccuracy } = result.pose;

      if (
        horizontalAccuracy < REQUIRED_ACCURACY &&
        verticalAccuracy < REQUIRED_ACCURACY
      ) {
        return result.pose;
      }
    }

    await new Promise((resolve) => setTimeout(resolve, 500));
  }

  return null;
};

3. Use Terrain Anchors for Unknown Altitudes

When you don't know the exact altitude, use terrain anchors:

// DON'T do this (unknown altitude)
await arSceneNavigator.createGeospatialAnchor(lat, lng, 0);

// DO this instead
await arSceneNavigator.createTerrainAnchor(lat, lng, 1.5);

4. Handle Accuracy Indicators

Show users the current accuracy to set expectations:

const AccuracyIndicator = ({ pose }: { pose: ViroGeospatialPose }) => {
  const getQuality = () => {
    if (pose.horizontalAccuracy < 1) return "Excellent";
    if (pose.horizontalAccuracy < 3) return "Good";
    if (pose.horizontalAccuracy < 10) return "Fair";
    return "Poor";
  };

  return (
    <View style={styles.indicator}>
      <Text>Accuracy: {getQuality()}</Text>
      <Text>±{pose.horizontalAccuracy.toFixed(1)}m</Text>
    </View>
  );
};

5. Check VPS Availability for Best Results

const placeAnchor = async (lat: number, lng: number) => {
  const vps = await arSceneNavigator.checkVPSAvailability(lat, lng);

  if (vps.availability !== "Available") {
    // Warn user about reduced accuracy
    Alert.alert(
      "Limited Accuracy",
      "Visual positioning is not available here. Anchor placement may be less accurate."
    );
  }

  // Proceed with anchor creation
  await arSceneNavigator.createTerrainAnchor(lat, lng, 1.0);
};

6. Clean Up Anchors

Remove anchors when no longer needed:

const [anchors, setAnchors] = useState<string[]>([]);

const createAnchor = async () => {
  const result = await arSceneNavigator.createTerrainAnchor(lat, lng, 1.0);
  if (result.success) {
    setAnchors((prev) => [...prev, result.anchor.anchorId]);
  }
};

const cleanup = () => {
  anchors.forEach((id) => arSceneNavigator.removeGeospatialAnchor(id));
  setAnchors([]);
};

useEffect(() => {
  return () => cleanup();
}, []);

Troubleshooting

"Geospatial not supported"

  • Ensure device has ARCore/ARKit support
  • Check that location permissions are granted
  • Verify internet connectivity
  • Confirm API key is correctly configured

Poor accuracy / anchors drifting

  • Move the device slowly to allow tracking to stabilize
  • Ensure good lighting conditions
  • Try to capture more visual features (textured surfaces)
  • Check if VPS is available at the location
  • Wait for accuracy values to improve before placing anchors

"API key not found" errors

iOS: Verify GARAPIKey is in your Info.plist

Android: Verify com.google.android.ar.API_KEY meta-data is in AndroidManifest.xml

Earth tracking state stuck on "Stopped"

  • Ensure setGeospatialModeEnabled(true) was called
  • Check that geospatialAnchorProvider="arcore" prop is set
  • Verify location services are enabled on the device
  • Grant location permissions to the app

Terrain/Rooftop anchors failing

  • These anchor types require additional data from Google's servers
  • Ensure internet connectivity
  • The location may not have terrain/rooftop data available
  • Try using WGS84 anchors as a fallback

Android-specific issues

  • Ensure ARCore is installed and up to date
  • Check that your app targets API level 24 or higher
  • Verify the ARCore dependency version matches your setup

iOS-specific issues

  • Ensure iOS 12.0 or later
  • Verify ARCore SDK is properly linked via CocoaPods
  • Check that the app has camera and location permissions in Info.plist