diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 0000000..9ae5b5e --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,10 @@ +# Keep CodeRabbit within the OSS per-review file cap: focus on plugin source. +reviews: + path_filters: + - "!example-app/**" + - "!.github/**" + - "!.maestro/**" + - "!assets/**" + - "!README.md" + - "!CHANGELOG.md" + - "!AGENTS.md" diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..d36e039 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +build +dist +example-app +docs diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..436be98 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: riderx diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..2698e92 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,84 @@ +--- +name: ๐Ÿšจ Bug Report +about: Report something not working +title: 'bug: ' +labels: '' +assignees: '' + +--- + +## Bug Report + +### Capacitor Version + + +``` +PASTE OUTPUT HERE +``` + +### Plugin Version + +``` +PASTE OUTPUT HERE +``` + +### context(s) + +```md +ManualModel: false +AutoMode: false +CapgoCloud: false +OnPremise: false +``` + +### Platform(s) + + + + +### Current Behavior + + + + +### Expected Behavior + + + + +### Code Reproduction + + + + +### Other Technical Details + + +`npm --version` output: + +`node --version` output: + +`pod --version` output (iOS issues only): + +### Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..d898438 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,41 @@ +--- +name: โšก๏ธ Feature Request +about: Request a feature or change +title: 'feat: ' +labels: '' +assignees: '' + +--- + +## Feature Request + +### Description + + + + +### Platform(s) + + + + +### Preferred Solution + + + + +### Alternatives + + + + +### Additional Context + + + diff --git a/.github/scripts/capture-android-readme-screenshot.sh b/.github/scripts/capture-android-readme-screenshot.sh new file mode 100755 index 0000000..0570724 --- /dev/null +++ b/.github/scripts/capture-android-readme-screenshot.sh @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="${GITHUB_WORKSPACE:-$(cd "$(dirname "$0")/../.." && pwd)}" +APK="$ROOT/example-app/android/app/build/outputs/apk/debug/app-debug.apk" + +adb wait-for-device +adb shell 'while [[ -z $(getprop sys.boot_completed) ]]; do sleep 2; done;' + +echo "Waiting for Android package manager..." +for _ in $(seq 1 120); do + packages="$(adb shell cmd package list packages 2>/dev/null || true)" + if echo "$packages" | grep -q package; then + break + fi + sleep 3 +done +adb shell input keyevent KEYCODE_WAKEUP +adb shell input keyevent 82 +sleep 30 + +for attempt in 1 2 3 4 5; do + if adb install -r -g "$APK"; then + break + fi + echo "adb install attempt $attempt failed, retrying..." + sleep 20 + if [ "$attempt" = 5 ]; then + exit 1 + fi +done + +adb shell am start -n app.capgo.nativemap/.MainActivity +sleep 50 +adb exec-out screencap -p > "$ROOT/readme-screenshot-android.png" diff --git a/.github/scripts/verify-packed-example.sh b/.github/scripts/verify-packed-example.sh new file mode 100755 index 0000000..0c220ad --- /dev/null +++ b/.github/scripts/verify-packed-example.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +set -euo pipefail + +platform="${1:-}" +case "$platform" in + android | ios | web) ;; + *) + echo "Usage: $0 " + exit 1 + ;; +esac + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +tmp_root="${RUNNER_TEMP:-$(mktemp -d)}" +pack_dir="$tmp_root/plugin-package" +test_app="$tmp_root/plugin-example-app" + +cd "$repo_root" + +bun run build + +rm -rf "$pack_dir" "$test_app" +mkdir -p "$pack_dir" "$test_app" +bun pm pack --destination "$pack_dir" --quiet + +shopt -s nullglob +packed_packages=("$pack_dir"/*.tgz) +shopt -u nullglob +if [ "${#packed_packages[@]}" -ne 1 ]; then + echo "Expected exactly one package tarball, found ${#packed_packages[@]}" + exit 1 +fi + +plugin_name="$(bun -e 'console.log(require("./package.json").name)')" +cp -R example-app/. "$test_app/" +cd "$test_app" +bun remove "$plugin_name" +bun add "${packed_packages[0]}" +bun run build + +case "$platform" in + android) + if [[ ! -d android ]]; then + bunx cap add android + fi + bunx cap sync android + cd android + ./gradlew build test + ;; + ios) + if [[ ! -d ios ]]; then + bunx cap add ios + fi + bunx cap sync ios + xcodebuild -project ios/App/App.xcodeproj -scheme App -destination generic/platform=iOS CODE_SIGNING_ALLOWED=NO + ;; + web) + ;; +esac diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..c1e788e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,89 @@ +name: Build and publish + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + tags: + - "*" + +jobs: + # Tests already passed before tag was created, so just build and deploy + deploy: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + name: "Build and publish to npm" + permissions: + contents: write + id-token: write + steps: + - name: Check out + uses: actions/checkout@v7 + with: + fetch-depth: 0 + filter: blob:none + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies + id: install_code + run: bun i + - name: Check plugin wiring + run: bun run check:wiring + - name: Build + id: build_code + run: bun run build + - name: Generate AI changelog + id: changelog + continue-on-error: true + env: + CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} + CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + run: bun run changelog:ai + - name: Setup Node.js for npm publish + uses: actions/setup-node@v7 + with: + node-version: 24.x + registry-url: 'https://registry.npmjs.org' + - name: Install npm CLI for staged publishing + run: npm install -g npm@^11.15.0 + - name: Stage publish to npm + if: ${{ !contains(github.ref, '-alpha.') }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm --version + npm stage publish --provenance --access public --ignore-scripts + - name: Stage publish to npm with next tag + if: ${{ contains(github.ref, '-alpha.') }} + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm --version + npm stage publish --tag next --provenance --access public --ignore-scripts + - name: Request stage approval + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.NPM_STAGE_DISPATCH_TOKEN }} + run: | + gh api --method POST repos/Cap-go/automations/dispatches \ + -f event_type=npm-stage-approve \ + -f "client_payload[repository]=${GITHUB_REPOSITORY}" \ + -f "client_payload[run_id]=${GITHUB_RUN_ID}" \ + -f "client_payload[package]=$(node -p "require('./package.json').name")" + - name: Create GitHub release + id: create_release + uses: softprops/action-gh-release@v3 + with: + body: | + ## ๐Ÿ†• Changelog + + ${{ steps.changelog.outputs.result || '_Changelog was not generated automatically for this release._' }} + + --- + + ๐Ÿ”— **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.changelog.outputs.from_tag || github.ref_name }}...${{ steps.changelog.outputs.to_tag || github.ref_name }} + make_latest: true + token: '${{ secrets.PERSONAL_ACCESS_TOKEN }}' + prerelease: ${{ contains(github.ref, '-alpha.') }} diff --git a/.github/workflows/bump_version.yml b/.github/workflows/bump_version.yml new file mode 100644 index 0000000..61e045f --- /dev/null +++ b/.github/workflows/bump_version.yml @@ -0,0 +1,63 @@ +name: Bump version + +on: + push: + branches: + - main + - development + paths-ignore: + - 'example-app/**' + - '.github/**' + +jobs: + # Run all tests first before creating any tags + test: + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} + # Called workflow jobs are capped at 10 minutes; never raise that unless explicitly asked. + uses: ./.github/workflows/test.yml + + # Only bump version and create tag if all tests pass + bump-version: + needs: [test] + if: ${{ !startsWith(github.event.head_commit.message, 'chore(release):') }} + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + name: "Bump version and create tag" + steps: + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Check out + uses: actions/checkout@v7 + with: + fetch-depth: 0 + filter: blob:none + token: '${{ secrets.PERSONAL_ACCESS_TOKEN }}' + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies + id: install_code + run: bun i + - name: Git config + run: | + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + - name: Update Doc + run: bun run docgen + - name: Add doc to github + run: | + git add README.md + git commit --m "docs: update doc" || true + - name: Create version bump main + if: github.ref == 'refs/heads/main' + run: bunx capacitor-plugin-standard-version@latest --skip.changelog + - name: Create version bump development + if: github.ref != 'refs/heads/main' + run: bunx capacitor-plugin-standard-version@latest --prerelease alpha --skip.changelog + - name: Push to origin + run: | + CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) + remote_repo="https://${GITHUB_ACTOR}:${{ secrets.PERSONAL_ACCESS_TOKEN }}@github.com/${GITHUB_REPOSITORY}.git" + git pull $remote_repo $CURRENT_BRANCH + git push $remote_repo HEAD:$CURRENT_BRANCH --follow-tags --tags diff --git a/.github/workflows/github-releases-to-discord.yml b/.github/workflows/github-releases-to-discord.yml new file mode 100644 index 0000000..22c7cdc --- /dev/null +++ b/.github/workflows/github-releases-to-discord.yml @@ -0,0 +1,25 @@ +name: GitHub Releases to Discord + +on: + release: + types: [published] + +jobs: + github-releases-to-discord: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Checkout + uses: actions/checkout@v7 + - name: GitHub Releases to Discord + uses: SethCohen/github-releases-to-discord@v1 + with: + webhook_url: ${{ secrets.WEBHOOK_DISCORD_RELEASE_URL }} + color: "2105893" + footer_title: "Release @${{ github.repository }}" + reduce_headings: true diff --git a/.github/workflows/pr_beta_prompt.yml b/.github/workflows/pr_beta_prompt.yml new file mode 100644 index 0000000..961d0d9 --- /dev/null +++ b/.github/workflows/pr_beta_prompt.yml @@ -0,0 +1,86 @@ +name: PR beta publish prompt + +on: + pull_request_target: + types: + - opened + - reopened + - synchronize + - ready_for_review + +permissions: + issues: write + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Upsert beta publish comment + uses: actions/github-script@v9 + env: + IS_SAME_REPO: ${{ github.event.pull_request.head.repo.full_name == github.repository && 'true' || 'false' }} + with: + script: | + const marker = ''; + const sameRepo = process.env.IS_SAME_REPO === 'true'; + const pr = context.payload.pull_request; + const body = sameRepo + ? [ + marker, + '', + '## Beta npm build', + '', + 'Maintainers can publish this PR to npm for fast testing.', + '', + 'Comment `/publish-beta` after the PR checks are green.', + '', + 'The workflow will:', + '- publish a prerelease package on the `beta` tag', + `- add a pinned \`pr-${pr.number}\` dist-tag for this exact PR build`, + '- update this comment with the install command', + '', + 'Security note: beta publish is only enabled for branches inside this repository.', + ].join('\n') + : [ + marker, + '', + '## Beta npm build', + '', + 'This PR comes from a fork, so beta publish is disabled for security.', + '', + 'If you need a beta package, move the branch into this repository first.', + ].join('\n'); + + const { owner, repo } = context.repo; + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pr.number, + per_page: 100, + }); + + const existing = comments.find((comment) => comment.user.type === 'Bot' && comment.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr.number, + body, + }); diff --git a/.github/workflows/pr_beta_publish.yml b/.github/workflows/pr_beta_publish.yml new file mode 100644 index 0000000..f82e70f --- /dev/null +++ b/.github/workflows/pr_beta_publish.yml @@ -0,0 +1,324 @@ +name: Publish PR beta package + +on: + issue_comment: + types: + - created + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to publish + required: true + type: number + +concurrency: + group: pr-beta-${{ github.event.issue.number || inputs.pr_number }} + cancel-in-progress: false + +permissions: + contents: read + id-token: write + issues: write + pull-requests: write + +jobs: + publish: + if: ${{ github.event_name == 'workflow_dispatch' || (github.event.issue.pull_request && contains(github.event.comment.body, '/publish-beta')) }} + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Resolve PR and access checks + id: pr + uses: actions/github-script@v9 + with: + script: | + const prNumber = context.eventName === 'workflow_dispatch' + ? Number(context.payload.inputs?.pr_number ?? 0) + : context.payload.issue.number; + + const { owner, repo } = context.repo; + const { data: pr } = await github.rest.pulls.get({ + owner, + repo, + pull_number: prNumber, + }); + + const actor = context.eventName === 'issue_comment' + ? context.payload.comment.user.login + : context.actor; + + core.setOutput('actor', actor); + core.setOutput('pr_number', String(prNumber)); + + if (pr.state !== 'open') { + core.setFailed(`PR #${prNumber} is not open.`); + return; + } + + if (pr.head.repo.full_name !== `${owner}/${repo}`) { + core.setFailed('Fork PRs cannot publish beta packages because the workflow needs repository secrets.'); + return; + } + + if (context.eventName === 'issue_comment') { + const actor = context.payload.comment.user.login; + const { data: permission } = await github.rest.repos.getCollaboratorPermissionLevel({ + owner, + repo, + username: actor, + }); + const allowed = ['admin', 'maintain', 'write'].includes(permission.permission); + + if (!allowed) { + core.setFailed(`@${actor} does not have permission to publish beta packages.`); + return; + } + } + + const prState = await github.graphql( + `query($owner: String!, $repo: String!, $prNumber: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $prNumber) { + commits(last: 1) { + nodes { + commit { + statusCheckRollup { + state + } + } + } + } + } + } + }`, + { + owner, + repo, + prNumber, + }, + ); + + const rollupState = prState.repository.pullRequest.commits.nodes[0]?.commit.statusCheckRollup?.state ?? 'PENDING'; + + if (rollupState !== 'SUCCESS') { + core.setFailed(`PR checks are not green yet (current state: ${rollupState}).`); + return; + } + core.setOutput('head_sha', pr.head.sha); + + - name: Mark publish as running + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const prNumber = Number('${{ steps.pr.outputs.pr_number }}'); + const actor = '${{ steps.pr.outputs.actor }}'; + const body = [ + marker, + '', + '## Beta npm build', + '', + 'Status: publishing', + '', + `Requested by @${actor}.`, + '', + `This run will publish the PR head to the \`beta\` npm tag.`, + '', + `Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const existing = comments.find((comment) => comment.user.type === 'Bot' && comment.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + + - name: Check out PR head + uses: actions/checkout@v7 + with: + ref: ${{ steps.pr.outputs.head_sha }} + fetch-depth: 0 + filter: blob:none + persist-credentials: false + + - uses: oven-sh/setup-bun@v2 + + - name: Install dependencies + run: bun i + + - name: Check plugin wiring + run: bun run check:wiring + + - name: Set beta package version + id: version + env: + PR_NUMBER: ${{ steps.pr.outputs.pr_number }} + run: | + node <<'NODE' >> "$GITHUB_OUTPUT" + const fs = require('fs'); + const path = 'package.json'; + const pkg = JSON.parse(fs.readFileSync(path, 'utf8')); + const baseVersion = pkg.version.split('-')[0]; + const prNumber = process.env.PR_NUMBER; + const runNumber = process.env.GITHUB_RUN_NUMBER; + const runAttempt = process.env.GITHUB_RUN_ATTEMPT; + const betaVersion = `${baseVersion}-beta.pr${prNumber}.${runNumber}.${runAttempt}`; + + pkg.version = betaVersion; + fs.writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`); + + console.log(`package_name=${pkg.name}`); + console.log(`version=${betaVersion}`); + NODE + + - name: Build + run: bun run build + + - name: Setup Node.js for npm publish + uses: actions/setup-node@v7 + with: + node-version: 24.x + registry-url: 'https://registry.npmjs.org' + + - name: Install npm CLI for staged publishing + run: npm install -g npm@^11.15.0 + + - name: Stage publish to npm beta + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + npm --version + npm stage publish --tag beta --provenance --access public --ignore-scripts + + + - name: Request stage approval + continue-on-error: true + env: + GH_TOKEN: ${{ secrets.NPM_STAGE_DISPATCH_TOKEN }} + run: | + gh api --method POST repos/Cap-go/automations/dispatches \ + -f event_type=npm-stage-approve \ + -f "client_payload[repository]=${GITHUB_REPOSITORY}" \ + -f "client_payload[run_id]=${GITHUB_RUN_ID}" \ + -f "client_payload[package]=${{ steps.version.outputs.package_name }}" + - name: Publish success comment + if: ${{ success() }} + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const prNumber = Number('${{ steps.pr.outputs.pr_number }}'); + const actor = '${{ steps.pr.outputs.actor }}'; + const packageName = '${{ steps.version.outputs.package_name }}'; + const version = '${{ steps.version.outputs.version }}'; + const shortSha = '${{ steps.pr.outputs.head_sha }}'.slice(0, 7); + const body = [ + marker, + '', + '## Beta npm build', + '', + 'Status: staged', + '', + `Version: \`${version}\``, + `Source: \`${shortSha}\``, + `Requested by @${actor}`, + '', + 'Latest beta:', + '```sh', + `bun add ${packageName}@beta`, + `bun add ${packageName}@${version}`, + '```', + '', + `Run: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const existing = comments.find((comment) => comment.user.type === 'Bot' && comment.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + + - name: Publish failure comment + if: ${{ failure() && steps.pr.outputs.pr_number != '' }} + uses: actions/github-script@v9 + with: + script: | + const marker = ''; + const prNumber = Number('${{ steps.pr.outputs.pr_number }}'); + const body = [ + marker, + '', + '## Beta npm build', + '', + 'Status: failed', + '', + 'Open the run for details:', + `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + per_page: 100, + }); + + const existing = comments.find((comment) => comment.user.type === 'Bot' && comment.body.includes(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + return; + } + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); diff --git a/.github/workflows/readme-screenshots.yml b/.github/workflows/readme-screenshots.yml new file mode 100644 index 0000000..1a1620f --- /dev/null +++ b/.github/workflows/readme-screenshots.yml @@ -0,0 +1,162 @@ +name: Capture README screenshots + +on: + workflow_dispatch: + +concurrency: + group: readme-screenshots-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + ios: + runs-on: macos-latest + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Build plugin and example app + run: | + set -euo pipefail + bun install + bun run build + cd example-app + bun install + bun run build + bunx cap sync ios + - name: Resolve iOS Simulator device + id: sim + run: | + set -euo pipefail + DEVICE_NAME="$(python3 <<'PY' + import json, subprocess, sys + out = subprocess.check_output(["xcrun", "simctl", "list", "devices", "available", "-j"], text=True) + data = json.loads(out) + preferred = ("iPhone 16", "iPhone 15", "iPhone 14") + for name in preferred: + for runtime, devices in data.get("devices", {}).items(): + if "iOS" not in runtime: + continue + for device in devices: + if device.get("isAvailable") and device.get("name") == name: + print(name) + sys.exit(0) + for runtime, devices in data.get("devices", {}).items(): + if "iOS" not in runtime: + continue + for device in devices: + if device.get("isAvailable") and "iPhone" in device.get("name", ""): + print(device["name"]) + sys.exit(0) + sys.exit("No available iPhone simulator found") + PY + )" + echo "device_name=$DEVICE_NAME" >> "$GITHUB_OUTPUT" + - name: Build for iOS Simulator + run: | + set -euo pipefail + cd example-app/ios/App + xcodebuild \ + -project App.xcodeproj \ + -scheme App \ + -configuration Debug \ + -sdk iphonesimulator \ + -derivedDataPath "$RUNNER_TEMP/DerivedData" \ + -destination "platform=iOS Simulator,name=${{ steps.sim.outputs.device_name }}" \ + CODE_SIGNING_ALLOWED=NO + - name: Launch app and capture screenshot + run: | + set -euo pipefail + DEVICE_NAME="${{ steps.sim.outputs.device_name }}" + xcrun simctl boot "$DEVICE_NAME" 2>/dev/null || true + xcrun simctl bootstatus booted -b + APP_PATH="$(find "$RUNNER_TEMP/DerivedData" -name 'App.app' -path '*iphonesimulator*' | head -1)" + if [ -z "$APP_PATH" ]; then + echo "Could not find App.app in DerivedData" + exit 1 + fi + xcrun simctl install booted "$APP_PATH" + xcrun simctl launch booted app.capgo.nativemap + sleep 45 + xcrun simctl io booted screenshot "$GITHUB_WORKSPACE/readme-screenshot-ios.png" + - name: Upload iOS screenshot + uses: actions/upload-artifact@v7 + with: + name: readme-screenshot-ios + path: readme-screenshot-ios.png + if-no-files-found: error + + android: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Setup Java + uses: actions/setup-java@v6 + with: + distribution: zulu + java-version: 21 + - name: Record Google Maps API key availability + id: maps_key + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + run: | + set -euo pipefail + if [ -n "${GOOGLE_MAPS_API_KEY:-}" ]; then + echo "present=true" >> "$GITHUB_OUTPUT" + echo "google_maps_api_key_present=true" > maps-key-status.txt + else + echo "present=false" >> "$GITHUB_OUTPUT" + echo "google_maps_api_key_present=false" > maps-key-status.txt + fi + - name: Report Maps API key availability + run: cat maps-key-status.txt + - name: Build plugin and example app + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + run: | + set -euo pipefail + bun install + bun run build + cd example-app + bun install + bun run build + bunx cap sync android + - name: Assemble debug APK + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + run: | + set -euo pipefail + cd example-app/android + ./gradlew assembleDebug + - name: Run emulator, install app, capture screenshot + uses: reactivecircus/android-emulator-runner@v2 + env: + GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} + with: + api-level: 29 + arch: x86_64 + target: google_apis + profile: pixel_4 + disable-animations: false + emulator-boot-timeout-minutes: 10 + emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim + script: bash .github/scripts/capture-android-readme-screenshot.sh + - name: Upload Android screenshot and key status + uses: actions/upload-artifact@v7 + with: + name: readme-screenshot-android + path: | + readme-screenshot-android.png + maps-key-status.txt + if-no-files-found: error diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..815e78a --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,105 @@ +name: Build source code and test it + +on: + push: + branches: + - renovate/** + pull_request: + branches: [ main ] + workflow_call: # Allow this workflow to be called by other workflows + +jobs: + guard_swiftpm_version: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v7 + - name: Enforce capacitor-swift-pm version + run: | + set -euo pipefail + if ! grep -Eq 'https://github.com/ionic-team/capacitor-swift-pm\.git"[[:space:]]*,[[:space:]]*from:[[:space:]]*"8\.0\.0"' Package.swift; then + echo "Expected capacitor-swift-pm to be pinned to 8.0.0 in Package.swift" + exit 1 + fi + build_android: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Install dependencies + id: install_code + run: bun i + - name: Check plugin wiring + run: bun run check:wiring + - name: Check Cap 9 deprecated APIs + run: bun run check:cap9-deprecated + - name: Setup java + uses: actions/setup-java@v6 + with: + distribution: 'zulu' + java-version: '21' + - name: Build + id: build_code + run: bun run verify:android + - name: Verify packed package in example app (Android) + shell: bash + run: bash ./.github/scripts/verify-packed-example.sh android + build_ios: + runs-on: macOS-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + steps: + - name: Check out + uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 24.x + - name: Install dependencies + id: install_code + run: bun i + - name: Check plugin wiring + run: bun run check:wiring + - name: Check Cap 9 deprecated APIs + run: bun run check:cap9-deprecated + - name: Build + id: build_code + run: bun run verify:ios + - name: Verify packed package in example app (iOS) + shell: bash + run: bash ./.github/scripts/verify-packed-example.sh ios + web: + runs-on: ubuntu-latest + # Keep this job capped at 10 minutes; never raise it unless explicitly asked. + timeout-minutes: 10 + name: 'Build code and test' + steps: + - name: Check out + uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + - name: Install dependencies + id: install_code + run: bun i + - name: Check plugin wiring + run: bun run check:wiring + - name: Check Cap 9 deprecated APIs + run: bun run check:cap9-deprecated + - name: Lint + id: lint_code + run: bun run lint + - name: Verify packed package in example app (Web) + shell: bash + run: bash ./.github/scripts/verify-packed-example.sh web + - name: Verify + id: verify_code + run: bun run verify:web diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0de7761 --- /dev/null +++ b/.gitignore @@ -0,0 +1,78 @@ +# node files +dist +node_modules + +# iOS files +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +Package.resolved +/.build + + +# macOS files +.DS_Store + + + +# Based on Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin +gen +out + +# Gradle files +.gradle +build + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation + +# Android Studio captures folder +captures + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Example app build output +example-app/node_modules +example-app/dist +package-lock.json + +# Development artifacts +.swiftpm/ +.maestro/results/ + +# Generated scripts +scripts/configure-dependencies.js diff --git a/.maestro/android/basic-features.yaml b/.maestro/android/basic-features.yaml new file mode 100644 index 0000000..32f3170 --- /dev/null +++ b/.maestro/android/basic-features.yaml @@ -0,0 +1,4 @@ +--- +- launchApp: + clearState: true +- runFlow: ../flows/native-map-smoke.yaml diff --git a/.maestro/flows/native-map-smoke.yaml b/.maestro/flows/native-map-smoke.yaml new file mode 100644 index 0000000..abf6185 --- /dev/null +++ b/.maestro/flows/native-map-smoke.yaml @@ -0,0 +1,4 @@ +appId: app.capgo.nativemap +name: Native map smoke +--- +- assertVisible: "@capgo/capacitor-native-map" diff --git a/.maestro/ios/basic-features.yaml b/.maestro/ios/basic-features.yaml new file mode 100644 index 0000000..32f3170 --- /dev/null +++ b/.maestro/ios/basic-features.yaml @@ -0,0 +1,4 @@ +--- +- launchApp: + clearState: true +- runFlow: ../flows/native-map-smoke.yaml diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f629a7a --- /dev/null +++ b/.npmignore @@ -0,0 +1,60 @@ +# node files +dist/ +node_modules/ + +# iOS files +Pods +Build +xcuserdata + +# macOS files +.DS_Store + + + +# Based on Android gitignore template: https://github.com/github/gitignore/blob/master/Android.gitignore + +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea + +# Keystore files +# Uncomment the following line if you do not want to check your keystore files in. +#*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..5ab6e14 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +example-app diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1d0df45 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# AGENTS.md + +This file provides guidance to AI agents and contributors working on this Capacitor plugin. + +## Quick Start + +```bash +# Install dependencies +bun install + +# Build the plugin (TypeScript + Rollup + docgen) +bun run build + +# Full verification (iOS, Android, Web) +bun run verify + +# Format code (ESLint + Prettier + SwiftLint) +bun run fmt + +# Lint without fixing +bun run lint +``` + +## Development Workflow + +1. **Install** - `bun install` (never use npm) +2. **Build** - `bun run build` compiles TypeScript, generates docs, and bundles with Rollup +3. **Verify** - `bun run verify` builds for iOS, Android, and Web. Always run this before submitting work +4. **Format** - `bun run fmt` auto-fixes ESLint, Prettier, and SwiftLint issues +5. **Lint** - `bun run lint` checks code quality without modifying files + +### Individual Platform Verification + +```bash +bun run verify:ios +bun run verify:android +bun run verify:web +``` + +### Example App + +If an `example-app/` directory exists, you can test the plugin locally: + +```bash +cd example-app +bun install +bun run start +``` + +The example app references the plugin via `file:..`. Use `bunx cap sync ` to sync native platforms. + +## Project Structure + +- `src/definitions.ts` - TypeScript interfaces and types (source of truth for API docs) +- `src/index.ts` - Plugin registration +- `src/web.ts` - Web implementation +- `ios/Sources/` - iOS native code (Swift) +- `android/src/main/` - Android native code (Java/Kotlin) +- `dist/` - Generated output (do not edit manually) +- `Package.swift` - SwiftPM definition +- `*.podspec` - CocoaPods spec + +## iOS Package Management + +We always support both **CocoaPods** and **Swift Package Manager (SPM)**. Every plugin must ship a valid `*.podspec` and `Package.swift`. Do not remove or break either integration โ€” users depend on both. + +## API Documentation + +API docs in the README are auto-generated from JSDoc in `src/definitions.ts`. **Never edit the `` or `` sections in README.md directly.** Instead, update `src/definitions.ts` and run `bun run docgen` (also runs as part of `bun run build`). Document any important default or future-major default candidate in `src/definitions.ts` so the next Capacitor major upgrade can change it deliberately. + +## Versioning + +The plugin major version must always follow the Capacitor major version (e.g., plugin v8 for Capacitor 8). **Do not introduce breaking changes in `src/definitions.ts` unless explicitly asked or the current definition is broken or unusable.** Breaking changes belong to the matching Capacitor major migration, and all other changes must stay backward compatible. + +## Changelog + +`CHANGELOG.md` is managed automatically by CI/CD. Do not edit it manually. + +## Pull Request Guidelines + +We welcome contributions, including AI-generated pull requests. Every PR must include: + +### Required Sections + +1. **What** - What does this PR change? +2. **Why** - What is the reason for this change? +3. **How** - How did you approach the implementation? +4. **Testing** - What did you test? How did you verify it works? +5. **Not Tested** - What is not yet tested or needs further validation? + +### Rules + +- **No breaking changes** unless aligned with a new Capacitor major release. +- Run `bun run verify` and `bun run fmt` before opening a PR. CI will catch failures, but catching them locally saves time. +- If you are an AI agent, that is perfectly fine. Just be transparent about it. We care that the code is correct and helpful, not who wrote it. +- We review PRs on a best-effort basis. We may request changes โ€” you are expected to address them for the PR to be merged. +- We use automated code review tools (CodeRabbit, and others). You will need to respond to their feedback and resolve any issues they raise. +- We have automatic releases. Once merged, your change will ship in the next release cycle. + +### PR Template + +``` +## What +- [Brief description of the change] + +## Why +- [Motivation for this change] + +## How +- [Implementation approach] + +## Testing +- [What was tested and how] + +## Not Tested +- [What still needs testing, if anything] +``` + +## Common Pitfalls + +- Always rename Swift/Java classes and package IDs when creating a new plugin from a template โ€” leftover names cause registration conflicts. +- We only use Java 21 for Android builds. +- Keep temporary files clean: delete or mark with `deleteOnExit` after use. +- `dist/` is fully regenerated on every build โ€” never edit generated files. +- Use Bun for everything. Do not use npm or npx. Use `bunx` if you need to run a package binary. +- Production and PR beta publishes use `npm stage publish`. Plugin CI only has the org `NPM_TOKEN`. + +## Timeout Policy + +- Keep CI, script, and runtime timeouts at 10 minutes or less. Use `timeout-minutes: 10` or lower in GitHub Actions and cap timeout values at `600000` ms, `600` seconds, or `10m` unless explicitly requested. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c9fe824 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.0.1 + +Initial release: unified `@capgo/capacitor-native-map` plugin with Google Maps on Android, MapKit on iOS, and Google Maps JS on web. diff --git a/CapgoCapacitorNativeMap.podspec b/CapgoCapacitorNativeMap.podspec new file mode 100644 index 0000000..e9a3036 --- /dev/null +++ b/CapgoCapacitorNativeMap.podspec @@ -0,0 +1,16 @@ +require 'json' + +package = JSON.parse(File.read(File.join(__dir__, 'package.json'))) + +Pod::Spec.new do |s| + s.name = 'CapgoCapacitorNativeMap' + s.version = package['version'] + s.summary = package['description'] + s.license = package['license'] + s.homepage = package['repository']['url'] + s.author = package['author'] + s.source = { :git => package['repository']['url'].sub(/\Agit\+/, ''), :tag => s.version.to_s } + s.source_files = 'ios/Sources/**/*.{swift,h,m,c,cc,mm,cpp}' + s.ios.deployment_target = '15.0' + s.dependency 'Capacitor' +end diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..185b4b6 --- /dev/null +++ b/Package.swift @@ -0,0 +1,31 @@ +// swift-tools-version: 5.9 +import PackageDescription + +let package = Package( + name: "CapgoCapacitorNativeMap", + platforms: [ + .iOS(.v15) + ], + products: [ + .library( + name: "CapgoCapacitorNativeMap", + targets: ["NativeMapPlugin"] + ) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0") + ], + targets: [ + .target( + name: "NativeMapPlugin", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm") + ], + path: "ios/Sources/NativeMapPlugin"), + .testTarget( + name: "NativeMapPluginTests", + dependencies: ["NativeMapPlugin"], + path: "ios/Tests/NativeMapPluginTests") + ] +) diff --git a/README.md b/README.md index dfa150b..b139044 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,1180 @@ Native maps for Capacitor: Google Maps on Android, Apple MapKit on iOS, and a web fallback. -Work in progress. Not published to npm yet. +
+

โžก๏ธ Get instant updates for your app with Capgo

+

Missing a feature? We can build the plugin for you

+
+ +![NPM Version](https://img.shields.io/npm/v/%40capgo%2Fcapacitor-native-map) +![NPM Downloads](https://img.shields.io/npm/dy/%40capgo%2Fcapacitor-native-map) +![GitHub Repo stars](https://img.shields.io/github/stars/Cap-go/capacitor-native-map) +![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/Cap-go/capacitor-native-map/.github%2Fworkflows%2Ftest.yml) +![GitHub License](https://img.shields.io/github/license/Cap-go/capacitor-native-map) +![Maintenance](https://img.shields.io/maintenance/yes/2026) + +**One TypeScript API for native maps in Capacitor apps:** Google Maps on Android, Apple MapKit on iOS, and Google Maps JS on the web. Ship markers, camera moves, shapes, clustering, search, and events from a single integration. + +

+ Native map with HTML overlay on iOS (example app, toBack overlay mode) +

+ +Docs: [Native Map plugin](https://capgo.app/docs/plugins/native-map/) ยท Tutorial: [capacitor-native-map](https://capgo.app/plugins/capacitor-native-map/) + +## Why use it + +- **Single API** on iOS, Android, and web (`NativeMap.create`, shared listeners, shared types). +- **Native map performance** on mobile (platform map views, not a WebView map). +- **Custom map UI** with `toBack`: render the native map behind a transparent WebView and build controls in HTML. +- **Capacitor 8** with TypeScript 6 and an example app you can run locally. + +## Features + +| Area | Highlights | +| --- | --- | +| **Map lifecycle** | Create, destroy, resize, `show` / `hide`, `updateLayout`, scroll sync with Ionic | +| **Background mode** | `toBack` compositing, multi-touch passthrough on transparent HTML (pinch, rotate, tilt, pan) | +| **Camera** | Center, zoom, bearing, tilt, fit bounds, min/max zoom | +| **Markers** | Add/update/remove, drag, selection, callouts, clustering | +| **Overlays** | Polygons, polylines, circles, tile overlays (platform-dependent) | +| **Events** | Map/marker/shape clicks, camera idle/move, clusters, my location | +| **Extras** | Snapshots, autocomplete/places search, geocode/reverse geocode | + +## Use cases + +- Store locators and field-service maps with live GPS +- Delivery and fleet dashboards with markers and routes +- Event venues, real-estate listings, and travel apps +- Custom-branded map UIs (search bars, filters, bottom sheets) over a native map + +## Compatibility + +| Platform | Map engine | API key | +| --- | --- | --- | +| **iOS** | Apple MapKit | Not required for the map | +| **Android** | Google Maps SDK | `GOOGLE_MAPS_API_KEY` in the app manifest | +| **Web** | Google Maps JavaScript API | `apiKey` + `config.mapId` on `create` | + +Requires **Capacitor 8+**. Plugin major version follows Capacitor (this package is **v8**). + +## Install + +```bash +npm install @capgo/capacitor-native-map +npx cap sync +``` + +## iOS (MapKit) + +No Google Maps API key is required on iOS. Add location usage strings to `Info.plist` if you enable current location: + +```xml +NSLocationWhenInUseUsageDescription +We use your location to show you on the map. +``` + +Sync the plugin with CocoaPods or Swift Package Manager (both are supported). + +## Android (Google Maps) + +1. Create a Google Maps SDK for Android key in [Google Cloud Console](https://console.cloud.google.com/). +2. Set the key on the application `meta-data` entry (see `example-app/android/app/src/main/AndroidManifest.xml`): + +```xml + +``` + +3. Provide `GOOGLE_MAPS_API_KEY` when building (Gradle manifest variable in `example-app/android/app/build.gradle`). + +## Usage + +### Embedded map + +```ts +import { NativeMap } from '@capgo/capacitor-native-map'; + +const map = await NativeMap.create({ + id: 'main-map', + element: document.getElementById('map')!, + apiKey: 'YOUR_GOOGLE_MAPS_API_KEY', + config: { + center: { lat: 37.7749, lng: -122.4194 }, + zoom: 12, + }, +}); + +map.setOnMapClickListener((e) => console.log('click', e.latitude, e.longitude)); +``` + +### Background map (`toBack`) with HTML overlay + +```ts +const map = await NativeMap.create({ + id: 'overlay-map', + toBack: true, + apiKey: 'YOUR_GOOGLE_MAPS_API_KEY', + config: { + center: { lat: 37.7749, lng: -122.4194 }, + zoom: 12, + x: 0, + y: 0, + width: window.innerWidth, + height: window.innerHeight, + }, +}); + +await map.updateLayout({ x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }); +await map.hide(); +await map.show(); +``` + +The [example app](./example-app) opens in **overlay (`toBack`)** mode by default. + +## Build your own map UI + +Product teams often want a **fully custom map screen** (brand colors, filters, bottom sheets) while keeping native map performance. This plugin supports that pattern: + +1. Call `NativeMap.create({ toBack: true, ... })` so MapKit or Google Maps renders **behind** the Capacitor WebView. +2. Make the WebView and page background **transparent** so the map is visible. +3. Build controls in HTML. Use `data-map-overlay` (or interactive elements) on controls that must receive touches; transparent areas pass gestures to the map. + +### Transparent app / page CSS + +```css +html.native-map-to-back, +body.native-map-to-back { + background: transparent !important; +} + +:root { + --ion-background-color: transparent !important; +} + +.map-overlay-root { + position: fixed; + inset: 0; + pointer-events: none; +} + +.map-overlay-root [data-map-overlay] { + pointer-events: auto; +} +``` + +### Full HTML overlay example + +```html +
+
+

Nearby stores

+ +
+
+

Pinch and pan on open areas to move the map.

+
+
+``` + +```ts +const map = await NativeMap.create({ + id: 'stores-map', + toBack: true, + apiKey: GOOGLE_KEY, + config: { + center: { lat: 40.7128, lng: -74.006 }, + zoom: 11, + width: window.innerWidth, + height: window.innerHeight, + }, +}); + +document.getElementById('recenter')?.addEventListener('click', () => { + map.setCamera({ coordinate: { lat: 40.7128, lng: -74.006 }, zoom: 13, animate: true }); +}); +``` + +## Example app + +```bash +cd example-app +bun install +bun run build +bunx cap sync +``` + +Set `VITE_GOOGLE_MAPS_API_KEY` in `example-app/.env` for the web dev server. Android native builds use `GOOGLE_MAPS_API_KEY` for the Gradle manifest variable (see `example-app/android/app/build.gradle`). iOS uses MapKit without a Google key. + +Credits: portions adapted from [katamalabs/capacitor-plugin-apple-maps](https://github.com/katamalabs/capacitor-plugin-apple-maps) and [ionic-team/capacitor-google-maps](https://github.com/ionic-team/capacitor-google-maps) (both MIT). + +## API + + + + + +### create(...) + +```typescript +create(options: CreateMapArgs) => Promise +``` + +| Param | Type | +| ------------- | ------------------------------------------------------- | +| **`options`** | CreateMapArgs | + +-------------------- + + +### enableTouch(...) + +```typescript +enableTouch(args: { id: string; }) => Promise +``` + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +-------------------- + + +### disableTouch(...) + +```typescript +disableTouch(args: { id: string; }) => Promise +``` + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +-------------------- + + +### addTileOverlay(...) + +```typescript +addTileOverlay(args: AddTileOverlayArgs) => Promise<{ id: string; }> +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------------- | +| **`args`** | AddTileOverlayArgs | + +**Returns:** Promise<{ id: string; }> + +-------------------- + + +### removeTileOverlay(...) + +```typescript +removeTileOverlay(args: RemoveTileOverlayArgs) => Promise +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------------------- | +| **`args`** | RemoveTileOverlayArgs | + +-------------------- + + +### addMarker(...) + +```typescript +addMarker(args: AddMarkerArgs) => Promise<{ id: string; }> +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | AddMarkerArgs | + +**Returns:** Promise<{ id: string; }> + +-------------------- + + +### addMarkers(...) + +```typescript +addMarkers(args: AddMarkersArgs) => Promise<{ ids: string[]; }> +``` + +| Param | Type | +| ---------- | --------------------------------------------------------- | +| **`args`** | AddMarkersArgs | + +**Returns:** Promise<{ ids: string[]; }> + +-------------------- + + +### removeMarker(...) + +```typescript +removeMarker(args: RemoveMarkerArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------------- | +| **`args`** | RemoveMarkerArgs | + +-------------------- + + +### removeMarkers(...) + +```typescript +removeMarkers(args: RemoveMarkersArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------------------- | +| **`args`** | RemoveMarkersArgs | + +-------------------- + + +### addPolygons(...) + +```typescript +addPolygons(args: AddPolygonsArgs) => Promise<{ ids: string[]; }> +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------- | +| **`args`** | AddPolygonsArgs | + +**Returns:** Promise<{ ids: string[]; }> + +-------------------- + + +### removePolygons(...) + +```typescript +removePolygons(args: RemovePolygonsArgs) => Promise +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------------- | +| **`args`** | RemovePolygonsArgs | + +-------------------- + + +### addCircles(...) + +```typescript +addCircles(args: AddCirclesArgs) => Promise<{ ids: string[]; }> +``` + +| Param | Type | +| ---------- | --------------------------------------------------------- | +| **`args`** | AddCirclesArgs | + +**Returns:** Promise<{ ids: string[]; }> + +-------------------- + + +### removeCircles(...) + +```typescript +removeCircles(args: RemoveCirclesArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------------------- | +| **`args`** | RemoveCirclesArgs | + +-------------------- + + +### addPolylines(...) + +```typescript +addPolylines(args: AddPolylinesArgs) => Promise<{ ids: string[]; }> +``` + +| Param | Type | +| ---------- | ------------------------------------------------------------- | +| **`args`** | AddPolylinesArgs | + +**Returns:** Promise<{ ids: string[]; }> + +-------------------- + + +### removePolylines(...) + +```typescript +removePolylines(args: RemovePolylinesArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------------------- | +| **`args`** | RemovePolylinesArgs | + +-------------------- + + +### enableClustering(...) + +```typescript +enableClustering(args: EnableClusteringArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------------------------- | +| **`args`** | EnableClusteringArgs | + +-------------------- + + +### disableClustering(...) + +```typescript +disableClustering(args: { id: string; }) => Promise +``` + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +-------------------- + + +### destroy(...) + +```typescript +destroy(args: DestroyMapArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------------- | +| **`args`** | DestroyMapArgs | + +-------------------- + + +### setCamera(...) + +```typescript +setCamera(args: CameraArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------- | +| **`args`** | CameraArgs | + +-------------------- + + +### getMapType(...) + +```typescript +getMapType(args: { id: string; }) => Promise<{ type: string; }> +``` + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +**Returns:** Promise<{ type: string; }> + +-------------------- + + +### setMapType(...) + +```typescript +setMapType(args: MapTypeArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------- | +| **`args`** | MapTypeArgs | + +-------------------- + + +### enableIndoorMaps(...) + +```typescript +enableIndoorMaps(args: IndoorMapArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | IndoorMapArgs | + +-------------------- + + +### enableTrafficLayer(...) + +```typescript +enableTrafficLayer(args: TrafficLayerArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------------- | +| **`args`** | TrafficLayerArgs | + +-------------------- + + +### enableAccessibilityElements(...) + +```typescript +enableAccessibilityElements(args: AccElementsArgs) => Promise +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------- | +| **`args`** | AccElementsArgs | + +-------------------- + + +### enableCurrentLocation(...) + +```typescript +enableCurrentLocation(args: CurrentLocArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------------- | +| **`args`** | CurrentLocArgs | + +-------------------- + + +### setPadding(...) + +```typescript +setPadding(args: PaddingArgs) => Promise +``` + +| Param | Type | +| ---------- | --------------------------------------------------- | +| **`args`** | PaddingArgs | + +-------------------- + + +### onScroll(...) + +```typescript +onScroll(args: MapBoundsArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | MapBoundsArgs | + +-------------------- + + +### onResize(...) + +```typescript +onResize(args: MapBoundsArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | MapBoundsArgs | + +-------------------- + + +### onDisplay(...) + +```typescript +onDisplay(args: MapBoundsArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | MapBoundsArgs | + +-------------------- + + +### dispatchMapEvent(...) + +```typescript +dispatchMapEvent(args: { id: string; focus: boolean; }) => Promise +``` + +| Param | Type | +| ---------- | -------------------------------------------- | +| **`args`** | { id: string; focus: boolean; } | + +-------------------- + + +### getMapBounds(...) + +```typescript +getMapBounds(args: { id: string; }) => Promise +``` + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +**Returns:** Promise<LatLngBounds> + +-------------------- + + +### fitBounds(...) + +```typescript +fitBounds(args: FitBoundsArgs) => Promise +``` + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | FitBoundsArgs | + +-------------------- + + +### mapBoundsContains(...) + +```typescript +mapBoundsContains(args: MapBoundsContainsArgs) => Promise<{ contains: boolean; }> +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------------------- | +| **`args`** | MapBoundsContainsArgs | + +**Returns:** Promise<{ contains: boolean; }> + +-------------------- + + +### mapBoundsExtend(...) + +```typescript +mapBoundsExtend(args: MapBoundsExtendArgs) => Promise<{ bounds: LatLngBounds; }> +``` + +| Param | Type | +| ---------- | ----------------------------------------------------------------------- | +| **`args`** | MapBoundsContainsArgs | + +**Returns:** Promise<{ bounds: LatLngBounds; }> + +-------------------- + + +### updateLayout(...) + +```typescript +updateLayout(args: MapLayoutArgs) => Promise +``` + +Updates the native map position and size without recreating the map. +Useful with `toBack` mode to resize or reposition the map layer. + +| Param | Type | +| ---------- | ------------------------------------------------------- | +| **`args`** | MapLayoutArgs | + +-------------------- + + +### show(...) + +```typescript +show(args: { id: string; }) => Promise +``` + +Shows a map that was hidden with {@link NativeMapPlugin.hide}. + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +-------------------- + + +### hide(...) + +```typescript +hide(args: { id: string; }) => Promise +``` + +Hides the native map view while keeping the instance alive. + +| Param | Type | +| ---------- | ---------------------------- | +| **`args`** | { id: string; } | + +-------------------- + + +### Interfaces + + +#### CreateMapArgs + +An interface containing the options used when creating a map. + +| Prop | Type | Description | Default | +| ----------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| **`id`** | string | A unique identifier for the map instance. | | +| **`apiKey`** | string | Google Maps API key. Required on web. On Android, also set `com.google.android.geo.API_KEY` in the app manifest. Not used on iOS (MapKit). | | +| **`config`** | NativeMapConfig | The initial configuration settings for the map. | | +| **`element`** | HTMLElement | The DOM element that determines size and positioning for embedded maps. Optional when `toBack` is `true` (defaults to `document.body`). | | +| **`forceCreate`** | boolean | Destroy and re-create the map instance if a map with the supplied id already exists | false | +| **`region`** | string | The region parameter alters your application to serve different map tiles or bias the application (such as biasing geocoding results towards the region). Only available for web. | | +| **`language`** | string | The language parameter affects the names of controls, copyright notices, driving directions, and control labels, as well as the responses to service requests. Only available for web. | | +| **`toBack`** | boolean | When `true`, renders the native map behind a transparent WebView so your HTML UI can sit on top. Touches on transparent web areas pass through to the map, including multi-touch gestures (pinch zoom, rotate, two-finger tilt, pan). Touches on interactive HTML elements stay in the WebView. On native platforms, set `config.x`, `config.y`, `config.width`, and `config.height` for position and size (defaults to the viewport when omitted). Use {@link NativeMap.updateLayout} to change layout at runtime. | false | + + +#### NativeMapConfig + +For web, all the javascript Google Maps options are available as +NativeMapConfig extends google.maps.MapOptions. +For iOS and Android only the config options declared on NativeMapConfig are available. + +| Prop | Type | Description | Default | Since | +| ---------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`width`** | number | Override width for native map. | | | +| **`height`** | number | Override height for native map. | | | +| **`x`** | number | Override absolute x coordinate position for native map. | | | +| **`y`** | number | Override absolute y coordinate position for native map. | | | +| **`center`** | LatLng | Default location on the Earth towards which the camera points. | | | +| **`zoom`** | number | Sets the zoom of the map. | | | +| **`androidLiteMode`** | boolean | Enables image-based lite mode on Android. | false | | +| **`devicePixelRatio`** | number | Override pixel ratio for native map. | | | +| **`styles`** | MapTypeStyle[] \| null | Styles to apply to each of the default map types. Note that for satellite, hybrid and terrain modes, these styles will only apply to labels and geometry. | | 4.3.0 | +| **`mapId`** | string | A map id associated with a specific map style or feature. [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) Only for Web. | | 5.4.0 | +| **`androidMapId`** | string | A map id associated with a specific map style or feature. [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) Only for Android. | | 5.4.0 | +| **`iOSMapId`** | string | A map id associated with a specific map style or feature. [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) Only for iOS. | | 5.4.0 | +| **`maxZoom`** | number \| null | The maximum zoom level which will be displayed on the map. If omitted, or set to <code>null</code>, the maximum zoom from the current map type is used instead. Valid zoom values are numbers from zero up to the supported <a href="https://developers.google.com/maps/documentation/javascript/maxzoom">maximum zoom level</a>. | | | +| **`minZoom`** | number \| null | The minimum zoom level which will be displayed on the map. If omitted, or set to <code>null</code>, the minimum zoom from the current map type is used instead. Valid zoom values are numbers from zero up to the supported <a href="https://developers.google.com/maps/documentation/javascript/maxzoom">maximum zoom level</a>. | | | +| **`mapTypeId`** | string \| null | The initial Map mapTypeId. Defaults to <code>ROADMAP</code>. | | | +| **`heading`** | number \| null | The heading for aerial imagery in degrees measured clockwise from cardinal direction North. Headings are snapped to the nearest available angle for which imagery is available. | | | +| **`restriction`** | MapRestriction \| null | Defines a boundary that restricts the area of the map accessible to users. When set, a user can only pan and zoom while the camera view stays inside the limits of the boundary. | | | + + +#### LatLng + +An interface representing a pair of latitude and longitude coordinates. + +| Prop | Type | Description | +| --------- | ------------------- | ------------------------------------------------------------------------- | +| **`lat`** | number | Coordinate latitude, in degrees. This value is in the range [-90, 90]. | +| **`lng`** | number | Coordinate longitude, in degrees. This value is in the range [-180, 180]. | + + +#### AddTileOverlayArgs + +| Prop | Type | +| ----------------- | --------------------------------------------------- | +| **`id`** | string | +| **`tileOverlay`** | TileOverlay | + + +#### TileOverlay + +A tile overlay is an image placed on top of your map at a specific zoom level. Supported on Android and web only. + +| Prop | Type | Description | Default | +| ------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| **`url`** | string | A string representing the tile url. Should contain `{x}`, `{y}` and `{z}` so they can be replaced with actual values for x, y and zoom. Android and web only. | | +| **`opacity`** | number | The opacity of the tile overlay, between 0 (completely transparent) and 1 inclusive. Available on iOS, Android and Web | undefined | +| **`visible`** | boolean | Controls whether this tile overlay should be visible. Available only on Android | undefined | +| **`zIndex`** | number | The zIndex of the tile overlay. Available on iOS and Android | undefined | + + +#### RemoveTileOverlayArgs + +| Prop | Type | +| ------------------- | ------------------- | +| **`id`** | string | +| **`tileOverlayId`** | string | + + +#### AddMarkerArgs + +| Prop | Type | +| ------------ | ----------------------------------------- | +| **`id`** | string | +| **`marker`** | Marker | + + +#### Marker + +A marker is an icon placed at a particular point on the map's surface. + +| Prop | Type | Description | Default | Since | +| ---------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | ----- | +| **`coordinate`** | LatLng | Marker position | | | +| **`opacity`** | number | Sets the opacity of the marker, between 0 (completely transparent) and 1 inclusive. | 1 | | +| **`title`** | string | Title, a short description of the overlay. | | | +| **`snippet`** | string | Snippet text, shown beneath the title in the info window when selected. | | | +| **`isFlat`** | boolean | Controls whether this marker should be flat against the Earth's surface or a billboard facing the camera. | false | | +| **`iconUrl`** | string | Path to a marker icon to render. It can be relative to the web app public directory, or a https url of a remote marker icon. **SVGs are not supported on native platforms.** | | 4.2.0 | +| **`iconSize`** | Size | Controls the scaled size of the marker image set in `iconUrl`. | | 4.2.0 | +| **`iconOrigin`** | Point | The position of the image within a sprite, if any. By default, the origin is located at the top left corner of the image . | | 4.2.0 | +| **`iconAnchor`** | Point | The position at which to anchor an image in correspondence to the location of the marker on the map. By default, the anchor is located along the center point of the bottom of the image. | | 4.2.0 | +| **`tintColor`** | { r: number; g: number; b: number; a: number; } | Customizes the color of the default marker image. Each value must be between 0 and 255. Only for iOS and Android. | | 4.2.0 | +| **`draggable`** | boolean | Controls whether this marker can be dragged interactively | false | | +| **`zIndex`** | number | Specifies the stack order of this marker, relative to other markers on the map. A marker with a high z-index is drawn on top of markers with lower z-indexes | 0 | | + + +#### Size + +Width and height in pixels. + +| Prop | Type | +| ------------ | ------------------- | +| **`width`** | number | +| **`height`** | number | + + +#### Point + +Point geometry object. +https://tools.ietf.org/html/rfc7946#section-3.1.2 + +| Prop | Type | Description | +| ----------------- | --------------------------------------------- | ------------------------------------- | +| **`type`** | 'Point' | Specifies the type of GeoJSON object. | +| **`coordinates`** | Position | | + + +#### Position + +| Prop | Type | Description | +| ------------ | ------------------- | ----------- | +| **`line`** | number | >= 1 | +| **`column`** | number | >= 0 | + + +#### AddMarkersArgs + +| Prop | Type | +| ------------- | --------------------- | +| **`id`** | string | +| **`markers`** | Marker[] | + + +#### RemoveMarkerArgs + +| Prop | Type | +| -------------- | ------------------- | +| **`id`** | string | +| **`markerId`** | string | + + +#### RemoveMarkersArgs + +| Prop | Type | +| --------------- | --------------------- | +| **`id`** | string | +| **`markerIds`** | string[] | + + +#### AddPolygonsArgs + +| Prop | Type | +| -------------- | ---------------------- | +| **`id`** | string | +| **`polygons`** | Polygon[] | + + +#### Polygon + +Polygon geometry object. +https://tools.ietf.org/html/rfc7946#section-3.1.6 + +| Prop | Type | Description | +| ----------------- | --------------------------------------------- | ------------------------------------- | +| **`type`** | 'Polygon' | Specifies the type of GeoJSON object. | +| **`coordinates`** | Position[][] | | + + +#### RemovePolygonsArgs + +| Prop | Type | +| ---------------- | --------------------- | +| **`id`** | string | +| **`polygonIds`** | string[] | + + +#### AddCirclesArgs + +| Prop | Type | +| ------------- | --------------------- | +| **`id`** | string | +| **`circles`** | Circle[] | + + +#### Circle + +For web, all the javascript Circle options are available as +Circle extends google.maps.CircleOptions. +For iOS and Android only the config options declared on Circle are available. + +| Prop | Type | Description | +| ------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`fillColor`** | string | The fill color. All CSS3 colors are supported except for extended named colors. | +| **`fillOpacity`** | number | The fill opacity between 0.0 and 1.0. | +| **`strokeColor`** | string | The stroke color. All CSS3 colors are supported except for extended named colors. | +| **`strokeWeight`** | number | The stroke width in pixels. | +| **`geodesic`** | boolean | | +| **`clickable`** | boolean | Indicates whether this <code>Circle</code> handles mouse events. | +| **`title`** | string | Title, a short description of the overlay. Some overlays, such as markers, will display the title on the map. The title is also the default accessibility text. Only available on iOS. | +| **`tag`** | string | | + + +#### RemoveCirclesArgs + +| Prop | Type | +| --------------- | --------------------- | +| **`id`** | string | +| **`circleIds`** | string[] | + + +#### AddPolylinesArgs + +| Prop | Type | +| --------------- | ----------------------- | +| **`id`** | string | +| **`polylines`** | Polyline[] | + + +#### Polyline + +For web, all the javascript Polyline options are available as +Polyline extends google.maps.PolylineOptions. +For iOS and Android only the config options declared on Polyline are available. + +| Prop | Type | Description | +| ------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **`strokeColor`** | string | The stroke color. All CSS3 colors are supported except for extended named colors. | +| **`strokeOpacity`** | number | The stroke opacity between 0.0 and 1.0. | +| **`strokeWeight`** | number | The stroke width in pixels. | +| **`geodesic`** | boolean | When <code>true</code>, edges of the polygon are interpreted as geodesic and will follow the curvature of the Earth. When <code>false</code>, edges of the polygon are rendered as straight lines in screen space. Note that the shape of a geodesic polygon may appear to change when dragged, as the dimensions are maintained relative to the surface of the earth. | +| **`clickable`** | boolean | Indicates whether this <code>Polyline</code> handles mouse events. | +| **`tag`** | string | | +| **`styleSpans`** | StyleSpan[] | Used to specify the color of one or more segments of a polyline. The styleSpans property is an array of StyleSpan objects. Setting the spans property is the preferred way to change the color of a polyline. Only on iOS and Android. | + + +#### StyleSpan + +Describes the style for some region of a polyline. + +| Prop | Type | Description | +| -------------- | ------------------- | --------------------------------------------------------------------------------- | +| **`color`** | string | The stroke color. All CSS3 colors are supported except for extended named colors. | +| **`segments`** | number | The length of this span in number of segments. | + + +#### RemovePolylinesArgs + +| Prop | Type | +| ----------------- | --------------------- | +| **`id`** | string | +| **`polylineIds`** | string[] | + + +#### EnableClusteringArgs + +| Prop | Type | +| -------------------- | ------------------- | +| **`id`** | string | +| **`minClusterSize`** | number | + + +#### DestroyMapArgs + +| Prop | Type | +| -------- | ------------------- | +| **`id`** | string | + + +#### CameraArgs + +| Prop | Type | +| ------------ | ----------------------------------------------------- | +| **`id`** | string | +| **`config`** | CameraConfig | + + +#### CameraConfig + +Configuration properties for a Google Map Camera + +| Prop | Type | Description | Default | +| ----------------------- | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------------------ | +| **`coordinate`** | LatLng | Location on the Earth towards which the camera points. | | +| **`zoom`** | number | Sets the zoom of the map. | | +| **`bearing`** | number | Bearing of the camera, in degrees clockwise from true north. | 0 | +| **`angle`** | number | The angle, in degrees, of the camera from the nadir (directly facing the Earth). The only allowed values are 0 and 45. | 0 | +| **`animate`** | boolean | Animate the transition to the new Camera properties. | false | +| **`animationDuration`** | number | This configuration option is not being used. | | + + +#### MapTypeArgs + +| Prop | Type | +| ------------- | ------------------------------------------- | +| **`id`** | string | +| **`mapType`** | MapType | + + +#### IndoorMapArgs + +| Prop | Type | +| ------------- | -------------------- | +| **`id`** | string | +| **`enabled`** | boolean | + + +#### TrafficLayerArgs + +| Prop | Type | +| ------------- | -------------------- | +| **`id`** | string | +| **`enabled`** | boolean | + + +#### AccElementsArgs + +| Prop | Type | +| ------------- | -------------------- | +| **`id`** | string | +| **`enabled`** | boolean | + + +#### CurrentLocArgs + +| Prop | Type | +| ------------- | -------------------- | +| **`id`** | string | +| **`enabled`** | boolean | + + +#### PaddingArgs + +| Prop | Type | +| ------------- | ------------------------------------------------- | +| **`id`** | string | +| **`padding`** | MapPadding | + + +#### MapPadding + +Controls for setting padding on the 'visible' region of the view. + +| Prop | Type | +| ------------ | ------------------- | +| **`top`** | number | +| **`left`** | number | +| **`right`** | number | +| **`bottom`** | number | + + +#### MapBoundsArgs + +| Prop | Type | +| --------------- | --------------------------------------------------------------------- | +| **`id`** | string | +| **`mapBounds`** | { x: number; y: number; width: number; height: number; } | + + +#### FitBoundsArgs + +| Prop | Type | +| ------------- | ------------------------- | +| **`id`** | string | +| **`bounds`** | LatLngBounds | +| **`padding`** | number | + + +#### MapBoundsContainsArgs + +| Prop | Type | +| ------------ | ----------------------------------------- | +| **`bounds`** | LatLngBounds | +| **`point`** | LatLng | + + +#### MapLayoutArgs + +Layout rectangle for {@link NativeMapPlugin.updateLayout}, in CSS pixels relative to the WebView. + +| Prop | Type | Description | +| ------------ | ------------------- | ---------------------------------------------------------- | +| **`id`** | string | Map instance id returned from {@link NativeMap.create}. | +| **`x`** | number | Distance from the left edge of the WebView, in CSS pixels. | +| **`y`** | number | Distance from the top edge of the WebView, in CSS pixels. | +| **`width`** | number | Map width in CSS pixels. | +| **`height`** | number | Map height in CSS pixels. | + + +### Type Aliases + + +#### Position + +A Position is an array of coordinates. +https://tools.ietf.org/html/rfc7946#section-3.1.1 +Array should contain between two and three elements. +The previous GeoJSON specification allowed more elements (e.g., which could be used to represent M values), +but the current specification only allows X, Y, and (optionally) Z to be defined. + +Note: the type will not be narrowed down to `[number, number] | [number, number, number]` due to +marginal benefits and the large impact of breaking change. + +See previous discussions on the type narrowing: +- {@link https://github.com/DefinitelyTyped/DefinitelyTyped/pull/21590|Nov 2017} +- {@link https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/67773|Dec 2023} +- {@link https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/71441| Dec 2024} + +One can use a +{@link https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates|user-defined type guard that returns a type predicate} +to determine if a position is a 2D or 3D position. + +number[] + + +#### Marker + +Supports markers of either "legacy" or "advanced" types. + +google.maps.Marker | google.maps.marker.AdvancedMarkerElement + + +#### MapBoundsExtendArgs + +MapBoundsContainsArgs + + +### Enums + + +#### MapType + +| Members | Value | Description | +| --------------- | ------------------------ | ---------------------------------------- | +| **`Normal`** | 'Normal' | Basic map. | +| **`Hybrid`** | 'Hybrid' | Satellite imagery with roads and labels. | +| **`Satellite`** | 'Satellite' | Satellite imagery with no labels. | +| **`Terrain`** | 'Terrain' | Topographic data. | +| **`None`** | 'None' | No base map tiles. | + + diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/android/.gitignore @@ -0,0 +1 @@ +/build diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..9e40e63 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,103 @@ +ext { + capacitorVersion = System.getenv('CAPACITOR_VERSION') + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.13.2' + androidxAppCompatVersion = project.hasProperty('androidxAppCompatVersion') ? rootProject.ext.androidxAppCompatVersion : '1.7.1' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.3.0' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.7.0' + androidxCoreKTXVersion = project.hasProperty('androidxCoreKTXVersion') ? rootProject.ext.androidxCoreKTXVersion : '1.17.0' + googleMapsPlayServicesVersion = project.hasProperty('googleMapsPlayServicesVersion') ? rootProject.ext.googleMapsPlayServicesVersion : '19.2.0' + googleMapsUtilsVersion = project.hasProperty('googleMapsUtilsVersion') ? rootProject.ext.googleMapsUtilsVersion : '3.19.1' + googleMapsKtxVersion = project.hasProperty('googleMapsKtxVersion') ? rootProject.ext.googleMapsKtxVersion : '5.2.1' + googleMapsUtilsKtxVersion = project.hasProperty('googleMapsUtilsKtxVersion') ? rootProject.ext.googleMapsUtilsKtxVersion : '5.2.1' + kotlinxCoroutinesVersion = project.hasProperty('kotlinxCoroutinesVersion') ? rootProject.ext.kotlinxCoroutinesVersion : '1.10.2' +} + +buildscript { + ext.kotlin_version = project.hasProperty("kotlin_version") ? rootProject.ext.kotlin_version : '2.2.20' + repositories { + google() + mavenCentral() + maven { + url = "https://plugins.gradle.org/m2/" + } + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + classpath 'io.github.gradle-nexus:publish-plugin:1.3.0' + } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + apply plugin: 'io.github.gradle-nexus.publish-plugin' + apply from: file('../../scripts/android/publish-root.gradle') + apply from: file('../../scripts/android/publish-module.gradle') +} + +android { + namespace = "com.capgo.plugins.nativemap" + compileSdk = project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 36 + defaultConfig { + minSdkVersion project.hasProperty('minSdkVersion') ? rootProject.ext.minSdkVersion : 24 + targetSdkVersion project.hasProperty('targetSdkVersion') ? rootProject.ext.targetSdkVersion : 36 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + lintOptions { + abortOnError = false + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } + publishing { + singleVariant("release") + } + dependencies { + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + } +} + +kotlin { + jvmToolchain(21) +} + +repositories { + google() + mavenCentral() +} + + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + if (System.getenv("CAP_PLUGIN_PUBLISH") == "true") { + implementation "com.capacitorjs:core:$capacitorVersion" + } else { + implementation project(':capacitor-android') + } + + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation "androidx.core:core-ktx:$androidxCoreKTXVersion" + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + + implementation "com.google.maps.android:maps-ktx:$googleMapsKtxVersion" + implementation "com.google.maps.android:maps-utils-ktx:$googleMapsUtilsKtxVersion" + implementation "com.google.maps.android:android-maps-utils:$googleMapsUtilsVersion" + implementation "com.google.android.gms:play-services-maps:$googleMapsPlayServicesVersion" +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..aa7ea25 --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=2g + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..7705927 --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright ยฉ 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, +# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; +# * compound commands having a testable exit status, especially ยซcaseยป; +# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/proguard-rules.pro b/android/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/android/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..1e5b843 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,2 @@ +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') \ No newline at end of file diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a2e33cc --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorClusterManagerRenderer.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorClusterManagerRenderer.kt new file mode 100644 index 0000000..87e825d --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorClusterManagerRenderer.kt @@ -0,0 +1,39 @@ +package com.capgo.plugins.nativemap + +import android.content.Context +import com.google.android.gms.maps.GoogleMap +import com.google.android.gms.maps.model.MarkerOptions +import com.google.maps.android.clustering.ClusterManager +import com.google.maps.android.clustering.view.DefaultClusterRenderer + +class CapacitorClusterManagerRenderer( + context: Context, + map: GoogleMap??, + clusterManager: ClusterManager?, + minClusterSize: Int? +) : DefaultClusterRenderer(context, map, clusterManager) { + + init { + if(minClusterSize != null && minClusterSize > 0) { + super.setMinClusterSize(minClusterSize) + } + } + + override fun onBeforeClusterItemRendered(item: CapacitorNativeMapViewMarker, markerOptions: MarkerOptions) { + super.onBeforeClusterItemRendered(item, markerOptions) + + item.markerOptions?.let { + markerOptions.position(it.position) + markerOptions.title(it.title) + markerOptions.snippet(it.snippet) + markerOptions.alpha(it.alpha) + markerOptions.flat(it.isFlat) + markerOptions.draggable(it.isDraggable) + markerOptions.zIndex(it.zIndex) + markerOptions.anchor(it.anchorU, it.anchorV) + if(null != it.icon) { + markerOptions.icon(it.icon) + } + } + } +} diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMap.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMap.kt new file mode 100644 index 0000000..8dd562f --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMap.kt @@ -0,0 +1,1266 @@ +package com.capgo.plugins.nativemap + +import android.annotation.SuppressLint +import android.graphics.* +import android.location.Location +import android.util.Log +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import com.getcapacitor.Bridge +import com.getcapacitor.JSArray +import com.getcapacitor.JSObject +import com.google.android.gms.maps.* +import com.google.android.gms.maps.GoogleMap.* +import com.google.android.gms.maps.model.* +import com.google.maps.android.clustering.Cluster +import com.google.maps.android.clustering.ClusterManager +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.io.InputStream +import java.net.URL + +class CapacitorNativeMapView( + val id: String, + val config: NativeMapConfig, + val delegate: CapacitorNativeMapPlugin +) : + OnCameraIdleListener, + OnCameraMoveStartedListener, + OnCameraMoveListener, + OnMyLocationButtonClickListener, + OnMyLocationClickListener, + OnMapReadyCallback, + OnMapClickListener, + OnMarkerClickListener, + OnMarkerDragListener, + OnInfoWindowClickListener, + OnCircleClickListener, + OnPolylineClickListener, + OnPolygonClickListener { + private var mapView: MapView + private var googleMap: GoogleMap? = null + private val markers = HashMap() + private val tileOverlays = HashMap() + private val polygons = HashMap() + private val circles = HashMap() + private val polylines = HashMap() + private val markerIcons = HashMap() + private var clusterManager: ClusterManager? = null + + private val isReadyChannel = Channel() + private var debounceJob: Job? = null + private var mapViewParent: FrameLayout? = null + private var isDestroyed = false + var toBack: Boolean = false + var isMapVisible: Boolean = true + private var originalWebViewAlpha: Float? = null + + init { + val bridge = delegate.bridge + + mapView = MapView(bridge.context, config.googleMapOptions) + initMap() + } + + private fun initMap() { + CoroutineScope(Dispatchers.Main).launch { + mapView.onCreate(null) + mapView.onStart() + mapView.getMapAsync(this@CapacitorNativeMapView) + mapView.setWillNotDraw(false) + isReadyChannel.receive() + render() + } + } + + private fun render() { + CoroutineScope(Dispatchers.Main).launch { + if (isDestroyed) return@launch + val bridge = delegate.bridge + val parent = FrameLayout(bridge.context) + parent.minimumHeight = bridge.webView.height + parent.minimumWidth = bridge.webView.width + + val parentLayoutParams = + FrameLayout.LayoutParams( + getScaledPixels(bridge, config.width), + getScaledPixels(bridge, config.height), + ) + parentLayoutParams.leftMargin = getScaledPixels(bridge, config.x) + parentLayoutParams.topMargin = getScaledPixels(bridge, config.y) + + parent.tag = id + parent.layoutParams = parentLayoutParams + + val mapChildParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + mapView.layoutParams = mapChildParams + parent.addView(mapView) + + val webParent = (bridge.webView.parent) as ViewGroup + if (toBack) { + webParent.addView(parent, 0) + ToBackCompositorHelper.onToBackMapCreated(bridge.webView, id) + applyToBackVisualState(bridge) + } else { + webParent.addView(parent) + bridge.webView.bringToFront() + bridge.webView.setBackgroundColor(Color.TRANSPARENT) + } + + mapViewParent = parent + parent.visibility = if (isMapVisible) View.VISIBLE else View.GONE + if (config.styles != null) { + googleMap?.setMapStyle(MapStyleOptions(config.styles!!)) + } + + if (config.maxZoom != null) { + googleMap?.setMaxZoomPreference(config.maxZoom!!.toFloat()) + } + + if (config.minZoom != null) { + googleMap?.setMinZoomPreference((config.minZoom!!.toFloat())) + } + + if (config.mapTypeId != null) { + when (config.mapTypeId!!) { + "hybrid" -> googleMap?.mapType = GoogleMap.MAP_TYPE_HYBRID + "roadmap" -> googleMap?.mapType = GoogleMap.MAP_TYPE_NORMAL + "satellite" -> googleMap?.mapType = GoogleMap.MAP_TYPE_SATELLITE + "terrain" -> googleMap?.mapType = GoogleMap.MAP_TYPE_TERRAIN + } + } + + if (config.restriction != null) { + googleMap?.setLatLngBoundsForCameraTarget(config.restriction!!.latLngBounds) + } + + if (config.heading != null) { + googleMap?.cameraPosition?.let { cameraPosition -> + googleMap?.animateCamera( + CameraUpdateFactory.newCameraPosition( + CameraPosition.Builder(cameraPosition) + .bearing(config.heading!!.toFloat()) + .build() + ) + ) + } + } + } + } + + fun updateRender(updatedBounds: RectF) { + this.config.x = updatedBounds.left.toInt() + this.config.y = updatedBounds.top.toInt() + this.config.width = updatedBounds.width().toInt() + this.config.height = updatedBounds.height().toInt() + + runBlocking { + CoroutineScope(Dispatchers.Main).launch { + applyLayoutFromConfig() + } + } + } + + fun updateLayout(x: Int, y: Int, width: Int, height: Int) { + config.x = x + config.y = y + config.width = width + config.height = height + CoroutineScope(Dispatchers.Main).launch { + applyLayoutFromConfig() + } + } + + fun applyMapVisibility(isVisible: Boolean) { + isMapVisible = isVisible + CoroutineScope(Dispatchers.Main).launch { + mapViewParent?.visibility = if (isVisible) View.VISIBLE else View.GONE + } + } + + private fun applyLayoutFromConfig() { + val bridge = delegate.bridge + val parent = mapViewParent ?: return + val layoutParams = parent.layoutParams as? FrameLayout.LayoutParams + ?: FrameLayout.LayoutParams( + getScaledPixels(bridge, config.width), + getScaledPixels(bridge, config.height), + ) + layoutParams.width = getScaledPixels(bridge, config.width) + layoutParams.height = getScaledPixels(bridge, config.height) + layoutParams.leftMargin = getScaledPixels(bridge, config.x) + layoutParams.topMargin = getScaledPixels(bridge, config.y) + parent.layoutParams = layoutParams + val childParams = + (mapView.layoutParams as? FrameLayout.LayoutParams) + ?: FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + childParams.width = FrameLayout.LayoutParams.MATCH_PARENT + childParams.height = FrameLayout.LayoutParams.MATCH_PARENT + childParams.leftMargin = 0 + childParams.topMargin = 0 + mapView.layoutParams = childParams + parent.requestLayout() + } + + private fun applyToBackVisualState(bridge: Bridge) { + val webView = bridge.webView + if (originalWebViewAlpha == null) { + originalWebViewAlpha = webView.alpha + } + if (ToBackCompositorHelper.shouldTransparentizeWebViewParent()) { + (webView.parent as? View)?.setBackgroundColor(ToBackCompositorHelper.resolveWebViewBackgroundColor()) + } + webView.setBackgroundColor(ToBackCompositorHelper.resolveWebViewBackgroundColor()) + webView.setLayerType(ToBackCompositorHelper.resolveWebViewLayerType(), null) + val alpha = ToBackCompositorHelper.resolveWebViewAlpha(originalWebViewAlpha ?: 1f) + webView.alpha = alpha + if (!ToBackCompositorHelper.shouldUseHardwareLayerOnPreviewContainer(true)) { + mapViewParent?.setLayerType(View.LAYER_TYPE_NONE, null) + } + bridge.webView.bringToFront() + } + + fun dispatchTouchEvent(event: MotionEvent) { + CoroutineScope(Dispatchers.Main).launch { + val offsetViewBounds = getMapBounds() + + val relativeTop = offsetViewBounds.top + val relativeLeft = offsetViewBounds.left + + event.setLocation(event.x - relativeLeft, event.y - relativeTop) + mapView.dispatchTouchEvent(event) + } + } + + fun bringToFront() { + CoroutineScope(Dispatchers.Main).launch { + val mapViewParent = + ((delegate.bridge.webView.parent) as ViewGroup).findViewWithTag( + this@CapacitorNativeMapView.id + ) + mapViewParent.bringToFront() + } + } + + fun destroy() { + runBlocking { + val job = + CoroutineScope(Dispatchers.Main).launch { + isDestroyed = true + val bridge = delegate.bridge + + val viewToRemove: View? = + ((bridge.webView.parent) as ViewGroup).findViewWithTag(id) + if (null != viewToRemove) { + ((bridge.webView.parent) as ViewGroup).removeView(viewToRemove) + } + if (toBack) { + ToBackCompositorHelper.onToBackMapDestroyed(bridge.webView, id) + } + mapView.onDestroy() + googleMap = null + clusterManager = null + } + + job.join() + } + } + + fun addTileOverlay( + tileOverlay: CapacitorNativeMapViewTileOverlay, + callback: (Result) -> Unit + ) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + val tileProvider = object : UrlTileProvider(256, 256) { + override fun getTileUrl(x: Int, y: Int, zoom: Int): URL? { + return URL(tileOverlay.url + .replace("{x}", "$x") + .replace("{y}", "$y") + .replace("{z}", "$zoom") + ) + } + } + var tileOverlayOptions = TileOverlayOptions().tileProvider(tileProvider) + if (tileOverlay.zIndex != null) { + tileOverlayOptions.zIndex(tileOverlay.zIndex!!) + } + if (tileOverlay.visible != null) { + tileOverlayOptions.visible(tileOverlay.visible!!) + } + if (tileOverlay.opacity != null) { + tileOverlayOptions.transparency(1f - tileOverlay.opacity!!) + } + + val googleMapTileOverlay = googleMap?.addTileOverlay(tileOverlayOptions) + + tileOverlay.googleMapTileOverlay = googleMapTileOverlay + tileOverlays[googleMapTileOverlay!!.id] = tileOverlay + + callback(Result.success(googleMapTileOverlay.id)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + fun removeTileOverlay(id: String, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + val tileOverlay = tileOverlays[id] + tileOverlay ?: throw TileOverlayNotFoundError() + + CoroutineScope(Dispatchers.Main).launch { + tileOverlay.googleMapTileOverlay?.remove() + tileOverlays.remove(id) + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun addMarkers( + newMarkers: List, + callback: (ids: Result>) -> Unit + ) { + try { + googleMap ?: throw NativeMapNotAvailable() + val markerIds: MutableList = mutableListOf() + + CoroutineScope(Dispatchers.Main).launch { + newMarkers.forEach { + val markerOptions: Deferred = + CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildMarker(it) + } + val googleMapMarker = googleMap?.addMarker(markerOptions.await()) + it.googleMapMarker = googleMapMarker + + if (googleMapMarker != null) { + if (clusterManager != null) { + googleMapMarker.remove() + } + + markers[googleMapMarker.id] = it + markerIds.add(googleMapMarker.id) + } + } + + if (clusterManager != null) { + clusterManager?.addItems(newMarkers) + clusterManager?.cluster() + } + + callback(Result.success(markerIds)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + fun addMarker(marker: CapacitorNativeMapViewMarker, callback: (result: Result) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + var markerId: String + + CoroutineScope(Dispatchers.Main).launch { + val markerOptions: Deferred = + CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildMarker(marker) + } + val googleMapMarker = googleMap?.addMarker(markerOptions.await()) + + marker.googleMapMarker = googleMapMarker + + if (clusterManager != null) { + googleMapMarker?.remove() + clusterManager?.addItem(marker) + clusterManager?.cluster() + } + + markers[googleMapMarker!!.id] = marker + + markerId = googleMapMarker.id + + callback(Result.success(markerId)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + fun addPolygons(newPolygons: List, callback: (ids: Result>) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + val shapeIds: MutableList = mutableListOf() + + CoroutineScope(Dispatchers.Main).launch { + newPolygons.forEach { + val polygonOptions: Deferred = CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildPolygon(it) + } + + val googleMapsPolygon = googleMap?.addPolygon(polygonOptions.await()) + googleMapsPolygon?.tag = it.tag + + it.googleMapsPolygon = googleMapsPolygon + + polygons[googleMapsPolygon!!.id] = it + shapeIds.add(googleMapsPolygon.id) + } + + callback(Result.success(shapeIds)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + fun addCircles(newCircles: List,callback: (ids: Result>) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + val circleIds: MutableList = mutableListOf() + + CoroutineScope(Dispatchers.Main).launch { + newCircles.forEach { + var circleOptions: Deferred = CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildCircle(it) + } + + val googleMapsCircle = googleMap?.addCircle(circleOptions.await()) + googleMapsCircle?.tag = it.tag + + it.googleMapsCircle = googleMapsCircle + + circles[googleMapsCircle!!.id] = it + circleIds.add(googleMapsCircle.id) + } + + callback(Result.success(circleIds)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + fun addPolylines(newLines: List, callback: (ids: Result>) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + val lineIds: MutableList = mutableListOf() + + CoroutineScope(Dispatchers.Main).launch { + newLines.forEach { + val polylineOptions: Deferred = CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildPolyline(it) + } + val googleMapPolyline = googleMap?.addPolyline(polylineOptions.await()) + googleMapPolyline?.tag = it.tag + + it.googleMapsPolyline = googleMapPolyline + + polylines[googleMapPolyline!!.id] = it + lineIds.add(googleMapPolyline.id) + } + + callback(Result.success(lineIds)) + } + } catch (e: NativeMapsError) { + callback(Result.failure(e)) + } + } + + private fun setClusterManagerRenderer(minClusterSize: Int?) { + clusterManager?.renderer = CapacitorClusterManagerRenderer( + delegate.bridge.context, + googleMap, + clusterManager, + minClusterSize + ) + } + + @SuppressLint("PotentialBehaviorOverride") + fun enableClustering(minClusterSize: Int?, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + if (clusterManager != null) { + setClusterManagerRenderer(minClusterSize) + callback(null) + return@launch + } + + val bridge = delegate.bridge + clusterManager = ClusterManager(bridge.context, googleMap) + + setClusterManagerRenderer(minClusterSize) + setClusterListeners() + + // add existing markers to the cluster + if (markers.isNotEmpty()) { + for ((_, marker) in markers) { + marker.googleMapMarker?.remove() + // marker.googleMapMarker = null + } + clusterManager?.addItems(markers.values) + clusterManager?.cluster() + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + @SuppressLint("PotentialBehaviorOverride") + fun disableClustering(callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + clusterManager?.clearItems() + clusterManager?.cluster() + clusterManager = null + + googleMap?.setOnMarkerClickListener(this@CapacitorNativeMapView) + + // add existing markers back to the map + if (markers.isNotEmpty()) { + for ((_, marker) in markers) { + val markerOptions: Deferred = + CoroutineScope(Dispatchers.IO).async { + this@CapacitorNativeMapView.buildMarker(marker) + } + val googleMapMarker = googleMap?.addMarker(markerOptions.await()) + marker.googleMapMarker = googleMapMarker + } + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun removePolygons(ids: List, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + ids.forEach { + val polygon = polygons[it] + if (polygon != null) { + polygon.googleMapsPolygon?.remove() + polygons.remove(it) + } + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun removeMarker(id: String, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + val marker = markers[id] + marker ?: throw MarkerNotFoundError() + + CoroutineScope(Dispatchers.Main).launch { + if (clusterManager != null) { + clusterManager?.removeItem(marker) + clusterManager?.cluster() + } + + marker.googleMapMarker?.remove() + markers.remove(id) + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun removeMarkers(ids: List, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + val deletedMarkers: MutableList = mutableListOf() + + ids.forEach { + val marker = markers[it] + if (marker != null) { + marker.googleMapMarker?.remove() + markers.remove(it) + + deletedMarkers.add(marker) + } + } + + if (clusterManager != null) { + clusterManager?.removeItems(deletedMarkers) + clusterManager?.cluster() + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun removeCircles(ids: List, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + ids.forEach { + val circle = circles[it] + if (circle != null) { + circle.googleMapsCircle?.remove() + circles.remove(it) + } + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun removePolylines(ids: List, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + + CoroutineScope(Dispatchers.Main).launch { + ids.forEach { + val polyline = polylines[it] + if (polyline != null) { + polyline.googleMapsPolyline?.remove() + polylines.remove(it) + } + } + + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun setCamera(config: NativeMapCameraConfig, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + val currentPosition = googleMap!!.cameraPosition + + var updatedTarget = config.coordinate + if (updatedTarget == null) { + updatedTarget = currentPosition.target + } + + var zoom = config.zoom + if (zoom == null) { + zoom = currentPosition.zoom.toDouble() + } + + var bearing = config.bearing + if (bearing == null) { + bearing = currentPosition.bearing.toDouble() + } + + var angle = config.angle + if (angle == null) { + angle = currentPosition.tilt.toDouble() + } + + var animate = config.animate + if (animate == null) { + animate = false + } + + val updatedPosition = + CameraPosition.Builder() + .target(updatedTarget) + .zoom(zoom.toFloat()) + .bearing(bearing.toFloat()) + .tilt(angle.toFloat()) + .build() + + if (animate) { + googleMap?.animateCamera(CameraUpdateFactory.newCameraPosition(updatedPosition)) + } else { + googleMap?.moveCamera(CameraUpdateFactory.newCameraPosition(updatedPosition)) + } + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun getMapType(callback: (type: String, error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + val mapType: String = when (googleMap?.mapType) { + MAP_TYPE_NORMAL -> "Normal" + MAP_TYPE_HYBRID -> "Hybrid" + MAP_TYPE_SATELLITE -> "Satellite" + MAP_TYPE_TERRAIN -> "Terrain" + MAP_TYPE_NONE -> "None" + else -> { + "Normal" + } + } + callback(mapType, null); + } + } catch (e: NativeMapsError) { + callback("", e) + } + } + + fun setMapType(mapType: String, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + val mapTypeInt: Int = + when (mapType) { + "Normal" -> MAP_TYPE_NORMAL + "Hybrid" -> MAP_TYPE_HYBRID + "Satellite" -> MAP_TYPE_SATELLITE + "Terrain" -> MAP_TYPE_TERRAIN + "None" -> MAP_TYPE_NONE + else -> { + Log.w( + "CapacitorNativeMap", + "unknown mapView type '$mapType' Defaulting to normal." + ) + MAP_TYPE_NORMAL + } + } + + googleMap?.mapType = mapTypeInt + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun enableIndoorMaps(enabled: Boolean, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + googleMap?.isIndoorEnabled = enabled + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun enableTrafficLayer(enabled: Boolean, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + googleMap?.isTrafficEnabled = enabled + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + @SuppressLint("MissingPermission") + fun enableCurrentLocation(enabled: Boolean, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + googleMap?.isMyLocationEnabled = enabled + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun setPadding(padding: NativeMapPadding, callback: (error: NativeMapsError?) -> Unit) { + try { + googleMap ?: throw NativeMapNotAvailable() + CoroutineScope(Dispatchers.Main).launch { + googleMap?.setPadding(padding.left, padding.top, padding.right, padding.bottom) + callback(null) + } + } catch (e: NativeMapsError) { + callback(e) + } + } + + fun getMapBounds(): Rect { + return Rect( + getScaledPixels(delegate.bridge, config.x), + getScaledPixels(delegate.bridge, config.y), + getScaledPixels(delegate.bridge, config.x + config.width), + getScaledPixels(delegate.bridge, config.y + config.height) + ) + } + + fun getLatLngBounds(): LatLngBounds { + return googleMap?.projection?.visibleRegion?.latLngBounds ?: throw BoundsNotFoundError() + } + + fun fitBounds(bounds: LatLngBounds, padding: Int) { + val cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, padding) + googleMap?.animateCamera(cameraUpdate) + } + + private fun getScaledPixels(bridge: Bridge, pixels: Int): Int { + // Get the screen's density scale + val scale = bridge.activity.resources.displayMetrics.density + // Convert the dps to pixels, based on density scale + return (pixels * scale + 0.5f).toInt() + } + + private fun getScaledPixelsF(bridge: Bridge, pixels: Float): Float { + // Get the screen's density scale + val scale = bridge.activity.resources.displayMetrics.density + // Convert the dps to pixels, based on density scale + return (pixels * scale + 0.5f) + } + + private fun getScaledRect(bridge: Bridge, rectF: RectF): RectF { + return RectF( + getScaledPixelsF(bridge, rectF.left), + getScaledPixelsF(bridge, rectF.top), + getScaledPixelsF(bridge, rectF.right), + getScaledPixelsF(bridge, rectF.bottom) + ) + } + + private fun buildCircle(circle: CapacitorNativeMapCircle): CircleOptions { + val circleOptions = CircleOptions() + circleOptions.fillColor(circle.fillColor) + circleOptions.strokeColor(circle.strokeColor) + circleOptions.strokeWidth(circle.strokeWidth) + circleOptions.zIndex(circle.zIndex) + circleOptions.clickable(circle.clickable) + circleOptions.radius(circle.radius.toDouble()) + circleOptions.center(circle.center) + + return circleOptions + } + + private fun buildPolygon(polygon: CapacitorNativeMapPolygon): PolygonOptions { + val polygonOptions = PolygonOptions() + polygonOptions.fillColor(polygon.fillColor) + polygonOptions.strokeColor(polygon.strokeColor) + polygonOptions.strokeWidth(polygon.strokeWidth) + polygonOptions.zIndex(polygon.zIndex) + polygonOptions.geodesic(polygon.geodesic) + polygonOptions.clickable(polygon.clickable) + + var shapeCounter = 0 + polygon.shapes.forEach { + if (shapeCounter == 0) { + // outer shape + it.forEach { + polygonOptions.add(it) + } + } else { + polygonOptions.addHole(it) + } + + shapeCounter += 1 + } + + return polygonOptions + } + + private fun buildPolyline(line: CapacitorNativeMapViewPolyline): PolylineOptions { + val polylineOptions = PolylineOptions() + polylineOptions.width(line.strokeWidth * this.config.devicePixelRatio) + polylineOptions.color(line.strokeColor) + polylineOptions.clickable(line.clickable) + polylineOptions.zIndex(line.zIndex) + polylineOptions.geodesic(line.geodesic) + + line.path.forEach { + polylineOptions.add(it) + } + + line.styleSpans.forEach { + if (it.segments != null) { + polylineOptions.addSpan(StyleSpan(it.color, it.segments)) + } else { + polylineOptions.addSpan(StyleSpan(it.color)) + } + } + + return polylineOptions + } + + private fun buildMarker(marker: CapacitorNativeMapViewMarker): MarkerOptions { + val markerOptions = MarkerOptions() + markerOptions.position(marker.coordinate) + markerOptions.title(marker.title) + markerOptions.snippet(marker.snippet) + markerOptions.alpha(marker.opacity) + markerOptions.flat(marker.isFlat) + markerOptions.draggable(marker.draggable) + markerOptions.zIndex(marker.zIndex) + if (marker.iconAnchor != null) { + markerOptions.anchor(marker.iconAnchor!!.x, marker.iconAnchor!!.y) + } + + + if (!marker.iconUrl.isNullOrEmpty()) { + if (this.markerIcons.contains(marker.iconUrl)) { + val cachedBitmap = this.markerIcons[marker.iconUrl] + markerOptions.icon(getResizedIcon(cachedBitmap!!, marker)) + } else { + try { + var stream: InputStream? = null + if (marker.iconUrl!!.startsWith("https:")) { + stream = URL(marker.iconUrl).openConnection().getInputStream() + } else { + stream = this.delegate.context.assets.open("public/${marker.iconUrl}") + } + var bitmap = BitmapFactory.decodeStream(stream) + this.markerIcons[marker.iconUrl!!] = bitmap + markerOptions.icon(getResizedIcon(bitmap, marker)) + } catch (e: Exception) { + var detailedMessage = "${e.javaClass} - ${e.localizedMessage}" + if (marker.iconUrl!!.endsWith(".svg")) { + detailedMessage = "SVG not supported" + } + + Log.w( + "CapacitorNativeMap", + "Could not load image '${marker.iconUrl}': ${detailedMessage}. Using default marker icon." + ) + } + } + } else { + if (marker.colorHue != null) { + markerOptions.icon(BitmapDescriptorFactory.defaultMarker(marker.colorHue!!)) + } + } + + marker.markerOptions = markerOptions + + return markerOptions + } + + private fun getResizedIcon( + _bitmap: Bitmap, + marker: CapacitorNativeMapViewMarker + ): BitmapDescriptor { + var bitmap = _bitmap + if (marker.iconSize != null) { + bitmap = + Bitmap.createScaledBitmap( + bitmap, + (marker.iconSize!!.width * this.config.devicePixelRatio).toInt(), + (marker.iconSize!!.height * this.config.devicePixelRatio).toInt(), + false + ) + } + return BitmapDescriptorFactory.fromBitmap(bitmap) + } + + fun onStart() { + mapView.onStart() + } + + fun onResume() { + mapView.onResume() + } + + fun onStop() { + mapView.onStop() + } + + fun onPause() { + mapView.onPause() + } + + fun onDestroy() { + mapView.onDestroy() + } + + override fun onMapReady(map: GoogleMap) { + if (isDestroyed) { + return + } + googleMap = map + setListeners() + + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + delegate.notify("onMapReady", data) + + CoroutineScope(Dispatchers.Main).launch { + if (isDestroyed) { + return@launch + } + isReadyChannel.send(true) + isReadyChannel.close() + } + } + + @SuppressLint("PotentialBehaviorOverride") + fun setListeners() { + CoroutineScope(Dispatchers.Main).launch { + this@CapacitorNativeMapView.googleMap?.setOnCameraIdleListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnCameraMoveStartedListener( + this@CapacitorNativeMapView + ) + this@CapacitorNativeMapView.googleMap?.setOnCameraMoveListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnMarkerClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnPolygonClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnCircleClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnMarkerDragListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnMapClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnMyLocationButtonClickListener( + this@CapacitorNativeMapView + ) + this@CapacitorNativeMapView.googleMap?.setOnMyLocationClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnInfoWindowClickListener(this@CapacitorNativeMapView) + this@CapacitorNativeMapView.googleMap?.setOnPolylineClickListener(this@CapacitorNativeMapView) + } + } + + fun setClusterListeners() { + CoroutineScope(Dispatchers.Main).launch { + clusterManager?.setOnClusterItemClickListener { + if (null == it.googleMapMarker) false + else this@CapacitorNativeMapView.onMarkerClick(it.googleMapMarker!!) + } + + clusterManager?.setOnClusterItemInfoWindowClickListener { + if (null != it.googleMapMarker) { + this@CapacitorNativeMapView.onInfoWindowClick(it.googleMapMarker!!) + } + } + + clusterManager?.setOnClusterInfoWindowClickListener { + val data = this@CapacitorNativeMapView.getClusterData(it) + delegate.notify("onClusterInfoWindowClick", data) + } + + clusterManager?.setOnClusterClickListener { + val data = this@CapacitorNativeMapView.getClusterData(it) + delegate.notify("onClusterClick", data) + false + } + } + } + + private fun getClusterData(it: Cluster): JSObject { + val data = JSObject() + data.put("mapId", this.id) + data.put("latitude", it.position.latitude) + data.put("longitude", it.position.longitude) + data.put("size", it.size) + + val items = JSArray() + for (item in it.items) { + val marker = item.googleMapMarker + + if (marker != null) { + val jsItem = JSObject() + jsItem.put("markerId", marker.id) + jsItem.put("latitude", marker.position.latitude) + jsItem.put("longitude", marker.position.longitude) + jsItem.put("title", marker.title) + jsItem.put("snippet", marker.snippet) + + items.put(jsItem) + } + } + + data.put("items", items) + + return data + } + + override fun onMapClick(point: LatLng) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("latitude", point.latitude) + data.put("longitude", point.longitude) + delegate.notify("onMapClick", data) + } + + override fun onMarkerClick(marker: Marker): Boolean { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("markerId", marker.id) + data.put("latitude", marker.position.latitude) + data.put("longitude", marker.position.longitude) + data.put("title", marker.title) + data.put("snippet", marker.snippet) + delegate.notify("onMarkerClick", data) + return false + } + + override fun onPolylineClick(polyline: Polyline) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("polylineId", polyline.id) + data.put("tag", polyline.tag) + delegate.notify("onPolylineClick", data) + } + + override fun onMarkerDrag(marker: Marker) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("markerId", marker.id) + data.put("latitude", marker.position.latitude) + data.put("longitude", marker.position.longitude) + data.put("title", marker.title) + data.put("snippet", marker.snippet) + delegate.notify("onMarkerDrag", data) + } + + override fun onMarkerDragStart(marker: Marker) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("markerId", marker.id) + data.put("latitude", marker.position.latitude) + data.put("longitude", marker.position.longitude) + data.put("title", marker.title) + data.put("snippet", marker.snippet) + delegate.notify("onMarkerDragStart", data) + } + + override fun onMarkerDragEnd(marker: Marker) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("markerId", marker.id) + data.put("latitude", marker.position.latitude) + data.put("longitude", marker.position.longitude) + data.put("title", marker.title) + data.put("snippet", marker.snippet) + delegate.notify("onMarkerDragEnd", data) + } + + override fun onMyLocationButtonClick(): Boolean { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + delegate.notify("onMyLocationButtonClick", data) + return false + } + + override fun onMyLocationClick(location: Location) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("latitude", location.latitude) + data.put("longitude", location.longitude) + delegate.notify("onMyLocationClick", data) + } + + override fun onCameraIdle() { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("bounds", getLatLngBoundsJSObject(getLatLngBounds())) + data.put("bearing", this@CapacitorNativeMapView.googleMap?.cameraPosition?.bearing) + data.put("latitude", this@CapacitorNativeMapView.googleMap?.cameraPosition?.target?.latitude) + data.put("longitude", this@CapacitorNativeMapView.googleMap?.cameraPosition?.target?.longitude) + data.put("tilt", this@CapacitorNativeMapView.googleMap?.cameraPosition?.tilt) + data.put("zoom", this@CapacitorNativeMapView.googleMap?.cameraPosition?.zoom) + delegate.notify("onCameraIdle", data) + delegate.notify("onBoundsChanged", data) + } + + override fun onCameraMoveStarted(reason: Int) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("isGesture", reason == 1) + delegate.notify("onCameraMoveStarted", data) + } + + override fun onInfoWindowClick(marker: Marker) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("markerId", marker.id) + data.put("latitude", marker.position.latitude) + data.put("longitude", marker.position.longitude) + data.put("title", marker.title) + data.put("snippet", marker.snippet) + delegate.notify("onInfoWindowClick", data) + } + + override fun onCameraMove() { + debounceJob?.cancel() + debounceJob = CoroutineScope(Dispatchers.Main).launch { + delay(100) + clusterManager?.cluster() + } + } + + override fun onPolygonClick(polygon: Polygon) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("polygonId", polygon.id) + data.put("tag", polygon.tag) + delegate.notify("onPolygonClick", data) + } + + override fun onCircleClick(circle: Circle) { + val data = JSObject() + data.put("mapId", this@CapacitorNativeMapView.id) + data.put("circleId", circle.id) + data.put("tag", circle.tag) + data.put("latitude", circle.center.latitude) + data.put("longitude", circle.center.longitude) + data.put("radius", circle.radius) + + delegate.notify("onCircleClick", data) + } +} + +fun getLatLngBoundsJSObject(bounds: LatLngBounds): JSObject { + val data = JSObject() + + val southwestJS = JSObject() + val centerJS = JSObject() + val northeastJS = JSObject() + + southwestJS.put("lat", bounds.southwest.latitude) + southwestJS.put("lng", bounds.southwest.longitude) + centerJS.put("lat", bounds.center.latitude) + centerJS.put("lng", bounds.center.longitude) + northeastJS.put("lat", bounds.northeast.latitude) + northeastJS.put("lng", bounds.northeast.longitude) + + data.put("southwest", southwestJS) + data.put("center", centerJS) + data.put("northeast", northeastJS) + + return data +} diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapMarker.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapMarker.kt new file mode 100644 index 0000000..e7750b7 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapMarker.kt @@ -0,0 +1,88 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color +import android.util.Size +import androidx.core.math.MathUtils +import com.google.android.gms.maps.model.* +import com.google.maps.android.clustering.ClusterItem +import org.json.JSONObject + + +class CapacitorNativeMapViewMarker(fromJSONObject: JSONObject): ClusterItem { + var coordinate: LatLng = LatLng(0.0, 0.0) + var opacity: Float = 1.0f + private var title: String + private var snippet: String + private var zIndex: Float = 0.0f + var isFlat: Boolean = false + var iconUrl: String? = null + var iconSize: Size? = null + var iconAnchor: CapacitorNativeMapPoint? = null + var draggable: Boolean = false + var googleMapMarker: Marker? = null + var colorHue: Float? = null + var markerOptions: MarkerOptions? = null + + init { + if (!fromJSONObject.has("coordinate")) { + throw InvalidArgumentsError("Marker object is missing the required 'coordinate' property") + } + + val latLngObj = fromJSONObject.getJSONObject("coordinate") + if (!latLngObj.has("lat") || !latLngObj.has("lng")) { + throw InvalidArgumentsError("LatLng object is missing the required 'lat' and/or 'lng' property") + } + + coordinate = LatLng(latLngObj.getDouble("lat"), latLngObj.getDouble("lng")) + title = fromJSONObject.optString("title") + opacity = fromJSONObject.optDouble("opacity", 1.0).toFloat() + snippet = fromJSONObject.optString("snippet") + isFlat = fromJSONObject.optBoolean("isFlat", false) + iconUrl = fromJSONObject.optString("iconUrl") + if (fromJSONObject.has("iconSize")) { + val iconSizeObject = fromJSONObject.getJSONObject("iconSize") + iconSize = Size(iconSizeObject.optInt("width", 0), iconSizeObject.optInt("height", 0)) + } + + if (fromJSONObject.has("iconAnchor")) { + val inputAnchorPoint = CapacitorNativeMapPoint(fromJSONObject.getJSONObject("iconAnchor")) + iconAnchor = this.buildIconAnchorPoint(inputAnchorPoint) + } + + if (fromJSONObject.has("tintColor")) { + val tintColorObject = fromJSONObject.getJSONObject("tintColor") + + val r = MathUtils.clamp(tintColorObject.optDouble("r", 0.00), 0.00, 255.0) + val g = MathUtils.clamp(tintColorObject.optDouble("g", 0.00), 0.00, 255.0) + val b = MathUtils.clamp(tintColorObject.optDouble("b", 0.00), 0.00, 255.0) + + val hsl = FloatArray(3) + Color.RGBToHSV(r.toInt(), g.toInt(), b.toInt(), hsl) + + colorHue = hsl[0] + } + + draggable = fromJSONObject.optBoolean("draggable", false) + zIndex = fromJSONObject.optDouble("zIndex", 0.0).toFloat() + } + + override fun getPosition(): LatLng { + return LatLng(coordinate.latitude, coordinate.longitude) + } + + override fun getTitle(): String { + return title + } + + override fun getSnippet(): String { + return snippet + } + + override fun getZIndex(): Float { + return zIndex + } + + private fun buildIconAnchorPoint(iconAnchor: CapacitorNativeMapPoint): CapacitorNativeMapPoint { + return CapacitorNativeMapPoint(iconAnchor.x, iconAnchor.y) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapPolyline.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapPolyline.kt new file mode 100644 index 0000000..eb42f41 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapPolyline.kt @@ -0,0 +1,78 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color +import androidx.core.graphics.toColor +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Polyline +import org.json.JSONObject + +class CapacitorNativeMapViewPolyline(fromJSONObject: JSONObject) { + var path: MutableList = mutableListOf() + var styleSpans: MutableList = mutableListOf() + var strokeWidth: Float = 1.0f + var strokeColor: Int = Color.BLUE + var clickable: Boolean + var geodesic: Boolean + var zIndex: Float = 0.00f + var tag: String = "" + var googleMapsPolyline: Polyline? = null + + init { + if (!fromJSONObject.has("path")) { + throw InvalidArgumentsError("Polyline object is missing the required 'path' property") + } + + val pathArray = fromJSONObject.getJSONArray("path") + + for (i in 0 until pathArray.length()) { + val obj = pathArray.getJSONObject(i) + if (!obj.has("lat") || !obj.has("lng")) { + throw InvalidArgumentsError("LatLng object is missing the required 'lat' and/or 'lng' property") + } + + val lat = obj.getDouble("lat") + val lng = obj.getDouble("lng") + + path.add(LatLng(lat, lng)) + } + + if (fromJSONObject.has("styleSpans")) { + val styleSpanArray = fromJSONObject.getJSONArray("styleSpans") + for (i in 0 until styleSpanArray.length()) { + val obj = styleSpanArray.getJSONObject(i) + + if (obj.has("color")) { + val color = obj.getString("color") + + if (obj.has("segments")) { + val segments = obj.getDouble("segments") + styleSpans.add(CapacitorNativeMapStyleSpan(Color.parseColor(color), segments)) + } else { + styleSpans.add(CapacitorNativeMapStyleSpan(Color.parseColor(color), null)) + } + } + } + } + + val strokeOpacity = fromJSONObject.optDouble("strokeOpacity", 1.0) + + strokeColor = this.processColor(fromJSONObject.optString("strokeColor", "#0000FF"), strokeOpacity) + strokeWidth = fromJSONObject.optDouble("strokeWeight", 1.0).toFloat() + clickable = fromJSONObject.optBoolean("clickable", false) + geodesic = fromJSONObject.optBoolean("geodesic", false) + zIndex = fromJSONObject.optDouble("zIndex", 1.0).toFloat() + tag = fromJSONObject.optString("tag", "") + } + + private fun processColor(hex: String, opacity: Double): Int { + val colorInt = Color.parseColor(hex) + + val alpha = (opacity * 255.0).toInt() + val red = android.graphics.Color.red(colorInt) + val green = android.graphics.Color.green(colorInt) + val blue = android.graphics.Color.blue(colorInt) + + return Color.argb(alpha, red, green, blue) + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapTileOverlay.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapTileOverlay.kt new file mode 100644 index 0000000..f7a0bb3 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapTileOverlay.kt @@ -0,0 +1,28 @@ +package com.capgo.plugins.nativemap + +import com.google.android.gms.maps.model.TileOverlay +import org.json.JSONObject + +class CapacitorNativeMapViewTileOverlay(fromJSONObject: JSONObject) { + var url: String + var opacity: Float? = null + var zIndex: Float? = null + var visible: Boolean? = null + var googleMapTileOverlay: TileOverlay? = null + + init { + if (!fromJSONObject.has("url")) { + throw InvalidArgumentsError("TileOverlay object is missing the required 'url' property") + } + url = fromJSONObject.getString("url") + if (fromJSONObject.has("opacity")) { + opacity = fromJSONObject.optDouble("opacity").toFloat() + } + if (fromJSONObject.has("visible")) { + visible = fromJSONObject.optBoolean("visible") + } + if (fromJSONObject.has("zIndex")) { + zIndex = fromJSONObject.optDouble("zIndex").toFloat() + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsBounds.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsBounds.kt new file mode 100644 index 0000000..2e4e002 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsBounds.kt @@ -0,0 +1,33 @@ +package com.capgo.plugins.nativemap + +import org.json.JSONObject + +class CapacitorNativeMapBounds(fromJSONObject: JSONObject) { + var width: Int = 0 + var height: Int = 0 + var x: Int = 0 + var y: Int = 0 + + init { + if(!fromJSONObject.has("width")) { + throw InvalidArgumentsError("NativeMapConfig object is missing the required 'width' property") + } + + if(!fromJSONObject.has("height")) { + throw InvalidArgumentsError("NativeMapConfig object is missing the required 'height' property") + } + + if(!fromJSONObject.has("x")) { + throw InvalidArgumentsError("NativeMapConfig object is missing the required 'x' property") + } + + if(!fromJSONObject.has("y")) { + throw InvalidArgumentsError("NativeMapConfig object is missing the required 'y' property") + } + + width = fromJSONObject.getInt("width") + height = fromJSONObject.getInt("height") + x = fromJSONObject.getInt("x") + y = fromJSONObject.getInt("y") + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsCircle.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsCircle.kt new file mode 100644 index 0000000..6298094 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsCircle.kt @@ -0,0 +1,67 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color +import com.google.android.gms.maps.model.Circle +import com.google.android.gms.maps.model.LatLng +import org.json.JSONObject + +class CapacitorNativeMapCircle(fromJSONObject: JSONObject) { + var center: LatLng + var radius: Float + var strokeWidth: Float = 1.0f + var strokeColor: Int = Color.BLUE + var fillColor: Int = Color.BLUE + var clickable: Boolean + var zIndex: Float = 0.00f + var tag: String = "" + var googleMapsCircle: Circle? = null + + init { + if (!fromJSONObject.has("center")) { + throw InvalidArgumentsError("Circle object is missing the required 'center' property") + } + + if (!fromJSONObject.has("radius")) { + throw InvalidArgumentsError("Circle object is missing the required 'radius' property") + } + + val latLng = fromJSONObject.getJSONObject("center") + if (!latLng.has("lat") || !latLng.has("lng")) { + throw InvalidArgumentsError("LatLng object is missing the required 'lat' and/or 'lng' property") + } + + val lat = latLng.getDouble("lat") + val lng = latLng.getDouble("lng") + + center = LatLng(lat, lng) + + radius = fromJSONObject.getDouble("radius").toFloat() + + val strokeOpacity = fromJSONObject.optDouble("strokeOpacity", 1.0) + if (fromJSONObject.has("strokeColor")) { + strokeColor = this.processColor(fromJSONObject.getString("strokeColor"), strokeOpacity) + } + + val fillOpacity = fromJSONObject.optDouble("fillOpacity", 1.0) + if (fromJSONObject.has("fillColor")) { + fillColor = this.processColor(fromJSONObject.getString("fillColor"), fillOpacity) + } + + strokeWidth = fromJSONObject.optDouble("strokeWeight", 1.0).toFloat() + clickable = fromJSONObject.optBoolean("clickable", true) + zIndex = fromJSONObject.optDouble("zIndex", 1.0).toFloat() + tag = fromJSONObject.optString("tag", "") + } + + + private fun processColor(hex: String, opacity: Double): Int { + val colorInt = Color.parseColor(hex) + + val alpha = (opacity * 255.0).toInt() + val red = android.graphics.Color.red(colorInt) + val green = android.graphics.Color.green(colorInt) + val blue = android.graphics.Color.blue(colorInt) + + return Color.argb(alpha, red, green, blue) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPlugin.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPlugin.kt new file mode 100644 index 0000000..4dd985c --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPlugin.kt @@ -0,0 +1,1227 @@ +/* + * Portions Copyright (c) 2020-present Ionic, licensed under the MIT License. + * Adapted for @capgo/capacitor-native-map (MPL-2.0). + */ +package com.capgo.plugins.nativemap + +import android.Manifest +import android.annotation.SuppressLint +import android.graphics.RectF +import android.util.Log +import android.view.MotionEvent +import android.view.View +import com.getcapacitor.* +import com.getcapacitor.annotation.CapacitorPlugin +import com.getcapacitor.annotation.Permission +import com.getcapacitor.annotation.PermissionCallback +import com.google.android.gms.maps.MapsInitializer +import com.google.android.gms.maps.OnMapsSdkInitializedCallback +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import org.json.JSONArray +import org.json.JSONObject + +@CapacitorPlugin( + name = "NativeMap", + permissions = + [ + Permission( + strings = [Manifest.permission.ACCESS_FINE_LOCATION], + alias = CapacitorNativeMapPlugin.LOCATION + ), + ], +) +class CapacitorNativeMapPlugin : Plugin(), OnMapsSdkInitializedCallback { + private var maps: HashMap = HashMap() + private var cachedTouchEvents: HashMap> = HashMap() + private val tag: String = "CAP-NATIVE-MAP" + private var touchEnabled: HashMap = HashMap() + + companion object { + const val LOCATION = "location" + } + + @SuppressLint("ClickableViewAccessibility") + override fun load() { + super.load() + + MapsInitializer.initialize(this.context, MapsInitializer.Renderer.LATEST, this) + + + this.bridge.webView.setOnTouchListener( + object : View.OnTouchListener { + override fun onTouch(v: View?, event: MotionEvent?): Boolean { + if (event != null) { + if (event.source == -1) { + return v?.onTouchEvent(event) ?: true + } + + val touchX = event.x + val touchY = event.y + + for ((id, map) in maps) { + if (touchEnabled[id] == false) { + continue + } + if (!map.isMapVisible) { + continue + } + val mapRect = map.getMapBounds() + if (mapRect.contains(touchX.toInt(), touchY.toInt())) { + if (event.action == MotionEvent.ACTION_DOWN) { + if (cachedTouchEvents[id] == null) { + cachedTouchEvents[id] = mutableListOf() + } + + cachedTouchEvents[id]?.clear() + } + + val motionEvent = MotionEvent.obtain(event) + cachedTouchEvents[id]?.add(motionEvent) + + val payload = JSObject() + payload.put("x", touchX / map.config.devicePixelRatio) + payload.put("y", touchY / map.config.devicePixelRatio) + payload.put("mapId", map.id) + payload.put("toBack", map.toBack) + + notifyListeners("isMapInFocus", payload) + return true + } + } + } + + return v?.onTouchEvent(event) ?: true + } + } + ) + } + + override fun onMapsSdkInitialized(renderer: MapsInitializer.Renderer) { + when (renderer) { + MapsInitializer.Renderer.LATEST -> Logger.debug("Capacitor Google Maps", "Latest Google Maps renderer enabled") + MapsInitializer.Renderer.LEGACY -> Logger.debug("Capacitor Google Maps", "Legacy Google Maps renderer enabled - Cloud based map styling and advanced drawing not available") + } + } + + override fun handleOnStart() { + super.handleOnStart() + maps.forEach { it.value.onStart() } + } + + override fun handleOnResume() { + super.handleOnResume() + maps.forEach { it.value.onResume() } + } + + override fun handleOnPause() { + super.handleOnPause() + maps.forEach { it.value.onPause() } + } + + override fun handleOnStop() { + super.handleOnStop() + maps.forEach { it.value.onStop() } + } + + override fun handleOnDestroy() { + super.handleOnDestroy() + maps.forEach { it.value.onDestroy() } + } + + @PluginMethod + fun create(call: PluginCall) { + try { + val id = call.getString("id") + + if (null == id || id.isEmpty()) { + throw InvalidMapIdError() + } + + val configObject = + call.getObject("config") + ?: throw InvalidArgumentsError("config object is missing") + + val forceCreate = call.getBoolean("forceCreate", false)!! + + val config = NativeMapConfig(configObject) + + if (maps.contains(id)) { + if (!forceCreate) { + call.resolve() + return + } + + val oldMap = maps.remove(id) + oldMap?.destroy() + } + + val toBack = call.getBoolean("toBack", false)!! + val newMap = CapacitorNativeMapView(id, config, this) + newMap.toBack = toBack + maps[id] = newMap + + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun destroy(call: PluginCall) { + try { + val id = call.getString("id") + + if (null == id || id.isEmpty()) { + throw InvalidMapIdError() + } + + val removedMap = maps.remove(id) ?: throw MapNotFoundError() + removedMap.destroy() + + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun updateLayout(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] ?: throw MapNotFoundError() + + val x = call.getInt("x", map.config.x)!! + val y = call.getInt("y", map.config.y)!! + val width = call.getInt("width", map.config.width)!! + val height = call.getInt("height", map.config.height)!! + + map.updateLayout(x, y, width, height) + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun show(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + val map = maps[id] ?: throw MapNotFoundError() + map.applyMapVisibility(true) + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun hide(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + val map = maps[id] ?: throw MapNotFoundError() + map.applyMapVisibility(false) + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableTouch(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + touchEnabled[id] = true + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun disableTouch(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + touchEnabled[id] = false + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addTileOverlay(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val tileOverlayObj = call.getObject("tileOverlay", null) + tileOverlayObj ?: throw InvalidArgumentsError("tileOverlay object is missing") + + val map = maps[id] + map ?: throw MapNotFoundError() + + val tileOverlay = CapacitorNativeMapViewTileOverlay(tileOverlayObj) + map.addTileOverlay(tileOverlay) { result -> + val tileOverlayId = result.getOrThrow() + + val res = JSObject() + res.put("id", tileOverlayId) + call.resolve(res) + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removeTileOverlay(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val tileOverlayId = call.getString("tileOverlayId") + tileOverlayId ?: throw InvalidArgumentsError("tileOverlayId is invalid or missing") + + val map = maps[id] + map ?: throw MapNotFoundError() + + map.removeTileOverlay(tileOverlayId) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addMarker(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val markerObj = call.getObject("marker", null) + markerObj ?: throw InvalidArgumentsError("marker object is missing") + + val map = maps[id] + map ?: throw MapNotFoundError() + + val marker = CapacitorNativeMapViewMarker(markerObj) + map.addMarker(marker) { result -> + val markerId = result.getOrThrow() + + val res = JSObject() + res.put("id", markerId) + call.resolve(res) + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addMarkers(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val markerObjectArray = call.getArray("markers", null) + markerObjectArray ?: throw InvalidArgumentsError("markers array is missing") + + if (markerObjectArray.length() == 0) { + throw InvalidArgumentsError("markers array requires at least one marker") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val markers: MutableList = mutableListOf() + + for (i in 0 until markerObjectArray.length()) { + val markerObj = markerObjectArray.getJSONObject(i) + val marker = CapacitorNativeMapViewMarker(markerObj) + + markers.add(marker) + } + + map.addMarkers(markers) { result -> + val ids = result.getOrThrow() + + val jsonIDs = JSONArray() + ids.forEach { jsonIDs.put(it) } + + val res = JSObject() + res.put("ids", jsonIDs) + call.resolve(res) + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addPolygons(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val polygonsObjectArray = call.getArray("polygons", null) + polygonsObjectArray ?: throw InvalidArgumentsError("polygons array is missing") + + if (polygonsObjectArray.length() == 0) { + throw InvalidArgumentsError("polygons requires at least one shape") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val polygons: MutableList = mutableListOf() + + for (i in 0 until polygonsObjectArray.length()) { + val polygonObj = polygonsObjectArray.getJSONObject(i) + val polygon = CapacitorNativeMapPolygon(polygonObj) + + polygons.add(polygon) + } + + map.addPolygons(polygons) { result -> + val ids = result.getOrThrow() + + val jsonIDs = JSONArray() + ids.forEach { jsonIDs.put(it) } + + val res = JSObject() + res.put("ids", jsonIDs) + call.resolve(res) + } + + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removePolygons(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val shapeIdsArray = call.getArray("polygonIds") + shapeIdsArray ?: throw InvalidArgumentsError("polygonIds are invalid or missing") + + if (shapeIdsArray.length() == 0) { + throw InvalidArgumentsError("polygonIds requires at least one shape id") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val shapeIds: MutableList = mutableListOf() + + for (i in 0 until shapeIdsArray.length()) { + val shapeId = shapeIdsArray.getString(i) + shapeIds.add(shapeId) + } + + map.removePolygons(shapeIds) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addCircles(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val circlesObjectArray = call.getArray("circles", null) + circlesObjectArray ?: throw InvalidArgumentsError("circles array is missing") + + if (circlesObjectArray.length() == 0) { + throw InvalidArgumentsError("circles array requires at least one circle") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val circles: MutableList = mutableListOf() + + for (i in 0 until circlesObjectArray.length()) { + val circleObj = circlesObjectArray.getJSONObject(i) + val circle = CapacitorNativeMapCircle(circleObj) + + circles.add(circle) + } + + map.addCircles(circles) { result -> + val ids = result.getOrThrow() + + val jsonIDs = JSONArray() + ids.forEach { jsonIDs.put(it) } + + val res = JSObject() + res.put("ids", jsonIDs) + call.resolve(res) + } + + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun addPolylines(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val polylinesObjectArray = call.getArray("polylines", null) + polylinesObjectArray ?: throw InvalidArgumentsError("polylines array is missing") + + if (polylinesObjectArray.length() == 0) { + throw InvalidArgumentsError("polylines requires at least one line") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val polylines: MutableList = mutableListOf() + + for (i in 0 until polylinesObjectArray.length()) { + val polylineObj = polylinesObjectArray.getJSONObject(i) + val polyline = CapacitorNativeMapViewPolyline(polylineObj) + + polylines.add(polyline) + } + + map.addPolylines(polylines) { result -> + val ids = result.getOrThrow() + + val jsonIDs = JSONArray() + ids.forEach { jsonIDs.put(it) } + + val res = JSObject() + res.put("ids", jsonIDs) + call.resolve(res) + } + + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removeCircles(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val circleIdsArray = call.getArray("circleIds") + circleIdsArray ?: throw InvalidArgumentsError("circleIds are invalid or missing") + + if (circleIdsArray.length() == 0) { + throw InvalidArgumentsError("circleIds requires at least one circle id") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val circleIds: MutableList = mutableListOf() + + for (i in 0 until circleIdsArray.length()) { + val circleId = circleIdsArray.getString(i) + circleIds.add(circleId) + } + + map.removeCircles(circleIds) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableClustering(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val minClusterSize = call.getInt("minClusterSize") + + val map = maps[id] + map ?: throw MapNotFoundError() + + map.enableClustering(minClusterSize, { err -> + if (err != null) { + throw err + } + + call.resolve() + }) + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun disableClustering(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + map.disableClustering { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removeMarker(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val markerId = call.getString("markerId") + markerId ?: throw InvalidArgumentsError("markerId is invalid or missing") + + val map = maps[id] + map ?: throw MapNotFoundError() + + map.removeMarker(markerId) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removeMarkers(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val markerIdsArray = call.getArray("markerIds") + markerIdsArray ?: throw InvalidArgumentsError("markerIds are invalid or missing") + + if (markerIdsArray.length() == 0) { + throw InvalidArgumentsError("markerIds requires at least one marker id") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val markerIds: MutableList = mutableListOf() + + for (i in 0 until markerIdsArray.length()) { + val markerId = markerIdsArray.getString(i) + markerIds.add(markerId) + } + + map.removeMarkers(markerIds) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun removePolylines(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val lineIdsArray = call.getArray("polylineIds") + lineIdsArray ?: throw InvalidArgumentsError("polylineIds are invalid or missing") + + if (lineIdsArray.length() == 0) { + throw InvalidArgumentsError("polylineIds requires at least one line id") + } + + val map = maps[id] + map ?: throw MapNotFoundError() + + val lineIds: MutableList = mutableListOf() + + for (i in 0 until lineIdsArray.length()) { + val markerId = lineIdsArray.getString(i) + lineIds.add(markerId) + } + + map.removePolylines(lineIds) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun setCamera(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val cameraConfigObject = + call.getObject("config") + ?: throw InvalidArgumentsError("config object is missing") + + val config = NativeMapCameraConfig(cameraConfigObject) + + map.setCamera(config) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun getMapType(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + map.getMapType() { type, err -> + + if (err != null) { + throw err + } + val data = JSObject() + data.put("type", type) + call.resolve(data) + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun setMapType(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val mapType = + call.getString("mapType") ?: throw InvalidArgumentsError("mapType is missing") + + map.setMapType(mapType) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableIndoorMaps(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val enabled = + call.getBoolean("enabled") ?: throw InvalidArgumentsError("enabled is missing") + + map.enableIndoorMaps(enabled) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableTrafficLayer(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val enabled = + call.getBoolean("enabled") ?: throw InvalidArgumentsError("enabled is missing") + + map.enableTrafficLayer(enabled) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableCurrentLocation(call: PluginCall) { + if (getPermissionState(LOCATION) != PermissionState.GRANTED) { + requestAllPermissions(call, "enableCurrentLocationCallback") + } else { + internalEnableCurrentLocation(call) + } + } + + @PermissionCallback + fun enableCurrentLocationCallback(call: PluginCall) { + if (getPermissionState(LOCATION) == PermissionState.GRANTED) { + internalEnableCurrentLocation(call) + } else { + call.reject("location permission was denied") + } + } + + @PluginMethod + fun setPadding(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val paddingObj = + call.getObject("padding") ?: throw InvalidArgumentsError("padding is missing") + + val padding = NativeMapPadding(paddingObj) + + map.setPadding(padding) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun enableAccessibilityElements(call: PluginCall) { + call.unavailable("this call is not available on android") + } + + @PluginMethod + fun onScroll(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val boundsObj = + call.getObject("mapBounds") + ?: throw InvalidArgumentsError("mapBounds object is missing") + + val bounds = boundsObjectToRect(boundsObj) + + map.updateRender(bounds) + + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun onResize(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val boundsObj = + call.getObject("mapBounds") + ?: throw InvalidArgumentsError("mapBounds object is missing") + + val bounds = boundsObjectToRect(boundsObj) + + map.updateRender(bounds) + + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun onDisplay(call: PluginCall) { + call.unavailable("this call is not available on android") + } + + @PluginMethod + fun dispatchMapEvent(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val focus = call.getBoolean("focus", false)!! + + val events = cachedTouchEvents[id] + if (events != null) { + while(events.size > 0) { + val event = events.first() + if (focus) { + map.dispatchTouchEvent(event) + } else { + this.bridge.webView.onTouchEvent(event) + } + events.removeAt(0) + } + } + + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun getMapBounds(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + CoroutineScope(Dispatchers.Main).launch { + try { + val bounds = map.getLatLngBounds() + val data = getLatLngBoundsJSObject(bounds) + call.resolve(data) + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun mapBoundsContains(call: PluginCall) { + try { + val boundsObject = + call.getObject("bounds") ?: throw InvalidArgumentsError("bounds is missing") + val pointObject = + call.getObject("point") ?: throw InvalidArgumentsError("point is missing") + val bounds = createLatLngBounds(boundsObject) + val point = createLatLng(pointObject) + + CoroutineScope(Dispatchers.Main).launch { + try { + val contains = bounds.contains(point) + val data = JSObject() + data.put("contains", contains) + call.resolve(data) + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun fitBounds(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val boundsObject = + call.getObject("bounds") ?: throw InvalidArgumentsError("bounds is missing") + + val padding = call.getInt("padding", 0)!! + val bounds = createLatLngBounds(boundsObject) + + CoroutineScope(Dispatchers.Main).launch { + try { + val density = bridge.context.resources.displayMetrics.density + val scaledPadding = (padding * density).toInt() + map.fitBounds(bounds, scaledPadding) + call.resolve() + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + @PluginMethod + fun mapBoundsExtend(call: PluginCall) { + try { + val boundsObject = + call.getObject("bounds") ?: throw InvalidArgumentsError("bounds is missing") + val pointObject = + call.getObject("point") ?: throw InvalidArgumentsError("point is missing") + val bounds = createLatLngBounds(boundsObject) + val point = createLatLng(pointObject) + + CoroutineScope(Dispatchers.Main).launch { + try { + val newBounds = bounds.including(point) + val data = JSObject() + data.put("bounds", getLatLngBoundsJSObject(newBounds)) + call.resolve(data) + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + private fun createLatLng(point: JSObject): LatLng { + return LatLng( + point.getDouble("lat"), + point.getDouble("lng") + ) + } + + private fun createLatLngBounds(boundsObject: JSObject): LatLngBounds { + val southwestObject = boundsObject.getJSObject("southwest")!! + val southwestLatLng = createLatLng(southwestObject) + + val northeastObject = boundsObject.getJSObject("northeast")!! + val northeastLatLng = createLatLng(northeastObject) + + return LatLngBounds(southwestLatLng, northeastLatLng) + } + + private fun internalEnableCurrentLocation(call: PluginCall) { + try { + val id = call.getString("id") + id ?: throw InvalidMapIdError() + + val map = maps[id] + map ?: throw MapNotFoundError() + + val enabled = + call.getBoolean("enabled") ?: throw InvalidArgumentsError("enabled is missing") + + map.enableCurrentLocation(enabled) { err -> + if (err != null) { + throw err + } + + call.resolve() + } + } catch (e: NativeMapsError) { + handleError(call, e) + } catch (e: Exception) { + handleError(call, e) + } + } + + fun notify(event: String, data: JSObject) { + notifyListeners(event, data) + } + + private fun handleError(call: PluginCall, e: Exception) { + val error: NativeMapErrorObject = getErrorObject(e) + Log.w(tag, error.toString()) + call.reject(error.message, error.code.toString(), e) + } + + private fun handleError(call: PluginCall, e: NativeMapsError) { + val error: NativeMapErrorObject = getErrorObject(e) + Log.w(tag, error.toString()) + call.reject(error.message, error.code.toString()) + } + + private fun boundsObjectToRect(jsonObject: JSONObject): RectF { + if (!jsonObject.has("width")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'width' property" + ) + } + + if (!jsonObject.has("height")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'height' property" + ) + } + + if (!jsonObject.has("x")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'x' property" + ) + } + + if (!jsonObject.has("y")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'y' property" + ) + } + + val width = jsonObject.getDouble("width") + val height = jsonObject.getDouble("height") + val x = jsonObject.getDouble("x") + val y = jsonObject.getDouble("y") + + return RectF(x.toFloat(), y.toFloat(), (x + width).toFloat(), (y + height).toFloat()) + } +} diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPoint.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPoint.kt new file mode 100644 index 0000000..628842b --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPoint.kt @@ -0,0 +1,23 @@ +package com.capgo.plugins.nativemap + +import org.json.JSONObject + +class CapacitorNativeMapPoint() { + var x: Float = 0.00f + var y: Float = 0.00f + + constructor(fromJSONObject: JSONObject) : this() { + if(fromJSONObject.has("x")) { + this.x = fromJSONObject.getDouble("x").toFloat() + } + + if(fromJSONObject.has("y")) { + this.y = fromJSONObject.getDouble("y").toFloat() + } + } + + constructor(x: Float, y: Float) : this() { + this.x = x; + this.y = y + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPolygon.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPolygon.kt new file mode 100644 index 0000000..a693b59 --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsPolygon.kt @@ -0,0 +1,84 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.Polygon +import org.json.JSONArray +import org.json.JSONObject + +class CapacitorNativeMapPolygon(fromJSONObject: JSONObject) { + var shapes: MutableList> = mutableListOf>() + var strokeWidth: Float = 1.0f + var strokeColor: Int = Color.BLUE + var fillColor: Int = Color.BLUE + var clickable: Boolean + var geodesic: Boolean + var zIndex: Float = 0.00f + var tag: String = "" + var googleMapsPolygon: Polygon? = null + + init { + if (!fromJSONObject.has("paths")) { + throw InvalidArgumentsError("Polygon object is missing the required 'paths' property") + } + + val pathsArray = fromJSONObject.getJSONArray("paths") + for (i in 0 until pathsArray.length()) { + val arr = pathsArray.optJSONArray(i) + if (arr == null) { + // is a single shape + val shape = this.processShape(pathsArray) + this.shapes.add(shape) + break + } else { + val shape = this.processShape(arr) + this.shapes.add(shape) + } + } + + val strokeOpacity = fromJSONObject.optDouble("strokeOpacity", 1.0) + if (fromJSONObject.has("strokeColor")) { + strokeColor = this.processColor(fromJSONObject.getString("strokeColor"), strokeOpacity) + } + + val fillOpacity = fromJSONObject.optDouble("fillOpacity", 1.0) + if (fromJSONObject.has("fillColor")) { + fillColor = this.processColor(fromJSONObject.getString("fillColor"), fillOpacity) + } + + strokeWidth = fromJSONObject.optDouble("strokeWeight", 1.0).toFloat() + clickable = fromJSONObject.optBoolean("clickable", false) + geodesic = fromJSONObject.optBoolean("geodesic", false) + zIndex = fromJSONObject.optDouble("zIndex", 1.0).toFloat() + tag = fromJSONObject.optString("tag", "") + } + + private fun processShape(shapeArr: JSONArray): MutableList { + var shape = mutableListOf() + + for (i in 0 until shapeArr.length()) { + val obj = shapeArr.getJSONObject(i) + if (!obj.has("lat") || !obj.has("lng")) { + throw InvalidArgumentsError("LatLng object is missing the required 'lat' and/or 'lng' property") + } + + val lat = obj.getDouble("lat") + val lng = obj.getDouble("lng") + + shape.add(LatLng(lat, lng)) + } + + return shape + } + + private fun processColor(hex: String, opacity: Double): Int { + val colorInt = Color.parseColor(hex) + + val alpha = (opacity * 255.0).toInt() + val red = android.graphics.Color.red(colorInt) + val green = android.graphics.Color.green(colorInt) + val blue = android.graphics.Color.blue(colorInt) + + return Color.argb(alpha, red, green, blue) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsStyleSpan.kt b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsStyleSpan.kt new file mode 100644 index 0000000..6b1055e --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/CapacitorGoogleMapsStyleSpan.kt @@ -0,0 +1,5 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color + +data class CapacitorNativeMapStyleSpan(val color: Int, val segments: Double?) diff --git a/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapCameraConfig.kt b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapCameraConfig.kt new file mode 100644 index 0000000..328cadc --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapCameraConfig.kt @@ -0,0 +1,50 @@ +package com.capgo.plugins.nativemap + +import com.google.android.gms.maps.model.LatLng +import org.json.JSONObject + +class NativeMapCameraConfig(fromJSONObject: JSONObject) { + var coordinate: LatLng? = null + var zoom: Double? = null + var angle: Double? = null + var bearing: Double? = null + var animate: Boolean? = null + var animationDuration: Double? = null + + init { + if (fromJSONObject.has("zoom")) { + zoom = fromJSONObject.getDouble("zoom") + } + + if(fromJSONObject.has("angle")) { + angle = fromJSONObject.getDouble("angle") + } + + if (fromJSONObject.has("bearing")) { + bearing = fromJSONObject.getDouble("bearing") + } + + if (fromJSONObject.has("animate")) { + animate = fromJSONObject.getBoolean("animate") + } + + if (fromJSONObject.has("animationDuration")) { + animationDuration = fromJSONObject.getDouble("animationDuration") + } + + if (fromJSONObject.has("coordinate")) { + val coordinateJSONObject = fromJSONObject.getJSONObject("coordinate") + if(!coordinateJSONObject.has("lat") || !coordinateJSONObject.has("lng")) { + throw InvalidArgumentsError("LatLng object is missing the required 'lat' and/or 'lng' property") + } + + val lat = coordinateJSONObject.getDouble("lat") + val lng = coordinateJSONObject.getDouble("lng") + coordinate = LatLng(lat, lng) + } else { + coordinate = null + } + } + + +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapConfig.kt b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapConfig.kt new file mode 100644 index 0000000..4eb56ae --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapConfig.kt @@ -0,0 +1,165 @@ +package com.capgo.plugins.nativemap + +import com.google.android.gms.maps.GoogleMapOptions +import com.google.android.gms.maps.model.CameraPosition +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps.model.LatLngBounds +import org.json.JSONObject + +class NativeMapConfig(fromJSONObject: JSONObject) { + var width: Int = 0 + var height: Int = 0 + var x: Int = 0 + var y: Int = 0 + var center: LatLng = LatLng(0.0, 0.0) + var googleMapOptions: GoogleMapOptions? = null + var zoom: Int = 0 + var liteMode: Boolean = false + var devicePixelRatio: Float = 1.00f + var styles: String? = null + var mapId: String? = null + var minZoom: Int? = null + var maxZoom: Int? = null + var mapTypeId: String? = null + var restriction: NativeMapConfigRestriction? = null + var heading: Double? = null + + init { + if (!fromJSONObject.has("width")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'width' property" + ) + } + + if (!fromJSONObject.has("height")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'height' property" + ) + } + + if (!fromJSONObject.has("x")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'x' property" + ) + } + + if (!fromJSONObject.has("y")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'y' property" + ) + } + + if (!fromJSONObject.has("zoom")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'zoom' property" + ) + } + + if (fromJSONObject.has("devicePixelRatio")) { + devicePixelRatio = fromJSONObject.getDouble("devicePixelRatio").toFloat() + } + + if (!fromJSONObject.has("center")) { + throw InvalidArgumentsError( + "NativeMapConfig object is missing the required 'center' property" + ) + } + + val centerJSONObject = fromJSONObject.getJSONObject("center") + + if (!centerJSONObject.has("lat") || !centerJSONObject.has("lng")) { + throw InvalidArgumentsError( + "LatLng object is missing the required 'lat' and/or 'lng' property" + ) + } + + liteMode = + fromJSONObject.has("androidLiteMode") && + fromJSONObject.getBoolean("androidLiteMode") + + width = fromJSONObject.getInt("width") + height = fromJSONObject.getInt("height") + x = fromJSONObject.getInt("x") + y = fromJSONObject.getInt("y") + + val tempZoom = fromJSONObject.getInt("zoom") + var tempMinZoom = fromJSONObject.optInt("minZoom").takeIf { fromJSONObject.has("minZoom") } + var tempMaxZoom = fromJSONObject.optInt("maxZoom").takeIf { fromJSONObject.has("maxZoom") } + + if (tempMinZoom != null && tempMaxZoom != null && tempMinZoom > tempMaxZoom) { + tempMinZoom = tempMaxZoom.also { tempMaxZoom = tempMinZoom } + } + + minZoom = tempMinZoom + maxZoom = tempMaxZoom + + zoom = tempZoom.coerceIn( + tempMinZoom ?: Int.MIN_VALUE, + tempMaxZoom ?: Int.MAX_VALUE + ) + + mapTypeId = fromJSONObject.optString("mapTypeId").takeIf { fromJSONObject.has("mapTypeId") } + + val lat = centerJSONObject.getDouble("lat") + val lng = centerJSONObject.getDouble("lng") + center = LatLng(lat, lng) + + val cameraPosition = CameraPosition(center, zoom.toFloat(), 0.0F, 0.0F) + + styles = fromJSONObject.optString("styles").takeIf { fromJSONObject.has("styles") } + + mapId = fromJSONObject.optString("androidMapId").takeIf { fromJSONObject.has("androidMapId") } + + restriction = fromJSONObject.optJSONObject("restriction")?.let { NativeMapConfigRestriction(it) } + + heading = fromJSONObject.optDouble("heading").takeIf { fromJSONObject.has("heading") } + + googleMapOptions = GoogleMapOptions().camera(cameraPosition).liteMode(liteMode) + if (mapId != null) { + googleMapOptions?.mapId(mapId!!) + } + } +} + +class NativeMapConfigRestriction(fromJSONObject: JSONObject) { + var latLngBounds: LatLngBounds + + init { + if (!fromJSONObject.has("latLngBounds")) { + throw InvalidArgumentsError( + "NativeMapConfigRestriction object is missing the required 'latLngBounds' property" + ) + } + val latLngBoundsObj = fromJSONObject.getJSONObject("latLngBounds") + + if (!latLngBoundsObj.has("north")) { + throw InvalidArgumentsError( + "NativeMapConfigRestriction object is missing the required 'latLngBounds.north' property" + ) + } + if (!latLngBoundsObj.has("south")) { + throw InvalidArgumentsError( + "NativeMapConfigRestriction object is missing the required 'latLngBounds.south' property" + ) + } + if (!latLngBoundsObj.has("east")) { + throw InvalidArgumentsError( + "NativeMapConfigRestriction object is missing the required 'latLngBounds.east' property" + ) + } + if (!latLngBoundsObj.has("west")) { + throw InvalidArgumentsError( + "NativeMapConfigRestriction object is missing the required 'latLngBounds.west' property" + ) + } + val north = latLngBoundsObj.getDouble("north") + val south = latLngBoundsObj.getDouble("south") + val east = latLngBoundsObj.getDouble("east") + val west = latLngBoundsObj.getDouble("west") + + latLngBounds = LatLngBounds( + LatLng(south, west), + LatLng(north, east) + ) + } +} diff --git a/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapErrors.kt b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapErrors.kt new file mode 100644 index 0000000..e08121a --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapErrors.kt @@ -0,0 +1,114 @@ +package com.capgo.plugins.nativemap + +import org.json.JSONObject +import kotlin.Exception + +enum class NativeMapErrors { + UNHANDLED_ERROR, INVALID_MAP_ID, MAP_NOT_FOUND, MARKER_NOT_FOUND, INVALID_ARGUMENTS, PERMISSIONS_DENIED_LOCATION, GOOGLE_MAP_NOT_AVAILABLE, BOUNDS_NOT_FOUND, TILE_OVERLAY_NOT_FOUND +} + +class NativeMapErrorObject(val code: String, val message: String, val extra: HashMap = HashMap()) { + private fun asJSONObject(): JSONObject { + val returnJSONObject = JSONObject() + + returnJSONObject.put("code", code) + returnJSONObject.put("message", message) + returnJSONObject.put("extra", extra) + + return returnJSONObject + } + + override fun toString(): String { + return this.asJSONObject().toString() + } +} + +fun getErrorObject(err: NativeMapsError): NativeMapErrorObject { + return when(err) { + is InvalidArgumentsError -> { + NativeMapErrorObject(err.getErrorCode(), "Invalid Arguments Provided: ${err.message}.") + } + is InvalidMapIdError -> { + NativeMapErrorObject(err.getErrorCode(), "Missing or invalid map id.") + } + is MapNotFoundError -> { + NativeMapErrorObject(err.getErrorCode(), "Map not found for provided id.") + } + is MarkerNotFoundError -> { + NativeMapErrorObject(err.getErrorCode(), "Marker not found for provided id.") + } + is PermissionDeniedLocation -> { + NativeMapErrorObject(err.getErrorCode(), "Permissions denied for accessing device location.") + } + is NativeMapNotAvailable -> { + NativeMapErrorObject(err.getErrorCode(), "Google Map is not available.") + } + is BoundsNotFoundError -> { + NativeMapErrorObject(err.getErrorCode(), "Google Map Bounds could not be found.") + } + is TileOverlayNotFoundError -> { + NativeMapErrorObject(err.getErrorCode(), "Tile overlay not found for provided id.") + } + else -> { + NativeMapErrorObject(err.getErrorCode(), "Unhandled Error: ${err.message}.") + } + } +} + +fun getErrorObject(err: Exception): NativeMapErrorObject { + return NativeMapErrorObject(NativeMapErrors.UNHANDLED_ERROR.name, "Unhandled Error: ${err.message}.") +} + +open class NativeMapsError(message: String? = ""): Throwable(message) { + open fun getErrorCode(): String { + return NativeMapErrors.UNHANDLED_ERROR.name + } +} + +class InvalidMapIdError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.INVALID_MAP_ID.name + } +} + +class MapNotFoundError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.MAP_NOT_FOUND.name + } +} + +class MarkerNotFoundError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.MARKER_NOT_FOUND.name + } +} + +class TileOverlayNotFoundError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.TILE_OVERLAY_NOT_FOUND.name + } +} + +class InvalidArgumentsError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.INVALID_ARGUMENTS.name + } +} + +class PermissionDeniedLocation(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.PERMISSIONS_DENIED_LOCATION.name + } +} + +class NativeMapNotAvailable(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.GOOGLE_MAP_NOT_AVAILABLE.name + } +} + +class BoundsNotFoundError(message: String? = ""): NativeMapsError(message) { + override fun getErrorCode(): String { + return NativeMapErrors.BOUNDS_NOT_FOUND.name + } +} diff --git a/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapPadding.kt b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapPadding.kt new file mode 100644 index 0000000..35d25ee --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/GoogleMapPadding.kt @@ -0,0 +1,28 @@ +package com.capgo.plugins.nativemap + +import org.json.JSONObject + +class NativeMapPadding(fromJSONObject: JSONObject) { + var top: Int = 0 + var bottom: Int = 0 + var left: Int = 0 + var right: Int = 0 + + init { + if(fromJSONObject.has("top")) { + top = fromJSONObject.getInt("top") + } + + if(fromJSONObject.has("bottom")) { + bottom = fromJSONObject.getInt("bottom") + } + + if(fromJSONObject.has("left")) { + left = fromJSONObject.getInt("left") + } + + if(fromJSONObject.has("right")) { + right = fromJSONObject.getInt("right") + } + } +} \ No newline at end of file diff --git a/android/src/main/java/com/capgo/plugins/nativemap/ToBackCompositorHelper.kt b/android/src/main/java/com/capgo/plugins/nativemap/ToBackCompositorHelper.kt new file mode 100644 index 0000000..fd2af1e --- /dev/null +++ b/android/src/main/java/com/capgo/plugins/nativemap/ToBackCompositorHelper.kt @@ -0,0 +1,62 @@ +package com.capgo.plugins.nativemap + +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.view.View +import android.webkit.WebView + +/** + * Policy for Android `toBack` compositing: native map behind the Capacitor WebView. + */ +object ToBackCompositorHelper { + private val acquiredMapIds: MutableSet = mutableSetOf() + private var savedParentBackgroundColor: Int? = null + + fun shouldTransparentizeWebViewParent(): Boolean = true + + fun resolveWebViewBackgroundColor(): Int = Color.TRANSPARENT + + fun resolveWebViewAlpha(originalAlpha: Float): Float = originalAlpha + + fun shouldUseHardwareLayerOnPreviewContainer(toBack: Boolean): Boolean = !toBack + + fun resolveWebViewLayerType(): Int = View.LAYER_TYPE_HARDWARE + + fun onToBackMapCreated(webView: WebView, mapId: String) { + if (!acquiredMapIds.add(mapId)) { + return + } + if (acquiredMapIds.size != 1) { + return + } + val parent = webView.parent as? View + savedParentBackgroundColor = + (parent?.background as? ColorDrawable)?.color ?: parent?.solidBackgroundColor() + } + + fun onToBackMapDestroyed(webView: WebView, mapId: String) { + if (!acquiredMapIds.remove(mapId)) { + return + } + if (acquiredMapIds.isNotEmpty()) { + return + } + val parent = webView.parent as? View + val restore = savedParentBackgroundColor + savedParentBackgroundColor = null + if (restore != null) { + parent?.setBackgroundColor(restore) + } else { + parent?.background = null + } + } + + private fun View.solidBackgroundColor(): Int? { + val drawable = background + return if (drawable is ColorDrawable) { + drawable.color + } else { + null + } + } +} diff --git a/android/src/main/res/.gitkeep b/android/src/main/res/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/assets/github-social-preview.png b/assets/github-social-preview.png new file mode 100644 index 0000000..61070ef Binary files /dev/null and b/assets/github-social-preview.png differ diff --git a/assets/github-social-template.svg b/assets/github-social-template.svg new file mode 100644 index 0000000..e0b6a1f --- /dev/null +++ b/assets/github-social-template.svg @@ -0,0 +1,70 @@ + + @capgo/capacitor-native-map + + + + + + + + + + + + + + + + + + + + + + + Native maps for Capacitor + with HTML on top + + + + One API for MapKit, Google Maps, and web. Native map behind your transparent UI. + + + + + + + + + + + + + + + + + Capgo + + + $ + + npm i @capgo/capacitor-native-map + + + + + + + iOS + + + + + + Android + + + + + + Web + + diff --git a/assets/readme-demo.webp b/assets/readme-demo.webp new file mode 100644 index 0000000..8b3241c Binary files /dev/null and b/assets/readme-demo.webp differ diff --git a/assets/readme-screenshot-android.webp b/assets/readme-screenshot-android.webp new file mode 100644 index 0000000..d1e96b3 Binary files /dev/null and b/assets/readme-screenshot-android.webp differ diff --git a/assets/readme-screenshot-ios.webp b/assets/readme-screenshot-ios.webp new file mode 100644 index 0000000..b693b43 Binary files /dev/null and b/assets/readme-screenshot-ios.webp differ diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1d29921 --- /dev/null +++ b/bun.lock @@ -0,0 +1,888 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "@capgo/capacitor-native-map", + "dependencies": { + "@googlemaps/js-api-loader": "^2.0.2", + "@googlemaps/markerclusterer": "^2.6.2", + "@types/google.maps": "^3.58.1", + }, + "devDependencies": { + "@capacitor/android": "^8.0.0", + "@capacitor/cli": "^8.0.0", + "@capacitor/core": "^8.0.0", + "@capacitor/docgen": "^0.3.1", + "@capacitor/ios": "^8.0.0", + "@happy-dom/global-registrator": "^20.14.5", + "@ionic/eslint-config": "^0.4.0", + "@ionic/prettier-config": "^4.0.0", + "@ionic/swiftlint-config": "^2.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@types/node": "^24.10.1", + "@types/resize-observer-browser": "^0.1.11", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "happy-dom": "^20.14.5", + "prettier": "^3.6.2", + "prettier-plugin-java": "^2.7.7", + "prettier-pretty-check": "^0.2.0", + "rimraf": "^6.1.0", + "rollup": "^4.53.2", + "swiftlint": "^2.0.0", + "typescript": "^6.0.3", + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@capacitor/android": ["@capacitor/android@8.5.2", "", { "peerDependencies": { "@capacitor/core": "^8.5.0" } }, "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ=="], + + "@capacitor/cli": ["@capacitor/cli@8.5.2", "", { "dependencies": { "@ionic/cli-framework-output": "^2.2.8", "@ionic/utils-subprocess": "^3.0.1", "@ionic/utils-terminal": "^2.3.5", "commander": "^12.1.0", "debug": "^4.4.0", "env-paths": "^2.2.0", "fs-extra": "^11.2.0", "kleur": "^4.1.5", "native-run": "^2.0.3", "open": "^8.4.0", "plist": "^3.1.0", "prompts": "^2.4.2", "rimraf": "^6.0.1", "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", "xcode": "^3.0.1", "xml2js": "^0.6.2" }, "bin": { "cap": "bin/capacitor", "capacitor": "bin/capacitor" } }, "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ=="], + + "@capacitor/core": ["@capacitor/core@8.5.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w=="], + + "@capacitor/docgen": ["@capacitor/docgen@0.3.1", "", { "dependencies": { "colorette": "^2.0.20", "github-slugger": "^1.5.0", "minimist": "^1.2.8", "typescript": "~4.2.4" }, "bin": { "docgen": "bin/docgen" } }, "sha512-WBdAUquQC4hiZOVk1YbY5Rs3vlDrNdCGt3KGnMa2dc2UBa91Q7PQ/jLW/oZyfVIfiXWu7t1KUhhM3MyiZepUrQ=="], + + "@capacitor/ios": ["@capacitor/ios@8.5.2", "", { "peerDependencies": { "@capacitor/core": "^8.5.0" } }, "sha512-eOT2S67AfTFG7KN0T3oxwAajrJz4rSye+ODfvChiwrk0F/+kWufFM1qU6cDjGJUOfwdAoeuEpZyWHSwZlu4GFg=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@googlemaps/js-api-loader": ["@googlemaps/js-api-loader@2.1.3", "", { "dependencies": { "@types/google.maps": "^3.65.3" } }, "sha512-1t6umwRufEGzIPjHPjPeuuY1dCal0zU2bZeomkf4OA0MprFdlKCnXY3zeFkzDaCvKjwbFubeapm61P5+EbrAoA=="], + + "@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.6.2", "", { "dependencies": { "@types/supercluster": "^7.1.3", "fast-equals": "^5.2.2", "supercluster": "^8.0.1" } }, "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ=="], + + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.14.5", "", { "dependencies": { "@types/node": ">=20.0.0", "happy-dom": "^20.14.5" } }, "sha512-B05ID9DhSwLs6mlm1fzlkAtTIvB3duCvjJjfr19LBrlTK7VZtRjDqoTRIVv13GuYuNdhByu8LSZgThwV3Rkj7g=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@ionic/cli-framework-output": ["@ionic/cli-framework-output@2.2.8", "", { "dependencies": { "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g=="], + + "@ionic/eslint-config": ["@ionic/eslint-config@0.4.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.0" }, "peerDependencies": { "eslint": ">=7" } }, "sha512-L8OXY29D3iGqNtteFj0iz3eoZIVgokBiVjCO8WMssNZa4GTHjYsase0rC9ASXGefMnLJu6rbNl3Gbx7NNxJRZQ=="], + + "@ionic/prettier-config": ["@ionic/prettier-config@4.0.0", "", { "peerDependencies": { "prettier": "^2.4.0 || ^3.0.0" } }, "sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag=="], + + "@ionic/swiftlint-config": ["@ionic/swiftlint-config@2.0.0", "", {}, "sha512-TXy76ALSKhUZzBziHz7aoEtSQwHofBIDRNzM9x4sndtC7fefbZsBw3UgGwXFTOc7hoj72EAGyqZNUhj9LlhaNQ=="], + + "@ionic/utils-array": ["@ionic/utils-array@2.1.6", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg=="], + + "@ionic/utils-fs": ["@ionic/utils-fs@3.1.7", "", { "dependencies": { "@types/fs-extra": "^8.0.0", "debug": "^4.0.0", "fs-extra": "^9.0.0", "tslib": "^2.0.1" } }, "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA=="], + + "@ionic/utils-object": ["@ionic/utils-object@2.1.6", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww=="], + + "@ionic/utils-process": ["@ionic/utils-process@2.1.12", "", { "dependencies": { "@ionic/utils-object": "2.1.6", "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "tslib": "^2.0.1" } }, "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg=="], + + "@ionic/utils-stream": ["@ionic/utils-stream@3.1.7", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w=="], + + "@ionic/utils-subprocess": ["@ionic/utils-subprocess@3.0.1", "", { "dependencies": { "@ionic/utils-array": "2.1.6", "@ionic/utils-fs": "3.1.7", "@ionic/utils-process": "2.1.12", "@ionic/utils-stream": "3.1.7", "@ionic/utils-terminal": "2.3.5", "cross-spawn": "^7.0.3", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A=="], + + "@ionic/utils-terminal": ["@ionic/utils-terminal@2.3.5", "", { "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", "signal-exit": "^3.0.3", "slice-ansi": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "tslib": "^2.0.1", "untildify": "^4.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="], + + "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.5", "", { "os": "android", "cpu": "arm" }, "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.5", "", { "os": "android", "cpu": "arm64" }, "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.5", "", { "os": "none", "cpu": "arm64" }, "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/fs-extra": ["@types/fs-extra@8.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/google.maps": ["@types/google.maps@3.66.4", "", {}, "sha512-t01LM0VeIsUOZWno7ABUv4XL82RalnhRGaTzWykPUtQ+hHT39vsIekeU+roqEaibwebpYIjS7dslx51HwG95nw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@24.19.0", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-zY+5tKxXdhGh1PYI0ac+7juvEu4OI6vWtVVoj5i2m42jxAY1U+zHGt6QCyOFwykdP62sM3MJ9stoYYUw5aCWew=="], + + "@types/resize-observer-browser": ["@types/resize-observer-browser@0.1.11", "", {}, "sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ=="], + + "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="], + + "@types/semver": ["@types/semver@7.8.0", "", {}, "sha512-1mAINjtQCXXeLkJ9ehXkwOcBpqtLxiVtKhpUf83DdRNdQKV0iXZpaHYqRr7nj+wvxuJzoAmAwXI+sCNMv1CzLQ=="], + + "@types/slice-ansi": ["@types/slice-ansi@4.0.0", "", {}, "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ=="], + + "@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="], + + "@types/whatwg-mimetype": ["@types/whatwg-mimetype@3.0.2", "", {}, "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA=="], + + "@types/ws": ["@types/ws@8.18.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.62.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*", "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "typescript": "*" }, "optionalPeers": ["typescript"] }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.4.0", "", {}, "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.9.12", "", {}, "sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A=="], + + "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "es-shim-unscopables": "^1.1.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-VXY5eFRarnXcYxwBjJzPmEhH55+rmP79/+ueDhi0F+TuqfHCItagIHqxeUZrmgrOPa31QTh9H85DjX3FfJ0FTg=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], + + "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], + + "brace-expansion": ["brace-expansion@1.1.21", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "buffer-image-size": ["buffer-image-size@0.6.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEh+kZOPY1w+gcCMobZ6ETUp9WfibndnosbpwB1iJk/8Gt5ZF2bhS6+B6bPYz424KtwsR6Rflc3tCz1/ghX2dQ=="], + + "call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cosmiconfig": ["cosmiconfig@9.0.2", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "elementtree": ["elementtree@0.1.7", "", { "dependencies": { "sax": "1.1.4" } }, "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-abstract-get": ["es-abstract-get@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "es-object-atoms": "^1.1.2", "is-callable": "^1.2.7", "object-inspect": "^1.13.4" } }, "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.4", "", { "dependencies": { "es-abstract-get": "^1.0.0", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "is-callable": "^1.2.7", "is-date-object": "^1.1.0", "is-symbol": "^1.1.1" } }, "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-prettier": ["eslint-config-prettier@8.10.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.14.0", "", { "dependencies": { "debug": "^3.2.7" }, "peerDependencies": { "eslint": "*" }, "optionalPeers": ["eslint"] }, "sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.4.3", "", {}, "sha512-8unmvA0qrfpFWBYUate9BTGtQdKHWBYt+KdAIkag7Vvxe7wz8Q2nJg7fLMC0bJqe0bjP31kca7Ckaawrs4Istg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.3", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.4.4", "", {}, "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fs-extra": ["fs-extra@11.4.1", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "github-slugger": ["github-slugger@1.5.0", "", {}, "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "happy-dom": ["happy-dom@20.14.5", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-x/RzkpWO40bTjIoT30iQtt64FLLmH/iRcUCN2X//bLx7H3ifkdfPXyqsro/OYtqzIAhiLMMA7mmiOR9C3NOKjQ=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.17.0", "", { "dependencies": { "hasown": "^2.0.4" } }, "sha512-J/vG0zBCbIKOQFfufSwyXdMrsohyJIUNkrnmo6WZGzoM7tr/lsbfW5b2BvisL6zsyMzK9UxV9L6c7AoFbyXHOA=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-document.all": ["is-document.all@1.0.0", "", { "dependencies": { "call-bound": "^1.0.4" } }, "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.3.2", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], + + "kdbush": ["kdbush@4.1.0", "", {}, "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "native-run": ["native-run@2.0.3", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-terminal": "^2.3.4", "bplist-parser": "^0.3.2", "debug": "^4.3.4", "elementtree": "^0.1.7", "ini": "^4.1.1", "plist": "^3.1.0", "split2": "^4.2.0", "through2": "^4.0.2", "tslib": "^2.6.2", "yauzl": "^2.10.0" }, "bin": { "native-run": "bin/native-run" } }, "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "natural-compare-lite": ["natural-compare-lite@1.4.1", "", {}, "sha512-8Mz06azhPqiDlxhUaUTjBhNAbib/1MoPr89kgmuV6Ir8ICT2/Q/XSYg6VfL3fq2x8MpSQX8sqyI/NeZ5GsGyXA=="], + + "node-exports-info": ["node-exports-info@1.6.2", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.2", "", { "dependencies": { "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "plist": ["plist@3.1.1", "", { "dependencies": { "@xmldom/xmldom": "^0.9.10", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-ZIfcLJC+7E7FBFnDxm9MPmt7D+DidyQ26lewieO75AdhA2ayMtsJSES0iWzqJQbcVRSrTufQoy0DR94xHue0oA=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.9.9", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-Z/CJHIkdujO/OtN7nXUii0Rf3VT5SRuhjBA82Xvu2XhBUgX3nhP67T0LHceBdQLex7OOFGTox+Q5Yg8Jk2Qivg=="], + + "prettier-plugin-java": ["prettier-plugin-java@2.11.0", "", { "dependencies": { "web-tree-sitter": "0.27.0" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-pLwm2wHvVvW5eVVLf1cq2orY2ATaIj5ngN8hJ5ZdXYzUHzxrfaZkk2xmu8Mz/duBXAc2xT/cUH5+dK414HQSTw=="], + + "prettier-pretty-check": ["prettier-pretty-check@0.2.0", "", { "dependencies": { "diff": "^5.2.0" }, "bin": { "prettier-pretty-check": "index.js" } }, "sha512-gVmwxYvm1Qii1H6eULNcNchlruyXyKkluP1LBhUaqdSvvACc8FZ99zTWzqQHJrBu9qUrYmNRysPmnYSm9c7G6Q=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "rollup": ["rollup@4.63.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.5", "@rollup/rollup-android-arm64": "4.63.5", "@rollup/rollup-darwin-arm64": "4.63.5", "@rollup/rollup-darwin-x64": "4.63.5", "@rollup/rollup-freebsd-arm64": "4.63.5", "@rollup/rollup-freebsd-x64": "4.63.5", "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", "@rollup/rollup-linux-arm-musleabihf": "4.63.5", "@rollup/rollup-linux-arm64-gnu": "4.63.5", "@rollup/rollup-linux-arm64-musl": "4.63.5", "@rollup/rollup-linux-loong64-gnu": "4.63.5", "@rollup/rollup-linux-loong64-musl": "4.63.5", "@rollup/rollup-linux-ppc64-gnu": "4.63.5", "@rollup/rollup-linux-ppc64-musl": "4.63.5", "@rollup/rollup-linux-riscv64-gnu": "4.63.5", "@rollup/rollup-linux-riscv64-musl": "4.63.5", "@rollup/rollup-linux-s390x-gnu": "4.63.5", "@rollup/rollup-linux-x64-gnu": "4.63.5", "@rollup/rollup-linux-x64-musl": "4.63.5", "@rollup/rollup-openbsd-x64": "4.63.5", "@rollup/rollup-openharmony-arm64": "4.63.5", "@rollup/rollup-win32-arm64-msvc": "4.63.5", "@rollup/rollup-win32-ia32-msvc": "4.63.5", "@rollup/rollup-win32-x64-gnu": "4.63.5", "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "sax": ["sax@1.6.1", "", {}, "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.11", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-object-atoms": "^1.1.2", "has-property-descriptors": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.10", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.2" } }, "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "swiftlint": ["swiftlint@2.0.0", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-subprocess": "^3.0.1", "cosmiconfig": "^9.0.0" }, "bin": { "node-swiftlint": "bin.js" } }, "sha512-MMVuyZ4/6WcIJlk0z6GM0pZjRuwnyUJqRPbJBFW3oACN/qjAvRbolCWEu+zE2MycF/cEgqfUpI+oLECNfjfOJA=="], + + "tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.5", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.10" } }, "sha512-0FHJvLPqZ7KJzp17O13jfsAjsqazgrxBu2zEK95PmUz8lv2+GjRuxUInCr2Rk9Dms3ihN21zJ929ZO43yJ95QQ=="], + + "typed-array-length": ["typed-array-length@1.0.8", "", { "dependencies": { "call-bind": "^1.0.9", "for-each": "^0.3.5", "gopd": "^1.2.0", "is-typed-array": "^1.1.15", "possible-typed-array-names": "^1.1.0", "reflect.getprototypeof": "^1.0.10" } }, "sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + + "web-tree-sitter": ["web-tree-sitter@0.27.0", "", {}, "sha512-XK08gj6RwTMQatAG7uVRP8MunqotL/XC19vHgkSPKmELgbGPBj4ECvB8haHOUnyj6ls2B8t42UTro14zxGgAHg=="], + + "whatwg-mimetype": ["whatwg-mimetype@3.0.0", "", {}, "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.24", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.9", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-wk4Mf4pR5mRP7eYuuTBCIQ9d0ud2Fv2jRLQpfgnRjbOxAFHmjKFValgTpitVKzJJS8ajnYQV2Du1SZ8j6b/EUQ=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], + + "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@capacitor/docgen/typescript": ["typescript@4.2.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg=="], + + "@happy-dom/global-registrator/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "@ionic/utils-fs/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@types/fs-extra/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "@types/ws/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "buffer-image-size/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "elementtree/sax": ["sax@1.1.4", "", {}, "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-import-resolver-node/resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "glob/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], + + "happy-dom/@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "micromatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "node-exports-info/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], + + "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "@types/fs-extra/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "@types/ws/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "buffer-image-size/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "flat-cache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], + + "happy-dom/@types/node/undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..2216136 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,2 @@ +[test] +preload = ["./src/test-setup.ts"] diff --git a/example-app/README.md b/example-app/README.md new file mode 100644 index 0000000..41680a9 --- /dev/null +++ b/example-app/README.md @@ -0,0 +1,18 @@ +# Native Map Example + +This Vite + React app demonstrates `@capgo/capacitor-native-map`: create a map, add a marker, and move the camera. + +## API keys + +- **Web / Android (Google Maps):** set a valid Google Maps API key when calling `NativeMap.create({ apiKey, ... })`. +- **Android:** also add your key to `AndroidManifest.xml` as `com.google.android.geo.API_KEY` (see the example manifest). +- **iOS:** uses Apple MapKit; no Google API key is required. + +## Run locally + +```bash +bun install +bun run build +bunx cap sync +bunx cap open android # or ios +``` diff --git a/example-app/android/.gitignore b/example-app/android/.gitignore new file mode 100644 index 0000000..48354a3 --- /dev/null +++ b/example-app/android/.gitignore @@ -0,0 +1,101 @@ +# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore + +# Built application files +*.apk +*.aar +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +# Cordova plugins for Capacitor +capacitor-cordova-android-plugins + +# Copied web assets +app/src/main/assets/public + +# Generated Config files +app/src/main/assets/capacitor.config.json +app/src/main/assets/capacitor.plugins.json +app/src/main/res/xml/config.xml diff --git a/example-app/android/app/.gitignore b/example-app/android/app/.gitignore new file mode 100644 index 0000000..043df80 --- /dev/null +++ b/example-app/android/app/.gitignore @@ -0,0 +1,2 @@ +/build/* +!/build/.npmkeep diff --git a/example-app/android/app/build.gradle b/example-app/android/app/build.gradle new file mode 100644 index 0000000..33fa869 --- /dev/null +++ b/example-app/android/app/build.gradle @@ -0,0 +1,57 @@ +apply plugin: 'com.android.application' + +android { + namespace = "app.capgo.nativemap" + compileSdk = rootProject.ext.compileSdkVersion + defaultConfig { + applicationId = "app.capgo.nativemap" + minSdkVersion = rootProject.ext.minSdkVersion + targetSdkVersion = rootProject.ext.targetSdkVersion + versionCode = 1 + versionName = "1.0" + manifestPlaceholders = [ + GOOGLE_MAPS_API_KEY: System.getenv("GOOGLE_MAPS_API_KEY") ?: "REPLACE_WITH_YOUR_GOOGLE_MAPS_API_KEY" + ] + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + aaptOptions { + // Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps. + // Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61 + ignoreAssetsPattern = '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' + } + } + buildTypes { + release { + minifyEnabled = false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +repositories { + flatDir{ + dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs' + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion" + implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion" + implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion" + implementation project(':capacitor-android') + testImplementation "junit:junit:$junitVersion" + androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" + androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" + implementation project(':capacitor-cordova-android-plugins') +} + +apply from: 'capacitor.build.gradle' + +try { + def servicesJSON = file('google-services.json') + if (servicesJSON.text) { + apply plugin: 'com.google.gms.google-services' + } +} catch(Exception e) { + logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work") +} diff --git a/example-app/android/app/capacitor.build.gradle b/example-app/android/app/capacitor.build.gradle new file mode 100644 index 0000000..3b38683 --- /dev/null +++ b/example-app/android/app/capacitor.build.gradle @@ -0,0 +1,19 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN + +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_21 + targetCompatibility JavaVersion.VERSION_21 + } +} + +apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" +dependencies { + implementation project(':capgo-capacitor-native-audio') + +} + + +if (hasProperty('postBuildExtras')) { + postBuildExtras() +} diff --git a/example-app/android/app/proguard-rules.pro b/example-app/android/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/example-app/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java b/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java new file mode 100644 index 0000000..f2c2217 --- /dev/null +++ b/example-app/android/app/src/androidTest/java/com/getcapacitor/myapp/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import android.content.Context; +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + + @Test + public void useAppContext() throws Exception { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("com.getcapacitor.app", appContext.getPackageName()); + } +} diff --git a/example-app/android/app/src/main/AndroidManifest.xml b/example-app/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a2047de --- /dev/null +++ b/example-app/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-app/android/app/src/main/java/app/capgo/nativemap/MainActivity.java b/example-app/android/app/src/main/java/app/capgo/nativemap/MainActivity.java new file mode 100644 index 0000000..0f1905f --- /dev/null +++ b/example-app/android/app/src/main/java/app/capgo/nativemap/MainActivity.java @@ -0,0 +1,5 @@ +package app.capgo.nativemap; + +import com.getcapacitor.BridgeActivity; + +public class MainActivity extends BridgeActivity {} \ No newline at end of file diff --git a/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png new file mode 100644 index 0000000..e31573b Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-hdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-mdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png new file mode 100644 index 0000000..8077255 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png new file mode 100644 index 0000000..14c6c8f Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xxhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png new file mode 100644 index 0000000..244ca25 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-land-xxxhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png new file mode 100644 index 0000000..74faaa5 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-hdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png new file mode 100644 index 0000000..e944f4a Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-mdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png new file mode 100644 index 0000000..564a82f Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png new file mode 100644 index 0000000..bfabe68 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xxhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png b/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png new file mode 100644 index 0000000..6929071 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable-port-xxxhdpi/splash.png differ diff --git a/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..c7bd21d --- /dev/null +++ b/example-app/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml b/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..d5fccc5 --- /dev/null +++ b/example-app/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-app/android/app/src/main/res/drawable/splash.png b/example-app/android/app/src/main/res/drawable/splash.png new file mode 100644 index 0000000..f7a6492 Binary files /dev/null and b/example-app/android/app/src/main/res/drawable/splash.png differ diff --git a/example-app/android/app/src/main/res/layout/activity_main.xml b/example-app/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..b5ad138 --- /dev/null +++ b/example-app/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,12 @@ + + + + + diff --git a/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..036d09b --- /dev/null +++ b/example-app/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..c023e50 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2127973 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..b441f37 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..72905b8 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..8ed0605 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..9502e47 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..4d1e077 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df0f158 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..853db04 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..6cdf97c Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..2960cbb Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..8e3093a Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..46de6e2 Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d2ea9ab Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..a40d73e Binary files /dev/null and b/example-app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/example-app/android/app/src/main/res/values/colors.xml b/example-app/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..8d53adf --- /dev/null +++ b/example-app/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #3880FF + #3171E0 + #5260FF + diff --git a/example-app/android/app/src/main/res/values/ic_launcher_background.xml b/example-app/android/app/src/main/res/values/ic_launcher_background.xml new file mode 100644 index 0000000..c5d5899 --- /dev/null +++ b/example-app/android/app/src/main/res/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + \ No newline at end of file diff --git a/example-app/android/app/src/main/res/values/strings.xml b/example-app/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..fd5e7fd --- /dev/null +++ b/example-app/android/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + Native Audio Example + Native Audio Example + app.capgo.nativemap + app.capgo.nativemap + diff --git a/example-app/android/app/src/main/res/values/styles.xml b/example-app/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..be874e5 --- /dev/null +++ b/example-app/android/app/src/main/res/values/styles.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/example-app/android/app/src/main/res/xml/file_paths.xml b/example-app/android/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..bd0c4d8 --- /dev/null +++ b/example-app/android/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java b/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java new file mode 100644 index 0000000..0297327 --- /dev/null +++ b/example-app/android/app/src/test/java/com/getcapacitor/myapp/ExampleUnitTest.java @@ -0,0 +1,18 @@ +package com.getcapacitor.myapp; + +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + + @Test + public void addition_isCorrect() throws Exception { + assertEquals(4, 2 + 2); + } +} diff --git a/example-app/android/build.gradle b/example-app/android/build.gradle new file mode 100644 index 0000000..f8f0e43 --- /dev/null +++ b/example-app/android/build.gradle @@ -0,0 +1,29 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.google.gms:google-services:4.4.4' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +apply from: "variables.gradle" + +allprojects { + repositories { + google() + mavenCentral() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/example-app/android/capacitor.settings.gradle b/example-app/android/capacitor.settings.gradle new file mode 100644 index 0000000..f5a0d1d --- /dev/null +++ b/example-app/android/capacitor.settings.gradle @@ -0,0 +1,6 @@ +// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN +include ':capacitor-android' +project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor') + +include ':capgo-capacitor-native-audio' +project(':capgo-capacitor-native-audio').projectDir = new File('../../android') diff --git a/example-app/android/gradle.properties b/example-app/android/gradle.properties new file mode 100644 index 0000000..2e87c52 --- /dev/null +++ b/example-app/android/gradle.properties @@ -0,0 +1,22 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/example-app/android/gradle/wrapper/gradle-wrapper.jar b/example-app/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/example-app/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/example-app/android/gradle/wrapper/gradle-wrapper.properties b/example-app/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..70fe02a --- /dev/null +++ b/example-app/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.5-all.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/example-app/android/gradlew b/example-app/android/gradlew new file mode 100755 index 0000000..23d15a9 --- /dev/null +++ b/example-app/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright ยฉ 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions ยซ$varยป, ยซ${var}ยป, ยซ${var:-default}ยป, ยซ${var+SET}ยป, +# ยซ${var#prefix}ยป, ยซ${var%suffix}ยป, and ยซ$( cmd )ยป; +# * compound commands having a testable exit status, especially ยซcaseยป; +# * various built-in commands including ยซcommandยป, ยซsetยป, and ยซulimitยป. +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/example-app/android/gradlew.bat b/example-app/android/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/example-app/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/example-app/android/settings.gradle b/example-app/android/settings.gradle new file mode 100644 index 0000000..3b4431d --- /dev/null +++ b/example-app/android/settings.gradle @@ -0,0 +1,5 @@ +include ':app' +include ':capacitor-cordova-android-plugins' +project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/') + +apply from: 'capacitor.settings.gradle' \ No newline at end of file diff --git a/example-app/android/variables.gradle b/example-app/android/variables.gradle new file mode 100644 index 0000000..89f7fe8 --- /dev/null +++ b/example-app/android/variables.gradle @@ -0,0 +1,16 @@ +ext { + minSdkVersion = 24 + compileSdkVersion = 36 + targetSdkVersion = 36 + androidxActivityVersion = '1.11.0' + androidxAppCompatVersion = '1.8.0' + androidxCoordinatorLayoutVersion = '1.3.0' + androidxCoreVersion = '1.17.0' + androidxFragmentVersion = '1.8.9' + coreSplashScreenVersion = '1.2.0' + androidxWebkitVersion = '1.14.0' + junitVersion = '4.13.2' + androidxJunitVersion = '1.3.0' + androidxEspressoCoreVersion = '3.7.0' + cordovaAndroidVersion = '14.0.1' +} diff --git a/example-app/bun.lock b/example-app/bun.lock new file mode 100644 index 0000000..21e9c7b --- /dev/null +++ b/example-app/bun.lock @@ -0,0 +1,924 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "native-audio-example", + "dependencies": { + "@capacitor/core": "^8.5.2", + "@capgo/capacitor-native-map": "..", + "react": "^18.3.1", + "react-dom": "^18.3.1", + }, + "devDependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/cli": "^8.5.2", + "@capacitor/ios": "^8.5.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^6.0.0", + "typescript": "^6.0.3", + "vite": "^8.3.0", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@capacitor/android": ["@capacitor/android@8.5.2", "", { "peerDependencies": { "@capacitor/core": "^8.5.0" } }, "sha512-rtiaz78egCcS+vs29wkWCL3C+RhazKMSygXcjutwWP8NmO6r5UBxOvGf3Wjz5+ahobyQ723tn9gCePwRwcNDZQ=="], + + "@capacitor/cli": ["@capacitor/cli@8.5.2", "", { "dependencies": { "@ionic/cli-framework-output": "^2.2.8", "@ionic/utils-subprocess": "^3.0.1", "@ionic/utils-terminal": "^2.3.5", "commander": "^12.1.0", "debug": "^4.4.0", "env-paths": "^2.2.0", "fs-extra": "^11.2.0", "kleur": "^4.1.5", "native-run": "^2.0.3", "open": "^8.4.0", "plist": "^3.1.0", "prompts": "^2.4.2", "rimraf": "^6.0.1", "semver": "^7.6.3", "tar": "^7.5.3", "tslib": "^2.8.1", "xcode": "^3.0.1", "xml2js": "^0.6.2" }, "bin": { "cap": "bin/capacitor", "capacitor": "bin/capacitor" } }, "sha512-O2kpA5ZPJUAmliDCcmpMf8xnJwMezxlFPUrlyOAjbL3U9MFNY3l4ZpPI/UXwvyPmR8ETcVF9pFr+VdWq61rqTQ=="], + + "@capacitor/core": ["@capacitor/core@8.5.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-agnQbGdhGw/KZFuLf7H9SY2o+oE7upkr/WPQsYaokXP6f9chaE8iVmbKkfN6SVl4w3pmz7oZ3Ur1hbkmy1hx+w=="], + + "@capacitor/docgen": ["@capacitor/docgen@0.3.1", "", { "dependencies": { "colorette": "^2.0.20", "github-slugger": "^1.5.0", "minimist": "^1.2.8", "typescript": "~4.2.4" }, "bin": { "docgen": "bin/docgen" } }, "sha512-WBdAUquQC4hiZOVk1YbY5Rs3vlDrNdCGt3KGnMa2dc2UBa91Q7PQ/jLW/oZyfVIfiXWu7t1KUhhM3MyiZepUrQ=="], + + "@capacitor/ios": ["@capacitor/ios@8.5.2", "", { "peerDependencies": { "@capacitor/core": "^8.5.0" } }, "sha512-eOT2S67AfTFG7KN0T3oxwAajrJz4rSye+ODfvChiwrk0F/+kWufFM1qU6cDjGJUOfwdAoeuEpZyWHSwZlu4GFg=="], + + "@capgo/capacitor-native-map": ["@capgo/capacitor-native-map@file:..", { "dependencies": { "@googlemaps/js-api-loader": "^2.0.2", "@googlemaps/markerclusterer": "^2.6.2", "@types/google.maps": "^3.58.1" }, "devDependencies": { "@capacitor/android": "^8.0.0", "@capacitor/cli": "^8.0.0", "@capacitor/core": "^8.0.0", "@capacitor/docgen": "^0.3.1", "@capacitor/ios": "^8.0.0", "@ionic/eslint-config": "^0.4.0", "@ionic/prettier-config": "^4.0.0", "@ionic/swiftlint-config": "^2.0.0", "@types/node": "^24.10.1", "@types/resize-observer-browser": "^0.1.11", "eslint": "^8.57.1", "eslint-plugin-import": "^2.31.0", "prettier": "^3.6.2", "prettier-plugin-java": "^2.7.7", "prettier-pretty-check": "^0.2.0", "rimraf": "^6.1.0", "rollup": "^4.53.2", "swiftlint": "^2.0.0", "typescript": "^5.9.3" }, "peerDependencies": { "@capacitor/core": ">=8.0.0" } }], + + "@chevrotain/cst-dts-gen": ["@chevrotain/cst-dts-gen@11.0.3", "", { "dependencies": { "@chevrotain/gast": "11.0.3", "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ=="], + + "@chevrotain/gast": ["@chevrotain/gast@11.0.3", "", { "dependencies": { "@chevrotain/types": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q=="], + + "@chevrotain/regexp-to-ast": ["@chevrotain/regexp-to-ast@11.0.3", "", {}, "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA=="], + + "@chevrotain/types": ["@chevrotain/types@11.0.3", "", {}, "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ=="], + + "@chevrotain/utils": ["@chevrotain/utils@11.0.3", "", {}, "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@2.1.4", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^9.6.0", "globals": "^13.19.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ=="], + + "@eslint/js": ["@eslint/js@8.57.1", "", {}, "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q=="], + + "@googlemaps/js-api-loader": ["@googlemaps/js-api-loader@2.1.3", "", { "dependencies": { "@types/google.maps": "^3.65.3" } }, "sha512-1t6umwRufEGzIPjHPjPeuuY1dCal0zU2bZeomkf4OA0MprFdlKCnXY3zeFkzDaCvKjwbFubeapm61P5+EbrAoA=="], + + "@googlemaps/markerclusterer": ["@googlemaps/markerclusterer@2.6.2", "", { "dependencies": { "@types/supercluster": "^7.1.3", "fast-equals": "^5.2.2", "supercluster": "^8.0.1" } }, "sha512-U6uVhq8iWhiIckA89sgRu8OK35mjd6/3CuoZKWakKEf0QmRRWpatlsPb3kqXkoWSmbcZkopRiI4dnW6DQSd7bQ=="], + + "@humanwhocodes/config-array": ["@humanwhocodes/config-array@0.13.0", "", { "dependencies": { "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" } }, "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/object-schema": ["@humanwhocodes/object-schema@2.0.3", "", {}, "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA=="], + + "@ionic/cli-framework-output": ["@ionic/cli-framework-output@2.2.8", "", { "dependencies": { "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-TshtaFQsovB4NWRBydbNFawql6yul7d5bMiW1WYYf17hd99V6xdDdk3vtF51bw6sLkxON3bDQpWsnUc9/hVo3g=="], + + "@ionic/eslint-config": ["@ionic/eslint-config@0.4.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "^5.58.0", "@typescript-eslint/parser": "^5.58.0", "eslint-config-prettier": "^8.8.0", "eslint-plugin-import": "^2.27.0" }, "peerDependencies": { "eslint": ">=7" } }, "sha512-L8OXY29D3iGqNtteFj0iz3eoZIVgokBiVjCO8WMssNZa4GTHjYsase0rC9ASXGefMnLJu6rbNl3Gbx7NNxJRZQ=="], + + "@ionic/prettier-config": ["@ionic/prettier-config@4.0.0", "", { "peerDependencies": { "prettier": "^2.4.0 || ^3.0.0" } }, "sha512-0DqL6CggVdgeJAWOLPUT73rF1VD5p0tVlCpC5GXz5vTIUBxNwsJ5085Q7wXjKiE5Odx3aOHGTcuRWCawFsLFag=="], + + "@ionic/swiftlint-config": ["@ionic/swiftlint-config@2.0.0", "", {}, "sha512-TXy76ALSKhUZzBziHz7aoEtSQwHofBIDRNzM9x4sndtC7fefbZsBw3UgGwXFTOc7hoj72EAGyqZNUhj9LlhaNQ=="], + + "@ionic/utils-array": ["@ionic/utils-array@2.1.6", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-0JZ1Zkp3wURnv8oq6Qt7fMPo5MpjbLoUoa9Bu2Q4PJuSDWM8H8gwF3dQO7VTeUj3/0o1IB1wGkFWZZYgUXZMUg=="], + + "@ionic/utils-fs": ["@ionic/utils-fs@3.1.7", "", { "dependencies": { "@types/fs-extra": "^8.0.0", "debug": "^4.0.0", "fs-extra": "^9.0.0", "tslib": "^2.0.1" } }, "sha512-2EknRvMVfhnyhL1VhFkSLa5gOcycK91VnjfrTB0kbqkTFCOXyXgVLI5whzq7SLrgD9t1aqos3lMMQyVzaQ5gVA=="], + + "@ionic/utils-object": ["@ionic/utils-object@2.1.6", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-vCl7sl6JjBHFw99CuAqHljYJpcE88YaH2ZW4ELiC/Zwxl5tiwn4kbdP/gxi2OT3MQb1vOtgAmSNRtusvgxI8ww=="], + + "@ionic/utils-process": ["@ionic/utils-process@2.1.12", "", { "dependencies": { "@ionic/utils-object": "2.1.6", "@ionic/utils-terminal": "2.3.5", "debug": "^4.0.0", "signal-exit": "^3.0.3", "tree-kill": "^1.2.2", "tslib": "^2.0.1" } }, "sha512-Jqkgyq7zBs/v/J3YvKtQQiIcxfJyplPgECMWgdO0E1fKrrH8EF0QGHNJ9mJCn6PYe2UtHNS8JJf5G21e09DfYg=="], + + "@ionic/utils-stream": ["@ionic/utils-stream@3.1.7", "", { "dependencies": { "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-eSELBE7NWNFIHTbTC2jiMvh1ABKGIpGdUIvARsNPMNQhxJB3wpwdiVnoBoTYp+5a6UUIww4Kpg7v6S7iTctH1w=="], + + "@ionic/utils-subprocess": ["@ionic/utils-subprocess@3.0.1", "", { "dependencies": { "@ionic/utils-array": "2.1.6", "@ionic/utils-fs": "3.1.7", "@ionic/utils-process": "2.1.12", "@ionic/utils-stream": "3.1.7", "@ionic/utils-terminal": "2.3.5", "cross-spawn": "^7.0.3", "debug": "^4.0.0", "tslib": "^2.0.1" } }, "sha512-cT4te3AQQPeIM9WCwIg8ohroJ8TjsYaMb2G4ZEgv9YzeDqHZ4JpeIKqG2SoaA3GmVQ3sOfhPM6Ox9sxphV/d1A=="], + + "@ionic/utils-terminal": ["@ionic/utils-terminal@2.3.5", "", { "dependencies": { "@types/slice-ansi": "^4.0.0", "debug": "^4.0.0", "signal-exit": "^3.0.3", "slice-ansi": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "tslib": "^2.0.1", "untildify": "^4.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-3cKScz9Jx2/Pr9ijj1OzGlBDfcmx7OMVBt4+P1uRR0SSW4cm1/y3Mo4OY3lfkuaYifMNBW8Wz6lQHbs1bihr7A=="], + + "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxc-project/types": ["@oxc-project/types@0.151.0", "", {}, "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA=="], + + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.10", "", { "os": "android", "cpu": "arm" }, "sha512-bp9svZb+QurZeh+8H4BhrZkifEB0YBNvTVzNSJnJQkj4NrRwmQoDUCGP0vSN7PbvLeM7l1tK6GXL8mrTiH2myg=="], + + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.10", "", { "os": "android", "cpu": "arm64" }, "sha512-wm6Dld3RXUAZ/gRWKyUy+4W1B5CB5UeFaOzsSWJWEdxZXHH8rCYiZ5dGe6oJmhsunAPWzL7FZV+VtvmN5Ye2eA=="], + + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-UbEfXq/AqGNgRTV3ik+X/iR6mUxu2QdYAadwRxJWquUGnW6gDqdP1FtLtFXRow7RJx0ssRwi80XAPr4r+4DtsA=="], + + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-7f5h17q5KZVx/ji1vb8OTq31ch1O2I7K8NPIr44GkyWTApXMIsmhWqZfgpOH10xeauqghDAvGlZktasCkcF6Eg=="], + + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.10", "", { "os": "freebsd", "cpu": "x64" }, "sha512-ynOk/eEYhC6ZB2xCGvKrEOwE58oBy9LnrAqtkrDF9Fz1VTaNdGZTsV0VarJdhPwb+sOJTGjCLwcuyRJZ1dnMcQ=="], + + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.10", "", { "os": "linux", "cpu": "arm" }, "sha512-ERrAs185meZZhGan7a4l3RiiJK1ArSDlHdST++uvSxe+FDbR4TwUPahT/cbZJvaG6fIpDpF78surN+tX708Y4Q=="], + + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-KN7OHKD0J3jy1UzBwZWPxpwhODf9IARUIJcrH+yLYKOcmegZ8luEUM38lDP1bDVj40yP6PsSzCqOJF76vljFnQ=="], + + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-8l9wP8O+wa8zD6iw6egSfzVtu7oZVfH3hlUsMM4MwbLMhxleqeoXbZzjddyK3YyNlwLhqznq3tF7PkNJ8T/V2w=="], + + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.10", "", { "os": "linux", "cpu": "ppc64" }, "sha512-SeXNKeQzA5kLhz/J0CH6ZP0/HJ3v1xm/0YbiYpE0kK7emfRC2OIGGIaE14xzkISEGv2aYuUSpiLiU5Gbq+OI0A=="], + + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.10", "", { "os": "linux", "cpu": "s390x" }, "sha512-mtht0nR+y8/hart4175Ll15w7lY8dg7CtQ+j2FDNTsDRspOWTK/2V3l0aj9sIj7XmvqxT8Yli/wq22e7feTTWg=="], + + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-FSM94nGd55NYo48usCyM/nHfUKRnqc9+b0vJNuKV0oCCpIp/OGims7rO1Nv/DkFkt0S/s2rxsJ2kkS8J3HcpeA=="], + + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.10", "", { "os": "linux", "cpu": "x64" }, "sha512-C3YxNB16myRLs7o+B+6PnQ6jBsdIS4+AE4Ah8glVGhDpEv9AOvxhZ/1duAb4B0UGczEK/lBbccksd8VI+p6zfw=="], + + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.10", "", { "os": "none", "cpu": "arm64" }, "sha512-571TlE/F1eeTjjdjYAMMMPs1Mfv3MtX6s3+ZKVU6HiUjZ5Njc6c/qzNy/8K3zALTZnaw3JQVYrHxvNfjm43KAg=="], + + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-QXW+ZWaiqs2c7Fi++D/SsW07LTPcUrncxcskJGfGNBoaLik1IU6fJymz4HsqwEO0u5Iq11yTO0B/mc4cPk7jrQ=="], + + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.10", "", { "os": "win32", "cpu": "x64" }, "sha512-5FQFGgah17YeMtG1Yd5a+rMxQpTksyNXxRtKz06FVTaQw3RKYUJQbUoKk0/5jrXBpDo+7makNP7UHA2LQyH64A=="], + + "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.59.0", "", { "os": "android", "cpu": "arm" }, "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.59.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.59.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.59.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.59.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.59.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.59.0", "", { "os": "linux", "cpu": "arm" }, "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.59.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.59.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.59.0", "", { "os": "linux", "cpu": "none" }, "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.59.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.59.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.59.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.59.0", "", { "os": "none", "cpu": "arm64" }, "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.59.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.59.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.59.0", "", { "os": "win32", "cpu": "x64" }, "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "@types/fs-extra": ["@types/fs-extra@8.1.5", "", { "dependencies": { "@types/node": "*" } }, "sha512-0dzKcwO+S8s2kuF5Z9oUWatQJj5Uq/iqphEtE3GQJVRRYm/tD1LglU2UnXi2A8jLq5umkGouOXOR9y0n613ZwQ=="], + + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], + + "@types/google.maps": ["@types/google.maps@3.66.4", "", {}, "sha512-t01LM0VeIsUOZWno7ABUv4XL82RalnhRGaTzWykPUtQ+hHT39vsIekeU+roqEaibwebpYIjS7dslx51HwG95nw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], + + "@types/node": ["@types/node@24.12.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ=="], + + "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], + + "@types/react": ["@types/react@18.3.28", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="], + + "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], + + "@types/resize-observer-browser": ["@types/resize-observer-browser@0.1.11", "", {}, "sha512-cNw5iH8JkMkb3QkCoe7DaZiawbDQEUX8t7iuQaRTyLOyQCR2h+ibBD4GJt7p5yhUHrlOeL7ZtbxNHeipqNsBzQ=="], + + "@types/semver": ["@types/semver@7.7.1", "", {}, "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA=="], + + "@types/slice-ansi": ["@types/slice-ansi@4.0.0", "", {}, "sha512-+OpjSaq85gvlZAYINyzKpLeiFkSC4EsC6IIiT6v6TLSU5k5U83fHGj9Lel8oKEXM0HqgrMVCjXPDPVICtxF7EQ=="], + + "@types/supercluster": ["@types/supercluster@7.1.3", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@5.62.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/type-utils": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", "semver": "^7.3.7", "tsutils": "^3.21.0" }, "peerDependencies": { "@typescript-eslint/parser": "^5.0.0", "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@5.62.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0" } }, "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@5.62.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "5.62.0", "@typescript-eslint/utils": "5.62.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, "peerDependencies": { "eslint": "*" } }, "sha512-xsSQreu+VnfbqQpW5vnCJdq1Z3Q0U31qiWmRhr98ONQmcp/yhiPJFPq8MXiJVLiksmOKSjIldZzkebzHuCGzew=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@5.62.0", "", {}, "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "@typescript-eslint/visitor-keys": "5.62.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@5.62.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", "@typescript-eslint/typescript-estree": "5.62.0", "eslint-scope": "^5.1.1", "semver": "^7.3.7" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@5.62.0", "", { "dependencies": { "@typescript-eslint/types": "5.62.0", "eslint-visitor-keys": "^3.3.0" } }, "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@vitejs/plugin-react": ["@vitejs/plugin-react@6.1.1", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "oxc-transform-react": "^0.145.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler", "oxc-transform-react"] }, "sha512-yxLaQV9gkhS8ezJqCM6+ndU7mDY6gqAg75NQ+0IjwEI8IYOmQCgkRwHKVSfWXW076DsqMo0Dk+0FK1U+M5RgFw=="], + + "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="], + + "array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + + "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="], + + "bplist-creator": ["bplist-creator@0.1.0", "", { "dependencies": { "stream-buffers": "2.2.x" } }, "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg=="], + + "bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="], + + "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "chevrotain": ["chevrotain@11.0.3", "", { "dependencies": { "@chevrotain/cst-dts-gen": "11.0.3", "@chevrotain/gast": "11.0.3", "@chevrotain/regexp-to-ast": "11.0.3", "@chevrotain/types": "11.0.3", "@chevrotain/utils": "11.0.3", "lodash-es": "4.17.21" } }, "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw=="], + + "chevrotain-allstar": ["chevrotain-allstar@0.3.1", "", { "dependencies": { "lodash-es": "^4.17.21" }, "peerDependencies": { "chevrotain": "^11.0.0" } }, "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw=="], + + "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="], + + "data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], + + "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "doctrine": ["doctrine@3.0.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "elementtree": ["elementtree@0.1.7", "", { "dependencies": { "sax": "1.1.4" } }, "sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", "@eslint/js": "8.57.1", "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "globals": "^13.19.0", "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3", "strip-ansi": "^6.0.1", "text-table": "^0.2.0" }, "bin": { "eslint": "bin/eslint.js" } }, "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA=="], + + "eslint-config-prettier": ["eslint-config-prettier@8.10.2", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "bin/cli.js" } }, "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A=="], + + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.9", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.13.0", "resolve": "^1.22.4" } }, "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + + "eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-equals": ["fast-equals@5.4.3", "", {}, "sha512-8unmvA0qrfpFWBYUate9BTGtQdKHWBYt+KdAIkag7Vvxe7wz8Q2nJg7fLMC0bJqe0bjP31kca7Ckaawrs4Istg=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "file-entry-cache": ["file-entry-cache@6.0.1", "", { "dependencies": { "flat-cache": "^3.0.4" } }, "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@3.2.0", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.3", "rimraf": "^3.0.2" } }, "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw=="], + + "flatted": ["flatted@3.4.0", "", {}, "sha512-kC6Bb+ooptOIvWj5B63EQWkF0FEnNjV2ZNkLMLZRDDduIiWeFF4iKnslwhiWxjAdbg4NzTNo6h0qLuvFrcx+Sw=="], + + "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], + + "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="], + + "functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "github-slugger": ["github-slugger@1.5.0", "", {}, "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw=="], + + "glob": ["glob@13.0.6", "", { "dependencies": { "minimatch": "^10.2.2", "minipass": "^7.1.3", "path-scurry": "^2.0.2" } }, "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@13.24.0", "", { "dependencies": { "type-fest": "^0.20.2" } }, "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ=="], + + "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@4.1.3", "", {}, "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg=="], + + "internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + + "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-map": ["is-map@2.0.3", "", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "is-path-inside": ["is-path-inside@3.0.3", "", {}, "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ=="], + + "is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="], + + "is-set": ["is-set@2.0.3", "", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + + "isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "java-parser": ["java-parser@3.0.1", "", { "dependencies": { "chevrotain": "11.0.3", "chevrotain-allstar": "0.3.1", "lodash": "4.17.21" } }, "sha512-sDIR7u9b7O2JViNUxiZRhnRz7URII/eE7g2B+BmGxDeS6Ex3OYAcCyz5oh0H4LQ+hL/BS8OJTz8apMy9xtGmrQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], + + "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + + "kdbush": ["kdbush@4.1.0", "", {}, "sha512-e9vurzrXJQrFX6ckpHP3bvj5l+9CnYzkxDNnNQ1h2QTqdWsUAJgXiKdGNcOa1EY85dU8KbQ+z/FdQdB7P+9yfQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lightningcss": ["lightningcss@1.33.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.33.0", "lightningcss-darwin-arm64": "1.33.0", "lightningcss-darwin-x64": "1.33.0", "lightningcss-freebsd-x64": "1.33.0", "lightningcss-linux-arm-gnueabihf": "1.33.0", "lightningcss-linux-arm64-gnu": "1.33.0", "lightningcss-linux-arm64-musl": "1.33.0", "lightningcss-linux-x64-gnu": "1.33.0", "lightningcss-linux-x64-musl": "1.33.0", "lightningcss-win32-arm64-msvc": "1.33.0", "lightningcss-win32-x64-msvc": "1.33.0" } }, "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA=="], + + "lightningcss-android-arm64": ["lightningcss-android-arm64@1.33.0", "", { "os": "android", "cpu": "arm64" }, "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg=="], + + "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.33.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg=="], + + "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.33.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ=="], + + "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.33.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg=="], + + "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.33.0", "", { "os": "linux", "cpu": "arm" }, "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ=="], + + "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg=="], + + "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.33.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ=="], + + "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg=="], + + "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.33.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw=="], + + "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.33.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA=="], + + "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.33.0", "", { "os": "win32", "cpu": "x64" }, "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], + + "lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "minizlib": ["minizlib@3.1.0", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "native-run": ["native-run@2.0.3", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-terminal": "^2.3.4", "bplist-parser": "^0.3.2", "debug": "^4.3.4", "elementtree": "^0.1.7", "ini": "^4.1.1", "plist": "^3.1.0", "split2": "^4.2.0", "through2": "^4.0.2", "tslib": "^2.6.2", "yauzl": "^2.10.0" }, "bin": { "native-run": "bin/native-run" } }, "sha512-U1PllBuzW5d1gfan+88L+Hky2eZx+9gv3Pf6rNBxKbORxi7boHzqiA6QFGSnqMem4j0A9tZ08NMIs5+0m/VS1Q=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "natural-compare-lite": ["natural-compare-lite@1.4.0", "", {}, "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], + + "possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], + + "prettier-plugin-java": ["prettier-plugin-java@2.8.1", "", { "dependencies": { "java-parser": "3.0.1" }, "peerDependencies": { "prettier": "^3.0.0" } }, "sha512-tkteH5OSCEb0E7wKnhhUSitr1pGUCUt9M//CwerSNhoalL/qv0jXTeSVBPZ36KC+kZl3nbq4dxh144NuGchACg=="], + + "prettier-pretty-check": ["prettier-pretty-check@0.2.0", "", { "dependencies": { "diff": "^5.2.0" }, "bin": { "prettier-pretty-check": "index.js" } }, "sha512-gVmwxYvm1Qii1H6eULNcNchlruyXyKkluP1LBhUaqdSvvACc8FZ99zTWzqQHJrBu9qUrYmNRysPmnYSm9c7G6Q=="], + + "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], + + "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "rimraf": ["rimraf@6.1.3", "", { "dependencies": { "glob": "^13.0.3", "package-json-from-dist": "^1.0.1" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA=="], + + "rolldown": ["rolldown@1.2.10", "", { "dependencies": { "@oxc-project/types": "=0.151.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.10", "@rolldown/binding-android-arm64": "1.2.10", "@rolldown/binding-darwin-arm64": "1.2.10", "@rolldown/binding-darwin-x64": "1.2.10", "@rolldown/binding-freebsd-x64": "1.2.10", "@rolldown/binding-linux-arm-gnueabihf": "1.2.10", "@rolldown/binding-linux-arm64-gnu": "1.2.10", "@rolldown/binding-linux-arm64-musl": "1.2.10", "@rolldown/binding-linux-ppc64-gnu": "1.2.10", "@rolldown/binding-linux-s390x-gnu": "1.2.10", "@rolldown/binding-linux-x64-gnu": "1.2.10", "@rolldown/binding-linux-x64-musl": "1.2.10", "@rolldown/binding-openharmony-arm64": "1.2.10", "@rolldown/binding-win32-arm64-msvc": "1.2.10", "@rolldown/binding-win32-x64-msvc": "1.2.10" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-OxkA08pSryMK7B3XiFA09B4OJ1xJMPgIYCBMY2xchzpqgBGsV1o0DetPAE+Sl3N3L4oCPiEzmHVSOj7iR04Zog=="], + + "rollup": ["rollup@4.59.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.59.0", "@rollup/rollup-android-arm64": "4.59.0", "@rollup/rollup-darwin-arm64": "4.59.0", "@rollup/rollup-darwin-x64": "4.59.0", "@rollup/rollup-freebsd-arm64": "4.59.0", "@rollup/rollup-freebsd-x64": "4.59.0", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", "@rollup/rollup-linux-arm-musleabihf": "4.59.0", "@rollup/rollup-linux-arm64-gnu": "4.59.0", "@rollup/rollup-linux-arm64-musl": "4.59.0", "@rollup/rollup-linux-loong64-gnu": "4.59.0", "@rollup/rollup-linux-loong64-musl": "4.59.0", "@rollup/rollup-linux-ppc64-gnu": "4.59.0", "@rollup/rollup-linux-ppc64-musl": "4.59.0", "@rollup/rollup-linux-riscv64-gnu": "4.59.0", "@rollup/rollup-linux-riscv64-musl": "4.59.0", "@rollup/rollup-linux-s390x-gnu": "4.59.0", "@rollup/rollup-linux-x64-gnu": "4.59.0", "@rollup/rollup-linux-x64-musl": "4.59.0", "@rollup/rollup-openbsd-x64": "4.59.0", "@rollup/rollup-openharmony-arm64": "4.59.0", "@rollup/rollup-win32-arm64-msvc": "4.59.0", "@rollup/rollup-win32-ia32-msvc": "4.59.0", "@rollup/rollup-win32-x64-gnu": "4.59.0", "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "sax": ["sax@1.5.0", "", {}, "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA=="], + + "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "simple-plist": ["simple-plist@1.3.1", "", { "dependencies": { "bplist-creator": "0.1.0", "bplist-parser": "0.3.1", "plist": "^3.0.5" } }, "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw=="], + + "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="], + + "stream-buffers": ["stream-buffers@2.2.0", "", {}, "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supercluster": ["supercluster@8.0.1", "", { "dependencies": { "kdbush": "^4.0.2" } }, "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "swiftlint": ["swiftlint@2.0.0", "", { "dependencies": { "@ionic/utils-fs": "^3.1.7", "@ionic/utils-subprocess": "^3.0.1", "cosmiconfig": "^9.0.0" }, "bin": { "node-swiftlint": "bin.js" } }, "sha512-MMVuyZ4/6WcIJlk0z6GM0pZjRuwnyUJqRPbJBFW3oACN/qjAvRbolCWEu+zE2MycF/cEgqfUpI+oLECNfjfOJA=="], + + "tar": ["tar@7.5.10", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw=="], + + "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], + + "through2": ["through2@4.0.2", "", { "dependencies": { "readable-stream": "3" } }, "sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], + + "tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "tsutils": ["tsutils@3.21.0", "", { "dependencies": { "tslib": "^1.8.1" }, "peerDependencies": { "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "type-fest": ["type-fest@0.20.2", "", {}, "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + + "untildify": ["untildify@4.0.0", "", {}, "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "uuid": ["uuid@7.0.3", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg=="], + + "vite": ["vite@8.3.0", "", { "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", "postcss": "^8.5.28", "rolldown": "~1.2.6", "tinyglobby": "^0.2.17" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.7.1", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-lhZBVvEHefgE+HQZC9O7EBJgCU/nVzFNl7vkS4RE0APtWLP02/8QVIkQtzBxPquh7lq5/78NHipTj7ODQ6XuyQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "xcode": ["xcode@3.0.1", "", { "dependencies": { "simple-plist": "^1.1.0", "uuid": "^7.0.3" } }, "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA=="], + + "xml2js": ["xml2js@0.6.2", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA=="], + + "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], + + "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], + + "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@capacitor/docgen/typescript": ["typescript@4.2.4", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg=="], + + "@capgo/capacitor-native-map/typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "@ionic/utils-fs/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], + + "@typescript-eslint/utils/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], + + "elementtree/sax": ["sax@1.1.4", "", {}, "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg=="], + + "eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="], + + "eslint-plugin-import/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "flat-cache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + + "glob/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], + + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "prompts/kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], + + "simple-plist/bplist-parser": ["bplist-parser@0.3.1", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA=="], + + "tsutils/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + + "xml2js/xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "@typescript-eslint/utils/eslint-scope/estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + + "flat-cache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + + "glob/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + } +} diff --git a/example-app/bunfig.toml b/example-app/bunfig.toml new file mode 100644 index 0000000..641e878 --- /dev/null +++ b/example-app/bunfig.toml @@ -0,0 +1,2 @@ +[install] +linker = "isolated" diff --git a/example-app/capacitor.config.ts b/example-app/capacitor.config.ts new file mode 100644 index 0000000..5caea73 --- /dev/null +++ b/example-app/capacitor.config.ts @@ -0,0 +1,13 @@ +import type { CapacitorConfig } from '@capacitor/cli'; + +const config: CapacitorConfig = { + appId: 'app.capgo.nativemap', + appName: 'Capgo Native Map', + webDir: 'dist', + bundledWebRuntime: false, + server: { + androidScheme: 'https', + }, +}; + +export default config; diff --git a/example-app/index.html b/example-app/index.html new file mode 100644 index 0000000..cd4dbd6 --- /dev/null +++ b/example-app/index.html @@ -0,0 +1,19 @@ + + + + + + + + + +@capgo/capacitor-native-audio + + +
+ + + diff --git a/example-app/ios/.gitignore b/example-app/ios/.gitignore new file mode 100644 index 0000000..f470299 --- /dev/null +++ b/example-app/ios/.gitignore @@ -0,0 +1,13 @@ +App/build +App/Pods +App/output +App/App/public +DerivedData +xcuserdata + +# Cordova plugins for Capacitor +capacitor-cordova-ios-plugins + +# Generated Config files +App/App/capacitor.config.json +App/App/config.xml diff --git a/example-app/ios/App/App.xcodeproj/project.pbxproj b/example-app/ios/App/App.xcodeproj/project.pbxproj new file mode 100644 index 0000000..61a4179 --- /dev/null +++ b/example-app/ios/App/App.xcodeproj/project.pbxproj @@ -0,0 +1,380 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 60; + objects = { + +/* Begin PBXBuildFile section */ + 2FAD9763203C412B000D30F8 /* config.xml in Resources */ = {isa = PBXBuildFile; fileRef = 2FAD9762203C412B000D30F8 /* config.xml */; }; + 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */ = {isa = PBXBuildFile; productRef = 4D22ABE82AF431CB00220026 /* CapApp-SPM */; }; + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */ = {isa = PBXBuildFile; fileRef = 50379B222058CBB4000EE86E /* capacitor.config.json */; }; + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504EC3071FED79650016851F /* AppDelegate.swift */; }; + 504EC30D1FED79650016851F /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30B1FED79650016851F /* Main.storyboard */; }; + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; }; + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; }; + 50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; }; + 8A37553416F54B05B9496449 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E48BEE0478545B69113EF4B /* SceneDelegate.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 2FAD9762203C412B000D30F8 /* config.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = config.xml; sourceTree = ""; }; + 50379B222058CBB4000EE86E /* capacitor.config.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = capacitor.config.json; sourceTree = ""; }; + 504EC3041FED79650016851F /* App.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = App.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 504EC3071FED79650016851F /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 504EC30C1FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 504EC30E1FED79650016851F /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = ""; }; + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; }; + 1E48BEE0478545B69113EF4B /* SceneDelegate.swift */ = {isa = PBXFileReference; name = "SceneDelegate.swift"; path = "SceneDelegate.swift"; sourceTree = ""; fileEncoding = 4; lastKnownFileType = sourcecode.swift; explicitFileType = undefined; includeInIndex = 0; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 504EC3011FED79650016851F /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4D22ABE92AF431CB00220026 /* CapApp-SPM in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 504EC2FB1FED79650016851F = { + isa = PBXGroup; + children = ( + 958DCC722DB07C7200EA8C5F /* debug.xcconfig */, + 504EC3061FED79650016851F /* App */, + 504EC3051FED79650016851F /* Products */, + ); + sourceTree = ""; + }; + 504EC3051FED79650016851F /* Products */ = { + isa = PBXGroup; + children = ( + 504EC3041FED79650016851F /* App.app */, + ); + name = Products; + sourceTree = ""; + }; + 504EC3061FED79650016851F /* App */ = { + isa = PBXGroup; + children = ( + 50379B222058CBB4000EE86E /* capacitor.config.json */, + 504EC3071FED79650016851F /* AppDelegate.swift */, + 504EC30B1FED79650016851F /* Main.storyboard */, + 504EC30E1FED79650016851F /* Assets.xcassets */, + 504EC3101FED79650016851F /* LaunchScreen.storyboard */, + 504EC3131FED79650016851F /* Info.plist */, + 2FAD9762203C412B000D30F8 /* config.xml */, + 50B271D01FEDC1A000F3C39B /* public */, + 1E48BEE0478545B69113EF4B /* SceneDelegate.swift */, + ); + path = App; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 504EC3031FED79650016851F /* App */ = { + isa = PBXNativeTarget; + buildConfigurationList = 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */; + buildPhases = ( + 504EC3001FED79650016851F /* Sources */, + 504EC3011FED79650016851F /* Frameworks */, + 504EC3021FED79650016851F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = App; + packageProductDependencies = ( + 4D22ABE82AF431CB00220026 /* CapApp-SPM */, + ); + productName = App; + productReference = 504EC3041FED79650016851F /* App.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 504EC2FC1FED79650016851F /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 920; + LastUpgradeCheck = 920; + TargetAttributes = { + 504EC3031FED79650016851F = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */; + compatibilityVersion = "Xcode 8.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 504EC2FB1FED79650016851F; + packageReferences = ( + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */, + ); + productRefGroup = 504EC3051FED79650016851F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 504EC3031FED79650016851F /* App */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 504EC3021FED79650016851F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */, + 50B271D11FEDC1A000F3C39B /* public in Resources */, + 504EC30F1FED79650016851F /* Assets.xcassets in Resources */, + 50379B232058CBB4000EE86E /* capacitor.config.json in Resources */, + 504EC30D1FED79650016851F /* Main.storyboard in Resources */, + 2FAD9763203C412B000D30F8 /* config.xml in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 504EC3001FED79650016851F /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 504EC3081FED79650016851F /* AppDelegate.swift in Sources */, + 8A37553416F54B05B9496449 /* SceneDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 504EC30B1FED79650016851F /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC30C1FED79650016851F /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 504EC3101FED79650016851F /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 504EC3111FED79650016851F /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 504EC3141FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 504EC3151FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 504EC3171FED79650016851F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; + PRODUCT_BUNDLE_IDENTIFIER = app.capgo.nativemap; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 504EC3181FED79650016851F /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + INFOPLIST_FILE = App/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = app.capgo.nativemap; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 504EC2FF1FED79650016851F /* Build configuration list for PBXProject "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3141FED79650016851F /* Debug */, + 504EC3151FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 504EC3161FED79650016851F /* Build configuration list for PBXNativeTarget "App" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 504EC3171FED79650016851F /* Debug */, + 504EC3181FED79650016851F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "CapApp-SPM"; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4D22ABE82AF431CB00220026 /* CapApp-SPM */ = { + isa = XCSwiftPackageProductDependency; + package = D4C12C0A2AAA248700AAC8A2 /* XCLocalSwiftPackageReference "CapApp-SPM" */; + productName = "CapApp-SPM"; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 504EC2FC1FED79650016851F /* Project object */; +} diff --git a/example-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example-app/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example-app/ios/App/App/AppDelegate.swift b/example-app/ios/App/App/AppDelegate.swift new file mode 100644 index 0000000..d3091c4 --- /dev/null +++ b/example-app/ios/App/App/AppDelegate.swift @@ -0,0 +1,58 @@ +import UIKit +import Capacitor + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { + // Called when the app was launched with a url. Feel free to add additional processing here, + // but if you want the App API to support tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(app, open: url, options: options) + } + + func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { + // Called when the app was launched with an activity, including Universal Links. + // Feel free to add additional processing here, but if you want the App API to support + // tracking app url opens, make sure to keep this call + return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler) + } + + + func application(_ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions) -> UISceneConfiguration { + let config = UISceneConfiguration(name: "Default Configuration", + sessionRole: connectingSceneSession.role) + config.delegateClass = SceneDelegate.self + return config + } +} diff --git a/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png new file mode 100644 index 0000000..adf6ba0 Binary files /dev/null and b/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ diff --git a/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json b/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9b7d382 --- /dev/null +++ b/example-app/ios/App/App/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon-512@2x.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/example-app/ios/App/App/Assets.xcassets/Contents.json b/example-app/ios/App/App/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/example-app/ios/App/App/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/example-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json new file mode 100644 index 0000000..d7d96a6 --- /dev/null +++ b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "splash-2732x2732-2.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732-1.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "splash-2732x2732.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png new file mode 100644 index 0000000..33ea6c9 Binary files /dev/null and b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-1.png differ diff --git a/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png new file mode 100644 index 0000000..33ea6c9 Binary files /dev/null and b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732-2.png differ diff --git a/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png new file mode 100644 index 0000000..33ea6c9 Binary files /dev/null and b/example-app/ios/App/App/Assets.xcassets/Splash.imageset/splash-2732x2732.png differ diff --git a/example-app/ios/App/App/Base.lproj/LaunchScreen.storyboard b/example-app/ios/App/App/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..e7ae5d7 --- /dev/null +++ b/example-app/ios/App/App/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example-app/ios/App/App/Base.lproj/Main.storyboard b/example-app/ios/App/App/Base.lproj/Main.storyboard new file mode 100644 index 0000000..b44df7b --- /dev/null +++ b/example-app/ios/App/App/Base.lproj/Main.storyboard @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/example-app/ios/App/App/Info.plist b/example-app/ios/App/App/Info.plist new file mode 100644 index 0000000..4e194dc --- /dev/null +++ b/example-app/ios/App/App/Info.plist @@ -0,0 +1,70 @@ + + + + + CAPACITOR_DEBUG + $(CAPACITOR_DEBUG) + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Native Audio Example + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + \ No newline at end of file diff --git a/example-app/ios/App/App/SceneDelegate.swift b/example-app/ios/App/App/SceneDelegate.swift new file mode 100644 index 0000000..f352e1e --- /dev/null +++ b/example-app/ios/App/App/SceneDelegate.swift @@ -0,0 +1,24 @@ +import UIKit +import Capacitor + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = scene as? UIWindowScene else { return } + + window = UIWindow(windowScene: windowScene) + window?.rootViewController = CAPBridgeViewController() + window?.makeKeyAndVisible() + + SceneDelegateProxy.shared.scene(scene, willConnectTo: session, options: connectionOptions) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + SceneDelegateProxy.shared.scene(scene, openURLContexts: URLContexts) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + SceneDelegateProxy.shared.scene(scene, continue: userActivity) + } +} diff --git a/example-app/ios/App/CapApp-SPM/.gitignore b/example-app/ios/App/CapApp-SPM/.gitignore new file mode 100644 index 0000000..3b29812 --- /dev/null +++ b/example-app/ios/App/CapApp-SPM/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/example-app/ios/App/CapApp-SPM/Package.swift b/example-app/ios/App/CapApp-SPM/Package.swift new file mode 100644 index 0000000..492ce35 --- /dev/null +++ b/example-app/ios/App/CapApp-SPM/Package.swift @@ -0,0 +1,27 @@ +// swift-tools-version: 5.9 +import PackageDescription + +// DO NOT MODIFY THIS FILE - managed by Capacitor CLI commands +let package = Package( + name: "CapApp-SPM", + platforms: [.iOS(.v15)], + products: [ + .library( + name: "CapApp-SPM", + targets: ["CapApp-SPM"]) + ], + dependencies: [ + .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.5.2"), + .package(name: "CapgoCapacitorNativeMap", path: "../../../node_modules/.bun/@capgo+capacitor-native-map@file+../node_modules/@capgo/capacitor-native-map") + ], + targets: [ + .target( + name: "CapApp-SPM", + dependencies: [ + .product(name: "Capacitor", package: "capacitor-swift-pm"), + .product(name: "Cordova", package: "capacitor-swift-pm"), + .product(name: "CapgoCapacitorNativeMap", package: "CapgoCapacitorNativeMap") + ] + ) + ] +) diff --git a/example-app/ios/App/CapApp-SPM/README.md b/example-app/ios/App/CapApp-SPM/README.md new file mode 100644 index 0000000..5e22a2f --- /dev/null +++ b/example-app/ios/App/CapApp-SPM/README.md @@ -0,0 +1,5 @@ +# CapApp-SPM + +This SPM is used to host SPM dependencies for you Capacitor project + +Do not modify the contents of it or there may be unintended consequences. diff --git a/example-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift b/example-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift new file mode 100644 index 0000000..945afec --- /dev/null +++ b/example-app/ios/App/CapApp-SPM/Sources/CapApp-SPM/CapApp-SPM.swift @@ -0,0 +1 @@ +public let isCapacitorApp = true diff --git a/example-app/ios/debug.xcconfig b/example-app/ios/debug.xcconfig new file mode 100644 index 0000000..53ce18d --- /dev/null +++ b/example-app/ios/debug.xcconfig @@ -0,0 +1 @@ +CAPACITOR_DEBUG = true diff --git a/example-app/package.json b/example-app/package.json new file mode 100644 index 0000000..5656c39 --- /dev/null +++ b/example-app/package.json @@ -0,0 +1,32 @@ +{ + "name": "native-map-example", + "version": "0.0.1", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "sync": "bunx cap sync", + "sync:ios": "bunx cap sync ios", + "sync:android": "bunx cap sync android", + "ios": "bunx cap run ios", + "android": "bunx cap run android" + }, + "dependencies": { + "@capacitor/core": "^8.5.2", + "@capgo/capacitor-native-map": "..", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@capacitor/android": "^8.5.2", + "@capacitor/cli": "^8.5.2", + "@capacitor/ios": "^8.5.2", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^6.0.0", + "typescript": "^6.0.3", + "vite": "^8.3.0" + } +} diff --git a/example-app/src/App.css b/example-app/src/App.css new file mode 100644 index 0000000..26f49f5 --- /dev/null +++ b/example-app/src/App.css @@ -0,0 +1,98 @@ +.app { + max-width: 960px; + margin: 0 auto; + padding: 1rem; + font-family: system-ui, sans-serif; +} + +.app-overlay { + max-width: none; + min-height: 100vh; + padding: 0; + margin: 0; +} + +.map-container { + width: 100%; + height: 420px; + border-radius: 12px; + overflow: hidden; + border: 1px solid #ccc; + margin-top: 1rem; +} + +.map-overlay-ui { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 10; +} + +.map-overlay-ui [data-map-overlay] { + pointer-events: auto; +} + +.overlay-bar { + margin: env(safe-area-inset-top) 1rem 0; + padding: 1rem 1.25rem; + border-radius: 14px; + background: rgba(15, 23, 42, 0.82); + color: #f8fafc; + backdrop-filter: blur(8px); +} + +.overlay-bar h1 { + margin: 0 0 0.35rem; + font-size: 1.15rem; +} + +.overlay-bar p { + margin: 0; + font-size: 0.9rem; + opacity: 0.9; +} + +.overlay-actions { + position: fixed; + left: 50%; + bottom: calc(1.25rem + env(safe-area-inset-bottom)); + transform: translateX(-50%); + display: flex; + gap: 0.75rem; +} + +.overlay-actions button, +.mode-switch { + border: 0; + border-radius: 999px; + padding: 0.65rem 1.1rem; + font-weight: 600; + background: #0ea5e9; + color: #fff; + box-shadow: 0 8px 24px rgba(14, 165, 233, 0.35); +} + +.tagline { + color: #444; +} + +.status-message { + margin-top: 0.75rem; + text-align: center; + color: #0f172a; + font-size: 0.85rem; + pointer-events: none; +} + +.app-overlay .status-message { + position: fixed; + left: 1rem; + right: 1rem; + bottom: calc(5rem + env(safe-area-inset-bottom)); + margin-top: 0; + color: #e2e8f0; +} + +.error { + color: #b00020; +} diff --git a/example-app/src/App.tsx b/example-app/src/App.tsx new file mode 100644 index 0000000..6d0433a --- /dev/null +++ b/example-app/src/App.tsx @@ -0,0 +1,120 @@ +import { useEffect, useRef, useState } from 'react'; +import { Capacitor } from '@capacitor/core'; +import { NativeMap } from '@capgo/capacitor-native-map'; +import './App.css'; + +const DEFAULT_CENTER = { lat: 37.7749, lng: -122.4194 }; +const API_KEY = import.meta.env.VITE_GOOGLE_MAPS_API_KEY ?? ''; + +type DemoMode = 'embedded' | 'overlay'; + +const App = () => { + const mapRef = useRef(null); + const nativeMapRef = useRef(null); + const [mode, setMode] = useState(Capacitor.isNativePlatform() ? 'overlay' : 'embedded'); + const [status, setStatus] = useState('Initializing map...'); + const [error, setError] = useState(null); + + useEffect(() => { + let destroyed = false; + + const init = async () => { + if (mode === 'embedded' && !mapRef.current) { + return; + } + if (Capacitor.getPlatform() !== 'ios' && !API_KEY) { + setError('Set VITE_GOOGLE_MAPS_API_KEY for web and Android builds.'); + setStatus('Missing API key'); + return; + } + + try { + await nativeMapRef.current?.destroy().catch(() => undefined); + nativeMapRef.current = null; + + const overlay = mode === 'overlay' && Capacitor.isNativePlatform(); + const map = await NativeMap.create({ + id: overlay ? 'overlay-map' : 'demo-map', + element: overlay ? document.body : mapRef.current!, + toBack: overlay, + apiKey: API_KEY, + config: { + center: DEFAULT_CENTER, + zoom: 12, + width: overlay ? window.innerWidth : undefined, + height: overlay ? window.innerHeight : undefined, + x: 0, + y: 0, + }, + }); + + if (destroyed) { + await map.destroy(); + return; + } + + nativeMapRef.current = map; + await map.addMarker({ + coordinate: DEFAULT_CENTER, + title: 'Hello', + snippet: overlay ? 'HTML overlay demo' : 'Capgo Native Map', + }); + await map.setCamera({ + coordinate: { lat: 37.7849, lng: -122.4094 }, + zoom: 14, + animate: true, + }); + setStatus(overlay ? 'Overlay map ready. Pan and pinch on open areas.' : 'Map ready with marker and camera move.'); + setError(null); + } catch (e) { + setError(e instanceof Error ? e.message : String(e)); + setStatus('Failed to load map'); + } + }; + + init(); + + return () => { + destroyed = true; + nativeMapRef.current?.destroy().catch(() => undefined); + nativeMapRef.current = null; + }; + }, [mode]); + + return ( +
+ {mode === 'overlay' && ( +
+
+

Native map behind HTML

+

Buttons stay in the WebView. Open areas pass gestures to the map.

+
+
+ + +
+
+ )} + + {mode === 'embedded' && ( + <> +

@capgo/capacitor-native-map

+

Google Maps on Android, Apple MapKit on iOS, Google Maps JS on web.

+ +
+ + )} + +

{status}

+ {error &&

{error}

} +
+ ); +}; + +export default App; diff --git a/example-app/src/index.css b/example-app/src/index.css new file mode 100644 index 0000000..ef214b6 --- /dev/null +++ b/example-app/src/index.css @@ -0,0 +1,49 @@ +/* Capacitor example app defaults */ +html { + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + touch-action: manipulation; + padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left); +} + +body { + overscroll-behavior: none; + -webkit-tap-highlight-color: transparent; +} + +:root { + color-scheme: light; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + line-height: 1.5; + font-weight: 400; + color: #0f172a; + background-color: #f1f5f9; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + min-height: 100vh; + background: linear-gradient(160deg, #e0f2fe 0%, #f8fafc 100%); +} + +html.native-map-to-back, +body.native-map-to-back { + background: transparent !important; +} + +#root { + max-width: 960px; + margin: 0 auto; + padding: 3rem 1.5rem; +} + +html.native-map-to-back #root { + max-width: none; + margin: 0; + padding: 0; + min-height: 100vh; +} diff --git a/example-app/src/main.tsx b/example-app/src/main.tsx new file mode 100644 index 0000000..a2bf01b --- /dev/null +++ b/example-app/src/main.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; +import './index.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +); diff --git a/example-app/src/vite-env.d.ts b/example-app/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/example-app/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/example-app/tsconfig.json b/example-app/tsconfig.json new file mode 100644 index 0000000..c569f01 --- /dev/null +++ b/example-app/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "ignoreDeprecations": "6.0", + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["DOM", "DOM.Iterable", "ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx" + }, + "include": ["src"], + "references": [ + { "path": "./tsconfig.node.json" } + ] +} diff --git a/example-app/tsconfig.node.json b/example-app/tsconfig.node.json new file mode 100644 index 0000000..b007a3c --- /dev/null +++ b/example-app/tsconfig.node.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "composite": true, + "module": "ESNext", + "moduleResolution": "Node", + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true + }, + "include": ["vite.config.ts", "capacitor.config.ts"] +} diff --git a/example-app/vite.config.ts b/example-app/vite.config.ts new file mode 100644 index 0000000..207e79b --- /dev/null +++ b/example-app/vite.config.ts @@ -0,0 +1,9 @@ +import react from '@vitejs/plugin-react'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [react()], + server: { + host: true, + }, +}); diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..afb34f8 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/ios/Sources/NativeMapPlugin/Appearance.swift b/ios/Sources/NativeMapPlugin/Appearance.swift new file mode 100644 index 0000000..cb442bc --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Appearance.swift @@ -0,0 +1,234 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Map appearance, location, gestures & padding (#8, #10) +// +// Split out of Overlays.swift to keep both files within SwiftLint's length +// budget. Map type + appearance toggles, the user-location dot, gesture toggles, +// and edge padding - the per-map methods plus their bridge entry points. + +extension NativeMapConfig { + /// Parses a `{ top, left, right, bottom }` object into edge insets, defaulting + /// missing sides to 0. Pure, so it can be unit-tested. + static func parsePadding(_ value: Any?) -> UIEdgeInsets { + guard let obj = value as? JSObject else { return .zero } + return UIEdgeInsets( + top: CGFloat(obj["top"] as? Double ?? 0), + left: CGFloat(obj["left"] as? Double ?? 0), + bottom: CGFloat(obj["bottom"] as? Double ?? 0), + right: CGFloat(obj["right"] as? Double ?? 0) + ) + } +} + +extension Map { + + func setMapType(_ type: String) { + DispatchQueue.main.async { self.mapView.mapType = Map.mapType(from: type) } + } + + /// The current base map imagery as a JS `MapType` string. Must be called on the + /// main thread. + func currentMapType() -> String { + Map.mapTypeString(from: mapView.mapType) + } + + func setCurrentLocation(_ enabled: Bool) { + DispatchQueue.main.async { self.mapView.showsUserLocation = enabled } + } + + /// Apply the create-time appearance toggles from a config in one pass. Must + /// be called on the main thread (invoked from `render()`); the runtime setters + /// below each target a single property. + func applyAppearance(_ config: NativeMapConfig) { + mapView.showsTraffic = config.showsTraffic + mapView.pointOfInterestFilter = config.showsPointsOfInterest ? .includingAll : .excludingAll + mapView.showsCompass = config.showsCompass + mapView.showsScale = config.showsScale + mapView.showsBuildings = config.showsBuildings + mapView.overrideUserInterfaceStyle = Map.userInterfaceStyle(from: config.colorScheme) + } + + func setTraffic(_ enabled: Bool) { + DispatchQueue.main.async { self.mapView.showsTraffic = enabled } + } + + func setPointsOfInterest(_ enabled: Bool) { + DispatchQueue.main.async { + self.mapView.pointOfInterestFilter = enabled ? .includingAll : .excludingAll + } + } + + func setCompass(_ enabled: Bool) { + DispatchQueue.main.async { self.mapView.showsCompass = enabled } + } + + func setScale(_ enabled: Bool) { + DispatchQueue.main.async { self.mapView.showsScale = enabled } + } + + func setColorScheme(_ scheme: String) { + DispatchQueue.main.async { + self.mapView.overrideUserInterfaceStyle = Map.userInterfaceStyle(from: scheme) + } + } + + /// Toggle user gestures; nil leaves that gesture unchanged. + func setGestures(scroll: Bool?, zoom: Bool?, rotate: Bool?, pitch: Bool?) { + DispatchQueue.main.async { + if let scroll = scroll { self.mapView.isScrollEnabled = scroll } + if let zoom = zoom { self.mapView.isZoomEnabled = zoom } + if let rotate = rotate { self.mapView.isRotateEnabled = rotate } + if let pitch = pitch { self.mapView.isPitchEnabled = pitch } + } + } + + func setPadding(_ insets: UIEdgeInsets) { + DispatchQueue.main.async { + self.mapView.layoutMargins = insets + self.contentInsets = insets + } + } + + static func mapType(from string: String) -> MKMapType { + switch string.lowercased() { + case "satellite": return .satellite + case "hybrid": return .hybrid + case "satelliteflyover": return .satelliteFlyover + case "hybridflyover": return .hybridFlyover + case "mutedstandard": return .mutedStandard + default: return .standard + } + } + + /// Inverse of `mapType(from:)` - the JS `MapType` string for an `MKMapType`. + static func mapTypeString(from type: MKMapType) -> String { + switch type { + case .satellite: return "satellite" + case .hybrid: return "hybrid" + case .satelliteFlyover: return "satelliteFlyover" + case .hybridFlyover: return "hybridFlyover" + case .mutedStandard: return "mutedStandard" + default: return "standard" + } + } + + /// Maps a `colorScheme` string to a `UIUserInterfaceStyle`; anything other + /// than "light"/"dark" follows the system. Pure, so it can be unit-tested. + static func userInterfaceStyle(from string: String) -> UIUserInterfaceStyle { + switch string.lowercased() { + case "light": return .light + case "dark": return .dark + default: return .unspecified + } + } +} + +// MARK: - Plugin: appearance / gestures / padding bridge + +extension NativeMapPlugin { + + @objc func setMapType(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let type = call.getString("mapType") else { + call.reject("mapType is required", PluginError.invalidArgument) + return + } + map.setMapType(type) + call.resolve() + } + + @objc func getMapType(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + runOnMainSync { + let mapType = map.currentMapType() + call.resolve(["mapType": mapType, "type": mapType]) + } + } + + @objc func enableCurrentLocation(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setCurrentLocation(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setTrafficEnabled(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setTraffic(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setPointsOfInterestEnabled(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setPointsOfInterest(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setCompassEnabled(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setCompass(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setScaleEnabled(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setScale(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setColorScheme(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setColorScheme(call.getString("colorScheme") ?? "default") + call.resolve() + } + + @objc func setGestures(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + let gestures = call.getObject("gestures") ?? [:] + map.setGestures( + scroll: gestures["scroll"] as? Bool, + zoom: gestures["zoom"] as? Bool, + rotate: gestures["rotate"] as? Bool, + pitch: gestures["pitch"] as? Bool + ) + call.resolve() + } + + @objc func setPadding(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setPadding(NativeMapConfig.parsePadding(call.getObject("padding"))) + call.resolve() + } +} diff --git a/ios/Sources/NativeMapPlugin/AppleMapMarker.swift b/ios/Sources/NativeMapPlugin/AppleMapMarker.swift new file mode 100644 index 0000000..4a37286 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/AppleMapMarker.swift @@ -0,0 +1,94 @@ +import Foundation +import MapKit +import Capacitor + +// MARK: - Annotation +// +// Split out of CapacitorNativeMap.swift to keep the core Map type within +// SwiftLint's length budget. The marker model plus the payload parsing that is +// specific to a marker (icon fields, anchor), so makeMarker/updateMarkers on the +// Map stay small. + +/// A map pin carrying a stable id echoed back to JS on tap. The id is generated +/// unless the caller supplied one in the marker payload. +class NativeMapMarker: MKPointAnnotation { + var markerId: String = UUID().uuidString + var iconUrl: String? + var iconSize: CGSize? + /// Where the icon image is pinned to the coordinate, in 0..1 fractions from + /// the image's top-left. `nil` keeps the bottom-centre default (a teardrop + /// pin's tip); `(0.5, 0.5)` centres the image, as a dot wants. + var iconAnchor: CGPoint? + /// When true the pin can be dragged (press-and-hold, then move), emitting the + /// `onMarkerDrag*` events. Dragging is driven by a long-press recognizer on the + /// map rather than `MKAnnotationView.isDraggable`, so intermediate coordinates + /// stream to JS instead of only the drop point. + var isDraggable = false + /// Marker opacity 0..1, applied to the annotation view's `alpha`. Defaults to 1. + var opacity: CGFloat = 1 + /// Tint for the default MapKit pin (`MKMarkerAnnotationView.markerTintColor`). + /// Ignored for image markers, which carry their own colors. nil = MapKit default. + var tintColor: UIColor? + /// Draw order relative to other markers (higher draws on top), mapped to + /// `MKAnnotationView.zPriority`. Defaults to 0. + var zIndex: Double = 0 + + /// The `MKAnnotationView.centerOffset` that lands this marker's anchor point on + /// its coordinate, for an icon rendered at `imageSize`. A view is centred on + /// its coordinate by default, so the offset moves the view until the anchor - + /// not the centre - sits on the point. The bottom-centre default `(0.5, 1)` + /// gives `(0, -height/2)`, the classic teardrop-tip anchoring. + func centerOffset(for imageSize: CGSize) -> CGPoint { + let anchor = iconAnchor ?? CGPoint(x: 0.5, y: 1.0) + return CGPoint(x: imageSize.width * (0.5 - anchor.x), + y: imageSize.height * (0.5 - anchor.y)) + } + + /// Applies whichever icon fields (`iconUrl`, `iconSize`, `iconAnchor`) are + /// present in an update payload, returning whether any changed - the caller + /// re-renders the annotation when so. A present `iconAnchor` of `null` resets + /// to the bottom-centre default. Mirrors the parsing in `Map.makeMarker`. + func applyIconUpdates(from obj: JSObject) -> Bool { + var changed = false + if obj.keys.contains("iconUrl") { + iconUrl = obj["iconUrl"] as? String + changed = true + } + if let size = NativeMapMarker.parseSize(obj["iconSize"]) { + iconSize = size + changed = true + } + if obj.keys.contains("iconAnchor") { + iconAnchor = NativeMapMarker.parseAnchor(obj["iconAnchor"]) + changed = true + } + return changed + } + + /// A `{ width, height }` payload as a `CGSize`, or nil when absent/malformed. + static func parseSize(_ value: Any?) -> CGSize? { + guard let obj = value as? JSObject, + let width = obj["width"] as? Double, + let height = obj["height"] as? Double else { return nil } + return CGSize(width: width, height: height) + } + + /// A `{ x, y }` anchor payload as a `CGPoint`, or nil when absent/malformed. + static func parseAnchor(_ value: Any?) -> CGPoint? { + guard let obj = value as? JSObject, + let x = obj["x"] as? Double, + let y = obj["y"] as? Double else { return nil } + return CGPoint(x: x, y: y) + } + + /// A `{ r, g, b, a }` payload (each channel 0..255, `a` optional) as a + /// `UIColor`, or nil when absent/malformed. + static func parseTintColor(_ value: Any?) -> UIColor? { + guard let obj = value as? JSObject, + let red = obj["r"] as? Double, + let green = obj["g"] as? Double, + let blue = obj["b"] as? Double else { return nil } + let alpha = obj["a"] as? Double ?? 255 + return UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha / 255) + } +} diff --git a/ios/Sources/NativeMapPlugin/Callout.swift b/ios/Sources/NativeMapPlugin/Callout.swift new file mode 100644 index 0000000..5e11418 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Callout.swift @@ -0,0 +1,133 @@ +import Foundation +import MapKit +import UIKit + +// MARK: - Custom info window (callout) +// +// MapKit's native callout bubble does not render when the MKMapView is composited +// inside the web view (the way @capacitor/google-maps mounts it). So instead of +// relying on `canShowCallout`, we draw our own bubble as a plain subview of the +// map and keep it glued to the pin with a display link. This renders reliably, +// exactly like the annotation pins do. + +/// A rounded info-window bubble showing a marker's title and optional snippet. +/// Interactive: a tap on it emits `onInfoWindowClick` (a tap elsewhere on the map +/// dismisses it). +final class CalloutBubble: UIView { + /// Widest the bubble grows before text wraps. + private static let maxWidth: CGFloat = 260 + + init(title: String, subtitle: String?) { + super.init(frame: .zero) + backgroundColor = .secondarySystemBackground + layer.cornerRadius = 10 + layer.shadowColor = UIColor.black.cgColor + layer.shadowOpacity = 0.18 + layer.shadowRadius = 5 + layer.shadowOffset = CGSize(width: 0, height: 2) + layer.masksToBounds = false + + let stack = UIStackView() + stack.axis = .vertical + stack.spacing = 2 + stack.isLayoutMarginsRelativeArrangement = true + stack.layoutMargins = UIEdgeInsets(top: 8, left: 12, bottom: 8, right: 12) + stack.translatesAutoresizingMaskIntoConstraints = false + addSubview(stack) + NSLayoutConstraint.activate([ + stack.topAnchor.constraint(equalTo: topAnchor), + stack.bottomAnchor.constraint(equalTo: bottomAnchor), + stack.leadingAnchor.constraint(equalTo: leadingAnchor), + stack.trailingAnchor.constraint(equalTo: trailingAnchor), + widthAnchor.constraint(lessThanOrEqualToConstant: CalloutBubble.maxWidth) + ]) + + let titleLabel = UILabel() + titleLabel.text = title + titleLabel.font = .systemFont(ofSize: 15, weight: .semibold) + titleLabel.textColor = .label + titleLabel.numberOfLines = 2 + stack.addArrangedSubview(titleLabel) + + if let subtitle = subtitle, !subtitle.isEmpty { + let subtitleLabel = UILabel() + subtitleLabel.text = subtitle + subtitleLabel.font = .systemFont(ofSize: 13) + subtitleLabel.textColor = .secondaryLabel + subtitleLabel.numberOfLines = 2 + stack.addArrangedSubview(subtitleLabel) + } + } + + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + /// The natural size for the current content (respecting `maxWidth`). + func fittingSize() -> CGSize { + systemLayoutSizeFitting( + UIView.layoutFittingCompressedSize, + withHorizontalFittingPriority: .fittingSizeLevel, + verticalFittingPriority: .fittingSizeLevel + ) + } +} + +extension Map { + /// Approximate height of a default MKMarkerAnnotationView, so the bubble sits + /// just above the balloon rather than over it. + private static let markerPinHeight: CGFloat = 46 + + /// Show the info window for `marker` (replacing any current one) and start the + /// display link that keeps it glued to the pin. Must run on the main thread. + func showCallout(for marker: NativeMapMarker) { + dismissCallout() + guard let title = marker.title, !title.isEmpty else { return } + + let bubble = CalloutBubble(title: title, subtitle: marker.subtitle) + bubble.bounds = CGRect(origin: .zero, size: bubble.fittingSize()) + let tap = UITapGestureRecognizer(target: self, action: #selector(handleCalloutTap)) + bubble.addGestureRecognizer(tap) + calloutView = bubble + calloutMarker = marker + mapView.addSubview(bubble) + repositionCallout() + + let link = CADisplayLink(target: self, selector: #selector(handleCalloutTick)) + link.add(to: .main, forMode: .common) + calloutDisplayLink = link + } + + @objc private func handleCalloutTick() { + repositionCallout() + } + + /// Emit `onInfoWindowClick` for the marker whose bubble is showing. + @objc private func handleCalloutTap() { + guard let marker = calloutMarker else { return } + delegate?.notifyListeners("onInfoWindowClick", data: [ + "mapId": id, + "markerId": marker.markerId, + "latitude": marker.coordinate.latitude, + "longitude": marker.coordinate.longitude, + "title": marker.title ?? "" + ]) + } + + /// Place the bubble centered horizontally over the pin and just above it. + func repositionCallout() { + guard let bubble = calloutView, let marker = calloutMarker else { return } + let anchor = mapView.convert(marker.coordinate, toPointTo: mapView) + bubble.center = CGPoint( + x: anchor.x, + y: anchor.y - Map.markerPinHeight - 6 - bubble.bounds.height / 2 + ) + } + + /// Remove the info window and stop its display link. Must run on the main thread. + func dismissCallout() { + calloutDisplayLink?.invalidate() + calloutDisplayLink = nil + calloutView?.removeFromSuperview() + calloutView = nil + calloutMarker = nil + } +} diff --git a/ios/Sources/NativeMapPlugin/CameraEvents.swift b/ios/Sources/NativeMapPlugin/CameraEvents.swift new file mode 100644 index 0000000..049cd86 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/CameraEvents.swift @@ -0,0 +1,92 @@ +import Foundation +import MapKit +import Capacitor + +// MARK: - Plugin: camera region-change events +// +// Split out of NativeMapPlugin.swift so the plugin type stays within +// SwiftLint's body-length budget. These are still MKMapViewDelegate callbacks +// on the same plugin type, relaying the map's region changes to JS +// (`onCameraMoveStarted` when a move begins, `onCameraIdle` when it settles) and +// enforcing the min/max-zoom bounce. + +extension NativeMapPlugin { + + public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { + guard let map = findMap(for: mapView) else { return } + // The self-triggered min/max-zoom bounce (see regionDidChange) is not a + // camera-move the caller started, so don't report it. + if map.isAdjustingRegion { return } + notifyListeners("onCameraMoveStarted", data: [ + "mapId": map.id, + "isGesture": Self.regionChangeIsGesture(mapView) + ]) + } + + /// Compares two regions for equality within a small epsilon (used to detect + /// no-op zoom clamps that would otherwise leave `isAdjustingRegion` set). + static func regionsAreEqual(_ lhs: MKCoordinateRegion, _ rhs: MKCoordinateRegion) -> Bool { + let epsilon = 1e-6 + return abs(lhs.center.latitude - rhs.center.latitude) < epsilon + && abs(lhs.center.longitude - rhs.center.longitude) < epsilon + && abs(lhs.span.latitudeDelta - rhs.span.latitudeDelta) < epsilon + && abs(lhs.span.longitudeDelta - rhs.span.longitudeDelta) < epsilon + } + + /// Whether an in-flight region change originates from a user gesture rather + /// than a programmatic move. MapKit hangs its pan/pinch/rotate recognizers on + /// the map's first subview; if any is mid-recognition the move is a gesture. + static func regionChangeIsGesture(_ mapView: MKMapView) -> Bool { + guard let gestureHost = mapView.subviews.first, + let recognizers = gestureHost.gestureRecognizers else { return false } + return recognizers.contains { recognizer in + switch recognizer.state { + case .began, .changed, .ended: + return true + default: + return false + } + } + } + + public func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) { + guard let map = findMap(for: mapView) else { return } + + if map.isAdjustingRegion { + // This callback is the settle after our own clamp - clear the guard + // and report the corrected camera. + map.isAdjustingRegion = false + } else if let minZoom = map.config.minZoom, map.currentZoom() < minZoom - 0.01 { + // The user zoomed out past the floor; bounce back to it. + let before = mapView.region + map.isAdjustingRegion = true + map.setCameraInternal(coordinate: mapView.centerCoordinate, zoom: minZoom, animate: true) + if Self.regionsAreEqual(before, mapView.region) { + map.isAdjustingRegion = false + } + return + } else if let maxZoom = map.config.maxZoom, map.currentZoom() > maxZoom + 0.01 { + // The user zoomed in past the ceiling; bounce back to it. + let before = mapView.region + map.isAdjustingRegion = true + map.setCameraInternal(coordinate: mapView.centerCoordinate, zoom: maxZoom, animate: true) + if Self.regionsAreEqual(before, mapView.region) { + map.isAdjustingRegion = false + } + return + } + + let center = mapView.centerCoordinate + let payload: PluginCallResultData = [ + "mapId": map.id, + "latitude": center.latitude, + "longitude": center.longitude, + "zoom": map.currentZoom(), + "bearing": mapView.camera.heading, + "tilt": Double(mapView.camera.pitch), + "bounds": map.boundsPayload() + ] + notifyListeners("onBoundsChanged", data: payload) + notifyListeners("onCameraIdle", data: payload) + } +} diff --git a/ios/Sources/NativeMapPlugin/CapacitorAppleMaps.swift b/ios/Sources/NativeMapPlugin/CapacitorAppleMaps.swift new file mode 100644 index 0000000..4b86a6b --- /dev/null +++ b/ios/Sources/NativeMapPlugin/CapacitorAppleMaps.swift @@ -0,0 +1,532 @@ +import Foundation +import MapKit +import Capacitor +import WebKit +import UIKit + +// MARK: - Errors + +enum NativeMapsError: Error, LocalizedError { + case invalidArguments(String) + + var errorDescription: String? { + switch self { + case .invalidArguments(let message): + return message + } + } +} + +// MARK: - Zoom <-> region conversion +// +// MapKit has no notion of a Google-style integer zoom; it works in region +// spans. These helpers convert between the two using the web-mercator tile +// relationship: the whole world (360ยฐ) is `256 * 2^zoom` points wide, so a +// viewport `widthPoints` points wide spans `360 * widthPoints / (256 * 2^zoom)` +// degrees of longitude. The round trip is approximate - MapKit adjusts spans to +// the view's aspect ratio - which is fine for the host app's radius estimates. + +func zoomToLongitudeDelta(_ zoom: Double, widthPoints: Double) -> Double { + let width = widthPoints > 0 ? widthPoints : 256.0 + return 360.0 * width / (256.0 * pow(2.0, zoom)) +} + +func longitudeDeltaToZoom(_ delta: Double, widthPoints: Double) -> Double { + let width = widthPoints > 0 ? widthPoints : 256.0 + let safeDelta = delta > 0 ? delta : 0.0001 + return log2(360.0 * width / (256.0 * safeDelta)) +} + +/// Clamps a Google-style zoom to an optional `[minZoom, maxZoom]` range. A nil +/// bound means unbounded on that side; if `minZoom > maxZoom` the ceiling wins. +/// Pure so the clamp behavior can be unit-tested without an `MKMapView`. +func clampZoom(_ zoom: Double, minZoom: Double?, maxZoom: Double?) -> Double { + var result = max(zoom, minZoom ?? -Double.greatestFiniteMagnitude) + result = min(result, maxZoom ?? Double.greatestFiniteMagnitude) + return result +} + +/// The smallest `MKMapRect` containing both corners, regardless of their relative +/// orientation (latitude grows north but `MKMapPoint.y` grows south). Pure so the +/// `fitBounds` framing math can be unit-tested without an `MKMapView`. +func boundingMapRect(southwest: CLLocationCoordinate2D, northeast: CLLocationCoordinate2D) -> MKMapRect { + let swPoint = MKMapPoint(southwest) + let nePoint = MKMapPoint(northeast) + return MKMapRect( + x: min(swPoint.x, nePoint.x), + y: min(swPoint.y, nePoint.y), + width: abs(swPoint.x - nePoint.x), + height: abs(swPoint.y - nePoint.y) + ) +} + +/// The corner coordinates of a region, used to report the visible bounds to JS. +/// Pure function so it can be unit-tested without an `MKMapView`. +func regionCorners(center: CLLocationCoordinate2D, span: MKCoordinateSpan) +-> (southwest: CLLocationCoordinate2D, northeast: CLLocationCoordinate2D) { + let southwest = CLLocationCoordinate2D( + latitude: center.latitude - span.latitudeDelta / 2, + longitude: center.longitude - span.longitudeDelta / 2 + ) + let northeast = CLLocationCoordinate2D( + latitude: center.latitude + span.latitudeDelta / 2, + longitude: center.longitude + span.longitudeDelta / 2 + ) + return (southwest, northeast) +} + +// MARK: - Annotation +// +// The pin model, `NativeMapMarker`, lives in NativeMapMarker.swift (kept there so +// this file stays within SwiftLint's length budget). + +/// Stroke/fill styling for an overlay, resolved from the JS payload and looked +/// up by the map's `rendererFor` delegate when MapKit asks how to draw it. +struct OverlayStyle { + var strokeColor: UIColor + var lineWidth: CGFloat + var fillColor: UIColor? + /// Dash pattern (on/off point lengths) for the stroke, or nil for a solid line. + var lineDashPattern: [NSNumber]? +} + +// MARK: - Config + +struct NativeMapConfig { + let center: CLLocationCoordinate2D + let zoom: Double + let minZoom: Double? + let maxZoom: Double? + var x: Double + var y: Double + var width: Double + var height: Double + let clustering: Bool + let mapType: String + let showInfoWindows: Bool + let showsTraffic: Bool + let showsPointsOfInterest: Bool + let showsCompass: Bool + let showsScale: Bool + let showsBuildings: Bool + let colorScheme: String + let scrollEnabled: Bool + let zoomEnabled: Bool + let rotateEnabled: Bool + let pitchEnabled: Bool + let padding: UIEdgeInsets + + init(fromJSObject obj: JSObject) throws { + guard let centerObj = obj["center"] as? JSObject, + let lat = centerObj["lat"] as? Double, + let lng = centerObj["lng"] as? Double else { + throw NativeMapsError.invalidArguments("config.center is missing or malformed") + } + self.center = CLLocationCoordinate2D(latitude: lat, longitude: lng) + self.zoom = obj["zoom"] as? Double ?? 12 + self.minZoom = obj["minZoom"] as? Double + self.maxZoom = obj["maxZoom"] as? Double + self.width = obj["width"] as? Double ?? 0 + self.height = obj["height"] as? Double ?? 0 + self.x = obj["x"] as? Double ?? 0 + self.y = obj["y"] as? Double ?? 0 + self.clustering = obj["clustering"] as? Bool ?? false + self.mapType = obj["mapType"] as? String ?? "standard" + self.showInfoWindows = obj["showInfoWindows"] as? Bool ?? false + // Defaults mirror MapKit's own (traffic/scale off, POI/compass on). + self.showsTraffic = obj["showsTraffic"] as? Bool ?? false + self.showsPointsOfInterest = obj["showsPointsOfInterest"] as? Bool ?? true + self.showsCompass = obj["showsCompass"] as? Bool ?? true + self.showsScale = obj["showsScale"] as? Bool ?? false + self.showsBuildings = obj["showsBuildings"] as? Bool ?? true + self.colorScheme = obj["colorScheme"] as? String ?? "default" + let gestures = obj["gestures"] as? JSObject ?? [:] + self.scrollEnabled = gestures["scroll"] as? Bool ?? true + self.zoomEnabled = gestures["zoom"] as? Bool ?? true + self.rotateEnabled = gestures["rotate"] as? Bool ?? true + self.pitchEnabled = gestures["pitch"] as? Bool ?? true + self.padding = NativeMapConfig.parsePadding(obj["padding"]) + } +} + +// MARK: - Map + +/// Owns one native `MKMapView` and mounts it into the WKWebView's view tree at +/// the bound element's location, mirroring the compositing approach of +/// `@capacitor/google-maps`. +public class Map: NSObject, UIGestureRecognizerDelegate { + static let mapTag = 99999 + /// Name given to the marker-drag long-press recognizer, so the gesture + /// delegate can let it receive touches on a marker while the map-surface + /// recognizers decline them (letting MapKit select the pin / show its callout). + static let markerDragGestureName = "capacitorNativeMapMarkerDrag" + + let id: String + var config: NativeMapConfig + let mapView: MKMapView + var toBack: Bool = false + var isMapVisible: Bool = true + + var markers: [String: NativeMapMarker] = [:] + /// Overlays keyed by the id handed back to JS, for removal. + var overlays: [String: MKOverlay] = [:] + /// Overlay styling keyed by the overlay instance, read back in `rendererFor`. + var overlayStyles: [ObjectIdentifier: OverlayStyle] = [:] + private(set) var clusteringEnabled = false + /// Best-effort lower bound on the total marker count before clustering applies. + /// MapKit has no per-cluster minimum, so this gates clustering on the number of + /// markers present when each annotation view is built. See enableClustering. + var clusterMinSize = 2 + /// Whether an annotation view should cluster right now, given the toggle and count. + var shouldCluster: Bool { clusteringEnabled && markers.count >= clusterMinSize } + + /// Guards against a feedback loop when we programmatically clamp the region + /// back to the minimum zoom inside `regionDidChange`. + var isAdjustingRegion = false + + /// The custom info-window bubble currently shown (a plain subview of the map, + /// since MapKit's own callout doesn't render when the map is composited inside + /// the web view), the marker it points at, and a display link that keeps it + /// glued to the pin as the map pans/zooms. See Callout.swift. + var calloutView: UIView? + var calloutMarker: NativeMapMarker? + var calloutDisplayLink: CADisplayLink? + + /// The marker currently being dragged (via the map's long-press recognizer), + /// or nil when no drag is in progress. + var draggingMarker: NativeMapMarker? + /// The map's `isScrollEnabled` value before a drag disabled it, restored when + /// the drag ends. + var scrollWasEnabledBeforeDrag = true + + /// Edge insets from `setPadding`, added to the `fitBounds` framing so it + /// respects the padding the caller asked for. + var contentInsets: UIEdgeInsets = .zero + + /// Retains the in-flight `MKMapSnapshotter` for the duration of `takeSnapshot`. + var pendingSnapshotter: MKMapSnapshotter? + var snapshotRequestId: UInt = 0 + + /// The user-tracking button (recenter/follow control) when shown, so it can be + /// removed again. See setUserTrackingButton in MapControls.swift. + var userTrackingButton: MKUserTrackingButton? + + weak var delegate: NativeMapPlugin? + var targetView: UIView? + /// Decoded marker icons keyed by their url/asset string. `NSCache` bounds the + /// footprint and evicts under memory pressure, unlike a plain dictionary that + /// would grow without limit as icons come and go. Read/written in MarkerIcons. + let iconCache: NSCache = { + let cache = NSCache() + cache.countLimit = 100 + return cache + }() + /// Remote icon urls with a download in flight, so a re-render (e.g. a + /// clustering toggle re-adding every annotation) doesn't start a duplicate. + var inFlightIconURLs: Set = [] + var pendingIconMarkers: [String: [NativeMapMarker]] = [:] + /// Remote icon urls that returned a response but no usable image - a permanent + /// miss we must not retry on every re-render (a transport error is left out so + /// a later render can retry it). + var failedIconURLs: Set = [] + + init(id: String, config: NativeMapConfig, delegate: NativeMapPlugin, toBack: Bool = false) { + self.id = id + self.config = config + self.mapView = MKMapView() + self.delegate = delegate + self.toBack = toBack + super.init() + // Start clustered when the caller asked for it, so markers added later + // cluster on their first render instead of flashing as individual pins. + self.clusteringEnabled = config.clustering + self.render() + } + + private func render() { + DispatchQueue.main.async { + self.mapView.delegate = self.delegate + self.mapView.showsUserLocation = false + self.mapView.mapType = Map.mapType(from: self.config.mapType) + self.applyAppearance(self.config) + self.mapView.isScrollEnabled = self.config.scrollEnabled + self.mapView.isZoomEnabled = self.config.zoomEnabled + self.mapView.isRotateEnabled = self.config.rotateEnabled + self.mapView.isPitchEnabled = self.config.pitchEnabled + self.mapView.layoutMargins = self.config.padding + self.contentInsets = self.config.padding + self.mapView.frame = CGRect(x: self.config.x, y: self.config.y, width: self.config.width, height: self.config.height) + self.setCameraInternal(coordinate: self.config.center, zoom: self.config.zoom, animate: false) + + // Emit onMapClick for taps that don't land on a marker. cancelsTouchesInView + // stays false and the delegate allows simultaneous recognition so this + // never swallows MapKit's own pan/zoom or marker-selection gestures. + let tap = UITapGestureRecognizer(target: self, action: #selector(self.handleMapTap(_:))) + tap.cancelsTouchesInView = false + tap.delegate = self + self.mapView.addGestureRecognizer(tap) + + // Emit onMapLongClick for a long-press that doesn't land on a marker. + let longPress = UILongPressGestureRecognizer(target: self, action: #selector(self.handleMapLongPress(_:))) + longPress.cancelsTouchesInView = false + longPress.delegate = self + self.mapView.addGestureRecognizer(longPress) + + // Drives marker dragging: a long-press that lands on a draggable pin + // picks it up; subsequent movement streams onMarkerDrag. Presses that + // miss a draggable pin are ignored here and handled by the long-press + // recognizer above. + let markerDrag = UILongPressGestureRecognizer(target: self, action: #selector(self.handleMarkerDrag(_:))) + markerDrag.cancelsTouchesInView = false + markerDrag.name = Map.markerDragGestureName + markerDrag.delegate = self + self.mapView.addGestureRecognizer(markerDrag) + + if self.toBack, let webView = self.delegate?.bridge?.webView { + self.mapView.frame = CGRect( + x: self.config.x, + y: self.config.y, + width: self.config.width, + height: self.config.height + ) + self.mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + webView.addSubview(self.mapView) + webView.sendSubviewToBack(self.mapView) + ToBackCompositor.acquireTransparentWebView(webView) + self.targetView = webView + self.mapView.isHidden = !self.isMapVisible + } else { + self.targetView = self.getTargetContainer(refWidth: self.config.width, refHeight: self.config.height) + if let target = self.targetView { + target.tag = Map.mapTag + target.removeAllSubview() + self.mapView.frame = target.bounds + self.mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + target.addSubview(self.mapView) + } else { + // The mount depends on finding WebKit's private child scroll view + // for the map element (see getTargetContainer). If that ever fails + // again on a future iOS, the map is silently never added to the + // view tree - a blank, touch-dead map with no error. Leave a + // breadcrumb so that failure is diagnosable from the console + // instead of a mystery. onMapReady still fires below, as before. + CAPLog.print("[NativeMaps] getTargetContainer found no container to mount into " + + "(ref=\(self.config.width)x\(self.config.height)); map will not render. " + + "Likely a WebKit view-tree change - inspect the WKWebView's scroll views.") + } + } + + self.delegate?.notifyListeners("onMapReady", data: ["mapId": self.id]) + } + } + + func destroy() { + DispatchQueue.main.async { + self.dismissCallout() + let webView = self.delegate?.bridge?.webView + self.mapView.removeFromSuperview() + self.mapView.delegate = nil + self.targetView?.tag = 0 + if self.toBack { + ToBackCompositor.releaseTransparentWebView(webView) + } + } + } + + func updateLayout(bounds: CGRect) { + config.x = bounds.origin.x + config.y = bounds.origin.y + config.width = bounds.size.width + config.height = bounds.size.height + runOnMainSync { + if self.toBack { + self.mapView.frame = bounds + } else { + self.mapView.frame = CGRect(origin: .zero, size: bounds.size) + } + } + } + + func setMapVisible(_ visible: Bool) { + isMapVisible = visible + runOnMainSync { + self.mapView.isHidden = !visible + } + } + + // MARK: Camera + + /// Must be called on the main thread. `bearing` (heading, degrees clockwise + /// from north) and `pitch` (tilt, degrees from top-down) are applied on top of + /// the region/center change when supplied; nil leaves that axis unchanged. + func setCameraInternal( + coordinate: CLLocationCoordinate2D?, + zoom: Double?, + animate: Bool, + bearing: Double? = nil, + pitch: Double? = nil + ) { + let center = coordinate ?? mapView.centerCoordinate + + if let zoom = zoom { + // Enforce the zoom range: a smaller zoom means a wider span (minZoom is the + // zoom-out floor), a larger zoom a tighter one (maxZoom is the zoom-in ceiling). + let clampedZoom = clampZoom(zoom, minZoom: config.minZoom, maxZoom: config.maxZoom) + + let width = Double(mapView.bounds.width > 0 ? mapView.bounds.width : UIScreen.main.bounds.width) + let height = Double(mapView.bounds.height > 0 ? mapView.bounds.height : UIScreen.main.bounds.height) + let lonDelta = min(zoomToLongitudeDelta(clampedZoom, widthPoints: width), 360.0) + let latDelta = min(lonDelta * (height / max(width, 1)), 180.0) + let span = MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lonDelta) + let region = mapView.regionThatFits(MKCoordinateRegion(center: center, span: span)) + mapView.setRegion(region, animated: animate) + } else if coordinate != nil { + mapView.setCenter(center, animated: animate) + } + + // Rotation/tilt ride on top of the region change, preserving the resulting + // center and distance. Reading the camera after the region set gives the + // new center/zoom to build on. + if bearing != nil || pitch != nil, let camera = mapView.camera.copy() as? MKMapCamera { + if let bearing = bearing { camera.heading = bearing } + if let pitch = pitch { camera.pitch = CGFloat(max(0, pitch)) } + mapView.setCamera(camera, animated: animate) + } + } + + /// Frame `southwest`..`northeast` in the viewport, inset by `padding` points. + func fitBounds(southwest: CLLocationCoordinate2D, northeast: CLLocationCoordinate2D, padding: Double, animate: Bool) { + let rect = boundingMapRect(southwest: southwest, northeast: northeast) + runOnMainSync { + // Combine the per-call padding with any standing content insets (setPadding). + let inset = UIEdgeInsets( + top: padding + self.contentInsets.top, + left: padding + self.contentInsets.left, + bottom: padding + self.contentInsets.bottom, + right: padding + self.contentInsets.right + ) + self.mapView.setVisibleMapRect(rect, edgePadding: inset, animated: animate) + } + } + + // MARK: Markers + + func addMarkers(_ markerObjs: [JSObject]) -> [String] { + var ids: [String] = [] + runOnMainSync { + var toAdd: [NativeMapMarker] = [] + for obj in markerObjs { + guard let marker = Map.makeMarker(from: obj) else { continue } + self.markers[marker.markerId] = marker + toAdd.append(marker) + ids.append(marker.markerId) + } + self.mapView.addAnnotations(toAdd) + } + return ids + } + + /// Builds an annotation from a marker payload, or nil if the coordinate is + /// missing/malformed. Pure (no `MKMapView`), so it can be unit-tested. + static func makeMarker(from obj: JSObject) -> NativeMapMarker? { + guard let coordObj = obj["coordinate"] as? JSObject, + let lat = coordObj["lat"] as? Double, + let lng = coordObj["lng"] as? Double else { return nil } + let marker = NativeMapMarker() + if let customId = obj["markerId"] as? String, !customId.isEmpty { + marker.markerId = customId + } + marker.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + marker.title = obj["title"] as? String + marker.subtitle = obj["snippet"] as? String + marker.iconUrl = obj["iconUrl"] as? String + marker.isDraggable = obj["draggable"] as? Bool ?? false + marker.iconSize = NativeMapMarker.parseSize(obj["iconSize"]) + marker.iconAnchor = NativeMapMarker.parseAnchor(obj["iconAnchor"]) + if let opacity = obj["opacity"] as? Double { marker.opacity = CGFloat(opacity) } + marker.tintColor = NativeMapMarker.parseTintColor(obj["tintColor"]) + if let zIndex = obj["zIndex"] as? Double { marker.zIndex = zIndex } + return marker + } + + func removeMarkers(_ ids: [String]) { + runOnMainSync { + var toRemove: [NativeMapMarker] = [] + for id in ids { + if let marker = self.markers[id] { + toRemove.append(marker) + self.markers.removeValue(forKey: id) + } + } + self.mapView.removeAnnotations(toRemove) + } + } + + /// Apply partial changes to existing markers. A moved marker animates to its + /// new coordinate; an icon change re-adds the annotation so `viewFor` reruns. + func updateMarkers(_ objs: [JSObject]) { + runOnMainSync { + var toRefresh: [NativeMapMarker] = [] + for obj in objs { + guard let markerId = obj["markerId"] as? String, + let marker = self.markers[markerId] else { continue } + + if let coordObj = obj["coordinate"] as? JSObject, + let lat = coordObj["lat"] as? Double, + let lng = coordObj["lng"] as? Double { + UIView.animate(withDuration: 0.25) { + marker.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + } + } + if obj.keys.contains("title") { + marker.title = obj["title"] as? String + } + if obj.keys.contains("snippet") { + marker.subtitle = obj["snippet"] as? String + } + if obj.keys.contains("draggable") { + // The drag gate reads this flag live, so no view rebuild is + // needed to enable/disable dragging. + marker.isDraggable = obj["draggable"] as? Bool ?? false + } + // Opacity / tint / z-order are applied to the live annotation view + // in place (see applyLiveStyleUpdates), so they don't need a + // re-render (which would flash the pin). + self.applyLiveStyleUpdates(from: obj, to: marker) + // Icon fields (url / size / anchor) re-render the annotation so + // viewFor reapplies the image and centerOffset; the fields above + // mutate in place. See NativeMapMarker.applyIconUpdates. + if marker.applyIconUpdates(from: obj) { toRefresh.append(marker) } + } + if !toRefresh.isEmpty { + self.mapView.removeAnnotations(toRefresh) + self.mapView.addAnnotations(toRefresh) + } + } + } + + func enableClustering(minClusterSize: Int? = nil) { + runOnMainSync { + if let minSize = minClusterSize { self.clusterMinSize = max(1, minSize) } + self.clusteringEnabled = true + self.refreshAnnotations() + } + } + + func disableClustering() { + runOnMainSync { + guard self.clusteringEnabled else { return } + self.clusteringEnabled = false + self.refreshAnnotations() + } + } + + /// Re-add every annotation so `viewFor` reruns and the clustering identifier + /// is applied or cleared. Must be called on the main thread. + private func refreshAnnotations() { + let all = Array(self.markers.values) + self.mapView.removeAnnotations(all) + self.mapView.addAnnotations(all) + } + +} diff --git a/ios/Sources/NativeMapPlugin/CapacitorAppleMapsPlugin.swift b/ios/Sources/NativeMapPlugin/CapacitorAppleMapsPlugin.swift new file mode 100644 index 0000000..4b47502 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/CapacitorAppleMapsPlugin.swift @@ -0,0 +1,350 @@ +// Portions Copyright Katama Labs contributors, licensed under the MIT License. +// Adapted for @capgo/capacitor-native-map (MPL-2.0). +import Foundation +import Capacitor +import MapKit +import UIKit + +/** + * Bridges the JS API to native MapKit and relays map events back to JS. + * See CapacitorNativeMap.swift for the per-map implementation. + */ +@objc(NativeMapPlugin) +public class NativeMapPlugin: CAPPlugin, CAPBridgedPlugin, MKMapViewDelegate { + public let identifier = "NativeMapPlugin" + public let jsName = "NativeMap" + public let pluginMethods: [CAPPluginMethod] = [ + CAPPluginMethod(name: "create", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "destroy", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setCamera", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getMapBounds", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getCameraPosition", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "fitBounds", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addMarkers", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addMarker", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "updateMarkers", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removeMarkers", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removeMarker", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableClustering", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "disableClustering", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addPolylines", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addPolygons", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addCircles", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removeOverlays", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setMapType", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "getMapType", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setUserTrackingMode", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setUserTrackingButtonVisible", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setBuildingsEnabled", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setCameraBoundary", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "selectMarker", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "deselectMarker", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableCurrentLocation", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setTrafficEnabled", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setPointsOfInterestEnabled", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setCompassEnabled", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setScaleEnabled", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setColorScheme", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setGestures", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "setPadding", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "takeSnapshot", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "searchAutocomplete", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "searchPlaces", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "searchResolve", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "reverseGeocode", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "geocode", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "onResize", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "onDisplay", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "onScroll", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableTouch", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "disableTouch", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableIndoorMaps", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableTrafficLayer", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "enableAccessibilityElements", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "addTileOverlay", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removeTileOverlay", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removePolygons", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removeCircles", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "removePolylines", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "mapBoundsContains", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "mapBoundsExtend", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "dispatchMapEvent", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "updateLayout", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "show", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "hide", returnType: CAPPluginReturnPromise) + ] + + var maps = [String: Map]() + fileprivate let mapsLock = NSLock() + + func toBackMap(containing point: CGPoint, in webView: WKWebView) -> Map? { + mapsLock.lock() + let candidates = maps.values.filter { $0.toBack && $0.isMapVisible } + mapsLock.unlock() + let ordered = candidates.sorted { lhs, rhs in + let left = webView.subviews.firstIndex(of: lhs.mapView) ?? -1 + let right = webView.subviews.firstIndex(of: rhs.mapView) ?? -1 + return left > right + } + for map in ordered { + let converted = webView.convert(point, to: map.mapView) + if map.mapView.point(inside: converted, with: nil) { + return map + } + } + return nil + } + private let searchService = SearchService() + private let geocodeService = GeocodeService() + + // MARK: - Location permission + // + // Backing state for the `location` permission alias. The check/request + // implementations and the CLLocationManagerDelegate callback live in + // Permissions.swift. The manager is created lazily on first use so a plugin + // that never touches location never instantiates one. + var locationManager: CLLocationManager? + /// In-flight `requestPermissions` call, held while the system prompt is up. + var pendingPermissionCall: CAPPluginCall? + + // MARK: - App lifecycle + + override public func load() { + WKWebView.registerTouchRouting(plugin: self) + TouchRoutingBridge.install(on: bridge?.webView) + NotificationCenter.default.addObserver( + self, + selector: #selector(handleDidBecomeActive), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + TouchRoutingBridge.uninstall(from: bridge?.webView) + } + + /// After the app returns to the foreground WebKit can rebuild its scroll-view + /// hierarchy, orphaning the native map's touch handling (it still renders but + /// gestures stop working). Re-mount each map into its current container. + @objc private func handleDidBecomeActive() { + mapsLock.lock() + let activeMaps = Array(maps.values) + mapsLock.unlock() + for map in activeMaps { + map.remountIntoContainer() + } + } + + // MARK: - Lifecycle + + @objc func create(_ call: CAPPluginCall) { + guard let id = call.getString("id") else { + call.reject("id is required", PluginError.invalidArgument) + return + } + guard let configObj = call.getObject("config") else { + call.reject("config is required", PluginError.invalidArgument) + return + } + let forceCreate = call.getBool("forceCreate", false) + + do { + let config = try NativeMapConfig(fromJSObject: configObj) + var alreadyExists = false + + runOnMainSync { + self.mapsLock.lock() + if self.maps[id] != nil { + if !forceCreate { + alreadyExists = true + self.mapsLock.unlock() + return + } + self.maps.removeValue(forKey: id)?.destroy() + } + let toBack = call.getBool("toBack", false) + self.maps[id] = Map(id: id, config: config, delegate: self, toBack: toBack) + self.mapsLock.unlock() + } + if alreadyExists { + call.resolve() + return + } + call.resolve() + } catch { + call.reject(error.localizedDescription, PluginError.operationFailed, error) + } + } + + @objc func destroy(_ call: CAPPluginCall) { + guard let id = call.getString("id") else { + call.reject("map not found", PluginError.mapNotFound) + return + } + runOnMainSync { + self.mapsLock.lock() + guard let map = self.maps.removeValue(forKey: id) else { + self.mapsLock.unlock() + call.reject("map not found", PluginError.mapNotFound) + return + } + self.mapsLock.unlock() + map.destroy() + call.resolve() + } + } + + // MARK: - Camera + + @objc func setCamera(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + runOnMainSync { + map.setCamera(from: call.getObject("config") ?? [:]) + } + call.resolve() + } + + @objc func getMapBounds(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + runOnMainSync { + call.resolve(map.boundsPayload()) + } + } + + @objc func getCameraPosition(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + runOnMainSync { + call.resolve(map.cameraPayload()) + } + } + + @objc func fitBounds(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let boundsObj = call.getObject("bounds"), + let swObj = boundsObj["southwest"] as? JSObject, + let swLat = swObj["lat"] as? Double, let swLng = swObj["lng"] as? Double, + let neObj = boundsObj["northeast"] as? JSObject, + let neLat = neObj["lat"] as? Double, let neLng = neObj["lng"] as? Double else { + call.reject("bounds with southwest and northeast is required", PluginError.invalidArgument) + return + } + let padding = call.getDouble("padding") ?? 0 + let animate = call.getBool("animate", true) + map.fitBounds( + southwest: CLLocationCoordinate2D(latitude: swLat, longitude: swLng), + northeast: CLLocationCoordinate2D(latitude: neLat, longitude: neLng), + padding: padding, animate: animate + ) + call.resolve() + } + + // MARK: - Markers + // + // The marker + clustering bridge methods live in MarkerBridge.swift to keep + // this type within SwiftLint's body-length budget. + + // MARK: - Search + + @objc func searchAutocomplete(_ call: CAPPluginCall) { + searchService.autocomplete(call) + } + + @objc func searchPlaces(_ call: CAPPluginCall) { + searchService.places(call) + } + + @objc func searchResolve(_ call: CAPPluginCall) { + searchService.resolve(call) + } + + // MARK: - Geocoding + + @objc func reverseGeocode(_ call: CAPPluginCall) { + geocodeService.reverse(call) + } + + @objc func geocode(_ call: CAPPluginCall) { + geocodeService.forward(call) + } + + // MARK: - MKMapViewDelegate + // + // The region-change delegate methods (onCameraMoveStarted / onCameraIdle and + // the min/max-zoom bounce) live in CameraEvents.swift to keep this type within + // SwiftLint's body-length budget. + + public func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? { + findMap(for: mapView)?.annotationView(for: annotation, in: mapView) + } + + public func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) { + guard let map = findMap(for: mapView) else { return } + + if let cluster = view.annotation as? MKClusterAnnotation { + let items: [JSObject] = cluster.memberAnnotations.compactMap { annotation in + guard let marker = annotation as? NativeMapMarker else { return nil } + return [ + "markerId": marker.markerId, + "latitude": marker.coordinate.latitude, + "longitude": marker.coordinate.longitude, + "title": marker.title ?? "", + "snippet": marker.subtitle ?? "" + ] + } + notifyListeners("onClusterClick", data: [ + "mapId": map.id, + "latitude": cluster.coordinate.latitude, + "longitude": cluster.coordinate.longitude, + "size": cluster.memberAnnotations.count, + "items": items + ]) + map.expandCluster(cluster, in: mapView) + return + } + + if view.annotation is MKUserLocation { + let coordinate = view.annotation?.coordinate ?? mapView.userLocation.coordinate + notifyListeners("onMyLocationClick", data: [ + "mapId": map.id, + "latitude": coordinate.latitude, + "longitude": coordinate.longitude + ]) + mapView.deselectAnnotation(view.annotation, animated: false) + return + } + + guard let marker = view.annotation as? NativeMapMarker else { return } + notifyListeners("onMarkerClick", data: [ + "mapId": map.id, + "markerId": marker.markerId, + "latitude": marker.coordinate.latitude, + "longitude": marker.coordinate.longitude, + "title": marker.title ?? "" + ]) + map.handleMarkerSelection(marker) + } + + // MARK: - Helpers + + func findMap(for mapView: MKMapView) -> Map? { + mapsLock.lock() + defer { mapsLock.unlock() } + return maps.values.first { $0.mapView === mapView } + } +} diff --git a/ios/Sources/NativeMapPlugin/ControlsBridge.swift b/ios/Sources/NativeMapPlugin/ControlsBridge.swift new file mode 100644 index 0000000..78c628f --- /dev/null +++ b/ios/Sources/NativeMapPlugin/ControlsBridge.swift @@ -0,0 +1,84 @@ +import Foundation +import MapKit +import Capacitor + +// MARK: - Plugin: user-tracking / buildings / camera-boundary / selection bridge +// +// Split out of NativeMapPlugin.swift so the plugin type stays within +// SwiftLint's body-length budget. The per-map work lives on `Map` (MapControls.swift). + +extension NativeMapPlugin { + + @objc func setUserTrackingMode(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setUserTrackingMode(call.getString("mode") ?? "none") + call.resolve() + } + + @objc func setUserTrackingButtonVisible(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setUserTrackingButton(call.getBool("visible", true)) + call.resolve() + } + + @objc func setBuildingsEnabled(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setBuildings(call.getBool("enabled", true)) + call.resolve() + } + + @objc func setCameraBoundary(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + // A bounds object restricts panning; omitting it (or null) clears the limit. + if let boundsObj = call.getObject("bounds"), + let swObj = boundsObj["southwest"] as? JSObject, + let swLat = swObj["lat"] as? Double, let swLng = swObj["lng"] as? Double, + let neObj = boundsObj["northeast"] as? JSObject, + let neLat = neObj["lat"] as? Double, let neLng = neObj["lng"] as? Double { + map.setCameraBoundary( + southwest: CLLocationCoordinate2D(latitude: swLat, longitude: swLng), + northeast: CLLocationCoordinate2D(latitude: neLat, longitude: neLng) + ) + } else { + map.setCameraBoundary(southwest: nil, northeast: nil) + } + call.resolve() + } + + @objc func selectMarker(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let markerId = call.getString("markerId") else { + call.reject("markerId is required", PluginError.invalidArgument) + return + } + if map.selectMarker(markerId) { + call.resolve() + } else { + call.reject("marker not found", PluginError.markerNotFound) + } + } + + @objc func deselectMarker(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.deselectMarker() + call.resolve() + } +} diff --git a/ios/Sources/NativeMapPlugin/Geocode.swift b/ios/Sources/NativeMapPlugin/Geocode.swift new file mode 100644 index 0000000..3650fd5 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Geocode.swift @@ -0,0 +1,112 @@ +import Foundation +import CoreLocation +import Contacts +import Capacitor + +/// Collapses a multi-line postal address (as `CNPostalAddressFormatter` writes +/// it) into one comma-separated line, dropping blank lines. Pure so it can be +/// unit-tested. +func singleLineAddress(_ multiline: String) -> String { + return multiline + .components(separatedBy: .newlines) + .map { $0.trimmingCharacters(in: .whitespaces) } + .filter { !$0.isEmpty } + .joined(separator: ", ") +} + +/// Joins whatever address parts exist, skipping nil/empty ones and immediate +/// repeats (a placemark's `name` is often just its locality). Nil when nothing +/// usable remains. Pure so it can be unit-tested. +func joinedAddressParts(_ parts: [String?]) -> String? { + var kept: [String] = [] + for part in parts { + guard let part = part?.trimmingCharacters(in: .whitespaces), !part.isEmpty, part != kept.last else { continue } + kept.append(part) + } + return kept.isEmpty ? nil : kept.joined(separator: ", ") +} + +/// One display line for a placemark. Prefers the locale-aware postal format +/// (which knows, e.g., that a German postal code precedes the city); falls back +/// to the loose parts for places with no postal address, like open water. +private func formattedAddress(for placemark: CLPlacemark) -> String? { + if let postal = placemark.postalAddress { + let line = singleLineAddress(CNPostalAddressFormatter.string(from: postal, style: .mailingAddress)) + if !line.isEmpty { return line } + } + return joinedAddressParts([placemark.name, placemark.locality, placemark.administrativeArea, placemark.country]) +} + +/// The bridge payload for a placemark. Absent fields are omitted, not null. +private func placePayload(for placemark: CLPlacemark) -> [String: Any] { + var payload: [String: Any] = [:] + if let coordinate = placemark.location?.coordinate { + payload["latitude"] = coordinate.latitude + payload["longitude"] = coordinate.longitude + } + let street = [placemark.subThoroughfare, placemark.thoroughfare].compactMap { $0 }.joined(separator: " ") + let fields: [String: String?] = [ + "address": formattedAddress(for: placemark), + "name": placemark.name, + "street": street, + "locality": placemark.locality, + "subLocality": placemark.subLocality, + "administrativeArea": placemark.administrativeArea, + "postalCode": placemark.postalCode, + "country": placemark.country, + "countryCode": placemark.isoCountryCode + ] + for (key, value) in fields { + if let value = value, !value.isEmpty { payload[key] = value } + } + return payload +} + +/// Native geocoding via `CLGeocoder`, in both directions. Needs no API key. +/// +/// Both fail soft, like search: no match, an offline device, or Apple's rate +/// limit resolve an empty object rather than rejecting, because callers show an +/// address as a nicety and must never be blocked by its absence. +class GeocodeService { + private func locale(_ call: CAPPluginCall) -> Locale? { + guard let language = call.getString("language"), !language.isEmpty else { return nil } + return Locale(identifier: language) + } + + func reverse(_ call: CAPPluginCall) { + guard let lat = call.getDouble("latitude"), let lng = call.getDouble("longitude"), + CLLocationCoordinate2DIsValid(CLLocationCoordinate2D(latitude: lat, longitude: lng)) else { + call.resolve([:]) + return + } + // A geocoder runs one request at a time, so each call gets its own; the + // completion handler keeps it alive until it answers. + let geocoder = CLGeocoder() + let location = CLLocation(latitude: lat, longitude: lng) + geocoder.reverseGeocodeLocation(location, preferredLocale: locale(call)) { placemarks, _ in + _ = geocoder + guard let placemark = placemarks?.first else { + call.resolve([:]) + return + } + call.resolve(placePayload(for: placemark)) + } + } + + func forward(_ call: CAPPluginCall) { + let address = (call.getString("address") ?? "").trimmingCharacters(in: .whitespacesAndNewlines) + if address.isEmpty { + call.resolve([:]) + return + } + let geocoder = CLGeocoder() + geocoder.geocodeAddressString(address, in: nil, preferredLocale: locale(call)) { placemarks, _ in + _ = geocoder + guard let placemark = placemarks?.first, placemark.location != nil else { + call.resolve([:]) + return + } + call.resolve(placePayload(for: placemark)) + } + } +} diff --git a/ios/Sources/NativeMapPlugin/GoogleMapsBridge.swift b/ios/Sources/NativeMapPlugin/GoogleMapsBridge.swift new file mode 100644 index 0000000..d98e982 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/GoogleMapsBridge.swift @@ -0,0 +1,153 @@ +// Portions adapted from @capacitor/google-maps (MIT) bridge method names for a unified JS API. +import Foundation +import Capacitor + +extension NativeMapPlugin { + + @objc func enableTouch(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setGestures(scroll: true, zoom: true, rotate: true, pitch: true) + call.resolve() + } + + @objc func disableTouch(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setGestures(scroll: false, zoom: false, rotate: false, pitch: false) + call.resolve() + } + + @objc func enableIndoorMaps(_ call: CAPPluginCall) { + call.resolve() + } + + @objc func enableAccessibilityElements(_ call: CAPPluginCall) { + call.resolve() + } + + @objc func enableTrafficLayer(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + let enabled = call.getBool("enabled", false) + map.setTraffic(enabled) + call.resolve() + } + + @objc func addTileOverlay(_ call: CAPPluginCall) { + call.reject("tile overlays are not supported on iOS", PluginError.unavailable) + } + + @objc func removeTileOverlay(_ call: CAPPluginCall) { + call.reject("tile overlays are not supported on iOS", PluginError.unavailable) + } + + @objc func removePolygons(_ call: CAPPluginCall) { + removeOverlayIds(call, key: "polygonIds") + } + + @objc func removeCircles(_ call: CAPPluginCall) { + removeOverlayIds(call, key: "circleIds") + } + + @objc func removePolylines(_ call: CAPPluginCall) { + removeOverlayIds(call, key: "polylineIds") + } + + private func removeOverlayIds(_ call: CAPPluginCall, key: String) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let ids = call.getArray(key) as? [String] else { + call.reject("\(key) is required", PluginError.invalidArgument) + return + } + map.removeOverlays(ids) + call.resolve() + } + + @objc func mapBoundsContains(_ call: CAPPluginCall) { + guard let boundsObj = call.getObject("bounds"), + let pointObj = call.getObject("point") else { + call.reject("bounds and point are required", PluginError.invalidArgument) + return + } + let sw = boundsObj["southwest"] as? JSObject ?? [:] + let ne = boundsObj["northeast"] as? JSObject ?? [:] + let lat = pointObj["lat"] as? Double ?? 0 + let lng = pointObj["lng"] as? Double ?? 0 + let south = sw["lat"] as? Double ?? -90 + let west = sw["lng"] as? Double ?? -180 + let north = ne["lat"] as? Double ?? 90 + let east = ne["lng"] as? Double ?? 180 + let contains = lat >= south && lat <= north && lng >= west && lng <= east + call.resolve(["contains": contains]) + } + + @objc func mapBoundsExtend(_ call: CAPPluginCall) { + guard let boundsObj = call.getObject("bounds"), + let pointObj = call.getObject("point") else { + call.reject("bounds and point are required", PluginError.invalidArgument) + return + } + let sw = boundsObj["southwest"] as? JSObject ?? [:] + let ne = boundsObj["northeast"] as? JSObject ?? [:] + let lat = pointObj["lat"] as? Double ?? 0 + let lng = pointObj["lng"] as? Double ?? 0 + let south = min(sw["lat"] as? Double ?? lat, lat) + let west = min(sw["lng"] as? Double ?? lng, lng) + let north = max(ne["lat"] as? Double ?? lat, lat) + let east = max(ne["lng"] as? Double ?? lng, lng) + let centerLat = (south + north) / 2 + let centerLng = (west + east) / 2 + call.resolve([ + "bounds": [ + "southwest": ["lat": south, "lng": west], + "northeast": ["lat": north, "lng": east], + "center": ["lat": centerLat, "lng": centerLng] + ] + ]) + } + + @objc func dispatchMapEvent(_ call: CAPPluginCall) { + call.resolve() + } + + @objc func updateLayout(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + let x = call.getDouble("x") ?? map.config.x + let y = call.getDouble("y") ?? map.config.y + let width = call.getDouble("width") ?? map.config.width + let height = call.getDouble("height") ?? map.config.height + map.updateLayout(bounds: CGRect(x: x, y: y, width: width, height: height)) + call.resolve() + } + + @objc func show(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setMapVisible(true) + call.resolve() + } + + @objc func hide(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.setMapVisible(false) + call.resolve() + } +} diff --git a/ios/Sources/NativeMapPlugin/MainThread.swift b/ios/Sources/NativeMapPlugin/MainThread.swift new file mode 100644 index 0000000..3fba1f5 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MainThread.swift @@ -0,0 +1,17 @@ +import Foundation + +/// Runs `block` synchronously on the main thread and returns its result. +/// +/// Unlike `DispatchQueue.main.sync`, this is safe to call from the main thread: +/// it runs the block inline instead of deadlocking. Capacitor dispatches plugin +/// calls off the main thread, so the bridge methods normally hop over via the +/// `DispatchQueue.main.sync` path; this guard keeps them correct if any path ever +/// reaches them already on the main thread (a lifecycle callback, a test, a +/// future caller). +@discardableResult +func runOnMainSync(_ block: () -> T) -> T { + if Thread.isMainThread { + return block() + } + return DispatchQueue.main.sync(execute: block) +} diff --git a/ios/Sources/NativeMapPlugin/MapAnnotations.swift b/ios/Sources/NativeMapPlugin/MapAnnotations.swift new file mode 100644 index 0000000..260001f --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MapAnnotations.swift @@ -0,0 +1,124 @@ +import Foundation +import MapKit +import Capacitor +import UIKit + +// MARK: - Map: camera + annotation views +// +// The per-map work behind the plugin's thin MKMapViewDelegate methods. Keeping it +// on `Map` (rather than in the bridge class) leaves NativeMapPlugin as a +// thin wrapper and keeps both types within SwiftLint's body-length budget. + +extension Map { + static let clusterReuseId = "appleMapCluster" + static let markerReuseId = "appleMapMarker" + // Markers with an icon and markers without one use different view classes + // (MKAnnotationView vs MKMarkerAnnotationView) and so must not share a reuse + // identifier - MapKit would hand back the wrong class from the reuse pool. + static let markerDefaultReuseId = "appleMapMarkerDefault" + + /// Parse a JS camera config (`coordinate` / `zoom` / `animate`) and apply it. + func setCamera(from configObj: JSObject) { + var coordinate: CLLocationCoordinate2D? + if let coordObj = configObj["coordinate"] as? JSObject, + let lat = coordObj["lat"] as? Double, + let lng = coordObj["lng"] as? Double { + coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lng) + } + let zoom = configObj["zoom"] as? Double + let animate = configObj["animate"] as? Bool ?? false + let bearing = configObj["bearing"] as? Double + let angle = configObj["angle"] as? Double + setCameraInternal(coordinate: coordinate, zoom: zoom, animate: animate, bearing: bearing, pitch: angle) + } + + /// Build (or recycle) the annotation view for a marker, cluster, or the user + /// location dot. Returns nil for the user location so MapKit draws its default. + func annotationView(for annotation: MKAnnotation, in mapView: MKMapView) -> MKAnnotationView? { + if annotation is MKUserLocation { return nil } + + if let cluster = annotation as? MKClusterAnnotation { + let view = (mapView.dequeueReusableAnnotationView(withIdentifier: Map.clusterReuseId) as? MKMarkerAnnotationView) + ?? MKMarkerAnnotationView(annotation: cluster, reuseIdentifier: Map.clusterReuseId) + view.annotation = cluster + view.canShowCallout = false + view.glyphText = "\(cluster.memberAnnotations.count)" + view.markerTintColor = .systemGray + view.displayPriority = .required + return view + } + + guard let marker = annotation as? NativeMapMarker else { return nil } + + // No icon โ†’ MapKit's native pin (MKMarkerAnnotationView), mirroring how + // @capacitor/google-maps renders a default marker when the host supplies + // none; a bare image-less MKAnnotationView would be invisible. The two + // view classes can't share a reuse id. + let hasIcon = !(marker.iconUrl?.isEmpty ?? true) + let view: MKAnnotationView + if hasIcon { + view = mapView.dequeueReusableAnnotationView(withIdentifier: Map.markerReuseId) + ?? MKAnnotationView(annotation: marker, reuseIdentifier: Map.markerReuseId) + } else { + view = mapView.dequeueReusableAnnotationView(withIdentifier: Map.markerDefaultReuseId) as? MKMarkerAnnotationView + ?? MKMarkerAnnotationView(annotation: marker, reuseIdentifier: Map.markerDefaultReuseId) + } + view.annotation = marker + view.clusteringIdentifier = shouldCluster ? Map.clusterReuseId : nil + // `.required` opts an annotation out of decluttering - which includes + // clustering - so a required marker will never collapse into a bubble no + // matter its clusteringIdentifier. When clustering is on, drop to + // `.defaultHigh` so overlapping markers may cluster; when it's off keep + // `.required` so no pin is ever silently hidden by decluttering. + view.displayPriority = shouldCluster ? .defaultHigh : .required + view.alpha = marker.opacity + view.zPriority = MKAnnotationViewZPriority(rawValue: Float(marker.zIndex)) + // Info windows are drawn as our own bubble (see Callout.swift), so the + // native callout stays off. When info windows are on, hide the inline + // title/subtitle labels too, so the bubble is the sole info display. + if let markerView = view as? MKMarkerAnnotationView { + let visibility: MKFeatureVisibility = config.showInfoWindows ? .hidden : .adaptive + markerView.titleVisibility = visibility + markerView.subtitleVisibility = visibility + // Tint applies only to the default pin; image markers carry their own colors. + markerView.markerTintColor = marker.tintColor + } + + // Reset first: a recycled image view must not keep a previous marker's + // icon while an `https:` icon for this one is still downloading (the + // async completion in `annotationImage` sets it on the live view). + view.image = nil + view.centerOffset = .zero + if hasIcon, let image = annotationImage(for: marker, in: mapView) { + view.image = image + view.centerOffset = marker.centerOffset(for: image.size) + } + // The native callout stays off - we render our own bubble (Callout.swift), + // because MapKit's callout doesn't show through the web-view compositing. + view.canShowCallout = false + return view + } + + /// Clear MapKit's selection for a marker and show (or hide) our own info-window + /// bubble. The pin stays its normal size rather than enlarging on selection. + func handleMarkerSelection(_ marker: NativeMapMarker) { + mapView.deselectAnnotation(marker, animated: false) + if config.showInfoWindows && !(marker.title?.isEmpty ?? true) { + showCallout(for: marker) + } else { + dismissCallout() + } + } + + /// Deselect a tapped cluster and zoom to frame its members. + func expandCluster(_ cluster: MKClusterAnnotation, in mapView: MKMapView) { + mapView.deselectAnnotation(cluster, animated: false) + var rect = MKMapRect.null + for member in cluster.memberAnnotations { + let point = MKMapPoint(member.coordinate) + rect = rect.union(MKMapRect(x: point.x, y: point.y, width: 0.01, height: 0.01)) + } + let padding = UIEdgeInsets(top: 60, left: 60, bottom: 60, right: 60) + mapView.setVisibleMapRect(rect, edgePadding: padding, animated: true) + } +} diff --git a/ios/Sources/NativeMapPlugin/MapControls.swift b/ios/Sources/NativeMapPlugin/MapControls.swift new file mode 100644 index 0000000..5d27c04 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MapControls.swift @@ -0,0 +1,101 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Map: user tracking, buildings, camera boundary, marker selection +// +// Per-map controls behind the plugin bridge (see ControlsBridge.swift). Split out +// to keep CapacitorNativeMap.swift within SwiftLint's length budget. + +extension Map { + + // MARK: User tracking + + /// Follow the user's location (and optionally heading). A non-`none` mode turns + /// the user-location dot on; the host app still needs location permission. + func setUserTrackingMode(_ mode: String) { + DispatchQueue.main.async { + let tracking = Map.userTrackingMode(from: mode) + if tracking != .none { + self.mapView.showsUserLocation = true + } + self.mapView.setUserTrackingMode(tracking, animated: true) + } + } + + /// Maps a JS tracking-mode string to `MKUserTrackingMode`. Pure, so it can be + /// unit-tested. + static func userTrackingMode(from string: String) -> MKUserTrackingMode { + switch string.lowercased() { + case "follow": return .follow + case "followwithheading": return .followWithHeading + default: return .none + } + } + + /// Show or hide an `MKUserTrackingButton` (a recenter/follow control) pinned to + /// the map's bottom-trailing corner. + func setUserTrackingButton(_ visible: Bool) { + DispatchQueue.main.async { + if visible { + guard self.userTrackingButton == nil else { return } + let button = MKUserTrackingButton(mapView: self.mapView) + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.85) + button.layer.cornerRadius = 6 + button.layer.masksToBounds = true + self.mapView.addSubview(button) + NSLayoutConstraint.activate([ + button.trailingAnchor.constraint(equalTo: self.mapView.trailingAnchor, constant: -12), + button.bottomAnchor.constraint(equalTo: self.mapView.bottomAnchor, constant: -12) + ]) + self.userTrackingButton = button + } else { + self.userTrackingButton?.removeFromSuperview() + self.userTrackingButton = nil + } + } + } + + // MARK: Buildings + + func setBuildings(_ enabled: Bool) { + DispatchQueue.main.async { self.mapView.showsBuildings = enabled } + } + + // MARK: Camera boundary + + /// Restrict panning so the camera center stays within `southwest`..`northeast`. + /// Passing nil corners clears the restriction. + func setCameraBoundary(southwest: CLLocationCoordinate2D?, northeast: CLLocationCoordinate2D?) { + runOnMainSync { + guard let southwest = southwest, let northeast = northeast else { + self.mapView.setCameraBoundary(nil, animated: true) + return + } + let rect = boundingMapRect(southwest: southwest, northeast: northeast) + self.mapView.setCameraBoundary(MKMapView.CameraBoundary(mapRect: rect), animated: true) + } + } + + // MARK: Marker selection + + /// Open the info-window bubble for a marker programmatically. Returns false if + /// no marker has that id. A marker with no title selects without a bubble. + @discardableResult + func selectMarker(_ markerId: String) -> Bool { + runOnMainSync { + guard let marker = self.markers[markerId] else { return false } + if !(marker.title?.isEmpty ?? true) { + self.showCallout(for: marker) + } + return true + } + } + + /// Close any open info-window bubble. + func deselectMarker() { + runOnMainSync { self.dismissCallout() } + } +} diff --git a/ios/Sources/NativeMapPlugin/MapMounting.swift b/ios/Sources/NativeMapPlugin/MapMounting.swift new file mode 100644 index 0000000..8800541 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MapMounting.swift @@ -0,0 +1,168 @@ +import Foundation +import MapKit +import Capacitor +import WebKit + +// MARK: - Native-view mounting and frame syncing +// +// Split out of CapacitorNativeMap.swift / NativeMapPlugin.swift to keep +// those types within SwiftLint's length budget. This is the compositing glue +// that keeps the native MKMapView aligned with its bound web element, ported +// from `@capacitor/google-maps`. + +extension Map { + + func updateRender(mapBounds: CGRect) { + runOnMainSync { + let newWidth = round(Double(mapBounds.width)) + let newHeight = round(Double(mapBounds.height)) + let widthEqual = round(Double(self.mapView.bounds.width)) == newWidth + let heightEqual = round(Double(self.mapView.bounds.height)) == newHeight + if !widthEqual || !heightEqual { + CATransaction.begin() + CATransaction.setDisableActions(true) + self.mapView.frame.size.width = mapBounds.width + self.mapView.frame.size.height = mapBounds.height + CATransaction.commit() + } + } + } + + func rebindTargetContainer(mapBounds: CGRect) { + runOnMainSync { + let refWidth = round(Double(mapBounds.width)) + let refHeight = round(Double(mapBounds.height)) + guard let target = self.getTargetContainer(refWidth: refWidth, refHeight: refHeight) else { return } + self.targetView = target + target.tag = Map.mapTag + target.removeAllSubview() + CATransaction.begin() + CATransaction.setDisableActions(true) + self.mapView.frame.size.width = mapBounds.width + self.mapView.frame.size.height = mapBounds.height + CATransaction.commit() + target.addSubview(self.mapView) + } + } + + /// Re-mount the map into its current webview container using the map's own + /// size. Called when the app returns to the foreground, after WebKit may + /// have rebuilt the scroll-view hierarchy and detached the native map's + /// touch handling. Keeps the existing mount if a container can't be found. + func remountIntoContainer() { + DispatchQueue.main.async { + if self.toBack, let webView = self.delegate?.bridge?.webView { + let frame = CGRect( + x: self.config.x, + y: self.config.y, + width: self.config.width, + height: self.config.height + ) + if self.mapView.superview !== webView { + self.mapView.removeFromSuperview() + webView.addSubview(self.mapView) + webView.sendSubviewToBack(self.mapView) + } + self.mapView.frame = frame + self.targetView = webView + self.mapView.isHidden = !self.isMapVisible + return + } + + let width = round(Double(self.mapView.bounds.width)) + let height = round(Double(self.mapView.bounds.height)) + guard width > 0, height > 0 else { return } + + // Clear the previous tag so getTargetContainer rediscovers from a + // clean slate (its default reference tag is Map.mapTag). + let previous = self.targetView + previous?.tag = 0 + self.targetView = nil + + guard let target = self.getTargetContainer(refWidth: width, refHeight: height) else { + // Couldn't rediscover a container; restore the previous mount. + previous?.tag = Map.mapTag + self.targetView = previous + return + } + + self.targetView = target + target.tag = Map.mapTag + target.removeAllSubview() + self.mapView.frame = target.bounds + target.addSubview(self.mapView) + } + } + + /// Finds the WKWebView child scroll view whose content size matches the bound + /// element, so the native map can be mounted into it. Ported from + /// `@capacitor/google-maps`. Must be called on the main thread. + func getTargetContainer(refWidth: Double, refHeight: Double) -> UIView? { + guard let webView = self.delegate?.bridge?.webView else { return nil } + for item in webView.getAllSubViews() { + guard let scrollView = item as? UIScrollView else { continue } + let childScrollClass = NSClassFromString("WKChildScrollView") + let scrollClass = NSClassFromString("WKScrollView") + let isChildScroll = (childScrollClass.map { item.isKind(of: $0) } ?? false) + || (scrollClass.map { item.isKind(of: $0) } ?? false) + let isBridgeScroll = item.isEqual(webView.scrollView) + if isChildScroll && !isBridgeScroll { + scrollView.isScrollEnabled = true + let height = Double(scrollView.contentSize.height) + let width = Double(scrollView.contentSize.width) + // The element carries an inner 200%-tall spacer, so the child + // scroll view's contentSize is ~2x the element's size. Match with + // a 1pt tolerance rather than an exact / floor-ceil-to-integer + // check: on devices whose layout lands the element on a + // half-point (e.g. iPhone 17 Pro at 558.5pt) contentSize.height/2 + // equals the element height exactly (1117/2 == 558.5), but the + // old integer floor(558.5)=558 / ceil=559 comparison missed it, + // leaving the map unmounted. Older devices happened to land on + // whole points, which is why the exact check worked there. + let widthEqual = abs(width - refWidth) <= 1.0 + let heightEqual = abs(height / 2.0 - refHeight) <= 1.0 + if widthEqual && heightEqual && item.tag < (self.targetView?.tag ?? Map.mapTag) { + return item + } + } + } + return nil + } +} + +// MARK: - Plugin frame-sync bridge + +extension NativeMapPlugin { + + @objc func onResize(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let boundsObj = call.getObject("mapBounds") else { + call.reject("mapBounds is required", PluginError.invalidArgument) + return + } + map.updateRender(mapBounds: CGRect.fromJSObject(boundsObj)) + call.resolve() + } + + @objc func onDisplay(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let boundsObj = call.getObject("mapBounds") else { + call.reject("mapBounds is required", PluginError.invalidArgument) + return + } + map.rebindTargetContainer(mapBounds: CGRect.fromJSObject(boundsObj)) + call.resolve() + } + + @objc func onScroll(_ call: CAPPluginCall) { + // The native map is a subview inside the webview's own scroll view on + // iOS, so it tracks page scrolling automatically. Nothing to do. + call.resolve() + } +} diff --git a/ios/Sources/NativeMapPlugin/MapReadback.swift b/ios/Sources/NativeMapPlugin/MapReadback.swift new file mode 100644 index 0000000..9668d87 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MapReadback.swift @@ -0,0 +1,66 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Map: camera/bounds read-back + live marker styling +// +// Split out of CapacitorNativeMap.swift to keep the core Map type within +// SwiftLint's length budget. These are still part of the same Map type: the +// payload readers behind getCameraPosition / getMapBounds, and the in-place +// style updates behind updateMarkers. + +extension Map { + + /// Must be called on the main thread. + func currentZoom() -> Double { + let width = Double(mapView.bounds.width > 0 ? mapView.bounds.width : UIScreen.main.bounds.width) + return longitudeDeltaToZoom(mapView.region.span.longitudeDelta, widthPoints: width) + } + + /// Must be called on the main thread. Shape matches `LatLngBounds` in JS. + func boundsPayload() -> PluginCallResultData { + let region = mapView.region + let corners = regionCorners(center: region.center, span: region.span) + return [ + "center": ["lat": region.center.latitude, "lng": region.center.longitude], + "southwest": ["lat": corners.southwest.latitude, "lng": corners.southwest.longitude], + "northeast": ["lat": corners.northeast.latitude, "lng": corners.northeast.longitude] + ] + } + + /// Must be called on the main thread. Shape matches `CameraPosition` in JS. + func cameraPayload() -> PluginCallResultData { + let center = mapView.centerCoordinate + let camera = mapView.camera + return [ + "latitude": center.latitude, + "longitude": center.longitude, + "zoom": currentZoom(), + "bearing": camera.heading, + "angle": Double(camera.pitch), + "bounds": boundsPayload() + ] + } + + /// Apply opacity / tint / z-order from an update payload to a marker and its + /// live annotation view, so the change shows without re-adding the annotation + /// (which would flash the pin). Only keys present in `obj` are touched. Must be + /// called on the main thread. + func applyLiveStyleUpdates(from obj: JSObject, to marker: NativeMapMarker) { + if obj.keys.contains("opacity") { + marker.opacity = CGFloat(obj["opacity"] as? Double ?? 1) + mapView.view(for: marker)?.alpha = marker.opacity + } + if obj.keys.contains("zIndex") { + marker.zIndex = obj["zIndex"] as? Double ?? 0 + mapView.view(for: marker)?.zPriority = MKAnnotationViewZPriority(rawValue: Float(marker.zIndex)) + } + if obj.keys.contains("tintColor") { + marker.tintColor = NativeMapMarker.parseTintColor(obj["tintColor"]) + if let markerView = mapView.view(for: marker) as? MKMarkerAnnotationView { + markerView.markerTintColor = marker.tintColor + } + } + } +} diff --git a/ios/Sources/NativeMapPlugin/MarkerBridge.swift b/ios/Sources/NativeMapPlugin/MarkerBridge.swift new file mode 100644 index 0000000..10a6547 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MarkerBridge.swift @@ -0,0 +1,98 @@ +import Foundation +import MapKit +import Capacitor + +// MARK: - Plugin: marker + clustering bridge +// +// Split out of NativeMapPlugin.swift so the plugin type stays within +// SwiftLint's body-length budget. These are still bridge methods on the same +// plugin type; the per-map work lives on `Map` (CapacitorNativeMap.swift). + +extension NativeMapPlugin { + + @objc func addMarkers(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let markerObjs = call.getArray("markers") as? [JSObject] else { + call.reject("markers array is required", PluginError.invalidArgument) + return + } + let ids = map.addMarkers(markerObjs) + call.resolve(["ids": ids]) + } + + @objc func addMarker(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let markerObj = call.getObject("marker") else { + call.reject("marker is required", PluginError.invalidArgument) + return + } + guard let markerId = map.addMarkers([markerObj]).first else { + call.reject("marker is missing or malformed", PluginError.invalidArgument) + return + } + call.resolve(["id": markerId]) + } + + @objc func updateMarkers(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let markerObjs = call.getArray("markers") as? [JSObject] else { + call.reject("markers array is required", PluginError.invalidArgument) + return + } + map.updateMarkers(markerObjs) + call.resolve() + } + + @objc func removeMarkers(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let ids = call.getArray("markerIds") as? [String] else { + call.reject("markerIds array is required", PluginError.invalidArgument) + return + } + map.removeMarkers(ids) + call.resolve() + } + + @objc func removeMarker(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let markerId = call.getString("markerId") else { + call.reject("markerId is required", PluginError.invalidArgument) + return + } + map.removeMarkers([markerId]) + call.resolve() + } + + @objc func enableClustering(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.enableClustering(minClusterSize: call.getInt("minClusterSize")) + call.resolve() + } + + @objc func disableClustering(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.disableClustering() + call.resolve() + } +} diff --git a/ios/Sources/NativeMapPlugin/MarkerIcons.swift b/ios/Sources/NativeMapPlugin/MarkerIcons.swift new file mode 100644 index 0000000..e286ab7 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/MarkerIcons.swift @@ -0,0 +1,99 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Marker icon resolution +// +// Split out of CapacitorNativeMap.swift to keep the core Map type within +// SwiftLint's length budget. Resolves a marker's `iconUrl` (bundled asset, +// `data:` URI, or `http(s):` download) to a sized image, caching results in the +// Map's `NSCache` and de-duping in-flight downloads. + +extension Map { + + /// Resolves a marker icon. Returns nil synchronously for `http(s):` URLs and + /// sets the image on the live annotation view once the download finishes. + /// Must be called on the main thread. + func annotationImage(for marker: NativeMapMarker, in mapView: MKMapView) -> UIImage? { + guard let iconUrl = marker.iconUrl else { return nil } + + if let cached = iconCache.object(forKey: iconUrl as NSString) { + return resize(cached, marker.iconSize) + } + + if iconUrl.hasPrefix("data:") { + if let commaIndex = iconUrl.firstIndex(of: ","), + let data = Data(base64Encoded: String(iconUrl[iconUrl.index(after: commaIndex)...])), + let image = UIImage(data: data) { + iconCache.setObject(image, forKey: iconUrl as NSString) + return resize(image, marker.iconSize) + } + return nil + } + + if iconUrl.hasPrefix("http") { + downloadRemoteIcon(iconUrl, for: marker, in: mapView) + return nil + } + + // Bundled web asset - Capacitor copies the web `static/` dir into the app + // bundle under `public/`. + if let image = UIImage(named: "public/\(iconUrl)") { + iconCache.setObject(image, forKey: iconUrl as NSString) + return resize(image, marker.iconSize) + } + return nil + } + + /// Fetches a remote icon once and applies it to the live annotation view. + /// De-dupes concurrent requests for the same url and remembers permanent + /// misses so a re-render doesn't re-download. Must be called on the main + /// thread; the completion hops back to it. + private func downloadRemoteIcon(_ iconUrl: String, for marker: NativeMapMarker, in mapView: MKMapView) { + guard !failedIconURLs.contains(iconUrl) else { return } + if inFlightIconURLs.contains(iconUrl) { + pendingIconMarkers[iconUrl, default: []].append(marker) + return + } + guard let url = URL(string: iconUrl) else { + failedIconURLs.insert(iconUrl) + return + } + inFlightIconURLs.insert(iconUrl) + pendingIconMarkers[iconUrl] = [marker] + URLSession.shared.dataTask(with: url) { [weak self, weak mapView] data, _, error in + DispatchQueue.main.async { + guard let self = self else { return } + self.inFlightIconURLs.remove(iconUrl) + let markers = self.pendingIconMarkers.removeValue(forKey: iconUrl) ?? [] + + guard let data = data, let image = UIImage(data: data) else { + // A response with no usable image (404 body, wrong content, + // decode failure) is a permanent miss; a transport error + // (offline, timeout) is left out so a later render can retry. + if error == nil { self.failedIconURLs.insert(iconUrl) } + return + } + + self.iconCache.setObject(image, forKey: iconUrl as NSString) + for waitingMarker in markers where waitingMarker.iconUrl == iconUrl { + guard let view = mapView?.view(for: waitingMarker) else { continue } + let sized = self.resize(image, waitingMarker.iconSize) + view.image = sized + if let sized = sized { + view.centerOffset = waitingMarker.centerOffset(for: sized.size) + } + } + } + }.resume() + } + + private func resize(_ image: UIImage, _ size: CGSize?) -> UIImage? { + guard let size = size else { return image } + let renderer = UIGraphicsImageRenderer(size: size) + return renderer.image { _ in + image.draw(in: CGRect(origin: .zero, size: size)) + } + } +} diff --git a/ios/Sources/NativeMapPlugin/Overlays.swift b/ios/Sources/NativeMapPlugin/Overlays.swift new file mode 100644 index 0000000..effe2c9 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Overlays.swift @@ -0,0 +1,427 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Map: overlays, appearance, tap, helpers +// +// Split out of CapacitorNativeMap.swift so the core Map type stays within +// SwiftLint's body-length budget. These are still part of the same Map type. + +extension Map { + + // MARK: Overlays + + func addPolylines(_ objs: [JSObject]) -> [String] { + var ids: [String] = [] + runOnMainSync { + for obj in objs { + let coords = Map.parseCoords(obj["path"]) + guard coords.count >= 2 else { continue } + // A geodesic line follows the great-circle (shortest) path between + // points rather than a straight screen line. MKGeodesicPolyline is a + // MKPolyline subclass, so it renders and hit-tests through the same paths. + let polyline: MKPolyline = (obj["geodesic"] as? Bool ?? false) + ? MKGeodesicPolyline(coordinates: coords, count: coords.count) + : MKPolyline(coordinates: coords, count: coords.count) + let style = Map.overlayStyle(from: obj, defaultLineWidth: 3, filled: false) + ids.append(self.register(polyline, style: style)) + } + } + return ids + } + + func addPolygons(_ objs: [JSObject]) -> [String] { + var ids: [String] = [] + runOnMainSync { + for obj in objs { + let rings = Map.parseRings(obj["paths"]) + guard let exterior = rings.first, exterior.count >= 3 else { continue } + let holes = rings.dropFirst().map { MKPolygon(coordinates: $0, count: $0.count) } + let polygon = MKPolygon( + coordinates: exterior, count: exterior.count, + interiorPolygons: holes.isEmpty ? nil : Array(holes) + ) + let style = Map.overlayStyle(from: obj, defaultLineWidth: 2, filled: true) + ids.append(self.register(polygon, style: style)) + } + } + return ids + } + + func addCircles(_ objs: [JSObject]) -> [String] { + var ids: [String] = [] + runOnMainSync { + for obj in objs { + guard let centerObj = obj["center"] as? JSObject, + let lat = centerObj["lat"] as? Double, + let lng = centerObj["lng"] as? Double, + let radius = obj["radius"] as? Double else { continue } + let circle = MKCircle( + center: CLLocationCoordinate2D(latitude: lat, longitude: lng), + radius: radius + ) + let style = Map.overlayStyle(from: obj, defaultLineWidth: 2, filled: true) + ids.append(self.register(circle, style: style)) + } + } + return ids + } + + func removeOverlays(_ ids: [String]) { + runOnMainSync { + for id in ids { + if let overlay = self.overlays.removeValue(forKey: id) { + self.mapView.removeOverlay(overlay) + self.overlayStyles.removeValue(forKey: ObjectIdentifier(overlay)) + } + } + } + } + + /// Store an overlay + its style under a fresh id and add it to the map. Must + /// be called on the main thread. + private func register(_ overlay: MKOverlay, style: OverlayStyle) -> String { + let id = UUID().uuidString + overlays[id] = overlay + overlayStyles[ObjectIdentifier(overlay)] = style + mapView.addOverlay(overlay) + return id + } + + // Appearance / location / gestures / padding live in Appearance.swift. + + // MARK: Map tap + + /// Emits `onMapClick` for taps on the map surface. Taps that land on a marker + /// never reach here (the gesture delegate declines them) so MapKit alone + /// selects the pin and shows its callout / info window; `onMarkerClick` fires + /// from `didSelect`. + @objc func handleMapTap(_ gesture: UITapGestureRecognizer) { + guard gesture.state == .ended else { return } + // A tap on the bare map closes any open info window. + dismissCallout() + let point = gesture.location(in: mapView) + // An overlay tap reports the overlay hit (polygon/polyline/circle) instead + // of onMapClick; a tap on the bare map surface falls through to onMapClick. + if let hit = overlayHit(at: point) { + let coordinate = mapView.convert(point, toCoordinateFrom: mapView) + delegate?.notifyListeners(hit.event, data: [ + "mapId": id, + hit.idKey: hit.id, + "latitude": coordinate.latitude, + "longitude": coordinate.longitude + ]) + return + } + emitMapGesture("onMapClick", at: point) + } + + /// A tapped overlay: the JS event name, the id key for that overlay kind, and + /// the overlay's id. + struct OverlayHit { + let event: String + let idKey: String + let id: String + } + + /// The topmost overlay under `point`, if the tap landed on one. Polygons and + /// circles hit on their filled interior; polylines hit within a finger-width + /// tolerance of the stroke. + private func overlayHit(at point: CGPoint) -> OverlayHit? { + let mapPoint = MKMapPoint(mapView.convert(point, toCoordinateFrom: mapView)) + for (id, overlay) in overlays { + guard let renderer = mapView.renderer(for: overlay) as? MKOverlayPathRenderer, + let path = renderer.path else { continue } + let rendererPoint = renderer.point(for: mapPoint) + + let event: String + let idKey: String + var hitPath = path + if overlay is MKPolyline { + event = "onPolylineClick" + idKey = "polylineId" + // A line has no interior, so widen the stroke to a tappable band. + let width = max(renderer.lineWidth, 22) + hitPath = path.copy(strokingWithWidth: width, lineCap: .round, lineJoin: .round, miterLimit: 1) + } else if overlay is MKCircle { + event = "onCircleClick" + idKey = "circleId" + } else if overlay is MKPolygon { + event = "onPolygonClick" + idKey = "polygonId" + } else { + continue + } + + if hitPath.contains(rendererPoint) { + return OverlayHit(event: event, idKey: idKey, id: id) + } + } + return nil + } + + /// The marker under `point`, if the point landed on one (any marker, draggable + /// or not); nil for the map surface or a cluster bubble. + private func markerAnnotation(at point: CGPoint) -> NativeMapMarker? { + var view = mapView.hitTest(point, with: nil) + while let current = view { + if let annotationView = current as? MKAnnotationView, + let marker = annotationView.annotation as? NativeMapMarker { + return marker + } + view = current.superview + } + return nil + } + + /// Emits `onMapLongClick` for a long-press on the map surface. A long-press + /// recognizer reports `.began` once when the press threshold is reached. + @objc func handleMapLongPress(_ gesture: UILongPressGestureRecognizer) { + guard gesture.state == .began else { return } + emitMapGesture("onMapLongClick", at: gesture.location(in: mapView)) + } + + /// Converts a view point to a coordinate and notifies `event`, unless the + /// point landed on an annotation (marker selection reports those separately). + private func emitMapGesture(_ event: String, at point: CGPoint) { + var view = mapView.hitTest(point, with: nil) + while let current = view { + if current is MKAnnotationView { return } + view = current.superview + } + let coordinate = mapView.convert(point, toCoordinateFrom: mapView) + delegate?.notifyListeners(event, data: [ + "mapId": id, + "latitude": coordinate.latitude, + "longitude": coordinate.longitude + ]) + } + + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer + ) -> Bool { + return true + } + + /// Decide which of our recognizers may claim a given touch. The marker-drag + /// recognizer wants touches only on a *draggable* pin. The tap and + /// map-long-press recognizers decline touches on *any* marker, so MapKit's own + /// selection runs and presents the pin's callout (info window) - relaying it + /// through our own tap recognizer raced with MapKit's and cancelled the + /// selection, so the callout never showed. Both still fire for the bare map + /// surface. + public func gestureRecognizer( + _ gestureRecognizer: UIGestureRecognizer, + shouldReceive touch: UITouch + ) -> Bool { + let marker = markerAnnotation(at: touch.location(in: mapView)) + if gestureRecognizer.name == Map.markerDragGestureName { + return marker?.isDraggable ?? false + } + // A tap on the info-window bubble must not reach the map tap handler (which + // dismisses it); the bubble's own recognizer emits onInfoWindowClick. + if let callout = calloutView, touch.view?.isDescendant(of: callout) ?? false { + return false + } + return marker == nil + } + + // MARK: Marker drag + + /// Drives dragging of `draggable` markers. On `.began` a press that lands on a + /// draggable pin picks it up and freezes the map; each `.changed` moves the + /// pin and streams `onMarkerDrag`; the end states drop it and restore the map. + @objc func handleMarkerDrag(_ gesture: UILongPressGestureRecognizer) { + let point = gesture.location(in: mapView) + switch gesture.state { + case .began: + guard let marker = draggableMarker(at: point) else { return } + draggingMarker = marker + scrollWasEnabledBeforeDrag = mapView.isScrollEnabled + mapView.isScrollEnabled = false + emitMarkerDrag("onMarkerDragStart", for: marker) + case .changed: + guard let marker = draggingMarker else { return } + marker.coordinate = mapView.convert(point, toCoordinateFrom: mapView) + emitMarkerDrag("onMarkerDrag", for: marker) + case .ended, .cancelled, .failed: + guard let marker = draggingMarker else { return } + emitMarkerDrag("onMarkerDragEnd", for: marker) + mapView.isScrollEnabled = scrollWasEnabledBeforeDrag + draggingMarker = nil + default: + break + } + } + + /// The draggable marker under `point`, if any; nil for the map surface, a + /// cluster bubble, or a non-draggable pin. + private func draggableMarker(at point: CGPoint) -> NativeMapMarker? { + guard let marker = markerAnnotation(at: point), marker.isDraggable else { return nil } + return marker + } + + private func emitMarkerDrag(_ event: String, for marker: NativeMapMarker) { + delegate?.notifyListeners(event, data: [ + "mapId": id, + "markerId": marker.markerId, + "latitude": marker.coordinate.latitude, + "longitude": marker.coordinate.longitude + ]) + } + + // MARK: Static helpers + + /// Resolves an overlay's stroke/fill styling from its payload, applying the + /// per-shape default line width and dropping the fill for unfilled shapes + /// (polylines). Pure, so overlay styling can be unit-tested. + static func overlayStyle(from obj: JSObject, defaultLineWidth: Double, filled: Bool) -> OverlayStyle { + OverlayStyle( + strokeColor: color(obj["strokeColor"], opacity: obj["strokeOpacity"]) ?? .systemBlue, + lineWidth: CGFloat(obj["strokeWeight"] as? Double ?? defaultLineWidth), + fillColor: filled ? color(obj["fillColor"], opacity: obj["fillOpacity"]) : nil, + lineDashPattern: parseDashPattern(obj["lineDashPattern"]) + ) + } + + /// A `[on, off, ...]` array of point lengths as `[NSNumber]` for a dashed + /// stroke, or nil for a solid line (absent, empty, or malformed). + static func parseDashPattern(_ value: Any?) -> [NSNumber]? { + guard let arr = value as? [Any] else { return nil } + let lengths = arr.compactMap { ($0 as? Double).map { NSNumber(value: $0) } } + return lengths.isEmpty ? nil : lengths + } + + /// Parses a flat array of `{lat,lng}` objects into coordinates. + static func parseCoords(_ value: Any?) -> [CLLocationCoordinate2D] { + guard let arr = value as? [Any] else { return [] } + return arr.compactMap { item in + guard let obj = item as? JSObject, + let lat = obj["lat"] as? Double, + let lng = obj["lng"] as? Double else { return nil } + return CLLocationCoordinate2D(latitude: lat, longitude: lng) + } + } + + /// Parses a polygon `paths` value: either a single ring of coordinates or an + /// array of rings (exterior first, then holes). + static func parseRings(_ value: Any?) -> [[CLLocationCoordinate2D]] { + guard let arr = value as? [Any] else { return [] } + if let first = arr.first, first is [Any] { + return arr.compactMap { ($0 as? [Any]).map { parseCoords($0) } } + } + return [parseCoords(arr)] + } + + /// Parses a `#RGB`/`#RRGGBB`/`#RRGGBBAA` hex string, overriding the alpha with + /// `opacity` (0..1) when supplied. Returns nil for a missing/malformed value. + static func color(_ value: Any?, opacity: Any?) -> UIColor? { + guard let hex = value as? String else { return nil } + var str = hex.trimmingCharacters(in: .whitespacesAndNewlines) + if str.hasPrefix("#") { str.removeFirst() } + guard let intVal = UInt64(str, radix: 16) else { return nil } + + let red, green, blue: Double + var alpha: Double = 1 + switch str.count { + case 8: + red = Double((intVal & 0xFF00_0000) >> 24) / 255 + green = Double((intVal & 0x00FF_0000) >> 16) / 255 + blue = Double((intVal & 0x0000_FF00) >> 8) / 255 + alpha = Double(intVal & 0x0000_00FF) / 255 + case 6: + red = Double((intVal & 0xFF0000) >> 16) / 255 + green = Double((intVal & 0x00FF00) >> 8) / 255 + blue = Double(intVal & 0x0000FF) / 255 + default: + return nil + } + if let alphaOverride = opacity as? Double { alpha = alphaOverride } + return UIColor(red: red, green: green, blue: blue, alpha: alpha) + } +} + +// MARK: - Plugin: overlay + appearance bridge and the overlay renderer + +extension NativeMapPlugin { + + @objc func addPolylines(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let objs = call.getArray("polylines") as? [JSObject] else { + call.reject("polylines array is required", PluginError.invalidArgument) + return + } + call.resolve(["ids": map.addPolylines(objs)]) + } + + @objc func addPolygons(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let objs = call.getArray("polygons") as? [JSObject] else { + call.reject("polygons array is required", PluginError.invalidArgument) + return + } + call.resolve(["ids": map.addPolygons(objs)]) + } + + @objc func addCircles(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let objs = call.getArray("circles") as? [JSObject] else { + call.reject("circles array is required", PluginError.invalidArgument) + return + } + call.resolve(["ids": map.addCircles(objs)]) + } + + @objc func removeOverlays(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + guard let ids = call.getArray("ids") as? [String] else { + call.reject("ids array is required", PluginError.invalidArgument) + return + } + map.removeOverlays(ids) + call.resolve() + } + + public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer { + let style = findMap(for: mapView)?.overlayStyles[ObjectIdentifier(overlay)] + + if let polyline = overlay as? MKPolyline { + let renderer = MKPolylineRenderer(polyline: polyline) + renderer.strokeColor = style?.strokeColor ?? .systemBlue + renderer.lineWidth = style?.lineWidth ?? 3 + renderer.lineDashPattern = style?.lineDashPattern + return renderer + } + if let polygon = overlay as? MKPolygon { + let renderer = MKPolygonRenderer(polygon: polygon) + renderer.strokeColor = style?.strokeColor ?? .systemBlue + renderer.lineWidth = style?.lineWidth ?? 2 + renderer.fillColor = style?.fillColor + renderer.lineDashPattern = style?.lineDashPattern + return renderer + } + if let circle = overlay as? MKCircle { + let renderer = MKCircleRenderer(circle: circle) + renderer.strokeColor = style?.strokeColor ?? .systemBlue + renderer.lineWidth = style?.lineWidth ?? 2 + renderer.fillColor = style?.fillColor + renderer.lineDashPattern = style?.lineDashPattern + return renderer + } + return MKOverlayRenderer(overlay: overlay) + } +} diff --git a/ios/Sources/NativeMapPlugin/Permissions.swift b/ios/Sources/NativeMapPlugin/Permissions.swift new file mode 100644 index 0000000..b88c508 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Permissions.swift @@ -0,0 +1,64 @@ +import Foundation +import Capacitor +import CoreLocation + +// MARK: - Location permission +// +// Implements the Capacitor permission pattern for the `location` alias, used by +// `enableCurrentLocation` (the blue user-location dot). The host app must still +// declare `NSLocationWhenInUseUsageDescription` in its Info.plist; without it the +// system prompt never appears and iOS treats access as denied. + +extension NativeMapPlugin: CLLocationManagerDelegate { + + /// The CLLocationManager, created on first use. Owning it (rather than a + /// throwaway) is what lets the authorization-change delegate fire back into us. + func ensureLocationManager() -> CLLocationManager { + if let manager = locationManager { + return manager + } + let manager = CLLocationManager() + manager.delegate = self + locationManager = manager + return manager + } + + /// Current authorization mapped to a Capacitor `PermissionState` string. + private func locationPermissionState() -> String { + switch ensureLocationManager().authorizationStatus { + case .notDetermined: + return "prompt" + case .restricted, .denied: + return "denied" + case .authorizedAlways, .authorizedWhenInUse: + return "granted" + @unknown default: + return "prompt" + } + } + + @objc override public func checkPermissions(_ call: CAPPluginCall) { + call.resolve(["location": locationPermissionState()]) + } + + @objc override public func requestPermissions(_ call: CAPPluginCall) { + let manager = ensureLocationManager() + if manager.authorizationStatus == .notDetermined { + pendingPermissionCall = call + call.keepAlive = true + manager.requestWhenInUseAuthorization() + } else { + checkPermissions(call) + } + } + + public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { + guard let call = pendingPermissionCall, + manager.authorizationStatus != .notDetermined else { + return + } + checkPermissions(call) + bridge?.releaseCall(withID: call.callbackId) + pendingPermissionCall = nil + } +} diff --git a/ios/Sources/NativeMapPlugin/PluginError.swift b/ios/Sources/NativeMapPlugin/PluginError.swift new file mode 100644 index 0000000..28ee058 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/PluginError.swift @@ -0,0 +1,21 @@ +import Foundation + +// MARK: - Rejection codes +// +// Stable, machine-readable codes passed as the second argument to +// `call.reject(_:_:)`. They reach JS as the `code` property of the rejected +// error, so host apps can branch on them instead of matching message strings +// (which are for humans and may change). Keep these values stable. + +enum PluginError { + /// No map is registered for the `id` the call supplied. + static let mapNotFound = "MAP_NOT_FOUND" + /// No marker is registered for the `markerId` the call supplied. + static let markerNotFound = "MARKER_NOT_FOUND" + /// A required argument was missing or the wrong type. + static let invalidArgument = "INVALID_ARGUMENT" + /// A native operation failed at runtime (its `Error` is forwarded too). + static let operationFailed = "OPERATION_FAILED" + /// The capability is not available on this platform. + static let unavailable = "UNAVAILABLE" +} diff --git a/ios/Sources/NativeMapPlugin/Search.swift b/ios/Sources/NativeMapPlugin/Search.swift new file mode 100644 index 0000000..a4ac160 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Search.swift @@ -0,0 +1,252 @@ +import Foundation +import MapKit +import Capacitor + +/// True when `coordinate` is within `maxKm` of `center`. A nil/zero limit or a +/// nil center means "no filter" (always true). Pure so it can be unit-tested. +func withinDistance(maxKm: Double?, from center: CLLocation?, to coordinate: CLLocationCoordinate2D) -> Bool { + guard let maxKm = maxKm, maxKm > 0, let center = center else { return true } + let here = CLLocation(latitude: coordinate.latitude, longitude: coordinate.longitude) + return center.distance(from: here) / 1000.0 <= maxKm +} + +/// Reads an optional `region` object into an `MKCoordinateRegion` and its center. +private func parseRegion(_ region: JSObject?) -> (region: MKCoordinateRegion, center: CLLocation)? { + guard let region = region, + let lat = region["latitude"] as? Double, + let lng = region["longitude"] as? Double else { return nil } + let latDelta = region["latitudeDelta"] as? Double ?? 1.0 + let lngDelta = region["longitudeDelta"] as? Double ?? 1.0 + let center = CLLocationCoordinate2D(latitude: lat, longitude: lng) + return ( + MKCoordinateRegion(center: center, span: MKCoordinateSpan(latitudeDelta: latDelta, longitudeDelta: lngDelta)), + CLLocation(latitude: lat, longitude: lng) + ) +} + +/// What `searchAutocomplete` returns when the caller does not say. +let defaultCompleterResultTypes: MKLocalSearchCompleter.ResultType = [.address, .pointOfInterest] + +/// Reads the optional `resultTypes` string list into completer result types. +/// Unknown names are ignored, and a list that names nothing usable falls back +/// to the default: an empty option set would make the completer return no +/// suggestions at all, which reads as a broken search rather than a filter. +func parseCompleterResultTypes(_ names: [String]?) -> MKLocalSearchCompleter.ResultType { + guard let names = names else { return defaultCompleterResultTypes } + var types: MKLocalSearchCompleter.ResultType = [] + for name in names { + switch name { + case "address": types.insert(.address) + case "pointOfInterest": types.insert(.pointOfInterest) + case "query": types.insert(.query) + default: break + } + } + return types.isEmpty ? defaultCompleterResultTypes : types +} + +/// A "City, State" style secondary line, skipping the locality when it just +/// repeats the primary name. +private func placeSubtitle(for placemark: MKPlacemark, name: String?) -> String { + var parts: [String] = [] + if let locality = placemark.locality, locality != name { + parts.append(locality) + } + if let admin = placemark.administrativeArea { + parts.append(admin) + } + if parts.isEmpty, let country = placemark.country { + parts.append(country) + } + return parts.joined(separator: ", ") +} + +/// Native place search. Offers two independent APIs: +/// +/// - `autocomplete` / `resolve`: idiomatic type-ahead via `MKLocalSearchCompleter`, +/// returning lightweight `{id, title, subtitle}` completions that `resolve` +/// turns into coordinates on demand. +/// - `places`: a one-shot `MKLocalSearch` returning coordinate-bearing results, +/// optionally scoped to a region and filtered by distance. +/// +/// Both kinds of id resolve through `resolve`. +class SearchService: NSObject, MKLocalSearchCompleterDelegate { + // Autocomplete (MKLocalSearchCompleter) + private var completer: MKLocalSearchCompleter? + private var pendingCall: CAPPluginCall? + private var completions: [String: MKLocalSearchCompletion] = [:] + + // One-shot search (MKLocalSearch) + private var items: [String: MKMapItem] = [:] + private var currentSearch: MKLocalSearch? + + // MARK: - Autocomplete (type-ahead) + + private func ensureCompleter() { + if completer == nil { + let newCompleter = MKLocalSearchCompleter() + newCompleter.resultTypes = defaultCompleterResultTypes + newCompleter.delegate = self + completer = newCompleter + } + } + + func autocomplete(_ call: CAPPluginCall) { + let query = call.getString("query") ?? "" + let regionObj = call.getObject("region") + let resultTypes = parseCompleterResultTypes(call.getArray("resultTypes") as? [String]) + DispatchQueue.main.async { + self.pendingCall?.resolve(["results": []]) + + if query.isEmpty { + self.pendingCall = nil + call.resolve(["results": []]) + return + } + + self.ensureCompleter() + guard let completer = self.completer else { + call.resolve(["results": []]) + return + } + + if let parsed = parseRegion(regionObj) { + completer.region = parsed.region + } + // Set on every call, not only when given: the completer is shared, so + // a call without the option must get the default back rather than + // inherit the previous caller's filter. + completer.resultTypes = resultTypes + + self.pendingCall = call + completer.queryFragment = query + } + } + + func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + guard let call = pendingCall else { return } + pendingCall = nil + + if completions.count > 300 { + completions.removeAll() + } + + var results: [[String: Any]] = [] + for completion in completer.results { + let id = UUID().uuidString + completions[id] = completion + results.append([ + "id": id, + "title": completion.title, + "subtitle": completion.subtitle + ]) + } + call.resolve(["results": results]) + } + + func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { + pendingCall?.resolve(["results": []]) + pendingCall = nil + } + + // MARK: - One-shot search (coordinate-bearing, filterable) + + func places(_ call: CAPPluginCall) { + let query = call.getString("query") ?? "" + if query.isEmpty { + call.resolve(["results": []]) + return + } + + let regionObj = call.getObject("region") + let maxDistanceKm = call.getDouble("maxDistanceKm") + let limit = call.getInt("limit") + + DispatchQueue.main.async { + self.currentSearch?.cancel() + + let request = MKLocalSearch.Request() + request.naturalLanguageQuery = query + + var center: CLLocation? + if let parsed = parseRegion(regionObj) { + request.region = parsed.region + center = parsed.center + } + + let search = MKLocalSearch(request: request) + self.currentSearch = search + search.start { response, _ in + if self.items.count > 300 { + self.items.removeAll() + } + + var results: [[String: Any]] = [] + for item in response?.mapItems ?? [] { + let coordinate = item.placemark.coordinate + guard withinDistance(maxKm: maxDistanceKm, from: center, to: coordinate) else { continue } + + let id = UUID().uuidString + self.items[id] = item + results.append([ + "id": id, + "title": item.name ?? item.placemark.locality ?? "", + "subtitle": placeSubtitle(for: item.placemark, name: item.name), + "latitude": coordinate.latitude, + "longitude": coordinate.longitude + ]) + + if let limit = limit, limit > 0, results.count >= limit { break } + } + call.resolve(["results": results]) + } + } + } + + // MARK: - Resolve (either kind of id) + + func resolve(_ call: CAPPluginCall) { + guard let id = call.getString("id") else { + call.resolve([:]) + return + } + + DispatchQueue.main.async { + self.resolveOnMain(call, id: id) + } + } + + private func resolveOnMain(_ call: CAPPluginCall, id: String) { + // A `places` result already carries its coordinate. + if let item = items[id] { + let coordinate = item.placemark.coordinate + call.resolve([ + "lat": coordinate.latitude, + "lng": coordinate.longitude, + "title": item.name ?? "" + ]) + return + } + + // An `autocomplete` completion needs a search to get its coordinate. + guard let completion = completions[id] else { + call.resolve([:]) + return + } + let search = MKLocalSearch(request: MKLocalSearch.Request(completion: completion)) + search.start { response, _ in + DispatchQueue.main.async { + guard let item = response?.mapItems.first else { + call.resolve([:]) + return + } + let coordinate = item.placemark.coordinate + call.resolve([ + "lat": coordinate.latitude, + "lng": coordinate.longitude, + "title": item.name ?? completion.title + ]) + } + } + } +} diff --git a/ios/Sources/NativeMapPlugin/Snapshot.swift b/ios/Sources/NativeMapPlugin/Snapshot.swift new file mode 100644 index 0000000..62b031d --- /dev/null +++ b/ios/Sources/NativeMapPlugin/Snapshot.swift @@ -0,0 +1,161 @@ +import Foundation +import MapKit +import UIKit +import Capacitor + +// MARK: - Map snapshot (#10) +// +// Renders the current map region to a PNG via MKMapSnapshotter, then composites +// the live annotation pins and overlays on top (MKMapSnapshotter itself renders +// only the base-map tiles, not the map view's subviews/overlays). + +enum SnapshotError: Error, LocalizedError { + case failed(String) + var errorDescription: String? { + switch self { + case .failed(let message): return message + } + } +} + +extension Map { + /// Snapshot the visible region (with pins + overlays drawn in) and hand back a + /// `data:image/png;base64,...` URL. `completion` is called on the main thread. + func takeSnapshot(completion: @escaping (Result) -> Void) { + DispatchQueue.main.async { + let options = MKMapSnapshotter.Options() + options.region = self.mapView.region + options.mapType = self.mapView.mapType + options.size = self.mapView.bounds.size + options.showsBuildings = self.mapView.showsBuildings + + self.snapshotRequestId += 1 + let requestId = self.snapshotRequestId + let snapshotter = MKMapSnapshotter(options: options) + self.pendingSnapshotter = snapshotter + // Composite on the main thread - it reads live annotation views. + snapshotter.start(with: .main) { snapshot, error in + if requestId != self.snapshotRequestId { + completion(.failure(SnapshotError.failed("snapshot superseded by a newer request"))) + return + } + if self.pendingSnapshotter === snapshotter { + self.pendingSnapshotter = nil + } + if let error = error { + completion(.failure(error)) + return + } + guard let snapshot = snapshot else { + completion(.failure(SnapshotError.failed("snapshot produced no image"))) + return + } + let composed = self.composite(snapshot) + guard let data = composed.pngData() else { + completion(.failure(SnapshotError.failed("could not encode snapshot"))) + return + } + completion(.success("data:image/png;base64," + data.base64EncodedString())) + } + } + } + + /// Draw the base-map snapshot, then overlays, then pins on top. + private func composite(_ snapshot: MKMapSnapshotter.Snapshot) -> UIImage { + let renderer = UIGraphicsImageRenderer(size: snapshot.image.size) + return renderer.image { context in + snapshot.image.draw(at: .zero) + drawOverlays(into: context.cgContext, snapshot: snapshot) + drawMarkers(into: context.cgContext, snapshot: snapshot) + } + } + + private func drawMarkers(into ctx: CGContext, snapshot: MKMapSnapshotter.Snapshot) { + for marker in markers.values { + // Render the live annotation view so the pin looks exactly as on the + // map (default balloon or custom icon). MapKit positions a view so its + // center sits at the coordinate offset by `centerOffset`. + guard let view = mapView.view(for: marker) else { continue } + let anchor = snapshot.point(for: marker.coordinate) + let center = CGPoint(x: anchor.x + view.centerOffset.x, y: anchor.y + view.centerOffset.y) + ctx.saveGState() + ctx.translateBy(x: center.x - view.bounds.width / 2, y: center.y - view.bounds.height / 2) + view.layer.render(in: ctx) + ctx.restoreGState() + } + } + + private func drawOverlays(into ctx: CGContext, snapshot: MKMapSnapshotter.Snapshot) { + for overlay in overlays.values { + let style = overlayStyles[ObjectIdentifier(overlay)] + let stroke = (style?.strokeColor ?? .systemBlue).cgColor + let lineWidth = style?.lineWidth ?? 3 + if let polyline = overlay as? MKPolyline { + strokePath(multiPointPath(polyline, snapshot: snapshot, closed: false), + into: ctx, stroke: stroke, fill: nil, lineWidth: lineWidth) + } else if let polygon = overlay as? MKPolygon { + strokePath(multiPointPath(polygon, snapshot: snapshot, closed: true), + into: ctx, stroke: stroke, fill: style?.fillColor?.cgColor, lineWidth: lineWidth) + } else if let circle = overlay as? MKCircle { + strokePath(circlePath(circle, snapshot: snapshot), + into: ctx, stroke: stroke, fill: style?.fillColor?.cgColor, lineWidth: lineWidth) + } + } + } + + private func strokePath(_ path: CGPath, into ctx: CGContext, stroke: CGColor, fill: CGColor?, lineWidth: CGFloat) { + if let fill = fill { + ctx.addPath(path) + ctx.setFillColor(fill) + ctx.fillPath() + } + ctx.addPath(path) + ctx.setStrokeColor(stroke) + ctx.setLineWidth(lineWidth) + ctx.setLineJoin(.round) + ctx.strokePath() + } + + private func multiPointPath(_ shape: MKMultiPoint, snapshot: MKMapSnapshotter.Snapshot, closed: Bool) -> CGPath { + let path = CGMutablePath() + let points = shape.points() + for index in 0.. CGPath { + let center = snapshot.point(for: circle.coordinate) + // Convert the radius (meters) to points via a coordinate one radius north. + let north = CLLocationCoordinate2D( + latitude: circle.coordinate.latitude + circle.radius / 111_320.0, + longitude: circle.coordinate.longitude + ) + let edge = snapshot.point(for: north) + let radius = hypot(edge.x - center.x, edge.y - center.y) + return CGPath( + ellipseIn: CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2), + transform: nil + ) + } +} + +extension NativeMapPlugin { + @objc func takeSnapshot(_ call: CAPPluginCall) { + guard let id = call.getString("id"), let map = maps[id] else { + call.reject("map not found", PluginError.mapNotFound) + return + } + map.takeSnapshot { result in + switch result { + case .success(let image): + call.resolve(["image": image]) + case .failure(let error): + call.reject(error.localizedDescription, PluginError.operationFailed, error) + } + } + } +} diff --git a/ios/Sources/NativeMapPlugin/ToBackCompositor.swift b/ios/Sources/NativeMapPlugin/ToBackCompositor.swift new file mode 100644 index 0000000..fbd2080 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/ToBackCompositor.swift @@ -0,0 +1,111 @@ +import Capacitor +import UIKit +import WebKit + +enum ToBackCompositor { + private struct SavedWebViewAppearance { + var isOpaque: Bool + var backgroundColor: UIColor? + var superviewBackgroundColor: UIColor? + var descendantBackgrounds: [ObjectIdentifier: UIColor?] + } + + private struct WebViewCompositorState { + var toBackMapCount: Int = 0 + var savedAppearance: SavedWebViewAppearance? + } + + private static var states: [ObjectIdentifier: WebViewCompositorState] = [:] + + private static func state(for webView: WKWebView) -> WebViewCompositorState { + let key = ObjectIdentifier(webView) + if let existing = states[key] { + return existing + } + let fresh = WebViewCompositorState() + states[key] = fresh + return fresh + } + + private static func setState(_ state: WebViewCompositorState, for webView: WKWebView) { + states[ObjectIdentifier(webView)] = state + } + + static func acquireTransparentWebView(_ webView: WKWebView?) { + guard let webView = webView else { return } + DispatchQueue.main.async { + var state = state(for: webView) + state.toBackMapCount += 1 + guard state.toBackMapCount == 1 else { + setState(state, for: webView) + return + } + + var descendantBackgrounds: [ObjectIdentifier: UIColor?] = [:] + func recordBackgrounds(_ view: UIView) { + descendantBackgrounds[ObjectIdentifier(view)] = view.backgroundColor + for subview in view.subviews { + recordBackgrounds(subview) + } + } + recordBackgrounds(webView) + + state.savedAppearance = SavedWebViewAppearance( + isOpaque: webView.isOpaque, + backgroundColor: webView.backgroundColor, + superviewBackgroundColor: webView.superview?.backgroundColor, + descendantBackgrounds: descendantBackgrounds + ) + + webView.isOpaque = false + webView.backgroundColor = .clear + func clearBackgrounds(_ view: UIView) { + view.backgroundColor = .clear + for subview in view.subviews { + clearBackgrounds(subview) + } + } + clearBackgrounds(webView) + webView.superview?.backgroundColor = .clear + webView.setNeedsLayout() + webView.layoutIfNeeded() + setState(state, for: webView) + } + } + + static func releaseTransparentWebView(_ webView: WKWebView?) { + guard let webView = webView else { return } + DispatchQueue.main.async { + var state = state(for: webView) + guard state.toBackMapCount > 0 else { return } + state.toBackMapCount -= 1 + guard state.toBackMapCount == 0, let saved = state.savedAppearance else { + setState(state, for: webView) + return + } + + webView.isOpaque = saved.isOpaque + webView.backgroundColor = saved.backgroundColor + webView.superview?.backgroundColor = saved.superviewBackgroundColor + + func restoreBackgrounds(_ view: UIView) { + let key = ObjectIdentifier(view) + if let prior = saved.descendantBackgrounds[key] { + view.backgroundColor = prior + } + for subview in view.subviews { + restoreBackgrounds(subview) + } + } + restoreBackgrounds(webView) + + state.savedAppearance = nil + setState(state, for: webView) + if state.toBackMapCount == 0 { + states.removeValue(forKey: ObjectIdentifier(webView)) + } + webView.setNeedsLayout() + webView.layoutIfNeeded() + } + } +} diff --git a/ios/Sources/NativeMapPlugin/TouchRoutingCache.swift b/ios/Sources/NativeMapPlugin/TouchRoutingCache.swift new file mode 100644 index 0000000..57fcca7 --- /dev/null +++ b/ios/Sources/NativeMapPlugin/TouchRoutingCache.swift @@ -0,0 +1,114 @@ +import Foundation +import UIKit +import WebKit + +/// Synchronous toBack touch routing from rects pushed by the WebView (see `touch-routing.ts`). +enum TouchRoutingCache { + private static var webCaptureRects: [ObjectIdentifier: [CGRect]] = [:] + private static let lock = NSLock() + + static func updateRects(for webView: WKWebView, rects: [CGRect]) { + lock.lock() + webCaptureRects[ObjectIdentifier(webView)] = rects + lock.unlock() + } + + static func shouldRouteTouchToMap(at point: CGPoint, in webView: WKWebView) -> Bool { + lock.lock() + let rects = webCaptureRects[ObjectIdentifier(webView)] ?? [] + lock.unlock() + for rect in rects where rect.contains(point) { + return false + } + return true + } + + static func clearRects(forWebViewKey key: ObjectIdentifier) { + lock.lock() + webCaptureRects.removeValue(forKey: key) + lock.unlock() + } + + static func clearRects(for webView: WKWebView) { + clearRects(forWebViewKey: ObjectIdentifier(webView)) + } +} + +final class TouchRoutingMessageHandler: NSObject, WKScriptMessageHandler { + static let handlerName = "nativeMapTouchRouting" + private weak var webView: WKWebView? + + init(webView: WKWebView) { + self.webView = webView + super.init() + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + guard message.name == Self.handlerName, + message.frameInfo.isMainFrame, + let webView = webView, + message.webView === webView else { return } + guard let body = message.body as? [[String: Any]] else { return } + let rects = body.compactMap { item -> CGRect? in + guard let x = item["x"] as? Double, + let y = item["y"] as? Double, + let width = item["width"] as? Double, + let height = item["height"] as? Double else { return nil } + return CGRect(x: x, y: y, width: width, height: height) + } + TouchRoutingCache.updateRects(for: webView, rects: rects) + } +} + +enum TouchRoutingBridge { + private struct Installation { + weak var webView: WKWebView? + let proxy: TouchRoutingMessageProxy + let handler: TouchRoutingMessageHandler + } + + private static var installedWebViews: [ObjectIdentifier: Installation] = [:] + + private static func purgeStaleInstallations() { + for (key, install) in installedWebViews where install.webView == nil { + installedWebViews.removeValue(forKey: key) + TouchRoutingCache.clearRects(forWebViewKey: key) + } + } + + static func install(on webView: WKWebView?) { + purgeStaleInstallations() + guard let webView = webView else { return } + let key = ObjectIdentifier(webView) + if let existing = installedWebViews[key], existing.webView === webView { + return + } + + let handler = TouchRoutingMessageHandler(webView: webView) + let proxy = TouchRoutingMessageProxy(handler: handler) + webView.configuration.userContentController.add(proxy, name: TouchRoutingMessageHandler.handlerName) + installedWebViews[key] = Installation(webView: webView, proxy: proxy, handler: handler) + } + + static func uninstall(from webView: WKWebView?) { + purgeStaleInstallations() + guard let webView = webView else { return } + let key = ObjectIdentifier(webView) + guard installedWebViews.removeValue(forKey: key) != nil else { return } + webView.configuration.userContentController.removeScriptMessageHandler(forName: TouchRoutingMessageHandler.handlerName) + TouchRoutingCache.clearRects(for: webView) + } +} + +private final class TouchRoutingMessageProxy: NSObject, WKScriptMessageHandler { + private let handler: TouchRoutingMessageHandler + + init(handler: TouchRoutingMessageHandler) { + self.handler = handler + super.init() + } + + func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { + handler.userContentController(userContentController, didReceive: message) + } +} diff --git a/ios/Sources/NativeMapPlugin/TouchRoutingJs.swift b/ios/Sources/NativeMapPlugin/TouchRoutingJs.swift new file mode 100644 index 0000000..8c0f91c --- /dev/null +++ b/ios/Sources/NativeMapPlugin/TouchRoutingJs.swift @@ -0,0 +1,8 @@ +import Foundation +import WebKit + +extension NativeMapPlugin { + func shouldRouteTouchToMap(at point: CGPoint, in webView: WKWebView) -> Bool { + TouchRoutingCache.shouldRouteTouchToMap(at: point, in: webView) + } +} diff --git a/ios/Sources/NativeMapPlugin/ViewHelpers.swift b/ios/Sources/NativeMapPlugin/ViewHelpers.swift new file mode 100644 index 0000000..7348e3b --- /dev/null +++ b/ios/Sources/NativeMapPlugin/ViewHelpers.swift @@ -0,0 +1,105 @@ +import Foundation +import UIKit +import WebKit +import Capacitor + +// MARK: - View-tree + touch-routing helpers (ported from @capacitor/google-maps) +// +// Split out of CapacitorNativeMap.swift so that file stays within SwiftLint's +// file-length budget. These compositing helpers mount the native map into the +// WKWebView's view tree and route touches down to it. + +// MARK: - WKWebView touch routing +// +// Routes touches that land on a WKChildScrollView down to the native map view +// mounted inside it. +extension WKWebView { + private static weak var touchRoutingPlugin: NativeMapPlugin? + + static func registerTouchRouting(plugin: NativeMapPlugin) { + touchRoutingPlugin = plugin + } + + override open func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var hitView = super.hitTest(point, with: event) + + if let plugin = WKWebView.touchRoutingPlugin, + let map = plugin.toBackMap(containing: point, in: self) { + if shouldPassTouchThroughToMap(hitView, at: point, in: self, plugin: plugin) { + let converted = convert(point, to: map.mapView) + return map.mapView.hitTest(converted, with: event) ?? hitView + } + } + + if let childScrollClass = NSClassFromString("WKChildScrollView"), + let candidate = hitView, candidate.isKind(of: childScrollClass) { + for item in candidate.subviews.reversed() { + let converted = item.convert(point, from: self) + if let inner = item.hitTest(converted, with: event) { + hitView = inner + break + } + } + } + return hitView + } +} + +private func shouldPassTouchThroughToMap( + _ hitView: UIView?, + at point: CGPoint, + in webView: WKWebView, + plugin: NativeMapPlugin +) -> Bool { + guard let hitView = hitView else { return true } + if hitView is UIControl { + return false + } + let name = String(describing: type(of: hitView)) + if name.contains("WKContentView") { + return plugin.shouldRouteTouchToMap(at: point, in: webView) + } + if let childScrollClass = NSClassFromString("WKChildScrollView"), + hitView.isKind(of: childScrollClass) { + return true + } + if hitView is UIScrollView { + return true + } + return hitView.backgroundColor == nil || hitView.backgroundColor == .clear +} + + +// MARK: - View tree helpers + +extension UIView { + func getAllSubViews() -> [UIView] { + var collected: [UIView] = [] + var nextTag = tag + var stack: [UIView] = [self] + while let root = stack.popLast() { + for view in root.subviews { + if view.tag == Map.mapTag { continue } + view.tag = nextTag + nextTag += 1 + collected.append(view) + stack.append(view) + } + } + return collected.reversed() + } + + func removeAllSubview() { + subviews.forEach { $0.removeFromSuperview() } + } +} + +extension CGRect { + static func fromJSObject(_ obj: JSObject) -> CGRect { + let x = obj["x"] as? Double ?? 0 + let y = obj["y"] as? Double ?? 0 + let width = obj["width"] as? Double ?? 0 + let height = obj["height"] as? Double ?? 0 + return CGRect(x: x, y: y, width: width, height: height) + } +} diff --git a/ios/Tests/NativeMapPluginTests/CapacitorAppleMapsTests.swift b/ios/Tests/NativeMapPluginTests/CapacitorAppleMapsTests.swift new file mode 100644 index 0000000..788a1d7 --- /dev/null +++ b/ios/Tests/NativeMapPluginTests/CapacitorAppleMapsTests.swift @@ -0,0 +1,490 @@ +import XCTest +import MapKit +import Capacitor +@testable import NativeMapPlugin + +class CapacitorNativeMapTests: XCTestCase { + + // MARK: - Zoom <-> region conversion + + /// The zoom <-> longitude-delta conversion should round-trip for a fixed + /// viewport width across the useful zoom range. + func testZoomRoundTrip() { + let width = 390.0 // typical iPhone point width + for zoom in stride(from: 3.0, through: 18.0, by: 1.0) { + let delta = zoomToLongitudeDelta(zoom, widthPoints: width) + let recovered = longitudeDeltaToZoom(delta, widthPoints: width) + XCTAssertEqual(zoom, recovered, accuracy: 0.0001, "zoom \(zoom) did not round-trip") + } + } + + /// A larger zoom must produce a narrower longitude span. + func testHigherZoomIsNarrower() { + let width = 390.0 + XCTAssertGreaterThan( + zoomToLongitudeDelta(5, widthPoints: width), + zoomToLongitudeDelta(12, widthPoints: width) + ) + } + + /// A zero/invalid viewport width must fall back rather than divide by zero. + func testZoomToDeltaHandlesZeroWidth() { + let delta = zoomToLongitudeDelta(10, widthPoints: 0) + XCTAssertTrue(delta.isFinite) + XCTAssertGreaterThan(delta, 0) + } + + /// A zero span must not produce a non-finite zoom. + func testLongitudeDeltaToZoomHandlesZeroDelta() { + let zoom = longitudeDeltaToZoom(0, widthPoints: 390) + XCTAssertTrue(zoom.isFinite) + } + + // MARK: - Zoom clamping (minZoom floor / maxZoom ceiling) + + func testClampZoomUnboundedIsIdentity() { + XCTAssertEqual(clampZoom(11.0, minZoom: nil, maxZoom: nil), 11.0) + } + + func testClampZoomAppliesFloorAndCeiling() { + XCTAssertEqual(clampZoom(3.0, minZoom: 7.0, maxZoom: nil), 7.0) // below floor + XCTAssertEqual(clampZoom(20.0, minZoom: nil, maxZoom: 18.0), 18.0) // above ceiling + XCTAssertEqual(clampZoom(12.0, minZoom: 7.0, maxZoom: 18.0), 12.0) // in range + } + + /// An inverted range should not trap the value between crossed bounds; the + /// ceiling wins (min is applied first, then max). + func testClampZoomInvertedRangeCeilingWins() { + XCTAssertEqual(clampZoom(12.0, minZoom: 18.0, maxZoom: 7.0), 7.0) + } + + // MARK: - fitBounds framing rect + + func testBoundingMapRectContainsBothCorners() { + let southWest = CLLocationCoordinate2D(latitude: 41.0, longitude: -73.0) + let northEast = CLLocationCoordinate2D(latitude: 43.0, longitude: -69.0) + let rect = boundingMapRect(southwest: southWest, northeast: northEast) + XCTAssertTrue(rect.contains(MKMapPoint(southWest))) + XCTAssertTrue(rect.contains(MKMapPoint(northEast))) + XCTAssertGreaterThan(rect.size.width, 0) + XCTAssertGreaterThan(rect.size.height, 0) + } + + /// The rect is orientation-independent: swapping the corners yields the same + /// rect (latitude grows north but MKMapPoint.y grows south). + func testBoundingMapRectIsOrientationIndependent() { + let southWest = CLLocationCoordinate2D(latitude: 41.0, longitude: -73.0) + let northEast = CLLocationCoordinate2D(latitude: 43.0, longitude: -69.0) + let rect = boundingMapRect(southwest: southWest, northeast: northEast) + let swapped = boundingMapRect(southwest: northEast, northeast: southWest) + XCTAssertEqual(rect.origin.x, swapped.origin.x, accuracy: 1e-6) + XCTAssertEqual(rect.origin.y, swapped.origin.y, accuracy: 1e-6) + XCTAssertEqual(rect.size.width, swapped.size.width, accuracy: 1e-6) + XCTAssertEqual(rect.size.height, swapped.size.height, accuracy: 1e-6) + } + + // MARK: - Marker payload parsing + + func testMakeMarkerReadsAllFields() { + let obj: JSObject = [ + "coordinate": ["lat": 42.36, "lng": -71.06] as JSObject, + "title": "Boston", + "snippet": "MA", + "iconUrl": "pin.png", + "iconSize": ["width": 30.0, "height": 36.0] as JSObject + ] + let marker = Map.makeMarker(from: obj) + XCTAssertNotNil(marker) + XCTAssertEqual(marker?.coordinate.latitude ?? 0, 42.36, accuracy: 1e-9) + XCTAssertEqual(marker?.title, "Boston") + XCTAssertEqual(marker?.subtitle, "MA") + XCTAssertEqual(marker?.iconUrl, "pin.png") + XCTAssertEqual(marker?.iconSize, CGSize(width: 30, height: 36)) + } + + func testMakeMarkerUsesCustomIdButIgnoresEmpty() { + let coordinate = ["lat": 0.0, "lng": 0.0] as JSObject + let custom = Map.makeMarker(from: ["coordinate": coordinate, "markerId": "abc"]) + XCTAssertEqual(custom?.markerId, "abc") + + let empty = Map.makeMarker(from: ["coordinate": coordinate, "markerId": ""]) + XCTAssertFalse(empty?.markerId.isEmpty ?? true) // fell back to a generated id + XCTAssertNotEqual(empty?.markerId, "") + } + + func testMakeMarkerParsesDraggable() { + let coordinate = ["lat": 1.0, "lng": 2.0] as JSObject + XCTAssertTrue(Map.makeMarker(from: ["coordinate": coordinate, "draggable": true])?.isDraggable ?? false) + // Defaults off when omitted, preserving the non-draggable behavior. + XCTAssertFalse(Map.makeMarker(from: ["coordinate": coordinate])?.isDraggable ?? true) + } + + func testMakeMarkerReturnsNilWithoutCoordinate() { + XCTAssertNil(Map.makeMarker(from: ["title": "no coord"])) + XCTAssertNil(Map.makeMarker(from: ["coordinate": ["lat": 1.0] as JSObject])) + } + + // MARK: - Overlay style parsing + + func testOverlayStyleUnfilledUsesDefaults() { + let style = Map.overlayStyle(from: [:], defaultLineWidth: 3, filled: false) + XCTAssertEqual(style.lineWidth, 3) + XCTAssertNil(style.fillColor) // polylines never fill + XCTAssertNotNil(style.strokeColor) // defaults to system blue + } + + func testOverlayStyleFilledParsesColorsAndWidth() { + let obj: JSObject = [ + "strokeColor": "#FF0000", + "strokeWeight": 5.0, + "fillColor": "#00FF00", + "fillOpacity": 0.5 + ] + let style = Map.overlayStyle(from: obj, defaultLineWidth: 2, filled: true) + XCTAssertEqual(style.lineWidth, 5) + + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + style.strokeColor.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(red, 1.0, accuracy: 1e-6) + + XCTAssertNotNil(style.fillColor) + style.fillColor?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(green, 1.0, accuracy: 1e-6) + XCTAssertEqual(alpha, 0.5, accuracy: 1e-6) + } + + // MARK: - CAPPluginCall string-array reading (diagnostic) + + /// Reproduces exactly how the bridge builds a call's options, to determine + /// whether `getArray` can read a JS string array (as removeMarkers / + /// removeOverlays need). + func testGetArrayReadsStringArrayFromCoercedOptions() { + let options = JSTypes.coerceDictionaryToJSObject(["id": "map", "ids": ["a", "b", "c"]]) ?? [:] + guard let call = CAPPluginCall( + callbackId: "t", methodName: "removeOverlays", + options: options, success: { _, _ in }, error: { _ in } + ) else { + XCTFail("could not construct CAPPluginCall") + return + } + let raw = call.getArray("ids") + XCTAssertNotNil(raw, "getArray returned nil for a string array") + XCTAssertEqual(raw?.compactMap { $0 as? String }, ["a", "b", "c"]) + XCTAssertNotNil(call.getArray("ids") as? [String], "whole-array cast returned nil") + } + + // MARK: - Region corners (visible bounds math) + + func testRegionCorners() { + let center = CLLocationCoordinate2D(latitude: 42.0, longitude: -71.0) + let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 4.0) + let corners = regionCorners(center: center, span: span) + + XCTAssertEqual(corners.southwest.latitude, 41.0, accuracy: 1e-9) + XCTAssertEqual(corners.southwest.longitude, -73.0, accuracy: 1e-9) + XCTAssertEqual(corners.northeast.latitude, 43.0, accuracy: 1e-9) + XCTAssertEqual(corners.northeast.longitude, -69.0, accuracy: 1e-9) + } + + // MARK: - Config parsing + + func testConfigParsesCenterAndZoom() throws { + let obj: JSObject = [ + "center": ["lat": 42.36, "lng": -71.06] as JSObject, + "zoom": 11.0, + "minZoom": 7.0 + ] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertEqual(config.center.latitude, 42.36, accuracy: 1e-9) + XCTAssertEqual(config.center.longitude, -71.06, accuracy: 1e-9) + XCTAssertEqual(config.zoom, 11.0) + XCTAssertEqual(config.minZoom, 7.0) + } + + func testConfigDefaultsZoomWhenMissing() throws { + let obj: JSObject = ["center": ["lat": 0.0, "lng": 0.0] as JSObject] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertEqual(config.zoom, 12.0) + XCTAssertNil(config.minZoom) + } + + func testConfigThrowsWithoutCenter() { + let obj: JSObject = ["zoom": 11.0] + XCTAssertThrowsError(try NativeMapConfig(fromJSObject: obj)) + } + + func testConfigParsesMaxZoomAndMapType() throws { + let obj: JSObject = [ + "center": ["lat": 0.0, "lng": 0.0] as JSObject, + "maxZoom": 18.0, + "mapType": "hybrid" + ] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertEqual(config.maxZoom, 18.0) + XCTAssertEqual(config.mapType, "hybrid") + } + + func testConfigDefaultsMapTypeToStandard() throws { + let obj: JSObject = ["center": ["lat": 0.0, "lng": 0.0] as JSObject] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertNil(config.maxZoom) + XCTAssertEqual(config.mapType, "standard") + } + + func testConfigParsesShowInfoWindows() throws { + let center = ["lat": 0.0, "lng": 0.0] as JSObject + let enabled = try NativeMapConfig(fromJSObject: ["center": center, "showInfoWindows": true]) + XCTAssertTrue(enabled.showInfoWindows) + // Defaults off, preserving the original tap-only behavior. + let disabled = try NativeMapConfig(fromJSObject: ["center": center]) + XCTAssertFalse(disabled.showInfoWindows) + } + + /// Parse the config through the SAME coercion the bridge applies to a JS + /// object (not a Swift literal), to catch a boolean that survives a literal + /// but not the real `create` path. + func testConfigParsesShowInfoWindowsThroughBridgeCoercion() throws { + let coerced = JSTypes.coerceDictionaryToJSObject([ + "center": ["lat": 1.0, "lng": 2.0], + "showInfoWindows": true + ]) ?? [:] + let config = try NativeMapConfig(fromJSObject: coerced) + XCTAssertTrue(config.showInfoWindows) + } + + // MARK: - Map type mapping + + func testMapTypeMappingIsCaseInsensitive() { + XCTAssertEqual(Map.mapType(from: "satellite"), .satellite) + XCTAssertEqual(Map.mapType(from: "Hybrid"), .hybrid) + XCTAssertEqual(Map.mapType(from: "satelliteFlyover"), .satelliteFlyover) + XCTAssertEqual(Map.mapType(from: "hybridflyover"), .hybridFlyover) + XCTAssertEqual(Map.mapType(from: "mutedStandard"), .mutedStandard) + } + + func testMapTypeUnknownFallsBackToStandard() { + XCTAssertEqual(Map.mapType(from: "standard"), .standard) + XCTAssertEqual(Map.mapType(from: "nonsense"), .standard) + } + + // MARK: - Overlay coordinate parsing + + func testParseCoordsReadsLatLngObjects() { + let arr: [JSObject] = [["lat": 1.0, "lng": 2.0], ["lat": 3.0, "lng": 4.0]] + let coords = Map.parseCoords(arr) + XCTAssertEqual(coords.count, 2) + XCTAssertEqual(coords[0].latitude, 1.0, accuracy: 1e-9) + XCTAssertEqual(coords[1].longitude, 4.0, accuracy: 1e-9) + } + + func testParseCoordsIgnoresMalformedEntriesAndNonArrays() { + let mixed: [JSObject] = [["lat": 1.0, "lng": 2.0], ["lat": 3.0]] + XCTAssertEqual(Map.parseCoords(mixed).count, 1) + XCTAssertEqual(Map.parseCoords(nil).count, 0) + XCTAssertEqual(Map.parseCoords("not an array").count, 0) + } + + /// A flat ring of coordinates yields a single exterior ring. + func testParseRingsSingleRing() { + let ring: [JSObject] = [ + ["lat": 0.0, "lng": 0.0], ["lat": 0.0, "lng": 1.0], ["lat": 1.0, "lng": 1.0] + ] + let rings = Map.parseRings(ring) + XCTAssertEqual(rings.count, 1) + XCTAssertEqual(rings[0].count, 3) + } + + /// An array of rings yields exterior + holes in order. + func testParseRingsMultipleRings() { + let exterior: [JSObject] = [ + ["lat": 0.0, "lng": 0.0], ["lat": 0.0, "lng": 4.0], ["lat": 4.0, "lng": 4.0] + ] + let hole: [JSObject] = [ + ["lat": 1.0, "lng": 1.0], ["lat": 1.0, "lng": 2.0], ["lat": 2.0, "lng": 2.0] + ] + let rings = Map.parseRings([exterior, hole]) + XCTAssertEqual(rings.count, 2) + XCTAssertEqual(rings[0].count, 3) + XCTAssertEqual(rings[1].count, 3) + } + + // MARK: - Hex color parsing + + func testColorParsesSixDigitHex() { + let color = Map.color("#FF0000", opacity: nil) + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + XCTAssertNotNil(color) + color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(red, 1.0, accuracy: 1e-6) + XCTAssertEqual(green, 0.0, accuracy: 1e-6) + XCTAssertEqual(blue, 0.0, accuracy: 1e-6) + XCTAssertEqual(alpha, 1.0, accuracy: 1e-6) + } + + func testColorParsesEightDigitHexAlpha() { + let color = Map.color("#0000FF80", opacity: nil) + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(blue, 1.0, accuracy: 1e-6) + XCTAssertEqual(alpha, 128.0 / 255.0, accuracy: 1e-6) + } + + /// An explicit opacity overrides any alpha baked into the hex string. + func testColorOpacityOverridesHexAlpha() { + let color = Map.color("#0000FF80", opacity: 0.5) + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + color?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(alpha, 0.5, accuracy: 1e-6) + } + + func testColorReturnsNilForMalformedInput() { + XCTAssertNil(Map.color(nil, opacity: nil)) + XCTAssertNil(Map.color("not-a-color", opacity: nil)) + XCTAssertNil(Map.color("#FFFF", opacity: nil)) // unsupported length + } + + // MARK: - CGRect parsing + + func testCGRectFromJSObject() { + let obj: JSObject = ["x": 1.0, "y": 2.0, "width": 3.0, "height": 4.0] + let rect = CGRect.fromJSObject(obj) + XCTAssertEqual(rect.origin.x, 1.0) + XCTAssertEqual(rect.origin.y, 2.0) + XCTAssertEqual(rect.size.width, 3.0) + XCTAssertEqual(rect.size.height, 4.0) + } + + func testCGRectFromJSObjectDefaultsToZero() { + let rect = CGRect.fromJSObject([:]) + XCTAssertEqual(rect, .zero) + } + + // MARK: - Search distance filter + + func testWithinDistanceNoFilterWhenLimitMissing() { + let boston = CLLocation(latitude: 42.36, longitude: -71.06) + let saoPaulo = CLLocationCoordinate2D(latitude: -23.55, longitude: -46.63) + // No/zero limit means "keep everything". + XCTAssertTrue(withinDistance(maxKm: nil, from: boston, to: saoPaulo)) + XCTAssertTrue(withinDistance(maxKm: 0, from: boston, to: saoPaulo)) + // No center means "keep everything". + XCTAssertTrue(withinDistance(maxKm: 800, from: nil, to: saoPaulo)) + } + + func testWithinDistanceKeepsNearbyDropsFar() { + let boston = CLLocation(latitude: 42.36, longitude: -71.06) + let portland = CLLocationCoordinate2D(latitude: 43.66, longitude: -70.26) // ~150 km + let saoPaulo = CLLocationCoordinate2D(latitude: -23.55, longitude: -46.63) // ~7000 km + XCTAssertTrue(withinDistance(maxKm: 800, from: boston, to: portland)) + XCTAssertFalse(withinDistance(maxKm: 800, from: boston, to: saoPaulo)) + } +} + +// MARK: - Appearance toggles (#8) +// +// In an extension so the primary test-class body stays within SwiftLint's +// type_body_length budget. +extension CapacitorNativeMapTests { + + // MARK: Color scheme mapping (dark-mode override) + + func testUserInterfaceStyleMapping() { + XCTAssertEqual(Map.userInterfaceStyle(from: "light"), .light) + XCTAssertEqual(Map.userInterfaceStyle(from: "Dark"), .dark) + } + + func testUserInterfaceStyleDefaultsToUnspecified() { + XCTAssertEqual(Map.userInterfaceStyle(from: "default"), .unspecified) + XCTAssertEqual(Map.userInterfaceStyle(from: "nonsense"), .unspecified) + } + + // MARK: Appearance config parsing + + func testConfigParsesAppearanceToggles() throws { + let obj: JSObject = [ + "center": ["lat": 0.0, "lng": 0.0] as JSObject, + "showsTraffic": true, + "showsPointsOfInterest": false, + "showsCompass": false, + "showsScale": true, + "colorScheme": "dark" + ] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertTrue(config.showsTraffic) + XCTAssertFalse(config.showsPointsOfInterest) + XCTAssertFalse(config.showsCompass) + XCTAssertTrue(config.showsScale) + XCTAssertEqual(config.colorScheme, "dark") + } + + /// The defaults mirror MapKit's own: traffic/scale off, POI/compass on, + /// system color scheme. + func testConfigAppearanceDefaults() throws { + let config = try NativeMapConfig(fromJSObject: ["center": ["lat": 0.0, "lng": 0.0] as JSObject]) + XCTAssertFalse(config.showsTraffic) + XCTAssertTrue(config.showsPointsOfInterest) + XCTAssertTrue(config.showsCompass) + XCTAssertFalse(config.showsScale) + XCTAssertEqual(config.colorScheme, "default") + } + + // MARK: Gestures + padding (#10) + + func testConfigParsesGestures() throws { + let obj: JSObject = [ + "center": ["lat": 0.0, "lng": 0.0] as JSObject, + "gestures": ["scroll": false, "zoom": false, "rotate": true, "pitch": false] as JSObject + ] + let config = try NativeMapConfig(fromJSObject: obj) + XCTAssertFalse(config.scrollEnabled) + XCTAssertFalse(config.zoomEnabled) + XCTAssertTrue(config.rotateEnabled) + XCTAssertFalse(config.pitchEnabled) + } + + /// Gestures all default on; an omitted gesture key keeps that gesture enabled. + func testConfigGestureDefaults() throws { + let config = try NativeMapConfig(fromJSObject: ["center": ["lat": 0.0, "lng": 0.0] as JSObject]) + XCTAssertTrue(config.scrollEnabled) + XCTAssertTrue(config.zoomEnabled) + XCTAssertTrue(config.rotateEnabled) + XCTAssertTrue(config.pitchEnabled) + } + + func testParsePaddingReadsSidesAndDefaultsToZero() { + let insets = NativeMapConfig.parsePadding(["top": 10.0, "left": 20.0, "bottom": 30.0] as JSObject) + XCTAssertEqual(insets.top, 10, accuracy: 1e-6) + XCTAssertEqual(insets.left, 20, accuracy: 1e-6) + XCTAssertEqual(insets.bottom, 30, accuracy: 1e-6) + XCTAssertEqual(insets.right, 0, accuracy: 1e-6) // omitted side defaults to 0 + XCTAssertEqual(NativeMapConfig.parsePadding(nil), .zero) + XCTAssertEqual(NativeMapConfig.parsePadding("not an object"), .zero) + } + + // MARK: - Autocomplete result types + + /// No option at all keeps the behaviour every existing caller relies on. + func testResultTypesDefaultWhenOmitted() { + XCTAssertEqual(parseCompleterResultTypes(nil), [.address, .pointOfInterest]) + } + + /// A "where" field asks for addresses alone and must not get landmarks back. + func testResultTypesAddressOnly() { + XCTAssertEqual(parseCompleterResultTypes(["address"]), [.address]) + } + + func testResultTypesCombine() { + XCTAssertEqual(parseCompleterResultTypes(["pointOfInterest", "query"]), [.pointOfInterest, .query]) + } + + /// An empty option set makes the completer return nothing, which looks like + /// a broken search - so nothing usable means the default, not silence. + func testResultTypesFallBackWhenNothingUsable() { + XCTAssertEqual(parseCompleterResultTypes([]), defaultCompleterResultTypes) + XCTAssertEqual(parseCompleterResultTypes(["landmark"]), defaultCompleterResultTypes) + } + + func testResultTypesIgnoreUnknownNames() { + XCTAssertEqual(parseCompleterResultTypes(["address", "landmark"]), [.address]) + } +} diff --git a/ios/Tests/NativeMapPluginTests/GeocodeTests.swift b/ios/Tests/NativeMapPluginTests/GeocodeTests.swift new file mode 100644 index 0000000..8f5cb97 --- /dev/null +++ b/ios/Tests/NativeMapPluginTests/GeocodeTests.swift @@ -0,0 +1,32 @@ +import XCTest +@testable import NativeMapPlugin + +// MARK: - Geocoding address formatting +final class GeocodeTests: XCTestCase { + + /// The postal formatter writes one line per address row; a heading wants one line. + func testSingleLineAddressJoinsRows() { + XCTAssertEqual( + singleLineAddress("1 Main St\nBoston MA 02110\nUnited States"), + "1 Main St, Boston MA 02110, United States" + ) + } + + func testSingleLineAddressDropsBlankRows() { + XCTAssertEqual(singleLineAddress("\n Boston MA \n\nUnited States\n"), "Boston MA, United States") + XCTAssertEqual(singleLineAddress(""), "") + } + + func testJoinedAddressPartsSkipsMissingAndEmpty() { + XCTAssertEqual(joinedAddressParts([nil, "Boston", "", "MA", " "]), "Boston, MA") + } + + /// A town's placemark is named after the town, which must not print twice. + func testJoinedAddressPartsSkipsImmediateRepeats() { + XCTAssertEqual(joinedAddressParts(["Boston", "Boston", "MA"]), "Boston, MA") + } + + func testJoinedAddressPartsNilWhenNothingUsable() { + XCTAssertNil(joinedAddressParts([nil, "", " "])) + } +} diff --git a/ios/Tests/NativeMapPluginTests/MapStateTests.swift b/ios/Tests/NativeMapPluginTests/MapStateTests.swift new file mode 100644 index 0000000..2e5b5f5 --- /dev/null +++ b/ios/Tests/NativeMapPluginTests/MapStateTests.swift @@ -0,0 +1,210 @@ +import XCTest +import MapKit +import Capacitor +@testable import NativeMapPlugin + +/// Headless `Map` bookkeeping: the marker/overlay/clustering state a live map +/// tracks, verified without rendering. `MKMapView` works fine off-screen in a unit +/// test, so these assert the data structures and flags Map maintains (not pixels). +/// Tests run on the main thread (XCTest's default), where `runOnMainSync` executes +/// inline; the few `DispatchQueue.main.async` setters are flushed explicitly. +final class MapStateTests: XCTestCase { + + /// The Map holds its delegate weakly, so the plugin must outlive it. A `lazy` + /// stored property does that and is created fresh for each test instance. + private lazy var plugin = NativeMapPlugin() + + private func makeMap(clustering: Bool = false) throws -> Map { + let config = try NativeMapConfig(fromJSObject: [ + "center": ["lat": 42.0, "lng": -71.0] as JSObject, + "zoom": 10.0, + "clustering": clustering + ]) + return Map(id: "test", config: config, delegate: plugin) + } + + /// Let queued `DispatchQueue.main.async` work (e.g. render(), setMapType) run. + /// Enqueued after those blocks on the serial main queue, so it drains them. + private func flushMainQueue() { + let drained = expectation(description: "main queue drained") + DispatchQueue.main.async { drained.fulfill() } + wait(for: [drained], timeout: 1) + } + + // MARK: - Markers + + func testAddAndRemoveMarkersTrackIds() throws { + let map = try makeMap() + let ids = map.addMarkers([ + ["coordinate": ["lat": 1.0, "lng": 2.0] as JSObject], + ["coordinate": ["lat": 3.0, "lng": 4.0] as JSObject, "markerId": "custom"] + ]) + XCTAssertEqual(ids.count, 2) + XCTAssertEqual(map.markers.count, 2) + XCTAssertTrue(ids.contains("custom")) + XCTAssertNotNil(map.markers["custom"]) + + map.removeMarkers(["custom"]) + XCTAssertNil(map.markers["custom"]) + XCTAssertEqual(map.markers.count, 1) + } + + func testMalformedMarkerIsSkipped() throws { + let map = try makeMap() + let ids = map.addMarkers([ + ["coordinate": ["lat": 1.0, "lng": 2.0] as JSObject], + ["title": "no coordinate"] // dropped: no coordinate + ]) + XCTAssertEqual(ids.count, 1) + XCTAssertEqual(map.markers.count, 1) + } + + func testUpdateMarkersAppliesFields() throws { + let map = try makeMap() + let id = try XCTUnwrap(map.addMarkers([["coordinate": ["lat": 1.0, "lng": 2.0] as JSObject]]).first) + map.updateMarkers([[ + "markerId": id, + "title": "Updated", + "opacity": 0.4, + "zIndex": 5.0 + ]]) + let marker = map.markers[id] + XCTAssertEqual(marker?.title, "Updated") + XCTAssertEqual(marker?.opacity, 0.4) + XCTAssertEqual(marker?.zIndex, 5.0) + } + + // MARK: - Overlays + + func testOverlayRegisterAndRemove() throws { + let map = try makeMap() + let polylineIds = map.addPolylines([[ + "path": [["lat": 0.0, "lng": 0.0] as JSObject, ["lat": 1.0, "lng": 1.0] as JSObject] + ]]) + let circleIds = map.addCircles([[ + "center": ["lat": 0.0, "lng": 0.0] as JSObject, "radius": 100.0 + ]]) + XCTAssertEqual(polylineIds.count, 1) + XCTAssertEqual(circleIds.count, 1) + XCTAssertEqual(map.overlays.count, 2) + XCTAssertEqual(map.overlayStyles.count, 2) + + map.removeOverlays(polylineIds + circleIds) + XCTAssertTrue(map.overlays.isEmpty) + XCTAssertTrue(map.overlayStyles.isEmpty) + } + + func testMalformedOverlayIsSkipped() throws { + let map = try makeMap() + // A polyline needs at least two points; one point is dropped. + let ids = map.addPolylines([["path": [["lat": 0.0, "lng": 0.0] as JSObject]]]) + XCTAssertTrue(ids.isEmpty) + XCTAssertTrue(map.overlays.isEmpty) + } + + // MARK: - Clustering + + func testClusteringToggleAndThreshold() throws { + let map = try makeMap() + XCTAssertFalse(map.clusteringEnabled) + XCTAssertFalse(map.shouldCluster) + + map.enableClustering(minClusterSize: 3) + XCTAssertTrue(map.clusteringEnabled) + XCTAssertEqual(map.clusterMinSize, 3) + XCTAssertFalse(map.shouldCluster) // no markers yet + + _ = map.addMarkers([ + ["coordinate": ["lat": 1.0, "lng": 1.0] as JSObject], + ["coordinate": ["lat": 2.0, "lng": 2.0] as JSObject] + ]) + XCTAssertFalse(map.shouldCluster) // 2 < 3 + _ = map.addMarkers([["coordinate": ["lat": 3.0, "lng": 3.0] as JSObject]]) + XCTAssertTrue(map.shouldCluster) // 3 >= 3 + + map.disableClustering() + XCTAssertFalse(map.clusteringEnabled) + XCTAssertFalse(map.shouldCluster) + } + + func testConfigClusteringStartsEnabledWithDefaultMinSize() throws { + let map = try makeMap(clustering: true) + XCTAssertTrue(map.clusteringEnabled) + XCTAssertEqual(map.clusterMinSize, 2) + } + + func testClusteringDrivesAnnotationViewPriority() throws { + let map = try makeMap() + _ = map.addMarkers([ + ["coordinate": ["lat": 1.0, "lng": 1.0] as JSObject], + ["coordinate": ["lat": 1.0, "lng": 1.0] as JSObject] + ]) + let marker = try XCTUnwrap(map.markers.values.first) + + // Clustering off: no clustering id, and `.required` so the pin is never + // hidden by decluttering. + let unclustered = try XCTUnwrap(map.annotationView(for: marker, in: map.mapView)) + XCTAssertNil(unclustered.clusteringIdentifier) + XCTAssertEqual(unclustered.displayPriority, .required) + + // Clustering on: a clustering id, and a below-required priority so MapKit is + // actually allowed to collapse overlapping markers into a bubble. + map.enableClustering() + let clustered = try XCTUnwrap(map.annotationView(for: marker, in: map.mapView)) + XCTAssertEqual(clustered.clusteringIdentifier, Map.clusterReuseId) + XCTAssertEqual(clustered.displayPriority, .defaultHigh) + } + + func testGeodesicPolylineUsesGeodesicClass() throws { + let map = try makeMap() + let ids = map.addPolylines([[ + "path": [["lat": 0.0, "lng": 0.0] as JSObject, ["lat": 10.0, "lng": 80.0] as JSObject], + "geodesic": true + ]]) + let overlay = try XCTUnwrap(map.overlays[try XCTUnwrap(ids.first)]) + XCTAssertTrue(overlay is MKGeodesicPolyline) + + // A plain polyline is not geodesic. + let plainId = try XCTUnwrap(map.addPolylines([[ + "path": [["lat": 0.0, "lng": 0.0] as JSObject, ["lat": 1.0, "lng": 1.0] as JSObject] + ]]).first) + XCTAssertFalse(try XCTUnwrap(map.overlays[plainId]) is MKGeodesicPolyline) + } + + // MARK: - Selection + + func testSelectMarkerReportsFoundState() throws { + let map = try makeMap() + let id = try XCTUnwrap(map.addMarkers([["coordinate": ["lat": 1.0, "lng": 2.0] as JSObject]]).first) + XCTAssertTrue(map.selectMarker(id)) + XCTAssertFalse(map.selectMarker("does-not-exist")) + map.deselectMarker() // no-op with nothing open; must not crash + } + + // MARK: - Camera boundary + + func testCameraBoundarySetAndClear() throws { + let map = try makeMap() + map.setCameraBoundary( + southwest: CLLocationCoordinate2D(latitude: 40, longitude: -75), + northeast: CLLocationCoordinate2D(latitude: 43, longitude: -70) + ) + XCTAssertNotNil(map.mapView.cameraBoundary) + + map.setCameraBoundary(southwest: nil, northeast: nil) + XCTAssertNil(map.mapView.cameraBoundary) + } + + // MARK: - Map type + + func testSetMapTypeRoundTripsThroughMap() throws { + let map = try makeMap() + map.setMapType("hybrid") + flushMainQueue() + XCTAssertEqual(map.currentMapType(), "hybrid") + + map.setMapType("satellite") + flushMainQueue() + XCTAssertEqual(map.currentMapType(), "satellite") + } +} diff --git a/ios/Tests/NativeMapPluginTests/MarkerAnchorTests.swift b/ios/Tests/NativeMapPluginTests/MarkerAnchorTests.swift new file mode 100644 index 0000000..ffe3dfc --- /dev/null +++ b/ios/Tests/NativeMapPluginTests/MarkerAnchorTests.swift @@ -0,0 +1,76 @@ +import XCTest +import MapKit +import Capacitor +@testable import NativeMapPlugin + +/// Icon anchoring: how a marker payload's `iconAnchor` is parsed and turned into +/// the `MKAnnotationView.centerOffset` that lands the anchor point on the +/// coordinate. Split from CapacitorNativeMapTests to keep that file's body within +/// SwiftLint's length budget. +class MarkerAnchorTests: XCTestCase { + + func testMakeMarkerParsesIconAnchor() { + let coordinate = ["lat": 1.0, "lng": 2.0] as JSObject + let anchored = Map.makeMarker(from: [ + "coordinate": coordinate, + "iconAnchor": ["x": 0.5, "y": 0.5] as JSObject + ]) + XCTAssertEqual(anchored?.iconAnchor, CGPoint(x: 0.5, y: 0.5)) + // Omitted stays nil, so centerOffset keeps the bottom-centre default. + XCTAssertNil(Map.makeMarker(from: ["coordinate": coordinate])?.iconAnchor) + // A malformed anchor is ignored rather than half-applied. + XCTAssertNil(Map.makeMarker(from: ["coordinate": coordinate, "iconAnchor": ["x": 0.5] as JSObject])?.iconAnchor) + } + + func testCenterOffsetHonoursAnchor() { + let size = CGSize(width: 40, height: 60) + let marker = NativeMapMarker() + + // Default (nil) anchors the bottom-centre - the classic teardrop tip. + XCTAssertEqual(marker.centerOffset(for: size), CGPoint(x: 0, y: -30)) + + marker.iconAnchor = CGPoint(x: 0.5, y: 0.5) // centred, as a dot wants + XCTAssertEqual(marker.centerOffset(for: size), .zero) + + marker.iconAnchor = CGPoint(x: 0, y: 0) // top-left corner on the coordinate + XCTAssertEqual(marker.centerOffset(for: size), CGPoint(x: 20, y: 30)) + } + + func testApplyIconUpdatesReportsAndResets() { + let marker = NativeMapMarker() + marker.iconAnchor = CGPoint(x: 0.5, y: 0.5) + + // A present iconAnchor of null resets to the bottom-centre default. + XCTAssertTrue(marker.applyIconUpdates(from: ["iconAnchor": NSNull()] as JSObject)) + XCTAssertNil(marker.iconAnchor) + + // No icon fields present โ†’ nothing changed, so no re-render is requested. + XCTAssertFalse(marker.applyIconUpdates(from: ["title": "unrelated"] as JSObject)) + } + + func testApplyIconUpdatesUrlAndSize() { + let marker = NativeMapMarker() + + // A url change is applied and reported (so the caller re-renders). + XCTAssertTrue(marker.applyIconUpdates(from: ["iconUrl": "pin.png"] as JSObject)) + XCTAssertEqual(marker.iconUrl, "pin.png") + + // A present iconUrl of null clears it (and still reports a change). + XCTAssertTrue(marker.applyIconUpdates(from: ["iconUrl": NSNull()] as JSObject)) + XCTAssertNil(marker.iconUrl) + + // A size change is applied and reported. + XCTAssertTrue(marker.applyIconUpdates(from: ["iconSize": ["width": 30.0, "height": 40.0] as JSObject])) + XCTAssertEqual(marker.iconSize, CGSize(width: 30, height: 40)) + } + + func testParseSizeValidAndMalformed() { + XCTAssertEqual( + NativeMapMarker.parseSize(["width": 12.0, "height": 24.0] as JSObject), + CGSize(width: 12, height: 24) + ) + // Missing a dimension, wrong type, or absent โ†’ nil (never half-applied). + XCTAssertNil(NativeMapMarker.parseSize(["width": 12.0] as JSObject)) + XCTAssertNil(NativeMapMarker.parseSize(nil)) + } +} diff --git a/ios/Tests/NativeMapPluginTests/MarkerStyleTests.swift b/ios/Tests/NativeMapPluginTests/MarkerStyleTests.swift new file mode 100644 index 0000000..36230cd --- /dev/null +++ b/ios/Tests/NativeMapPluginTests/MarkerStyleTests.swift @@ -0,0 +1,80 @@ +import XCTest +import MapKit +import Capacitor +@testable import NativeMapPlugin + +/// Parity additions: marker opacity/tint/z-order parsing and the `MKMapType` +/// string round-trip behind get/setMapType. Split from CapacitorNativeMapTests to +/// keep that file's body within SwiftLint's length budget. +class MarkerStyleTests: XCTestCase { + + func testMakeMarkerParsesStyleFields() { + let coordinate = ["lat": 1.0, "lng": 2.0] as JSObject + let marker = Map.makeMarker(from: [ + "coordinate": coordinate, + "opacity": 0.5, + "zIndex": 7.0, + "tintColor": ["r": 255.0, "g": 0.0, "b": 0.0, "a": 255.0] as JSObject + ]) + XCTAssertEqual(marker?.opacity, 0.5) + XCTAssertEqual(marker?.zIndex, 7.0) + XCTAssertNotNil(marker?.tintColor) + + // Defaults when omitted: fully opaque, z 0, no tint. + let plain = Map.makeMarker(from: ["coordinate": coordinate]) + XCTAssertEqual(plain?.opacity, 1) + XCTAssertEqual(plain?.zIndex, 0) + XCTAssertNil(plain?.tintColor) + } + + func testParseTintColor() { + // r/g/b are 0..255; alpha defaults to fully opaque when absent. + let opaqueRed = NativeMapMarker.parseTintColor(["r": 255.0, "g": 0.0, "b": 0.0] as JSObject) + var red: CGFloat = 0, green: CGFloat = 0, blue: CGFloat = 0, alpha: CGFloat = 0 + opaqueRed?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(red, 1, accuracy: 0.01) + XCTAssertEqual(green, 0, accuracy: 0.01) + XCTAssertEqual(blue, 0, accuracy: 0.01) + XCTAssertEqual(alpha, 1, accuracy: 0.01) + + // A half-alpha value is honoured. + let half = NativeMapMarker.parseTintColor(["r": 0.0, "g": 0.0, "b": 0.0, "a": 128.0] as JSObject) + half?.getRed(&red, green: &green, blue: &blue, alpha: &alpha) + XCTAssertEqual(alpha, 128.0 / 255.0, accuracy: 0.01) + + // Malformed (missing a channel) is ignored rather than half-applied. + XCTAssertNil(NativeMapMarker.parseTintColor(["r": 255.0, "g": 0.0] as JSObject)) + } + + func testMapTypeStringRoundTrip() { + let names = ["standard", "satellite", "hybrid", "satelliteFlyover", "hybridFlyover", "mutedStandard"] + for name in names { + XCTAssertEqual(Map.mapTypeString(from: Map.mapType(from: name)), name) + } + // An unknown type string falls back to standard. + XCTAssertEqual(Map.mapType(from: "bogus"), .standard) + } + + func testUserTrackingModeMapping() { + XCTAssertEqual(Map.userTrackingMode(from: "follow"), .follow) + XCTAssertEqual(Map.userTrackingMode(from: "followWithHeading"), .followWithHeading) + XCTAssertEqual(Map.userTrackingMode(from: "none"), MKUserTrackingMode.none) + // Unknown/absent falls back to no tracking. + XCTAssertEqual(Map.userTrackingMode(from: "bogus"), MKUserTrackingMode.none) + } + + func testParseDashPattern() { + XCTAssertEqual(Map.parseDashPattern([8.0, 4.0]), [NSNumber(value: 8.0), NSNumber(value: 4.0)]) + // Empty, non-array, and absent all mean "solid line". + XCTAssertNil(Map.parseDashPattern([])) + XCTAssertNil(Map.parseDashPattern("dashed")) + XCTAssertNil(Map.parseDashPattern(nil)) + } + + func testConfigParsesShowsBuildings() throws { + let center = ["lat": 0.0, "lng": 0.0] as JSObject + // Defaults to true (MapKit's default), and honours an explicit false. + XCTAssertTrue(try NativeMapConfig(fromJSObject: ["center": center]).showsBuildings) + XCTAssertFalse(try NativeMapConfig(fromJSObject: ["center": center, "showsBuildings": false]).showsBuildings) + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..98ee402 --- /dev/null +++ b/package.json @@ -0,0 +1,109 @@ +{ + "name": "@capgo/capacitor-native-map", + "version": "8.0.0", + "description": "Native maps for Capacitor: Google Maps on Android, Apple MapKit on iOS, Google Maps JS on web", + "license": "MPL-2.0", + "main": "dist/plugin.cjs.js", + "module": "dist/esm/index.js", + "types": "dist/typings/index.d.ts", + "unpkg": "dist/plugin.js", + "files": [ + "android/src/main/", + "android/build.gradle", + "dist/", + "ios/Sources", + "ios/Tests", + "Package.swift", + "CapgoCapacitorNativeMap.podspec", + "assets/readme-demo.webp", + "assets/readme-screenshot-ios.webp", + "assets/readme-screenshot-android.webp", + "assets/github-social-preview.png", + "assets/github-social-template.svg" + ], + "author": "Martin Donadieu", + "repository": { + "type": "git", + "url": "git+https://github.com/Cap-go/capacitor-native-map.git" + }, + "bugs": { + "url": "https://github.com/Cap-go/capacitor-native-map/issues" + }, + "homepage": "https://capgo.app/docs/plugins/native-map/", + "keywords": [ + "capacitor", + "plugin", + "maps", + "google-maps", + "mapkit", + "capgo", + "native" + ], + "scripts": { + "verify": "bun run verify:ios && bun run verify:android && bun run verify:web", + "verify:ios": "xcodebuild -scheme CapgoCapacitorNativeMap -destination generic/platform=iOS", + "verify:android": "cd android && ./gradlew clean build test && cd ..", + "test": "bun test", + "verify:web": "bun run build && bun test", + "lint": "bun run eslint && bun run prettier -- --check && bun run swiftlint -- lint", + "fmt": "bun run eslint -- --fix && bun run prettier -- --write && bun run swiftlint -- --fix --format", + "eslint": "eslint . --ext .ts", + "prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java", + "swiftlint": "node-swiftlint", + "docgen": "docgen --api NativeMapPlugin --output-readme README.md --output-json dist/docs.json", + "build": "bun run clean && bun run docgen && tsc && rollup -c rollup.config.mjs", + "clean": "rimraf ./dist", + "watch": "tsc --watch", + "example:install": "cd example-app && bun install", + "example:build": "cd example-app && bun run build", + "prepublishOnly": "bun run build", + "check:wiring": "node scripts/check-capacitor-plugin-wiring.mjs", + "check:cap9-deprecated": "node scripts/check-cap9-deprecated.mjs", + "changelog:ai": "node scripts/generate-ai-changelog.mjs" + }, + "dependencies": { + "@googlemaps/js-api-loader": "^2.0.2", + "@googlemaps/markerclusterer": "^2.6.2", + "@types/google.maps": "^3.58.1" + }, + "devDependencies": { + "@capacitor/android": "^8.0.0", + "@capacitor/cli": "^8.0.0", + "@capacitor/core": "^8.0.0", + "@capacitor/docgen": "^0.3.1", + "@capacitor/ios": "^8.0.0", + "@happy-dom/global-registrator": "^20.14.5", + "@ionic/eslint-config": "^0.4.0", + "@ionic/prettier-config": "^4.0.0", + "@ionic/swiftlint-config": "^2.0.0", + "@rollup/plugin-node-resolve": "^16.0.3", + "@types/node": "^24.10.1", + "@types/resize-observer-browser": "^0.1.11", + "eslint": "^8.57.1", + "eslint-plugin-import": "^2.31.0", + "happy-dom": "^20.14.5", + "prettier": "^3.6.2", + "prettier-plugin-java": "^2.7.7", + "prettier-pretty-check": "^0.2.0", + "rimraf": "^6.1.0", + "rollup": "^4.53.2", + "swiftlint": "^2.0.0", + "typescript": "^6.0.3" + }, + "peerDependencies": { + "@capacitor/core": ">=8.0.0" + }, + "prettier": "@ionic/prettier-config", + "swiftlint": "@ionic/swiftlint-config", + "eslintConfig": { + "extends": "@ionic/eslint-config/recommended" + }, + "capacitor": { + "ios": { + "src": "ios" + }, + "android": { + "src": "android" + } + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..2cc02e4 --- /dev/null +++ b/renovate.json @@ -0,0 +1,93 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base", + "schedule:earlyMondays" + ], + "dependencyDashboard": false, + "lockFileMaintenance": { + "enabled": true, + "automerge": true, + "automergeType": "branch", + "platformAutomerge": true + }, + "packageRules": [ + { + "matchUpdateTypes": [ + "minor", + "patch" + ], + "matchCurrentVersion": "!/^0/", + "automerge": true + }, + { + "matchPackageNames": [ + "eslint" + ], + "matchManagers": [ + "npm" + ], + "enabled": false + }, + { + "matchPackagePatterns": [ + "^com\\.google\\..*" + ], + "matchCurrentVersion": "/^.*-android$/", + "allowedVersions": "/^.*-android$/" + }, + { + "matchPackagePatterns": [ + "^com\\.android\\.tools\\.build\\:gradle$" + ], + "matchManagers": [ + "gradle" + ], + "enabled": false + }, + { + "matchManagers": [ + "swift" + ], + "matchPackagePatterns": [ + "^https://github.com/ionic-team/capacitor-swift-pm\\.git$", + "^ionic-team/capacitor-swift-pm$", + "^capacitor-swift-pm$" + ], + "allowedVersions": "8.0.0", + "enabled": false + }, + { + "matchDepTypes": [ + "peerDependencies" + ], + "matchPackageNames": [ + "@capacitor/core" + ], + "matchUpdateTypes": [ + "minor", + "patch" + ], + "enabled": false + }, + { + "matchDepTypes": [ + "peerDependencies" + ], + "matchPackageNames": [ + "@capacitor/core" + ], + "rangeStrategy": "replace", + "allowedVersions": "/^>=\\d+\\.0\\.0$/" + }, + { + "matchPackageNames": [ + "androidx.core:core" + ], + "matchManagers": [ + "gradle" + ], + "enabled": false + } + ] +} \ No newline at end of file diff --git a/rollup.config.mjs b/rollup.config.mjs new file mode 100644 index 0000000..cfa7adb --- /dev/null +++ b/rollup.config.mjs @@ -0,0 +1,25 @@ +import { nodeResolve } from '@rollup/plugin-node-resolve'; + +export default { + input: 'dist/esm/index.js', + plugins: [nodeResolve({ preferBuiltins: false })], + output: [ + { + file: 'dist/plugin.js', + format: 'iife', + name: 'capacitorNativeMap', + globals: { + '@capacitor/core': 'capacitorExports', + }, + sourcemap: true, + inlineDynamicImports: true, + }, + { + file: 'dist/plugin.cjs.js', + format: 'cjs', + sourcemap: true, + inlineDynamicImports: true, + }, + ], + external: ['@capacitor/core'], +}; diff --git a/scripts/check-cap9-deprecated.mjs b/scripts/check-cap9-deprecated.mjs new file mode 100644 index 0000000..8502fb0 --- /dev/null +++ b/scripts/check-cap9-deprecated.mjs @@ -0,0 +1,304 @@ +#!/usr/bin/env node +/** + * Capacitor 9 deprecated native API guard. + * + * Fails when plugin native sources still use APIs removed in Capacitor 9. + * Does not flag Cordova SwiftPM product dependencies (still required on Cap 8). + * + * Usage: + * node scripts/check-cap9-deprecated.mjs + * node scripts/check-cap9-deprecated.mjs --workspace + * node scripts/check-cap9-deprecated.mjs --dir path + */ + +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const SKIP_DIRS = new Set([ + "node_modules", + "dist", + "build", + ".build", + ".gradle", + "Pods", + "DerivedData", + ".swiftpm", + ".git", + "example-app", +]); + +/** @type {{ id: string, pattern: RegExp, exts: string[], ignoreLine?: RegExp }[]} */ +const RULES = [ + { + id: "hasOption", + pattern: /\bhasOption\s*\(/, + exts: [".java", ".kt", ".swift"], + }, + { + id: "getConfigValue", + pattern: /\bgetConfigValue\s*\(/, + exts: [".java", ".kt", ".swift"], + }, + { + id: "@NativePlugin", + pattern: /@NativePlugin\b/, + exts: [".java", ".kt"], + }, + { + id: "saveCall", + pattern: /\bsaveCall\s*\(/, + exts: [".java", ".kt", ".swift"], + }, + { + id: "getSavedCall", + pattern: /\bgetSavedCall\s*\(/, + exts: [".java", ".kt", ".swift"], + }, + { + id: "freeSavedCall", + pattern: /\bfreeSavedCall\s*\(/, + exts: [".java", ".kt", ".swift"], + }, + { + id: "releaseCall", + pattern: /\breleaseCall\s*\(/, + exts: [".java", ".kt", ".swift"], + ignoreLine: /\.releaseCall\s*\(\s*withID:/, + }, + { + id: "pluginRequestPermission", + pattern: /\bpluginRequestPermissions?\s*\(/, + exts: [".java", ".kt"], + }, + { + id: "pluginRequestAllPermissions", + pattern: /\bpluginRequestAllPermissions\s*\(/, + exts: [".java", ".kt"], + }, + { + id: "hasDefinedPermissions", + pattern: /\bhasDefinedPermissions\s*\(/, + exts: [".java", ".kt"], + }, + { + id: "CAPBridge", + pattern: /\bCAPBridge\./, + exts: [".swift"], + ignoreLine: /CAPBridgedPlugin/, + }, + { + id: "CAPNotifications", + pattern: /\bCAPNotifications\b/, + exts: [".swift"], + }, +]; + +const CORDova_SPM_LINE = + /\.product\s*\(\s*name\s*:\s*"Cordova"\s*,\s*package\s*:\s*"capacitor-swift-pm"\s*\)/; + +function readText(p) { + try { + return fs.readFileSync(p, "utf8"); + } catch { + return ""; + } +} + +function exists(p) { + try { + fs.accessSync(p); + return true; + } catch { + return false; + } +} + +function parseArgs(argv) { + const out = { dir: process.cwd(), workspace: false }; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + if (a === "--workspace") { + out.workspace = true; + continue; + } + if (a === "--dir" || a === "--pluginDir") { + out.dir = path.resolve(argv[++i] || "."); + continue; + } + } + return out; +} + +function walkFiles(rootDir, exts) { + const out = []; + const stack = [rootDir]; + while (stack.length) { + const dir = stack.pop(); + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const e of entries) { + if (e.isDirectory()) { + if (SKIP_DIRS.has(e.name)) continue; + stack.push(path.join(dir, e.name)); + continue; + } + if (!e.isFile()) continue; + for (const ext of exts) { + if (e.name.endsWith(ext)) { + out.push(path.join(dir, e.name)); + break; + } + } + } + } + out.sort(); + return out; +} + +function collectScanRoots(pluginDir, cap) { + const roots = []; + if (cap.android) { + const androidMain = path.join(pluginDir, "android", "src", "main"); + if (exists(androidMain)) roots.push(androidMain); + } + if (cap.ios) { + const iosSources = path.join(pluginDir, "ios", "Sources"); + if (exists(iosSources)) roots.push(iosSources); + else { + const iosDir = path.join(pluginDir, "ios"); + if (exists(iosDir)) roots.push(iosDir); + } + } + const packageSwift = path.join(pluginDir, "Package.swift"); + if (exists(packageSwift)) roots.push(packageSwift); + return roots; +} + +function scanFile(filePath, rule) { + const ext = path.extname(filePath); + if (!rule.exts.includes(ext)) return []; + + const txt = readText(filePath); + const lines = txt.split(/\r?\n/); + const hits = []; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (filePath.endsWith("Package.swift") && CORDova_SPM_LINE.test(line)) { + continue; + } + if (rule.ignoreLine?.test(line)) continue; + if (rule.pattern.test(line)) { + hits.push({ line: i + 1, text: line.trim() }); + } + } + return hits; +} + +function listWorkspacePluginDirs(repoRoot) { + const packagesRoot = path.join(repoRoot, "packages"); + if (!exists(packagesRoot)) return []; + return fs + .readdirSync(packagesRoot, { withFileTypes: true }) + .filter((e) => e.isDirectory()) + .map((e) => path.join(packagesRoot, e.name)) + .filter((dir) => { + const pkgPath = path.join(dir, "package.json"); + if (!exists(pkgPath)) return false; + try { + const pkg = JSON.parse(readText(pkgPath)); + const cap = typeof pkg.capacitor === "object" && pkg.capacitor ? pkg.capacitor : {}; + return Boolean(cap.android || cap.ios); + } catch { + return false; + } + }) + .sort(); +} + +/** + * @returns {boolean} true when the plugin passes + */ +function checkPluginDir(pluginDir) { + const pkgPath = path.join(pluginDir, "package.json"); + + if (!exists(pkgPath)) { + console.error(`[cap9-deprecated] ERROR: missing package.json in ${pluginDir}`); + return false; + } + + let pkg; + try { + pkg = JSON.parse(readText(pkgPath)); + } catch (e) { + console.error(`[cap9-deprecated] ERROR: invalid package.json (${pkgPath}): ${e?.message || e}`); + return false; + } + + const cap = typeof pkg.capacitor === "object" && pkg.capacitor ? pkg.capacitor : {}; + if (!cap.android && !cap.ios) { + return true; + } + + const scanRoots = collectScanRoots(pluginDir, cap); + const allExts = [...new Set(RULES.flatMap((r) => r.exts))]; + const files = []; + for (const root of scanRoots) { + if (root.endsWith("Package.swift")) { + files.push(root); + continue; + } + files.push(...walkFiles(root, allExts)); + } + + const violations = []; + for (const file of files) { + for (const rule of RULES) { + const hits = scanFile(file, rule); + for (const hit of hits) { + violations.push({ + rule: rule.id, + file: path.relative(pluginDir, file), + line: hit.line, + text: hit.text, + }); + } + } + } + + if (violations.length) { + const relDir = path.relative(process.cwd(), pluginDir) || "."; + console.error(`[cap9-deprecated] FAIL in ${relDir}`); + for (const v of violations) { + console.error(`- ${v.rule}: ${v.file}:${v.line}: ${v.text}`); + } + return false; + } + + return true; +} + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const args = parseArgs(process.argv); + +if (args.workspace) { + const pluginDirs = listWorkspacePluginDirs(repoRoot); + if (!pluginDirs.length) { + console.error("[cap9-deprecated] ERROR: no Capacitor plugin packages found under packages/*"); + process.exit(2); + } + let failed = false; + for (const dir of pluginDirs) { + if (!checkPluginDir(dir)) failed = true; + } + if (failed) process.exit(1); + console.log(`[cap9-deprecated] OK (${pluginDirs.length} plugin package(s) scanned)`); + process.exit(0); +} + +const ok = checkPluginDir(args.dir); +process.exit(ok ? 0 : 1); diff --git a/scripts/check-capacitor-plugin-wiring.mjs b/scripts/check-capacitor-plugin-wiring.mjs new file mode 100644 index 0000000..582eac1 --- /dev/null +++ b/scripts/check-capacitor-plugin-wiring.mjs @@ -0,0 +1,257 @@ +#!/usr/bin/env node +/** + * Capacitor plugin wiring/name checker. + * + * Enforces that runtime plugin name matches across: + * - JS: registerPlugin('Name') + * - Android: @CapacitorPlugin(name = "Name") + * - iOS: CAPBridgedPlugin jsName = "Name" + * + * And (when iOS is declared in package.json capacitor config): + * - CocoaPods podspec s.name matches SwiftPM Package(name: ...) and .library(name: ...) + * + * Usage: + * node scripts/check-capacitor-plugin-wiring.mjs # checks current working dir + * node scripts/check-capacitor-plugin-wiring.mjs --dir path # checks given plugin dir + */ + +import fs from "node:fs"; +import path from "node:path"; + +const SKIP_DIRS = new Set([ + "node_modules", + "dist", + "build", + ".build", + ".gradle", + "Pods", + "DerivedData", + ".swiftpm", + ".git", +]); + +function readText(p) { + try { + return fs.readFileSync(p, "utf8"); + } catch { + return ""; + } +} + +function exists(p) { + try { + fs.accessSync(p); + return true; + } catch { + return false; + } +} + +function walkFiles(rootDir, exts) { + const out = []; + const stack = [rootDir]; + while (stack.length) { + const dir = stack.pop(); + let entries; + try { + entries = fs.readdirSync(dir, { withFileTypes: true }); + } catch { + continue; + } + for (const e of entries) { + if (e.isDirectory()) { + if (SKIP_DIRS.has(e.name)) continue; + stack.push(path.join(dir, e.name)); + continue; + } + if (!e.isFile()) continue; + for (const ext of exts) { + if (e.name.endsWith(ext)) { + out.push(path.join(dir, e.name)); + break; + } + } + } + } + out.sort(); + return out; +} + +function uniq(arr) { + const out = []; + for (const x of arr) { + if (!x) continue; + if (!out.includes(x)) out.push(x); + } + return out; +} + +function parseArgs(argv) { + const out = { dir: process.cwd() }; + for (let i = 2; i < argv.length; i++) { + const a = argv[i]; + if (a === "--dir" || a === "--pluginDir") { + out.dir = path.resolve(argv[++i] || "."); + continue; + } + } + return out; +} + +const args = parseArgs(process.argv); +const pluginDir = args.dir; +const pkgPath = path.join(pluginDir, "package.json"); + +if (!exists(pkgPath)) { + console.error(`[wiring] ERROR: missing package.json in ${pluginDir}`); + process.exit(2); +} + +let pkg; +try { + pkg = JSON.parse(readText(pkgPath)); +} catch (e) { + console.error(`[wiring] ERROR: invalid package.json (${pkgPath}): ${e?.message || e}`); + process.exit(2); +} + +const cap = typeof pkg.capacitor === "object" && pkg.capacitor ? pkg.capacitor : {}; +const supportsAndroid = typeof cap.android === "object" && cap.android; +const supportsIos = typeof cap.ios === "object" && cap.ios; + +// Not a Capacitor plugin package (e.g. meta/workspace package). +// We only enforce wiring rules for actual plugin packages declaring a `capacitor` config. +if (!supportsAndroid && !supportsIos) { + process.exit(0); +} + +// ---------------- JS (registerPlugin) ---------------- +const jsSrcDir = path.join(pluginDir, "src"); +let jsName = ""; +if (exists(jsSrcDir)) { + const jsFiles = walkFiles(jsSrcDir, [".ts", ".js"]); + const reRegister = /registerPlugin(?:<[^>]*>)?\(\s*['"]([^'"]+)['"]/; + for (const f of jsFiles) { + const m = reRegister.exec(readText(f)); + if (m) { + jsName = m[1]; + break; + } + } +} + +// ---------------- Android (@CapacitorPlugin) ---------------- +let androidNames = []; +if (supportsAndroid) { + const androidMain = path.join(pluginDir, "android", "src", "main"); + const files = walkFiles(androidMain, [".java", ".kt"]); + const foundAnnotations = []; + for (const f of files) { + const txt = readText(f); + if (!txt.includes("@CapacitorPlugin")) continue; + foundAnnotations.push(f); + const m = + /@CapacitorPlugin\(\s*name\s*=\s*"([^"]+)"/.exec(txt) || + /@CapacitorPlugin\(\s*name\s*=\s*([A-Za-z0-9_]+)\b/.exec(txt); + if (m) androidNames.push(m[1]); + } + androidNames = uniq(androidNames); + + // Enforce explicit name attribute to prevent silent class-name drift. + if (foundAnnotations.length && !androidNames.length) { + console.error( + `[wiring] ERROR: Android has @CapacitorPlugin but none specify name = \"...\". Add an explicit name to the plugin class.` + ); + process.exit(1); + } +} + +// ---------------- iOS (jsName) ---------------- +let iosJsNames = []; +if (supportsIos) { + const iosDir = path.join(pluginDir, "ios"); + const scanRoot = exists(path.join(iosDir, "Sources")) ? path.join(iosDir, "Sources") : iosDir; + const swiftFiles = walkFiles(scanRoot, [".swift"]); + const reJsName = /\bjsName\s*=\s*"([^"]+)"/g; + for (const f of swiftFiles) { + const txt = readText(f); + if (!txt.includes("jsName")) continue; + let m; + while ((m = reJsName.exec(txt))) iosJsNames.push(m[1]); + } + iosJsNames = uniq(iosJsNames); +} + +// ---------------- Podspec/SPM ---------------- +function parsePodspecName(podspecPath) { + const txt = readText(podspecPath); + const m = /\bs\.name\s*=\s*'([^']+)'/.exec(txt); + return m ? m[1] : ""; +} + +function parseSpmNames(packageSwiftPath) { + const txt = readText(packageSwiftPath); + const pkg = /Package\(\s*name\s*:\s*"([^"]+)"/.exec(txt)?.[1] || ""; + const libs = []; + const reLib = /\.library\(\s*name\s*:\s*"([^"]+)"/g; + let m; + while ((m = reLib.exec(txt))) libs.push(m[1]); + return { pkgName: pkg, libNames: uniq(libs) }; +} + +// ---------------- Validate ---------------- +const errors = []; + +if (!jsName) { + errors.push("JS: no registerPlugin('...') found under src/"); +} + +if (supportsAndroid) { + if (!androidNames.length) errors.push("Android: missing @CapacitorPlugin(name = \"...\")"); + if (jsName && androidNames.length && androidNames.some((n) => n !== jsName)) { + errors.push(`Android: @CapacitorPlugin(name)=${JSON.stringify(androidNames)} != JS registerPlugin=${jsName}`); + } +} + +if (supportsIos) { + if (!iosJsNames.length) errors.push('iOS: missing jsName = "..." in Swift sources'); + if (jsName && iosJsNames.length && iosJsNames.some((n) => n !== jsName)) { + errors.push(`iOS: jsName=${JSON.stringify(iosJsNames)} != JS registerPlugin=${jsName}`); + } + + const podspecs = fs + .readdirSync(pluginDir, { withFileTypes: true }) + .filter((e) => e.isFile() && e.name.endsWith(".podspec")) + .map((e) => path.join(pluginDir, e.name)) + .sort(); + if (!podspecs.length) errors.push("iOS: missing *.podspec at plugin root"); + if (podspecs.length > 1) errors.push(`iOS: multiple podspecs at plugin root: ${podspecs.map((p) => path.basename(p))}`); + + const pkgSwift = path.join(pluginDir, "Package.swift"); + if (!exists(pkgSwift)) { + errors.push("iOS: missing Package.swift at plugin root"); + } else if (podspecs.length) { + const podName = parsePodspecName(podspecs[0]); + const { pkgName, libNames } = parseSpmNames(pkgSwift); + if (!podName) errors.push("Podspec: missing s.name = '...'"); + if (!pkgName) errors.push('SPM: missing Package(name: "...")'); + if (podName && pkgName && podName !== pkgName) { + errors.push(`Podspec: s.name=${podName} != Package(name)=${pkgName}`); + } + if (pkgName && !libNames.length) { + errors.push('SPM: Package.swift must declare at least one .library product'); + } + if (pkgName && libNames.length && !libNames.includes(pkgName)) { + errors.push(`SPM: Package(name)=${pkgName} not present in .library(name) list ${JSON.stringify(libNames)}`); + } + } +} + +if (errors.length) { + const relDir = path.relative(process.cwd(), pluginDir) || "."; + console.error(`[wiring] FAIL in ${relDir}`); + for (const e of errors) console.error(`- ${e}`); + process.exit(1); +} + +process.exit(0); diff --git a/scripts/generate-ai-changelog.mjs b/scripts/generate-ai-changelog.mjs new file mode 100755 index 0000000..fad185e --- /dev/null +++ b/scripts/generate-ai-changelog.mjs @@ -0,0 +1,135 @@ +#!/usr/bin/env node +/** + * Generate a release changelog with Cloudflare Workers AI in CI. + * Invoked from build.yml on tag releases via `bun run changelog:ai`. + */ + +import { execFileSync } from 'node:child_process'; +import { appendFileSync } from 'node:fs'; + +const DEFAULT_MODEL = '@cf/moonshotai/kimi-k2.7-code'; + +function parseArgs(argv) { + const args = { fromTag: process.env.FROM_TAG, toTag: process.env.TO_TAG }; + for (let i = 0; i < argv.length; i++) { + if (argv[i] === '--from-tag') args.fromTag = argv[++i]; + if (argv[i] === '--to-tag') args.toTag = argv[++i]; + if (argv[i] === '--model') args.model = argv[++i]; + } + return args; +} + +function git(args) { + return execFileSync('/usr/bin/git', args, { encoding: 'utf8' }).trim(); +} + +function resolveTags({ fromTag, toTag }) { + const currentTag = + toTag ?? + (process.env.GITHUB_REF?.startsWith('refs/tags/') ? process.env.GITHUB_REF.replace('refs/tags/', '') : null); + + if (!currentTag) { + throw new Error('Missing target tag. Set GITHUB_REF to refs/tags/ or pass --to-tag.'); + } + + const previousTag = fromTag ?? git(['describe', '--tags', '--abbrev=0', `${currentTag}^`]); + return { previousTag, currentTag }; +} + +function buildPrompt(previousTag, currentTag) { + const commits = git(['log', `${previousTag}..${currentTag}`, '--pretty=format:%s']); + const diffLines = git(['diff', '--stat', `${previousTag}..${currentTag}`]).split('\n'); + const diffStat = diffLines.at(-1)?.trim() ?? ''; + + return `You are a technical writer creating a changelog for a software project. + +There are changes in the git repository between two points: +- From tag: ${previousTag} +- To tag: ${currentTag} +- Commits: +${commits || '(none)'} +- Diff summary: ${diffStat || '(no file changes)'} + +## Instructions: +1. **Categorize changes** using standard changelog categories: + - **Added**: New features + - **Changed**: Changes in existing functionality + - **Deprecated**: Soon-to-be removed features + - **Removed**: Removed features + - **Fixed**: Bug fixes + - **Security**: Security improvements + +2. **Write clear, user-focused descriptions** that explain: + - What changed from a user's perspective + - Why it matters + - Any breaking changes or migration notes + +3. **Use consistent formatting**: + - Each item should be a concise bullet point + - Start with an action verb when possible + - Group related changes together + +4. **Focus on semantic meaning** rather than technical implementation details + +Output ONLY the changelog content in markdown format. Do NOT include any explanatory text, metadata, introductory phrases like "Based on the git analysis" or "Here's the changelog", or concluding remarks. Start IMMEDIATELY with the changelog categories and items (e.g., "## Added"). Do not include version numbers or dates. Do not preface your response with any text whatsoever.`; +} + +async function generateChangelog(prompt, model) { + const accountId = process.env.CLOUDFLARE_ACCOUNT_ID; + const apiToken = process.env.CLOUDFLARE_API_TOKEN; + + if (!accountId || !apiToken) { + throw new Error('CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are required.'); + } + + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`; + const response = await fetch(url, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + messages: [{ role: 'user', content: prompt }], + }), + }); + + const payload = await response.json(); + if (!response.ok || !payload.success) { + throw new Error(`Cloudflare AI request failed with status ${response.status}`); + } + + const content = payload.result?.choices?.[0]?.message?.content ?? payload.result?.response; + if (!content || typeof content !== 'string') { + throw new Error('Cloudflare AI returned an empty changelog response'); + } + + return content.trim(); +} + +function writeGithubOutput({ result, fromTag, toTag }) { + const outputFile = process.env.GITHUB_OUTPUT; + if (!outputFile) return; + + const delimiter = `changelog_${Date.now()}`; + appendFileSync(outputFile, `result<<${delimiter}\n${result}\n${delimiter}\n`); + appendFileSync(outputFile, `from_tag=${fromTag}\n`); + appendFileSync(outputFile, `to_tag=${toTag}\n`); +} + +const args = parseArgs(process.argv.slice(2)); +const model = args.model ?? process.env.CLOUDFLARE_AI_MODEL ?? DEFAULT_MODEL; +const { previousTag, currentTag } = resolveTags(args); +const prompt = buildPrompt(previousTag, currentTag); + +console.error(`Generating changelog with ${model}`); +console.error(`Range: ${previousTag}..${currentTag}`); + +try { + const result = await generateChangelog(prompt, model); + writeGithubOutput({ result, fromTag: previousTag, toTag: currentTag }); +} catch (error) { + const message = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Changelog generation failed: ${message}`); + process.exit(1); +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..a622fa5 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": ".", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": false, + "sourceMap": false + }, + "include": ["configure-dependencies.ts"], + "exclude": ["node_modules"] +} diff --git a/src/definitions.ts b/src/definitions.ts new file mode 100644 index 0000000..a34014d --- /dev/null +++ b/src/definitions.ts @@ -0,0 +1,561 @@ +import { CapacitorNativeMap } from './implementation'; + +/** + * An interface representing the viewports latitude and longitude bounds. + */ +export interface LatLngBoundsInterface { + southwest: LatLng; + center: LatLng; + northeast: LatLng; +} + +/** + * Geographic bounding box with helpers for contains/extend backed by the native plugin. + */ +export class LatLngBounds { + /** South-west corner of the bounds. */ + southwest: LatLng; + /** Center of the bounds. */ + center: LatLng; + /** North-east corner of the bounds. */ + northeast: LatLng; + + constructor(bounds: LatLngBoundsInterface) { + this.southwest = bounds.southwest; + this.center = bounds.center; + this.northeast = bounds.northeast; + } + + /** + * Returns whether `point` lies inside this bounds. + */ + async contains(point: LatLng): Promise { + const result = await CapacitorNativeMap.mapBoundsContains({ + bounds: this, + point, + }); + return result['contains']; + } + + /** + * Expands this bounds to include `point` and returns the same instance. + */ + async extend(point: LatLng): Promise { + const result = await CapacitorNativeMap.mapBoundsExtend({ + bounds: this, + point, + }); + this.southwest = result['bounds']['southwest']; + this.center = result['bounds']['center']; + this.northeast = result['bounds']['northeast']; + return this; + } +} + +/** + * An interface representing a pair of latitude and longitude coordinates. + */ +export interface LatLng { + /** + * Coordinate latitude, in degrees. This value is in the range [-90, 90]. + */ + lat: number; + + /** + * Coordinate longitude, in degrees. This value is in the range [-180, 180]. + */ + lng: number; +} + +/** Width and height in pixels. */ +export interface Size { + width: number; + height: number; +} + +/** Point in pixel coordinates. */ +export interface Point { + x: number; + y: number; +} + +/** + * For web, all the javascript Polygon options are available as + * Polygon extends google.maps.PolygonOptions. + * For iOS and Android only the config options declared on Polygon are available. + */ +export interface Polygon extends google.maps.PolygonOptions { + paths?: google.maps.MVCArray | any[]; + strokeColor?: string; + strokeOpacity?: number; + strokeWeight?: number; + fillColor?: string; + fillOpacity?: number; + geodesic?: boolean; + clickable?: boolean; + /** + * Title, a short description of the overlay. Some overlays, such as markers, will display the title on the map. The title is also the default accessibility text. + * + * Only available on iOS. + */ + title?: string; + tag?: string; +} + +/** + * For web, all the javascript Circle options are available as + * Circle extends google.maps.CircleOptions. + * For iOS and Android only the config options declared on Circle are available. + */ +export interface Circle extends google.maps.CircleOptions { + fillColor?: string; + fillOpacity?: number; + strokeColor?: string; + strokeWeight?: number; + geodesic?: boolean; + clickable?: boolean; + /** + * Title, a short description of the overlay. Some overlays, such as markers, will display the title on the map. The title is also the default accessibility text. + * + * Only available on iOS. + */ + title?: string; + tag?: string; +} + +/** + * For web, all the javascript Polyline options are available as + * Polyline extends google.maps.PolylineOptions. + * For iOS and Android only the config options declared on Polyline are available. + */ +export interface Polyline extends google.maps.PolylineOptions { + strokeColor?: string; + strokeOpacity?: number; + strokeWeight?: number; + geodesic?: boolean; + clickable?: boolean; + tag?: string; + /** + * Used to specify the color of one or more segments of a polyline. The styleSpans property is an array of StyleSpan objects. + * Setting the spans property is the preferred way to change the color of a polyline. + * + * Only on iOS and Android. + */ + styleSpans?: StyleSpan[]; +} + +/** + * Describes the style for some region of a polyline. + */ +export interface StyleSpan { + /** + * The stroke color. All CSS3 colors are supported except for extended named colors. + */ + color: string; + /** + * The length of this span in number of segments. + */ + segments?: number; +} + +/** + * For web, all the javascript Google Maps options are available as + * NativeMapConfig extends google.maps.MapOptions. + * For iOS and Android only the config options declared on NativeMapConfig are available. + */ +export interface NativeMapConfig extends google.maps.MapOptions { + /** + * Override width for native map. + */ + width?: number; + /** + * Override height for native map. + */ + height?: number; + /** + * Override absolute x coordinate position for native map. + */ + x?: number; + /** + * Override absolute y coordinate position for native map. + */ + y?: number; + /** + * Default location on the Earth towards which the camera points. + */ + center: LatLng; + /** + * Sets the zoom of the map. + */ + zoom: number; + /** + * Enables image-based lite mode on Android. + * + * @default false + */ + androidLiteMode?: boolean; + /** + * Override pixel ratio for native map. + */ + devicePixelRatio?: number; + /** + * Styles to apply to each of the default map types. Note that for + * satellite, hybrid and terrain modes, + * these styles will only apply to labels and geometry. + * + * @since 4.3.0 + */ + styles?: google.maps.MapTypeStyle[] | null; + /** + * A map id associated with a specific map style or feature. + * + * [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) + * + * Only for Web. + * + * @since 5.4.0 + */ + mapId?: string; + /** + * A map id associated with a specific map style or feature. + * + * [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) + * + * Only for Android. + * + * @since 5.4.0 + */ + androidMapId?: string; + /** + * A map id associated with a specific map style or feature. + * + * [Use Map IDs](https://developers.google.com/maps/documentation/get-map-id) + * + * Only for iOS. + * + * @since 5.4.0 + */ + iOSMapId?: string; + /** + * The maximum zoom level which will be displayed on the map. If omitted, or + * set to null, the maximum zoom from the current map type is + * used instead. Valid zoom values are numbers from zero up to the supported + * maximum + * zoom level. + */ + maxZoom?: number | null; + /** + * The minimum zoom level which will be displayed on the map. If omitted, or + * set to null, the minimum zoom from the current map type is + * used instead. Valid zoom values are numbers from zero up to the supported + * maximum + * zoom level. + */ + minZoom?: number | null; + /** + * The initial Map mapTypeId. Defaults to ROADMAP. + */ + mapTypeId?: string | null; + /** + * The heading for aerial imagery in degrees measured clockwise from + * cardinal direction North. Headings are snapped to the nearest available + * angle for which imagery is available. + */ + heading?: number | null; + /** + * Defines a boundary that restricts the area of the map accessible to + * users. When set, a user can only pan and zoom while the camera view stays + * inside the limits of the boundary. + */ + restriction?: google.maps.MapRestriction | null; +} + +/** + * Configuration properties for a Google Map Camera + */ +export interface CameraConfig { + /** + * Location on the Earth towards which the camera points. + */ + coordinate?: LatLng; + /** + * Sets the zoom of the map. + */ + zoom?: number; + /** + * Bearing of the camera, in degrees clockwise from true north. + * + * @default 0 + */ + bearing?: number; + /** + * The angle, in degrees, of the camera from the nadir (directly facing the Earth). + * + * The only allowed values are 0 and 45. + * + * @default 0 + */ + angle?: number; + /** + * Animate the transition to the new Camera properties. + * + * @default false + */ + animate?: boolean; + + /** + * This configuration option is not being used. + */ + animationDuration?: number; +} + +export enum MapType { + /** + * Basic map. + */ + Normal = 'Normal', + /** + * Satellite imagery with roads and labels. + */ + Hybrid = 'Hybrid', + /** + * Satellite imagery with no labels. + */ + Satellite = 'Satellite', + /** + * Topographic data. + */ + Terrain = 'Terrain', + /** + * No base map tiles. + */ + None = 'None', +} + +/** + * Controls for setting padding on the 'visible' region of the view. + */ +export interface MapPadding { + top: number; + left: number; + right: number; + bottom: number; +} + +/** + * A tile overlay is an image placed on top of your map at a specific zoom level. Supported on Android and web only. + */ +export interface TileOverlay { + /** + * A string representing the tile url. Should contain `{x}`, `{y}` and `{z}` so they can be replaced with actual values for x, y and zoom. Android and web only. + * + * @type {string} + */ + url: string; + + /** + * The opacity of the tile overlay, between 0 (completely transparent) and 1 inclusive. Available on iOS, Android and Web + * + * @type {number | undefined} + * @default undefined + */ + opacity?: number; + + /** + * Controls whether this tile overlay should be visible. Available only on Android + * + * @type {boolean | undefined} + * @default undefined + */ + visible?: boolean; + + /** + * The zIndex of the tile overlay. Available on iOS and Android + * + * @type {number | undefined} + * @default undefined + */ + zIndex?: number; +} + +/** + * A marker is an icon placed at a particular point on the map's surface. + */ +export interface Marker { + /** + * Marker position + */ + coordinate: LatLng; + /** + * Sets the opacity of the marker, between 0 (completely transparent) and 1 inclusive. + * + * @default 1 + */ + opacity?: number; + /** + * Title, a short description of the overlay. + */ + title?: string; + /** + * Snippet text, shown beneath the title in the info window when selected. + */ + snippet?: string; + /** + * Controls whether this marker should be flat against the Earth's surface or a billboard facing the camera. + * + * @default false + */ + isFlat?: boolean; + /** + * Path to a marker icon to render. It can be relative to the web app public directory, + * or a https url of a remote marker icon. + * + * **SVGs are not supported on native platforms.** + * + * @usage + * ```typescript + * { + * ... + * iconUrl: 'assets/icon/pin.png', + * ... + * } + * ``` + * + * @since 4.2.0 + */ + iconUrl?: string; + /** + * Controls the scaled size of the marker image set in `iconUrl`. + * + * @since 4.2.0 + */ + iconSize?: Size; + + /** + * The position of the image within a sprite, if any. By default, the origin is located at the top left corner of the image . + * + * @since 4.2.0 + */ + iconOrigin?: Point; + + /** + * The position at which to anchor an image in correspondence to the location of the marker on the map. By default, the anchor is located along the center point of the bottom of the image. + * + * @since 4.2.0 + */ + iconAnchor?: Point; + /** + * Customizes the color of the default marker image. Each value must be between 0 and 255. + * + * Only for iOS and Android. + * + * @since 4.2.0 + */ + tintColor?: { + r: number; + g: number; + b: number; + a: number; + }; + + /** + * Controls whether this marker can be dragged interactively + * + * @default false + */ + draggable?: boolean; + + /** + * Specifies the stack order of this marker, relative to other markers on the map. + * A marker with a high z-index is drawn on top of markers with lower z-indexes + * + * @default 0 + */ + zIndex?: number; +} + +/** + * The callback function to be called when map events are emitted. + */ +export type MapListenerCallback = (data: T) => void; + +/** Payload for the `onMapReady` event. */ +export interface MapReadyCallbackData { + /** Map instance id passed to {@link NativeMap.create}. */ + mapId: string; +} + +/** Base marker fields included in marker-related events. */ +export interface MarkerCallbackData { + markerId: string; + latitude: number; + longitude: number; + title: string; + snippet: string; +} + +/** Payload for polyline click events. */ +export interface PolylineCallbackData { + polylineId: string; + tag?: string; +} + +/** Camera stopped moving; includes visible bounds and camera attitude. */ +export interface CameraIdleCallbackData { + mapId: string; + bounds: LatLngBoundsInterface; + bearing: number; + latitude: number; + longitude: number; + tilt: number; + zoom: number; +} + +/** Camera started moving. */ +export interface CameraMoveStartedCallbackData { + mapId: string; + /** True when the user gesture started the move. */ + isGesture: boolean; +} + +/** Cluster marker tapped. */ +export interface ClusterClickCallbackData { + mapId: string; + latitude: number; + longitude: number; + size: number; + items: MarkerCallbackData[]; +} + +/** Map surface tapped (not on a marker). */ +export interface MapClickCallbackData { + mapId: string; + latitude: number; + longitude: number; +} + +/** Marker tapped. */ +export interface MarkerClickCallbackData extends MarkerCallbackData { + mapId: string; +} + +/** Polygon tapped. */ +export interface PolygonClickCallbackData { + mapId: string; + polygonId: string; + tag?: string; +} + +/** Circle tapped. */ +export interface CircleClickCallbackData { + mapId: string; + circleId: string; + tag?: string; +} + +/** My-location control tapped (Android). */ +export interface MyLocationButtonClickCallbackData { + mapId: string; +} diff --git a/src/implementation.ts b/src/implementation.ts new file mode 100644 index 0000000..56cbe42 --- /dev/null +++ b/src/implementation.ts @@ -0,0 +1,288 @@ +import type { Plugin } from '@capacitor/core'; +import { registerPlugin } from '@capacitor/core'; + +import type { + CameraConfig, + Circle, + NativeMapConfig, + LatLng, + LatLngBounds, + MapPadding, + MapType, + Marker, + Polygon, + Polyline, + TileOverlay, +} from './definitions'; +import { shouldRouteTouchToMap } from './touch-routing'; + +/** + * An interface containing the options used when creating a map. + */ +export interface CreateMapArgs { + /** + * A unique identifier for the map instance. + */ + id: string; + /** + * Google Maps API key. Required on web. On Android, also set + * `com.google.android.geo.API_KEY` in the app manifest. Not used on iOS (MapKit). + */ + apiKey?: string; + /** + * The initial configuration settings for the map. + */ + config: NativeMapConfig; + /** + * The DOM element that determines size and positioning for embedded maps. + * Optional when `toBack` is `true` (defaults to `document.body`). + */ + element?: HTMLElement; + /** + * Destroy and re-create the map instance if a map with the supplied id already exists + * @default false + */ + forceCreate?: boolean; + /** + * The region parameter alters your application to serve different map tiles or bias the application (such as biasing geocoding results towards the region). + * + * Only available for web. + */ + region?: string; + + /** + * The language parameter affects the names of controls, copyright notices, driving directions, and control labels, as well as the responses to service requests. + * + * Only available for web. + */ + language?: string; + /** + * When `true`, renders the native map behind a transparent WebView so your HTML UI + * can sit on top. Touches on transparent web areas pass through to the map, + * including multi-touch gestures (pinch zoom, rotate, two-finger tilt, pan). + * Touches on interactive HTML elements stay in the WebView. + * + * On native platforms, set `config.x`, `config.y`, `config.width`, and `config.height` + * for position and size (defaults to the viewport when omitted). Use {@link NativeMap.updateLayout} + * to change layout at runtime. + * + * @default false + */ + toBack?: boolean; +} + +/** + * Layout rectangle for {@link NativeMapPlugin.updateLayout}, in CSS pixels relative to the WebView. + */ +export interface MapLayoutArgs { + /** + * Map instance id returned from {@link NativeMap.create}. + */ + id: string; + /** + * Distance from the left edge of the WebView, in CSS pixels. + */ + x?: number; + /** + * Distance from the top edge of the WebView, in CSS pixels. + */ + y?: number; + /** + * Map width in CSS pixels. + */ + width?: number; + /** + * Map height in CSS pixels. + */ + height?: number; +} + +export interface DestroyMapArgs { + id: string; +} + +export interface RemoveMarkerArgs { + id: string; + markerId: string; +} + +export interface RemoveMarkersArgs { + id: string; + markerIds: string[]; +} + +export interface AddMarkerArgs { + id: string; + marker: Marker; +} + +export interface AddPolygonsArgs { + id: string; + polygons: Polygon[]; +} + +export interface RemovePolygonsArgs { + id: string; + polygonIds: string[]; +} + +export interface AddCirclesArgs { + id: string; + circles: Circle[]; +} + +export interface RemoveCirclesArgs { + id: string; + circleIds: string[]; +} +export interface AddPolylinesArgs { + id: string; + polylines: Polyline[]; +} + +export interface RemovePolylinesArgs { + id: string; + polylineIds: string[]; +} + +export interface CameraArgs { + id: string; + config: CameraConfig; +} + +export interface MapTypeArgs { + id: string; + mapType: MapType; +} + +export interface IndoorMapArgs { + id: string; + enabled: boolean; +} + +export interface RemoveTileOverlayArgs { + id: string; + tileOverlayId: string; +} + +export interface AddTileOverlayArgs { + id: string; + tileOverlay: TileOverlay; +} + +export interface TrafficLayerArgs { + id: string; + enabled: boolean; +} + +export interface AccElementsArgs { + id: string; + enabled: boolean; +} + +export interface PaddingArgs { + id: string; + padding: MapPadding; +} + +export interface CurrentLocArgs { + id: string; + enabled: boolean; +} +export interface AddMarkersArgs { + id: string; + markers: Marker[]; +} + +export interface MapBoundsArgs { + id: string; + mapBounds: { + x: number; + y: number; + width: number; + height: number; + }; +} + +export interface MapBoundsContainsArgs { + bounds: LatLngBounds; + point: LatLng; +} + +export type MapBoundsExtendArgs = MapBoundsContainsArgs; + +export interface EnableClusteringArgs { + id: string; + minClusterSize?: number; +} + +export interface FitBoundsArgs { + id: string; + bounds: LatLngBounds; + padding?: number; +} + +export interface NativeMapPlugin extends Plugin { + create(options: CreateMapArgs): Promise; + enableTouch(args: { id: string }): Promise; + disableTouch(args: { id: string }): Promise; + addTileOverlay(args: AddTileOverlayArgs): Promise<{ id: string }>; + removeTileOverlay(args: RemoveTileOverlayArgs): Promise; + addMarker(args: AddMarkerArgs): Promise<{ id: string }>; + addMarkers(args: AddMarkersArgs): Promise<{ ids: string[] }>; + removeMarker(args: RemoveMarkerArgs): Promise; + removeMarkers(args: RemoveMarkersArgs): Promise; + addPolygons(args: AddPolygonsArgs): Promise<{ ids: string[] }>; + removePolygons(args: RemovePolygonsArgs): Promise; + addCircles(args: AddCirclesArgs): Promise<{ ids: string[] }>; + removeCircles(args: RemoveCirclesArgs): Promise; + addPolylines(args: AddPolylinesArgs): Promise<{ ids: string[] }>; + removePolylines(args: RemovePolylinesArgs): Promise; + enableClustering(args: EnableClusteringArgs): Promise; + disableClustering(args: { id: string }): Promise; + destroy(args: DestroyMapArgs): Promise; + setCamera(args: CameraArgs): Promise; + getMapType(args: { id: string }): Promise<{ type: string }>; + setMapType(args: MapTypeArgs): Promise; + enableIndoorMaps(args: IndoorMapArgs): Promise; + enableTrafficLayer(args: TrafficLayerArgs): Promise; + enableAccessibilityElements(args: AccElementsArgs): Promise; + enableCurrentLocation(args: CurrentLocArgs): Promise; + setPadding(args: PaddingArgs): Promise; + onScroll(args: MapBoundsArgs): Promise; + onResize(args: MapBoundsArgs): Promise; + onDisplay(args: MapBoundsArgs): Promise; + dispatchMapEvent(args: { id: string; focus: boolean }): Promise; + getMapBounds(args: { id: string }): Promise; + fitBounds(args: FitBoundsArgs): Promise; + mapBoundsContains(args: MapBoundsContainsArgs): Promise<{ contains: boolean }>; + mapBoundsExtend(args: MapBoundsExtendArgs): Promise<{ bounds: LatLngBounds }>; + /** + * Updates the native map position and size without recreating the map. + * Useful with `toBack` mode to resize or reposition the map layer. + */ + updateLayout(args: MapLayoutArgs): Promise; + /** + * Shows a map that was hidden with {@link NativeMapPlugin.hide}. + */ + show(args: { id: string }): Promise; + /** + * Hides the native map view while keeping the instance alive. + */ + hide(args: { id: string }): Promise; +} + +const CapacitorNativeMap = registerPlugin('NativeMap', { + web: () => import('./web').then((m) => new m.CapacitorNativeMapWeb()), +}); + +CapacitorNativeMap.addListener('isMapInFocus', (data) => { + const x = data.x as number; + const y = data.y as number; + const mapId = data.mapId as string; + const toBack = data.toBack === true; + const mapInFocus = shouldRouteTouchToMap(x, y, mapId, toBack); + CapacitorNativeMap.dispatchMapEvent({ id: mapId, focus: mapInFocus }); +}); + +export { CapacitorNativeMap }; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..4b669db --- /dev/null +++ b/src/index.ts @@ -0,0 +1,13 @@ +/* eslint-disable @typescript-eslint/no-namespace */ +import { LatLngBounds, MapType, Marker, Polygon, Circle, Polyline, StyleSpan } from './definitions'; +import { NativeMap } from './map'; + +export { NativeMap, LatLngBounds, MapType, Marker, Polygon, Circle, Polyline, StyleSpan }; + +declare global { + export namespace JSX { + export interface IntrinsicElements { + 'capacitor-native-map': any; + } + } +} diff --git a/src/map-ready.test.ts b/src/map-ready.test.ts new file mode 100644 index 0000000..e035fbd --- /dev/null +++ b/src/map-ready.test.ts @@ -0,0 +1,205 @@ +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'; + +import type { MapReadyCallbackData } from './definitions'; +import { MapReadyQueue } from './map-ready'; + +type MapReadyListener = (data: MapReadyCallbackData) => void; + +const onMapReadyListeners: MapReadyListener[] = []; +const createImpl = mock(async (): Promise => undefined); + +mock.module('@capacitor/core', () => ({ + Capacitor: { + getPlatform: () => 'web', + isNativePlatform: () => false, + }, +})); + +mock.module('./implementation', () => ({ + CapacitorNativeMap: { + create: createImpl, + addListener: mock(async (event: string, cb: MapReadyListener) => { + if (event === 'onMapReady') { + onMapReadyListeners.push(cb); + } + return { remove: mock((): void => undefined) }; + }), + destroy: mock(async (): Promise => undefined), + }, +})); + +const { NativeMap } = await import('./map'); + +function emitMapReady(mapId: string): void { + const data: MapReadyCallbackData = { mapId }; + for (const listener of onMapReadyListeners) { + listener(data); + } +} + +function makeHostElement(): HTMLElement { + const element = document.createElement('div'); + element.getBoundingClientRect = () => + ({ + x: 0, + y: 0, + width: 320, + height: 240, + top: 0, + left: 0, + right: 320, + bottom: 240, + toJSON: () => ({}), + }) as DOMRect; + document.body.appendChild(element); + return element; +} + +async function waitForNativeCreate(): Promise { + await new Promise((resolve) => setTimeout(resolve, 250)); +} + +async function flushMicrotasks(): Promise { + await new Promise((resolve) => queueMicrotask(resolve)); +} + +describe('MapReadyQueue', () => { + test('fires queued callback once on deliver', () => { + const queue = new MapReadyQueue(); + let calls = 0; + queue.whenReady(() => { + calls += 1; + }); + expect(calls).toBe(0); + queue.deliver({ mapId: 'a' }); + expect(calls).toBe(1); + queue.deliver({ mapId: 'a' }); + expect(calls).toBe(1); + }); + + test('continues draining when a callback throws', () => { + const queue = new MapReadyQueue(); + let secondCalls = 0; + queue.whenReady(() => { + throw new Error('boom'); + }); + queue.whenReady(() => { + secondCalls += 1; + }); + queue.deliver({ mapId: 'a' }); + expect(secondCalls).toBe(1); + }); +}); + +describe('NativeMap.create map ready', () => { + beforeEach(() => { + onMapReadyListeners.length = 0; + createImpl.mockClear(); + (NativeMap as unknown as { activeMapsById: Map }).activeMapsById.clear(); + }); + + afterEach(async () => { + const active = (NativeMap as unknown as { activeMapsById: Map }).activeMapsById; + for (const map of [...active.values()]) { + await map.destroy(); + } + active.clear(); + document.body.innerHTML = ''; + }); + + test('second create after ready runs callback on microtask', async () => { + const element = makeHostElement(); + const readyData: MapReadyCallbackData[] = []; + + await NativeMap.create( + { + id: 'map-dup', + apiKey: 'test', + config: { center: { lat: 0, lng: 0 }, zoom: 1 }, + element, + }, + (data) => { + readyData.push(data); + }, + ); + await waitForNativeCreate(); + emitMapReady('map-dup'); + await flushMicrotasks(); + expect(readyData).toHaveLength(1); + + let secondCalled = false; + const secondCreate = NativeMap.create( + { + id: 'map-dup', + apiKey: 'test', + config: { center: { lat: 0, lng: 0 }, zoom: 1 }, + element, + }, + () => { + secondCalled = true; + }, + ); + expect(secondCalled).toBe(false); + await secondCreate; + await flushMicrotasks(); + expect(secondCalled).toBe(true); + }); + + test('first create before ready fires callback once on deliver', async () => { + const element = makeHostElement(); + let calls = 0; + + const createPromise = NativeMap.create( + { + id: 'map-first', + apiKey: 'test', + config: { center: { lat: 0, lng: 0 }, zoom: 1 }, + element, + }, + () => { + calls += 1; + }, + ); + + expect(calls).toBe(0); + await waitForNativeCreate(); + expect(calls).toBe(0); + + emitMapReady('map-first'); + await createPromise; + expect(calls).toBe(1); + + emitMapReady('map-first'); + expect(calls).toBe(1); + }); + + test('web forceCreate keeps prior instance when replacement create fails', async () => { + const element = makeHostElement(); + createImpl.mockImplementation(async () => undefined); + + const first = await NativeMap.create({ + id: 'force-web', + apiKey: 'test', + config: { center: { lat: 0, lng: 0 }, zoom: 1 }, + element, + }); + await waitForNativeCreate(); + + createImpl.mockImplementation(async () => { + throw new Error('create failed'); + }); + + await expect( + NativeMap.create({ + id: 'force-web', + forceCreate: true, + apiKey: 'test', + config: { center: { lat: 0, lng: 0 }, zoom: 1 }, + element, + }), + ).rejects.toThrow('create failed'); + + const active = (NativeMap as unknown as { activeMapsById: Map }).activeMapsById; + expect(active.get('force-web')).toBe(first); + }); +}); diff --git a/src/map-ready.ts b/src/map-ready.ts new file mode 100644 index 0000000..3bec210 --- /dev/null +++ b/src/map-ready.ts @@ -0,0 +1,41 @@ +import type { MapReadyCallbackData } from './definitions'; + +export class MapReadyQueue { + private ready = false; + private readyData?: MapReadyCallbackData; + private pending: ((data: MapReadyCallbackData) => void)[] = []; + + deliver(data: MapReadyCallbackData): void { + if (this.ready) { + return; + } + this.ready = true; + this.readyData = data; + const callbacks = this.pending; + this.pending = []; + for (const cb of callbacks) { + try { + cb(data); + } catch (error) { + queueMicrotask(() => { + throw error; + }); + } + } + } + + whenReady(cb: (data: MapReadyCallbackData) => void): void { + if (this.ready && this.readyData != null) { + const data = this.readyData; + queueMicrotask(() => cb(data)); + return; + } + this.pending.push(cb); + } + + clear(): void { + this.ready = false; + this.readyData = undefined; + this.pending = []; + } +} diff --git a/src/map.ts b/src/map.ts new file mode 100644 index 0000000..7bf8a46 --- /dev/null +++ b/src/map.ts @@ -0,0 +1,1331 @@ +import { Capacitor } from '@capacitor/core'; +import type { PluginListenerHandle } from '@capacitor/core'; + +import type { + CameraConfig, + Marker, + MapPadding, + MapListenerCallback, + MapReadyCallbackData, + CameraIdleCallbackData, + CameraMoveStartedCallbackData, + ClusterClickCallbackData, + MapClickCallbackData, + MarkerClickCallbackData, + MyLocationButtonClickCallbackData, + Polygon, + PolygonClickCallbackData, + Circle, + CircleClickCallbackData, + Polyline, + PolylineCallbackData, + TileOverlay, +} from './definitions'; +import { LatLngBounds, MapType } from './definitions'; +import type { CreateMapArgs } from './implementation'; +import { CapacitorNativeMap } from './implementation'; +import { MapReadyQueue } from './map-ready'; +import { installIosTouchRoutingCachePublisher, uninstallIosTouchRoutingCachePublisher } from './touch-routing'; + +export interface NativeMapInterface { + create(options: CreateMapArgs, callback?: MapListenerCallback): Promise; + enableTouch(): Promise; + disableTouch(): Promise; + /** + * Updates native map position and size (CSS pixels). Especially useful in `toBack` mode. + */ + updateLayout(layout: { x?: number; y?: number; width?: number; height?: number }): Promise; + /** Shows the native map after {@link NativeMap.hide}. */ + show(): Promise; + /** Hides the native map without destroying it. */ + hide(): Promise; + enableClustering( + /** + * The minimum number of markers that can be clustered together. The default is 4 markers. + */ + minClusterSize?: number, + ): Promise; + disableClustering(): Promise; + addTileOverlay(tileOverlay: TileOverlay): Promise<{ id: string }>; + removeTileOverlay(id: string): Promise; + addMarker(marker: Marker): Promise; + addMarkers(markers: Marker[]): Promise; + removeMarker(id: string): Promise; + removeMarkers(ids: string[]): Promise; + addPolygons(polygons: Polygon[]): Promise; + removePolygons(ids: string[]): Promise; + addCircles(circles: Circle[]): Promise; + removeCircles(ids: string[]): Promise; + addPolylines(polylines: Polyline[]): Promise; + removePolylines(ids: string[]): Promise; + destroy(): Promise; + setCamera(config: CameraConfig): Promise; + /** + * Get current map type + */ + getMapType(): Promise; + setMapType(mapType: MapType): Promise; + enableIndoorMaps(enabled: boolean): Promise; + enableTrafficLayer(enabled: boolean): Promise; + enableAccessibilityElements(enabled: boolean): Promise; + enableCurrentLocation(enabled: boolean): Promise; + setPadding(padding: MapPadding): Promise; + /** + * Get the map's current viewport latitude and longitude bounds. + * + * @returns {LatLngBounds} + */ + getMapBounds(): Promise; + /** + * Sets the map viewport to contain the given bounds. + * @param bounds The bounds to fit in the viewport. + * @param padding Optional padding to apply in pixels. The bounds will be fit in the part of the map that remains after padding is removed. + */ + fitBounds(bounds: LatLngBounds, padding?: number): Promise; + setOnBoundsChangedListener(callback?: MapListenerCallback): Promise; + setOnCameraIdleListener(callback?: MapListenerCallback): Promise; + setOnCameraMoveStartedListener(callback?: MapListenerCallback): Promise; + setOnClusterClickListener(callback?: MapListenerCallback): Promise; + setOnClusterInfoWindowClickListener(callback?: MapListenerCallback): Promise; + setOnInfoWindowClickListener(callback?: MapListenerCallback): Promise; + setOnMapClickListener(callback?: MapListenerCallback): Promise; + setOnMarkerClickListener(callback?: MapListenerCallback): Promise; + setOnPolygonClickListener(callback?: MapListenerCallback): Promise; + setOnCircleClickListener(callback?: MapListenerCallback): Promise; + setOnPolylineClickListener(callback?: MapListenerCallback): Promise; + setOnMarkerDragStartListener(callback?: MapListenerCallback): Promise; + setOnMarkerDragListener(callback?: MapListenerCallback): Promise; + setOnMarkerDragEndListener(callback?: MapListenerCallback): Promise; + setOnMyLocationButtonClickListener(callback?: MapListenerCallback): Promise; + setOnMyLocationClickListener(callback?: MapListenerCallback): Promise; +} + +class MapCustomElement extends HTMLElement { + constructor() { + super(); + } + + connectedCallback() { + this.innerHTML = ''; + + if (Capacitor.getPlatform() == 'ios') { + this.style.overflow = 'scroll'; + (this.style as any)['-webkit-overflow-scrolling'] = 'touch'; + + const overflowDiv = document.createElement('div'); + overflowDiv.style.height = '200%'; + + this.appendChild(overflowDiv); + } + } +} + +customElements.define('capacitor-native-map', MapCustomElement); + +export class NativeMap { + private static toBackMapCount = 0; + private static activeMapsById = new Map(); + + private id: string; + private element: HTMLElement | null = null; + private toBack = false; + private toBackLayout?: { x: number; y: number; width: number; height: number }; + private windowResizeHandler?: () => void; + private resizeObserver: ResizeObserver | null = null; + private orientationChangeTimeoutId?: ReturnType; + private orientationChangeHandler = (): void => { + if (this.orientationChangeTimeoutId != null) { + clearTimeout(this.orientationChangeTimeoutId); + } + this.orientationChangeTimeoutId = setTimeout(() => { + this.orientationChangeTimeoutId = undefined; + this.updateMapBounds(); + }, 500); + }; + + private onBoundsChangedListener?: PluginListenerHandle; + private onCameraIdleListener?: PluginListenerHandle; + private onCameraMoveStartedListener?: PluginListenerHandle; + private onClusterClickListener?: PluginListenerHandle; + private onClusterInfoWindowClickListener?: PluginListenerHandle; + private onInfoWindowClickListener?: PluginListenerHandle; + private onMapClickListener?: PluginListenerHandle; + private onPolylineClickListener?: PluginListenerHandle; + private onMarkerClickListener?: PluginListenerHandle; + private onPolygonClickListener?: PluginListenerHandle; + private onCircleClickListener?: PluginListenerHandle; + private onMarkerDragStartListener?: PluginListenerHandle; + private onMarkerDragListener?: PluginListenerHandle; + private onMarkerDragEndListener?: PluginListenerHandle; + private onMyLocationButtonClickListener?: PluginListenerHandle; + private onMyLocationClickListener?: PluginListenerHandle; + + private readonly mapReadyQueue = new MapReadyQueue(); + private onMapReadyListener?: PluginListenerHandle; + + private constructor(id: string) { + this.id = id; + } + + /** + * Creates a new instance of a Google Map + * @param options + * @param callback + * @returns NativeMap + */ + public static async create( + options: CreateMapArgs, + callback?: MapListenerCallback, + ): Promise { + const existingMap = NativeMap.activeMapsById.get(options.id); + if (existingMap && !options.forceCreate) { + if (callback) { + existingMap.whenMapReady(callback); + } + return existingMap; + } + + const newMap = new NativeMap(options.id); + + const toBack = options.toBack === true; + newMap.toBack = toBack; + + if (!toBack && !options.element) { + throw new Error('element is required when toBack is false'); + } + + const hostElement = options.element ?? document.body; + if (!hostElement) { + throw new Error('container element is required'); + } + + const createOptions: CreateMapArgs = { + ...options, + toBack, + config: { + ...options.config, + androidLiteMode: options.config.androidLiteMode ?? false, + }, + }; + + newMap.element = hostElement; + if (!toBack) { + newMap.element.dataset.internalId = options.id; + } + + let elementBounds: DOMRect; + if (toBack) { + elementBounds = new DOMRect( + options.config.x ?? 0, + options.config.y ?? 0, + options.config.width ?? window.innerWidth, + options.config.height ?? window.innerHeight, + ); + newMap.toBackLayout = { + x: elementBounds.x, + y: elementBounds.y, + width: elementBounds.width, + height: elementBounds.height, + }; + NativeMap.toBackMapCount += 1; + if (NativeMap.toBackMapCount === 1) { + document.documentElement.classList.add('native-map-to-back'); + document.body.classList.add('native-map-to-back'); + if (Capacitor.getPlatform() === 'ios') { + installIosTouchRoutingCachePublisher(); + } + } + } else { + elementBounds = await NativeMap.getElementBounds(hostElement); + } + createOptions.config.width = elementBounds.width; + createOptions.config.height = elementBounds.height; + createOptions.config.x = elementBounds.x; + createOptions.config.y = elementBounds.y; + createOptions.config.devicePixelRatio = window.devicePixelRatio; + + if (Capacitor.getPlatform() == 'android') { + newMap.initScrolling(); + } + if (Capacitor.isNativePlatform()) { + createOptions.element = {} as HTMLElement; + + const getMapBounds = () => newMap.mapBoundsForNative(); + + const onDisplay = () => { + CapacitorNativeMap.onDisplay({ + id: newMap.id, + mapBounds: getMapBounds(), + }); + }; + + const onResize = () => { + CapacitorNativeMap.onResize({ + id: newMap.id, + mapBounds: getMapBounds(), + }); + }; + + const ionicPage = newMap.element.closest('.ion-page'); + if (Capacitor.getPlatform() === 'ios' && ionicPage) { + ionicPage.addEventListener('ionViewWillEnter', () => { + setTimeout(() => { + onDisplay(); + }, 100); + }); + ionicPage.addEventListener('ionViewDidEnter', () => { + setTimeout(() => { + onDisplay(); + }, 100); + }); + } + + const lastState = { + width: elementBounds.width, + height: elementBounds.height, + isHidden: false, + }; + if (toBack) { + newMap.windowResizeHandler = onResize; + window.addEventListener('resize', onResize); + } else { + newMap.resizeObserver = new ResizeObserver(() => { + if (newMap.element != null) { + const mapRect = newMap.element.getBoundingClientRect(); + + const isHidden = mapRect.width === 0 && mapRect.height === 0; + if (!isHidden) { + if (lastState.isHidden) { + if (Capacitor.getPlatform() === 'ios' && !ionicPage) { + onDisplay(); + } + } else if (lastState.width !== mapRect.width || lastState.height !== mapRect.height) { + onResize(); + } + } + + lastState.width = mapRect.width; + lastState.height = mapRect.height; + lastState.isHidden = isHidden; + } + }); + newMap.resizeObserver.observe(newMap.element); + } + } + + await newMap.attachMapReadyListener(); + if (callback) { + newMap.whenMapReady(callback); + } + + // small delay to allow for iOS WKWebView to setup corresponding element sub-scroll views ??? + const cleanupFailedCreate = (): void => { + newMap.clearMapReadyState(); + if (Capacitor.isNativePlatform()) { + newMap.resizeObserver?.disconnect(); + if (newMap.windowResizeHandler) { + window.removeEventListener('resize', newMap.windowResizeHandler); + newMap.windowResizeHandler = undefined; + } + newMap.disableScrolling(); + } + if (toBack) { + NativeMap.releaseToBackDocumentClasses(); + newMap.toBackLayout = undefined; + } + }; + + try { + await new Promise((resolve, reject) => { + setTimeout(async () => { + try { + if (options.forceCreate) { + const replaced = NativeMap.activeMapsById.get(options.id); + if (replaced && replaced !== newMap && Capacitor.isNativePlatform()) { + replaced.abandonAfterNativeForceReplace(); + } + } + await CapacitorNativeMap.create(createOptions); + if (options.forceCreate && !Capacitor.isNativePlatform()) { + const replaced = NativeMap.activeMapsById.get(options.id); + if (replaced && replaced !== newMap) { + replaced.abandonAfterNativeForceReplace(); + } + } + resolve(undefined); + } catch (err) { + reject(err); + } + }, 200); + }); + } catch (err) { + cleanupFailedCreate(); + throw err; + } + + NativeMap.activeMapsById.set(options.id, newMap); + return newMap; + } + + /** + * Native `forceCreate` already destroyed the map instance; release JS-side toBack ownership only. + */ + private abandonAfterNativeForceReplace(): void { + NativeMap.activeMapsById.delete(this.id); + + this.clearMapReadyState(); + + if (Capacitor.isNativePlatform()) { + this.resizeObserver?.disconnect(); + if (this.windowResizeHandler) { + window.removeEventListener('resize', this.windowResizeHandler); + this.windowResizeHandler = undefined; + } + this.disableScrolling(); + } + + if (this.toBack) { + NativeMap.releaseToBackDocumentClasses(); + this.toBackLayout = undefined; + this.toBack = false; + } + + this.removeAllMapListeners(); + } + + private mapBoundsForNative(): { x: number; y: number; width: number; height: number } { + if (this.toBack && this.toBackLayout) { + return this.toBackLayout; + } + return NativeMap.layoutBoundsForElement(this.element, this.toBack); + } + + private static releaseToBackDocumentClasses(): void { + if (NativeMap.toBackMapCount <= 0) { + return; + } + NativeMap.toBackMapCount -= 1; + if (NativeMap.toBackMapCount === 0) { + document.documentElement.classList.remove('native-map-to-back'); + document.body.classList.remove('native-map-to-back'); + if (Capacitor.getPlatform() === 'ios') { + uninstallIosTouchRoutingCachePublisher(); + } + } + } + + private static async getElementBounds(element: HTMLElement): Promise { + return new Promise((resolve) => { + let elementBounds = element.getBoundingClientRect(); + if (elementBounds.width == 0) { + let retries = 0; + const boundsInterval = setInterval(function () { + if (elementBounds.width == 0 && retries < 30) { + elementBounds = element.getBoundingClientRect(); + retries++; + } else { + if (retries == 30) { + console.warn('Map size could not be determined'); + } + clearInterval(boundsInterval); + resolve(elementBounds); + } + }, 100); + } else { + resolve(elementBounds); + } + }); + } + + /** + * Enable touch events on native map + * + * @returns void + */ + async enableTouch(): Promise { + return CapacitorNativeMap.enableTouch({ + id: this.id, + }); + } + + /** + * Disable touch events on native map + * + * @returns void + */ + async disableTouch(): Promise { + return CapacitorNativeMap.disableTouch({ + id: this.id, + }); + } + + async updateLayout(layout: { x?: number; y?: number; width?: number; height?: number }): Promise { + await CapacitorNativeMap.updateLayout({ + id: this.id, + ...layout, + }); + if (layout.width != null || layout.height != null || layout.x != null || layout.y != null) { + if (this.toBack && this.toBackLayout) { + this.toBackLayout = { + x: layout.x ?? this.toBackLayout.x, + y: layout.y ?? this.toBackLayout.y, + width: layout.width ?? this.toBackLayout.width, + height: layout.height ?? this.toBackLayout.height, + }; + } + this.updateMapBounds(); + } + } + + async show(): Promise { + return CapacitorNativeMap.show({ id: this.id }); + } + + async hide(): Promise { + return CapacitorNativeMap.hide({ id: this.id }); + } + + /** + * Enable marker clustering + * + * @param minClusterSize - The minimum number of markers that can be clustered together. + * @defaultValue 4 + * + * @returns void + */ + async enableClustering(minClusterSize?: number): Promise { + return CapacitorNativeMap.enableClustering({ + id: this.id, + minClusterSize, + }); + } + + /** + * Disable marker clustering + * + * @returns void + */ + async disableClustering(): Promise { + return CapacitorNativeMap.disableClustering({ + id: this.id, + }); + } + + /** + * Adds a tile overlay to the map + * + * @param tileOverlay + * @returns created tile overlay id + */ + async addTileOverlay(tileOverlay: TileOverlay): Promise { + const res = await CapacitorNativeMap.addTileOverlay({ + id: this.id, + tileOverlay, + }); + + return res.id; + } + + /** + * Removes a tile overlay from the map + * + * @param id of the tile overlay to remove from the map + * @returns void + */ + async removeTileOverlay(id: string): Promise { + return CapacitorNativeMap.removeTileOverlay({ + id: this.id, + tileOverlayId: id, + }); + } + + /** + * Adds a marker to the map + * + * @param marker + * @returns created marker id + */ + async addMarker(marker: Marker): Promise { + const res = await CapacitorNativeMap.addMarker({ + id: this.id, + marker, + }); + + return res.id; + } + + /** + * Adds multiple markers to the map + * + * @param markers + * @returns array of created marker IDs + */ + async addMarkers(markers: Marker[]): Promise { + const res = await CapacitorNativeMap.addMarkers({ + id: this.id, + markers, + }); + + return res.ids; + } + + /** + * Remove marker from the map + * + * @param id id of the marker to remove from the map + * @returns + */ + async removeMarker(id: string): Promise { + return CapacitorNativeMap.removeMarker({ + id: this.id, + markerId: id, + }); + } + + /** + * Remove markers from the map + * + * @param ids array of ids to remove from the map + * @returns + */ + async removeMarkers(ids: string[]): Promise { + return CapacitorNativeMap.removeMarkers({ + id: this.id, + markerIds: ids, + }); + } + + async addPolygons(polygons: Polygon[]): Promise { + const res = await CapacitorNativeMap.addPolygons({ + id: this.id, + polygons, + }); + + return res.ids; + } + + async addPolylines(polylines: Polyline[]): Promise { + const res = await CapacitorNativeMap.addPolylines({ + id: this.id, + polylines, + }); + + return res.ids; + } + + async removePolygons(ids: string[]): Promise { + return CapacitorNativeMap.removePolygons({ + id: this.id, + polygonIds: ids, + }); + } + + async addCircles(circles: Circle[]): Promise { + const res = await CapacitorNativeMap.addCircles({ + id: this.id, + circles, + }); + + return res.ids; + } + + async removeCircles(ids: string[]): Promise { + return CapacitorNativeMap.removeCircles({ + id: this.id, + circleIds: ids, + }); + } + + async removePolylines(ids: string[]): Promise { + return CapacitorNativeMap.removePolylines({ + id: this.id, + polylineIds: ids, + }); + } + + /** + * Destroy the current instance of the map + */ + async destroy(): Promise { + if (NativeMap.activeMapsById.get(this.id) !== this) { + return; + } + + if (Capacitor.getPlatform() == 'android') { + this.disableScrolling(); + } + + if (Capacitor.isNativePlatform()) { + this.resizeObserver?.disconnect(); + if (this.windowResizeHandler) { + window.removeEventListener('resize', this.windowResizeHandler); + this.windowResizeHandler = undefined; + } + } + + if (this.toBack) { + NativeMap.releaseToBackDocumentClasses(); + this.toBackLayout = undefined; + } + + this.removeAllMapListeners(); + this.clearMapReadyState(); + + NativeMap.activeMapsById.delete(this.id); + + return CapacitorNativeMap.destroy({ + id: this.id, + }); + } + + /** + * Update the map camera configuration + * + * @param config + * @returns + */ + async setCamera(config: CameraConfig): Promise { + return CapacitorNativeMap.setCamera({ + id: this.id, + config, + }); + } + + async getMapType(): Promise { + const res = await CapacitorNativeMap.getMapType({ id: this.id }); + const raw = (res as { type?: string; mapType?: string }).type ?? (res as { mapType?: string }).mapType ?? 'Normal'; + if (raw in MapType) { + return MapType[raw as keyof typeof MapType]; + } + const capitalized = `${raw.charAt(0).toUpperCase()}${raw.slice(1)}`; + if (capitalized in MapType) { + return MapType[capitalized as keyof typeof MapType]; + } + if (raw === 'standard' || raw === 'roadmap') { + return MapType.Normal; + } + return MapType.Normal; + } + + /** + * Sets the type of map tiles that should be displayed. + * + * @param mapType + * @returns + */ + async setMapType(mapType: MapType): Promise { + return CapacitorNativeMap.setMapType({ + id: this.id, + mapType, + }); + } + + /** + * Sets whether indoor maps are shown, where available. + * + * @param enabled + * @returns + */ + async enableIndoorMaps(enabled: boolean): Promise { + return CapacitorNativeMap.enableIndoorMaps({ + id: this.id, + enabled, + }); + } + + /** + * Controls whether the map is drawing traffic data, if available. + * + * @param enabled + * @returns + */ + async enableTrafficLayer(enabled: boolean): Promise { + return CapacitorNativeMap.enableTrafficLayer({ + id: this.id, + enabled, + }); + } + + /** + * Show accessibility elements for overlay objects, such as Marker and Polyline. + * + * Only available on iOS. + * + * @param enabled + * @returns + */ + async enableAccessibilityElements(enabled: boolean): Promise { + return CapacitorNativeMap.enableAccessibilityElements({ + id: this.id, + enabled, + }); + } + + /** + * Set whether the My Location dot and accuracy circle is enabled. + * + * @param enabled + * @returns + */ + async enableCurrentLocation(enabled: boolean): Promise { + return CapacitorNativeMap.enableCurrentLocation({ + id: this.id, + enabled, + }); + } + + /** + * Set padding on the 'visible' region of the view. + * + * @param padding + * @returns + */ + async setPadding(padding: MapPadding): Promise { + return CapacitorNativeMap.setPadding({ + id: this.id, + padding, + }); + } + + /** + * Get the map's current viewport latitude and longitude bounds. + * + * @returns {LatLngBounds} + */ + async getMapBounds(): Promise { + return new LatLngBounds( + await CapacitorNativeMap.getMapBounds({ + id: this.id, + }), + ); + } + + async fitBounds(bounds: LatLngBounds, padding?: number): Promise { + return CapacitorNativeMap.fitBounds({ + id: this.id, + bounds, + padding, + }); + } + + initScrolling(): void { + const ionContents = document.getElementsByTagName('ion-content'); + + Array.from(ionContents).forEach((ionContent) => { + (ionContent as any).scrollEvents = true; + }); + + window.addEventListener('ionScroll', this.handleScrollEvent); + window.addEventListener('scroll', this.handleScrollEvent); + window.addEventListener('resize', this.handleScrollEvent); + if (screen.orientation) { + screen.orientation.addEventListener('change', this.orientationChangeHandler); + } else { + window.addEventListener('orientationchange', this.orientationChangeHandler); + } + } + + disableScrolling(): void { + if (this.orientationChangeTimeoutId != null) { + clearTimeout(this.orientationChangeTimeoutId); + this.orientationChangeTimeoutId = undefined; + } + window.removeEventListener('ionScroll', this.handleScrollEvent); + window.removeEventListener('scroll', this.handleScrollEvent); + window.removeEventListener('resize', this.handleScrollEvent); + if (screen.orientation) { + screen.orientation.removeEventListener('change', this.orientationChangeHandler); + } else { + window.removeEventListener('orientationchange', this.orientationChangeHandler); + } + } + + handleScrollEvent = (): void => this.updateMapBounds(); + + private updateMapBounds(): void { + if (this.element) { + CapacitorNativeMap.onScroll({ + id: this.id, + mapBounds: this.mapBoundsForNative(), + }); + } + } + + private static layoutBoundsForElement( + element: HTMLElement | null, + toBack: boolean, + ): { x: number; y: number; width: number; height: number } { + if (toBack) { + return { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight }; + } + const mapRect = element?.getBoundingClientRect() ?? ({} as DOMRect); + return { + x: mapRect.x, + y: mapRect.y, + width: mapRect.width, + height: mapRect.height, + }; + } + + /* + private findContainerElement(): HTMLElement | null { + if (!this.element) { + return null; + } + + let parentElement = this.element.parentElement; + while (parentElement !== null) { + if (window.getComputedStyle(parentElement).overflowY !== 'hidden') { + return parentElement; + } + + parentElement = parentElement.parentElement; + } + + return null; + } + */ + + /** + * Set the event listener on the map for 'onCameraIdle' events. + * + * @param callback + * @returns + */ + async setOnCameraIdleListener(callback?: MapListenerCallback): Promise { + if (this.onCameraIdleListener) { + this.onCameraIdleListener.remove(); + } + + if (callback) { + this.onCameraIdleListener = await CapacitorNativeMap.addListener('onCameraIdle', this.generateCallback(callback)); + } else { + this.onCameraIdleListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onBoundsChanged' events. + * + * @param callback + * @returns + */ + async setOnBoundsChangedListener(callback?: MapListenerCallback): Promise { + if (this.onBoundsChangedListener) { + this.onBoundsChangedListener.remove(); + } + + if (callback) { + this.onBoundsChangedListener = await CapacitorNativeMap.addListener( + 'onBoundsChanged', + this.generateCallback(callback), + ); + } else { + this.onBoundsChangedListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onCameraMoveStarted' events. + * + * @param callback + * @returns + */ + async setOnCameraMoveStartedListener(callback?: MapListenerCallback): Promise { + if (this.onCameraMoveStartedListener) { + this.onCameraMoveStartedListener.remove(); + } + + if (callback) { + this.onCameraMoveStartedListener = await CapacitorNativeMap.addListener( + 'onCameraMoveStarted', + this.generateCallback(callback), + ); + } else { + this.onCameraMoveStartedListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onClusterClick' events. + * + * @param callback + * @returns + */ + async setOnClusterClickListener(callback?: MapListenerCallback): Promise { + if (this.onClusterClickListener) { + this.onClusterClickListener.remove(); + } + + if (callback) { + this.onClusterClickListener = await CapacitorNativeMap.addListener( + 'onClusterClick', + this.generateCallback(callback), + ); + } else { + this.onClusterClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onClusterInfoWindowClick' events. + * + * @param callback + * @returns + */ + async setOnClusterInfoWindowClickListener(callback?: MapListenerCallback): Promise { + if (this.onClusterInfoWindowClickListener) { + this.onClusterInfoWindowClickListener.remove(); + } + + if (callback) { + this.onClusterInfoWindowClickListener = await CapacitorNativeMap.addListener( + 'onClusterInfoWindowClick', + this.generateCallback(callback), + ); + } else { + this.onClusterInfoWindowClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onInfoWindowClick' events. + * + * @param callback + * @returns + */ + async setOnInfoWindowClickListener(callback?: MapListenerCallback): Promise { + if (this.onInfoWindowClickListener) { + this.onInfoWindowClickListener.remove(); + } + + if (callback) { + this.onInfoWindowClickListener = await CapacitorNativeMap.addListener( + 'onInfoWindowClick', + this.generateCallback(callback), + ); + } else { + this.onInfoWindowClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMapClick' events. + * + * @param callback + * @returns + */ + async setOnMapClickListener(callback?: MapListenerCallback): Promise { + if (this.onMapClickListener) { + this.onMapClickListener.remove(); + } + + if (callback) { + this.onMapClickListener = await CapacitorNativeMap.addListener('onMapClick', this.generateCallback(callback)); + } else { + this.onMapClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onPolygonClick' events. + * + * @param callback + * @returns + */ + async setOnPolygonClickListener(callback?: MapListenerCallback): Promise { + if (this.onPolygonClickListener) { + this.onPolygonClickListener.remove(); + } + + if (callback) { + this.onPolygonClickListener = await CapacitorNativeMap.addListener( + 'onPolygonClick', + this.generateCallback(callback), + ); + } else { + this.onPolygonClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onCircleClick' events. + * + * @param callback + * @returns + */ + async setOnCircleClickListener(callback?: MapListenerCallback): Promise { + if (this.onCircleClickListener) [this.onCircleClickListener.remove()]; + + if (callback) { + this.onCircleClickListener = await CapacitorNativeMap.addListener( + 'onCircleClick', + this.generateCallback(callback), + ); + } else { + this.onCircleClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMarkerClick' events. + * + * @param callback + * @returns + */ + async setOnMarkerClickListener(callback?: MapListenerCallback): Promise { + if (this.onMarkerClickListener) { + this.onMarkerClickListener.remove(); + } + + if (callback) { + this.onMarkerClickListener = await CapacitorNativeMap.addListener( + 'onMarkerClick', + this.generateCallback(callback), + ); + } else { + this.onMarkerClickListener = undefined; + } + } + /** + * Set the event listener on the map for 'onPolylineClick' events. + * + * @param callback + * @returns + */ + async setOnPolylineClickListener(callback?: MapListenerCallback): Promise { + if (this.onPolylineClickListener) { + this.onPolylineClickListener.remove(); + } + + if (callback) { + this.onPolylineClickListener = await CapacitorNativeMap.addListener( + 'onPolylineClick', + this.generateCallback(callback), + ); + } else { + this.onPolylineClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMarkerDragStart' events. + * + * @param callback + * @returns + */ + async setOnMarkerDragStartListener(callback?: MapListenerCallback): Promise { + if (this.onMarkerDragStartListener) { + this.onMarkerDragStartListener.remove(); + } + + if (callback) { + this.onMarkerDragStartListener = await CapacitorNativeMap.addListener( + 'onMarkerDragStart', + this.generateCallback(callback), + ); + } else { + this.onMarkerDragStartListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMarkerDrag' events. + * + * @param callback + * @returns + */ + async setOnMarkerDragListener(callback?: MapListenerCallback): Promise { + if (this.onMarkerDragListener) { + this.onMarkerDragListener.remove(); + } + + if (callback) { + this.onMarkerDragListener = await CapacitorNativeMap.addListener('onMarkerDrag', this.generateCallback(callback)); + } else { + this.onMarkerDragListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMarkerDragEnd' events. + * + * @param callback + * @returns + */ + async setOnMarkerDragEndListener(callback?: MapListenerCallback): Promise { + if (this.onMarkerDragEndListener) { + this.onMarkerDragEndListener.remove(); + } + + if (callback) { + this.onMarkerDragEndListener = await CapacitorNativeMap.addListener( + 'onMarkerDragEnd', + this.generateCallback(callback), + ); + } else { + this.onMarkerDragEndListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMyLocationButtonClick' events. + * + * @param callback + * @returns + */ + async setOnMyLocationButtonClickListener( + callback?: MapListenerCallback, + ): Promise { + if (this.onMyLocationButtonClickListener) { + this.onMyLocationButtonClickListener.remove(); + } + + if (callback) { + this.onMyLocationButtonClickListener = await CapacitorNativeMap.addListener( + 'onMyLocationButtonClick', + this.generateCallback(callback), + ); + } else { + this.onMyLocationButtonClickListener = undefined; + } + } + + /** + * Set the event listener on the map for 'onMyLocationClick' events. + * + * @param callback + * @returns + */ + async setOnMyLocationClickListener(callback?: MapListenerCallback): Promise { + if (this.onMyLocationClickListener) { + this.onMyLocationClickListener.remove(); + } + + if (callback) { + this.onMyLocationClickListener = await CapacitorNativeMap.addListener( + 'onMyLocationClick', + this.generateCallback(callback), + ); + } else { + this.onMyLocationClickListener = undefined; + } + } + + /** + * Remove all event listeners on the map. + * + * @param callback + * @returns + */ + async removeAllMapListeners(): Promise { + if (this.onBoundsChangedListener) { + this.onBoundsChangedListener.remove(); + this.onBoundsChangedListener = undefined; + } + if (this.onCameraIdleListener) { + this.onCameraIdleListener.remove(); + this.onCameraIdleListener = undefined; + } + if (this.onCameraMoveStartedListener) { + this.onCameraMoveStartedListener.remove(); + this.onCameraMoveStartedListener = undefined; + } + + if (this.onClusterClickListener) { + this.onClusterClickListener.remove(); + this.onClusterClickListener = undefined; + } + + if (this.onClusterInfoWindowClickListener) { + this.onClusterInfoWindowClickListener.remove(); + this.onClusterInfoWindowClickListener = undefined; + } + + if (this.onInfoWindowClickListener) { + this.onInfoWindowClickListener.remove(); + this.onInfoWindowClickListener = undefined; + } + + if (this.onMapClickListener) { + this.onMapClickListener.remove(); + this.onMapClickListener = undefined; + } + + if (this.onPolylineClickListener) { + this.onPolylineClickListener.remove(); + this.onPolylineClickListener = undefined; + } + + if (this.onMarkerClickListener) { + this.onMarkerClickListener.remove(); + this.onMarkerClickListener = undefined; + } + + if (this.onPolygonClickListener) { + this.onPolygonClickListener.remove(); + this.onPolygonClickListener = undefined; + } + + if (this.onCircleClickListener) { + this.onCircleClickListener.remove(); + this.onCircleClickListener = undefined; + } + + if (this.onMarkerDragStartListener) { + this.onMarkerDragStartListener.remove(); + this.onMarkerDragStartListener = undefined; + } + + if (this.onMarkerDragListener) { + this.onMarkerDragListener.remove(); + this.onMarkerDragListener = undefined; + } + + if (this.onMarkerDragEndListener) { + this.onMarkerDragEndListener.remove(); + this.onMarkerDragEndListener = undefined; + } + + if (this.onMyLocationButtonClickListener) { + this.onMyLocationButtonClickListener.remove(); + this.onMyLocationButtonClickListener = undefined; + } + + if (this.onMyLocationClickListener) { + this.onMyLocationClickListener.remove(); + this.onMyLocationClickListener = undefined; + } + } + + private generateCallback(callback: MapListenerCallback): MapListenerCallback { + const mapId = this.id; + return (data: any) => { + if (data.mapId == mapId) { + callback(data); + } + }; + } + + whenMapReady(callback: MapListenerCallback): void { + this.mapReadyQueue.whenReady(callback); + } + + private async attachMapReadyListener(): Promise { + if (this.onMapReadyListener) { + return; + } + const mapId = this.id; + this.onMapReadyListener = await CapacitorNativeMap.addListener('onMapReady', (data: MapReadyCallbackData) => { + if (data.mapId === mapId) { + this.mapReadyQueue.deliver(data); + } + }); + } + + private clearMapReadyState(): void { + this.mapReadyQueue.clear(); + this.onMapReadyListener?.remove(); + this.onMapReadyListener = undefined; + } +} diff --git a/src/test-setup.ts b/src/test-setup.ts new file mode 100644 index 0000000..1512a7e --- /dev/null +++ b/src/test-setup.ts @@ -0,0 +1,3 @@ +import { GlobalRegistrator } from '@happy-dom/global-registrator'; + +GlobalRegistrator.register(); diff --git a/src/touch-routing.ts b/src/touch-routing.ts new file mode 100644 index 0000000..3d423d3 --- /dev/null +++ b/src/touch-routing.ts @@ -0,0 +1,182 @@ +const INTERACTIVE_SELECTOR = + 'a,button,input,select,textarea,label,summary,details,[role="button"],[contenteditable="true"],[data-map-overlay]'; + +const IOS_TOUCH_ROUTING_HANDLER = 'nativeMapTouchRouting'; + +const MUTATION_ATTRIBUTE_FILTER = ['class', 'style', 'role', 'contenteditable', 'data-map-overlay']; + +export type WebCaptureRect = { x: number; y: number; width: number; height: number }; + +function parseBackgroundAlpha(color: string): number { + if (color === 'transparent') { + return 0; + } + const rgbaMatch = color.match(/rgba?\(\s*([^)]+)\)/i); + if (rgbaMatch) { + const parts = rgbaMatch[1].split(',').map((part) => part.trim()); + if (parts.length >= 4) { + return parseFloat(parts[3]); + } + return 1; + } + return 1; +} + +function backgroundBlocksTouches(style: CSSStyleDeclaration): boolean { + const bg = style.backgroundColor; + if (!bg || bg === 'transparent') { + return false; + } + if (parseBackgroundAlpha(bg) <= 0.05) { + return false; + } + const opacity = parseFloat(style.opacity || '1'); + return opacity > 0.05; +} + +function isCaptureEligible(el: Element): boolean { + if (!(el instanceof HTMLElement)) { + return false; + } + let current: HTMLElement | null = el; + while (current && current !== document.documentElement) { + const style = window.getComputedStyle(current); + if (style.display === 'none' || style.visibility === 'hidden') { + return false; + } + if (parseFloat(style.opacity || '1') <= 0.05) { + return false; + } + if (style.pointerEvents === 'none') { + return false; + } + current = current.parentElement; + } + return true; +} + +/** Regions where the WebView should keep the touch (not the native map). */ +export function collectWebCaptureRects(): WebCaptureRect[] { + const out: WebCaptureRect[] = []; + const seen = new WeakSet(); + + const addRect = (el: Element): void => { + if (seen.has(el) || !isCaptureEligible(el)) { + return; + } + seen.add(el); + const r = el.getBoundingClientRect(); + if (r.width === 0 || r.height === 0) { + return; + } + out.push({ x: r.left, y: r.top, width: r.width, height: r.height }); + }; + + document.querySelectorAll(INTERACTIVE_SELECTOR).forEach((el) => addRect(el)); + + const nodes = document.body?.querySelectorAll('*') ?? []; + for (const el of nodes) { + if (!(el instanceof HTMLElement)) { + continue; + } + if (el.closest(INTERACTIVE_SELECTOR)) { + continue; + } + const style = window.getComputedStyle(el); + if (backgroundBlocksTouches(style)) { + addRect(el); + } + } + + return out; +} + +export function publishIosTouchRoutingCache(): void { + const handlers = ( + window as Window & { webkit?: { messageHandlers?: Record void }> } } + ).webkit?.messageHandlers; + const handler = handlers?.[IOS_TOUCH_ROUTING_HANDLER]; + if (!handler) { + return; + } + handler.postMessage(collectWebCaptureRects()); +} + +let iosTouchRoutingPublisherCount = 0; +let iosTouchRoutingRafId = 0; +let iosTouchRoutingMutationObserver: MutationObserver | undefined; +const iosTouchRoutingOnLayout = (): void => { + if (iosTouchRoutingRafId) { + cancelAnimationFrame(iosTouchRoutingRafId); + } + iosTouchRoutingRafId = requestAnimationFrame(() => { + iosTouchRoutingRafId = 0; + publishIosTouchRoutingCache(); + }); +}; + +export function installIosTouchRoutingCachePublisher(): void { + iosTouchRoutingPublisherCount += 1; + if (iosTouchRoutingPublisherCount !== 1) { + return; + } + window.addEventListener('resize', iosTouchRoutingOnLayout); + window.addEventListener('scroll', iosTouchRoutingOnLayout, true); + iosTouchRoutingMutationObserver = new MutationObserver(iosTouchRoutingOnLayout); + iosTouchRoutingMutationObserver.observe(document.documentElement, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: MUTATION_ATTRIBUTE_FILTER, + }); + iosTouchRoutingOnLayout(); +} + +export function uninstallIosTouchRoutingCachePublisher(): void { + if (iosTouchRoutingPublisherCount <= 0) { + return; + } + iosTouchRoutingPublisherCount -= 1; + if (iosTouchRoutingPublisherCount !== 0) { + return; + } + window.removeEventListener('resize', iosTouchRoutingOnLayout); + window.removeEventListener('scroll', iosTouchRoutingOnLayout, true); + iosTouchRoutingMutationObserver?.disconnect(); + iosTouchRoutingMutationObserver = undefined; + if (iosTouchRoutingRafId) { + cancelAnimationFrame(iosTouchRoutingRafId); + iosTouchRoutingRafId = 0; + } +} + +/** + * Decide whether a native touch at (x, y) should be routed to the map or the WebView. + */ +export function shouldRouteTouchToMap(x: number, y: number, mapId: string, toBack: boolean): boolean { + if (!toBack) { + const elem = document.elementFromPoint(x, y) as HTMLElement | null; + const mapElement = elem?.closest('[data-internal-id]') as HTMLElement | null; + return mapElement?.dataset?.internalId === mapId; + } + + const elem = document.elementFromPoint(x, y) as HTMLElement | null; + if (!elem) { + return true; + } + + if (elem.closest(INTERACTIVE_SELECTOR)) { + return false; + } + + let current: HTMLElement | null = elem; + while (current && current !== document.documentElement) { + const style = window.getComputedStyle(current); + if (backgroundBlocksTouches(style)) { + return false; + } + current = current.parentElement; + } + + return true; +} diff --git a/src/web.ts b/src/web.ts new file mode 100644 index 0000000..a4f5912 --- /dev/null +++ b/src/web.ts @@ -0,0 +1,822 @@ +import { WebPlugin } from '@capacitor/core'; +import type { Cluster, onClusterClickHandler } from '@googlemaps/markerclusterer'; +import { MarkerClusterer, SuperClusterAlgorithm } from '@googlemaps/markerclusterer'; + +import type { Marker, TileOverlay } from './definitions'; +import { MapType, LatLngBounds } from './definitions'; +import type { + AddTileOverlayArgs, + AddMarkerArgs, + CameraArgs, + AddMarkersArgs, + NativeMapPlugin, + CreateMapArgs, + CurrentLocArgs, + DestroyMapArgs, + MapTypeArgs, + PaddingArgs, + RemoveMarkerArgs, + TrafficLayerArgs, + RemoveMarkersArgs, + MapBoundsContainsArgs, + EnableClusteringArgs, + FitBoundsArgs, + MapBoundsExtendArgs, + AddPolygonsArgs, + RemovePolygonsArgs, + AddCirclesArgs, + RemoveCirclesArgs, + AddPolylinesArgs, + RemovePolylinesArgs, + RemoveTileOverlayArgs, +} from './implementation'; + +export class CapacitorNativeMapWeb extends WebPlugin implements NativeMapPlugin { + private gMapsRef: google.maps.MapsLibrary | undefined = undefined; + private AdvancedMarkerElement: typeof google.maps.marker.AdvancedMarkerElement | undefined = undefined; + private PinElement: typeof google.maps.marker.PinElement | undefined = undefined; + private maps: { + [id: string]: { + element: HTMLElement; + ownsElement: boolean; + map: google.maps.Map; + markers: { + [id: string]: google.maps.marker.AdvancedMarkerElement; + }; + tileOverlays: { + [id: string]: google.maps.ImageMapType; + }; + polygons: { + [id: string]: google.maps.Polygon; + }; + circles: { + [id: string]: google.maps.Circle; + }; + polylines: { + [id: string]: google.maps.Polyline; + }; + markerClusterer?: MarkerClusterer; + trafficLayer?: google.maps.TrafficLayer; + }; + } = {}; + private currMarkerId = 0; + private currTileOverlayId = 0; + private currPolygonId = 0; + private currCircleId = 0; + private currPolylineId = 0; + + private onClusterClickHandler: onClusterClickHandler = ( + _: google.maps.MapMouseEvent, + cluster: Cluster, + map: google.maps.Map, + ): void => { + const mapId = this.getIdFromMap(map); + const items: any[] = []; + + if (cluster.markers != undefined && this.AdvancedMarkerElement) { + for (const marker of cluster.markers) { + if (marker instanceof this.AdvancedMarkerElement) { + const markerId = this.getIdFromMarker(mapId, marker); + const position = marker.position as google.maps.LatLngLiteral; + + items.push({ + markerId: markerId, + latitude: position.lat, + longitude: position.lng, + title: marker.title ?? '', + snippet: '', + }); + } + } + } + + this.notifyListeners('onClusterClick', { + mapId: mapId, + latitude: cluster.position.lat, + longitude: cluster.position.lng, + size: cluster.count, + items: items, + }); + }; + + private getIdFromMap(map: google.maps.Map): string { + for (const id in this.maps) { + if (this.maps[id].map == map) { + return id; + } + } + + return ''; + } + + private getIdFromMarker(mapId: string, marker: google.maps.marker.AdvancedMarkerElement): string { + for (const id in this.maps[mapId].markers) { + if (this.maps[mapId].markers[id] == marker) { + return id; + } + } + + return ''; + } + + private async importGoogleLib(apiKey: string, region?: string, language?: string) { + if (this.gMapsRef === undefined) { + const lib = await import('@googlemaps/js-api-loader'); + lib.setOptions({ + key: apiKey ?? '', + language, + region, + }); + + this.gMapsRef = await lib.importLibrary('maps'); + + // Import marker library once + const { AdvancedMarkerElement, PinElement } = (await google.maps.importLibrary( + 'marker', + )) as google.maps.MarkerLibrary; + this.AdvancedMarkerElement = AdvancedMarkerElement; + this.PinElement = PinElement; + + console.log('Loaded google maps API'); + } + } + + async enableTouch(_args: { id: string }): Promise { + this.maps[_args.id].map.setOptions({ gestureHandling: 'auto' }); + } + + async disableTouch(_args: { id: string }): Promise { + this.maps[_args.id].map.setOptions({ gestureHandling: 'none' }); + } + + private createOwnedMapHost(id: string): HTMLElement { + const host = document.createElement('div'); + host.dataset.capacitorNativeMapHost = id; + host.style.position = 'absolute'; + host.style.left = '0'; + host.style.top = '0'; + host.style.width = '100%'; + host.style.height = '100%'; + host.style.zIndex = '0'; + const firstChild = document.body.firstChild; + if (firstChild) { + document.body.insertBefore(host, firstChild); + } else { + document.body.appendChild(host); + } + return host; + } + + async updateLayout(_args: { id: string; x?: number; y?: number; width?: number; height?: number }): Promise { + const entry = this.maps[_args.id]; + if (!entry) { + return; + } + const el = entry.element; + if (_args.x != null || _args.y != null) { + el.style.position = 'absolute'; + } + if (_args.x != null) { + el.style.left = `${_args.x}px`; + } + if (_args.y != null) { + el.style.top = `${_args.y}px`; + } + if (_args.width != null) { + el.style.width = `${_args.width}px`; + } + if (_args.height != null) { + el.style.height = `${_args.height}px`; + } + } + + async show(args: { id: string }): Promise { + const entry = this.maps[args.id]; + if (entry) { + entry.element.style.visibility = 'visible'; + } + } + + async hide(args: { id: string }): Promise { + const entry = this.maps[args.id]; + if (entry) { + entry.element.style.visibility = 'hidden'; + } + } + + async setCamera(_args: CameraArgs): Promise { + // Animation not supported yet... + this.maps[_args.id].map.moveCamera({ + center: _args.config.coordinate, + heading: _args.config.bearing, + tilt: _args.config.angle, + zoom: _args.config.zoom, + }); + } + + async getMapType(_args: { id: string }): Promise<{ type: string }> { + let type = this.maps[_args.id].map.getMapTypeId(); + if (type !== undefined) { + if (type === 'roadmap') { + type = MapType.Normal; + } + return { type: `${type.charAt(0).toUpperCase()}${type.slice(1)}` }; + } + throw new Error('Map type is undefined'); + } + + async setMapType(_args: MapTypeArgs): Promise { + let mapType = _args.mapType.toLowerCase(); + if (_args.mapType === MapType.Normal) { + mapType = 'roadmap'; + } + this.maps[_args.id].map.setMapTypeId(mapType); + } + + async enableIndoorMaps(): Promise { + throw new Error('Method not supported on web.'); + } + + async enableTrafficLayer(_args: TrafficLayerArgs): Promise { + const trafficLayer = this.maps[_args.id].trafficLayer ?? new google.maps.TrafficLayer(); + + if (_args.enabled) { + trafficLayer.setMap(this.maps[_args.id].map); + this.maps[_args.id].trafficLayer = trafficLayer; + } else if (this.maps[_args.id].trafficLayer) { + trafficLayer.setMap(null); + this.maps[_args.id].trafficLayer = undefined; + } + } + + async enableAccessibilityElements(): Promise { + throw new Error('Method not supported on web.'); + } + + dispatchMapEvent(): Promise { + throw new Error('Method not supported on web.'); + } + + async enableCurrentLocation(_args: CurrentLocArgs): Promise { + if (!_args.enabled) { + return; + } + if (!navigator.geolocation) { + throw new Error('Geolocation not supported on web browser.'); + } + await new Promise((resolve, reject) => { + navigator.geolocation.getCurrentPosition( + (position: GeolocationPosition) => { + const pos = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + + const map = this.maps[_args.id]?.map; + if (!map) { + reject(new Error('Map no longer exists.')); + return; + } + map.setCenter(pos); + + this.notifyListeners('onMyLocationButtonClick', { mapId: _args.id }); + + this.notifyListeners('onMyLocationClick', { + mapId: _args.id, + latitude: pos.lat, + longitude: pos.lng, + }); + resolve(); + }, + () => { + reject(new Error('Geolocation permission denied or unavailable on web browser.')); + }, + ); + }); + } + async setPadding(_args: PaddingArgs): Promise { + const bounds = this.maps[_args.id].map.getBounds(); + + if (bounds !== undefined) { + this.maps[_args.id].map.fitBounds(bounds, _args.padding); + } + } + + async getMapBounds(_args: { id: string }): Promise { + const bounds = this.maps[_args.id].map.getBounds(); + + if (!bounds) { + throw new Error('Google Map Bounds could not be found.'); + } + + return new LatLngBounds({ + southwest: { + lat: bounds.getSouthWest().lat(), + lng: bounds.getSouthWest().lng(), + }, + center: { + lat: bounds.getCenter().lat(), + lng: bounds.getCenter().lng(), + }, + northeast: { + lat: bounds.getNorthEast().lat(), + lng: bounds.getNorthEast().lng(), + }, + }); + } + + async fitBounds(_args: FitBoundsArgs): Promise { + const map = this.maps[_args.id].map; + const bounds = this.getLatLngBounds(_args.bounds); + map.fitBounds(bounds, _args.padding); + } + + async addTileOverlay(_args: AddTileOverlayArgs): Promise<{ id: string }> { + const tileOverlay = _args.tileOverlay as TileOverlay; + + const map = this.maps[_args.id].map; + + const id = '' + this.currTileOverlayId; + + const customMapOverlay = new google.maps.ImageMapType({ + getTileUrl: function (coord, zoom) { + return tileOverlay.url.replace('{x}', `${coord.x}`).replace('{y}', `${coord.y}`).replace('{z}', `${zoom}`); + }, + tileSize: new google.maps.Size(256, 256), + opacity: tileOverlay.opacity, + }); + + this.maps[_args.id].tileOverlays[id] = customMapOverlay; + + map.overlayMapTypes.push(customMapOverlay); + + this.currTileOverlayId++; + + return { id: id }; + } + + async removeTileOverlay(_args: RemoveTileOverlayArgs): Promise { + const map = this.maps[_args.id].map; + if (!map) { + return; + } + + for (let i = 0; i < map.overlayMapTypes.getLength(); i++) { + if (map.overlayMapTypes.getAt(i) === this.maps[_args.id].tileOverlays[_args.tileOverlayId]) { + map.overlayMapTypes.removeAt(i); + delete this.maps[_args.id].tileOverlays[_args.tileOverlayId]; + break; + } + } + } + + private attachMarkerToMap(mapId: string, marker: google.maps.marker.AdvancedMarkerElement): void { + const mapState = this.maps[mapId]; + if (mapState.markerClusterer) { + mapState.markerClusterer.addMarker(marker); + } else { + marker.map = mapState.map; + } + } + + private detachMarkerFromMap(mapId: string, markerId: string): void { + const mapState = this.maps[mapId]; + const marker = mapState.markers[markerId]; + if (!marker) { + return; + } + if (mapState.markerClusterer) { + mapState.markerClusterer.removeMarker(marker); + } else { + marker.map = null; + } + delete mapState.markers[markerId]; + } + + async addMarkers(_args: AddMarkersArgs): Promise<{ ids: string[] }> { + const markerIds: string[] = []; + const map = this.maps[_args.id]; + + for (const markerArgs of _args.markers) { + const advancedMarker = this.buildMarkerOpts(markerArgs); + + const id = '' + this.currMarkerId; + + map.markers[id] = advancedMarker; + await this.setMarkerListeners(_args.id, id, advancedMarker); + this.attachMarkerToMap(_args.id, advancedMarker); + + markerIds.push(id); + this.currMarkerId++; + } + + return { ids: markerIds }; + } + + async addMarker(_args: AddMarkerArgs): Promise<{ id: string }> { + const advancedMarker = this.buildMarkerOpts(_args.marker); + + const id = '' + this.currMarkerId; + + this.maps[_args.id].markers[id] = advancedMarker; + await this.setMarkerListeners(_args.id, id, advancedMarker); + this.attachMarkerToMap(_args.id, advancedMarker); + + this.currMarkerId++; + + return { id: id }; + } + + async removeMarkers(_args: RemoveMarkersArgs): Promise { + for (const id of _args.markerIds) { + this.detachMarkerFromMap(_args.id, id); + } + } + + async removeMarker(_args: RemoveMarkerArgs): Promise { + this.detachMarkerFromMap(_args.id, _args.markerId); + } + + async addPolygons(args: AddPolygonsArgs): Promise<{ ids: string[] }> { + const polygonIds: string[] = []; + const map = this.maps[args.id]; + + for (const polygonArgs of args.polygons) { + const polygon = new google.maps.Polygon(polygonArgs); + polygon.setMap(map.map); + + const id = '' + this.currPolygonId; + this.maps[args.id].polygons[id] = polygon; + this.setPolygonListeners(args.id, id, polygon); + + polygonIds.push(id); + this.currPolygonId++; + } + + return { ids: polygonIds }; + } + + async removePolygons(args: RemovePolygonsArgs): Promise { + const map = this.maps[args.id]; + + for (const id of args.polygonIds) { + map.polygons[id].setMap(null); + delete map.polygons[id]; + } + } + + async addCircles(args: AddCirclesArgs): Promise<{ ids: string[] }> { + const circleIds: string[] = []; + const map = this.maps[args.id]; + + for (const circleArgs of args.circles) { + const circle = new google.maps.Circle(circleArgs); + circle.setMap(map.map); + + const id = '' + this.currCircleId; + this.maps[args.id].circles[id] = circle; + this.setCircleListeners(args.id, id, circle); + + circleIds.push(id); + this.currCircleId++; + } + + return { ids: circleIds }; + } + + async removeCircles(args: RemoveCirclesArgs): Promise { + const map = this.maps[args.id]; + + for (const id of args.circleIds) { + map.circles[id].setMap(null); + delete map.circles[id]; + } + } + + async addPolylines(args: AddPolylinesArgs): Promise<{ ids: string[] }> { + const lineIds: string[] = []; + const map = this.maps[args.id]; + + for (const polylineArgs of args.polylines) { + const polyline = new google.maps.Polyline(polylineArgs); + polyline.set('tag', polylineArgs.tag); + polyline.setMap(map.map); + + const id = '' + this.currPolylineId; + this.maps[args.id].polylines[id] = polyline; + this.setPolylineListeners(args.id, id, polyline); + + lineIds.push(id); + this.currPolylineId++; + } + + return { + ids: lineIds, + }; + } + + async removePolylines(args: RemovePolylinesArgs): Promise { + const map = this.maps[args.id]; + + for (const id of args.polylineIds) { + map.polylines[id].setMap(null); + delete map.polylines[id]; + } + } + + async enableClustering(_args: EnableClusteringArgs): Promise { + const markers: google.maps.marker.AdvancedMarkerElement[] = []; + + for (const id in this.maps[_args.id].markers) { + markers.push(this.maps[_args.id].markers[id]); + } + + this.maps[_args.id].markerClusterer = new MarkerClusterer({ + map: this.maps[_args.id].map, + markers: markers, + algorithm: new SuperClusterAlgorithm({ + minPoints: _args.minClusterSize ?? 4, + }), + onClusterClick: this.onClusterClickHandler, + }); + } + + async disableClustering(_args: { id: string }): Promise { + const mapInstance = this.maps[_args.id]; + if (mapInstance.markerClusterer) { + const markers = Object.values(mapInstance.markers); + + mapInstance.markerClusterer.setMap(null); + mapInstance.markerClusterer = undefined; + + for (const marker of markers) { + marker.map = mapInstance.map; + } + } + } + + async onScroll(): Promise { + throw new Error('Method not supported on web.'); + } + + async onResize(): Promise { + throw new Error('Method not supported on web.'); + } + + async onDisplay(): Promise { + throw new Error('Method not supported on web.'); + } + + async create(_args: CreateMapArgs): Promise { + console.log(`Create map: ${_args.id}`); + if (this.maps[_args.id] && !_args.forceCreate) { + return; + } + if (!_args.apiKey) { + throw new Error('apiKey is required when using NativeMap on web'); + } + await this.importGoogleLib(_args.apiKey, _args.region, _args.language); + + const config = { ..._args.config }; + if (!config.mapId) { + throw new Error('config.mapId is required on web (create a Map ID in Google Cloud Console for Advanced Markers)'); + } + + const ownsElement = _args.element == null; + if (this.maps[_args.id]) { + await this.destroy({ id: _args.id }); + } + const element = _args.element ?? this.createOwnedMapHost(_args.id); + this.maps[_args.id] = { + map: new window.google.maps.Map(element, config), + element, + ownsElement, + markers: {}, + tileOverlays: {}, + polygons: {}, + circles: {}, + polylines: {}, + }; + this.setMapListeners(_args.id); + } + + async destroy(_args: DestroyMapArgs): Promise { + console.log(`Destroy map: ${_args.id}`); + const mapItem = this.maps[_args.id]; + mapItem.element.innerHTML = ''; + mapItem.map.unbindAll(); + if (mapItem.ownsElement) { + mapItem.element.remove(); + } + delete this.maps[_args.id]; + } + + async mapBoundsContains(_args: MapBoundsContainsArgs): Promise<{ contains: boolean }> { + const bounds = this.getLatLngBounds(_args.bounds); + const point = new google.maps.LatLng(_args.point.lat, _args.point.lng); + return { contains: bounds.contains(point) }; + } + + async mapBoundsExtend(_args: MapBoundsExtendArgs): Promise<{ bounds: LatLngBounds }> { + const bounds = this.getLatLngBounds(_args.bounds); + const point = new google.maps.LatLng(_args.point.lat, _args.point.lng); + bounds.extend(point); + const result = new LatLngBounds({ + southwest: { + lat: bounds.getSouthWest().lat(), + lng: bounds.getSouthWest().lng(), + }, + center: { + lat: bounds.getCenter().lat(), + lng: bounds.getCenter().lng(), + }, + northeast: { + lat: bounds.getNorthEast().lat(), + lng: bounds.getNorthEast().lng(), + }, + }); + return { bounds: result }; + } + + private getLatLngBounds(_args: LatLngBounds): google.maps.LatLngBounds { + return new google.maps.LatLngBounds( + new google.maps.LatLng(_args.southwest.lat, _args.southwest.lng), + new google.maps.LatLng(_args.northeast.lat, _args.northeast.lng), + ); + } + + async setCircleListeners(mapId: string, circleId: string, circle: google.maps.Circle): Promise { + circle.addListener('click', () => { + this.notifyListeners('onCircleClick', { + mapId: mapId, + circleId: circleId, + tag: circle.get('tag'), + }); + }); + } + + async setPolygonListeners(mapId: string, polygonId: string, polygon: google.maps.Polygon): Promise { + polygon.addListener('click', () => { + this.notifyListeners('onPolygonClick', { + mapId: mapId, + polygonId: polygonId, + tag: polygon.get('tag'), + }); + }); + } + + async setPolylineListeners(mapId: string, polylineId: string, polyline: google.maps.Polyline): Promise { + polyline.addListener('click', () => { + this.notifyListeners('onPolylineClick', { + mapId: mapId, + polylineId: polylineId, + tag: polyline.get('tag'), + }); + }); + } + + async setMarkerListeners( + mapId: string, + markerId: string, + marker: google.maps.marker.AdvancedMarkerElement, + ): Promise { + marker.addListener('click', () => { + const position = marker.position as google.maps.LatLngLiteral; + this.notifyListeners('onMarkerClick', { + mapId: mapId, + markerId: markerId, + latitude: position.lat, + longitude: position.lng, + title: marker.title ?? '', + snippet: '', + }); + }); + + if (marker.gmpDraggable) { + marker.addListener('dragstart', () => { + const position = marker.position as google.maps.LatLngLiteral; + this.notifyListeners('onMarkerDragStart', { + mapId: mapId, + markerId: markerId, + latitude: position.lat, + longitude: position.lng, + title: marker.title ?? '', + snippet: '', + }); + }); + + marker.addListener('drag', () => { + const position = marker.position as google.maps.LatLngLiteral; + this.notifyListeners('onMarkerDrag', { + mapId: mapId, + markerId: markerId, + latitude: position.lat, + longitude: position.lng, + title: marker.title ?? '', + snippet: '', + }); + }); + + marker.addListener('dragend', () => { + const position = marker.position as google.maps.LatLngLiteral; + this.notifyListeners('onMarkerDragEnd', { + mapId: mapId, + markerId: markerId, + latitude: position.lat, + longitude: position.lng, + title: marker.title ?? '', + snippet: '', + }); + }); + } + } + + async setMapListeners(mapId: string): Promise { + const map = this.maps[mapId].map; + + map.addListener('idle', async () => { + const bounds = await this.getMapBounds({ id: mapId }); + this.notifyListeners('onCameraIdle', { + mapId: mapId, + bearing: map.getHeading(), + bounds: bounds, + latitude: map.getCenter()?.lat(), + longitude: map.getCenter()?.lng(), + tilt: map.getTilt(), + zoom: map.getZoom(), + }); + }); + + map.addListener('center_changed', () => { + this.notifyListeners('onCameraMoveStarted', { + mapId: mapId, + isGesture: true, + }); + }); + + map.addListener('bounds_changed', async () => { + const bounds = await this.getMapBounds({ id: mapId }); + this.notifyListeners('onBoundsChanged', { + mapId: mapId, + bearing: map.getHeading(), + bounds: bounds, + latitude: map.getCenter()?.lat(), + longitude: map.getCenter()?.lng(), + tilt: map.getTilt(), + zoom: map.getZoom(), + }); + }); + + map.addListener('click', (e: google.maps.MapMouseEvent | google.maps.IconMouseEvent) => { + this.notifyListeners('onMapClick', { + mapId: mapId, + latitude: e.latLng?.lat(), + longitude: e.latLng?.lng(), + }); + }); + + this.notifyListeners('onMapReady', { + mapId: mapId, + }); + } + + private buildMarkerOpts(marker: Marker): google.maps.marker.AdvancedMarkerElement { + if (!this.AdvancedMarkerElement || !this.PinElement) { + throw new Error('Marker library not loaded'); + } + + let content: HTMLElement | undefined = undefined; + + if (marker.iconUrl) { + const img = document.createElement('img'); + img.src = marker.iconUrl; + if (marker.iconSize) { + img.style.width = `${marker.iconSize.width}px`; + img.style.height = `${marker.iconSize.height}px`; + } + content = img; + } else { + const pinOptions: google.maps.marker.PinElementOptions = { + glyph: marker.title, + background: marker.tintColor + ? `rgb(${marker.tintColor.r}, ${marker.tintColor.g}, ${marker.tintColor.b})` + : undefined, + }; + + const pin = new this.PinElement(pinOptions); + content = pin.element; + if (marker.opacity != null) { + content.style.opacity = String(marker.opacity); + } + } + + const advancedMarker = new this.AdvancedMarkerElement({ + position: marker.coordinate, + content: content, + title: marker.title, + gmpDraggable: marker.draggable, + }); + + return advancedMarker; + } +} diff --git a/swiftlint.config.js b/swiftlint.config.js new file mode 100644 index 0000000..f2db685 --- /dev/null +++ b/swiftlint.config.js @@ -0,0 +1,25 @@ +const base = require('@ionic/swiftlint-config'); + +// Extends the Ionic base config. This is a single small MapKit plugin: the +// delegate-heavy plugin class and the map-implementation file run a little past +// SwiftLint's conservative length defaults, and `x` / `y` are the honest names +// for the geometry the plugin exchanges with JS. Everything else stays on base. +module.exports = { + ...base, + // `unused_import` only runs under `swiftlint analyze`; the base lists it in + // `opt_in_rules`, which makes `swiftlint lint` print a placement note. Move it + // to `analyzer_rules` so a plain lint run is silent. + opt_in_rules: base.opt_in_rules.filter((rule) => rule !== 'unused_import'), + analyzer_rules: ['unused_import'], + identifier_name: { + excluded: ['id', 'x', 'y'], + }, + type_body_length: { + warning: 300, + error: 400, + }, + file_length: { + warning: 500, + error: 600, + }, +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8797203 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "allowUnreachableCode": false, + "declaration": true, + "declarationDir": "dist/typings", + "rootDir": "src", + "ignoreDeprecations": "6.0", + "esModuleInterop": true, + "inlineSources": true, + "lib": ["dom", "es2017"], + "module": "esnext", + "moduleResolution": "bundler", + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "outDir": "dist/esm", + "pretty": true, + "sourceMap": true, + "strict": true, + "skipLibCheck": true, + "types": ["google.maps"], + "target": "es2017" + }, + "files": ["src/index.ts"] +}