├── .watchmanconfig ├── .husky ├── .npmignore ├── pre-commit └── commit-msg ├── .nvmrc ├── example ├── .watchmanconfig ├── jest.config.js ├── .bundle │ └── config ├── app.json ├── android │ ├── app │ │ ├── debug.keystore │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── strings.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ │ └── ic_launcher_round.png │ │ │ │ │ └── drawable │ │ │ │ │ │ └── rn_edit_text_material.xml │ │ │ │ ├── java │ │ │ │ │ └── clusterer │ │ │ │ │ │ └── example │ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ │ └── MainApplication.kt │ │ │ │ └── AndroidManifest.xml │ │ │ └── debug │ │ │ │ └── AndroidManifest.xml │ │ ├── proguard-rules.pro │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── settings.gradle │ ├── build.gradle │ ├── gradle.properties │ ├── gradlew.bat │ └── gradlew ├── ios │ ├── ClustererExample │ │ ├── Images.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── PrivacyInfo.xcprivacy │ │ ├── AppDelegate.swift │ │ ├── Info.plist │ │ └── LaunchScreen.storyboard │ ├── ClustererExample.xcworkspace │ │ └── contents.xcworkspacedata │ ├── .xcode.env │ ├── Podfile │ └── ClustererExample.xcodeproj │ │ └── xcshareddata │ │ └── xcschemes │ │ └── ClustererExample.xcscheme ├── index.js ├── babel.config.js ├── src │ ├── test │ │ └── fixtures │ │ │ ├── index.ts │ │ │ └── places-z0-0-0.json │ ├── places.ts │ ├── App.tsx │ ├── Point.tsx │ ├── utils.ts │ ├── Map.tsx │ ├── Comparison.tsx │ ├── GetTile.tsx │ ├── GetClusters.tsx │ └── Tests.tsx ├── metro.config.js ├── react-native.config.js ├── Gemfile ├── package.json ├── Gemfile.lock └── README.md ├── nitrogen └── generated │ ├── .gitattributes │ ├── ios │ ├── ClustererAutolinking.swift │ ├── Clusterer-Swift-Cxx-Bridge.cpp │ ├── Clusterer-Swift-Cxx-Bridge.hpp │ ├── ClustererAutolinking.mm │ ├── Clusterer-Swift-Cxx-Umbrella.hpp │ └── Clusterer+autolinking.rb │ ├── shared │ └── c++ │ │ ├── HybridClustererSpec.cpp │ │ └── HybridClustererSpec.hpp │ └── android │ ├── clusterer+autolinking.gradle │ ├── clustererOnLoad.hpp │ ├── kotlin │ └── com │ │ └── margelo │ │ └── nitro │ │ └── clusterer │ │ └── clustererOnLoad.kt │ ├── clustererOnLoad.cpp │ └── clusterer+autolinking.cmake ├── .gitattributes ├── tsconfig.build.json ├── android ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ ├── cpp │ │ └── cpp-adapter.cpp │ │ └── java │ │ └── com │ │ └── margelo │ │ └── nitro │ │ └── clusterer │ │ └── ClustererPackage.kt ├── gradle.properties ├── CMakeLists.txt └── build.gradle ├── src ├── Clusterer.nitro.ts ├── index.tsx ├── Clusterer.tsx ├── useClusterer.ts ├── utils.ts ├── types.ts └── Supercluster.ts ├── .clang-format ├── babel.config.js ├── lefthook.yml ├── .editorconfig ├── .yarnrc.yml ├── nitro.json ├── Clusterer.podspec ├── eslint.config.mjs ├── tsconfig.json ├── LICENSE ├── turbo.json ├── cpp ├── clustererJSIHelpers.hpp ├── HybridClusterer.hpp ├── HybridClusterer.cpp └── clustererJSIHelpers.cpp ├── .gitignore ├── package.json ├── CONTRIBUTING.md └── README.md /.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.husky/.npmignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v22.20.0 2 | -------------------------------------------------------------------------------- /example/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /nitrogen/generated/.gitattributes: -------------------------------------------------------------------------------- 1 | ** linguist-generated=true 2 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'react-native', 3 | }; 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.pbxproj -text 2 | # specific for windows script files 3 | *.bat text eol=crlf -------------------------------------------------------------------------------- /example/.bundle/config: -------------------------------------------------------------------------------- 1 | BUNDLE_PATH: "vendor/bundle" 2 | BUNDLE_FORCE_RUBY_PLATFORM: 1 3 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["example", "lib"] 4 | } 5 | -------------------------------------------------------------------------------- /example/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ClustererExample", 3 | "displayName": "ClustererExample" 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint && yarn typescript 5 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint -E HUSKY_GIT_PARAMS 5 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /example/android/app/debug.keystore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/debug.keystore -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ClustererExample 3 | 4 | -------------------------------------------------------------------------------- /example/ios/ClustererExample/Images.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/Clusterer.nitro.ts: -------------------------------------------------------------------------------- 1 | import type { HybridObject } from 'react-native-nitro-modules'; 2 | 3 | export interface Clusterer 4 | extends HybridObject<{ ios: 'c++'; android: 'c++' }> {} 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | Clusterer_kotlinVersion=2.0.21 2 | Clusterer_minSdkVersion=24 3 | Clusterer_targetSdkVersion=34 4 | Clusterer_compileSdkVersion=35 5 | Clusterer_ndkVersion=27.1.12297006 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | import { AppRegistry } from 'react-native'; 2 | import App from './src/App'; 3 | import { name as appName } from './app.json'; 4 | 5 | AppRegistry.registerComponent(appName, () => App); 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JiriHoffmann/react-native-clusterer/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /android/src/main/cpp/cpp-adapter.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "clustererOnLoad.hpp" 3 | 4 | JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { 5 | return margelo::nitro::clusterer::initialize(vm); 6 | } 7 | -------------------------------------------------------------------------------- /.clang-format: -------------------------------------------------------------------------------- 1 | Language: Cpp 2 | BasedOnStyle: Google 3 | IndentWidth: 2 4 | MaxEmptyLinesToKeep: 1 5 | NamespaceIndentation: None 6 | ColumnLimit: 80 7 | PenaltyBreakAssignment: 2 8 | PenaltyReturnTypeOnItsOwnLine: 200 9 | PointerAlignment: Left 10 | SpaceBeforeParens: Never 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | exclude: /\/node_modules\//, 5 | presets: ['module:react-native-builder-bob/babel-preset'], 6 | }, 7 | { 8 | include: /\/node_modules\//, 9 | presets: ['module:@react-native/babel-preset'], 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /example/ios/ClustererExample.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | parallel: true 3 | commands: 4 | lint: 5 | glob: "*.{js,ts,jsx,tsx}" 6 | run: npx eslint {staged_files} 7 | types: 8 | glob: "*.{js,ts, jsx, tsx}" 9 | run: npx tsc 10 | commit-msg: 11 | parallel: true 12 | commands: 13 | commitlint: 14 | run: npx commitlint --edit 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 2 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | nmHoistingLimits: workspaces 3 | 4 | plugins: 5 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 6 | spec: "@yarnpkg/plugin-interactive-tools" 7 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 8 | spec: "@yarnpkg/plugin-workspace-tools" 9 | 10 | yarnPath: .yarn/releases/yarn-3.6.1.cjs 11 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getConfig } = require('react-native-builder-bob/babel-config'); 3 | const pkg = require('../package.json'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | module.exports = getConfig( 8 | { 9 | presets: ['module:@react-native/babel-preset'], 10 | }, 11 | { root, pkg } 12 | ); 13 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { includeBuild("../node_modules/@react-native/gradle-plugin") } 2 | plugins { id("com.facebook.react.settings") } 3 | extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() } 4 | rootProject.name = 'clusterer.example' 5 | include ':app' 6 | includeBuild('../node_modules/@react-native/gradle-plugin') 7 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /nitro.json: -------------------------------------------------------------------------------- 1 | { 2 | "cxxNamespace": ["clusterer"], 3 | "ios": { 4 | "iosModuleName": "Clusterer" 5 | }, 6 | "android": { 7 | "androidNamespace": ["clusterer"], 8 | "androidCxxLibName": "clusterer" 9 | }, 10 | "autolinking": { 11 | "Clusterer": { 12 | "cpp": "HybridClusterer" 13 | } 14 | }, 15 | "ignorePaths": ["node_modules", "example", "lib"] 16 | } 17 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/ClustererAutolinking.swift: -------------------------------------------------------------------------------- 1 | /// 2 | /// ClustererAutolinking.swift 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | public final class ClustererAutolinking { 9 | public typealias bridge = margelo.nitro.clusterer.bridge.swift 10 | 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { Clusterer } from './Clusterer'; 2 | import SuperclusterClass from './Supercluster'; 3 | import type { Supercluster } from './types'; 4 | import { useClusterer } from './useClusterer'; 5 | import { coordsToGeoJSONFeature, isClusterFeature } from './utils'; 6 | 7 | export type { Supercluster }; 8 | export { Clusterer, useClusterer, isClusterFeature, coordsToGeoJSONFeature }; 9 | 10 | export default SuperclusterClass; 11 | -------------------------------------------------------------------------------- /example/src/test/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | import placesJSON from './places.json'; 2 | import placesTile from './places-z0-0-0.json'; 3 | import placesTileMin5 from './places-z0-0-0-min5.json'; 4 | import type { Supercluster } from 'react-native-clusterer'; 5 | 6 | type Places = { 7 | type: 'FeatureCollection'; 8 | features: Supercluster.PointFeature[]; 9 | }; 10 | 11 | const places = placesJSON as Places; 12 | 13 | export { places, placesTile, placesTileMin5 }; 14 | -------------------------------------------------------------------------------- /example/android/app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/Clusterer-Swift-Cxx-Bridge.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// Clusterer-Swift-Cxx-Bridge.cpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #include "Clusterer-Swift-Cxx-Bridge.hpp" 9 | 10 | // Include C++ implementation defined types 11 | 12 | 13 | namespace margelo::nitro::clusterer::bridge::swift { 14 | 15 | 16 | 17 | } // namespace margelo::nitro::clusterer::bridge::swift 18 | -------------------------------------------------------------------------------- /example/metro.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const { getDefaultConfig } = require('@react-native/metro-config'); 3 | const { withMetroConfig } = require('react-native-monorepo-config'); 4 | 5 | const root = path.resolve(__dirname, '..'); 6 | 7 | /** 8 | * Metro configuration 9 | * https://facebook.github.io/metro/docs/configuration 10 | * 11 | * @type {import('metro-config').MetroConfig} 12 | */ 13 | module.exports = withMetroConfig(getDefaultConfig(__dirname), { 14 | root, 15 | dirname: __dirname, 16 | }); 17 | -------------------------------------------------------------------------------- /example/ios/.xcode.env: -------------------------------------------------------------------------------- 1 | # This `.xcode.env` file is versioned and is used to source the environment 2 | # used when running script phases inside Xcode. 3 | # To customize your local environment, you can create an `.xcode.env.local` 4 | # file that is not versioned. 5 | 6 | # NODE_BINARY variable contains the PATH to the node executable. 7 | # 8 | # Customize the NODE_BINARY variable here. 9 | # For example, to use nvm with brew, add the following line 10 | # . "$(brew --prefix nvm)/nvm.sh" --no-use 11 | export NODE_BINARY=$(command -v node) 12 | -------------------------------------------------------------------------------- /example/react-native.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('../package.json'); 3 | 4 | module.exports = { 5 | project: { 6 | ios: { 7 | automaticPodsInstallation: true, 8 | }, 9 | }, 10 | dependencies: { 11 | [pkg.name]: { 12 | root: path.join(__dirname, '..'), 13 | platforms: { 14 | // Codegen script incorrectly fails without this 15 | // So we explicitly specify the platforms with empty object 16 | ios: {}, 17 | android: {}, 18 | }, 19 | }, 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /example/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # You may use http://rbenv.org/ or https://rvm.io/ to install and use this version 4 | ruby ">= 2.6.10" 5 | 6 | # Exclude problematic versions of cocoapods and activesupport that causes build failures. 7 | gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1' 8 | gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0' 9 | gem 'xcodeproj', '< 1.26.0' 10 | gem 'concurrent-ruby', '< 1.3.4' 11 | 12 | # Ruby 3.4.0 has removed some libraries from the standard library. 13 | gem 'bigdecimal' 14 | gem 'logger' 15 | gem 'benchmark' 16 | gem 'mutex_m' 17 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | buildToolsVersion = "36.0.0" 4 | minSdkVersion = 24 5 | compileSdkVersion = 36 6 | targetSdkVersion = 36 7 | ndkVersion = "27.1.12297006" 8 | kotlinVersion = "2.1.20" 9 | } 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | dependencies { 15 | classpath("com.android.tools.build:gradle") 16 | classpath("com.facebook.react:react-native-gradle-plugin") 17 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") 18 | } 19 | } 20 | 21 | apply plugin: "com.facebook.react.rootproject" 22 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/HybridClustererSpec.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// HybridClustererSpec.cpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #include "HybridClustererSpec.hpp" 9 | 10 | namespace margelo::nitro::clusterer { 11 | 12 | void HybridClustererSpec::loadHybridMethods() { 13 | // load base methods/properties 14 | HybridObject::loadHybridMethods(); 15 | // load custom methods/properties 16 | registerHybrids(this, [](Prototype& prototype) { 17 | 18 | }); 19 | } 20 | 21 | } // namespace margelo::nitro::clusterer 22 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/Clusterer-Swift-Cxx-Bridge.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// Clusterer-Swift-Cxx-Bridge.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | // Forward declarations of C++ defined types 11 | 12 | 13 | // Forward declarations of Swift defined types 14 | 15 | 16 | // Include C++ defined types 17 | 18 | 19 | /** 20 | * Contains specialized versions of C++ templated types so they can be accessed from Swift, 21 | * as well as helper functions to interact with those C++ types from Swift. 22 | */ 23 | namespace margelo::nitro::clusterer::bridge::swift { 24 | 25 | 26 | 27 | } // namespace margelo::nitro::clusterer::bridge::swift 28 | -------------------------------------------------------------------------------- /android/src/main/java/com/margelo/nitro/clusterer/ClustererPackage.kt: -------------------------------------------------------------------------------- 1 | package com.margelo.nitro.clusterer 2 | 3 | import com.facebook.react.TurboReactPackage 4 | import com.facebook.react.bridge.NativeModule 5 | import com.facebook.react.bridge.ReactApplicationContext 6 | import com.facebook.react.module.model.ReactModuleInfoProvider 7 | 8 | class ClustererPackage : TurboReactPackage() { 9 | override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { 10 | return null 11 | } 12 | 13 | override fun getReactModuleInfoProvider(): ReactModuleInfoProvider { 14 | return ReactModuleInfoProvider { HashMap() } 15 | } 16 | 17 | companion object { 18 | init { 19 | System.loadLibrary("clusterer") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /android/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | project(clusterer) 2 | cmake_minimum_required(VERSION 3.9.0) 3 | 4 | set(PACKAGE_NAME clusterer) 5 | set(CMAKE_VERBOSE_MAKEFILE ON) 6 | set(CMAKE_CXX_STANDARD 20) 7 | 8 | file(GLOB_RECURSE cpp_files RELATIVE ${CMAKE_SOURCE_DIR} 9 | "../cpp/**.cpp" 10 | "../cpp/**.hpp" 11 | "cpp-adapter.cpp" 12 | ) 13 | 14 | # Define C++ library and add all sources 15 | add_library(${PACKAGE_NAME} SHARED ${cpp_files}) 16 | 17 | # Add Nitrogen specs :) 18 | include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/clusterer+autolinking.cmake) 19 | 20 | # Set up local includes 21 | include_directories("src/main/cpp" "../cpp") 22 | 23 | find_library(LOG_LIB log) 24 | 25 | # Link all libraries together 26 | target_link_libraries( 27 | ${PACKAGE_NAME} 28 | ${LOG_LIB} 29 | android # <-- Android core 30 | ) 31 | -------------------------------------------------------------------------------- /nitrogen/generated/android/clusterer+autolinking.gradle: -------------------------------------------------------------------------------- 1 | /// 2 | /// clusterer+autolinking.gradle 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | /// This is a Gradle file that adds all files generated by Nitrogen 9 | /// to the current Gradle project. 10 | /// 11 | /// To use it, add this to your build.gradle: 12 | /// ```gradle 13 | /// apply from: '../nitrogen/generated/android/clusterer+autolinking.gradle' 14 | /// ``` 15 | 16 | logger.warn("[NitroModules] 🔥 clusterer is boosted by nitro!") 17 | 18 | android { 19 | sourceSets { 20 | main { 21 | java.srcDirs += [ 22 | // Nitrogen files 23 | "${project.projectDir}/../nitrogen/generated/android/kotlin" 24 | ] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /nitrogen/generated/android/clustererOnLoad.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// clustererOnLoad.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #include 9 | #include 10 | 11 | namespace margelo::nitro::clusterer { 12 | 13 | /** 14 | * Initializes the native (C++) part of clusterer, and autolinks all Hybrid Objects. 15 | * Call this in your `JNI_OnLoad` function (probably inside `cpp-adapter.cpp`). 16 | * Example: 17 | * ```cpp (cpp-adapter.cpp) 18 | * JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { 19 | * return margelo::nitro::clusterer::initialize(vm); 20 | * } 21 | * ``` 22 | */ 23 | int initialize(JavaVM* vm); 24 | 25 | } // namespace margelo::nitro::clusterer 26 | -------------------------------------------------------------------------------- /Clusterer.podspec: -------------------------------------------------------------------------------- 1 | require "json" 2 | 3 | package = JSON.parse(File.read(File.join(__dir__, "package.json"))) 4 | 5 | Pod::Spec.new do |s| 6 | s.name = "Clusterer" 7 | s.version = package["version"] 8 | s.summary = package["description"] 9 | s.homepage = package["homepage"] 10 | s.license = package["license"] 11 | s.authors = package["author"] 12 | 13 | s.platforms = { :ios => min_ios_version_supported } 14 | s.source = { :git => "https://github.com/JiriHoffmann/react-native-clusterer.git", :tag => "#{s.version}" } 15 | 16 | 17 | s.source_files = [ 18 | "ios/**/*.{swift}", 19 | "ios/**/*.{m,mm}", 20 | "cpp/**/*.{hpp,cpp}", 21 | ] 22 | 23 | s.dependency 'React-jsi' 24 | s.dependency 'React-callinvoker' 25 | 26 | load 'nitrogen/generated/ios/Clusterer+autolinking.rb' 27 | add_nitrogen_files(s) 28 | 29 | install_modules_dependencies(s) 30 | end 31 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { fixupConfigRules } from '@eslint/compat'; 2 | import { FlatCompat } from '@eslint/eslintrc'; 3 | import js from '@eslint/js'; 4 | import prettier from 'eslint-plugin-prettier'; 5 | import { defineConfig } from 'eslint/config'; 6 | import path from 'node:path'; 7 | import { fileURLToPath } from 'node:url'; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }); 16 | 17 | export default defineConfig([ 18 | { 19 | extends: fixupConfigRules(compat.extends('@react-native', 'prettier')), 20 | plugins: { prettier }, 21 | rules: { 22 | 'react/react-in-jsx-scope': 'off', 23 | 'prettier/prettier': 'error', 24 | }, 25 | }, 26 | { 27 | ignores: ['node_modules/', 'lib/'], 28 | }, 29 | ]); 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "paths": { 5 | "react-native-clusterer": ["./src/index"] 6 | }, 7 | "allowUnreachableCode": false, 8 | "allowUnusedLabels": false, 9 | "customConditions": ["react-native-strict-api"], 10 | "esModuleInterop": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "jsx": "react-jsx", 13 | "lib": ["ESNext"], 14 | "module": "ESNext", 15 | "moduleResolution": "bundler", 16 | "noEmit": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noImplicitReturns": true, 19 | "noImplicitUseStrict": false, 20 | "noStrictGenericChecks": false, 21 | "noUncheckedIndexedAccess": true, 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "resolveJsonModule": true, 25 | "skipLibCheck": true, 26 | "strict": true, 27 | "target": "ESNext", 28 | "verbatimModuleSyntax": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /example/src/places.ts: -------------------------------------------------------------------------------- 1 | import { places } from './test/fixtures'; 2 | 3 | export const initialRegion = { 4 | latitude: 17.150642213990213, 5 | latitudeDelta: 102.40413819692193, 6 | longitude: -90.13384625315666, 7 | longitudeDelta: 72.32146382331848, 8 | }; 9 | 10 | export const parsedPlacesData = places.features.map((f, i) => ({ 11 | ...f, 12 | properties: { ...f.properties, id: i }, 13 | })); 14 | 15 | export const getRandomNum = (min: number, max: number) => { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | }; 18 | 19 | export const getRandomData = (size: number | string) => { 20 | return Array.from({ length: parseInt(`${size}`, 10) }, (_, i) => { 21 | return { 22 | type: 'Feature' as const, 23 | geometry: { 24 | type: 'Point' as const, 25 | coordinates: [getRandomNum(-180, 180), getRandomNum(-90, 90)], 26 | }, 27 | properties: { 28 | id: `point-${i}`, 29 | }, 30 | }; 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/clusterer/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package clusterer.example 2 | 3 | import com.facebook.react.ReactActivity 4 | import com.facebook.react.ReactActivityDelegate 5 | import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled 6 | import com.facebook.react.defaults.DefaultReactActivityDelegate 7 | 8 | class MainActivity : ReactActivity() { 9 | 10 | /** 11 | * Returns the name of the main component registered from JavaScript. This is used to schedule 12 | * rendering of the component. 13 | */ 14 | override fun getMainComponentName(): String = "ClustererExample" 15 | 16 | /** 17 | * Returns the instance of the [ReactActivityDelegate]. We use [DefaultReactActivityDelegate] 18 | * which allows you to enable New Architecture with a single boolean flags [fabricEnabled] 19 | */ 20 | override fun createReactActivityDelegate(): ReactActivityDelegate = 21 | DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled) 22 | } 23 | -------------------------------------------------------------------------------- /src/Clusterer.tsx: -------------------------------------------------------------------------------- 1 | import React, { type ReactElement } from 'react'; 2 | import { useClusterer } from './useClusterer'; 3 | import type { Supercluster } from './types'; 4 | import type { MapDimensions, Region } from './types'; 5 | 6 | export interface ClustererProps< 7 | P extends Supercluster.AnyProps, 8 | C extends Supercluster.AnyProps, 9 | > { 10 | data: Array>; 11 | mapDimensions: MapDimensions; 12 | region: Region; 13 | renderItem: ( 14 | item: Supercluster.PointOrClusterFeature, 15 | index: number, 16 | array: Supercluster.PointOrClusterFeature[] 17 | ) => React.ReactElement; 18 | options?: Supercluster.Options; 19 | } 20 | 21 | export function Clusterer< 22 | P extends Supercluster.AnyProps, 23 | C extends Supercluster.AnyProps, 24 | >({ 25 | data, 26 | options, 27 | region, 28 | mapDimensions, 29 | renderItem, 30 | }: ClustererProps): ReactElement { 31 | const [points] = useClusterer(data, mapDimensions, region, options); 32 | 33 | return <>{points.map(renderItem)}; 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jiri Hoffmann 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | SOFTWARE. 21 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 13 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/ClustererExample/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ios-marketing", 45 | "scale" : "1x", 46 | "size" : "1024x1024" 47 | } 48 | ], 49 | "info" : { 50 | "author" : "xcode", 51 | "version" : 1 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": [".nvmrc", ".yarnrc.yml"], 4 | "globalEnv": ["NODE_ENV"], 5 | "tasks": { 6 | "build:android": { 7 | "env": ["ANDROID_HOME", "ORG_GRADLE_PROJECT_newArchEnabled"], 8 | "inputs": [ 9 | "package.json", 10 | "android", 11 | "!android/build", 12 | "src/*.ts", 13 | "src/*.tsx", 14 | "example/package.json", 15 | "example/android", 16 | "!example/android/.gradle", 17 | "!example/android/build", 18 | "!example/android/app/build" 19 | ], 20 | "outputs": [] 21 | }, 22 | "build:ios": { 23 | "env": [ 24 | "RCT_NEW_ARCH_ENABLED", 25 | "RCT_USE_RN_DEP", 26 | "RCT_USE_PREBUILT_RNCORE" 27 | ], 28 | "inputs": [ 29 | "package.json", 30 | "*.podspec", 31 | "ios", 32 | "src/*.ts", 33 | "src/*.tsx", 34 | "example/package.json", 35 | "example/ios", 36 | "!example/ios/build", 37 | "!example/ios/Pods" 38 | ], 39 | "outputs": [] 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/ClustererAutolinking.mm: -------------------------------------------------------------------------------- 1 | /// 2 | /// ClustererAutolinking.mm 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #import 9 | #import 10 | 11 | #import 12 | 13 | #include "HybridClusterer.hpp" 14 | 15 | @interface ClustererAutolinking : NSObject 16 | @end 17 | 18 | @implementation ClustererAutolinking 19 | 20 | + (void) load { 21 | using namespace margelo::nitro; 22 | using namespace margelo::nitro::clusterer; 23 | 24 | HybridObjectRegistry::registerHybridObjectConstructor( 25 | "Clusterer", 26 | []() -> std::shared_ptr { 27 | static_assert(std::is_default_constructible_v, 28 | "The HybridObject \"HybridClusterer\" is not default-constructible! " 29 | "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); 30 | return std::make_shared(); 31 | } 32 | ); 33 | } 34 | 35 | @end 36 | -------------------------------------------------------------------------------- /example/ios/ClustererExample/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyAccessedAPITypes 6 | 7 | 8 | NSPrivacyAccessedAPIType 9 | NSPrivacyAccessedAPICategoryFileTimestamp 10 | NSPrivacyAccessedAPITypeReasons 11 | 12 | C617.1 13 | 14 | 15 | 16 | NSPrivacyAccessedAPIType 17 | NSPrivacyAccessedAPICategoryUserDefaults 18 | NSPrivacyAccessedAPITypeReasons 19 | 20 | CA92.1 21 | 22 | 23 | 24 | NSPrivacyAccessedAPIType 25 | NSPrivacyAccessedAPICategorySystemBootTime 26 | NSPrivacyAccessedAPITypeReasons 27 | 28 | 35F9.1 29 | 30 | 31 | 32 | NSPrivacyCollectedDataTypes 33 | 34 | NSPrivacyTracking 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /cpp/clustererJSIHelpers.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include 6 | 7 | #include "supercluster.hpp" 8 | 9 | using namespace std; 10 | using namespace facebook; 11 | 12 | namespace margelo::nitro::clusterer { 13 | 14 | void parseJSIOptions(jsi::Runtime &rt, mapbox::supercluster::Options &options, 15 | jsi::Value const &jsiOptions); 16 | 17 | void parseJSIFeature(jsi::Runtime &rt, int featureIndex, 18 | mapbox::feature::feature &feature, 19 | jsi::Value const &jsiFeature); 20 | 21 | void clusterToJSI(jsi::Runtime &rt, jsi::Object &jsiObject, 22 | mapbox::feature::feature &f, 23 | jsi::Array &featuresInput); 24 | 25 | void tileToJSI(jsi::Runtime &rt, jsi::Object &jsiObject, 26 | mapbox::feature::feature &f, 27 | jsi::Array &featuresInput); 28 | 29 | void featurePropertyToJSI(jsi::Runtime &rt, 30 | jsi::Object &jsiFeatureProperties, 31 | std::pair &itr, 32 | int &origFeatureIndex); 33 | 34 | } // namespace clusterer 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # XDE 6 | .expo/ 7 | 8 | # VSCode 9 | .vscode/ 10 | jsconfig.json 11 | 12 | # Xcode 13 | # 14 | build/ 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata 24 | *.xccheckout 25 | *.moved-aside 26 | DerivedData 27 | *.hmap 28 | *.ipa 29 | *.xcuserstate 30 | project.xcworkspace 31 | **/.xcode.env.local 32 | 33 | # Android/IJ 34 | # 35 | .classpath 36 | .cxx 37 | .gradle 38 | .idea 39 | .project 40 | .settings 41 | local.properties 42 | android.iml 43 | 44 | # Cocoapods 45 | # 46 | example/ios/Pods 47 | 48 | # Ruby 49 | example/vendor/ 50 | 51 | # node.js 52 | # 53 | node_modules/ 54 | npm-debug.log 55 | yarn-debug.log 56 | yarn-error.log 57 | 58 | # BUCK 59 | buck-out/ 60 | \.buckd/ 61 | android/app/libs 62 | android/keystores/debug.keystore 63 | 64 | # Yarn 65 | .yarn/* 66 | !.yarn/patches 67 | !.yarn/plugins 68 | !.yarn/releases 69 | !.yarn/sdks 70 | !.yarn/versions 71 | 72 | # Expo 73 | .expo/ 74 | 75 | # Turborepo 76 | .turbo/ 77 | 78 | # generated by bob 79 | lib/ 80 | 81 | # React Native Codegen 82 | ios/generated 83 | android/generated 84 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | ENV['RCT_NEW_ARCH_ENABLED'] = '1' 2 | 3 | # Resolve react_native_pods.rb with node to allow for hoisting 4 | require Pod::Executable.execute_command('node', ['-p', 5 | 'require.resolve( 6 | "react-native/scripts/react_native_pods.rb", 7 | {paths: [process.argv[1]]}, 8 | )', __dir__]).strip 9 | 10 | platform :ios, min_ios_version_supported 11 | prepare_react_native_project! 12 | 13 | linkage = ENV['USE_FRAMEWORKS'] 14 | if linkage != nil 15 | Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green 16 | use_frameworks! :linkage => linkage.to_sym 17 | end 18 | 19 | target 'ClustererExample' do 20 | config = use_native_modules! 21 | 22 | use_react_native!( 23 | :path => config[:reactNativePath], 24 | # An absolute path to your application root. 25 | :app_path => "#{Pod::Config.instance.installation_root}/.." 26 | ) 27 | 28 | post_install do |installer| 29 | # https://github.com/facebook/react-native/blob/main/packages/react-native/scripts/react_native_pods.rb#L197-L202 30 | react_native_post_install( 31 | installer, 32 | config[:reactNativePath], 33 | :mac_catalyst_enabled => false, 34 | # :ccache_enabled => true 35 | ) 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /nitrogen/generated/android/kotlin/com/margelo/nitro/clusterer/clustererOnLoad.kt: -------------------------------------------------------------------------------- 1 | /// 2 | /// clustererOnLoad.kt 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | package com.margelo.nitro.clusterer 9 | 10 | import android.util.Log 11 | 12 | internal class clustererOnLoad { 13 | companion object { 14 | private const val TAG = "clustererOnLoad" 15 | private var didLoad = false 16 | /** 17 | * Initializes the native part of "clusterer". 18 | * This method is idempotent and can be called more than once. 19 | */ 20 | @JvmStatic 21 | fun initializeNative() { 22 | if (didLoad) return 23 | try { 24 | Log.i(TAG, "Loading clusterer C++ library...") 25 | System.loadLibrary("clusterer") 26 | Log.i(TAG, "Successfully loaded clusterer C++ library!") 27 | didLoad = true 28 | } catch (e: Error) { 29 | Log.e(TAG, "Failed to load clusterer C++ library! Is it properly installed and linked? " + 30 | "Is the name correct? (see `CMakeLists.txt`, at `add_library(...)`)", e) 31 | throw e 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/Clusterer-Swift-Cxx-Umbrella.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// Clusterer-Swift-Cxx-Umbrella.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | // Forward declarations of C++ defined types 11 | 12 | 13 | // Include C++ defined types 14 | 15 | 16 | // C++ helpers for Swift 17 | #include "Clusterer-Swift-Cxx-Bridge.hpp" 18 | 19 | // Common C++ types used in Swift 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | // Forward declarations of Swift defined types 26 | 27 | 28 | // Include Swift defined types 29 | #if __has_include("Clusterer-Swift.h") 30 | // This header is generated by Xcode/Swift on every app build. 31 | // If it cannot be found, make sure the Swift module's name (= podspec name) is actually "Clusterer". 32 | #include "Clusterer-Swift.h" 33 | // Same as above, but used when building with frameworks (`use_frameworks`) 34 | #elif __has_include() 35 | #include 36 | #else 37 | #error Clusterer's autogenerated Swift header cannot be found! Make sure the Swift module's name (= podspec name) is actually "Clusterer", and try building the app first. 38 | #endif 39 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-clusterer-example", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "android": "react-native run-android", 7 | "ios": "react-native run-ios", 8 | "start": "react-native start", 9 | "build:android": "react-native build-android --extra-params \"--no-daemon --console=plain -PreactNativeArchitectures=arm64-v8a\"", 10 | "build:ios": "react-native build-ios --mode Debug" 11 | }, 12 | "dependencies": { 13 | "@react-native/new-app-screen": "0.81.4", 14 | "@types/supercluster": "^7.1.3", 15 | "react": "19.1.0", 16 | "react-native": "0.81.4", 17 | "react-native-maps": "^1.26.17", 18 | "react-native-nitro-modules": "^0.31.5", 19 | "supercluster": "^8.0.1" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.25.2", 23 | "@babel/preset-env": "^7.25.3", 24 | "@babel/runtime": "^7.25.0", 25 | "@react-native-community/cli": "20.0.2", 26 | "@react-native-community/cli-platform-android": "20.0.2", 27 | "@react-native-community/cli-platform-ios": "20.0.2", 28 | "@react-native/babel-preset": "0.81.4", 29 | "@react-native/metro-config": "0.81.4", 30 | "@react-native/typescript-config": "0.81.4", 31 | "@types/react": "^19.1.0", 32 | "react-native-builder-bob": "^0.40.13", 33 | "react-native-monorepo-config": "^0.1.9" 34 | }, 35 | "engines": { 36 | "node": ">=20" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/ios/ClustererExample/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import React 3 | import React_RCTAppDelegate 4 | import ReactAppDependencyProvider 5 | 6 | @main 7 | class AppDelegate: UIResponder, UIApplicationDelegate { 8 | var window: UIWindow? 9 | 10 | var reactNativeDelegate: ReactNativeDelegate? 11 | var reactNativeFactory: RCTReactNativeFactory? 12 | 13 | func application( 14 | _ application: UIApplication, 15 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil 16 | ) -> Bool { 17 | let delegate = ReactNativeDelegate() 18 | let factory = RCTReactNativeFactory(delegate: delegate) 19 | delegate.dependencyProvider = RCTAppDependencyProvider() 20 | 21 | reactNativeDelegate = delegate 22 | reactNativeFactory = factory 23 | 24 | window = UIWindow(frame: UIScreen.main.bounds) 25 | 26 | factory.startReactNative( 27 | withModuleName: "ClustererExample", 28 | in: window, 29 | launchOptions: launchOptions 30 | ) 31 | 32 | return true 33 | } 34 | } 35 | 36 | class ReactNativeDelegate: RCTDefaultReactNativeFactoryDelegate { 37 | override func sourceURL(for bridge: RCTBridge) -> URL? { 38 | self.bundleURL() 39 | } 40 | 41 | override func bundleURL() -> URL? { 42 | #if DEBUG 43 | RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index") 44 | #else 45 | Bundle.main.url(forResource: "main", withExtension: "jsbundle") 46 | #endif 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /nitrogen/generated/android/clustererOnLoad.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// clustererOnLoad.cpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #ifndef BUILDING_CLUSTERER_WITH_GENERATED_CMAKE_PROJECT 9 | #error clustererOnLoad.cpp is not being built with the autogenerated CMakeLists.txt project. Is a different CMakeLists.txt building this? 10 | #endif 11 | 12 | #include "clustererOnLoad.hpp" 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | #include "HybridClusterer.hpp" 19 | 20 | namespace margelo::nitro::clusterer { 21 | 22 | int initialize(JavaVM* vm) { 23 | using namespace margelo::nitro; 24 | using namespace margelo::nitro::clusterer; 25 | using namespace facebook; 26 | 27 | return facebook::jni::initialize(vm, [] { 28 | // Register native JNI methods 29 | 30 | 31 | // Register Nitro Hybrid Objects 32 | HybridObjectRegistry::registerHybridObjectConstructor( 33 | "Clusterer", 34 | []() -> std::shared_ptr { 35 | static_assert(std::is_default_constructible_v, 36 | "The HybridObject \"HybridClusterer\" is not default-constructible! " 37 | "Create a public constructor that takes zero arguments to be able to autolink this HybridObject."); 38 | return std::make_shared(); 39 | } 40 | ); 41 | }); 42 | } 43 | 44 | } // namespace margelo::nitro::clusterer 45 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/clusterer/example/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package clusterer.example 2 | 3 | import android.app.Application 4 | import com.facebook.react.PackageList 5 | import com.facebook.react.ReactApplication 6 | import com.facebook.react.ReactHost 7 | import com.facebook.react.ReactNativeApplicationEntryPoint.loadReactNative 8 | import com.facebook.react.ReactNativeHost 9 | import com.facebook.react.ReactPackage 10 | import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost 11 | import com.facebook.react.defaults.DefaultReactNativeHost 12 | 13 | class MainApplication : Application(), ReactApplication { 14 | 15 | override val reactNativeHost: ReactNativeHost = 16 | object : DefaultReactNativeHost(this) { 17 | override fun getPackages(): List = 18 | PackageList(this).packages.apply { 19 | // Packages that cannot be autolinked yet can be added manually here, for example: 20 | // add(MyReactNativePackage()) 21 | } 22 | 23 | override fun getJSMainModuleName(): String = "index" 24 | 25 | override fun getUseDeveloperSupport(): Boolean = BuildConfig.DEBUG 26 | 27 | override val isNewArchEnabled: Boolean = BuildConfig.IS_NEW_ARCHITECTURE_ENABLED 28 | override val isHermesEnabled: Boolean = BuildConfig.IS_HERMES_ENABLED 29 | } 30 | 31 | override val reactHost: ReactHost 32 | get() = getDefaultReactHost(applicationContext, reactNativeHost) 33 | 34 | override fun onCreate() { 35 | super.onCreate() 36 | loadReactNative(this) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/useClusterer.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import type { Supercluster } from './types'; 3 | import type { MapDimensions, Region } from './types'; 4 | import SuperclusterClass from './Supercluster'; 5 | import type * as GeoJSON from 'geojson'; 6 | 7 | export function useClusterer< 8 | P extends GeoJSON.GeoJsonProperties = Supercluster.AnyProps, 9 | C extends GeoJSON.GeoJsonProperties = Supercluster.AnyProps, 10 | >( 11 | data: Array>, 12 | mapDimensions: MapDimensions, 13 | region: Region, 14 | options?: Supercluster.Options 15 | ): [ 16 | (Supercluster.PointFeature

