diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.yml b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml new file mode 100644 index 0000000..80240f5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.yml @@ -0,0 +1,125 @@ +name: 🐛 Bug Report +description: Something in the map renders or behaves incorrectly at runtime +labels: [bug] +body: + - type: textarea + attributes: + label: What happened? + description: Explain what you were trying to do and what happened instead. Be as precise as possible — an issue that cannot be understood cannot be fixed. + placeholder: I rendered a MapView with 2000 markers and the `onMarkerPress` callback fired with the wrong marker id after clustering kicked in. + validations: + required: true + - type: textarea + attributes: + label: Reproduceable code + description: > + Share a small reproduceable snippet — ideally the whole component. + Include the `MapView` props you set, the marker/overlay data shape, and any imperative `MapViewRef` calls. + + **Redact secrets first.** Replace Google Maps API keys, tokens and any private configuration with placeholders — this issue is public. + render: tsx + placeholder: | + console.log(id)} + /> + validations: + required: true + - type: textarea + attributes: + label: Relevant log output + description: > + Paste any relevant **native log output** here. This is automatically formatted as code, so no backticks are needed. + + * For iOS, run the app from Xcode and copy the console output. + + * For Android, use the Android Studio Logcat window or run `adb logcat` in a terminal. + + **Redact secrets first.** Strip API keys, tokens and credentials from the logs before pasting. + If the log shows this library itself leaking a key — into a log line, a request, a crash payload — do not post it here: report it privately through a [security advisory](https://github.com/gmi-software/react-native-better-maps/security/advisories/new). + render: shell + validations: + required: false + - type: dropdown + attributes: + label: Map provider + description: Which provider does this happen with? Select every provider you reproduced it on. + multiple: true + options: + - apple + - google + validations: + required: true + - type: dropdown + attributes: + label: Platforms + description: Select every platform you reproduced this on. + multiple: true + options: + - iOS + - Android + validations: + required: true + - type: input + attributes: + label: Device + description: > + Which device shows the problem? Give the full name plus the OS version, and say whether it is a simulator/emulator or a physical device. + If you tested several, list them comma separated. + placeholder: ex. iPhone 15 Pro (iOS 18.2, simulator), Pixel 7 (Android 15, physical) + validations: + required: true + - type: input + attributes: + label: react-native-better-maps version + placeholder: ex. 1.1.0 + validations: + required: true + - type: input + attributes: + label: React Native version + description: The library requires 0.78+ with the New Architecture enabled. + placeholder: ex. 0.81.4 + validations: + required: true + - type: input + attributes: + label: react-native-nitro-modules version + placeholder: ex. 0.35.10 + validations: + required: true + - type: input + attributes: + label: Expo SDK version + description: Leave empty if this is a bare React Native app. + placeholder: ex. 57.0.0 + validations: + required: false + - type: dropdown + attributes: + label: Can you reproduce this in the example app? + description: > + Run the example app (`example/`, see [Example app](https://github.com/gmi-software/react-native-better-maps#example-app)) and check whether the problem shows up there too. + **Note:** an issue that does not reproduce in the example app and ships no reproduction is much harder to act on. + options: + - I didn't try (⚠️ your issue may take much longer to get looked at) + - Yes, I can reproduce it in the example app + - 'No, the example app works fine' + default: 0 + validations: + required: true + - type: checkboxes + attributes: + label: Additional information + description: Please check all the boxes that apply + options: + - label: I am using Expo with a development build + - label: I am using clustering + - label: I am using GeoJSON overlays + - label: I have the New Architecture enabled + - label: I checked [Common problems](https://github.com/gmi-software/react-native-better-maps#common-problems) and my issue is not listed there. + required: true + - label: I searched for [similar issues in this repository](https://github.com/gmi-software/react-native-better-maps/issues) and found none. + required: true diff --git a/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml b/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml new file mode 100644 index 0000000..ba1a8cb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUILD_ERROR.yml @@ -0,0 +1,121 @@ +name: 🔧 Build Error +description: The native app fails to build, prebuild, or install pods +labels: [build/setup] +body: + - type: textarea + attributes: + label: How were you trying to build the app? + description: Explain how you built — Xcode, `expo run:ios`, `pod install`, a Gradle task, EAS Build, CI, or something else. Be as precise as possible. + placeholder: I added react-native-better-maps to an Expo SDK 57 app, ran `expo prebuild --clean` and then `expo run:ios`, and the build failed while compiling the Google adapter files. + validations: + required: true + - type: textarea + attributes: + label: Full build logs + description: > + Share the full build output, from the first command to the last line. + Do not paste only the final few lines — the real cause is usually much earlier. + + **Redact secrets first.** Build logs can echo `googleMapsApiKey`, `GoogleMapsIosApiKey` and `com.google.android.geo.API_KEY` — replace them with placeholders before pasting. + render: shell + validations: + required: true + - type: textarea + attributes: + label: Project dependencies + description: Share the `dependencies` block from your app's `package.json`, so conflicting libraries are visible. + render: json + placeholder: | + "dependencies": { + "expo": "~57.0.0", + "react-native": "0.81.4", + "react-native-better-maps": "^1.1.0", + "react-native-nitro-modules": "^0.35.10" + } + validations: + required: true + - type: textarea + attributes: + label: Expo config plugin setup + description: > + If you use the config plugin, share the `react-native-better-maps` entry from your `app.json` / `app.config.js`. + For a bare app, share the relevant parts of `Info.plist`, `AndroidManifest.xml` and `Podfile.properties.json` instead. + See [Expo setup](https://github.com/gmi-software/react-native-better-maps/blob/main/docs/expo-setup.md). + + **Redact secrets first.** Replace the actual API key values with placeholders such as `` — this issue is public. + render: json + validations: + required: false + - type: dropdown + attributes: + label: Target platforms + description: Select every platform the build fails on. + multiple: true + options: + - iOS + - Android + validations: + required: true + - type: dropdown + attributes: + label: Operating system + description: Select the operating system you are building on. + multiple: true + options: + - macOS + - Windows + - Linux + validations: + required: true + - type: input + attributes: + label: react-native-better-maps version + placeholder: ex. 1.1.0 + validations: + required: true + - type: input + attributes: + label: React Native version + description: The library requires 0.78+ with the New Architecture enabled. + placeholder: ex. 0.81.4 + validations: + required: true + - type: input + attributes: + label: react-native-nitro-modules version + placeholder: ex. 0.35.10 + validations: + required: true + - type: input + attributes: + label: Expo SDK version + description: Leave empty if this is a bare React Native app. + placeholder: ex. 57.0.0 + validations: + required: false + - type: dropdown + attributes: + label: Can you build the example app? + description: > + Try to build the example app (`example/`, see [Example app](https://github.com/gmi-software/react-native-better-maps#example-app)). + **Note:** this separates a problem in the library from a problem in your app's native setup, and is the single most useful thing you can report here. + options: + - I didn't try (⚠️ your issue may take much longer to get looked at) + - Yes, the example app builds fine + - 'No, the example app fails to build too' + default: 0 + validations: + required: true + - type: checkboxes + attributes: + label: Additional information + description: Please check all the boxes that apply + options: + - label: I am using Expo with a development build + - label: I am using the Expo config plugin + - label: I am using Google Maps on iOS (`betterMaps.iosGoogleProvider`) + - label: I retried after a clean build (`expo prebuild --clean`, deleted `ios/Pods` and `android/build`) + - label: I checked [Common problems](https://github.com/gmi-software/react-native-better-maps#common-problems) and my issue is not listed there. + required: true + - label: I searched for [similar issues in this repository](https://github.com/gmi-software/react-native-better-maps/issues) and found none. + required: true diff --git a/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml new file mode 100644 index 0000000..7275f24 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/ENHANCEMENT.yml @@ -0,0 +1,42 @@ +name: ✨ Enhancement +description: Request a feature or an enhancement to react-native-better-maps +labels: [enhancement] +body: + - type: textarea + attributes: + label: Feature request / enhancement + description: Describe the feature in detail, and explain the motivation that led you to open this issue — what are you building, and what is blocked without it? + placeholder: I need heatmap overlays so I can show density of my delivery data. react-native-maps has this, and both MapKit and the Google Maps SDK support it natively. + validations: + required: true + - type: dropdown + attributes: + label: Which providers should this cover? + description: > + Providers differ in what their native SDKs support, so a feature may land on one before the other. + See the [capability matrix](https://github.com/gmi-software/react-native-better-maps#capability-matrix). + multiple: true + options: + - apple (iOS) + - google (iOS) + - google (Android) + validations: + required: true + - type: textarea + attributes: + label: Are there any existing workarounds? + description: List anything you do today to work around the missing feature, so others reading this issue are not blocked by it. + placeholder: I render an absolutely positioned overlay on top of the map and sync it with `onRegionChangeComplete`, but it drifts during the gesture. + validations: + required: false + - type: checkboxes + attributes: + label: Additional information + description: Please check all the boxes that apply + options: + - label: I checked the [README](https://github.com/gmi-software/react-native-better-maps#readme) and the [docs](https://github.com/gmi-software/react-native-better-maps/tree/main/docs) and this feature does not exist yet. + required: true + - label: I checked the [roadmap](https://github.com/gmi-software/react-native-better-maps/blob/main/docs/roadmap.md) and it is not already planned there. + - label: I can open a pull request to implement this. (See [Contributing](https://github.com/gmi-software/react-native-better-maps/blob/main/CONTRIBUTING.md)) + - label: I searched for [similar issues in this repository](https://github.com/gmi-software/react-native-better-maps/issues) and found none. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d3a8851 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: 💬 Question or usage help + url: https://github.com/gmi-software/react-native-better-maps/discussions + about: Ask how to do something, or float an idea before opening an enhancement. + - name: 📖 Documentation + url: https://github.com/gmi-software/react-native-better-maps#readme + about: Installation, providers, clustering, GeoJSON, Google Maps keys and the capability matrix. + - name: 🔐 Report a security vulnerability + url: https://github.com/gmi-software/react-native-better-maps/security/advisories/new + about: Do not open a public issue. Report it privately through a GitHub security advisory. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..0249668 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,24 @@ +## What does this change? + + + +## How was it verified? + + + +## Scope + +- **Providers:** +- **Platforms:** + +## Checklist + +- [ ] `bun run lint`, `bun run typecheck` and `bun run build` pass +- [ ] Tests pass, and new behavior is covered by a test +- [ ] Nitro specs changed? `bun run nitrogen` was re-run and the generated code is committed +- [ ] Public API changed? The README and the capability matrix are updated +- [ ] Commits follow [Conventional Commits](https://www.conventionalcommits.org/) +- [ ] Behavior changed without a type change? Say so explicitly above — it breaks consumers whose code still compiles diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..5a22eec --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,43 @@ +version: 2 + +updates: + # The workflows pin every action by commit SHA, which never resolves to a newer + # release on its own. Without this, those pins silently rot. + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + groups: + github-actions: + patterns: + - '*' + # Keep the subject conventional — the commitlint job runs on every pull + # request, Dependabot's included. + commit-message: + prefix: chore + + # Bun is its own Dependabot ecosystem, not part of `npm`: it reads `bun.lock`. + # Version updates are supported; security updates are not, so Dependabot + # security alerts for these dependencies have to be acted on by hand. + - package-ecosystem: bun + directories: + - / + - /package + - /example + schedule: + interval: weekly + groups: + dev-dependencies: + dependency-type: development + patterns: + - '*' + # React Native, React and Nitro are peer dependencies with native + # counterparts; upgrading them is a deliberate, tested change, never an + # automated bump. + ignore: + - dependency-name: react + - dependency-name: react-native + - dependency-name: react-native-nitro-modules + - dependency-name: nitrogen + commit-message: + prefix: chore diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ccb9d0d..aa80b3e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ on: - 'v[0-9]+.[0-9]+.[0-9]+' - 'v[0-9]+.[0-9]+.[0-9]+-*' # Rehearsal against the current branch. Always a dry run: it validates the - # version, changelog and full gate without tagging or publishing anything. + # version and the full gate without tagging or publishing anything. workflow_dispatch: concurrency: @@ -81,15 +81,6 @@ jobs: exit 1 fi - - name: Verify CHANGELOG entry exists - env: - VERSION: ${{ steps.version.outputs.version }} - run: | - if ! grep -qE "^## +${VERSION//./\\.}( |$)" CHANGELOG.md; then - echo "::error::CHANGELOG.md has no '## $VERSION' section. Add the release notes before publishing." - exit 1 - fi - - name: Verify version is not already published env: VERSION: ${{ steps.version.outputs.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 09bd535..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,76 +0,0 @@ -# Changelog - -All notable changes to this project are documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## 1.1.0 - -### Behavior changes - -Two changes alter runtime behavior without changing any type signatures, so your -code keeps compiling but may behave differently after upgrading. - -**`onRegionChange` and `onRegionChangeComplete` now fire once per gesture** - -In 1.0.0 these fired repeatedly while the map was moving, and also fired for -programmatic camera updates. Now: - -- `onRegionChange` fires **once** when a user-initiated region change **begins** -- `onRegionChangeComplete` fires **once** when the user gesture **ends** -- Programmatic updates (`setCamera`, `animateCamera`, `fitToCoordinates`) no - longer emit either callback - -If you relied on a continuous stream of region updates — a live coordinate -readout, or a "search this area" button that re-renders while panning — move that -work to `onRegionChangeComplete`, which now marks the end of the gesture: - -```tsx -// Before: fired continuously during the gesture - setSearchArea(region)} /> - -// After: fires once when the user stops moving the map - setSearchArea(region)} /> -``` - -**`MapViewRef` camera methods now return `Promise`** - -`setCamera`, `animateCamera`, and `fitToCoordinates` previously returned `void`. -Existing call sites still compile, but linters configured with -`@typescript-eslint/no-floating-promises` will now flag them, and any custom -implementation or test mock of `MapViewRef` must be updated to match. - -```tsx -// Await the call, or explicitly ignore the promise -await mapRef.current?.animateCamera(camera, 300); -``` - -### Features - -- Add native POI press events with provider-specific payloads - (`onPoiPress`, `PoiPressEvent`, `ApplePoiPressEvent`, `GooglePoiPressEvent`) - ([#36](https://github.com/gmi-software/react-native-better-maps/pull/36)) -- Add Expo SDK 57 support - ([#49](https://github.com/gmi-software/react-native-better-maps/pull/49)) -- Rework map region change handling and camera update logic; programmatic - updates now skip no-op native calls - ([#48](https://github.com/gmi-software/react-native-better-maps/pull/48)) - -### Bug Fixes - -- **ios:** Remove `main.sync` from `HybridMapView` and make camera APIs async, - fixing main-thread deadlocks - ([#45](https://github.com/gmi-software/react-native-better-maps/pull/45)) -- **ios:** Fix threading issues in map view ownership - ([#43](https://github.com/gmi-software/react-native-better-maps/pull/43)) -- **android:** Align SDK versions with the nitro-modules prefab - ([#41](https://github.com/gmi-software/react-native-better-maps/pull/41)) -- Fix failure on first-time build - ([#39](https://github.com/gmi-software/react-native-better-maps/pull/39)) - -## 1.0.0 - -Initial public release: high-performance maps for React Native built on Nitro -Modules and the New Architecture, with Apple Maps and Google Maps providers on -iOS and Android. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..6867b7c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +security@gmi.software. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTEXT.md b/CONTEXT.md deleted file mode 100644 index 10ad9e1..0000000 --- a/CONTEXT.md +++ /dev/null @@ -1,33 +0,0 @@ -# React Native Nitro Maps - -Shared language for the map rendering domain in `react-native-better-maps`. - -## Language - -**Map Provider**: -A native rendering backend for the map view, such as Apple MapKit, Google Maps SDK, Mapbox SDK, or an OpenStreetMap-backed renderer. -_Avoid_: Tile source, geocoding provider - -**Google Map ID**: -A Google Cloud Map ID used by the Google Maps SDK to apply cloud-based map styling. It is supported only by the `google` map provider and is distinct from the Google Maps API key required to load the SDK. -_Avoid_: API key, style JSON - -**Marker**: -A point annotation created and owned by the app at a geographic coordinate on the map. -_Avoid_: Pin - -**Point of Interest (POI)**: -A provider-owned map feature rendered by the base map, such as a business, park, school, or public place. It is not a `Marker` because the app does not create, own, or update it as an overlay. -_Avoid_: Marker, app marker, custom marker - -**Native POI Detail Presentation**: -A provider-owned native UI surface that presents details for a selected point of interest. -_Avoid_: React Native callout, custom POI card - -**Marker Cluster**: -A grouped marker representation shown when nearby clusterable markers collapse into one map annotation. -_Avoid_: Cluster pin, marker group - -**Entering Animation**: -The visual transition used when a marker or marker cluster first appears on the map. -_Avoid_: Appear animation, spawn animation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e57e65e..07551c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,6 +32,10 @@ Thank you for your interest in contributing! | `bun run build` | Build the library with react-native-builder-bob | | `bun run nitrogen` | Run Nitrogen codegen (when specs are ready) | | `bun run format` | Format all files with Prettier | +| `bun run format:cpp` | Format C++ with clang-format (`config/.clang-format`) | +| `bun run format:kotlin` | Format Kotlin with ktlint (`config/.editorconfig`) | +| `bun run format:swift` | Format Swift with swift-format (`config/.swift-format`) | +| `bun run format:native` | Run all three native formatters | | `bun run doctor` | Run React Doctor locally | ## React Doctor @@ -57,6 +61,14 @@ React Doctor runs in a separate GitHub Actions workflow (`.github/workflows/reac After the baseline is documented and critical findings are addressed, CI will switch to blocking new errors on changed files only. +## Code of conduct + +This project follows the [Contributor Covenant](CODE_OF_CONDUCT.md). By taking part you are expected to uphold it; report unacceptable behavior to security@gmi.software. + +## Security + +Do not report vulnerabilities through issues or pull requests. See [SECURITY.md](SECURITY.md) for the private reporting channel. + ## Commit messages This project uses [Conventional Commits](https://www.conventionalcommits.org/). Commit messages are validated locally via Husky and on pull requests in CI. diff --git a/LICENSE b/LICENSE index ddcd15b..b37ca5e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 YOUR_NAME +Copyright (c) 2026 gmi.software Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 2026089..a73cfae 100644 --- a/README.md +++ b/README.md @@ -612,6 +612,9 @@ See [example/.env.example](example/.env.example) for the supported environment v - [GeoJSON overlays](docs/geojson.md) - [Roadmap](docs/roadmap.md) - [Contributing](CONTRIBUTING.md) +- [Code of conduct](CODE_OF_CONDUCT.md) +- [Security policy](SECURITY.md) +- [Releasing](RELEASING.md) - [ADRs](docs/adr) ## Common problems diff --git a/RELEASING.md b/RELEASING.md index 6c2c3ff..e4144ef 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -8,24 +8,22 @@ developer machine, and no long-lived npm token exists anywhere. Publishing is triggered by pushing a version tag. CI never writes to git: `main` requires pull request reviews with `enforce_admins` enabled, so nothing — not even `github-actions[bot]` — can push a release commit to it. The version bump -goes through a normal reviewed pull request instead, which has the useful side -effect of putting the changelog in front of a reviewer. +goes through a normal reviewed pull request instead. ## Cutting a release -**1. Open a release pull request.** Bump the version and write the notes: +**1. Open a release pull request.** Bump the version: ```bash cd package npm version 1.1.0 --no-git-tag-version ``` -Then add a matching `## 1.1.0` section to `CHANGELOG.md`, and open a pull request -with both changes. Review and merge it as usual. +Open a pull request with that change, then review and merge it as usual. **2. Rehearse (optional).** Run the **Release** workflow manually from the -Actions tab. A manual run is always a dry run: it validates the version, the -changelog and the full gate without publishing. +Actions tab. A manual run is always a dry run: it validates the version and the +full gate without publishing. **3. Push the tag.** @@ -35,10 +33,10 @@ git tag -a v1.1.0 -m 'v1.1.0' git push origin v1.1.0 ``` -The workflow then verifies the tag matches `package/package.json`, that -`CHANGELOG.md` has a section for it, and that the version is not already on npm; -runs the full gate; publishes to npm with provenance; and creates the GitHub -Release with notes generated from the conventional commits since the last tag. +The workflow then verifies the tag matches `package/package.json` and that the +version is not already on npm; runs the full gate; publishes to npm with +provenance; and creates the GitHub Release with notes generated from the +conventional commits since the last tag. The iOS podspec reads its version from `package.json`, so there is no second version to keep in sync. @@ -50,11 +48,13 @@ usual rules apply — `fix:` is a patch, `feat:` a minor, an incompatible API change a major — but two cases are easy to get wrong: - A commit that is not conventional (for example `Fix threading issues on ios`) - is invisible to the generated release notes. Add it to the changelog by hand. + is invisible to the generated release notes. Edit the GitHub Release body by + hand after the workflow creates it. - A change in runtime behavior that keeps the same types — such as a callback that starts firing once per gesture instead of continuously — breaks consumers even though their code still compiles. Either take the major, or ship it as a - minor with a prominent **Behavior changes** section, as 1.1.0 did. + minor and add a prominent **Behavior changes** section to the GitHub Release + body, as 1.1.0 did. ## Pre-releases @@ -69,15 +69,16 @@ Consumers opt in explicitly: npm install react-native-better-maps@rc ``` -## Changelog +## Release notes -`CHANGELOG.md` is written by hand, not generated. This is deliberate: the parts -of a release that matter most — behavior changes, migration snippets, the reason -a fix exists — cannot be derived from commit subjects. The workflow **fails** if -there is no `## ` section, so the notes cannot be forgotten. +There is no `CHANGELOG.md`. The release notes are the GitHub Release body, +generated from the conventional commits since the previous tag by +`@release-it/conventional-changelog`. -Notes generated from conventional commits still go into the GitHub Release body, -so commit-level detail is not lost. +Commit subjects cannot carry the parts of a release that matter most — behavior +changes, migration snippets, the reason a fix exists. Nothing enforces those any +more, so when a release needs them, edit the GitHub Release body by hand once the +workflow has created it. ## One-time setup: npm trusted publishing diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..11d2536 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,79 @@ +# Security Policy + +## Supported versions + +Fixes land on the latest minor release. Older minors do not receive backports — +upgrading is the supported path. + +| Version | Supported | +| ------- | ------------------- | +| 1.1.x | ✅ | +| 1.0.x | ❌ upgrade to 1.1.x | +| < 1.0 | ❌ | + +## Reporting a vulnerability + +**Do not open a public issue.** + +Report it privately through +[GitHub Security Advisories](https://github.com/gmi-software/react-native-better-maps/security/advisories/new). +If that is not possible, email . + +Please include: + +- the version of `react-native-better-maps`, React Native and + `react-native-nitro-modules` +- the affected map provider (`apple` / `google`) and platform +- what an attacker can do with it, and a reproduction if you have one + +We aim to acknowledge a report within five working days. If a fix is warranted +we will agree a disclosure timeline with you, credit you in the advisory unless +you prefer otherwise, and publish the advisory alongside the release that fixes +it. + +## Map provider API keys are not a vulnerability in this library + +The most common report we expect is _"I extracted the Google Maps API key from +an app that uses this library."_ That is expected behavior of the Google Maps +SDKs, not a flaw here. + +The key has to reach the native SDK inside the app process, so it lives in the +shipped binary: + +- **iOS** — `GoogleMapsIosApiKey` in `Info.plist`, read via `Bundle.main` and + handed to `GMSServices.provideAPIKey` +- **Android** — `com.google.android.geo.API_KEY` in `AndroidManifest.xml`, read + by the Google Maps SDK itself + +Anyone with the `.ipa` or `.apk` can read it. This library does not transmit the +key anywhere: it only passes it to the provider SDK in-process. + +The actual protection is server-side, in Google Cloud: + +- restrict the key to your iOS bundle ID and your Android package name plus the + SHA-1 fingerprint of your signing certificate +- restrict it to only the APIs you use (Maps SDK for iOS / Android) +- use separate keys per platform and per build variant, and set quotas + +A report that a key can be extracted from a binary will be closed with a link to +this section. A report that this library leaks a key somewhere it should not — +a log line, a network request, a crash report payload — is a real issue and we +want to hear about it. + +## Supply chain + +Releases are built and published only by the +[Release workflow](.github/workflows/release.yml), never from a developer +machine: + +- npm publishing uses OIDC trusted publishing, so no long-lived npm token exists + in the repository or on any laptop +- every published version carries + [npm provenance](https://docs.npmjs.com/generating-provenance-statements), + which you can verify with `npm audit signatures` +- every GitHub Action is pinned by commit SHA, not by tag +- workflows check out with `persist-credentials: false`, and CI never writes to + git + +If you believe a published artifact does not match this repository, treat it as +a vulnerability and report it through the channel above. diff --git a/config/.clang-format b/config/.clang-format new file mode 100644 index 0000000..ecdd3f3 --- /dev/null +++ b/config/.clang-format @@ -0,0 +1,26 @@ +# Config for clang-format version 16+ + +# Standard +BasedOnStyle: llvm +Standard: c++20 + +# Indentation +IndentWidth: 2 +ColumnLimit: 140 + +# Includes +SortIncludes: CaseSensitive +SortUsingDeclarations: true + +# Pointer and reference alignment +PointerAlignment: Left +ReferenceAlignment: Left +ReflowComments: true + +# Line breaking options +BreakBeforeBraces: Attach +BreakConstructorInitializers: BeforeColon +AlwaysBreakTemplateDeclarations: true +AllowShortFunctionsOnASingleLine: Empty +IndentCaseLabels: true +NamespaceIndentation: Inner diff --git a/config/.editorconfig b/config/.editorconfig new file mode 100644 index 0000000..0f0e725 --- /dev/null +++ b/config/.editorconfig @@ -0,0 +1,9 @@ +[*.{kt,kts}] +# Sources are named after the extension they add, not after a single top-level +# declaration — Camera+CameraPosition.kt, MapType+GoogleMap.kt, String+ColorInt.kt. +ktlint_standard_filename = disabled +ktlint_standard_function-expression-body = disabled + +[*] +indent_style = space +indent_size = 2 diff --git a/config/.swift-format b/config/.swift-format new file mode 100644 index 0000000..0a07407 --- /dev/null +++ b/config/.swift-format @@ -0,0 +1,6 @@ +{ + "version": 1, + "//": "Everything not listed here is swift-format's default. Two deviations: the Google Maps adapters are whole files wrapped in `#if canImport(GoogleMaps)` and indenting that block would re-indent every one of them for no benefit; and the line length matches config/.clang-format so both native formatters agree on one width.", + "indentConditionalCompilationBlocks": false, + "lineLength": 140 +} diff --git a/tsconfig.base.json b/config/tsconfig.json similarity index 100% rename from tsconfig.base.json rename to config/tsconfig.json diff --git a/package.json b/package.json index 13670b7..46f3275 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "nitrogen": "bun run --filter react-native-better-maps nitrogen", "example": "bun run --filter react-native-better-maps-example", "format": "prettier --write .", + "format:cpp": "bash scripts/clang-format.sh", + "format:kotlin": "bash scripts/kotlin-format.sh", + "format:swift": "bash scripts/swift-format.sh", + "format:native": "bun run format:cpp && bun run format:kotlin && bun run format:swift", "doctor": "npx react-doctor@latest", "commitlint": "commitlint", "prepare": "husky" diff --git a/package/android/src/main/cpp/cpp-adapter.cpp b/package/android/src/main/cpp/cpp-adapter.cpp index a273b3e..83e2492 100644 --- a/package/android/src/main/cpp/cpp-adapter.cpp +++ b/package/android/src/main/cpp/cpp-adapter.cpp @@ -1,9 +1,7 @@ -#include -#include #include "NitroMapsOnLoad.hpp" +#include +#include JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { - return facebook::jni::initialize(vm, []() { - margelo::nitro::nitromaps::registerAllNatives(); - }); + return facebook::jni::initialize(vm, []() { margelo::nitro::nitromaps::registerAllNatives(); }); } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/Camera+CameraPosition.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/Camera+CameraPosition.kt index 65dec5a..55c271d 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/Camera+CameraPosition.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/Camera+CameraPosition.kt @@ -5,7 +5,8 @@ import com.google.android.gms.maps.model.LatLng import kotlin.math.abs fun Camera.toCameraPosition(current: CameraPosition? = null): CameraPosition { - return CameraPosition.Builder() + return CameraPosition + .Builder() .target(LatLng(center.latitude, center.longitude)) .zoom((zoom ?: current?.zoom?.toDouble() ?: 10.0).toFloat()) .bearing((heading ?: current?.bearing?.toDouble() ?: 0.0).toFloat()) @@ -29,9 +30,9 @@ fun CameraPosition.approximatelyEquals( zoomEpsilon: Float = MapApproximateEquality.ZOOM_EPSILON, angleEpsilon: Float = MapApproximateEquality.ANGLE_EPSILON, ): Boolean { - return abs(target.latitude - other.target.latitude) < coordinateEpsilon - && abs(target.longitude - other.target.longitude) < coordinateEpsilon - && abs(zoom - other.zoom) < zoomEpsilon - && abs(bearing - other.bearing) < angleEpsilon - && abs(tilt - other.tilt) < angleEpsilon + return abs(target.latitude - other.target.latitude) < coordinateEpsilon && + abs(target.longitude - other.target.longitude) < coordinateEpsilon && + abs(zoom - other.zoom) < zoomEpsilon && + abs(bearing - other.bearing) < angleEpsilon && + abs(tilt - other.tilt) < angleEpsilon } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/CircleDescriptor+CircleOptions.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/CircleDescriptor+CircleOptions.kt index 161033f..269d8db 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/CircleDescriptor+CircleOptions.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/CircleDescriptor+CircleOptions.kt @@ -4,11 +4,12 @@ import com.google.android.gms.maps.model.CircleOptions import com.google.android.gms.maps.model.LatLng fun CircleDescriptor.toCircleOptions(): CircleOptions { - val options = CircleOptions() - .center(LatLng(center.latitude, center.longitude)) - .radius(radius) - .strokeWidth((strokeWidth ?: 2.0).toFloat()) - .clickable(tappable != false) + val options = + CircleOptions() + .center(LatLng(center.latitude, center.longitude)) + .radius(radius) + .strokeWidth((strokeWidth ?: 2.0).toFloat()) + .clickable(tappable != false) strokeColor?.let { options.strokeColor(it.toColorInt()) } fillColor?.let { options.fillColor(it.toColorInt()) } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterBadgeMetrics.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterBadgeMetrics.kt index a80d51e..a165f12 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterBadgeMetrics.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterBadgeMetrics.kt @@ -4,18 +4,20 @@ package com.margelo.nitro.nitromaps internal object ClusterBadgeMetrics { const val MERGE_GAP_DP = 6.0 - fun badgeRadiusDp(count: Int): Double = when { - count < 2 -> 14.0 - count < 10 -> 17.0 - count < 100 -> 20.0 - count < 1000 -> 24.0 - else -> 28.0 - } + fun badgeRadiusDp(count: Int): Double = + when { + count < 2 -> 14.0 + count < 10 -> 17.0 + count < 100 -> 20.0 + count < 1000 -> 24.0 + else -> 28.0 + } - fun diameterDp(count: Int): Float = when { - count < 10 -> 34f - count < 100 -> 40f - count < 1000 -> 48f - else -> 56f - } + fun diameterDp(count: Int): Float = + when { + count < 10 -> 34f + count < 100 -> 40f + count < 1000 -> 48f + else -> 56f + } } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterIconFactory.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterIconFactory.kt index a32fcd1..f223f32 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterIconFactory.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/ClusterIconFactory.kt @@ -11,7 +11,9 @@ import com.google.android.gms.maps.model.BitmapDescriptorFactory import java.util.Locale /** Builds and caches circular cluster badge icons (gradient + soft shadow). */ -internal class ClusterIconFactory(private val density: Float) { +internal class ClusterIconFactory( + private val density: Float, +) { private val cache = HashMap() fun icon(count: Int): BitmapDescriptor { @@ -26,37 +28,42 @@ internal class ClusterIconFactory(private val density: Float) { val center = size / 2f val radius = diameter / 2f - val shadow = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.argb(70, 0, 0, 0) - maskFilter = android.graphics.BlurMaskFilter(2.5f * density, android.graphics.BlurMaskFilter.Blur.NORMAL) - } + val shadow = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.argb(70, 0, 0, 0) + maskFilter = android.graphics.BlurMaskFilter(2.5f * density, android.graphics.BlurMaskFilter.Blur.NORMAL) + } canvas.drawCircle(center, center + 1f * density, radius, shadow) - val fill = Paint(Paint.ANTI_ALIAS_FLAG).apply { - shader = RadialGradient( - center, - center - radius * 0.3f, - radius, - intArrayOf(Color.parseColor("#4D9EFF"), Color.parseColor("#0A84FF")), - null, - Shader.TileMode.CLAMP, - ) - } + val fill = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + shader = + RadialGradient( + center, + center - radius * 0.3f, + radius, + intArrayOf(Color.parseColor("#4D9EFF"), Color.parseColor("#0A84FF")), + null, + Shader.TileMode.CLAMP, + ) + } canvas.drawCircle(center, center, radius, fill) - val border = Paint(Paint.ANTI_ALIAS_FLAG).apply { - style = Paint.Style.STROKE - strokeWidth = 2f * density - color = Color.WHITE - } + val border = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + strokeWidth = 2f * density + color = Color.WHITE + } canvas.drawCircle(center, center, radius - density, border) - val text = Paint(Paint.ANTI_ALIAS_FLAG).apply { - color = Color.WHITE - textAlign = Paint.Align.CENTER - textSize = 13f * density - isFakeBoldText = true - } + val text = + Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = Color.WHITE + textAlign = Paint.Align.CENTER + textSize = 13f * density + isFakeBoldText = true + } val baseline = center - (text.descent() + text.ascent()) / 2 canvas.drawText(label, center, baseline, text) diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt index 06f555f..3cf9ac4 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/GoogleMapProviderAdapter.kt @@ -27,10 +27,8 @@ import com.margelo.nitro.core.Promise class GoogleMapProviderAdapter( private val context: ThemedReactContext, initialGoogleMapId: String?, -) : - MapProviderAdapter, +) : MapProviderAdapter, LifecycleEventListener { - private var googleMap: GoogleMap? = null private var isUserGesture = false private var hasFiredMapReady = false @@ -43,14 +41,15 @@ class GoogleMapProviderAdapter( private val googleMapIdAtCreation: String? = normalizeGoogleMapId(initialGoogleMapId) - override val view: MapView = MapView( - context, - GoogleMapOptions().apply { - googleMapIdAtCreation?.let { mapId -> - mapId(mapId) - } - }, - ) + override val view: MapView = + MapView( + context, + GoogleMapOptions().apply { + googleMapIdAtCreation?.let { mapId -> + mapId(mapId) + } + }, + ) private val lifecycle = MapViewLifecycleOwner(view) @@ -59,25 +58,27 @@ class GoogleMapProviderAdapter( /** React only mounts views while the host runs; [onHostPause] corrects this. */ private var isHostResumed = true - private val attachStateListener = object : View.OnAttachStateChangeListener { - override fun onViewAttachedToWindow(v: View) { - isAttachedToWindow = true - syncLifecycleState() - } + private val attachStateListener = + object : View.OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + isAttachedToWindow = true + syncLifecycleState() + } - override fun onViewDetachedFromWindow(v: View) { - isAttachedToWindow = false - syncLifecycleState() + override fun onViewDetachedFromWindow(v: View) { + isAttachedToWindow = false + syncLifecycleState() + } } - } - private val memoryCallbacks = object : ComponentCallbacks { - override fun onConfigurationChanged(newConfig: Configuration) = Unit + private val memoryCallbacks = + object : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) = Unit - override fun onLowMemory() { - lifecycle.onLowMemory() + override fun onLowMemory() { + lifecycle.onLowMemory() + } } - } init { context.addLifecycleEventListener(this) @@ -313,9 +314,10 @@ class GoogleMapProviderAdapter( syncMarkerPressHandlers() } - override fun fetchCamera(): Promise = promiseOnMain { - googleMap?.cameraPosition?.toCamera() ?: fallbackCamera() - } + override fun fetchCamera(): Promise = + promiseOnMain { + googleMap?.cameraPosition?.toCamera() ?: fallbackCamera() + } /** The camera the caller last asked for, used until the map itself can answer. */ private fun fallbackCamera(): Camera { @@ -325,10 +327,11 @@ class GoogleMapProviderAdapter( } return Camera( - center = Coordinate( - latitude = _region?.latitude ?: 0.0, - longitude = _region?.longitude ?: 0.0, - ), + center = + Coordinate( + latitude = _region?.latitude ?: 0.0, + longitude = _region?.longitude ?: 0.0, + ), zoom = 10.0, heading = null, pitch = null, @@ -340,14 +343,18 @@ class GoogleMapProviderAdapter( updateMapCamera(camera, animated = false) } - override fun animateCamera(camera: Camera, duration: Double?) { + override fun animateCamera( + camera: Camera, + duration: Double?, + ) { val animationDuration = duration ?: 0.25 updateMapCamera(camera, animated = true, durationMs = (animationDuration * 1000).toInt()) } - override fun getVisibleRegion(): Promise = promiseOnMain { - googleMap?.projection?.toNitroVisibleRegion() ?: emptyVisibleRegion() - } + override fun getVisibleRegion(): Promise = + promiseOnMain { + googleMap?.projection?.toNitroVisibleRegion() ?: emptyVisibleRegion() + } override fun fitToCoordinates( coordinates: Array, @@ -399,11 +406,12 @@ class GoogleMapProviderAdapter( * host is in the foreground. Leaving the window stops the map, never destroys it. */ private fun syncLifecycleState() { - val target = when { - !isAttachedToWindow -> MapViewLifecycleState.CREATED - isHostResumed -> MapViewLifecycleState.RESUMED - else -> MapViewLifecycleState.STARTED - } + val target = + when { + !isAttachedToWindow -> MapViewLifecycleState.CREATED + isHostResumed -> MapViewLifecycleState.RESUMED + else -> MapViewLifecycleState.STARTED + } lifecycle.moveTo(target) } @@ -522,9 +530,10 @@ class GoogleMapProviderAdapter( private fun syncMarkerPressHandlers() { overlayController.setMarkerPressHandlers( onMarkerPress = onMarkerPress, - onClusterPress = onClusterPress?.let { callback -> - { ids, coordinate -> callback(ids.toTypedArray(), coordinate) } - }, + onClusterPress = + onClusterPress?.let { callback -> + { ids, coordinate -> callback(ids.toTypedArray(), coordinate) } + }, ) } @@ -545,14 +554,16 @@ class GoogleMapProviderAdapter( return } - val hasFineLocationPermission = ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_FINE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED - val hasCoarseLocationPermission = ContextCompat.checkSelfPermission( - context, - Manifest.permission.ACCESS_COARSE_LOCATION, - ) == PackageManager.PERMISSION_GRANTED + val hasFineLocationPermission = + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_FINE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED + val hasCoarseLocationPermission = + ContextCompat.checkSelfPermission( + context, + Manifest.permission.ACCESS_COARSE_LOCATION, + ) == PackageManager.PERMISSION_GRANTED if (hasFineLocationPermission || hasCoarseLocationPermission) { map?.isMyLocationEnabled = true @@ -587,7 +598,10 @@ class GoogleMapProviderAdapter( map?.setMapStyle(MapStyleOptions(styleJson)) } - private fun applyRegion(region: Region, animated: Boolean = false) { + private fun applyRegion( + region: Region, + animated: Boolean = false, + ) { val map = googleMap ?: return val bounds = region.toLatLngBounds() val paddingPx = _mapPadding.toPaddingPixels() @@ -646,7 +660,10 @@ class GoogleMapProviderAdapter( runWhenViewLaidOut(view, block) } - private fun runWhenViewLaidOut(target: View, block: () -> Unit) { + private fun runWhenViewLaidOut( + target: View, + block: () -> Unit, + ) { if (target.width > 0 && target.height > 0) { updateOverlayViewportSize() block() diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/HybridMapView.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/HybridMapView.kt index 8eab2bf..ef7f568 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/HybridMapView.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/HybridMapView.kt @@ -12,10 +12,10 @@ private const val MAP_VIEW_NOT_MOUNTED_MESSAGE = "MapView is not mounted" @Keep @DoNotStrip -class HybridMapView(private val context: ThemedReactContext) : - HybridMapViewSpec(), +class HybridMapView( + private val context: ThemedReactContext, +) : HybridMapViewSpec(), RecyclableView { - /** Written on the UI thread, read from the JS thread by the imperative methods. */ @Volatile private var adapter: MapProviderAdapter? = null @@ -284,7 +284,10 @@ class HybridMapView(private val context: ThemedReactContext) : return Promise.resolved(Unit) } - override fun animateCamera(camera: Camera, duration: Double?): Promise { + override fun animateCamera( + camera: Camera, + duration: Double?, + ): Promise { val mounted = adapter ?: return notMountedRejection() mounted.animateCamera(camera, duration) return Promise.resolved(Unit) @@ -347,8 +350,7 @@ class HybridMapView(private val context: ThemedReactContext) : onClusterPress = null } - private fun notMountedRejection(): Promise = - Promise.rejected(IllegalStateException(MAP_VIEW_NOT_MOUNTED_MESSAGE)) + private fun notMountedRejection(): Promise = Promise.rejected(IllegalStateException(MAP_VIEW_NOT_MOUNTED_MESSAGE)) /** * Detaches and destroys the installed adapter. Both teardown paths land here: @@ -374,6 +376,7 @@ class HybridMapView(private val context: ThemedReactContext) : private fun makeAdapter(provider: MapProvider): MapProviderAdapter { return when (provider) { MapProvider.GOOGLE -> GoogleMapProviderAdapter(context, _googleMapId) + MapProvider.APPLE, MapProvider.OPENSTREETMAP, MapProvider.MAPBOX, diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt index ed08196..d2c4d9d 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapOverlayController.kt @@ -58,7 +58,10 @@ class MapOverlayController( } /** Updates the cached map viewport size used to size the clustering grid. */ - fun setViewportSize(widthPx: Int, heightPx: Int) { + fun setViewportSize( + widthPx: Int, + heightPx: Int, + ) { if (viewWidthPx == widthPx && viewHeightPx == heightPx) { return } @@ -164,12 +167,14 @@ class MapOverlayController( computeExecutor.execute { val candidates = index.candidates(bounds) - val elements: List = if (clustering) { - MarkerClusterEngine.clusters(candidates, bounds, widthPx, heightPx, density) - } else { - MarkerViewportFilter.displaySubset(candidates, bounds, latitudeSpan) - .map { ClusterElement.Single(it) } - } + val elements: List = + if (clustering) { + MarkerClusterEngine.clusters(candidates, bounds, widthPx, heightPx, density) + } else { + MarkerViewportFilter + .displaySubset(candidates, bounds, latitudeSpan) + .map { ClusterElement.Single(it) } + } val diff = computeMarkerRenderDiff(elements, displayedVersions) @@ -220,9 +225,10 @@ class MapOverlayController( when (element) { is ClusterElement.Single -> { val animation = enteringAnimation(element) - val shouldAnimate = animateEntering && - remainingAnimationBudget > 0 && - OverlayEnteringAnimationResolver.shouldRun(animation) + val shouldAnimate = + animateEntering && + remainingAnimationBudget > 0 && + OverlayEnteringAnimationResolver.shouldRun(animation) val options = element.descriptor.toMarkerOptions() if (shouldAnimate) { options.alpha(0f) @@ -239,15 +245,18 @@ class MapOverlayController( } } } + is ClusterElement.Cluster -> { val animation = enteringAnimation(element) - val shouldAnimate = animateEntering && - remainingAnimationBudget > 0 && - OverlayEnteringAnimationResolver.shouldRun(animation) - val options = MarkerOptions() - .position(element.position) - .icon(iconFactory.icon(element.count)) - .anchor(0.5f, 0.5f) + val shouldAnimate = + animateEntering && + remainingAnimationBudget > 0 && + OverlayEnteringAnimationResolver.shouldRun(animation) + val options = + MarkerOptions() + .position(element.position) + .icon(iconFactory.icon(element.count)) + .anchor(0.5f, 0.5f) if (shouldAnimate) { options.alpha(0f) } @@ -272,16 +281,18 @@ class MapOverlayController( when (element) { is ClusterElement.Single -> { marker.tag = element.descriptor.id - marker.position = LatLng( - element.descriptor.coordinate.latitude, - element.descriptor.coordinate.longitude, - ) + marker.position = + LatLng( + element.descriptor.coordinate.latitude, + element.descriptor.coordinate.longitude, + ) marker.title = element.descriptor.title marker.snippet = element.descriptor.subtitle marker.isDraggable = element.descriptor.draggable == true markerIconFactory.applyVisualProps(element.descriptor, marker, key) clusterByKey.remove(key) } + is ClusterElement.Cluster -> { marker.alpha = 1f marker.position = element.position @@ -301,23 +312,25 @@ class MapOverlayController( return } - val animated = added.mapNotNull { addedMarker -> - if (!OverlayEnteringAnimationResolver.shouldRun(addedMarker.animation)) { - return@mapNotNull null + val animated = + added.mapNotNull { addedMarker -> + if (!OverlayEnteringAnimationResolver.shouldRun(addedMarker.animation)) { + return@mapNotNull null + } + cancelEnteringAnimation(addedMarker.key) + addedMarker.marker.alpha = 0f + addedMarker } - cancelEnteringAnimation(addedMarker.key) - addedMarker.marker.alpha = 0f - addedMarker - } if (animated.isEmpty()) { return } val startDelayMs = animated.minOf { it.animation.delayMs } - val totalDurationMs = animated.maxOf { - it.animation.delayMs + it.animation.durationMs - } - startDelayMs + val totalDurationMs = + animated.maxOf { + it.animation.delayMs + it.animation.durationMs + } - startDelayMs val animator = ValueAnimator.ofFloat(0f, 1f) animator.duration = totalDurationMs @@ -327,22 +340,25 @@ class MapOverlayController( val elapsed = (animator.animatedFraction * duration).toLong() animated.forEach { animatedMarker -> val localElapsed = elapsed - (animatedMarker.animation.delayMs - startDelay) - val progress = (localElapsed.toFloat() / animatedMarker.animation.durationMs.toFloat()) - .coerceIn(0f, 1f) + val progress = + (localElapsed.toFloat() / animatedMarker.animation.durationMs.toFloat()) + .coerceIn(0f, 1f) animatedMarker.marker.alpha = progress * animatedMarker.targetAlpha } } - addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - revealAnimatedMarkers(animated) - clearCompletedAnimator(animation, animated) - } + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + revealAnimatedMarkers(animated) + clearCompletedAnimator(animation, animated) + } - override fun onAnimationCancel(animation: Animator) { - revealAnimatedMarkers(animated) - clearCompletedAnimator(animation, animated) - } - }) + override fun onAnimationCancel(animation: Animator) { + revealAnimatedMarkers(animated) + clearCompletedAnimator(animation, animated) + } + }, + ) } animated.forEach { markerEnterAnimators[it.key] = animator } animator.start() @@ -358,7 +374,10 @@ class MapOverlayController( markerEnterAnimators.remove(key)?.cancel() } - private fun clearCompletedAnimator(animator: Animator, animated: List) { + private fun clearCompletedAnimator( + animator: Animator, + animated: List, + ) { animated.forEach { animatedMarker -> if (markerEnterAnimators[animatedMarker.key] === animator) { markerEnterAnimators.remove(animatedMarker.key) @@ -368,11 +387,16 @@ class MapOverlayController( private fun enteringAnimation(element: ClusterElement): ResolvedOverlayEnteringAnimation { return when (element) { - is ClusterElement.Single -> OverlayEnteringAnimationResolver.resolve( - element.descriptor.enteringAnimation, - markerEnteringAnimation, - ) - is ClusterElement.Cluster -> OverlayEnteringAnimationResolver.resolve(clusterEnteringAnimation) + is ClusterElement.Single -> { + OverlayEnteringAnimationResolver.resolve( + element.descriptor.enteringAnimation, + markerEnteringAnimation, + ) + } + + is ClusterElement.Cluster -> { + OverlayEnteringAnimationResolver.resolve(clusterEnteringAnimation) + } } } @@ -412,10 +436,11 @@ class MapOverlayController( val key = "s:" + descriptor.id (marker.tag as? String)?.let { cancelEnteringAnimation(it) } marker.tag = descriptor.id - marker.position = LatLng( - descriptor.coordinate.latitude, - descriptor.coordinate.longitude, - ) + marker.position = + LatLng( + descriptor.coordinate.latitude, + descriptor.coordinate.longitude, + ) marker.title = descriptor.title marker.snippet = descriptor.subtitle marker.isDraggable = descriptor.draggable == true @@ -441,12 +466,13 @@ class MapOverlayController( private fun scheduleIdleRefresh() { cancelLiveRefresh() cancelIdleRefresh() - val runnable = Runnable { - idleRefreshRunnable = null - if (usesViewportPipeline()) { - refreshViewportMarkers() + val runnable = + Runnable { + idleRefreshRunnable = null + if (usesViewportPipeline()) { + refreshViewportMarkers() + } } - } idleRefreshRunnable = runnable mainHandler.postDelayed(runnable, IDLE_REFRESH_DEBOUNCE_MS) } @@ -463,10 +489,11 @@ class MapOverlayController( return } - val runnable = Runnable { - liveRefreshRunnable = null - runLiveRefresh() - } + val runnable = + Runnable { + liveRefreshRunnable = null + runLiveRefresh() + } liveRefreshRunnable = runnable mainHandler.postDelayed(runnable, LIVE_REFRESH_THROTTLE_MS - elapsed) } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapProviderAdapter.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapProviderAdapter.kt index 06f7e14..e9916c9 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapProviderAdapter.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapProviderAdapter.kt @@ -44,10 +44,21 @@ interface MapProviderAdapter { var onClusterPress: ((markerIds: Array, coordinate: Coordinate) -> Unit)? fun fetchCamera(): Promise + fun applyCamera(camera: Camera) - fun animateCamera(camera: Camera, duration: Double?) + + fun animateCamera( + camera: Camera, + duration: Double?, + ) + fun getVisibleRegion(): Promise - fun fitToCoordinates(coordinates: Array, padding: EdgePadding?, animated: Boolean?) + + fun fitToCoordinates( + coordinates: Array, + padding: EdgePadding?, + animated: Boolean?, + ) /** * Destroys the underlying native map and unregisters everything the adapter owns. diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapType+GoogleMap.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapType+GoogleMap.kt index 9a363b3..80cf732 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapType+GoogleMap.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapType+GoogleMap.kt @@ -3,9 +3,10 @@ package com.margelo.nitro.nitromaps import com.google.android.gms.maps.GoogleMap /** Converts the cross-platform map type to Google Maps' native type. */ -fun MapType.toGoogleMapType(): Int = when (this) { - MapType.STANDARD -> GoogleMap.MAP_TYPE_NORMAL - MapType.SATELLITE -> GoogleMap.MAP_TYPE_SATELLITE - MapType.HYBRID -> GoogleMap.MAP_TYPE_HYBRID - MapType.TERRAIN -> GoogleMap.MAP_TYPE_TERRAIN -} +fun MapType.toGoogleMapType(): Int = + when (this) { + MapType.STANDARD -> GoogleMap.MAP_TYPE_NORMAL + MapType.SATELLITE -> GoogleMap.MAP_TYPE_SATELLITE + MapType.HYBRID -> GoogleMap.MAP_TYPE_HYBRID + MapType.TERRAIN -> GoogleMap.MAP_TYPE_TERRAIN + } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapViewLifecycleOwner.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapViewLifecycleOwner.kt index a7503a9..007ebb0 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MapViewLifecycleOwner.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MapViewLifecycleOwner.kt @@ -9,7 +9,9 @@ import com.google.android.gms.maps.MapView * Detaching never destroys the map — only an explicit move to * [MapViewLifecycleState.DESTROYED] does — because a destroyed map cannot be resumed. */ -internal class MapViewLifecycleOwner(private val mapView: MapView) { +internal class MapViewLifecycleOwner( + private val mapView: MapView, +) { private var state = MapViewLifecycleState.CREATED val isDestroyed: Boolean @@ -70,7 +72,9 @@ internal class MapViewLifecycleOwner(private val mapView: MapView) { MapViewLifecycleState.RESUMED, MapViewLifecycleState.DESTROYED, - -> Unit + -> { + Unit + } } } @@ -88,7 +92,9 @@ internal class MapViewLifecycleOwner(private val mapView: MapView) { MapViewLifecycleState.CREATED, MapViewLifecycleState.DESTROYED, - -> Unit + -> { + Unit + } } } } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerClusterEngine.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerClusterEngine.kt index 71f4c56..0bf5d45 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerClusterEngine.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerClusterEngine.kt @@ -12,7 +12,9 @@ internal sealed interface ClusterElement { val diffKey: String val renderVersion: Long - data class Single(val descriptor: MarkerDescriptor) : ClusterElement { + data class Single( + val descriptor: MarkerDescriptor, + ) : ClusterElement { override val diffKey: String get() = "s:" + descriptor.id override val renderVersion: Long = descriptor.displayedIdentityVersion() } @@ -25,18 +27,19 @@ internal sealed interface ClusterElement { val bounds: LatLngBounds, ) : ClusterElement { override val diffKey: String get() = "c:$key" - override val renderVersion: Long = renderSignature( - "cluster", - key, - position.latitude, - position.longitude, - count, - memberIds.sorted(), - bounds.southwest.latitude, - bounds.southwest.longitude, - bounds.northeast.latitude, - bounds.northeast.longitude, - ) + override val renderVersion: Long = + renderSignature( + "cluster", + key, + position.latitude, + position.longitude, + count, + memberIds.sorted(), + bounds.southwest.latitude, + bounds.southwest.longitude, + bounds.northeast.latitude, + bounds.northeast.longitude, + ) } } @@ -50,16 +53,25 @@ internal sealed interface ClusterElement { internal object MarkerClusterEngine { private const val CELL_DP = 64.0 - private fun wrapsLongitude(sw: LatLng, ne: LatLng): Boolean { + private fun wrapsLongitude( + sw: LatLng, + ne: LatLng, + ): Boolean { return ne.longitude < sw.longitude } - private fun longitudeSpan(sw: LatLng, ne: LatLng): Double { + private fun longitudeSpan( + sw: LatLng, + ne: LatLng, + ): Double { val raw = ne.longitude - sw.longitude return if (raw < 0) raw + 360.0 else raw } - private fun normalizeLongitude(lon: Double, reference: Double): Double { + private fun normalizeLongitude( + lon: Double, + reference: Double, + ): Double { var normalized = lon while (normalized - reference > 180.0) { normalized -= 360.0 @@ -132,11 +144,12 @@ internal object MarkerClusterEngine { val buckets = HashMap() for (descriptor in clusterableCandidates) { val lat = descriptor.coordinate.latitude - val lon = if (wraps) { - normalizeLongitude(descriptor.coordinate.longitude, sw.longitude) - } else { - descriptor.coordinate.longitude - } + val lon = + if (wraps) { + normalizeLongitude(descriptor.coordinate.longitude, sw.longitude) + } else { + descriptor.coordinate.longitude + } val row = floor(lat / cellLat).toInt() val col = floor(lon / cellLon).toInt() val key = "$row:$col" @@ -155,14 +168,15 @@ internal object MarkerClusterEngine { bucket.memberIds.add(descriptor.id) } - val merged = mergeOverlapping( - ArrayList(buckets.values), - bounds, - wraps, - viewWidthPx, - viewHeightPx, - density, - ) + val merged = + mergeOverlapping( + ArrayList(buckets.values), + bounds, + wraps, + viewWidthPx, + viewHeightPx, + density, + ) val result = ArrayList(merged.size + singles.size) result.addAll(singles) @@ -177,10 +191,11 @@ internal object MarkerClusterEngine { position = LatLng(bucket.sumLat / bucket.count, bucket.sumLon / bucket.count), count = bucket.count, memberIds = bucket.memberIds, - bounds = LatLngBounds( - LatLng(bucket.minLat, wrapTo180(bucket.minLon)), - LatLng(bucket.maxLat, wrapTo180(bucket.maxLon)), - ), + bounds = + LatLngBounds( + LatLng(bucket.minLat, wrapTo180(bucket.minLon)), + LatLng(bucket.maxLat, wrapTo180(bucket.maxLon)), + ), ), ) } @@ -220,27 +235,30 @@ internal object MarkerClusterEngine { val centerLat = (sw.latitude + ne.latitude) / 2 val spanLat = maxOf(ne.latitude - sw.latitude, 1e-9) val spanLon = maxOf(longitudeSpan(sw, ne), 1e-9) - val centerLon = if (wraps) { - normalizeLongitude(sw.longitude + spanLon / 2, sw.longitude) - } else { - (sw.longitude + ne.longitude) / 2 - } + val centerLon = + if (wraps) { + normalizeLongitude(sw.longitude + spanLon / 2, sw.longitude) + } else { + (sw.longitude + ne.longitude) / 2 + } val px = DoubleArray(n) val py = DoubleArray(n) for (i in 0 until n) { val bucket = buckets[i] val lat = bucket.sumLat / bucket.count - val lon = if (wraps) { - normalizeLongitude(bucket.sumLon / bucket.count, sw.longitude) - } else { - bucket.sumLon / bucket.count - } + val lon = + if (wraps) { + normalizeLongitude(bucket.sumLon / bucket.count, sw.longitude) + } else { + bucket.sumLon / bucket.count + } px[i] = (lon - centerLon) / spanLon * width py[i] = (centerLat - lat) / spanLat * height } val parent = IntArray(n) { it } + fun find(value: Int): Int { var root = value while (parent[root] != root) { diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt index 6249df6..c44d27d 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerIconFactory.kt @@ -60,18 +60,24 @@ internal class MarkerIconFactory( ) } - private fun applyAnchor(descriptor: MarkerDescriptor, marker: Marker) { - val size = if (descriptor.image != null) { - displaySizePx(descriptor.image) ?: (0f to 0f) - } else { - defaultMarkerDisplaySizePx() - } + private fun applyAnchor( + descriptor: MarkerDescriptor, + marker: Marker, + ) { + val size = + if (descriptor.image != null) { + displaySizePx(descriptor.image) ?: (0f to 0f) + } else { + defaultMarkerDisplaySizePx() + } val (anchorX, anchorY) = descriptor.effectiveGoogleMapsAnchor(size.first, size.second, density) marker.setAnchor(anchorX, anchorY) } - private fun isMarkerCurrent(key: String, marker: Marker): Boolean = - markerRegistry()[key] === marker + private fun isMarkerCurrent( + key: String, + marker: Marker, + ): Boolean = markerRegistry()[key] === marker private fun applyIcon( marker: Marker, @@ -85,13 +91,14 @@ internal class MarkerIconFactory( if (isIconApplied(marker, iconKey)) { return } - val icon = if (markerColor == null) { - BitmapDescriptorFactory.defaultMarker() - } else { - val hsv = FloatArray(3) - Color.colorToHSV(markerColor.toColorInt(Color.RED), hsv) - BitmapDescriptorFactory.defaultMarker(hsv[0]) - } + val icon = + if (markerColor == null) { + BitmapDescriptorFactory.defaultMarker() + } else { + val hsv = FloatArray(3) + Color.colorToHSV(markerColor.toColorInt(Color.RED), hsv) + BitmapDescriptorFactory.defaultMarker(hsv[0]) + } marker.setIcon(icon) setApplied(marker, iconKey) onIconApplied() @@ -153,14 +160,16 @@ internal class MarkerIconFactory( return null } - private fun defaultMarkerDisplaySizePx(): Pair = - (DEFAULT_MARKER_WIDTH_DP * density) to (DEFAULT_MARKER_HEIGHT_DP * density) + private fun defaultMarkerDisplaySizePx(): Pair = (DEFAULT_MARKER_WIDTH_DP * density) to (DEFAULT_MARKER_HEIGHT_DP * density) private fun cacheKey(image: MarkerImage): String { return "${image.uri}|${image.width ?: ""}|${image.height ?: ""}|${image.scale ?: ""}" } - private fun loadLocalIcon(image: MarkerImage, key: String): BitmapDescriptor? { + private fun loadLocalIcon( + image: MarkerImage, + key: String, + ): BitmapDescriptor? { val bitmap = loadLocalBitmap(image) ?: return null return cacheBitmap(key, bitmap) } @@ -184,19 +193,25 @@ internal class MarkerIconFactory( } loadExecutor.execute { - val descriptor = if (isRemoteMarkerUri(image.uri)) { - loadRemoteIcon(image, key) - } else { - loadLocalIcon(image, key) - } + val descriptor = + if (isRemoteMarkerUri(image.uri)) { + loadRemoteIcon(image, key) + } else { + loadLocalIcon(image, key) + } deliverOnMainThread { onLoaded(descriptor) } } } - private fun isIconApplied(marker: Marker, key: String): Boolean = - appliedIconKeys[marker] == key + private fun isIconApplied( + marker: Marker, + key: String, + ): Boolean = appliedIconKeys[marker] == key - private fun setApplied(marker: Marker, key: String) { + private fun setApplied( + marker: Marker, + key: String, + ) { appliedIconKeys[marker] = key invalidatePendingLoad(marker) } @@ -225,7 +240,10 @@ internal class MarkerIconFactory( } } - private fun loadRemoteIcon(image: MarkerImage, key: String): BitmapDescriptor? { + private fun loadRemoteIcon( + image: MarkerImage, + key: String, + ): BitmapDescriptor? { remoteMarkerUriRejectReason(image.uri, resolveHostAddress = true)?.let { reason -> logRejectedRemoteMarkerUri(image.uri, reason) return null @@ -276,7 +294,10 @@ internal class MarkerIconFactory( } } - private fun decodeFile(path: String, image: MarkerImage): Bitmap? { + private fun decodeFile( + path: String, + image: MarkerImage, + ): Bitmap? { val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeFile(path, bounds) val options = buildDecodeOptions(bounds.outWidth, bounds.outHeight, image) ?: return null @@ -284,7 +305,10 @@ internal class MarkerIconFactory( return resizeBitmap(decoded, image) } - private fun decodeResource(resourceId: Int, image: MarkerImage): Bitmap? { + private fun decodeResource( + resourceId: Int, + image: MarkerImage, + ): Bitmap? { val bounds = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeResource(context.resources, resourceId, bounds) val options = buildDecodeOptions(bounds.outWidth, bounds.outHeight, image) ?: return null @@ -292,7 +316,10 @@ internal class MarkerIconFactory( return resizeBitmap(decoded, image) } - private fun decodeByteArray(bytes: ByteArray, image: MarkerImage): Bitmap? { + private fun decodeByteArray( + bytes: ByteArray, + image: MarkerImage, + ): Bitmap? { if (bytes.isEmpty()) { return null } @@ -317,12 +344,13 @@ internal class MarkerIconFactory( return null } val target = targetDecodeSizePx(image) - val sampleSize = computeInSampleSize( - sourceWidth = sourceWidth, - sourceHeight = sourceHeight, - reqWidth = target?.first, - reqHeight = target?.second, - ) + val sampleSize = + computeInSampleSize( + sourceWidth = sourceWidth, + sourceHeight = sourceHeight, + reqWidth = target?.first, + reqHeight = target?.second, + ) return BitmapFactory.Options().apply { inSampleSize = sampleSize inScaled = false @@ -356,13 +384,20 @@ internal class MarkerIconFactory( return sampleSize } - private fun decodedPixelCount(sourceWidth: Int, sourceHeight: Int, sampleSize: Int): Long { + private fun decodedPixelCount( + sourceWidth: Int, + sourceHeight: Int, + sampleSize: Int, + ): Long { val width = sourceWidth / sampleSize val height = sourceHeight / sampleSize return width.toLong() * height } - private fun cacheBitmap(key: String, bitmap: Bitmap): BitmapDescriptor { + private fun cacheBitmap( + key: String, + bitmap: Bitmap, + ): BitmapDescriptor { val descriptor = BitmapDescriptorFactory.fromBitmap(bitmap) cache.put(key, descriptor) sizeCache.put(key, bitmap.width.toFloat() to bitmap.height.toFloat()) @@ -381,7 +416,10 @@ internal class MarkerIconFactory( null } - private fun remoteMarkerUriRejectReason(uriString: String, resolveHostAddress: Boolean): String? { + private fun remoteMarkerUriRejectReason( + uriString: String, + resolveHostAddress: Boolean, + ): String? { val uri = parseRemoteMarkerUri(uriString) ?: return "invalid URI" when (uri.scheme?.lowercase(Locale.US)) { @@ -402,7 +440,10 @@ internal class MarkerIconFactory( return null } - private fun isAllowlistedRemoteHost(host: String, resolveHostAddress: Boolean): Boolean { + private fun isAllowlistedRemoteHost( + host: String, + resolveHostAddress: Boolean, + ): Boolean { if (host == "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) { return false } @@ -414,11 +455,12 @@ internal class MarkerIconFactory( return true } - val address = try { - InetAddress.getByName(host) - } catch (_: UnknownHostException) { - return !resolveHostAddress - } + val address = + try { + InetAddress.getByName(host) + } catch (_: UnknownHostException) { + return !resolveHostAddress + } return isAllowlistedRemoteAddress(address) } @@ -429,10 +471,11 @@ internal class MarkerIconFactory( } val parts = host.split('.') - return parts.size == 4 && parts.all { part -> - val value = part.toIntOrNull() ?: return@all false - value in 0..255 - } + return parts.size == 4 && + parts.all { part -> + val value = part.toIntOrNull() ?: return@all false + value in 0..255 + } } private fun isAllowlistedRemoteAddress(address: InetAddress): Boolean { @@ -456,11 +499,17 @@ internal class MarkerIconFactory( return true } - private fun logRejectedRemoteMarkerUri(uri: String, reason: String) { + private fun logRejectedRemoteMarkerUri( + uri: String, + reason: String, + ) { Log.w(TAG, "Rejected remote marker image URI ($reason): $uri") } - private fun resizeBitmap(source: Bitmap, image: MarkerImage): Bitmap { + private fun resizeBitmap( + source: Bitmap, + image: MarkerImage, + ): Bitmap { val width = image.width ?: return source val height = image.height ?: return source val targetWidth = (width * density).toInt().coerceAtLeast(1) @@ -483,9 +532,10 @@ internal class MarkerIconFactory( private val loadExecutor: ExecutorService = Executors.newSingleThreadExecutor() - private val BLOCKED_REMOTE_HOSTS = setOf( - "metadata.google.internal", - "metadata.goog", - ) + private val BLOCKED_REMOTE_HOSTS = + setOf( + "metadata.google.internal", + "metadata.goog", + ) } } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerSpatialIndex.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerSpatialIndex.kt index 0c53335..409187b 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerSpatialIndex.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerSpatialIndex.kt @@ -56,17 +56,21 @@ internal class MarkerSpatialIndex( } /** Markers whose grid cells overlap the padded bounds. */ - fun candidates(bounds: LatLngBounds, padding: Double = 0.2): List { + fun candidates( + bounds: LatLngBounds, + padding: Double = 0.2, + ): List { if (count == 0) { return emptyList() } val latSpan = bounds.northeast.latitude - bounds.southwest.latitude - val lonSpan = if (bounds.northeast.longitude < bounds.southwest.longitude) { - bounds.northeast.longitude - bounds.southwest.longitude + 360.0 - } else { - bounds.northeast.longitude - bounds.southwest.longitude - } + val lonSpan = + if (bounds.northeast.longitude < bounds.southwest.longitude) { + bounds.northeast.longitude - bounds.southwest.longitude + 360.0 + } else { + bounds.northeast.longitude - bounds.southwest.longitude + } val latPad = latSpan * padding val lonPad = lonSpan * padding @@ -88,7 +92,10 @@ internal class MarkerSpatialIndex( return result } - private fun longitudeColumns(minLon: Double, maxLon: Double): List { + private fun longitudeColumns( + minLon: Double, + maxLon: Double, + ): List { if (maxLon - minLon >= 360.0) { return (0 until side).toList() } @@ -113,7 +120,10 @@ internal class MarkerSpatialIndex( return wrapped } - private fun cellIndex(lat: Double, lon: Double): Int { + private fun cellIndex( + lat: Double, + lon: Double, + ): Int { return clampedRow(lat) * side + clampedColumn(lon) } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt index 036f881..dc87521 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/MarkerViewportFilter.kt @@ -22,11 +22,12 @@ internal object MarkerViewportFilter { val maxCount = maxMarkersForZoom(latitudeSpan) val paddedBounds = bounds.expandBy(0.2) - val visible = candidates.filter { descriptor -> - paddedBounds.contains( - LatLng(descriptor.coordinate.latitude, descriptor.coordinate.longitude), - ) - } + val visible = + candidates.filter { descriptor -> + paddedBounds.contains( + LatLng(descriptor.coordinate.latitude, descriptor.coordinate.longitude), + ) + } if (visible.size <= maxCount) { return visible diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/NitroMapsPackage.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/NitroMapsPackage.kt index 2feeb9e..6b0ebef 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/NitroMapsPackage.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/NitroMapsPackage.kt @@ -12,7 +12,10 @@ import com.margelo.nitro.nitromaps.views.HybridMapViewManager * manager and loads the native NitroMaps C++ library. */ class NitroMapsPackage : BaseReactPackage() { - override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? { + override fun getModule( + name: String, + reactContext: ReactApplicationContext, + ): NativeModule? { return null } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/OverlayEnteringAnimation.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/OverlayEnteringAnimation.kt index 3b0c1ab..6bbae97 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/OverlayEnteringAnimation.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/OverlayEnteringAnimation.kt @@ -22,14 +22,18 @@ internal object OverlayEnteringAnimationResolver { fallback: OverlayEnteringAnimationDescriptor? = null, ): ResolvedOverlayEnteringAnimation { val descriptor = animation ?: fallback - val kind = when (descriptor?.kind) { - OverlayEnteringAnimationKind.NONE -> ResolvedOverlayEnteringAnimationKind.NONE - OverlayEnteringAnimationKind.FADE -> ResolvedOverlayEnteringAnimationKind.FADE - OverlayEnteringAnimationKind.FADE_SCALE -> ResolvedOverlayEnteringAnimationKind.FADE - OverlayEnteringAnimationKind.SYSTEM, - null, - -> ResolvedOverlayEnteringAnimationKind.SYSTEM - } + val kind = + when (descriptor?.kind) { + OverlayEnteringAnimationKind.NONE -> ResolvedOverlayEnteringAnimationKind.NONE + + OverlayEnteringAnimationKind.FADE -> ResolvedOverlayEnteringAnimationKind.FADE + + OverlayEnteringAnimationKind.FADE_SCALE -> ResolvedOverlayEnteringAnimationKind.FADE + + OverlayEnteringAnimationKind.SYSTEM, + null, + -> ResolvedOverlayEnteringAnimationKind.SYSTEM + } return ResolvedOverlayEnteringAnimation( kind = kind, @@ -56,7 +60,10 @@ internal object OverlayEnteringAnimationResolver { return animation.durationMs > 0 } - private fun milliseconds(value: Double?, fallback: Long): Long { + private fun milliseconds( + value: Double?, + fallback: Long, + ): Long { if (value == null || !value.isFinite()) { return fallback } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/PolygonDescriptor+PolygonOptions.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/PolygonDescriptor+PolygonOptions.kt index 1219aba..50db49f 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/PolygonDescriptor+PolygonOptions.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/PolygonDescriptor+PolygonOptions.kt @@ -4,11 +4,12 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PolygonOptions fun PolygonDescriptor.toPolygonOptions(): PolygonOptions { - val options = PolygonOptions() - .addAll(coordinates.map { LatLng(it.latitude, it.longitude) }) - .strokeWidth((strokeWidth ?: 2.0).toFloat()) - .zIndex((zIndex ?: 0.0).toFloat()) - .clickable(tappable == true) + val options = + PolygonOptions() + .addAll(coordinates.map { LatLng(it.latitude, it.longitude) }) + .strokeWidth((strokeWidth ?: 2.0).toFloat()) + .zIndex((zIndex ?: 0.0).toFloat()) + .clickable(tappable == true) holes?.forEach { hole -> options.addHole(hole.map { LatLng(it.latitude, it.longitude) }) diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/PolylineDescriptor+PolylineOptions.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/PolylineDescriptor+PolylineOptions.kt index 054b3c7..02c2541 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/PolylineDescriptor+PolylineOptions.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/PolylineDescriptor+PolylineOptions.kt @@ -4,11 +4,12 @@ import com.google.android.gms.maps.model.LatLng import com.google.android.gms.maps.model.PolylineOptions fun PolylineDescriptor.toPolylineOptions(): PolylineOptions { - val options = PolylineOptions() - .addAll(coordinates.map { LatLng(it.latitude, it.longitude) }) - .width((strokeWidth ?: 4.0).toFloat()) - .zIndex((zIndex ?: 0.0).toFloat()) - .clickable(tappable == true) + val options = + PolylineOptions() + .addAll(coordinates.map { LatLng(it.latitude, it.longitude) }) + .width((strokeWidth ?: 4.0).toFloat()) + .zIndex((zIndex ?: 0.0).toFloat()) + .clickable(tappable == true) strokeColor?.let { options.color(it.toColorInt()) } diff --git a/package/android/src/main/java/com/margelo/nitro/nitromaps/String+ColorInt.kt b/package/android/src/main/java/com/margelo/nitro/nitromaps/String+ColorInt.kt index caf4566..2c0e37c 100644 --- a/package/android/src/main/java/com/margelo/nitro/nitromaps/String+ColorInt.kt +++ b/package/android/src/main/java/com/margelo/nitro/nitromaps/String+ColorInt.kt @@ -7,11 +7,12 @@ package com.margelo.nitro.nitromaps fun String.toColorInt(fallback: Int = 0xFF000000.toInt()): Int { val digits = trim().removePrefix("#") - val expanded = when (digits.length) { - 3, 4 -> digits.map { "$it$it" }.joinToString("") - 6, 8 -> digits - else -> return fallback - } + val expanded = + when (digits.length) { + 3, 4 -> digits.map { "$it$it" }.joinToString("") + 6, 8 -> digits + else -> return fallback + } val rgba = if (expanded.length == 6) "${expanded}FF" else expanded if (!rgba.all { it.digitToIntOrNull(16) != null }) { diff --git a/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerDisplayedIdentityTest.kt b/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerDisplayedIdentityTest.kt index ac15b88..3aad949 100644 --- a/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerDisplayedIdentityTest.kt +++ b/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerDisplayedIdentityTest.kt @@ -8,24 +8,25 @@ import org.junit.Test class MarkerDisplayedIdentityTest { @Test fun `visual field changes update displayed identity`() { - val pairs = listOf( - "image" to ( - marker(image = MarkerImage("asset:/pin.png", 32.0, 32.0, 2.0)) to - marker(image = MarkerImage("asset:/pin-alt.png", 32.0, 32.0, 2.0)) + val pairs = + listOf( + "image" to ( + marker(image = MarkerImage("asset:/pin.png", 32.0, 32.0, 2.0)) to + marker(image = MarkerImage("asset:/pin-alt.png", 32.0, 32.0, 2.0)) ), - "rotation" to (marker(rotation = 0.0) to marker(rotation = 45.0)), - "opacity" to (marker(opacity = 1.0) to marker(opacity = 0.4)), - "markerColor" to (marker(markerColor = "#FF0000") to marker(markerColor = "#00FF00")), - "zIndex" to (marker(zIndex = 1.0) to marker(zIndex = 2.0)), - "anchor" to ( - marker(anchor = MarkerAnchor(0.5, 1.0)) to marker(anchor = MarkerAnchor(0.5, 0.5)) + "rotation" to (marker(rotation = 0.0) to marker(rotation = 45.0)), + "opacity" to (marker(opacity = 1.0) to marker(opacity = 0.4)), + "markerColor" to (marker(markerColor = "#FF0000") to marker(markerColor = "#00FF00")), + "zIndex" to (marker(zIndex = 1.0) to marker(zIndex = 2.0)), + "anchor" to ( + marker(anchor = MarkerAnchor(0.5, 1.0)) to marker(anchor = MarkerAnchor(0.5, 0.5)) ), - "centerOffset" to ( - marker(centerOffset = MarkerPoint(0.0, 0.0)) to - marker(centerOffset = MarkerPoint(4.0, -8.0)) + "centerOffset" to ( + marker(centerOffset = MarkerPoint(0.0, 0.0)) to + marker(centerOffset = MarkerPoint(4.0, -8.0)) ), - "flat" to (marker(flat = false) to marker(flat = true)), - ) + "flat" to (marker(flat = false) to marker(flat = true)), + ) for ((field, pair) in pairs) { val (before, after) = pair @@ -59,22 +60,26 @@ class MarkerDisplayedIdentityTest { @Test fun `entering animation change does not update displayed identity`() { - val before = marker( - enteringAnimation = OverlayEnteringAnimationDescriptor( - OverlayEnteringAnimationKind.FADE, - 200.0, - 0.0, - OverlayEnteringAnimationReduceMotion.SYSTEM, - ), - ) - val after = marker( - enteringAnimation = OverlayEnteringAnimationDescriptor( - OverlayEnteringAnimationKind.NONE, - 400.0, - 50.0, - OverlayEnteringAnimationReduceMotion.NEVER, - ), - ) + val before = + marker( + enteringAnimation = + OverlayEnteringAnimationDescriptor( + OverlayEnteringAnimationKind.FADE, + 200.0, + 0.0, + OverlayEnteringAnimationReduceMotion.SYSTEM, + ), + ) + val after = + marker( + enteringAnimation = + OverlayEnteringAnimationDescriptor( + OverlayEnteringAnimationKind.NONE, + 400.0, + 50.0, + OverlayEnteringAnimationReduceMotion.NEVER, + ), + ) assertEquals(before.displayedIdentityVersion(), after.displayedIdentityVersion()) assertNotEquals(before.fingerprint(), after.fingerprint()) @@ -84,40 +89,46 @@ class MarkerDisplayedIdentityTest { fun `displayed identity change reaches retained list`() { val displayed = ClusterElement.Single(marker(opacity = 1.0)) val next = ClusterElement.Single(marker(opacity = 0.2)) - val diff = computeMarkerRenderDiff( - listOf(next), - mapOf(displayed.diffKey to displayed.renderVersion), - ) + val diff = + computeMarkerRenderDiff( + listOf(next), + mapOf(displayed.diffKey to displayed.renderVersion), + ) assertEquals(listOf(next), diff.retained) } @Test fun `entering animation change does not reach retained list`() { - val displayed = ClusterElement.Single( - marker( - enteringAnimation = OverlayEnteringAnimationDescriptor( - OverlayEnteringAnimationKind.FADE, - 200.0, - null, - null, + val displayed = + ClusterElement.Single( + marker( + enteringAnimation = + OverlayEnteringAnimationDescriptor( + OverlayEnteringAnimationKind.FADE, + 200.0, + null, + null, + ), ), - ), - ) - val next = ClusterElement.Single( - marker( - enteringAnimation = OverlayEnteringAnimationDescriptor( - OverlayEnteringAnimationKind.NONE, - 400.0, - null, - null, + ) + val next = + ClusterElement.Single( + marker( + enteringAnimation = + OverlayEnteringAnimationDescriptor( + OverlayEnteringAnimationKind.NONE, + 400.0, + null, + null, + ), ), - ), - ) - val diff = computeMarkerRenderDiff( - listOf(next), - mapOf(displayed.diffKey to displayed.renderVersion), - ) + ) + val diff = + computeMarkerRenderDiff( + listOf(next), + mapOf(displayed.diffKey to displayed.renderVersion), + ) assertTrue(diff.retained.isEmpty()) assertTrue(diff.added.isEmpty()) diff --git a/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerRenderDiffTest.kt b/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerRenderDiffTest.kt index 916e577..2a3c95f 100644 --- a/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerRenderDiffTest.kt +++ b/package/android/src/test/java/com/margelo/nitro/nitromaps/MarkerRenderDiffTest.kt @@ -19,10 +19,11 @@ class MarkerRenderDiffTest { @Test fun `missing keys are removed`() { val kept = ClusterElement.Single(marker(id = "a")) - val diff = computeMarkerRenderDiff( - listOf(kept), - mapOf("s:a" to kept.renderVersion, "s:gone" to 9L), - ) + val diff = + computeMarkerRenderDiff( + listOf(kept), + mapOf("s:a" to kept.renderVersion, "s:gone" to 9L), + ) assertEquals(setOf("s:gone"), diff.removedKeys) assertTrue(diff.added.isEmpty()) @@ -33,10 +34,11 @@ class MarkerRenderDiffTest { fun `version change marks retained`() { val displayed = ClusterElement.Single(marker(id = "a", opacity = 1.0)) val next = ClusterElement.Single(marker(id = "a", opacity = 0.2)) - val diff = computeMarkerRenderDiff( - listOf(next), - mapOf(displayed.diffKey to displayed.renderVersion), - ) + val diff = + computeMarkerRenderDiff( + listOf(next), + mapOf(displayed.diffKey to displayed.renderVersion), + ) assertTrue(diff.removedKeys.isEmpty()) assertTrue(diff.added.isEmpty()) @@ -46,10 +48,11 @@ class MarkerRenderDiffTest { @Test fun `unchanged version is skipped`() { val element = ClusterElement.Single(marker(id = "a")) - val diff = computeMarkerRenderDiff( - listOf(element), - mapOf(element.diffKey to element.renderVersion), - ) + val diff = + computeMarkerRenderDiff( + listOf(element), + mapOf(element.diffKey to element.renderVersion), + ) assertTrue(diff.removedKeys.isEmpty()) assertTrue(diff.added.isEmpty()) diff --git a/package/ios/CustomMapStyle+MKMapConfiguration.swift b/package/ios/CustomMapStyle+MKMapConfiguration.swift index 9192346..5f9c5d8 100644 --- a/package/ios/CustomMapStyle+MKMapConfiguration.swift +++ b/package/ios/CustomMapStyle+MKMapConfiguration.swift @@ -6,8 +6,9 @@ enum CustomMapStyleParser { @available(iOS 16.0, *) static func apply(json: String?, mapType: MapType, to mapView: MKMapView) { guard let json, !json.isEmpty, - let data = json.data(using: .utf8), - let rules = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] else { + let data = json.data(using: .utf8), + let rules = try? JSONSerialization.jsonObject(with: data) as? [[String: Any]] + else { mapView.preferredConfiguration = mapType.toMKMapConfiguration() return } diff --git a/package/ios/GoogleMapOverlayController.swift b/package/ios/GoogleMapOverlayController.swift index 6636451..a1183fc 100644 --- a/package/ios/GoogleMapOverlayController.swift +++ b/package/ios/GoogleMapOverlayController.swift @@ -125,10 +125,10 @@ final class GoogleMapOverlayController { func handleMarkerTap(_ marker: GMSMarker) -> Bool { switch marker.userData as? MarkerPayload { - case let .marker(id): + case .marker(let id): onMarkerPress?(id) return marker.title == nil && marker.snippet == nil - case let .cluster(memberIds, region): + case .cluster(let memberIds, let region): onClusterPress?( memberIds, Coordinate(latitude: marker.position.latitude, longitude: marker.position.longitude) @@ -141,7 +141,7 @@ final class GoogleMapOverlayController { } func handleMarkerDragEnd(_ marker: GMSMarker) { - guard case let .marker(id) = marker.userData as? MarkerPayload else { + guard case .marker(let id) = marker.userData as? MarkerPayload else { return } @@ -228,7 +228,8 @@ final class GoogleMapOverlayController { let marker = GMSMarker() updateMarker(marker, with: entry.element) let animation = enteringAnimation(for: entry.element) - let shouldAnimate = animateEntering + let shouldAnimate = + animateEntering && remainingAnimationBudget > 0 && OverlayEnteringAnimationResolver.canAnimateGoogleMarker(animation) @@ -275,7 +276,7 @@ final class GoogleMapOverlayController { for element: MarkerClusterEngine.Element ) -> ResolvedOverlayEnteringAnimation { switch element { - case let .single(descriptor): + case .single(let descriptor): return OverlayEnteringAnimationResolver.resolve( descriptor.enteringAnimation, fallback: markerEnteringAnimation @@ -287,7 +288,7 @@ final class GoogleMapOverlayController { private func updateMarker(_ marker: GMSMarker, with element: MarkerClusterEngine.Element) { switch element { - case let .single(descriptor): + case .single(let descriptor): marker.position = descriptor.coordinate.toCLLocationCoordinate2D() marker.title = descriptor.title marker.snippet = descriptor.subtitle @@ -295,7 +296,7 @@ final class GoogleMapOverlayController { marker.zIndex = Self.nativeZIndex(descriptor.zIndex) marker.userData = MarkerPayload.marker(descriptor.id) visualApplier.apply(descriptor, to: marker) - case let .cluster(_, coordinate, count, memberIds, region): + case .cluster(_, let coordinate, let count, let memberIds, let region): marker.position = coordinate marker.title = nil marker.snippet = nil @@ -323,10 +324,11 @@ final class GoogleMapOverlayController { let icon = UIGraphicsImageRenderer(size: CGSize(width: diameter, height: diameter), format: format) .image { context in let rect = CGRect(x: 0, y: 0, width: diameter, height: diameter) - let colors = [ - UIColor(red: 0.30, green: 0.62, blue: 1.0, alpha: 1).cgColor, - UIColor(red: 0.04, green: 0.52, blue: 1.0, alpha: 1).cgColor, - ] as CFArray + let colors = + [ + UIColor(red: 0.30, green: 0.62, blue: 1.0, alpha: 1).cgColor, + UIColor(red: 0.04, green: 0.52, blue: 1.0, alpha: 1).cgColor, + ] as CFArray let colorSpace = CGColorSpaceCreateDeviceRGB() let gradient = CGGradient(colorsSpace: colorSpace, colors: colors, locations: [0, 1])! context.cgContext.addEllipse(in: rect.insetBy(dx: 1, dy: 1)) @@ -412,9 +414,10 @@ final class GoogleMapOverlayController { polygon.path = descriptor.coordinates.toGMSPath() polygon.holes = descriptor.holes?.map { $0.toGMSPath() } polygon.strokeColor = descriptor.strokeColor?.toUIColor(fallback: .systemBlue) ?? .systemBlue - polygon.fillColor = descriptor.fillColor?.toUIColor( - fallback: UIColor.systemBlue.withAlphaComponent(0.2) - ) ?? UIColor.systemBlue.withAlphaComponent(0.2) + polygon.fillColor = + descriptor.fillColor?.toUIColor( + fallback: UIColor.systemBlue.withAlphaComponent(0.2) + ) ?? UIColor.systemBlue.withAlphaComponent(0.2) polygon.strokeWidth = CGFloat(descriptor.strokeWidth ?? 2) polygon.zIndex = Self.nativeZIndex(descriptor.zIndex) polygon.isTappable = descriptor.tappable ?? false @@ -434,9 +437,10 @@ final class GoogleMapOverlayController { circle.position = descriptor.center.toCLLocationCoordinate2D() circle.radius = descriptor.radius circle.strokeColor = descriptor.strokeColor?.toUIColor(fallback: .systemBlue) ?? .systemBlue - circle.fillColor = descriptor.fillColor?.toUIColor( - fallback: UIColor.systemBlue.withAlphaComponent(0.2) - ) ?? UIColor.systemBlue.withAlphaComponent(0.2) + circle.fillColor = + descriptor.fillColor?.toUIColor( + fallback: UIColor.systemBlue.withAlphaComponent(0.2) + ) ?? UIColor.systemBlue.withAlphaComponent(0.2) circle.strokeWidth = CGFloat(descriptor.strokeWidth ?? 2) circle.isTappable = descriptor.tappable ?? false circle.userData = descriptor.id @@ -478,8 +482,8 @@ extension PolylineDescriptor: IdentifiedOverlayDescriptor {} extension PolygonDescriptor: IdentifiedOverlayDescriptor {} extension CircleDescriptor: IdentifiedOverlayDescriptor {} -private extension Array where Element == Coordinate { - func toGMSPath() -> GMSPath { +extension Array where Element == Coordinate { + fileprivate func toGMSPath() -> GMSPath { let path = GMSMutablePath() for coordinate in self { path.add(coordinate.toCLLocationCoordinate2D()) diff --git a/package/ios/GoogleMapProviderAdapter.swift b/package/ios/GoogleMapProviderAdapter.swift index 95c5b6a..591ad83 100644 --- a/package/ios/GoogleMapProviderAdapter.swift +++ b/package/ios/GoogleMapProviderAdapter.swift @@ -32,11 +32,13 @@ final class GoogleMapProviderAdapter: NSObject, MapProviderAdapter { } lazy var view: GMSMapView = { - let camera = self.camera?.toGMSCameraPosition() + let camera = + self.camera?.toGMSCameraPosition() ?? GMSCameraPosition(latitude: 0, longitude: 0, zoom: 10) let mapView: GMSMapView if let googleMapId = _googleMapId?.trimmingCharacters(in: .whitespacesAndNewlines), - !googleMapId.isEmpty { + !googleMapId.isEmpty + { mapView = GMSMapView( frame: .zero, mapID: GMSMapID(identifier: googleMapId), @@ -454,10 +456,11 @@ final class GoogleMapProviderAdapter: NSObject, MapProviderAdapter { followedLocationMapView = mapView myLocationObservation = mapView.observe(\.myLocation, options: [.new]) { [weak self, weak mapView] _, change in guard let self, - self.followsUserLocation == true, - self.showsUserLocation == true, - let mapView, - let location = change.newValue ?? mapView.myLocation else { + self.followsUserLocation == true, + self.showsUserLocation == true, + let mapView, + let location = change.newValue ?? mapView.myLocation + else { return } diff --git a/package/ios/GoogleMapsAPIKey.swift b/package/ios/GoogleMapsAPIKey.swift index 0604b38..ecc1578 100644 --- a/package/ios/GoogleMapsAPIKey.swift +++ b/package/ios/GoogleMapsAPIKey.swift @@ -9,8 +9,9 @@ enum GoogleMapsAPIKey { static func configureIfNeeded() throws { let key = Bundle.main.object(forInfoDictionaryKey: "GoogleMapsIosApiKey") as? String guard let key = key?.trimmingCharacters(in: .whitespacesAndNewlines), - !key.isEmpty, - !key.hasPrefix("$(") else { + !key.isEmpty, + !key.hasPrefix("$(") + else { throw MapProviderConfigurationError.missingGoogleMapsIosApiKey } @@ -34,8 +35,9 @@ enum MapProviderConfigurationError: LocalizedError { case .missingGoogleMapsIosApiKey: return "react-native-better-maps: provider=\"google\" on iOS requires GoogleMapsIosApiKey in the host app Info.plist." case .googleMapsSdkNotLinked: - return "react-native-better-maps: provider=\"google\" on iOS requires the Google Maps SDK to be linked. Configure iosGoogleMapsApiKey or googleMapsApiKey in the config plugin, or set betterMaps.iosGoogleProvider=true in Podfile.properties.json, then run pod install." - case let .unsupportedIOSProvider(provider): + return + "react-native-better-maps: provider=\"google\" on iOS requires the Google Maps SDK to be linked. Configure iosGoogleMapsApiKey or googleMapsApiKey in the config plugin, or set betterMaps.iosGoogleProvider=true in Podfile.properties.json, then run pod install." + case .unsupportedIOSProvider(let provider): return "Map provider \"\(provider)\" is not supported on iOS." } } diff --git a/package/ios/GoogleMarkerVisualApplier.swift b/package/ios/GoogleMarkerVisualApplier.swift index e1f4dc2..97d64d8 100644 --- a/package/ios/GoogleMarkerVisualApplier.swift +++ b/package/ios/GoogleMarkerVisualApplier.swift @@ -51,9 +51,10 @@ final class GoogleMarkerVisualApplier { guard let image = descriptor.image else { cancelPending(state) let markerColor = descriptor.markerColor - let iconToken = markerColor.map { - "\(Self.defaultIconToken):\($0)" as NSString - } ?? Self.defaultIconToken + let iconToken = + markerColor.map { + "\(Self.defaultIconToken):\($0)" as NSString + } ?? Self.defaultIconToken if state.appliedImageToken != iconToken { marker.icon = markerColor.map { GMSMarker.markerImage(with: $0.toUIColor(fallback: .systemRed)) @@ -89,7 +90,7 @@ final class GoogleMarkerVisualApplier { return } guard let pending = state.pending, - pending.applicationToken === applicationToken + pending.applicationToken === applicationToken else { return } @@ -134,7 +135,7 @@ final class GoogleMarkerVisualApplier { private func anchorImageSize(_ descriptor: MarkerDescriptor, icon: UIImage?) -> CGSize { if let image = descriptor.image, let width = image.width, let height = image.height, - width > 0, height > 0 + width > 0, height > 0 { return CGSize(width: width, height: height) } diff --git a/package/ios/HybridMapView.swift b/package/ios/HybridMapView.swift index 05dca26..de8d26f 100644 --- a/package/ios/HybridMapView.swift +++ b/package/ios/HybridMapView.swift @@ -350,7 +350,8 @@ final class HybridMapView: HybridMapViewSpec { } if let lifecycle, - lifecycle.isRecycled || lifecycle.generation != currentGeneration { + lifecycle.isRecycled || lifecycle.generation != currentGeneration + { throw Self.mapViewNotMountedError() } } @@ -376,17 +377,17 @@ final class HybridMapView: HybridMapViewSpec { case .apple: return AppleMapProviderAdapter() case .google: -#if canImport(GoogleMaps) + #if canImport(GoogleMaps) do { return try GoogleMapProviderAdapter(googleMapId: withStateLock { self._state.googleMapId }) } catch { return UnavailableMapProviderAdapter(error: error) } -#else + #else return UnavailableMapProviderAdapter( error: MapProviderConfigurationError.googleMapsSdkNotLinked ) -#endif + #endif case .openstreetmap, .mapbox: return UnavailableMapProviderAdapter( error: MapProviderConfigurationError.unsupportedIOSProvider(provider) diff --git a/package/ios/HybridMapViewDelegate.swift b/package/ios/HybridMapViewDelegate.swift index 67891a2..9303e8e 100644 --- a/package/ios/HybridMapViewDelegate.swift +++ b/package/ios/HybridMapViewDelegate.swift @@ -101,9 +101,10 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { if let cluster = annotation as? MapClusterAnnotation { - let view = mapView.dequeueReusableAnnotationView( - withIdentifier: NitroClusterAnnotationView.reuseIdentifier - ) as? NitroClusterAnnotationView + let view = + mapView.dequeueReusableAnnotationView( + withIdentifier: NitroClusterAnnotationView.reuseIdentifier + ) as? NitroClusterAnnotationView ?? NitroClusterAnnotationView( annotation: cluster, reuseIdentifier: NitroClusterAnnotationView.reuseIdentifier @@ -119,19 +120,21 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni } if marker.image != nil { - let imageView = mapView.dequeueReusableAnnotationView( - withIdentifier: NitroImageAnnotationView.reuseIdentifier, - for: marker - ) as! NitroImageAnnotationView + let imageView = + mapView.dequeueReusableAnnotationView( + withIdentifier: NitroImageAnnotationView.reuseIdentifier, + for: marker + ) as! NitroImageAnnotationView imageView.configure(for: marker) return imageView } - let pinView = mapView.dequeueReusableAnnotationView( - withIdentifier: NitroPinAnnotationView.reuseIdentifier, - for: marker - ) as! NitroPinAnnotationView + let pinView = + mapView.dequeueReusableAnnotationView( + withIdentifier: NitroPinAnnotationView.reuseIdentifier, + for: marker + ) as! NitroPinAnnotationView pinView.configure(for: marker) return pinView @@ -140,14 +143,16 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) { for view in views { if let marker = view.annotation as? MapMarkerAnnotation, - marker.enteringAnimation.kind != .system { + marker.enteringAnimation.kind != .system + { OverlayEnteringAnimationResolver.animateAnnotationView( view, animation: marker.enteringAnimation, supportsScale: true ) } else if let cluster = view.annotation as? MapClusterAnnotation, - cluster.enteringAnimation.kind != .system { + cluster.enteringAnimation.kind != .system + { OverlayEnteringAnimationResolver.animateAnnotationView( view, animation: cluster.enteringAnimation, @@ -159,8 +164,9 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { if #available(iOS 16.0, *), - let mapFeature = view.annotation as? MKMapFeatureAnnotation, - handleMapFeatureSelection(mapFeature) { + let mapFeature = view.annotation as? MKMapFeatureAnnotation, + handleMapFeatureSelection(mapFeature) + { return } @@ -187,7 +193,8 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni func mapView(_ mapView: MKMapView, didSelect annotation: MKAnnotation) { if #available(iOS 16.0, *), - let mapFeature = annotation as? MKMapFeatureAnnotation { + let mapFeature = annotation as? MKMapFeatureAnnotation + { _ = handleMapFeatureSelection(mapFeature) } } @@ -221,7 +228,8 @@ final class HybridMapViewDelegate: NSObject, MKMapViewDelegate, UIGestureRecogni fromOldState oldState: MKAnnotationView.DragState ) { guard newState == .ending, - let marker = view.annotation as? MapMarkerAnnotation else { + let marker = view.annotation as? MapMarkerAnnotation + else { return } diff --git a/package/ios/MapMarkerAnnotation.swift b/package/ios/MapMarkerAnnotation.swift index 97e8d04..9c916ec 100644 --- a/package/ios/MapMarkerAnnotation.swift +++ b/package/ios/MapMarkerAnnotation.swift @@ -62,15 +62,17 @@ final class MapMarkerAnnotation: NSObject, MKAnnotation { draggable = nextDraggable isClusterable = nextClusterable - let imageChanged = switch (image, descriptor.image) { - case (nil, nil): false - case let (current?, next?): - MarkerImageLoader.cacheKey(for: current) != MarkerImageLoader.cacheKey(for: next) - default: true - } + let imageChanged = + switch (image, descriptor.image) { + case (nil, nil): false + case (let current?, let next?): + MarkerImageLoader.cacheKey(for: current) != MarkerImageLoader.cacheKey(for: next) + default: true + } let markerColorChanged = markerColor != descriptor.markerColor let anchorChanged = anchor?.x != descriptor.anchor?.x || anchor?.y != descriptor.anchor?.y - let centerOffsetChanged = centerOffset?.x != descriptor.centerOffset?.x + let centerOffsetChanged = + centerOffset?.x != descriptor.centerOffset?.x || centerOffset?.y != descriptor.centerOffset?.y let rotationChanged = rotation != descriptor.rotation let flatChanged = flat != descriptor.flat diff --git a/package/ios/MapOverlayController.swift b/package/ios/MapOverlayController.swift index 73a6378..4127fc4 100644 --- a/package/ios/MapOverlayController.swift +++ b/package/ios/MapOverlayController.swift @@ -146,14 +146,14 @@ final class MapOverlayController { } switch entry.element { - case let .single(descriptor): + case .single(let descriptor): if let marker = existing as? MapMarkerAnnotation { let visualChanged = marker.update(from: descriptor) if visualChanged { refreshMarkerView(for: marker) } } - case let .cluster(key, coordinate, count, memberIds, region): + case .cluster(let key, let coordinate, let count, let memberIds, let region): if let cluster = existing as? MapClusterAnnotation { cluster.update( id: key, diff --git a/package/ios/MarkerClusterEngine.swift b/package/ios/MarkerClusterEngine.swift index d45c6b6..4dc7287 100644 --- a/package/ios/MarkerClusterEngine.swift +++ b/package/ios/MarkerClusterEngine.swift @@ -21,18 +21,18 @@ enum MarkerClusterEngine { /// instead of removing and re-adding the native marker during gestures. var diffKey: String { switch self { - case let .single(descriptor): + case .single(let descriptor): return "s:" + descriptor.id - case let .cluster(key, _, _, _, _): + case .cluster(let key, _, _, _, _): return "c:" + key } } var renderVersion: Int { switch self { - case let .single(descriptor): + case .single(let descriptor): return descriptor.displayedIdentityVersion() - case let .cluster(key, coordinate, count, memberIds, region): + case .cluster(let key, let coordinate, let count, let memberIds, let region): var hasher = Hasher() hasher.combine("cluster") hasher.combine(key) @@ -55,7 +55,7 @@ enum MarkerClusterEngine { clusterEnteringAnimation: OverlayEnteringAnimationDescriptor? ) -> MKAnnotation { switch self { - case let .single(descriptor): + case .single(let descriptor): return MapMarkerAnnotation( descriptor: descriptor, enteringAnimation: OverlayEnteringAnimationResolver.resolve( @@ -63,7 +63,7 @@ enum MarkerClusterEngine { fallback: markerEnteringAnimation ) ) - case let .cluster(key, coordinate, count, memberIds, region): + case .cluster(let key, let coordinate, let count, let memberIds, let region): return MapClusterAnnotation( id: key, coordinate: coordinate, @@ -187,7 +187,8 @@ enum MarkerClusterEngine { var buckets: [String: Bucket] = [:] for descriptor in clusterableCandidates { let lat = descriptor.coordinate.latitude - let lon = wraps + let lon = + wraps ? normalizeLongitude(descriptor.coordinate.longitude, reference: referenceLon) : descriptor.coordinate.longitude let row = Int((lat / cellLat).rounded(.down)) @@ -223,21 +224,22 @@ enum MarkerClusterEngine { if bucket.count == 1, let descriptor = bucket.first { elements.append(.single(descriptor)) } else { - elements.append(.cluster( - key: bucket.key, - coordinate: CLLocationCoordinate2D( - latitude: bucket.sumLat / Double(bucket.count), - longitude: bucket.sumLon / Double(bucket.count) - ), - count: bucket.count, - memberIds: bucket.memberIds, - region: expandedRegion( - minLat: bucket.minLat, - maxLat: bucket.maxLat, - minLon: wrapTo180(bucket.minLon), - maxLon: wrapTo180(bucket.maxLon) - ) - )) + elements.append( + .cluster( + key: bucket.key, + coordinate: CLLocationCoordinate2D( + latitude: bucket.sumLat / Double(bucket.count), + longitude: bucket.sumLon / Double(bucket.count) + ), + count: bucket.count, + memberIds: bucket.memberIds, + region: expandedRegion( + minLat: bucket.minLat, + maxLat: bucket.maxLat, + minLon: wrapTo180(bucket.minLon), + maxLon: wrapTo180(bucket.maxLon) + ) + )) } } return elements @@ -264,7 +266,8 @@ enum MarkerClusterEngine { let referenceLon = region.center.longitude - region.span.longitudeDelta / 2 let spanLat = max(region.span.latitudeDelta, 1e-9) let spanLon = max(region.span.longitudeDelta, 1e-9) - let centerLon = wraps + let centerLon = + wraps ? normalizeLongitude(referenceLon + spanLon / 2, reference: referenceLon) : region.center.longitude @@ -273,7 +276,8 @@ enum MarkerClusterEngine { for i in 0.. Bool { +extension MKCoordinateRegion { + fileprivate func contains(_ coordinate: Coordinate, padding: Double) -> Bool { let latPadding = span.latitudeDelta * padding let lonPadding = span.longitudeDelta * padding let minLat = center.latitude - span.latitudeDelta / 2 - latPadding diff --git a/package/ios/NitroImageAnnotationView.swift b/package/ios/NitroImageAnnotationView.swift index 4e2aee1..ab01643 100644 --- a/package/ios/NitroImageAnnotationView.swift +++ b/package/ios/NitroImageAnnotationView.swift @@ -24,9 +24,10 @@ final class NitroImageAnnotationView: MKAnnotationView { isDraggable = marker.draggable canShowCallout = marker.title != nil || marker.subtitle != nil alpha = marker.opacity - zPriority = marker.zIndex.map { - MKAnnotationViewZPriority(rawValue: Float($0)) - } ?? .defaultUnselected + zPriority = + marker.zIndex.map { + MKAnnotationViewZPriority(rawValue: Float($0)) + } ?? .defaultUnselected guard let imageDescriptor = marker.image else { loadToken = nil @@ -49,8 +50,9 @@ final class NitroImageAnnotationView: MKAnnotationView { guard let self else { return } guard let marker = self.annotation as? MapMarkerAnnotation, - let image = marker.image, - MarkerImageLoader.cacheKey(for: image) == token else { + let image = marker.image, + MarkerImageLoader.cacheKey(for: image) == token + else { if self.loadToken == token { self.loadToken = nil } @@ -70,7 +72,8 @@ final class NitroImageAnnotationView: MKAnnotationView { centerOffset = marker.centerOffset(forImageSize: imageSize) let rotation = marker.rotation ?? 0 - transform = marker.flat != true && rotation != 0 + transform = + marker.flat != true && rotation != 0 ? CGAffineTransform(rotationAngle: rotation * .pi / 180) : .identity } diff --git a/package/ios/NitroPinAnnotationView.swift b/package/ios/NitroPinAnnotationView.swift index 9226cab..0c52091 100644 --- a/package/ios/NitroPinAnnotationView.swift +++ b/package/ios/NitroPinAnnotationView.swift @@ -30,16 +30,18 @@ final class NitroPinAnnotationView: MKMarkerAnnotationView { displayPriority = .required alpha = marker.opacity markerTintColor = marker.markerColor?.toUIColor(fallback: .systemRed) - zPriority = marker.zIndex.map { - MKAnnotationViewZPriority(rawValue: Float($0)) - } ?? .defaultUnselected + zPriority = + marker.zIndex.map { + MKAnnotationViewZPriority(rawValue: Float($0)) + } ?? .defaultUnselected layoutIfNeeded() let pinSize = bounds.size == .zero ? Self.defaultPinSize : bounds.size centerOffset = marker.centerOffset(forImageSize: pinSize) let rotation = marker.rotation ?? 0 - transform = marker.flat != true && rotation != 0 + transform = + marker.flat != true && rotation != 0 ? CGAffineTransform(rotationAngle: rotation * .pi / 180) : .identity } diff --git a/package/ios/OverlayEnteringAnimation.swift b/package/ios/OverlayEnteringAnimation.swift index 914ebc8..882eee2 100644 --- a/package/ios/OverlayEnteringAnimation.swift +++ b/package/ios/OverlayEnteringAnimation.swift @@ -1,8 +1,9 @@ +import QuartzCore +import UIKit + #if canImport(GoogleMaps) import GoogleMaps #endif -import QuartzCore -import UIKit enum ResolvedOverlayEnteringAnimationKind: Equatable { case none @@ -81,7 +82,7 @@ enum OverlayEnteringAnimationResolver { } } -#if canImport(GoogleMaps) + #if canImport(GoogleMaps) static func prepareGoogleMarker(_ marker: GMSMarker, animation: ResolvedOverlayEnteringAnimation) { marker.appearAnimation = .none marker.iconView = nil @@ -176,7 +177,7 @@ enum OverlayEnteringAnimationResolver { } } -#endif + #endif private static func seconds( fromMilliseconds value: Double?, diff --git a/package/tsconfig.json b/package/tsconfig.json index b0190d2..d744904 100644 --- a/package/tsconfig.json +++ b/package/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../config/tsconfig.json", "compilerOptions": { "rootDir": "src", "outDir": "lib/typescript", diff --git a/package/tsconfig.plugin.json b/package/tsconfig.plugin.json index fa11563..17d5538 100644 --- a/package/tsconfig.plugin.json +++ b/package/tsconfig.plugin.json @@ -1,5 +1,5 @@ { - "extends": "../tsconfig.base.json", + "extends": "../config/tsconfig.json", "compilerOptions": { "module": "commonjs", "moduleResolution": "node", diff --git a/scripts/clang-format.sh b/scripts/clang-format.sh new file mode 100755 index 0000000..fcb832c --- /dev/null +++ b/scripts/clang-format.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +set -e + +# Run from the repository root, whatever the caller's working directory is, so +# the config path below always resolves. +cd "$(dirname "$0")/.." + +CPP_DIRS=( + # shared C++ + "package/cpp" + # Android JNI adapter + "package/android/src/main/cpp" + # iOS — Objective-C / Objective-C++ bridging files, if any + "package/ios" +) + +# Note: package/nitrogen is generated by nitrogen and git-ignored. It is never +# formatted here — regenerating it would undo the changes anyway. + +if which clang-format >/dev/null; then + DIRS=$(printf "%s " "${CPP_DIRS[@]}") + find $DIRS -type f \( -name "*.h" -o -name "*.hpp" -o -name "*.cpp" -o -name "*.m" -o -name "*.mm" -o -name "*.c" \) -print0 | while read -r -d '' file; do + clang-format -style=file:./config/.clang-format -i "$file" + done + echo "C++ Format done!" +else + echo "error: clang-format not installed, install with 'brew install clang-format' (or manually from https://clang.llvm.org/docs/ClangFormat.html )" + exit 1 +fi diff --git a/scripts/kotlin-format.sh b/scripts/kotlin-format.sh new file mode 100755 index 0000000..8f51436 --- /dev/null +++ b/scripts/kotlin-format.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")/.." + +KOTLIN_PATTERNS=( + "package/android/src/main/java/**/*.kt" + "package/android/src/test/java/**/*.kt" +) + +if which ktlint >/dev/null; then + ktlint --editorconfig=./config/.editorconfig --format "${KOTLIN_PATTERNS[@]}" + echo "Kotlin Format done!" +else + echo "error: ktlint not installed, install with 'brew install ktlint' (see https://github.com/pinterest/ktlint )" + exit 1 +fi diff --git a/scripts/swift-format.sh b/scripts/swift-format.sh new file mode 100755 index 0000000..64474c6 --- /dev/null +++ b/scripts/swift-format.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +set -e + +cd "$(dirname "$0")/.." + +SWIFT_DIRS=( + "package/ios" + "package/iosTests" +) + +if which swift >/dev/null; then + DIRS=$(printf "%s " "${SWIFT_DIRS[@]}") + find $DIRS -type f \( -name "*.swift" \) -print0 | while read -r -d '' file; do + swift format --configuration ./config/.swift-format --in-place "$file" + done + echo "Swift Format done!" +else + echo "error: swift not installed, install the toolchain with Xcode." + exit 1 +fi