├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── CODEOWNERS ├── bundlewatch.config.json ├── dependabot.yml ├── sync-repo-settings.yaml └── workflows │ ├── bundlewatch.yml │ ├── codeql.yml │ ├── dependabot.yml │ ├── docs.yml │ ├── e2e.yml │ ├── package.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .prettierrc ├── .releaserc ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── e2e └── README.md ├── examples ├── anchor.ts ├── basic.ts ├── config.ts ├── hemispheres.ts ├── orientation.ts └── raycasting.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.examples.js ├── rollup.config.js ├── src ├── __tests__ │ ├── __utils__ │ │ └── createWebGlContext.ts │ ├── three.test.ts │ └── util.test.ts ├── index.ts ├── three.ts └── util.ts ├── tsconfig.examples.json ├── tsconfig.json └── typedoc.cjs /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/env", 5 | { 6 | "targets": { 7 | "browsers": "ie>=11, > 0.25%, not dead" 8 | }, 9 | "corejs": "3.6", 10 | "useBuiltIns": "usage" 11 | } 12 | ] 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | coverage/ 3 | dist/ 4 | docs/ 5 | lib/ 6 | node_modules/ 7 | public/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["eslint:recommended", "plugin:prettier/recommended"], 3 | "parserOptions": { 4 | "ecmaVersion": 12, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "plugins": ["jest"], 11 | "rules": { 12 | "no-var": 2, 13 | "prefer-arrow-callback": 2 14 | }, 15 | "overrides": [ 16 | { 17 | "files": ["*.ts", "*.tsx"], 18 | "parser": "@typescript-eslint/parser", 19 | "plugins": ["@typescript-eslint"], 20 | "extends": ["plugin:@typescript-eslint/recommended"], 21 | "rules": { 22 | "@typescript-eslint/ban-ts-comment": 0, 23 | "@typescript-eslint/ban-types": 1, 24 | "@typescript-eslint/no-empty-function": 1, 25 | "@typescript-eslint/member-ordering": 1, 26 | "@typescript-eslint/explicit-member-accessibility": [ 27 | 1, 28 | { 29 | "accessibility": "explicit", 30 | "overrides": { 31 | "accessors": "explicit", 32 | "constructors": "no-public", 33 | "methods": "explicit", 34 | "properties": "explicit", 35 | "parameterProperties": "explicit" 36 | } 37 | } 38 | ] 39 | } 40 | } 41 | ], 42 | "env": { 43 | "browser": true, 44 | "node": true, 45 | "es6": true, 46 | "jest/globals": true 47 | }, 48 | "globals": { "google": "readonly" } 49 | } 50 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners 16 | 17 | .github/ @googlemaps/admin 18 | -------------------------------------------------------------------------------- /.github/bundlewatch.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | { 4 | "path": "dist/index.*.js" 5 | } 6 | ], 7 | "ci": { 8 | "trackBranches": ["main"], 9 | "repoBranchBase": "main" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | version: 2 16 | updates: 17 | - package-ecosystem: "npm" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | -------------------------------------------------------------------------------- /.github/sync-repo-settings.yaml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # https://github.com/googleapis/repo-automation-bots/tree/main/packages/sync-repo-settings 16 | 17 | rebaseMergeAllowed: true 18 | squashMergeAllowed: true 19 | mergeCommitAllowed: false 20 | deleteBranchOnMerge: true 21 | branchProtectionRules: 22 | - pattern: main 23 | isAdminEnforced: false 24 | requiresStrictStatusChecks: false 25 | requiredStatusCheckContexts: 26 | - 'cla/google' 27 | - 'test' 28 | - 'snippet-bot check' 29 | - 'header-check' 30 | requiredApprovingReviewCount: 1 31 | requiresCodeOwnerReviews: true 32 | - pattern: master 33 | isAdminEnforced: false 34 | requiresStrictStatusChecks: false 35 | requiredStatusCheckContexts: 36 | - 'cla/google' 37 | - 'test' 38 | - 'snippet-bot check' 39 | - 'header-check' 40 | requiredApprovingReviewCount: 1 41 | requiresCodeOwnerReviews: true 42 | permissionRules: 43 | - team: admin 44 | permission: admin 45 | -------------------------------------------------------------------------------- /.github/workflows/bundlewatch.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Bundlewatch 16 | 17 | on: 18 | push: 19 | branches: 20 | - main 21 | pull_request: 22 | types: [synchronize, opened] 23 | 24 | jobs: 25 | bundlewatch: 26 | runs-on: ubuntu-latest 27 | env: 28 | CI_BRANCH_BASE: main 29 | steps: 30 | - uses: actions/checkout@v2 31 | - uses: jackyef/bundlewatch-gh-action@b9753bc9b3ea458ff21069eaf6206e01e046f0b5 32 | with: 33 | build-script: npm i 34 | bundlewatch-github-token: ${{ secrets.BUNDLEWATCH_GITHUB_TOKEN }} 35 | bundlewatch-config: .github/bundlewatch.config.json 36 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # For most projects, this workflow file will not need changing; you simply need 16 | # to commit it to your repository. 17 | # 18 | # You may wish to alter this file to override the set of languages analyzed, 19 | # or to provide custom queries or build logic. 20 | # 21 | # ******** NOTE ******** 22 | # We have attempted to detect the languages in your repository. Please check 23 | # the `language` matrix defined below to confirm you have the correct set of 24 | # supported CodeQL languages. 25 | # 26 | name: "CodeQL" 27 | 28 | on: 29 | push: 30 | branches: [main] 31 | pull_request: 32 | # The branches below must be a subset of the branches above 33 | branches: [main] 34 | schedule: 35 | - cron: "0 13 * * *" 36 | 37 | jobs: 38 | analyze: 39 | name: Analyze 40 | runs-on: ubuntu-latest 41 | permissions: 42 | actions: read 43 | contents: read 44 | security-events: write 45 | 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | language: ["javascript"] 50 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 51 | # Learn more: 52 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 53 | 54 | steps: 55 | - name: Checkout repository 56 | uses: actions/checkout@v2 57 | 58 | # Initializes the CodeQL tools for scanning. 59 | - name: Initialize CodeQL 60 | uses: github/codeql-action/init@v1 61 | with: 62 | languages: ${{ matrix.language }} 63 | # If you wish to specify custom queries, you can do so here or in a config file. 64 | # By default, queries listed here will override any specified in a config file. 65 | # Prefix the list here with "+" to use these queries and those in the config file. 66 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 67 | 68 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 69 | # If this step fails, then you should remove it and run the build manually (see below) 70 | - name: Autobuild 71 | uses: github/codeql-action/autobuild@v1 72 | 73 | # ℹ️ Command-line programs to run using the OS shell. 74 | # 📚 https://git.io/JvXDl 75 | 76 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 77 | # and modify them (or add more) to build your code if your project 78 | # uses a compiled language 79 | 80 | #- run: | 81 | # make bootstrap 82 | # make release 83 | 84 | - name: Perform CodeQL Analysis 85 | uses: github/codeql-action/analyze@v1 86 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Dependabot 16 | on: pull_request 17 | 18 | permissions: 19 | contents: write 20 | 21 | jobs: 22 | dependabot: 23 | runs-on: ubuntu-latest 24 | if: ${{ github.actor == 'dependabot[bot]' }} 25 | env: 26 | PR_URL: ${{github.event.pull_request.html_url}} 27 | GITHUB_TOKEN: ${{secrets.SYNCED_GITHUB_TOKEN_REPO}} 28 | steps: 29 | - name: approve 30 | run: gh pr review --approve "$PR_URL" 31 | - name: merge 32 | run: gh pr merge --auto --squash --delete-branch "$PR_URL" 33 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Docs 16 | on: [push, pull_request] 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v2 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: | 29 | npm i 30 | npm run docs 31 | - uses: peaceiris/actions-gh-pages@v3 32 | if: github.ref == 'refs/heads/main' 33 | with: 34 | github_token: ${{ secrets.GITHUB_TOKEN }} 35 | publish_dir: ./docs 36 | user_name: 'googlemaps-bot' 37 | user_email: 'googlemaps-bot@users.noreply.github.com' 38 | commit_message: ${{ github.event.head_commit.message }} 39 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: e2e 16 | on: 17 | push: 18 | schedule: 19 | - cron: "0 12 * * *" 20 | jobs: 21 | chrome: 22 | runs-on: ubuntu-latest 23 | services: 24 | hub: 25 | image: selenium/standalone-chrome 26 | volumes: 27 | - ${{ github.workspace }}:${{ github.workspace }} 28 | ports: 29 | - 4444:4444 30 | steps: 31 | - uses: actions/checkout@v2 32 | - run: npm i 33 | - run: npm run test:e2e 34 | firefox: 35 | runs-on: ubuntu-latest 36 | services: 37 | hub: 38 | image: selenium/standalone-firefox 39 | volumes: 40 | - ${{ github.workspace }}:${{ github.workspace }} 41 | ports: 42 | - 4444:4444 43 | steps: 44 | - uses: actions/checkout@v2 45 | - run: npm i 46 | - run: npm run test:e2e 47 | env: 48 | BROWSER: firefox 49 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Package 16 | on: 17 | - push 18 | - pull_request 19 | jobs: 20 | package: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v2 24 | - run: npm i 25 | - uses: jpoehnelt/verify-npm-files-action@main 26 | with: 27 | keys: | 28 | types 29 | main 30 | module 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Release 16 | on: 17 | push: 18 | branches: 19 | - main 20 | concurrency: release 21 | jobs: 22 | build: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/setup-node@v3 26 | with: 27 | node-version: '16' 28 | - name: Checkout 29 | uses: actions/checkout@v3 30 | with: 31 | token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 32 | - uses: actions/cache@v2 33 | with: 34 | path: ~/.npm 35 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 36 | restore-keys: | 37 | ${{ runner.os }}-node- 38 | - name: Test 39 | run: | 40 | npm i 41 | npm run lint 42 | npm test 43 | - name: Release 44 | uses: cycjimmy/semantic-release-action@v3 45 | with: 46 | semantic_version: 19 47 | extra_plugins: | 48 | @semantic-release/commit-analyzer@^9 49 | semantic-release-interval 50 | @semantic-release/release-notes-generator@^10 51 | @semantic-release/git 52 | @semantic-release/github@^8 53 | @semantic-release/npm@^9 54 | @googlemaps/semantic-release-config 55 | semantic-release-npm-deprecate 56 | env: 57 | GH_TOKEN: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }} 58 | NPM_TOKEN: ${{ secrets.NPM_WOMBAT_TOKEN }} 59 | RUNNER_DEBUG: 1 60 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | name: Test 16 | on: [push, pull_request] 17 | jobs: 18 | test: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/cache@v2 23 | with: 24 | path: ~/.npm 25 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 26 | restore-keys: | 27 | ${{ runner.os }}-node- 28 | - run: npm i 29 | - run: npm run lint 30 | - run: npm test 31 | - uses: codecov/codecov-action@v1 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | .npmrc 4 | **/docs 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Microbundle cache 62 | .rpt2_cache/ 63 | .rts2_cache_cjs/ 64 | .rts2_cache_es/ 65 | .rts2_cache_umd/ 66 | 67 | # Optional REPL history 68 | .node_repl_history 69 | 70 | # Output of 'npm pack' 71 | *.tgz 72 | 73 | # Yarn Integrity file 74 | .yarn-integrity 75 | 76 | # dotenv environment variables file 77 | .env 78 | .env.test 79 | 80 | # parcel-bundler cache (https://parceljs.org/) 81 | .cache 82 | 83 | # next.js build output 84 | .next 85 | 86 | # nuxt.js build output 87 | .nuxt 88 | 89 | # gatsby files 90 | .cache/ 91 | public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 109 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 110 | 111 | # User-specific stuff 112 | .idea/**/workspace.xml 113 | .idea/**/tasks.xml 114 | .idea/**/usage.statistics.xml 115 | .idea/**/dictionaries 116 | .idea/**/shelf 117 | 118 | # Generated files 119 | .idea/**/contentModel.xml 120 | 121 | # Sensitive or high-churn files 122 | .idea/**/dataSources/ 123 | .idea/**/dataSources.ids 124 | .idea/**/dataSources.local.xml 125 | .idea/**/sqlDataSources.xml 126 | .idea/**/dynamic.xml 127 | .idea/**/uiDesigner.xml 128 | .idea/**/dbnavigator.xml 129 | 130 | # Gradle 131 | .idea/**/gradle.xml 132 | .idea/**/libraries 133 | 134 | # Gradle and Maven with auto-import 135 | # When using Gradle or Maven with auto-import, you should exclude module files, 136 | # since they will be recreated, and may cause churn. Uncomment if using 137 | # auto-import. 138 | # .idea/modules.xml 139 | # .idea/*.iml 140 | # .idea/modules 141 | # *.iml 142 | # *.ipr 143 | 144 | # CMake 145 | cmake-build-*/ 146 | 147 | # Mongo Explorer plugin 148 | .idea/**/mongoSettings.xml 149 | 150 | # File-based project format 151 | *.iws 152 | 153 | # IntelliJ 154 | out/ 155 | 156 | # mpeltonen/sbt-idea plugin 157 | .idea_modules/ 158 | 159 | # JIRA plugin 160 | atlassian-ide-plugin.xml 161 | 162 | # Cursive Clojure plugin 163 | .idea/replstate.xml 164 | 165 | # Crashlytics plugin (for Android Studio and IntelliJ) 166 | com_crashlytics_export_strings.xml 167 | crashlytics.properties 168 | crashlytics-build.properties 169 | fabric.properties 170 | 171 | # Editor-based Rest Client 172 | .idea/httpRequests 173 | 174 | # Android studio 3.1+ serialized cache file 175 | .idea/caches/build_file_checksums.ser 176 | 177 | # General 178 | .DS_Store 179 | .AppleDouble 180 | .LSOverride 181 | 182 | # Icon must end with two \r 183 | Icon 184 | 185 | 186 | # Thumbnails 187 | ._* 188 | 189 | # Files that might appear in the root of a volume 190 | .DocumentRevisions-V100 191 | .fseventsd 192 | .Spotlight-V100 193 | .TemporaryItems 194 | .Trashes 195 | .VolumeIcon.icns 196 | .com.apple.timemachine.donotpresent 197 | 198 | # Directories potentially created on remote AFP share 199 | .AppleDB 200 | .AppleDesktop 201 | Network Trash Folder 202 | Temporary Items 203 | .apdisk 204 | 205 | # Windows thumbnail cache files 206 | Thumbs.db 207 | Thumbs.db:encryptable 208 | ehthumbs.db 209 | ehthumbs_vista.db 210 | 211 | # Dump file 212 | *.stackdump 213 | 214 | # Folder config file 215 | [Dd]esktop.ini 216 | 217 | # Recycle Bin used on file shares 218 | $RECYCLE.BIN/ 219 | 220 | # Windows Installer files 221 | *.cab 222 | *.msi 223 | *.msix 224 | *.msm 225 | *.msp 226 | 227 | # Windows shortcuts 228 | *.lnk 229 | .vscode 230 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { "trailingComma": "es5" } 2 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | extends: "@googlemaps/semantic-release-config" 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to become a contributor and submit your own code 2 | 3 | 1. Submit an issue describing your proposed change to the repo in question. 4 | 1. The repo owner will respond to your issue promptly. 5 | 1. Fork the desired repo, develop and test your code changes. 6 | 1. Ensure that your code adheres to the existing style in the code to which 7 | you are contributing. 8 | 1. Ensure that your code has an appropriate set of tests which all pass. 9 | 1. Title your pull request following [Conventional Commits](https://www.conventionalcommits.org/) styling. 10 | 1. Submit a pull request. 11 | 12 | ## Running the tests 13 | 14 | 1. Install dependencies: 15 | 16 | npm i 17 | 1. Run lint 18 | 19 | npm run lint 20 | npm run format # will fix some issues 21 | 22 | 1. Run the tests: 23 | 24 | # Run unit tests. 25 | npm test 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Maps ThreeJS Overlay View and Utilities 2 | 3 | [![npm](https://img.shields.io/npm/v/@googlemaps/three)](https://www.npmjs.com/package/@googlemaps/three) 4 | ![Build](https://github.com/googlemaps/js-three/workflows/Test/badge.svg) 5 | ![Release](https://github.com/googlemaps/js-three/workflows/Release/badge.svg) 6 | [![codecov](https://codecov.io/gh/googlemaps/js-three/branch/main/graph/badge.svg)](https://codecov.io/gh/googlemaps/js-three) 7 | ![GitHub contributors](https://img.shields.io/github/contributors/googlemaps/js-three?color=green) 8 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 9 | [![](https://github.com/jpoehnelt/in-solidarity-bot/raw/main/static//badge-flat.png)](https://github.com/apps/in-solidarity) 10 | [![Discord](https://img.shields.io/discord/676948200904589322?color=6A7EC2&logo=discord&logoColor=ffffff)](https://discord.gg/jRteCzP) 11 | 12 | ## Description 13 | 14 | Add [three.js](https://threejs.org) objects to Google Maps Platform JS. The 15 | library provides a `ThreeJSOverlayView` class extending `google.maps.WebGLOverlayView` 16 | and utility functions for converting geo-coordinates (latitude/longitude) to 17 | vectors in the coordinate system used by three.js. 18 | 19 | ## Install 20 | 21 | Available via npm as the package [@googlemaps/three](https://www.npmjs.com/package/@googlemaps/three). 22 | 23 | ``` 24 | npm i @googlemaps/three 25 | ``` 26 | 27 | Alternatively you can load the package directly to the html document using 28 | unpkg or other CDNs. In this case, make sure to load three.js before loading 29 | this library: 30 | 31 | ``` 32 | 33 | 34 | ``` 35 | 36 | When adding via unpkg, the package can be accessed as 37 | `google.maps.plugins.three`. A version can be specified by using 38 | `https://unpkg.com/@googlemaps/three@VERSION/dist/...`. 39 | 40 | The third option to use it is via ES-Module imports, similar to how the 41 | three.js examples work. For this, you first need to specify an 42 | [importmap](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/script/type/importmap) 43 | (example using unpkg.com, but it works the same way with any other CDN 44 | or self-hosted files): 45 | 46 | ```html 47 | 55 | ``` 56 | 57 | In order to support browsers that don't yet implement importmap, you can 58 | use the [es-module-shims package](https://github.com/guybedford/es-module-shims). 59 | 60 | After that, you can use three.js and the ThreeJSOverlayView like you would when 61 | using a bundler. 62 | 63 | ```html 64 | 70 | ``` 71 | 72 | ## Documentation 73 | 74 | Checkout the reference [documentation](https://googlemaps.github.io/js-three/index.html). 75 | 76 | ### Coordinates, Projection and Anchor-Points 77 | 78 | The coordinate system within the three.js scene (so-called 'world 79 | coordinates') is a right-handed coordinate system in z-up orientation. 80 | The y-axis is pointing true north, and the x-axis is pointing east. The 81 | units are meters. So the point `new Vector3(0, 50, 10)` is 10 meters 82 | above ground and 50 meters east of the specified anchor point. 83 | 84 | This anchor-point and orientation can be set in the constructor, or by using the 85 | `setAnchor()` and `setUpAxis()`-methods (be aware that all object-positions in 86 | your scene depend on the anchor-point and orientation, so they have to be 87 | recomputed when either of them is changed): 88 | 89 | ```typescript 90 | import { ThreeJSOverlayView } from "@googlemaps/three"; 91 | 92 | const overlay = new ThreeJSOverlayView({ 93 | anchor: { lat: 37.7793, lng: -122.4192, altitude: 0 }, 94 | upAxis: "Y", 95 | }); 96 | 97 | overlay.setAnchor({ lat: 35.680432, lng: 139.769013, altitude: 0 }); 98 | overlay.setUpAxis("Z"); 99 | // can also be specified as Vector3: 100 | overlay.setUpAxis(new Vector3(0, 0, 1)); 101 | ``` 102 | 103 | > The default up-axis used in this library is the z-axis (+x is east 104 | > and +y is north), which is different from the y-up orientation normally 105 | > used in three. 106 | 107 | All computations on the GPU related to the position use float32 numbers, 108 | which limits the possible precision to about 7 decimal digits. Because 109 | of this, we cannot use a global reference system and still have the 110 | precision to show details in the meters to centimeters range. 111 | 112 | This is where the anchor point is important. The anchor specifies the 113 | geo-coordinates (lat/lng/altitude) where the origin of the world-space 114 | coordinate system is, and you should always define it close to where the 115 | objects are placed in the scene - unless of course you are only working with 116 | large-scale (city-sized) objects distributed globally. 117 | 118 | Another reason why setting the anchor close to the objects in the scene 119 | is generally a good idea: In the mercator map-projection used in Google Maps, 120 | the scaling of meters is only accurate in regions close to the equator. This 121 | can be compensated for by applying a scale factor that depends on the 122 | latitude of the anchor. This scale factor is factored into the coordinate 123 | calculations in WebGlOverlayView based on the latitude of the anchor. 124 | 125 | #### Converting coordinates 126 | 127 | When you need more than just a single georeferenced object in your scene, 128 | you need to compute the world-space position for those coordinates. The 129 | ThreeJSOverlayView class provides a helper function for this conversion that 130 | takes the current `anchor` and `upAxis` into account: 131 | 132 | ```typescript 133 | const coordinates = { lat: 12.34, lng: 56.78 }; 134 | const position: Vector3 = overlay.latLngAltitudeToVector3(coordinates); 135 | 136 | // alternative: pass the Vector3 to write the position 137 | // to as the second parameter, so to set the position of a mesh: 138 | overlay.latLngAltitudeToVector3(coordinates, mesh.position); 139 | ``` 140 | 141 | ### Raycasting and Interactions 142 | 143 | If you want to add interactivity to any three.js content, you typically 144 | have to implement raycasting. We took care of that for you, and the 145 | ThreeJSOverlayView provides a method `overlay.raycast()` for this. To make 146 | use of it, you first have to keep track of mouse movements on the map: 147 | 148 | ```js 149 | import { Vector2 } from "three"; 150 | 151 | // ... 152 | 153 | const mapDiv = map.getDiv(); 154 | const mousePosition = new Vector2(); 155 | 156 | map.addListener("mousemove", (ev) => { 157 | const { domEvent } = ev; 158 | const { left, top, width, height } = mapDiv.getBoundingClientRect(); 159 | 160 | const x = domEvent.clientX - left; 161 | const y = domEvent.clientY - top; 162 | 163 | mousePosition.x = 2 * (x / width) - 1; 164 | mousePosition.y = 1 - 2 * (y / height); 165 | 166 | // since the actual raycasting is performed when the next frame is 167 | // rendered, we have to make sure that it will be called for the next frame. 168 | overlay.requestRedraw(); 169 | }); 170 | ``` 171 | 172 | With the mouse position being always up to date, you can then use the 173 | `raycast()` function in the `onBeforeDraw` callback. 174 | In this example, we change the color of the object under the cursor: 175 | 176 | ```js 177 | const DEFAULT_COLOR = 0xffffff; 178 | const HIGHLIGHT_COLOR = 0xff0000; 179 | 180 | let highlightedObject = null; 181 | 182 | overlay.onBeforeDraw = () => { 183 | const intersections = overlay.raycast(mousePosition); 184 | if (highlightedObject) { 185 | highlightedObject.material.color.setHex(DEFAULT_COLOR); 186 | } 187 | 188 | if (intersections.length === 0) return; 189 | 190 | highlightedObject = intersections[0].object; 191 | highlightedObject.material.color.setHex(HIGHLIGHT_COLOR); 192 | }; 193 | ``` 194 | 195 | The full examples can be found in [`./examples/raycasting.ts`](./examples/raycasting.ts). 196 | 197 | ## Example 198 | 199 | The following example provides a skeleton for adding objects to the map with this library. 200 | 201 | ```js 202 | import * as THREE from "three"; 203 | import { ThreeJSOverlayView, latLngToVector3 } from "@googlemaps/three"; 204 | 205 | // when loading via UMD, remove the imports and use this instead: 206 | // const { ThreeJSOverlayView, latLngToVector3 } = google.maps.plugins.three; 207 | 208 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 209 | const overlay = new ThreeJSOverlayView({ 210 | map, 211 | upAxis: "Y", 212 | anchor: mapOptions.center, 213 | }); 214 | 215 | // create a box mesh 216 | const box = new THREE.Mesh( 217 | new THREE.BoxGeometry(10, 50, 10), 218 | new THREE.MeshMatcapMaterial() 219 | ); 220 | // move the box up so the origin of the box is at the bottom 221 | box.geometry.translateY(25); 222 | 223 | // set position at center of map 224 | box.position.copy(overlay.latLngAltitudeToVector3(mapOptions.center)); 225 | 226 | // add box mesh to the scene 227 | overlay.scene.add(box); 228 | 229 | // rotate the box using requestAnimationFrame 230 | const animate = () => { 231 | box.rotateY(THREE.MathUtils.degToRad(0.1)); 232 | 233 | requestAnimationFrame(animate); 234 | }; 235 | 236 | // start animation loop 237 | requestAnimationFrame(animate); 238 | ``` 239 | 240 | This adds a box to the map. 241 | 242 | threejs box on map 243 | 244 | ## Demos 245 | 246 | View the package in action: 247 | 248 | - [Basic Example](https://googlemaps.github.io/js-three/public/basic/) 249 | - [Anchor Example](https://googlemaps.github.io/js-three/public/anchor/) 250 | - [Orientation Example](https://googlemaps.github.io/js-three/public/orientation/) 251 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Report a security issue 2 | 3 | To report a security issue, please use https://g.co/vulnz. We use 4 | https://g.co/vulnz for our intake, and do coordination and disclosure here on 5 | GitHub (including using GitHub Security Advisory). The Google Security Team will 6 | respond within 5 working days of your report on g.co/vulnz. 7 | 8 | To contact us about other bugs, please open an issue on GitHub. 9 | 10 | > **Note**: This file is synchronized from the https://github.com/googlemaps/.github repository. 11 | -------------------------------------------------------------------------------- /e2e/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/googlemaps/js-three/19874f82388ddb9ef344904ef2a9459a936cc16a/e2e/README.md -------------------------------------------------------------------------------- /examples/anchor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LOADER_OPTIONS, MAP_ID } from "./config"; 18 | import { ThreeJSOverlayView, WORLD_SIZE } from "../src"; 19 | 20 | import { Loader } from "@googlemaps/js-api-loader"; 21 | import { AxesHelper } from "three"; 22 | 23 | const mapOptions = { 24 | center: { 25 | lat: 45, 26 | lng: 0, 27 | }, 28 | mapId: MAP_ID, 29 | zoom: 5, 30 | heading: -45, 31 | tilt: 45, 32 | }; 33 | 34 | new Loader(LOADER_OPTIONS).load().then(() => { 35 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 36 | const overlay = new ThreeJSOverlayView({ 37 | anchor: { ...mapOptions.center, altitude: 0 }, 38 | map, 39 | }); 40 | 41 | overlay.scene.add(new AxesHelper(WORLD_SIZE)); 42 | }); 43 | -------------------------------------------------------------------------------- /examples/basic.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LOADER_OPTIONS, MAP_ID } from "./config"; 18 | import { ThreeJSOverlayView } from "../src"; 19 | 20 | import { Loader } from "@googlemaps/js-api-loader"; 21 | import { BoxGeometry, MathUtils, Mesh, MeshMatcapMaterial } from "three"; 22 | 23 | const mapOptions = { 24 | center: { 25 | lng: -122.343787, 26 | lat: 47.607465, 27 | }, 28 | mapId: MAP_ID, 29 | zoom: 15, 30 | heading: 45, 31 | tilt: 67, 32 | }; 33 | 34 | new Loader(LOADER_OPTIONS).load().then(() => { 35 | // create the map and ThreeJS Overlay 36 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 37 | const overlay = new ThreeJSOverlayView({ map }); 38 | 39 | // Create a box mesh 40 | const box = new Mesh( 41 | new BoxGeometry(100, 200, 500), 42 | new MeshMatcapMaterial() 43 | ); 44 | 45 | // set position at center of map 46 | const pos = overlay.latLngAltitudeToVector3(mapOptions.center); 47 | box.position.copy(pos); 48 | 49 | // set position vertically 50 | box.position.z = 25; 51 | 52 | // add box mesh to the scene 53 | overlay.scene.add(box); 54 | 55 | // rotate the box using requestAnimationFrame 56 | const animate = () => { 57 | box.rotateZ(MathUtils.degToRad(0.1)); 58 | 59 | requestAnimationFrame(animate); 60 | }; 61 | 62 | requestAnimationFrame(animate); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LoaderOptions } from "@googlemaps/js-api-loader"; 18 | 19 | export const MAP_ID = "7b9a897acd0a63a4"; 20 | 21 | export const LOADER_OPTIONS: LoaderOptions = { 22 | apiKey: "AIzaSyD8xiaVPWB02OeQkJOenLiJzdeUHzlhu00", 23 | version: "beta", 24 | libraries: [], 25 | }; 26 | -------------------------------------------------------------------------------- /examples/hemispheres.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LOADER_OPTIONS, MAP_ID } from "./config"; 18 | import { ThreeJSOverlayView } from "../src"; 19 | 20 | import { Loader } from "@googlemaps/js-api-loader"; 21 | import { BoxGeometry, Mesh, MeshMatcapMaterial } from "three"; 22 | 23 | const mapOptions = { 24 | center: { 25 | lng: 0, 26 | lat: 0, 27 | }, 28 | mapId: MAP_ID, 29 | zoom: 4, 30 | tilt: 67, 31 | }; 32 | 33 | new Loader(LOADER_OPTIONS).load().then(() => { 34 | // create the map and overlay 35 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 36 | const overlay = new ThreeJSOverlayView({ map }); 37 | 38 | [ 39 | { lat: 45, lng: -90 }, 40 | { lat: 45, lng: 90 }, 41 | { lat: -45, lng: -90 }, 42 | { lat: -45, lng: 90 }, 43 | ].forEach((latLng: google.maps.LatLngLiteral) => { 44 | // create a box mesh with origin on the ground, in z-up orientation 45 | const geometry = new BoxGeometry(10, 50, 10) 46 | .translate(0, 25, 0) 47 | .rotateX(Math.PI / 2); 48 | 49 | const box = new Mesh(geometry, new MeshMatcapMaterial()); 50 | 51 | // make it huge 52 | box.scale.multiplyScalar(10000); 53 | 54 | // set position at center of map 55 | overlay.latLngAltitudeToVector3(latLng, box.position); 56 | 57 | // add box mesh to the scene 58 | overlay.scene.add(box); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/orientation.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LOADER_OPTIONS, MAP_ID } from "./config"; 18 | import { ThreeJSOverlayView, WORLD_SIZE } from "../src"; 19 | 20 | import { Loader } from "@googlemaps/js-api-loader"; 21 | import { AxesHelper, Scene } from "three"; 22 | 23 | const mapOptions = { 24 | center: { 25 | lat: 0, 26 | lng: 0, 27 | }, 28 | mapId: MAP_ID, 29 | zoom: 5, 30 | heading: -45, 31 | tilt: 45, 32 | }; 33 | 34 | new Loader(LOADER_OPTIONS).load().then(() => { 35 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 36 | const scene = new Scene(); 37 | 38 | scene.add(new AxesHelper(WORLD_SIZE)); 39 | 40 | new ThreeJSOverlayView({ scene, map }); 41 | }); 42 | -------------------------------------------------------------------------------- /examples/raycasting.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { LOADER_OPTIONS } from "./config"; 18 | import { ThreeJSOverlayView } from "../src"; 19 | 20 | import { Loader } from "@googlemaps/js-api-loader"; 21 | import { 22 | AxesHelper, 23 | CylinderGeometry, 24 | GridHelper, 25 | MathUtils, 26 | Mesh, 27 | MeshMatcapMaterial, 28 | Vector2, 29 | } from "three"; 30 | 31 | // the corners of the field in the Levi’s Stadium in Santa Clara 32 | const coordinates = [ 33 | { lng: -121.9702904, lat: 37.4034362 }, 34 | { lng: -121.9698018, lat: 37.4027095 }, 35 | { lng: -121.9693109, lat: 37.402918 }, 36 | { lng: -121.969804, lat: 37.4036465 }, 37 | ]; 38 | const center = { lng: -121.9698032, lat: 37.4031777, altitude: 0 }; 39 | 40 | const DEFAULT_COLOR = 0xffffff; 41 | const HIGHLIGHT_COLOR = 0xff0000; 42 | 43 | const mapOptions = { 44 | center, 45 | mapId: "7057886e21226ff7", 46 | zoom: 18, 47 | tilt: 67.5, 48 | disableDefaultUI: true, 49 | backgroundColor: "transparent", 50 | gestureHandling: "greedy", 51 | }; 52 | 53 | new Loader(LOADER_OPTIONS).load().then(() => { 54 | // create the map and overlay 55 | const map = new google.maps.Map(document.getElementById("map"), mapOptions); 56 | const overlay = new ThreeJSOverlayView({ map, anchor: center, upAxis: "Y" }); 57 | 58 | const mapDiv = map.getDiv(); 59 | const mousePosition = new Vector2(); 60 | 61 | map.addListener("mousemove", (ev: google.maps.MapMouseEvent) => { 62 | const domEvent = ev.domEvent as MouseEvent; 63 | const { left, top, width, height } = mapDiv.getBoundingClientRect(); 64 | 65 | const x = domEvent.clientX - left; 66 | const y = domEvent.clientY - top; 67 | 68 | mousePosition.x = 2 * (x / width) - 1; 69 | mousePosition.y = 1 - 2 * (y / height); 70 | 71 | // since the actual raycasting is performed when the next frame is 72 | // rendered, we have to make sure that it will be called for the next frame. 73 | overlay.requestRedraw(); 74 | }); 75 | 76 | // grid- and axes helpers to help with the orientation 77 | const grid = new GridHelper(1); 78 | 79 | grid.rotation.y = MathUtils.degToRad(28.1); 80 | grid.scale.set(48.8, 0, 91.44); 81 | overlay.scene.add(grid); 82 | overlay.scene.add(new AxesHelper(20)); 83 | 84 | const meshes = coordinates.map((p) => { 85 | const mesh = new Mesh( 86 | new CylinderGeometry(2, 1, 20, 24, 1), 87 | new MeshMatcapMaterial() 88 | ); 89 | mesh.geometry.translate(0, mesh.geometry.parameters.height / 2, 0); 90 | overlay.latLngAltitudeToVector3(p, mesh.position); 91 | 92 | overlay.scene.add(mesh); 93 | 94 | return mesh; 95 | }); 96 | 97 | let highlightedObject: (typeof meshes)[number] | null = null; 98 | 99 | overlay.onBeforeDraw = () => { 100 | const intersections = overlay.raycast(mousePosition, meshes, { 101 | recursive: false, 102 | }); 103 | 104 | if (highlightedObject) { 105 | // when there's a previously highlighted object, reset the highlighting 106 | highlightedObject.material.color.setHex(DEFAULT_COLOR); 107 | } 108 | 109 | if (intersections.length === 0) { 110 | // reset default cursor when no object is under the cursor 111 | map.setOptions({ draggableCursor: null }); 112 | highlightedObject = null; 113 | return; 114 | } 115 | 116 | // change the color of the object and update the map-cursor to indicate 117 | // the object is clickable. 118 | highlightedObject = intersections[0].object; 119 | highlightedObject.material.color.setHex(HIGHLIGHT_COLOR); 120 | map.setOptions({ draggableCursor: "pointer" }); 121 | }; 122 | }); 123 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export default { 18 | roots: ["/src"], 19 | preset: "ts-jest", 20 | testPathIgnorePatterns: ["/dist/", "/__utils__/"], 21 | testEnvironment: "jsdom", 22 | setupFilesAfterEnv: ["jest-extended/all"], 23 | }; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@googlemaps/three", 3 | "version": "4.0.18", 4 | "type": "module", 5 | "keywords": [ 6 | "google", 7 | "maps", 8 | "webgl", 9 | "threejs" 10 | ], 11 | "homepage": "https://github.com/googlemaps/js-three", 12 | "bugs": { 13 | "url": "https://github.com/googlemaps/js-three/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/googlemaps/js-three.git" 18 | }, 19 | "license": "Apache-2.0", 20 | "author": "Justin Poehnelt", 21 | "source": "src/index.ts", 22 | "main": "dist/index.umd.js", 23 | "exports": { 24 | ".": { 25 | "import": { 26 | "types": "./dist/index.d.ts", 27 | "default": "./dist/index.esm.js" 28 | }, 29 | "require": "./dist/index.umd.js", 30 | "umd": "./dist/index.umd.js", 31 | "browser": "./dist/index.esm.js" 32 | } 33 | }, 34 | "unpkg": "dist/index.min.js", 35 | "module": "dist/index.esm.js", 36 | "types": "dist/index.d.ts", 37 | "files": [ 38 | "/src", 39 | "/dist" 40 | ], 41 | "scripts": { 42 | "build:examples": "rm -rf public && rollup -c rollup.config.examples.js", 43 | "docs": "typedoc src/index.ts && npm run build:examples && cp -r dist docs/dist && cp -r public docs/public", 44 | "format": "eslint . --fix", 45 | "lint": "eslint .", 46 | "prepare": "rm -rf dist && rollup -c", 47 | "start": "run-p start:*", 48 | "start:rollup": "rollup -c rollup.config.examples.js -w --no-watch.clearScreen", 49 | "start:server": "http-server ./public", 50 | "test": "jest --coverage=true src/*", 51 | "test:e2e": "jest --passWithNoTests e2e/*" 52 | }, 53 | "peerDependencies": { 54 | "three": "*" 55 | }, 56 | "devDependencies": { 57 | "@babel/preset-env": "^7.25.7", 58 | "@babel/preset-modules": "^0.1.6", 59 | "@babel/runtime-corejs3": "^7.25.7", 60 | "@googlemaps/jest-mocks": "^2.21.4", 61 | "@googlemaps/js-api-loader": "^1.16.8", 62 | "@rollup/plugin-babel": "^6.0.4", 63 | "@rollup/plugin-commonjs": "^26.0.1", 64 | "@rollup/plugin-html": "^1.0.3", 65 | "@rollup/plugin-json": "^6.1.0", 66 | "@rollup/plugin-node-resolve": "^15.2.4", 67 | "@rollup/plugin-terser": "^0.4.4", 68 | "@rollup/plugin-typescript": "^11.1.6", 69 | "@types/d3-random": "^3.0.3", 70 | "@types/google.maps": "^3.58.1", 71 | "@types/jest": "^29.5.14", 72 | "@types/proj4": "^2.5.5", 73 | "@types/selenium-webdriver": "^4.1.25", 74 | "@types/stats.js": "^0.17.3", 75 | "@types/three": "^0.156.0", 76 | "@typescript-eslint/eslint-plugin": "^7.0.0", 77 | "@typescript-eslint/parser": "^6.19.1", 78 | "core-js": "^3.37.1", 79 | "d3-random": "^3.0.1", 80 | "eslint": "^8.57.0", 81 | "eslint-config-prettier": "^9.1.0", 82 | "eslint-plugin-jest": "^28.9.0", 83 | "eslint-plugin-prettier": "^5.1.3", 84 | "geckodriver": "^5.0.0", 85 | "http-server": "^14.1.1", 86 | "jest": "^29.7.0", 87 | "jest-environment-jsdom": "^29.7.0", 88 | "jest-extended": "^4.0.2", 89 | "jest-webgl-canvas-mock": "^2.5.3", 90 | "npm-run-all": "^4.1.5", 91 | "prettier": "^3.3.3", 92 | "rollup": "^4.22.4", 93 | "selenium-webdriver": "^4.23.0", 94 | "three": "^0.161.0", 95 | "ts-jest": "^29.2.5", 96 | "typedoc": "^0.25.13", 97 | "typescript": "^5.4.5" 98 | }, 99 | "publishConfig": { 100 | "access": "public", 101 | "registry": "https://wombat-dressing-room.appspot.com" 102 | } 103 | } -------------------------------------------------------------------------------- /rollup.config.examples.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import fs from "node:fs"; 17 | import path from "node:path"; 18 | import * as url from "node:url"; 19 | 20 | import html, { makeHtmlAttributes } from "@rollup/plugin-html"; 21 | 22 | import commonjs from "@rollup/plugin-commonjs"; 23 | 24 | import jsonNodeResolve from "@rollup/plugin-json"; 25 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 26 | 27 | import typescript from "@rollup/plugin-typescript"; 28 | 29 | const template = ({ attributes, files, meta, publicPath, title }) => { 30 | const scripts = (files.js || []) 31 | .map(({ fileName }) => { 32 | const attrs = makeHtmlAttributes(attributes.script); 33 | return ``; 34 | }) 35 | .join("\n"); 36 | 37 | const links = (files.css || []) 38 | .map(({ fileName }) => { 39 | const attrs = makeHtmlAttributes(attributes.link); 40 | return ``; 41 | }) 42 | .join("\n"); 43 | 44 | const metas = meta 45 | .map((input) => { 46 | const attrs = makeHtmlAttributes(input); 47 | return ``; 48 | }) 49 | .join("\n"); 50 | 51 | return ` 52 | 53 | 54 | 55 | ${metas} 56 | ${title} 57 | 64 | ${links} 65 | 66 | 67 |
68 | ${scripts} 69 | 70 | `; 71 | }; 72 | 73 | const typescriptOptions = { 74 | tsconfig: "tsconfig.examples.json", 75 | compilerOptions: { 76 | sourceMap: true, 77 | inlineSources: true, 78 | }, 79 | }; 80 | 81 | const dirname = url.fileURLToPath(new URL(".", import.meta.url)); 82 | const examples = fs 83 | .readdirSync(path.join(dirname, "examples")) 84 | .filter((f) => f !== "config.ts") 85 | .map((f) => f.slice(0, f.length - 3)); 86 | 87 | export default examples.map((name) => ({ 88 | input: `examples/${name}.ts`, 89 | 90 | plugins: [ 91 | typescript(typescriptOptions), 92 | commonjs(), 93 | nodeResolve(), 94 | jsonNodeResolve(), 95 | ], 96 | output: { 97 | dir: `public/${name}`, 98 | sourcemap: "inline", 99 | plugins: [ 100 | html({ 101 | fileName: `index.html`, 102 | title: `@googlemaps/three: ${name}`, 103 | template, 104 | }), 105 | ], 106 | manualChunks: (id) => { 107 | if (id.includes("node_modules")) { 108 | return "vendor"; 109 | } 110 | }, 111 | }, 112 | })); 113 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { babel } from "@rollup/plugin-babel"; 18 | import commonjs from "@rollup/plugin-commonjs"; 19 | import terser from "@rollup/plugin-terser"; 20 | import typescript from "@rollup/plugin-typescript"; 21 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 22 | 23 | const babelOptions = { 24 | extensions: [".js", ".ts"], 25 | babelHelpers: "bundled", 26 | }; 27 | 28 | const terserOptions = { output: { comments: "some" } }; 29 | 30 | export default [ 31 | { 32 | input: "src/index.ts", 33 | plugins: [ 34 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./" }), 35 | 36 | commonjs(), 37 | babel(babelOptions), 38 | nodeResolve(), 39 | terser(terserOptions), 40 | ], 41 | external: ["three"], 42 | output: [ 43 | { 44 | file: "dist/index.umd.js", 45 | format: "umd", 46 | sourcemap: true, 47 | name: "google.maps.plugins.three", 48 | globals: { 49 | three: "THREE", 50 | }, 51 | }, 52 | { 53 | file: "dist/index.min.js", 54 | format: "iife", 55 | sourcemap: true, 56 | name: "google.maps.plugins.three", 57 | globals: { 58 | three: "THREE", 59 | }, 60 | }, 61 | ], 62 | }, 63 | { 64 | input: "src/index.ts", 65 | plugins: [ 66 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./" }), 67 | 68 | commonjs(), 69 | babel(babelOptions), 70 | nodeResolve(), 71 | terser(terserOptions), 72 | ], 73 | external: ["three"], 74 | output: { 75 | file: "dist/index.dev.js", 76 | format: "iife", 77 | sourcemap: true, 78 | name: "google.maps.plugins.three", 79 | globals: { 80 | three: "THREE", 81 | }, 82 | }, 83 | }, 84 | { 85 | input: "src/index.ts", 86 | external: ["three"], 87 | plugins: [ 88 | typescript({ tsconfig: "./tsconfig.json", declarationDir: "./" }), 89 | babel({ 90 | presets: ["@babel/preset-modules"], 91 | babelrc: false, 92 | extensions: [".js", ".ts"], 93 | babelHelpers: "bundled", 94 | }), 95 | terser(terserOptions), 96 | ], 97 | output: { 98 | file: "dist/index.esm.js", 99 | sourcemap: true, 100 | format: "esm", 101 | }, 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /src/__tests__/__utils__/createWebGlContext.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // using 'jest-webgl-canvas-mock' as intended as a setup-script in the jest 18 | // configuration causes an error 'TypeError: Cannot redefine property: window' 19 | // in newer node-version (last known working version is 18.13.0), which is why 20 | // we do the initialization manually here. 21 | // @ts-ignore 22 | import registerWebglMock from "jest-webgl-canvas-mock/lib/window.js"; 23 | 24 | /** 25 | * Creates a mocked WebGL 1.0 context (based on the one provided by 26 | * the jest-webgl-canvas-mock package) three.js can work with. 27 | */ 28 | export function createWebGlContext() { 29 | registerWebglMock(window); 30 | 31 | const gl = new WebGLRenderingContext(); 32 | const glParameters: Record = { 33 | [gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS]: 8, 34 | [gl.VERSION]: "WebGL 1.0 (OpenGL ES 2.0 Chromium)", 35 | [gl.SCISSOR_BOX]: [0, 0, 100, 100], 36 | [gl.VIEWPORT]: [0, 0, 100, 100], 37 | }; 38 | 39 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 40 | const glExtensions: Record = { 41 | EXT_blend_minmax: {}, 42 | }; 43 | 44 | jest.spyOn(gl, "getContextAttributes").mockReturnValue({}); 45 | jest.spyOn(gl, "getParameter").mockImplementation((key) => glParameters[key]); 46 | jest.spyOn(gl, "getShaderPrecisionFormat").mockImplementation(() => ({ 47 | rangeMin: 127, 48 | rangeMax: 127, 49 | precision: 23, 50 | })); 51 | 52 | const getExtensionOrig = gl.getExtension; 53 | jest.spyOn(gl, "getExtension").mockImplementation((id) => { 54 | return glExtensions[id] || getExtensionOrig(id); 55 | }); 56 | 57 | const canvas = document.createElement("canvas"); 58 | canvas.width = 200; 59 | canvas.height = 100; 60 | 61 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 62 | (gl as any).canvas = canvas; 63 | 64 | return gl; 65 | } 66 | -------------------------------------------------------------------------------- /src/__tests__/three.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* eslint-disable @typescript-eslint/explicit-member-accessibility */ 18 | 19 | // prevent "WARNING: Multiple instances of Three.js being imported.” when 20 | // importing three.js 21 | Object.defineProperty(window, "__THREE__", { 22 | get: () => null, 23 | set: () => null, 24 | configurable: false, 25 | }); 26 | 27 | import { ThreeJSOverlayView, ThreeJSOverlayViewOptions } from "../three"; 28 | import * as util from "../util"; 29 | 30 | import { 31 | BoxGeometry, 32 | Camera, 33 | Group, 34 | Light, 35 | Matrix4, 36 | Mesh, 37 | Object3D, 38 | PerspectiveCamera, 39 | RaycasterParameters, 40 | Scene, 41 | Vector2, 42 | Vector3, 43 | Vector4, 44 | WebGLRenderer, 45 | } from "three"; 46 | 47 | import "jest-extended"; 48 | import { initialize, Map } from "@googlemaps/jest-mocks"; 49 | import { createWebGlContext } from "./__utils__/createWebGlContext"; 50 | 51 | // setup mocked dependencies 52 | jest.mock("../util"); 53 | 54 | beforeEach(() => { 55 | initialize(); 56 | google.maps.WebGLOverlayView = jest.fn().mockImplementation(() => { 57 | return new (class extends google.maps.MVCObject { 58 | getMap = jest.fn(); 59 | setMap = jest.fn(); 60 | requestRedraw = jest.fn(); 61 | requestStateUpdate = jest.fn(); 62 | addListener = jest.fn().mockImplementation(() => { 63 | return { remove: jest.fn() } as google.maps.MapsEventListener; 64 | }); 65 | })(); 66 | }); 67 | }); 68 | 69 | afterEach(() => { 70 | jest.restoreAllMocks(); 71 | }); 72 | 73 | describe("basic functions", () => { 74 | test("instantiates with defaults", () => { 75 | const overlay = new ThreeJSOverlayView(); 76 | 77 | expect(overlay["overlay"]).toBeDefined(); 78 | expect(overlay["camera"]).toBeInstanceOf(PerspectiveCamera); 79 | 80 | expect(overlay.scene).toBeInstanceOf(Scene); 81 | 82 | // required hooks must be defined 83 | expect(overlay["overlay"].onAdd).toBeDefined(); 84 | expect(overlay["overlay"].onRemove).toBeDefined(); 85 | expect(overlay["overlay"].onContextLost).toBeDefined(); 86 | expect(overlay["overlay"].onContextRestored).toBeDefined(); 87 | expect(overlay["overlay"].onDraw).toBeDefined(); 88 | }); 89 | 90 | test("instantiates with map and calls setMap", () => { 91 | const map = new Map( 92 | document.createElement("div"), 93 | {} 94 | ) as unknown as google.maps.Map; 95 | 96 | const overlay = new ThreeJSOverlayView({ 97 | map, 98 | }); 99 | 100 | expect(overlay["overlay"].setMap).toHaveBeenCalledWith(map); 101 | }); 102 | 103 | test("setMap is called on overlay", () => { 104 | const map = new Map( 105 | document.createElement("div"), 106 | {} 107 | ) as unknown as google.maps.Map; 108 | const overlay = new ThreeJSOverlayView(); 109 | overlay.setMap(map); 110 | 111 | expect(overlay["overlay"].setMap).toHaveBeenCalledWith(map); 112 | }); 113 | 114 | test("getMap is called on overlay", () => { 115 | const overlay = new ThreeJSOverlayView(); 116 | overlay.getMap(); 117 | 118 | expect(overlay["overlay"].getMap).toHaveBeenCalledWith(); 119 | }); 120 | 121 | test("addListener is called on overlay", () => { 122 | const overlay = new ThreeJSOverlayView(); 123 | const handler = jest.fn(); 124 | const eventName = "foo"; 125 | 126 | expect(overlay.addListener(eventName, handler)).toBeDefined(); 127 | expect(overlay["overlay"].addListener).toHaveBeenCalledWith( 128 | eventName, 129 | handler 130 | ); 131 | }); 132 | }); 133 | 134 | describe("MVCObject interface", () => { 135 | let overlay: ThreeJSOverlayView; 136 | let webglOverlay: google.maps.WebGLOverlayView; 137 | 138 | beforeEach(() => { 139 | overlay = new ThreeJSOverlayView(); 140 | webglOverlay = overlay["overlay"]; 141 | }); 142 | 143 | test.each([ 144 | ["bindTo", "eventName", () => void 0, "targetKey", true], 145 | ["get", "key"], 146 | ["notify", "key"], 147 | ["set", "key", "value"], 148 | ["setValues", { key: "value" }], 149 | ["unbind", "key"], 150 | ["unbindAll"], 151 | ] as const)( 152 | "method '%s' is forwarded to overlay", 153 | (method: keyof google.maps.MVCObject, ...args) => { 154 | overlay[method].call(overlay, ...args); 155 | expect(webglOverlay[method]).toHaveBeenCalledWith(...args); 156 | } 157 | ); 158 | }); 159 | 160 | describe("WebGLOverlayView interface", () => { 161 | let overlay: ThreeJSOverlayView; 162 | let gl: WebGLRenderingContext; 163 | let transformer: google.maps.CoordinateTransformer; 164 | const projMatrixArray = new Float64Array([ 165 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 166 | ]); 167 | 168 | beforeEach(() => { 169 | overlay = new ThreeJSOverlayView(); 170 | gl = createWebGlContext(); 171 | 172 | transformer = { 173 | fromLatLngAltitude: jest.fn(() => projMatrixArray), 174 | getCameraParams: jest.fn(), 175 | }; 176 | }); 177 | 178 | test("onContextRestored creates the renderer", () => { 179 | overlay.onContextRestored({ gl }); 180 | const renderer: WebGLRenderer = overlay["renderer"]; 181 | expect(renderer).toBeDefined(); 182 | 183 | const viewport = renderer.getViewport(new Vector4()); 184 | expect(viewport.x).toEqual(0); 185 | expect(viewport.y).toEqual(0); 186 | expect(viewport.width).toEqual(gl.canvas.width); 187 | expect(viewport.height).toEqual(gl.canvas.height); 188 | }); 189 | 190 | test("onDraw renders the scene and resets the state", () => { 191 | overlay.onContextRestored({ gl }); 192 | const renderer: WebGLRenderer = overlay["renderer"]; 193 | let scene: Object3D, camera: Camera; 194 | const renderSpy = jest 195 | .spyOn(renderer, "render") 196 | .mockImplementation((s, c) => { 197 | scene = s; 198 | camera = c; 199 | }); 200 | const resetStateSpy = jest.spyOn(renderer, "resetState"); 201 | 202 | overlay.onDraw({ gl, transformer }); 203 | 204 | expect(renderSpy).toHaveBeenCalled(); 205 | expect(scene).toBe(overlay.scene); 206 | expect(camera.projectionMatrix).toEqual( 207 | new Matrix4().fromArray(projMatrixArray) 208 | ); 209 | expect(resetStateSpy).toHaveBeenCalledAfter(renderSpy); 210 | }); 211 | 212 | test("onBeforeDraw gets called before render", () => { 213 | overlay.onContextRestored({ gl }); 214 | const renderer: WebGLRenderer = overlay["renderer"]; 215 | const renderSpy = jest 216 | .spyOn(renderer, "render") 217 | .mockImplementation(() => void 0); 218 | 219 | overlay.onBeforeDraw = jest.fn(); 220 | overlay.onDraw({ 221 | gl, 222 | transformer, 223 | }); 224 | 225 | expect(overlay.onBeforeDraw).toHaveBeenCalled(); 226 | expect(overlay.onBeforeDraw).toHaveBeenCalledBefore(renderSpy); 227 | }); 228 | 229 | test("onContextLost disposes of renderer", () => { 230 | overlay.onContextRestored({ gl }); 231 | 232 | const renderer: WebGLRenderer = overlay["renderer"]; 233 | const disposeSpy = jest.spyOn(renderer, "dispose"); 234 | overlay.onContextLost(); 235 | 236 | expect(disposeSpy).toHaveBeenCalled(); 237 | expect(overlay["renderer"]).toBeNull(); 238 | }); 239 | 240 | test("requestRedraw is forwarded to overlay", () => { 241 | overlay.requestRedraw(); 242 | 243 | expect(overlay["overlay"].requestRedraw).toHaveBeenCalledWith(); 244 | }); 245 | 246 | test("requestStateUpdate is forwarded to overlay", () => { 247 | overlay.requestStateUpdate(); 248 | 249 | expect(overlay["overlay"].requestStateUpdate).toHaveBeenCalledWith(); 250 | }); 251 | }); 252 | 253 | describe("setUpAxis() / scene orientation", () => { 254 | const latLngAlt = { lat: 0, lng: 0, altitude: 10 }; 255 | 256 | beforeEach(() => { 257 | const mockedUtil = util as jest.Mocked; 258 | mockedUtil.latLngToVector3Relative.mockImplementation( 259 | (p, r, target = new Vector3()) => { 260 | return target.set(1, 2, 3); 261 | } 262 | ); 263 | }); 264 | 265 | test.each([ 266 | [undefined, { x: 1, y: 2, z: 3 }], 267 | ["Z", { x: 1, y: 2, z: 3 }], 268 | ["Y", { x: 1, y: 3, z: -2 }], 269 | [new Vector3(1, 0, 0), { x: 3, y: 2, z: -1 }], 270 | ])("upAxis: %s", (upAxis, expectedCoords) => { 271 | const overlay = new ThreeJSOverlayView({ 272 | upAxis: upAxis as ThreeJSOverlayViewOptions["upAxis"], 273 | }); 274 | 275 | const v3 = overlay.latLngAltitudeToVector3(latLngAlt); 276 | expect(v3.x).toBeCloseTo(expectedCoords.x, 8); 277 | expect(v3.y).toBeCloseTo(expectedCoords.y, 8); 278 | expect(v3.z).toBeCloseTo(expectedCoords.z, 8); 279 | }); 280 | 281 | test("error for invalid upAxis values", () => { 282 | const mock = jest.spyOn(console, "warn").mockImplementation(() => void 0); 283 | const overlay = new ThreeJSOverlayView({ 284 | upAxis: "a" as ThreeJSOverlayViewOptions["upAxis"], 285 | }); 286 | 287 | expect(mock).toHaveBeenCalled(); 288 | 289 | // check that the default z-up is used 290 | const v3 = overlay.latLngAltitudeToVector3(latLngAlt); 291 | 292 | expect(v3.x).toBeCloseTo(1, 8); 293 | expect(v3.y).toBeCloseTo(2, 8); 294 | expect(v3.z).toBeCloseTo(3, 8); 295 | }); 296 | }); 297 | 298 | describe("latLngAltitudeToVector3()", () => { 299 | let mockedUtil: jest.Mocked; 300 | beforeEach(() => { 301 | mockedUtil = jest.mocked(util); 302 | const { latLngToVector3Relative } = mockedUtil; 303 | 304 | latLngToVector3Relative.mockImplementation( 305 | (p, r, target = new Vector3()) => { 306 | return target.set(1, 2, 3); 307 | } 308 | ); 309 | }); 310 | 311 | test("calls util-functions", () => { 312 | const overlay = new ThreeJSOverlayView({ 313 | anchor: { lat: 5, lng: 6, altitude: 7 }, 314 | }); 315 | const p = { lat: 0, lng: 0, altitude: 0 }; 316 | const v3 = overlay.latLngAltitudeToVector3(p); 317 | 318 | expect(mockedUtil.latLngToVector3Relative).toHaveBeenCalled(); 319 | expect(v3).toEqual(new Vector3(1, 2, 3)); 320 | }); 321 | 322 | test("writes value to target parameter", () => { 323 | const overlay = new ThreeJSOverlayView({ 324 | anchor: { lat: 5, lng: 6, altitude: 7 }, 325 | }); 326 | const p = { lat: 0, lng: 0, altitude: 0 }; 327 | const t = new Vector3(); 328 | const v3 = overlay.latLngAltitudeToVector3(p, t); 329 | 330 | expect(mockedUtil.latLngToVector3Relative).toHaveBeenCalled(); 331 | expect(v3).toBe(t); 332 | expect(t).toEqual(new Vector3(1, 2, 3)); 333 | }); 334 | }); 335 | 336 | describe("addDefaultLighting()", () => { 337 | test("lights are added to the default scene", () => { 338 | const overlay = new ThreeJSOverlayView(); 339 | 340 | const lights: Light[] = []; 341 | overlay.scene.traverse((o) => { 342 | if ((o as Light).isLight) lights.push(o as Light); 343 | }); 344 | 345 | expect(lights).not.toHaveLength(0); 346 | }); 347 | 348 | test("addDefaultLighting:false", () => { 349 | const overlay = new ThreeJSOverlayView({ addDefaultLighting: false }); 350 | 351 | const lights: Light[] = []; 352 | overlay.scene.traverse((o) => { 353 | if ((o as Light).isLight) lights.push(o as Light); 354 | }); 355 | 356 | expect(lights).toHaveLength(0); 357 | }); 358 | }); 359 | 360 | describe("raycast()", () => { 361 | let overlay: ThreeJSOverlayView; 362 | let camera: PerspectiveCamera; 363 | let box: Mesh; 364 | 365 | // these values were taken from a running application and are known to work 366 | const projMatrix = [ 367 | 0.024288994132302996, -0.0001544860884193919, -0.00004410021260124961, 368 | -0.00004410021260124961, 6.275603421503094e-20, 0.017096574772793482, 369 | -0.002943529080808796, -0.002943529080808796, -0.00028262805230606344, 370 | -0.01327650198026164, -0.0037899629741724055, -0.0037899629741724055, 371 | -0.10144748239547549, 0.2775102128618734, 0.4125525158446316, 372 | 1.079219172577191, 373 | ]; 374 | const boxPosition = new Vector3(0.12366377626911729, 0, 52.06138372088319); 375 | const mouseHitPosition = new Vector2(-0.131, -0.464); 376 | 377 | beforeEach(() => { 378 | overlay = new ThreeJSOverlayView(); 379 | 380 | // this could be done by providing a mocked CoordinateTransformer 381 | // to the onDraw function, but this is arguably easier (although 382 | // it's not ideal to access protected members) 383 | camera = overlay["camera"]; 384 | camera.projectionMatrix.fromArray(projMatrix); 385 | 386 | box = new Mesh(new BoxGeometry()); 387 | box.position.copy(boxPosition); 388 | }); 389 | 390 | test("returns an empty array for an empty scene", () => { 391 | const res = overlay.raycast(new Vector2(0, 0)); 392 | expect(res).toEqual([]); 393 | }); 394 | 395 | test("returns correct results in a known to work setting", () => { 396 | overlay.scene.add(box); 397 | box.updateMatrixWorld(true); 398 | 399 | // check for no hit at [0,0] 400 | expect(overlay.raycast(new Vector2(0, 0))).toEqual([]); 401 | 402 | let res; 403 | 404 | // we know where the box would be rendered 405 | res = overlay.raycast(mouseHitPosition); 406 | expect(res).toHaveLength(1); 407 | expect(res[0].object).toBe(box); 408 | 409 | // check that it ignores {recursive:false} here and returns the same result 410 | const res2 = overlay.raycast(mouseHitPosition, { recursive: false }); 411 | expect(res2).toEqual(res); 412 | 413 | // test calls with explicit object-list 414 | res = overlay.raycast(mouseHitPosition, [box], { recursive: false }); 415 | expect(res).toEqual(res); 416 | 417 | const box2 = new Mesh(new BoxGeometry()); 418 | res = overlay.raycast(mouseHitPosition, [box2]); 419 | expect(res).toEqual([]); 420 | 421 | // test recursion 422 | const g = new Group(); 423 | g.add(box); 424 | res = overlay.raycast(mouseHitPosition, [g], { recursive: false }); 425 | expect(res).toEqual([]); 426 | }); 427 | 428 | test("sets and restores raycaster parameters", () => { 429 | const raycaster = overlay["raycaster"]; 430 | 431 | const origParams = {} as unknown as RaycasterParameters; 432 | const customParams = {} as unknown as RaycasterParameters; 433 | 434 | let currParams = origParams; 435 | let intersectParams = null; 436 | 437 | const setParamsMock = jest.fn((v) => (currParams = v)); 438 | const getParamsMock = jest.fn(() => origParams); 439 | 440 | jest.spyOn(raycaster, "intersectObjects").mockImplementation(() => { 441 | intersectParams = currParams; 442 | return []; 443 | }); 444 | 445 | Object.defineProperty(raycaster, "params", { 446 | get: getParamsMock, 447 | set: setParamsMock, 448 | }); 449 | 450 | overlay.scene.add(box); 451 | box.updateMatrixWorld(true); 452 | 453 | overlay.raycast(mouseHitPosition, { raycasterParameters: customParams }); 454 | 455 | expect(setParamsMock).toHaveBeenCalledTimes(2); 456 | 457 | const [[arg1], [arg2]] = setParamsMock.mock.calls; 458 | expect(arg1).toBe(customParams); 459 | expect(arg2).toBe(origParams); 460 | expect(intersectParams).toBe(customParams); 461 | }); 462 | }); 463 | -------------------------------------------------------------------------------- /src/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { initialize } from "@googlemaps/jest-mocks"; 18 | import { 19 | latLngToXY, 20 | latLngToVector3Relative, 21 | toLatLngAltitudeLiteral, 22 | xyToLatLng, 23 | } from "../util"; 24 | 25 | beforeEach(() => { 26 | initialize(); 27 | }); 28 | 29 | describe("toLatLngAltitudeLiteral()", () => { 30 | test.each([ 31 | ["LatLngLiteral", { lat: 10, lng: 20 }, { lat: 10, lng: 20, altitude: 0 }], 32 | [ 33 | "LatLngAltitudeLiteral", 34 | { lat: 10, lng: 20, altitude: 30 }, 35 | { lat: 10, lng: 20, altitude: 30 }, 36 | ], 37 | ["LatLng", { lat: 10, lng: 20 }, { lat: 10, lng: 20, altitude: 0 }], 38 | [ 39 | "LatLngAltitude", 40 | { lat: 10, lng: 20, altitude: 30 }, 41 | { lat: 10, lng: 20, altitude: 30 }, 42 | ], 43 | ] as const)("toLatLngAltitudeLiteral: %p", (type, json, output) => { 44 | let input: Parameters[0] = json; 45 | 46 | if (type === "LatLng" || type === "LatLngAltitude") { 47 | input = new google.maps[type]({ lat: 0, lng: 0 }); 48 | (input as google.maps.LatLng).toJSON = jest.fn(() => json); 49 | } 50 | 51 | expect(toLatLngAltitudeLiteral(input)).toEqual(output); 52 | }); 53 | }); 54 | 55 | test.each([ 56 | [ 57 | { lng: 0, lat: 0 }, 58 | { x: 0, y: 0 }, 59 | ], 60 | [ 61 | { lng: -90, lat: 45 }, 62 | { x: -10007559.105973555, y: 5615239.936637378 }, 63 | ], 64 | [ 65 | { lng: 90, lat: -45 }, 66 | { x: 10007559.105973555, y: -5615239.936637378 }, 67 | ], 68 | [ 69 | { lng: 90, lat: 45 }, 70 | { x: 10007559.105973555, y: 5615239.936637378 }, 71 | ], 72 | [ 73 | { lng: -90, lat: -45 }, 74 | { x: -10007559.105973555, y: -5615239.936637378 }, 75 | ], 76 | [ 77 | { lng: 151.2093, lat: -33.8688 }, 78 | { x: 16813733.4125, y: -4006716.49009 }, 79 | ], 80 | ])( 81 | "latLngToXY and xyToLatLng are correct for %p", 82 | (latLng: google.maps.LatLngLiteral, expected: { x: number; y: number }) => { 83 | const [x, y] = latLngToXY(latLng); 84 | expect(x).toBeCloseTo(expected.x); 85 | expect(y).toBeCloseTo(expected.y); 86 | 87 | const { lat, lng } = xyToLatLng([x, y]); 88 | expect(lat).toBeCloseTo(latLng.lat); 89 | expect(lng).toBeCloseTo(latLng.lng); 90 | } 91 | ); 92 | 93 | test.each([ 94 | // 0 same 95 | { 96 | latLng: { lat: 0, lng: 0 }, 97 | reference: { lat: 0, lng: 0 }, 98 | relative: { x: 0, y: 0 }, 99 | }, 100 | // 1 northwest of reference 101 | { 102 | latLng: { lat: 0, lng: 0 }, 103 | reference: { lat: -1, lng: 1 }, 104 | relative: { 105 | x: -111178.17, 106 | y: 111183.81, 107 | }, 108 | }, 109 | // 2 northeast of reference 110 | { 111 | latLng: { lat: 0, lng: 2 }, 112 | reference: { lat: -1, lng: 1 }, 113 | relative: { 114 | x: 111178.17, 115 | y: 111183.81, 116 | }, 117 | }, 118 | // 3 southeast of reference 119 | { 120 | latLng: { lat: -2, lng: 2 }, 121 | reference: { lat: -1, lng: 1 }, 122 | relative: { 123 | x: 111178.17, 124 | y: -111217.69, 125 | }, 126 | }, 127 | // 4 southwest of reference 128 | { 129 | latLng: { lat: -2, lng: 0 }, 130 | reference: { lat: -1, lng: 1 }, 131 | relative: { 132 | x: -111178.17, 133 | y: -111217.69, 134 | }, 135 | }, 136 | { 137 | latLng: { lat: 48.861168, lng: 2.324197 }, 138 | reference: { lat: 48.862676, lng: 2.319095 }, 139 | relative: { 140 | x: 373.22, 141 | y: -167.68, 142 | }, 143 | }, 144 | ])( 145 | "latLngToVector3Relative is correct: %# %j", 146 | ({ latLng, reference, relative }) => { 147 | const vector = latLngToVector3Relative( 148 | { ...latLng, altitude: 0 }, 149 | { ...reference, altitude: 0 } 150 | ); 151 | expect(vector.x).toBeCloseTo(relative.x, 2); 152 | expect(vector.y).toBeCloseTo(relative.y, 2); 153 | expect(vector.z).toBeCloseTo(0, 2); 154 | } 155 | ); 156 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | export * from "./three"; 18 | export * from "./util"; 19 | -------------------------------------------------------------------------------- /src/three.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { 18 | DirectionalLight, 19 | Euler, 20 | HemisphereLight, 21 | Intersection, 22 | MathUtils, 23 | Matrix4, 24 | Object3D, 25 | PCFSoftShadowMap, 26 | PerspectiveCamera, 27 | Quaternion, 28 | Raycaster, 29 | RaycasterParameters, 30 | REVISION, 31 | Scene, 32 | Vector2, 33 | Vector3, 34 | WebGLRenderer, 35 | } from "three"; 36 | import { latLngToVector3Relative, toLatLngAltitudeLiteral } from "./util"; 37 | 38 | import type { LatLngTypes } from "./util"; 39 | 40 | // Since r162, the sRGBEncoding constant is no longer exported from three. 41 | // The value is kept here to keep compatibility with older three.js versions. 42 | // This will be removed with the next major release. 43 | const sRGBEncoding = 3001; 44 | 45 | const DEFAULT_UP = new Vector3(0, 0, 1); 46 | 47 | export interface RaycastOptions { 48 | /** 49 | * Set to true to also test children of the specified objects for 50 | * intersections. 51 | * 52 | * @default false 53 | */ 54 | recursive?: boolean; 55 | 56 | /** 57 | * Update the inverse-projection-matrix before casting the ray (set this 58 | * to false if you need to run multiple raycasts for the same frame). 59 | * 60 | * @default true 61 | */ 62 | updateMatrix?: boolean; 63 | 64 | /** 65 | * Additional parameters to pass to the three.js raycaster. 66 | * 67 | * @see https://threejs.org/docs/#api/en/core/Raycaster.params 68 | */ 69 | raycasterParameters?: RaycasterParameters; 70 | } 71 | 72 | export interface ThreeJSOverlayViewOptions { 73 | /** 74 | * The anchor for the scene. 75 | * 76 | * @default {lat: 0, lng: 0, altitude: 0} 77 | */ 78 | anchor?: LatLngTypes; 79 | 80 | /** 81 | * The axis pointing up in the scene. Can be specified as "Z", "Y" or a 82 | * Vector3, in which case the normalized vector will become the up-axis. 83 | * 84 | * @default "Z" 85 | */ 86 | upAxis?: "Z" | "Y" | Vector3; 87 | 88 | /** 89 | * The map the overlay will be added to. 90 | * Can be set at initialization or by calling `setMap(map)`. 91 | */ 92 | map?: google.maps.Map; 93 | 94 | /** 95 | * The scene object to render in the overlay. If no scene is specified, a 96 | * new scene is created and can be accessed via `overlay.scene`. 97 | */ 98 | scene?: Scene; 99 | 100 | /** 101 | * The animation mode controls when the overlay will redraw, either 102 | * continuously (`always`) or on demand (`ondemand`). When using the 103 | * on demand mode, the overlay will re-render whenever the map renders 104 | * (camera movements) or when `requestRedraw()` is called. 105 | * 106 | * To achieve animations in this mode, you can either use an outside 107 | * animation-loop that calls `requestRedraw()` as long as needed or call 108 | * `requestRedraw()` from within the `onBeforeRender` function to 109 | * 110 | * @default "ondemand" 111 | */ 112 | animationMode?: "always" | "ondemand"; 113 | 114 | /** 115 | * Add default lighting to the scene. 116 | * @default true 117 | */ 118 | addDefaultLighting?: boolean; 119 | } 120 | 121 | /* eslint-disable @typescript-eslint/no-empty-function */ 122 | 123 | /** 124 | * Add a [three.js](https://threejs.org) scene as a [Google Maps WebGLOverlayView](http://goo.gle/WebGLOverlayView-ref). 125 | */ 126 | export class ThreeJSOverlayView implements google.maps.WebGLOverlayView { 127 | /** {@inheritDoc ThreeJSOverlayViewOptions.scene} */ 128 | public readonly scene: Scene; 129 | 130 | /** {@inheritDoc ThreeJSOverlayViewOptions.animationMode} */ 131 | public animationMode: "always" | "ondemand" = "ondemand"; 132 | 133 | /** {@inheritDoc ThreeJSOverlayViewOptions.anchor} */ 134 | protected anchor: google.maps.LatLngAltitudeLiteral; 135 | protected readonly camera: PerspectiveCamera; 136 | protected readonly rotationArray: Float32Array = new Float32Array(3); 137 | protected readonly rotationInverse: Quaternion = new Quaternion(); 138 | protected readonly projectionMatrixInverse = new Matrix4(); 139 | 140 | protected readonly overlay: google.maps.WebGLOverlayView; 141 | protected renderer: WebGLRenderer; 142 | protected raycaster: Raycaster = new Raycaster(); 143 | 144 | constructor(options: ThreeJSOverlayViewOptions = {}) { 145 | const { 146 | anchor = { lat: 0, lng: 0, altitude: 0 }, 147 | upAxis = "Z", 148 | scene, 149 | map, 150 | animationMode = "ondemand", 151 | addDefaultLighting = true, 152 | } = options; 153 | 154 | this.overlay = new google.maps.WebGLOverlayView(); 155 | this.renderer = null; 156 | this.camera = null; 157 | this.animationMode = animationMode; 158 | 159 | this.setAnchor(anchor); 160 | this.setUpAxis(upAxis); 161 | 162 | this.scene = scene ?? new Scene(); 163 | if (addDefaultLighting) this.initSceneLights(); 164 | 165 | this.overlay.onAdd = this.onAdd.bind(this); 166 | this.overlay.onRemove = this.onRemove.bind(this); 167 | this.overlay.onContextLost = this.onContextLost.bind(this); 168 | this.overlay.onContextRestored = this.onContextRestored.bind(this); 169 | this.overlay.onStateUpdate = this.onStateUpdate.bind(this); 170 | this.overlay.onDraw = this.onDraw.bind(this); 171 | 172 | this.camera = new PerspectiveCamera(); 173 | 174 | if (map) { 175 | this.setMap(map); 176 | } 177 | } 178 | 179 | /** 180 | * Sets the anchor-point. 181 | * @param anchor 182 | */ 183 | public setAnchor(anchor: LatLngTypes) { 184 | this.anchor = toLatLngAltitudeLiteral(anchor); 185 | } 186 | 187 | /** 188 | * Sets the axis to use as "up" in the scene. 189 | * @param axis 190 | */ 191 | public setUpAxis(axis: "Y" | "Z" | Vector3): void { 192 | const upVector = new Vector3(0, 0, 1); 193 | if (typeof axis !== "string") { 194 | upVector.copy(axis); 195 | } else { 196 | if (axis.toLowerCase() === "y") { 197 | upVector.set(0, 1, 0); 198 | } else if (axis.toLowerCase() !== "z") { 199 | console.warn(`invalid value '${axis}' specified as upAxis`); 200 | } 201 | } 202 | 203 | upVector.normalize(); 204 | 205 | const q = new Quaternion(); 206 | q.setFromUnitVectors(upVector, DEFAULT_UP); 207 | 208 | // inverse rotation is needed in latLngAltitudeToVector3() 209 | this.rotationInverse.copy(q).invert(); 210 | 211 | // copy to rotationArray for transformer.fromLatLngAltitude() 212 | const euler = new Euler().setFromQuaternion(q, "XYZ"); 213 | this.rotationArray[0] = MathUtils.radToDeg(euler.x); 214 | this.rotationArray[1] = MathUtils.radToDeg(euler.y); 215 | this.rotationArray[2] = MathUtils.radToDeg(euler.z); 216 | } 217 | 218 | /** 219 | * Runs raycasting for the specified screen-coordinates against all objects 220 | * in the scene. 221 | * 222 | * @param p normalized screenspace coordinates of the 223 | * mouse-cursor. x/y are in range [-1, 1], y is pointing up. 224 | * @param options raycasting options. In this case the `recursive` option 225 | * has no effect as it is always recursive. 226 | * @return the list of intersections 227 | */ 228 | public raycast(p: Vector2, options?: RaycastOptions): Intersection[]; 229 | 230 | /** 231 | * Runs raycasting for the specified screen-coordinates against the specified 232 | * list of objects. 233 | * 234 | * Note for typescript users: the returned Intersection objects can only be 235 | * properly typed for non-recursive lookups (this is handled by the internal 236 | * signature below). 237 | * 238 | * @param p normalized screenspace coordinates of the 239 | * mouse-cursor. x/y are in range [-1, 1], y is pointing up. 240 | * @param objects list of objects to test 241 | * @param options raycasting options. 242 | */ 243 | public raycast( 244 | p: Vector2, 245 | objects: Object3D[], 246 | options?: RaycastOptions & { recursive: true } 247 | ): Intersection[]; 248 | 249 | // additional signature to enable typings in returned objects when possible 250 | public raycast( 251 | p: Vector2, 252 | objects: T[], 253 | options?: 254 | | Omit 255 | | (RaycastOptions & { recursive: false }) 256 | ): Intersection[]; 257 | 258 | // implemetation 259 | public raycast( 260 | p: Vector2, 261 | optionsOrObjects?: Object3D[] | RaycastOptions, 262 | options: RaycastOptions = {} 263 | ): Intersection[] { 264 | let objects: Object3D[]; 265 | if (Array.isArray(optionsOrObjects)) { 266 | objects = optionsOrObjects || null; 267 | } else { 268 | objects = [this.scene]; 269 | options = { ...optionsOrObjects, recursive: true }; 270 | } 271 | 272 | const { 273 | updateMatrix = true, 274 | recursive = false, 275 | raycasterParameters, 276 | } = options; 277 | 278 | // when `raycast()` is called from within the `onBeforeRender()` callback, 279 | // the mvp-matrix for this frame has already been computed and stored in 280 | // `this.camera.projectionMatrix`. 281 | // The mvp-matrix transforms world-space meters to clip-space 282 | // coordinates. The inverse matrix created here does the exact opposite 283 | // and converts clip-space coordinates to world-space. 284 | if (updateMatrix) { 285 | this.projectionMatrixInverse.copy(this.camera.projectionMatrix).invert(); 286 | } 287 | 288 | // create two points (with different depth) from the mouse-position and 289 | // convert them into world-space coordinates to set up the ray. 290 | this.raycaster.ray.origin 291 | .set(p.x, p.y, 0) 292 | .applyMatrix4(this.projectionMatrixInverse); 293 | 294 | this.raycaster.ray.direction 295 | .set(p.x, p.y, 0.5) 296 | .applyMatrix4(this.projectionMatrixInverse) 297 | .sub(this.raycaster.ray.origin) 298 | .normalize(); 299 | 300 | // back up the raycaster parameters 301 | const oldRaycasterParams = this.raycaster.params; 302 | if (raycasterParameters) { 303 | this.raycaster.params = raycasterParameters; 304 | } 305 | 306 | const results = this.raycaster.intersectObjects(objects, recursive); 307 | 308 | // reset raycaster params to whatever they were before 309 | this.raycaster.params = oldRaycasterParams; 310 | 311 | return results; 312 | } 313 | 314 | /** 315 | * Overwrite this method to handle any GL state updates outside the 316 | * render animation frame. 317 | * @param options 318 | */ 319 | public onStateUpdate(options: google.maps.WebGLStateOptions): void; 320 | public onStateUpdate(): void {} 321 | 322 | /** 323 | * Overwrite this method to fetch or create intermediate data structures 324 | * before the overlay is drawn that don’t require immediate access to the 325 | * WebGL rendering context. 326 | */ 327 | public onAdd(): void {} 328 | 329 | /** 330 | * Overwrite this method to update your scene just before a new frame is 331 | * drawn. 332 | */ 333 | public onBeforeDraw(): void {} 334 | 335 | /** 336 | * This method is called when the overlay is removed from the map with 337 | * `overlay.setMap(null)`, and is where you can remove all intermediate 338 | * objects created in onAdd. 339 | */ 340 | public onRemove(): void {} 341 | 342 | /** 343 | * Triggers the map to update GL state. 344 | */ 345 | public requestStateUpdate(): void { 346 | this.overlay.requestStateUpdate(); 347 | } 348 | 349 | /** 350 | * Triggers the map to redraw a frame. 351 | */ 352 | public requestRedraw(): void { 353 | this.overlay.requestRedraw(); 354 | } 355 | 356 | /** 357 | * Returns the map the overlay is added to. 358 | */ 359 | public getMap(): google.maps.Map { 360 | return this.overlay.getMap(); 361 | } 362 | 363 | /** 364 | * Adds the overlay to the map. 365 | * @param map The map to access the div, model and view state. 366 | */ 367 | public setMap(map: google.maps.Map): void { 368 | this.overlay.setMap(map); 369 | } 370 | 371 | /** 372 | * Adds the given listener function to the given event name. Returns an 373 | * identifier for this listener that can be used with 374 | * google.maps.event.removeListener. 375 | */ 376 | public addListener( 377 | eventName: string, 378 | handler: (...args: unknown[]) => void 379 | ): google.maps.MapsEventListener { 380 | return this.overlay.addListener(eventName, handler); 381 | } 382 | 383 | /** 384 | * This method is called once the rendering context is available. Use it to 385 | * initialize or bind any WebGL state such as shaders or buffer objects. 386 | * @param options that allow developers to restore the GL context. 387 | */ 388 | public onContextRestored({ gl }: google.maps.WebGLStateOptions) { 389 | this.renderer = new WebGLRenderer({ 390 | canvas: gl.canvas, 391 | context: gl, 392 | ...gl.getContextAttributes(), 393 | }); 394 | this.renderer.autoClear = false; 395 | this.renderer.autoClearDepth = false; 396 | this.renderer.shadowMap.enabled = true; 397 | this.renderer.shadowMap.type = PCFSoftShadowMap; 398 | 399 | // Since r152, default outputColorSpace is SRGB 400 | // Deprecated outputEncoding kept for backwards compatibility 401 | if (Number(REVISION) < 152) { 402 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 403 | (this.renderer as any).outputEncoding = sRGBEncoding; 404 | } 405 | 406 | const { width, height } = gl.canvas; 407 | this.renderer.setViewport(0, 0, width, height); 408 | } 409 | 410 | /** 411 | * This method is called when the rendering context is lost for any reason, 412 | * and is where you should clean up any pre-existing GL state, since it is 413 | * no longer needed. 414 | */ 415 | public onContextLost() { 416 | if (!this.renderer) { 417 | return; 418 | } 419 | 420 | this.renderer.dispose(); 421 | this.renderer = null; 422 | } 423 | 424 | /** 425 | * Implement this method to draw WebGL content directly on the map. Note 426 | * that if the overlay needs a new frame drawn then call {@link 427 | * ThreeJSOverlayView.requestRedraw}. 428 | * @param options that allow developers to render content to an associated 429 | * Google basemap. 430 | */ 431 | public onDraw({ gl, transformer }: google.maps.WebGLDrawOptions): void { 432 | this.camera.projectionMatrix.fromArray( 433 | transformer.fromLatLngAltitude(this.anchor, this.rotationArray) 434 | ); 435 | 436 | gl.disable(gl.SCISSOR_TEST); 437 | 438 | this.onBeforeDraw(); 439 | 440 | this.renderer.render(this.scene, this.camera); 441 | this.renderer.resetState(); 442 | 443 | if (this.animationMode === "always") this.requestRedraw(); 444 | } 445 | 446 | /** 447 | * Convert coordinates from WGS84 Latitude Longitude to world-space 448 | * coordinates while taking the origin and orientation into account. 449 | */ 450 | public latLngAltitudeToVector3( 451 | position: LatLngTypes, 452 | target = new Vector3() 453 | ) { 454 | latLngToVector3Relative( 455 | toLatLngAltitudeLiteral(position), 456 | this.anchor, 457 | target 458 | ); 459 | 460 | target.applyQuaternion(this.rotationInverse); 461 | 462 | return target; 463 | } 464 | 465 | // MVCObject interface forwarded to the overlay 466 | 467 | /** 468 | * Binds a View to a Model. 469 | */ 470 | public bindTo( 471 | key: string, 472 | target: google.maps.MVCObject, 473 | targetKey?: string, 474 | noNotify?: boolean 475 | ): void { 476 | this.overlay.bindTo(key, target, targetKey, noNotify); 477 | } 478 | 479 | /** 480 | * Gets a value. 481 | */ 482 | public get(key: string) { 483 | return this.overlay.get(key); 484 | } 485 | 486 | /** 487 | * Notify all observers of a change on this property. This notifies both 488 | * objects that are bound to the object's property as well as the object 489 | * that it is bound to. 490 | */ 491 | public notify(key: string): void { 492 | this.overlay.notify(key); 493 | } 494 | 495 | /** 496 | * Sets a value. 497 | */ 498 | public set(key: string, value: unknown): void { 499 | this.overlay.set(key, value); 500 | } 501 | 502 | /** 503 | * Sets a collection of key-value pairs. 504 | */ 505 | public setValues(values?: object): void { 506 | this.overlay.setValues(values); 507 | } 508 | 509 | /** 510 | * Removes a binding. Unbinding will set the unbound property to the current 511 | * value. The object will not be notified, as the value has not changed. 512 | */ 513 | public unbind(key: string): void { 514 | this.overlay.unbind(key); 515 | } 516 | 517 | /** 518 | * Removes all bindings. 519 | */ 520 | public unbindAll(): void { 521 | this.overlay.unbindAll(); 522 | } 523 | 524 | /** 525 | * Creates lights (directional and hemisphere light) to illuminate the model 526 | * (roughly approximates the lighting of buildings in maps) 527 | */ 528 | private initSceneLights() { 529 | const hemiLight = new HemisphereLight(0xffffff, 0x444444, 1); 530 | hemiLight.position.set(0, -0.2, 1).normalize(); 531 | 532 | const dirLight = new DirectionalLight(0xffffff); 533 | dirLight.position.set(0, 10, 100); 534 | 535 | this.scene.add(hemiLight, dirLight); 536 | } 537 | } 538 | -------------------------------------------------------------------------------- /src/util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import { MathUtils, Vector3 } from "three"; 18 | 19 | export type LatLngTypes = 20 | | google.maps.LatLngLiteral 21 | | google.maps.LatLng 22 | | google.maps.LatLngAltitudeLiteral 23 | | google.maps.LatLngAltitude; 24 | 25 | // shorthands for math-functions, makes equations more readable 26 | const { atan, cos, exp, log, tan, PI } = Math; 27 | const { degToRad, radToDeg } = MathUtils; 28 | 29 | export const EARTH_RADIUS = 6371010.0; 30 | export const WORLD_SIZE = Math.PI * EARTH_RADIUS; 31 | 32 | /** 33 | * Converts any of the supported position formats into the 34 | * google.maps.LatLngAltitudeLiteral format used for the calculations. 35 | * @param point 36 | */ 37 | export function toLatLngAltitudeLiteral( 38 | point: LatLngTypes 39 | ): google.maps.LatLngAltitudeLiteral { 40 | if ( 41 | window.google && 42 | google.maps && 43 | (point instanceof google.maps.LatLng || 44 | point instanceof google.maps.LatLngAltitude) 45 | ) { 46 | return { altitude: 0, ...point.toJSON() }; 47 | } 48 | 49 | return { altitude: 0, ...(point as google.maps.LatLngLiteral) }; 50 | } 51 | 52 | /** 53 | * Converts latitude and longitude to world space coordinates relative 54 | * to a reference location with y up. 55 | */ 56 | export function latLngToVector3Relative( 57 | point: google.maps.LatLngAltitudeLiteral, 58 | reference: google.maps.LatLngAltitudeLiteral, 59 | target = new Vector3() 60 | ) { 61 | const [px, py] = latLngToXY(point); 62 | const [rx, ry] = latLngToXY(reference); 63 | 64 | target.set(px - rx, py - ry, 0); 65 | 66 | // apply the spherical mercator scale-factor for the reference latitude 67 | target.multiplyScalar(cos(degToRad(reference.lat))); 68 | 69 | target.z = point.altitude - reference.altitude; 70 | 71 | return target; 72 | } 73 | 74 | /** 75 | * Converts WGS84 latitude and longitude to (uncorrected) WebMercator meters. 76 | * (WGS84 --> WebMercator (EPSG:3857)) 77 | */ 78 | export function latLngToXY(position: google.maps.LatLngLiteral): number[] { 79 | return [ 80 | EARTH_RADIUS * degToRad(position.lng), 81 | EARTH_RADIUS * log(tan(0.25 * PI + 0.5 * degToRad(position.lat))), 82 | ]; 83 | } 84 | 85 | /** 86 | * Converts WebMercator meters to WGS84 latitude/longitude. 87 | * (WebMercator (EPSG:3857) --> WGS84) 88 | */ 89 | export function xyToLatLng(p: number[]): google.maps.LatLngLiteral { 90 | const [x, y] = p; 91 | 92 | return { 93 | lat: radToDeg(PI * 0.5 - 2.0 * atan(exp(-y / EARTH_RADIUS))), 94 | lng: radToDeg(x) / EARTH_RADIUS, 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /tsconfig.examples.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "noEmit": true, 6 | "outDir": null, 7 | "declarationDir": null, 8 | "noImplicitAny": false, 9 | "resolveJsonModule": true 10 | }, 11 | "include": ["src/**/*", "examples/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "./dist", 5 | "noImplicitAny": true, 6 | "outDir": "./dist", 7 | "sourceMap": true, 8 | "esModuleInterop": true, 9 | "lib": ["DOM", "ESNext", "ES2019"], 10 | "target": "ES2020", 11 | "module": "ES2020", 12 | "moduleResolution": "node", 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "./dist"] 18 | } 19 | -------------------------------------------------------------------------------- /typedoc.cjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2021 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | module.exports = { 18 | out: "docs", 19 | exclude: ["**/node_modules/**", "**/*.spec.ts", "**/*.test.ts"], 20 | name: "@googlemaps/three", 21 | excludePrivate: true, 22 | media: "assets", 23 | }; 24 | --------------------------------------------------------------------------------