| Supercluster.ClusterFeature)[], 17 | SuperclusterClass, 18 | ] { 19 | const [supercluster, setSupercluster] = useState( 20 | new SuperclusterClass(options).load(data) 21 | ); 22 | 23 | const [points, setPoints] = useState( 24 | supercluster.getClustersFromRegion(region, mapDimensions) 25 | ); 26 | 27 | useEffect(() => { 28 | setSupercluster(new SuperclusterClass(options).load(data)); 29 | // Keep option properties as individual dependencies in case "options" prop is passed as an inline object 30 | // eslint-disable-next-line react-hooks/exhaustive-deps 31 | }, [ 32 | data, 33 | options?.extent, 34 | options?.radius, 35 | options?.minPoints, 36 | options?.minZoom, 37 | options?.maxZoom, 38 | ]); 39 | 40 | useEffect(() => { 41 | setPoints(supercluster.getClustersFromRegion(region, mapDimensions)); 42 | }, [supercluster, region, mapDimensions]); 43 | 44 | return [points, supercluster]; 45 | } 46 | -------------------------------------------------------------------------------- /nitrogen/generated/shared/c++/HybridClustererSpec.hpp: -------------------------------------------------------------------------------- 1 | /// 2 | /// HybridClustererSpec.hpp 3 | /// This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | /// https://github.com/mrousavy/nitro 5 | /// Copyright © 2025 Marc Rousavy @ Margelo 6 | /// 7 | 8 | #pragma once 9 | 10 | #if __has_include() 11 | #include 12 | #else 13 | #error NitroModules cannot be found! Are you sure you installed NitroModules properly? 14 | #endif 15 | 16 | 17 | 18 | 19 | 20 | namespace margelo::nitro::clusterer { 21 | 22 | using namespace margelo::nitro; 23 | 24 | /** 25 | * An abstract base class for `Clusterer` 26 | * Inherit this class to create instances of `HybridClustererSpec` in C++. 27 | * You must explicitly call `HybridObject`'s constructor yourself, because it is virtual. 28 | * @example 29 | * ```cpp 30 | * class HybridClusterer: public HybridClustererSpec { 31 | * public: 32 | * HybridClusterer(...): HybridObject(TAG) { ... } 33 | * // ... 34 | * }; 35 | * ``` 36 | */ 37 | class HybridClustererSpec: public virtual HybridObject { 38 | public: 39 | // Constructor 40 | explicit HybridClustererSpec(): HybridObject(TAG) { } 41 | 42 | // Destructor 43 | ~HybridClustererSpec() override = default; 44 | 45 | public: 46 | // Properties 47 | 48 | 49 | public: 50 | // Methods 51 | 52 | 53 | protected: 54 | // Hybrid Setup 55 | void loadHybridMethods() override; 56 | 57 | protected: 58 | // Tag for logging 59 | static constexpr auto TAG = "Clusterer"; 60 | }; 61 | 62 | } // namespace margelo::nitro::clusterer 63 | -------------------------------------------------------------------------------- /example/ios/ClustererExample/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleDisplayName 8 | ClustererExample 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(CURRENT_PROJECT_VERSION) 25 | LSRequiresIPhoneOS 26 | 27 | NSAppTransportSecurity 28 | 29 | NSAllowsArbitraryLoads 30 | 31 | NSAllowsLocalNetworking 32 | 33 | 34 | NSLocationWhenInUseUsageDescription 35 | 36 | RCTNewArchEnabled 37 | 38 | UILaunchStoryboardName 39 | LaunchScreen 40 | UIRequiredDeviceCapabilities 41 | 42 | arm64 43 | 44 | UISupportedInterfaceOrientations 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationLandscapeLeft 48 | UIInterfaceOrientationLandscapeRight 49 | 50 | UIViewControllerBasedStatusBarAppearance 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/rn_edit_text_material.xml: -------------------------------------------------------------------------------- 1 | 2 | 16 | 22 | 23 | 24 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx512m -XX:MaxMetaspaceSize=256m 13 | org.gradle.jvmargs=-Xmx2048m -XX:MaxMetaspaceSize=512m 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | # AndroidX package structure to make it clearer which packages are bundled with the 21 | # Android operating system, and which are packaged with your app's APK 22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 23 | android.useAndroidX=true 24 | 25 | # Use this property to specify which architecture you want to build. 26 | # You can also override it from the CLI using 27 | # ./gradlew -PreactNativeArchitectures=x86_64 28 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64 29 | 30 | # Use this property to enable support to the new architecture. 31 | # This will allow you to use TurboModules and the Fabric render in 32 | # your application. You should enable this flag either if you want 33 | # to write custom TurboModules/Fabric components OR use libraries that 34 | # are providing them. 35 | newArchEnabled=true 36 | 37 | # Use this property to enable or disable the Hermes JS engine. 38 | # If set to false, you will be using JSC instead. 39 | hermesEnabled=true 40 | 41 | # Use this property to enable edge-to-edge display support. 42 | # This allows your app to draw behind system bars for an immersive UI. 43 | # Note: Only works with ReactActivity and should not be used with custom Activity. 44 | edgeToEdgeEnabled=false 45 | -------------------------------------------------------------------------------- /nitrogen/generated/ios/Clusterer+autolinking.rb: -------------------------------------------------------------------------------- 1 | # 2 | # Clusterer+autolinking.rb 3 | # This file was generated by nitrogen. DO NOT MODIFY THIS FILE. 4 | # https://github.com/mrousavy/nitro 5 | # Copyright © 2025 Marc Rousavy @ Margelo 6 | # 7 | 8 | # This is a Ruby script that adds all files generated by Nitrogen 9 | # to the given podspec. 10 | # 11 | # To use it, add this to your .podspec: 12 | # ```ruby 13 | # Pod::Spec.new do |spec| 14 | # # ... 15 | # 16 | # # Add all files generated by Nitrogen 17 | # load 'nitrogen/generated/ios/Clusterer+autolinking.rb' 18 | # add_nitrogen_files(spec) 19 | # end 20 | # ``` 21 | 22 | def add_nitrogen_files(spec) 23 | Pod::UI.puts "[NitroModules] 🔥 Clusterer is boosted by nitro!" 24 | 25 | spec.dependency "NitroModules" 26 | 27 | current_source_files = Array(spec.attributes_hash['source_files']) 28 | spec.source_files = current_source_files + [ 29 | # Generated cross-platform specs 30 | "nitrogen/generated/shared/**/*.{h,hpp,c,cpp,swift}", 31 | # Generated bridges for the cross-platform specs 32 | "nitrogen/generated/ios/**/*.{h,hpp,c,cpp,mm,swift}", 33 | ] 34 | 35 | current_public_header_files = Array(spec.attributes_hash['public_header_files']) 36 | spec.public_header_files = current_public_header_files + [ 37 | # Generated specs 38 | "nitrogen/generated/shared/**/*.{h,hpp}", 39 | # Swift to C++ bridging helpers 40 | "nitrogen/generated/ios/Clusterer-Swift-Cxx-Bridge.hpp" 41 | ] 42 | 43 | current_private_header_files = Array(spec.attributes_hash['private_header_files']) 44 | spec.private_header_files = current_private_header_files + [ 45 | # iOS specific specs 46 | "nitrogen/generated/ios/c++/**/*.{h,hpp}", 47 | # Views are framework-specific and should be private 48 | "nitrogen/generated/shared/**/views/**/*" 49 | ] 50 | 51 | current_pod_target_xcconfig = spec.attributes_hash['pod_target_xcconfig'] || {} 52 | spec.pod_target_xcconfig = current_pod_target_xcconfig.merge({ 53 | # Use C++ 20 54 | "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", 55 | # Enables C++ <-> Swift interop (by default it's only C) 56 | "SWIFT_OBJC_INTEROP_MODE" => "objcxx", 57 | # Enables stricter modular headers 58 | "DEFINES_MODULE" => "YES", 59 | }) 60 | end 61 | -------------------------------------------------------------------------------- /example/src/App.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react-native/no-inline-styles */ 2 | import { useState } from 'react'; 3 | import { 4 | StyleSheet, 5 | View, 6 | Text, 7 | TouchableOpacity, 8 | SafeAreaView, 9 | } from 'react-native'; 10 | import { Map } from './Map'; 11 | import { Comparison } from './Comparison'; 12 | import { Tests } from './Tests'; 13 | 14 | export default function App() { 15 | const [showType, setType] = useState(null); 16 | 17 | return ( 18 | 19 | Clusterer 20 | 21 | setType('map')} 28 | > 29 | 🗺️ Map Clustering 30 | 31 | setType('speed')} 38 | > 39 | ⚡ Speed Comparison 40 | 41 | setType('test')} 48 | > 49 | 🧪 Tests 50 | 51 | 52 | {showType === 'map' && } 53 | {showType === 'speed' && } 54 | {showType === 'test' && } 55 | 56 | ); 57 | } 58 | 59 | const styles = StyleSheet.create({ 60 | container: { 61 | flex: 1, 62 | backgroundColor: '#fff', 63 | }, 64 | header: { 65 | fontSize: 22, 66 | fontWeight: 'bold', 67 | marginHorizontal: 10, 68 | marginBottom: 5, 69 | }, 70 | buttonContainer: { 71 | flexDirection: 'row', 72 | marginBottom: 10, 73 | marginHorizontal: 10, 74 | gap: 10, 75 | }, 76 | button: { 77 | flex: 1, 78 | borderRadius: 5, 79 | justifyContent: 'center', 80 | alignItems: 'center', 81 | height: 40, 82 | borderWidth: 2, 83 | }, 84 | }); 85 | -------------------------------------------------------------------------------- /example/src/Point.tsx: -------------------------------------------------------------------------------- 1 | import { type FunctionComponent, memo } from 'react'; 2 | import { Text, StyleSheet, View } from 'react-native'; 3 | import { Marker as MapsMarker, Callout } from 'react-native-maps'; 4 | 5 | import type { Supercluster } from 'react-native-clusterer'; 6 | 7 | type IFeature = Supercluster.PointOrClusterFeature; 8 | 9 | interface Props { 10 | item: IFeature; 11 | onPress: (item: IFeature) => void; 12 | } 13 | 14 | export const Point: FunctionComponent = memo( 15 | ({ item, onPress }) => { 16 | return ( 17 | onPress(item)} 25 | > 26 | {item.properties?.cluster ? ( 27 | // Render Cluster 28 | 29 | 30 | {item.properties.point_count} 31 | 32 | 33 | ) : ( 34 | // Else, use default behavior to render 35 | // a marker and add a callout to it 36 | 37 | 38 | {JSON.stringify(item.properties)} 39 | 40 | 41 | )} 42 | 43 | ); 44 | }, 45 | (prevProps, nextProps) => 46 | prevProps.item.properties?.cluster_id === 47 | nextProps.item.properties?.cluster_id && 48 | prevProps.item.properties?.id === nextProps.item.properties?.id && 49 | prevProps.item.properties?.point_count === 50 | nextProps.item.properties?.point_count && 51 | prevProps.item.properties?.onItemPress === 52 | nextProps.item.properties?.onItemPress && 53 | prevProps.item.properties?.getExpansionRegion === 54 | nextProps.item.properties?.getExpansionRegion 55 | ); 56 | 57 | const styles = StyleSheet.create({ 58 | calloutContainer: { 59 | width: 200, 60 | height: 200, 61 | backgroundColor: '#fff', 62 | borderRadius: 5, 63 | padding: 10, 64 | }, 65 | clusterMarker: { 66 | width: 40, 67 | height: 40, 68 | borderRadius: 20, 69 | backgroundColor: '#8eb3ed', 70 | justifyContent: 'center', 71 | alignItems: 'center', 72 | }, 73 | clusterMarkerText: { 74 | color: '#fff', 75 | fontSize: 16, 76 | }, 77 | }); 78 | -------------------------------------------------------------------------------- /cpp/HybridClusterer.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "HybridClustererSpec.hpp" 4 | #include "supercluster.hpp" 5 | 6 | namespace margelo::nitro::clusterer { 7 | class HybridClusterer final : public HybridClustererSpec { 8 | public: 9 | HybridClusterer() : HybridObject(TAG) {} 10 | ~HybridClusterer() { 11 | auto instancePtr = instance.value(); 12 | delete instancePtr; 13 | } 14 | 15 | public: 16 | jsi::Value load(jsi::Runtime &runtime, const jsi::Value &thisValue, 17 | const jsi::Value *args, size_t count); 18 | jsi::Value getClusters(jsi::Runtime &runtime, const jsi::Value &thisValue, 19 | const jsi::Value *args, size_t count); 20 | jsi::Value getTile(jsi::Runtime &rt, 21 | const jsi::Value &thisValue, 22 | const jsi::Value *args, size_t count); 23 | jsi::Value getChildren(jsi::Runtime &rt, 24 | const jsi::Value &thisValue, 25 | const jsi::Value *args, size_t count); 26 | jsi::Value getLeaves(jsi::Runtime &rt, 27 | const jsi::Value &thisValue, 28 | const jsi::Value *args, 29 | size_t count); 30 | jsi::Value getClusterExpansionZoom( 31 | jsi::Runtime &rt, const jsi::Value &thisValue, const jsi::Value *args, 32 | size_t count); 33 | 34 | void loadHybridMethods() override { 35 | // register base prototype 36 | HybridClustererSpec::loadHybridMethods(); 37 | // register all methods we override here 38 | registerHybrids(this, [](Prototype &prototype) { 39 | prototype.registerRawHybridMethod("load", 0, &HybridClusterer::load); 40 | prototype.registerRawHybridMethod("getClusters", 0, 41 | &HybridClusterer::getClusters); 42 | prototype.registerRawHybridMethod("getTile", 0, 43 | &HybridClusterer::getTile); 44 | prototype.registerRawHybridMethod("getChildren", 0, 45 | &HybridClusterer::getChildren); 46 | prototype.registerRawHybridMethod("getLeaves", 0, 47 | &HybridClusterer::getLeaves); 48 | prototype.registerRawHybridMethod("getClusterExpansionZoom", 0, 49 | &HybridClusterer::getClusterExpansionZoom); 50 | }); 51 | } 52 | 53 | private: 54 | std::optional instance = std::nullopt; 55 | std::optional featuresInput = std::nullopt; 56 | }; 57 | 58 | } // namespace margelo::nitro::clusterer 59 | -------------------------------------------------------------------------------- /example/src/utils.ts: -------------------------------------------------------------------------------- 1 | export const superclusterOptions = { 2 | minZoom: 0, 3 | maxZoom: 16, 4 | radius: 40, 5 | extent: 512, 6 | minPoints: 2, 7 | }; 8 | 9 | export const PerformanceNow = 10 | // @ts-ignore 11 | (global.performance && global.performance.now) || 12 | // @ts-ignore 13 | global.performanceNow || 14 | // @ts-ignore 15 | global.nativePerformanceNow || 16 | (() => { 17 | try { 18 | var now = require('fbjs/lib/performanceNow'); 19 | } finally { 20 | return now; 21 | } 22 | })(); 23 | 24 | const fixed = (n: number) => (Math.trunc(n) === n ? n + '' : n.toFixed(3)); 25 | 26 | export const timeDelta = (start: number, end: number) => fixed(end - start); 27 | 28 | export const deepEqualWithoutIds = (x: any, y: any) => { 29 | if (x === y) return true; 30 | // if both x and y are null or undefined and exactly the same 31 | 32 | if (!(x instanceof Object) || !(y instanceof Object)) return false; 33 | // if they are not strictly equal, they both need to be Objects 34 | 35 | if (x.constructor !== y.constructor) return false; 36 | // they must have the exact same prototype chain, the closest we can do is 37 | // test there constructor. 38 | 39 | for (var p in x) { 40 | if (p === 'id' || p === 'cluster_id' || p === 'getExpansionRegion') 41 | continue; 42 | // skip id and cluster_id and getExpansionRegion 43 | 44 | if (!x.hasOwnProperty(p)) continue; 45 | // other properties were tested using x.constructor === y.constructor 46 | 47 | if (!y.hasOwnProperty(p)) return false; 48 | 49 | // point_count_abbreviated can be a number or string in the JS implementation - "2k" vs 20 50 | if (p === 'point_count_abbreviated' && String(x[p]) === String(y[p])) 51 | continue; 52 | 53 | // check if the coordinates are close to each other 54 | if (p === 'coordinates' && Array.isArray(x[p]) && Array.isArray(y[p])) { 55 | if (x[p].length !== y[p].length) return false; 56 | 57 | for (let i = 0; i < x[p].length; i++) { 58 | const diff = x[p][i] / y[p][i]; 59 | if (diff > 1.01 || diff < 0.99) { 60 | return false; 61 | } 62 | } 63 | continue; 64 | } 65 | 66 | if (x[p] === y[p]) continue; 67 | // if they have the same strict value or identity then they are equal 68 | 69 | if (typeof x[p] !== 'object') return false; 70 | // Numbers, Strings, Functions, Booleans must be strictly equal 71 | 72 | if (!deepEqualWithoutIds(x[p], y[p])) return false; 73 | // Objects and Arrays must be tested recursively 74 | } 75 | 76 | for (p in y) { 77 | if (p === 'id' || p === 'cluster_id' || p === 'getExpansionRegion') 78 | continue; 79 | // skip id and cluster_id 80 | 81 | if (y.hasOwnProperty(p) && !x.hasOwnProperty(p)) return false; 82 | // allows x[ p ] to be set to undefined 83 | } 84 | 85 | return true; 86 | }; 87 | -------------------------------------------------------------------------------- /example/src/Map.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useRef } from 'react'; 2 | import { Button, Dimensions, StyleSheet, View } from 'react-native'; 3 | import { 4 | Clusterer, 5 | isClusterFeature, 6 | type Supercluster, 7 | } from 'react-native-clusterer'; 8 | import MapView, { type Region } from 'react-native-maps'; 9 | import { getRandomData, initialRegion, parsedPlacesData } from './places'; 10 | import { Point } from './Point'; 11 | 12 | type IFeature = Supercluster.PointOrClusterFeature; 13 | 14 | const MAP_WIDTH = Dimensions.get('window').width; 15 | const MAP_HEIGHT = Dimensions.get('window').height - 80; 16 | const MAP_DIMENSIONS = { width: MAP_WIDTH, height: MAP_HEIGHT }; 17 | 18 | export const Map = () => { 19 | const [region, setRegion] = useState(initialRegion); 20 | const [places, setPlaces] = 21 | useState[]>(parsedPlacesData); 22 | const [options, setOptions] = useState({ radius: 18 }); 23 | const mapRef = useRef(null); 24 | 25 | const _handlePointPress = (point: IFeature) => { 26 | if (isClusterFeature(point)) { 27 | const toRegion = point.properties.getExpansionRegion(); 28 | mapRef.current?.animateToRegion(toRegion, 500); 29 | } 30 | }; 31 | 32 | return ( 33 | 34 | 40 | { 46 | return ( 47 | 56 | ); 57 | }} 58 | /> 59 | 60 | 61 |