Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-for-forked-repos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:
name: "CI requiring tokens"
uses: ./.github/workflows/ci-requiring-tokens.yml
with:
NVMRC: v20.14.0
NVMRC: v22.16.0
env_name: CI with Mapbox Tokens
ref: ${{ github.event.pull_request.head.sha }}
secrets:
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,10 @@ rnmapbox-maps.tgz

# generated by bob
lib/

# Bot secrets and credentials
scripts/github-bot/.env
scripts/github-bot/*.env
BOT_SETUP.md
**/bot-credentials.json
**/*-token.txt
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
v20.14.0
v22.16.0

141 changes: 141 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

This is the React Native Mapbox Maps SDK (@rnmapbox/maps) - a community-supported library for building maps with Mapbox Maps SDK for iOS and Android in React Native applications.

## Prerequisites

- **Node.js**: This project requires Node.js v22.16.0 (see .nvmrc)
- The project has been updated to use Node.js 22 with the new import syntax
- Use `nvm use` to switch to the correct version

## Common Development Commands

### Building and Running
```bash
# Install dependencies
yarn install

# Run the example app
cd example
yarn ios # iOS: runs on iPhone SE (3rd generation) simulator
yarn android # Android

# Web development (experimental)
yarn web # or: npx expo start -c --web

# Install iOS pods
yarn pod:install
```

### Testing and Quality
```bash
# Run tests
yarn test # Runs lint and unit tests
yarn unittest # Just unit tests
yarn unittest:single "test name" # Run specific test

# Code quality
yarn lint # ESLint check
yarn lint:fix # Auto-fix ESLint issues
yarn type:check # TypeScript type checking

# In example app
cd example && yarn type:check
```

### Code Generation
```bash
# IMPORTANT: Run after making changes to components or style properties
yarn generate

# This updates:
# - TypeScript definitions from style-spec
# - iOS/Android native style setters
# - Component documentation
# - Codepart replacements
```

### Building for Different Configurations

#### Mapbox v11 (Beta)
```bash
# iOS
cd example/ios
RNMBX11=1 pod update MapboxMaps

# Android
# Edit example/android/gradle.properties: RNMBX11=true
```

#### New Architecture/Fabric
```bash
# iOS
cd example/ios
RCT_NEW_ARCH_ENABLED=1 pod update MapboxMaps

# Android
# Edit example/android/gradle.properties: newArchEnabled=true
```

## Architecture Overview

### Component Structure
- **Components** (`src/components/`): React Native components that wrap native Mapbox functionality
- Layer components: `BackgroundLayer`, `CircleLayer`, `FillLayer`, `LineLayer`, etc.
- Source components: `VectorSource`, `ShapeSource`, `RasterSource`, etc.
- Core components: `MapView`, `Camera`, `UserLocation`, `MarkerView`, `PointAnnotation`
- Each component extends either `AbstractLayer` or `AbstractSource` for common functionality

### Native Bridge
- **Specs** (`src/specs/`): TurboModule/Fabric component specs for new architecture
- **Native Components**: Each component has corresponding native implementations:
- iOS: `ios/RNMBX/RNMBX*.swift` and `RNMBX*ComponentView.mm`
- Android: `android/src/main/java/` (generated from specs)

### Module Organization
- **location**: Location management and custom location providers
- **offline**: Offline map pack management and tile store
- **snapshot**: Map snapshot generation

### Style System
- Styles are defined in `style-spec/v8.json` (Mapbox style specification)
- TypeScript definitions generated in `utils/MapboxStyles.d.ts`
- Native style setters generated for iOS/Android

## Key Development Patterns

### Adding/Modifying Components
1. Update TypeScript component in `src/components/`
2. Update or create specs in `src/specs/` if needed
3. Run `yarn generate` to update generated code
4. Implement native changes if required
5. Add example in `example/src/examples/`
6. Update tests in `__tests__/`

### Working with Styles
- Layer styles use the Mapbox Style Specification
- Style props are validated and converted through `StyleValue` utilities
- Dynamic styles can use expressions and data-driven styling

### Testing Approach
- Unit tests use Jest with React Native preset
- Components are tested with mocked native modules
- Example app serves as integration testing ground
- Use `yarn test` before committing

### Documentation
- Component docs are auto-generated from JSDoc comments
- Don't edit `.md` files in `docs/` directly - edit source files and run `yarn generate`
- Examples in `example/src/examples/` are used for documentation

## Important Notes

- Always run `yarn generate` after modifying components or styles
- The example app is the primary way to test changes
- Native changes require rebuilding the app
- Web support is experimental and may have limited functionality
- Support both old and new React Native architectures
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
def defaultMapboxMapsImpl = "mapbox"
def defaultMapboxMapsVersion = "10.18.4"
def defaultMapboxMapsVersion = "10.19.0"

def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class RNMBXMarkerView(context: Context?, private val mManager: RNMBXMarkerViewMa
allowOverlapWithPuck(mAllowOverlapWithPuck)
offsets(offset.dx, offset.dy)
selected(mIsSelected)

ignoreCameraPadding(true)
}
return options
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import com.rnmapbox.rnmbx.components.mapview.RNMBXMapView
import com.rnmapbox.rnmbx.v11compat.annotation.*
import com.rnmapbox.rnmbx.utils.LatLng
import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toGNPointGeometry
import com.rnmapbox.rnmbx.utils.Logger

class RNMBXMarkerViewManager(reactApplicationContext: ReactApplicationContext) :
AbstractEventEmitter<RNMBXMarkerView>(reactApplicationContext),
Expand All @@ -39,12 +40,21 @@ class RNMBXMarkerViewManager(reactApplicationContext: ReactApplicationContext) :
@ReactProp(name = "coordinate")
override fun setCoordinate(markerView: RNMBXMarkerView, value: Dynamic) {
val array = value.asArray()
if (array == null) {
Logger.e("RNMBXMarkerViewManager", "array in setCoordinate is null")
return
}
markerView.setCoordinate(toGNPointGeometry(LatLng(array.getDouble(1), array.getDouble(0))))
}

@ReactProp(name = "anchor")
override fun setAnchor(markerView: RNMBXMarkerView, map: Dynamic) {
markerView.setAnchor(map.asMap().getDouble("x").toFloat(), map.asMap().getDouble("y").toFloat())
val mapValue = map.asMap()
if (mapValue == null) {
Logger.e("RNMBXMarkerViewManager", "map in setAnchor is null")
return
}
markerView.setAnchor(mapValue.getDouble("x").toFloat(), mapValue.getDouble("y").toFloat())
}

@ReactProp(name = "allowOverlap")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.rnmapbox.rnmbx.events.constants.EventKeys
import com.rnmapbox.rnmbx.events.constants.eventMapOf
import com.rnmapbox.rnmbx.utils.GeoJSONUtils.toPointGeometry
import com.rnmapbox.rnmbx.utils.ViewTagResolver
import com.rnmapbox.rnmbx.utils.Logger

class RNMBXPointAnnotationManager(reactApplicationContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) : AbstractEventEmitter<RNMBXPointAnnotation>(reactApplicationContext),
RNMBXPointAnnotationManagerInterface<RNMBXPointAnnotation> {
Expand Down Expand Up @@ -69,7 +70,12 @@ class RNMBXPointAnnotationManager(reactApplicationContext: ReactApplicationConte

@ReactProp(name = "anchor")
override fun setAnchor(annotation: RNMBXPointAnnotation, map: Dynamic) {
annotation.setAnchor(map.asMap().getDouble("x").toFloat(), map.asMap().getDouble("y").toFloat())
val mapValue = map.asMap()
if (mapValue == null) {
Logger.e("RNMBXPointAnnotationManager", "anchor map is null")
return
}
annotation.setAnchor(mapValue.getDouble("x").toFloat(), mapValue.getDouble("y").toFloat())
}

@ReactProp(name = "draggable")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.rnmapbox.rnmbx.utils.extensions.asBooleanOrNull
import com.rnmapbox.rnmbx.utils.extensions.asDoubleOrNull
import com.rnmapbox.rnmbx.utils.extensions.asStringOrNull
import com.rnmapbox.rnmbx.rncompat.dynamic.*
import com.rnmapbox.rnmbx.utils.Logger

class RNMBXCameraManager(private val mContext: ReactApplicationContext, val viewTagResolver: ViewTagResolver) :
AbstractEventEmitter<RNMBXCamera>(
Expand All @@ -34,15 +35,25 @@ class RNMBXCameraManager(private val mContext: ReactApplicationContext, val view
@ReactProp(name = "stop")
override fun setStop(camera: RNMBXCamera, map: Dynamic) {
if (!map.isNull) {
val stop = fromReadableMap(mContext, map.asMap(), null)
val mapValue = map.asMap()
if (mapValue == null) {
Logger.e("RNMBXCameraManager", "stop map is null")
return
}
val stop = fromReadableMap(mContext, mapValue, null)
camera.setStop(stop)
}
}

@ReactProp(name = "defaultStop")
override fun setDefaultStop(camera: RNMBXCamera, map: Dynamic) {
if (!map.isNull) {
val stop = fromReadableMap(mContext, map.asMap(), null)
val mapValue = map.asMap()
if (mapValue == null) {
Logger.e("RNMBXCameraManager", "defaultStop map is null")
return
}
val stop = fromReadableMap(mContext, mapValue, null)
camera.setDefaultStop(stop)
}
}
Expand Down Expand Up @@ -95,13 +106,23 @@ class RNMBXCameraManager(private val mContext: ReactApplicationContext, val view

@ReactProp(name = "followPadding")
override fun setFollowPadding(camera: RNMBXCamera, value: Dynamic) {
camera.setFollowPadding(value.asMap())
val mapValue = value.asMap()
if (mapValue == null) {
Logger.e("RNMBXCameraManager", "followPadding map is null")
return
}
camera.setFollowPadding(mapValue)
}

@ReactProp(name = "maxBounds")
override fun setMaxBounds(camera: RNMBXCamera, value: Dynamic) {
if (!value.isNull) {
val collection = FeatureCollection.fromJson(value.asString())
val stringValue = value.asString()
if (stringValue == null) {
Logger.e("RNMBXCameraManager", "maxBounds string is null")
return
}
val collection = FeatureCollection.fromJson(stringValue)
camera.setMaxBounds(toLatLngBounds(collection))
} else {
camera.setMaxBounds(null)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
package com.rnmapbox.rnmbx.components.camera

import com.facebook.react.bridge.Callback
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.LinearInterpolator
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReadableArray
import com.facebook.react.bridge.ReadableMap
import com.facebook.react.bridge.WritableMap
import com.facebook.react.bridge.WritableNativeMap
import com.mapbox.maps.ScreenCoordinate
import com.mapbox.maps.plugin.animation.MapAnimationOptions
import com.mapbox.maps.plugin.animation.easeTo
import com.mapbox.maps.plugin.animation.moveBy
import com.mapbox.maps.plugin.animation.scaleBy
import com.mapbox.maps.toCameraOptions
import com.rnmapbox.rnmbx.NativeRNMBXCameraModuleSpec
import com.rnmapbox.rnmbx.components.camera.constants.CameraMode
import com.rnmapbox.rnmbx.components.mapview.CommandResponse
import com.rnmapbox.rnmbx.utils.ViewRefTag
import com.rnmapbox.rnmbx.utils.ViewTagResolver
Expand Down Expand Up @@ -49,4 +56,60 @@ class RNMBXCameraModule(context: ReactApplicationContext, val viewTagResolver: V
promise.resolve(null)
}
}

private fun getAnimationOptions(
animationMode: Double,
animationDuration: Double
): MapAnimationOptions {
return MapAnimationOptions.Builder()
.apply {
when (animationMode.toInt()) {
CameraMode.LINEAR -> interpolator(LinearInterpolator())
CameraMode.EASE -> interpolator(AccelerateDecelerateInterpolator())
}
animationDuration.let { duration ->
duration(duration.toLong())
}
}
.build()
}

override fun moveBy(
viewRef: ViewRefTag?,
x: Double,
y: Double,
animationMode: Double,
animationDuration: Double,
promise: Promise
) {
withViewportOnUIThread(viewRef, promise) {
it.mapboxMap?.let { map ->
val animationOptions = getAnimationOptions(animationMode, animationDuration)
map.moveBy(ScreenCoordinate(x, y), animationOptions)

promise.resolve(null)
}
}
}

override fun scaleBy(
viewRef: ViewRefTag?,
x: Double,
y: Double,
animationMode: Double,
animationDuration: Double,
scaleFactor: Double,
promise: Promise
) {
withViewportOnUIThread(viewRef, promise) {
it.mapboxMap?.let { map ->
val animationOptions =
getAnimationOptions(animationMode, animationDuration)

map.scaleBy(scaleFactor, ScreenCoordinate(x, y), animationOptions)

promise.resolve(null)
}
}
}
}
Loading
Loading