├── .github └── workflows │ ├── compilation-check.yml │ └── publish.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── maps-build-logic ├── build.gradle.kts └── src │ └── main │ └── kotlin │ ├── android-app-convention.gradle.kts │ ├── android-base-convention.gradle.kts │ ├── android-library-convention.gradle.kts │ ├── android-publication-convention.gradle.kts │ ├── detekt-convention.gradle.kts │ ├── javadoc-stub-convention.gradle.kts │ ├── multiplatform-library-convention.gradle.kts │ └── publication-convention.gradle.kts ├── maps-google ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── google │ │ ├── GoogleMapController.kt │ │ ├── GoogleMarker.kt │ │ ├── GooglePolygon.kt │ │ ├── GoogleRoute.kt │ │ └── Utils.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── google │ │ ├── ColorUtils.kt │ │ ├── GoogleMapController.kt │ │ ├── GoogleMarker.kt │ │ ├── GoogleRoute.kt │ │ └── UiSettings.kt │ ├── iosArm64Main │ ├── iosMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── google │ │ ├── Async.kt │ │ ├── GDirection.kt │ │ └── Utils.kt │ └── iosX64Main │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── maps │ └── google │ ├── GoogleMapController.kt │ ├── GoogleMarker.kt │ ├── GooglePolygon.kt │ └── GoogleRoute.kt ├── maps-mapbox ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── mapbox │ │ ├── MapboxController.kt │ │ ├── MapboxLine.kt │ │ ├── MapboxMarker.kt │ │ ├── MapboxPolygon.kt │ │ ├── MapboxRoute.kt │ │ ├── MapboxStyleElement.kt │ │ └── Utils.kt │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── mapbox │ │ ├── MapboxController.kt │ │ ├── MapboxMarker.kt │ │ ├── MapboxPolygon.kt │ │ ├── MapboxRoute.kt │ │ └── UiSettings.kt │ ├── iosArm64Main │ ├── iosMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ └── mapbox │ │ ├── MapboxPolygon.kt │ │ ├── MapboxPolygonSettings.kt │ │ ├── MapboxRoute.kt │ │ └── Utils.kt │ └── iosX64Main │ └── kotlin │ └── dev │ └── icerock │ └── moko │ └── maps │ └── mapbox │ ├── MapboxAnnotation.kt │ ├── MapboxController.kt │ └── MapboxMarker.kt ├── maps ├── build.gradle.kts └── src │ ├── androidMain │ └── AndroidManifest.xml │ ├── commonMain │ └── kotlin │ │ └── dev │ │ └── icerock │ │ └── moko │ │ └── maps │ │ ├── LineType.kt │ │ ├── MapAddress.kt │ │ ├── MapController.kt │ │ ├── MapControllerExt.kt │ │ ├── MapElement.kt │ │ ├── Marker.kt │ │ ├── OnMapListener.kt │ │ └── ZoomConfig.kt │ └── iosMain │ └── kotlin │ └── Dummy.kt ├── sample ├── android-app │ ├── build.gradle.kts │ ├── proguard-rules.pro │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── icerockdev │ │ │ └── app │ │ │ ├── GoogleMapsActivity.kt │ │ │ ├── MainActivity.kt │ │ │ └── MapboxActivity.kt │ │ └── res │ │ └── layout │ │ ├── activity_google_maps.xml │ │ ├── activity_main.xml │ │ └── activity_mapbox.xml ├── gradlew ├── ios-app │ ├── Podfile │ ├── Podfile.lock │ ├── TestProj.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ ├── TestProj.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── src │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ └── marker.imageset │ │ │ ├── Contents.json │ │ │ ├── office_marker.png │ │ │ ├── office_marker@2x.png │ │ │ └── office_marker@3x.png │ │ ├── GoogleMapViewController.swift │ │ ├── Info.plist │ │ ├── MapboxViewController.swift │ │ └── Resources │ │ └── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard └── mpp-library │ ├── MultiPlatformLibrary.podspec │ ├── build.gradle.kts │ └── src │ ├── androidMain │ └── AndroidManifest.xml │ └── commonMain │ ├── kotlin │ └── com │ │ └── icerockdev │ │ └── library │ │ ├── GoogleMapViewModel.kt │ │ └── MapboxViewModel.kt │ └── resources │ └── MR │ └── images │ ├── marker@1x.png │ ├── marker@2x.png │ └── marker@3x.png └── settings.gradle.kts /.github/workflows/compilation-check.yml: -------------------------------------------------------------------------------- 1 | name: KMP library compilation check 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - develop 8 | 9 | jobs: 10 | build: 11 | runs-on: macOS-latest 12 | env: 13 | MAPBOX_SECRET_TOKEN: ${{ secrets.MAPBOX_SECRET_TOKEN }} 14 | MAPBOX_PUBLIC_TOKEN: ${{ secrets.MAPBOX_PUBLIC_TOKEN }} 15 | GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} 16 | steps: 17 | - uses: actions/checkout@v1 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 11 22 | - name: Set up netrc 23 | uses: extractions/netrc@v1 24 | with: 25 | machine: api.mapbox.com 26 | username: mapbox 27 | password: ${{ secrets.MAPBOX_SECRET_TOKEN }} 28 | - name: Cocoapods install 29 | run: (cd sample/ios-app && pod install) 30 | - name: Check library 31 | run: ./gradlew build publishToMavenLocal syncMultiPlatformLibraryDebugFrameworkIosX64 32 | - name: Install pods with kotlin 33 | run: cd sample/ios-app && pod install 34 | - name: build ios sample 35 | run: cd sample/ios-app && set -o pipefail && xcodebuild -scheme TestProj -workspace TestProj.xcworkspace -configuration Debug -sdk iphonesimulator -arch x86_64 build CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcpretty 36 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Create release 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | description: 'Version' 8 | default: '0.1.0' 9 | required: true 10 | 11 | jobs: 12 | publish: 13 | name: Publish library at mavenCentral 14 | runs-on: macOS-latest 15 | env: 16 | OSSRH_USER: ${{ secrets.OSSRH_USER }} 17 | OSSRH_KEY: ${{ secrets.OSSRH_KEY }} 18 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEYID }} 19 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 20 | SIGNING_KEY: ${{ secrets.GPG_KEY_CONTENTS }} 21 | MAPBOX_SECRET_TOKEN: ${{ secrets.MAPBOX_SECRET_TOKEN }} 22 | MAPBOX_PUBLIC_TOKEN: ${{ secrets.MAPBOX_PUBLIC_TOKEN }} 23 | GOOGLE_MAPS_API_KEY: ${{ secrets.GOOGLE_MAPS_API_KEY }} 24 | steps: 25 | - uses: actions/checkout@v1 26 | - name: Set up JDK 11 27 | uses: actions/setup-java@v1 28 | with: 29 | java-version: 11 30 | - name: Set up netrc 31 | uses: extractions/netrc@v1 32 | with: 33 | machine: api.mapbox.com 34 | username: mapbox 35 | password: ${{ secrets.MAPBOX_SECRET_TOKEN }} 36 | - name: Cocoapods install 37 | run: (cd sample/ios-app && pod install) 38 | - name: Publish library 39 | run: ./gradlew publish 40 | release: 41 | name: Create release 42 | needs: publish 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Create Release 46 | id: create_release 47 | uses: actions/create-release@v1 48 | env: 49 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 50 | with: 51 | commitish: ${{ github.ref }} 52 | tag_name: release/${{ github.event.inputs.version }} 53 | release_name: ${{ github.event.inputs.version }} 54 | body: "Will be filled later" 55 | draft: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .settings 3 | .project 4 | .classpath 5 | .vscode 6 | .idea 7 | build 8 | *.iml 9 | Pods 10 | xcuserdata 11 | local.properties 12 | local.gradle -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Do’s and Don’ts 2 | 3 | * **Search tickets before you file a new one.** Add to tickets if you have new information about the issue. 4 | * **Keep tickets short but sweet.** Make sure you include all the context needed to solve the issue. Don't overdo it. Great tickets allow us to focus on solving problems instead of discussing them. 5 | * **Take care of your ticket.** When you spend time to report a ticket with care we'll enjoy fixing it for you. 6 | * **Use [GitHub-flavored Markdown](https://help.github.com/articles/markdown-basics/).** Especially put code blocks and console outputs in backticks (```` ``` ````). That increases the readability. Bonus points for applying the appropriate syntax highlighting. 7 | 8 | ## Bug Reports 9 | 10 | In short, since you are most likely a developer, provide a ticket that you _yourself_ would _like_ to receive. 11 | 12 | First check if you are using the latest library version and Kotlin version before filing a ticket. 13 | 14 | Please include steps to reproduce and _all_ other relevant information, including any other relevant dependency and version information. 15 | 16 | ## Feature Requests 17 | 18 | Please try to be precise about the proposed outcome of the feature and how it 19 | would related to existing features. 20 | 21 | 22 | ## Pull Requests 23 | 24 | We **love** pull requests! 25 | 26 | All contributions _will_ be licensed under the Apache 2 license. 27 | 28 | Code/comments should adhere to the following rules: 29 | 30 | * Names should be descriptive and concise. 31 | * Use four spaces and no tabs. 32 | * Remember that source code usually gets written once and read often: ensure 33 | the reader doesn't have to make guesses. Make sure that the purpose and inner 34 | logic are either obvious to a reasonably skilled professional, or add a 35 | comment that explains it. 36 | * Please add a detailed description. 37 | 38 | If you consistently contribute improvements and/or bug fixes, we're happy to make you a maintainer. -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![moko-maps](https://user-images.githubusercontent.com/5010169/71351401-27c14d80-25a6-11ea-9183-17821f6d4212.png) 2 | [![GitHub license](https://img.shields.io/badge/license-Apache%20License%202.0-blue.svg?style=flat)](http://www.apache.org/licenses/LICENSE-2.0) [![Download](https://img.shields.io/maven-central/v/dev.icerock.moko/maps) ](https://repo1.maven.org/maven2/dev/icerock/moko/maps) ![kotlin-version](https://kotlin-version.aws.icerock.dev/kotlin-version?group=dev.icerock.moko&name=maps) 3 | 4 | # Mobile Kotlin maps module 5 | This is a Kotlin Multiplatform library that provides controls of maps to common code. 6 | 7 | ## Table of Contents 8 | - [Features](#features) 9 | - [Requirements](#requirements) 10 | - [Installation](#installation) 11 | - [Usage](#usage) 12 | - [Samples](#samples) 13 | - [Set Up Locally](#set-up-locally) 14 | - [Contributing](#contributing) 15 | - [License](#license) 16 | 17 | ## Features 18 | - **Markers** - add markers to map from common code; 19 | - **Route** - draw route by waypoints from common code; 20 | - **Camera** - control camera (zoom, location) from common code. 21 | 22 | ## Requirements 23 | - Gradle version 6.0+ 24 | - Android API 16+ 25 | - iOS version 9.0+ 26 | 27 | ## Installation 28 | root build.gradle 29 | ```groovy 30 | allprojects { 31 | repositories { 32 | mavenCentral() 33 | maven { url = "https://mapbox.bintray.com/mapbox" } // if mapbox required 34 | } 35 | } 36 | ``` 37 | 38 | project build.gradle 39 | ```groovy 40 | dependencies { 41 | commonMainApi("dev.icerock.moko:maps:0.6.0") 42 | commonMainApi("dev.icerock.moko:maps-google:0.6.0") 43 | commonMainApi("dev.icerock.moko:maps-mapbox:0.6.0") 44 | } 45 | 46 | kotlin.targets 47 | .matching { it is org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget } 48 | .configureEach { 49 | val target = this as org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget 50 | 51 | target.binaries 52 | .matching { it is org.jetbrains.kotlin.gradle.plugin.mpp.Framework } 53 | .configureEach { 54 | val framework = this as org.jetbrains.kotlin.gradle.plugin.mpp.Framework 55 | val frameworks = listOf("Base", "Maps").map { frameworkPath -> 56 | project.file("../ios-app/Pods/GoogleMaps/$frameworkPath/Frameworks").path.let { "-F$it" } 57 | }.plus( 58 | project.file("../ios-app/Pods/Mapbox-iOS-SDK/dynamic").path.let { "-F$it" } 59 | ) 60 | 61 | framework.linkerOpts(frameworks) 62 | } 63 | } 64 | ``` 65 | 66 | With [mobile-multiplatform-gradle-plugin](https://github.com/icerockdev/mobile-multiplatform-gradle-plugin) cocoapods configuration simplest: 67 | `build.gradle.kts`: 68 | ```kotlin 69 | cocoaPods { 70 | podsProject = file("ios-app/Pods/Pods.xcodeproj") 71 | 72 | precompiledPod( 73 | scheme = "GoogleMaps", 74 | onlyLink = true 75 | ) { podsDir -> 76 | listOf( 77 | File(podsDir, "GoogleMaps/Base/Frameworks"), 78 | File(podsDir, "GoogleMaps/Maps/Frameworks") 79 | ) 80 | } 81 | precompiledPod( 82 | scheme = "Mapbox", 83 | onlyLink = true 84 | ) { podsDir -> 85 | listOf(File(podsDir, "Mapbox-iOS-SDK/dynamic")) 86 | } 87 | } 88 | ``` 89 | 90 | project Podfile 91 | ```ruby 92 | pod 'GoogleMaps', '3.7.0' 93 | pod 'Mapbox-iOS-SDK', '5.5.0' 94 | 95 | # GoogleMaps is static library that already linked in moko-maps-google. Remove duplicated linking. 96 | post_install do |installer| 97 | host_targets = installer.aggregate_targets.select { |aggregate_target| 98 | aggregate_target.name.include? "Pods-" 99 | } 100 | 101 | host_targets.each do |host_target| 102 | host_target.xcconfigs.each do |config_name, config_file| 103 | config_file.frameworks.delete("GoogleMaps") 104 | config_file.frameworks.delete("GoogleMapsBase") 105 | config_file.frameworks.delete("GoogleMapsCore") 106 | 107 | xcconfig_path = host_target.xcconfig_path(config_name) 108 | config_file.save_as(xcconfig_path) 109 | end 110 | end 111 | end 112 | ``` 113 | 114 | ## Usage 115 | ### Markers 116 | ```kotlin 117 | class MarkerViewModel( 118 | val mapsController: GoogleMapController 119 | ) : ViewModel() { 120 | 121 | fun start() { 122 | viewModelScope.launch { 123 | val marker1 = mapsController.addMarker( 124 | image = MR.images.marker, 125 | latLng = LatLng( 126 | latitude = 55.045853, 127 | longitude = 82.920154 128 | ), 129 | rotation = 0.0f 130 | ) { 131 | println("marker 1 pressed!") 132 | } 133 | 134 | marker1.rotation = 90.0f 135 | } 136 | } 137 | } 138 | ``` 139 | ### Route 140 | ```kotlin 141 | class MarkerViewModel( 142 | val mapsController: GoogleMapController 143 | ) : ViewModel() { 144 | 145 | fun start() { 146 | viewModelScope.launch { 147 | val route = mapsController.buildRoute( 148 | points = listOf( 149 | LatLng( 150 | latitude = 55.032200, 151 | longitude = 82.889360 152 | ), 153 | LatLng( 154 | latitude = 55.030853, 155 | longitude = 82.920154 156 | ), 157 | LatLng( 158 | latitude = 55.013109, 159 | longitude = 82.926480 160 | ) 161 | ), 162 | lineColor = Color(0xCCCC00FF), 163 | markersImage = MR.images.marker 164 | ) 165 | } 166 | } 167 | } 168 | ``` 169 | 170 | ## Samples 171 | Please see more examples in the [sample directory](sample). 172 | 173 | ## Set Up Locally 174 | Before open project need to setup `gradle.properties` with tokens: 175 | ``` 176 | # mapbox tokens by guide https://docs.mapbox.com/android/maps/guides/install/ 177 | mapbox.secretToken=YOUR_SECRET_MAPBOX_KEY 178 | mapbox.publicToken=YOUR_PUBLIC_MAPBOX_KEY 179 | 180 | # google maps api key by guide https://developers.google.com/maps/documentation/android-sdk/get-api-key 181 | googleMaps.apiKey=YOUR_API_KEY 182 | ``` 183 | 184 | # ios info.plist setup with tokens: 185 | ``` 186 | MGLMapboxAccessToken=YOUR_PUBLIC_MAPBOX_KEY 187 | GoogleAPIkey=YOUR_API_KEY 188 | ``` 189 | add the following entry to your `.netrc` file: 190 | ``` 191 | machine api.mapbox.com 192 | login mapbox 193 | password YOUR_SECRET_MAPBOX_KEY 194 | ``` 195 | 196 | - The [maps directory](maps) contains the base classes for all maps providers; 197 | - The [maps-google directory](maps-google) contains the Google Maps implementation; 198 | - The [maps-mapbox directory](maps-mapbox) contains the mapbox implementation; 199 | - In [sample directory](sample) contains sample apps for Android and iOS; plus the mpp-library connected to the apps. 200 | 201 | *before compilation of iOS target required `pod install` in `sample/ios-app` directory* 202 | 203 | ## Contributing 204 | All development (both new features and bug fixes) is performed in the `develop` branch. This way `master` always contains the sources of the most recently released version. Please send PRs with bug fixes to the `develop` branch. Documentation fixes in the markdown files are an exception to this rule. They are updated directly in `master`. 205 | 206 | The `develop` branch is pushed to `master` on release. 207 | 208 | For more details on contributing please see the [contributing guide](CONTRIBUTING.md). 209 | 210 | ## License 211 | 212 | Copyright 2019 IceRock MAG Inc. 213 | 214 | Licensed under the Apache License, Version 2.0 (the "License"); 215 | you may not use this file except in compliance with the License. 216 | You may obtain a copy of the License at 217 | 218 | http://www.apache.org/licenses/LICENSE-2.0 219 | 220 | Unless required by applicable law or agreed to in writing, software 221 | distributed under the License is distributed on an "AS IS" BASIS, 222 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 223 | See the License for the specific language governing permissions and 224 | limitations under the License. 225 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | import org.gradle.api.internal.artifacts.DefaultModuleVersionSelector 5 | 6 | buildscript { 7 | repositories { 8 | mavenCentral() 9 | google() 10 | gradlePluginPortal() 11 | } 12 | dependencies { 13 | classpath(":maps-build-logic") 14 | classpath("dev.icerock.moko:resources-generator:0.16.1") 15 | classpath("org.jetbrains.kotlin:kotlin-serialization:1.5.20") 16 | } 17 | } 18 | 19 | allprojects { 20 | plugins.withId("org.gradle.maven-publish") { 21 | group = "dev.icerock.moko" 22 | version = libs.versions.mokoMapsVersion.get() 23 | } 24 | configurations.configureEach { 25 | resolutionStrategy { 26 | val coroutines: MinimalExternalModuleDependency = rootProject.libs.coroutines.get() 27 | val forcedCoroutines: ModuleVersionSelector = DefaultModuleVersionSelector.newSelector( 28 | coroutines.module, 29 | coroutines.versionConstraint.requiredVersion 30 | ) 31 | force(forcedCoroutines) 32 | } 33 | } 34 | } 35 | 36 | tasks.register("clean", Delete::class).configure { 37 | delete(rootProject.buildDir) 38 | } 39 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096m 2 | org.gradle.configureondemand=false 3 | org.gradle.parallel=true 4 | 5 | kotlin.code.style=official 6 | kotlin.native.enableDependencyPropagation=false 7 | kotlin.mpp.enableGranularSourceSetsMetadata=true 8 | kotlin.mpp.enableCompatibilityMetadataVariant=true 9 | 10 | android.useAndroidX=true 11 | android.enableJetifier=true 12 | 13 | xcodeproj=./sample/ios-app 14 | 15 | mobile.multiplatform.iosTargetWarning=false 16 | mobile.multiplatform.podsProject=sample/ios-app/Pods/Pods.xcodeproj 17 | kotlin.mpp.stability.nowarn=true 18 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlinVersion = "1.5.20" 3 | lifecycleVersion = "2.2.0" 4 | androidAppCompatVersion = "1.2.0" 5 | espressoCoreVersion = "3.2.0" 6 | testRunnerVersion = "1.2.0" 7 | testExtJunitVersion = "1.1.1" 8 | playServicesLocationVersion = "18.0.0" 9 | playServicesMapsVersion = "17.0.0" 10 | googleMapsServicesVersion = "0.2.11" 11 | mapboxVersion = "9.6.1" 12 | mapboxNavigationVersion = "1.5.1" 13 | mapboxAnnotationVersion = "0.9.0" 14 | mapboxServicesVersion = "5.8.0" 15 | multidexVersion = "2.0.1" 16 | kotlinxSerializationVersion = "1.2.1" 17 | coroutinesVersion = "1.5.0-native-mt" 18 | ktorClientVersion = "1.6.0" 19 | mokoGraphicsVersion = "0.7.0" 20 | mokoParcelizeVersion = "0.7.1" 21 | mokoResourcesVersion = "0.16.1" 22 | mokoMvvmVersion = "0.11.0" 23 | mokoGeoVersion = "0.4.0" 24 | mokoPermissionsVersion = "0.10.1" 25 | mokoMapsVersion = "0.6.0" 26 | 27 | [libraries] 28 | appCompat = { module = "androidx.appcompat:appcompat", version.ref = "androidAppCompatVersion" } 29 | lifecycle = { module = "androidx.lifecycle:lifecycle-extensions", version.ref = "lifecycleVersion" } 30 | playServicesLocation = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocationVersion" } 31 | playServicesMaps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMapsVersion" } 32 | googleMapsServices = { module = "com.google.maps:google-maps-services", version.ref = "googleMapsServicesVersion" } 33 | mapbox = { module = "com.mapbox.mapboxsdk:mapbox-android-sdk", version.ref = "mapboxVersion" } 34 | mapboxNavigation = { module = "com.mapbox.navigation:core", version.ref = "mapboxNavigationVersion" } 35 | mapboxAnnotation = { module = "com.mapbox.mapboxsdk:mapbox-android-plugin-annotation-v9", version.ref = "mapboxAnnotationVersion" } 36 | mapboxServices = { module = "com.mapbox.mapboxsdk:mapbox-sdk-services", version.ref = "mapboxServicesVersion" } 37 | multidex = { module = "androidx.multidex:multidex", version.ref = "multidexVersion" } 38 | ktorClientOkHttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorClientVersion" } 39 | kotlinSerialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationVersion" } 40 | coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } 41 | ktorClient = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientVersion" } 42 | mokoResources = { module = "dev.icerock.moko:resources", version.ref = "mokoResourcesVersion" } 43 | mokoParcelize = { module = "dev.icerock.moko:parcelize", version.ref = "mokoParcelizeVersion" } 44 | mokoGraphics = { module = "dev.icerock.moko:graphics", version.ref = "mokoGraphicsVersion" } 45 | mokoMvvm = { module = "dev.icerock.moko:mvvm", version.ref = "mokoMvvmVersion" } 46 | mokoGeo = { module = "dev.icerock.moko:geo", version.ref = "mokoGeoVersion" } 47 | mokoPermissions = { module = "dev.icerock.moko:permissions", version.ref = "mokoPermissionsVersion" } 48 | mokoMaps = { module = "dev.icerock.moko:maps", version.ref = "mokoMapsVersion" } 49 | mokoMapsGoogle = { module = "dev.icerock.moko:maps-google", version.ref = "mokoMapsVersion" } 50 | mokoMapsMapbox = { module = "dev.icerock.moko:maps-mapbox", version.ref = "mokoMapsVersion" } 51 | kotlinTest = { module = "org.jetbrains.kotlin:kotlin-test-common", version.ref = "kotlinVersion" } 52 | ktorClientIos = { module = "io.ktor:ktor-client-ios", version.ref = "ktorClientVersion" } 53 | 54 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icerockdev/moko-maps/b87a7d88c38657de1b94d372a531890776233a1a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /maps-build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | mavenCentral() 7 | google() 8 | 9 | gradlePluginPortal() 10 | } 11 | 12 | dependencies { 13 | api("dev.icerock:mobile-multiplatform:0.12.0") 14 | api("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.20") 15 | api("com.android.tools.build:gradle:4.2.1") 16 | api("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.15.0") 17 | } 18 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/android-app-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("com.android.application") 7 | id("android-base-convention") 8 | id("kotlin-android") 9 | } 10 | 11 | android { 12 | dexOptions { 13 | javaMaxHeapSize = "2g" 14 | } 15 | 16 | buildTypes { 17 | getByName("release") { 18 | isMinifyEnabled = true 19 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro") 20 | } 21 | getByName("debug") { 22 | isDebuggable = true 23 | applicationIdSuffix = ".debug" 24 | } 25 | } 26 | 27 | packagingOptions { 28 | exclude("META-INF/*.kotlin_module") 29 | exclude("META-INF/AL2.0") 30 | exclude("META-INF/LGPL2.1") 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/android-base-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import com.android.build.gradle.BaseExtension 6 | 7 | configure { 8 | compileSdkVersion(30) 9 | 10 | defaultConfig { 11 | minSdkVersion(21) 12 | targetSdkVersion(30) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/android-library-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("com.android.library") 7 | id("kotlin-android") 8 | id("android-base-convention") 9 | } 10 | 11 | android { 12 | sourceSets.all { java.srcDir("src/$name/kotlin") } 13 | } 14 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/android-publication-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("publication-convention") 7 | } 8 | 9 | afterEvaluate { 10 | publishing.publications { 11 | create("release", MavenPublication::class.java) { 12 | from(components.getByName("release")) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/detekt-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("io.gitlab.arturbosch.detekt") 7 | } 8 | 9 | detekt { 10 | input.setFrom("src/commonMain/kotlin", "src/androidMain/kotlin", "src/iosMain/kotlin", "src/main/kotlin") 11 | } 12 | 13 | dependencies { 14 | "detektPlugins"("io.gitlab.arturbosch.detekt:detekt-formatting:1.15.0") 15 | } 16 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/javadoc-stub-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("org.gradle.maven-publish") 7 | } 8 | 9 | val javadocJar by tasks.registering(Jar::class) { 10 | archiveClassifier.set("javadoc") 11 | } 12 | 13 | publishing.publications.withType { 14 | // Stub javadoc.jar artifact 15 | artifact(javadocJar.get()) 16 | } 17 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/multiplatform-library-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("com.android.library") 7 | id("org.jetbrains.kotlin.multiplatform") 8 | id("android-base-convention") 9 | id("dev.icerock.mobile.multiplatform.android-manifest") 10 | } 11 | 12 | kotlin { 13 | ios() 14 | android { 15 | publishLibraryVariants("release", "debug") 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /maps-build-logic/src/main/kotlin/publication-convention.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | import java.util.Base64 6 | 7 | plugins { 8 | id("javadoc-stub-convention") 9 | id("org.gradle.maven-publish") 10 | id("signing") 11 | } 12 | 13 | publishing { 14 | repositories.maven("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") { 15 | name = "OSSRH" 16 | 17 | credentials { 18 | username = System.getenv("OSSRH_USER") 19 | password = System.getenv("OSSRH_KEY") 20 | } 21 | } 22 | 23 | publications.withType { 24 | // Provide artifacts information requited by Maven Central 25 | pom { 26 | name.set("MOKO maps") 27 | description.set("Control your map from common code for mobile (android & ios) Kotlin Multiplatform development") 28 | url.set("https://github.com/icerockdev/moko-maps") 29 | licenses { 30 | license { 31 | name.set("Apache-2.0") 32 | distribution.set("repo") 33 | url.set("https://github.com/icerockdev/moko-maps/blob/master/LICENSE.md") 34 | } 35 | } 36 | 37 | developers { 38 | developer { 39 | id.set("Alex009") 40 | name.set("Aleksey Mikhailov") 41 | email.set("aleksey.mikhailov@icerockdev.com") 42 | } 43 | developer { 44 | id.set("prokopishin") 45 | name.set("Nikita Prokopishin") 46 | email.set("nprokopishin@icerockdev.com") 47 | } 48 | developer { 49 | id.set("Dorofeev") 50 | name.set("Andrey Dorofeev") 51 | email.set("adorofeev@icerockdev.com") 52 | } 53 | } 54 | 55 | scm { 56 | connection.set("scm:git:ssh://github.com/icerockdev/moko-maps.git") 57 | developerConnection.set("scm:git:ssh://github.com/icerockdev/moko-maps.git") 58 | url.set("https://github.com/icerockdev/moko-maps") 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | signing { 66 | val signingKeyId: String? = System.getenv("SIGNING_KEY_ID") 67 | val signingPassword: String? = System.getenv("SIGNING_PASSWORD") 68 | val signingKey: String? = System.getenv("SIGNING_KEY")?.let { base64Key -> 69 | String(Base64.getDecoder().decode(base64Key)) 70 | } 71 | if (signingKeyId != null) { 72 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 73 | sign(publishing.publications) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /maps-google/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("multiplatform-library-convention") 7 | id("dev.icerock.mobile.multiplatform.android-manifest") 8 | id("publication-convention") 9 | id("kotlin-parcelize") 10 | id("kotlin-kapt") 11 | id("kotlinx-serialization") 12 | id("dev.icerock.mobile.multiplatform.cocoapods") 13 | } 14 | 15 | dependencies { 16 | commonMainImplementation(libs.coroutines) 17 | commonMainImplementation(libs.ktorClient) 18 | commonMainImplementation(libs.kotlinSerialization) 19 | 20 | commonMainApi(projects.maps) 21 | commonMainApi(libs.mokoGeo) 22 | commonMainApi(libs.mokoGraphics) 23 | 24 | "androidMainImplementation"(libs.appCompat) 25 | "androidMainImplementation"(libs.lifecycle) 26 | "androidMainApi"(libs.playServicesLocation) 27 | "androidMainApi"(libs.playServicesMaps) 28 | "androidMainImplementation"(libs.googleMapsServices) 29 | "androidMainImplementation"(libs.ktorClientOkHttp) 30 | 31 | "iosMainImplementation"(libs.ktorClientIos) 32 | } 33 | 34 | cocoaPods { 35 | precompiledPod( 36 | scheme = "GoogleMaps", 37 | extraModules = listOf("GoogleMapsBase"), 38 | extraLinkerOpts = listOf( 39 | "GoogleMapsBase", "GoogleMapsCore", "CoreGraphics", "QuartzCore", "UIKit", 40 | "ImageIO", "OpenGLES", "CoreData", "CoreText", "SystemConfiguration", "Security", 41 | "CoreTelephony", "CoreImage" 42 | ).map { "-framework $it" } 43 | ) { podsDir -> 44 | listOf( 45 | File(podsDir, "GoogleMaps/Base/Frameworks"), 46 | File(podsDir, "GoogleMaps/Maps/Frameworks") 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/kotlin/dev/icerock/moko/maps/google/GoogleMapController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import android.annotation.SuppressLint 8 | import android.content.Context 9 | import android.location.Geocoder 10 | import android.location.Location 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.LifecycleObserver 13 | import androidx.lifecycle.OnLifecycleEvent 14 | import com.google.android.gms.location.FusedLocationProviderClient 15 | import com.google.android.gms.location.LocationServices 16 | import com.google.android.gms.maps.CameraUpdateFactory 17 | import com.google.android.gms.maps.GoogleMap 18 | import com.google.android.gms.maps.GoogleMap.OnCameraMoveStartedListener.REASON_GESTURE 19 | import com.google.android.gms.maps.model.BitmapDescriptorFactory 20 | import com.google.android.gms.maps.model.Dash 21 | import com.google.android.gms.maps.model.Gap 22 | import com.google.android.gms.maps.model.LatLngBounds 23 | import com.google.android.gms.maps.model.Marker 24 | import com.google.android.gms.maps.model.MarkerOptions 25 | import com.google.android.gms.maps.model.PatternItem 26 | import com.google.android.gms.maps.model.PolygonOptions 27 | import com.google.android.gms.maps.model.PolylineOptions 28 | import com.google.android.gms.tasks.Task 29 | import com.google.maps.DirectionsApiRequest 30 | import com.google.maps.GeoApiContext 31 | import com.google.maps.PendingResult 32 | import com.google.maps.PlacesApi 33 | import com.google.maps.errors.NotFoundException 34 | import com.google.maps.model.DirectionsResult 35 | import com.google.maps.model.LatLng 36 | import dev.icerock.moko.graphics.Color 37 | import dev.icerock.moko.maps.LineType 38 | import dev.icerock.moko.maps.MapAddress 39 | import dev.icerock.moko.maps.MapController 40 | import dev.icerock.moko.maps.MapElement 41 | import dev.icerock.moko.maps.ZoomConfig 42 | import dev.icerock.moko.resources.ImageResource 43 | import kotlinx.coroutines.Dispatchers 44 | import kotlinx.coroutines.withContext 45 | import java.util.Locale 46 | import kotlin.coroutines.resume 47 | import kotlin.coroutines.resumeWithException 48 | import kotlin.coroutines.suspendCoroutine 49 | import com.google.android.gms.maps.model.LatLng as AndroidLatLng 50 | import dev.icerock.moko.geo.LatLng as GeoLatLng 51 | 52 | @SuppressLint("MissingPermission") 53 | @Suppress("TooManyFunctions") 54 | actual class GoogleMapController( 55 | geoApiKey: String 56 | ) : MapController { 57 | class LifecycleHolder { 58 | private var data: T? = null 59 | private val actions = mutableListOf<(T) -> Unit>() 60 | 61 | fun set(data: T) { 62 | this.data = data 63 | 64 | with(actions) { 65 | forEach { it.invoke(data) } 66 | clear() 67 | } 68 | } 69 | 70 | fun clear() { 71 | this.data = null 72 | } 73 | 74 | fun doWith(block: (T) -> Unit) { 75 | val map = data 76 | if (map == null) { 77 | actions.add(block) 78 | return 79 | } 80 | 81 | block(map) 82 | } 83 | 84 | suspend fun get(): T = suspendCoroutine { continuation -> 85 | doWith { continuation.resume(it) } 86 | } 87 | } 88 | 89 | private val mapHolder = LifecycleHolder() 90 | private val geoCoderHolder = LifecycleHolder() 91 | private val locationHolder = LifecycleHolder() 92 | private val geoApiContext: GeoApiContext = GeoApiContext.Builder() 93 | .apiKey(geoApiKey) 94 | .build() 95 | 96 | actual var onCameraScrollStateChanged: ((scrolling: Boolean, isUserGesture: Boolean) -> Unit)? = 97 | null 98 | 99 | fun bind(lifecycle: Lifecycle, context: Context, googleMap: GoogleMap) { 100 | mapHolder.set(googleMap) 101 | locationHolder.set(LocationServices.getFusedLocationProviderClient(context)) 102 | geoCoderHolder.set(Geocoder(context, Locale.getDefault())) 103 | 104 | lifecycle.addObserver( 105 | object : LifecycleObserver { 106 | @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) 107 | fun onDestroy() { 108 | mapHolder.clear() 109 | locationHolder.clear() 110 | geoCoderHolder.clear() 111 | } 112 | } 113 | ) 114 | 115 | googleMap.uiSettings.apply { 116 | // unsupported on iOS side at all 117 | isMapToolbarEnabled = false 118 | isZoomControlsEnabled = false 119 | } 120 | 121 | googleMap.setOnMarkerClickListener { marker -> 122 | @Suppress("UNCHECKED_CAST") 123 | (marker.tag as? (() -> Unit))?.invoke() 124 | false // not show info box 125 | } 126 | 127 | googleMap.setOnCameraIdleListener { 128 | onCameraScrollStateChanged?.invoke(false, false) 129 | } 130 | 131 | googleMap.setOnCameraMoveStartedListener { reason -> 132 | onCameraScrollStateChanged?.invoke(true, reason == REASON_GESTURE) 133 | } 134 | } 135 | 136 | private suspend fun FusedLocationProviderClient.getLastLocationSuspended(): Location { 137 | val task: Task = lastLocation 138 | return suspendCoroutine { continuation -> 139 | task.addOnSuccessListener { location -> 140 | if (location == null) { 141 | continuation.resumeWithException(NotFoundException("location not found")) 142 | return@addOnSuccessListener 143 | } 144 | 145 | continuation.resume(location) 146 | } 147 | } 148 | } 149 | 150 | override fun showMyLocation(zoom: Float) { 151 | locationHolder.doWith { client -> 152 | client.lastLocation.addOnSuccessListener { location -> 153 | if (location == null) return@addOnSuccessListener 154 | 155 | showLocation( 156 | latLng = GeoLatLng( 157 | latitude = location.latitude, 158 | longitude = location.longitude 159 | ), 160 | zoom = zoom 161 | ) 162 | } 163 | } 164 | } 165 | 166 | override fun showLocation(latLng: GeoLatLng, zoom: Float, animation: Boolean) { 167 | val factory = CameraUpdateFactory.newLatLngZoom( 168 | AndroidLatLng(latLng.latitude, latLng.longitude), 169 | zoom 170 | ) 171 | 172 | mapHolder.doWith { map -> 173 | when (animation) { 174 | true -> map.animateCamera(factory) 175 | false -> map.moveCamera(factory) 176 | } 177 | } 178 | } 179 | 180 | override suspend fun getAddressByLatLng(latitude: Double, longitude: Double): String? { 181 | val geoCoder = geoCoderHolder.get() 182 | 183 | val locations = withContext(Dispatchers.Default) { 184 | @Suppress("BlockingMethodInNonBlockingContext") 185 | geoCoder.getFromLocation(latitude, longitude, SINGLE_RESULT_ADDRESS) 186 | } 187 | 188 | return locations.getOrNull(0) 189 | ?.getAddressLine(0) 190 | } 191 | 192 | override suspend fun getSimilarNearAddresses( 193 | text: String?, 194 | maxResults: Int, 195 | maxRadius: Int 196 | ): List { 197 | if (text.isNullOrEmpty()) return emptyList() 198 | 199 | val lastLocation = locationHolder.get().getLastLocationSuspended() 200 | 201 | return withContext(Dispatchers.IO) { 202 | val nearbyRequest = PlacesApi.nearbySearchQuery( 203 | geoApiContext, 204 | LatLng( 205 | lastLocation.latitude, 206 | lastLocation.longitude 207 | ) 208 | ).keyword(text) 209 | .radius(maxRadius) 210 | 211 | @Suppress("BlockingMethodInNonBlockingContext") 212 | val nearbyResponse = nearbyRequest.await() 213 | 214 | nearbyResponse.results.map { 215 | MapAddress( 216 | it.name, 217 | it.formattedAddress ?: it.vicinity, 218 | GeoLatLng( 219 | it.geometry.location.lat, 220 | it.geometry.location.lng 221 | ) 222 | ) 223 | }.take(maxResults) 224 | } 225 | } 226 | 227 | override suspend fun getCurrentZoom(): Float { 228 | return mapHolder.get().cameraPosition.zoom 229 | } 230 | 231 | override suspend fun setCurrentZoom(zoom: Float) { 232 | val update = CameraUpdateFactory.zoomTo(zoom) 233 | mapHolder.get().moveCamera(update) 234 | } 235 | 236 | override suspend fun getZoomConfig(): ZoomConfig { 237 | val map = mapHolder.get() 238 | return ZoomConfig( 239 | min = map.minZoomLevel, 240 | max = map.maxZoomLevel 241 | ) 242 | } 243 | 244 | override suspend fun setZoomConfig(config: ZoomConfig) { 245 | with(mapHolder.get()) { 246 | resetMinMaxZoomPreference() 247 | config.min?.also { setMinZoomPreference(it) } 248 | config.max?.also { setMaxZoomPreference(it) } 249 | } 250 | } 251 | 252 | override suspend fun getMapCenterLatLng(): GeoLatLng { 253 | return mapHolder.get().cameraPosition.target.let { 254 | GeoLatLng(it.latitude, it.longitude) 255 | } 256 | } 257 | 258 | override suspend fun drawPolygon( 259 | pointList: List, 260 | backgroundColor: Color, 261 | lineColor: Color, 262 | backgroundOpacity: Float, 263 | lineWidth: Float, 264 | lineOpacity: Float, 265 | lineType: LineType 266 | ): MapElement { 267 | val map = mapHolder.get() 268 | val polygonOptions = PolygonOptions().apply { 269 | addAll(pointList.map { it.toAndroidLatLng() }) 270 | fillColor(colorWithOpacity(backgroundColor, backgroundOpacity).argb.toInt()) 271 | strokeColor(colorWithOpacity(lineColor, lineOpacity).argb.toInt()) 272 | strokeWidth(lineWidth) 273 | val pattern: List = when (lineType) { 274 | LineType.SOLID -> emptyList() 275 | LineType.DASHED -> listOf(Dash(DASHED_SOLID_SIZE * lineWidth), Gap(DASHED_EMPTY_SIZE * lineWidth)) 276 | } 277 | strokePattern(pattern) 278 | clickable(false) 279 | } 280 | val polygon = map.addPolygon(polygonOptions) 281 | return GooglePolygon(polygon) 282 | } 283 | 284 | @Suppress("LongMethod") 285 | override suspend fun buildRoute( 286 | points: List, 287 | lineColor: Color, 288 | markersImage: ImageResource? 289 | ): MapElement { 290 | val map = mapHolder.get() 291 | 292 | val from = points.first() 293 | val to = points.last() 294 | val markers = mutableListOf() 295 | 296 | val origin = from.toMapsLatLng() 297 | val destination = to.toMapsLatLng() 298 | 299 | fun addRoutePoint(latLng: GeoLatLng) { 300 | if (markersImage == null) return 301 | 302 | val markerOptions = MarkerOptions() 303 | .position(latLng.toAndroidLatLng()) 304 | .icon(BitmapDescriptorFactory.fromResource(markersImage.drawableResId)) 305 | 306 | val marker = map.addMarker(markerOptions) 307 | markers.add(marker) 308 | } 309 | 310 | val waypoints = mutableListOf() 311 | if (points.size > 2) { 312 | for (i in 1 until points.size - 1) { 313 | waypoints.add(points[i].toMapsLatLng()) 314 | addRoutePoint(points[i]) 315 | } 316 | } 317 | 318 | val directionsResult: DirectionsResult = getDirection( 319 | origin = origin, 320 | destination = destination, 321 | wayPoints = waypoints 322 | ) 323 | 324 | val path = directionsResult.routes 325 | .getOrNull(0) 326 | ?.overviewPolyline 327 | ?.decodePath() 328 | ?.toList() 329 | .orEmpty() 330 | 331 | val lines = PolylineOptions() 332 | lines.width(WIDTH_POLYLINE) 333 | lines.color(lineColor.argb.toInt()) 334 | 335 | val boundsBuilder = LatLngBounds.builder() 336 | 337 | path.map { it.toAndroidLatLng() }.forEach { 338 | lines.add(it) 339 | boundsBuilder.include(it) 340 | } 341 | 342 | val southwestLatLng = boundsBuilder.build().southwest 343 | val northeastLatLng = boundsBuilder.build().northeast 344 | 345 | val coefficient = 346 | (southwestLatLng.latitude - northeastLatLng.latitude) * DIVIDER_BOUND_LATITUDE_PADDING 347 | 348 | val latLngPadding = AndroidLatLng( 349 | southwestLatLng.latitude + coefficient, 350 | southwestLatLng.longitude 351 | ) 352 | boundsBuilder.include(latLngPadding) 353 | 354 | val latLongBounds = boundsBuilder.build() 355 | 356 | path.firstOrNull()?.let { 357 | addRoutePoint(it.toGeoLatLng()) 358 | } 359 | 360 | path.lastOrNull()?.let { 361 | addRoutePoint(it.toGeoLatLng()) 362 | } 363 | 364 | val polyline = map.addPolyline(lines) 365 | map.moveCamera( 366 | CameraUpdateFactory.newLatLngBounds( 367 | latLongBounds, 368 | BOUNDS_PADDING 369 | ) 370 | ) 371 | 372 | return GoogleRoute( 373 | points = markers, 374 | polyline = polyline 375 | ) 376 | } 377 | 378 | private suspend fun getDirection( 379 | origin: LatLng, 380 | destination: LatLng, 381 | wayPoints: MutableList 382 | ): DirectionsResult = withContext(Dispatchers.IO) { 383 | suspendCoroutine { continuation -> 384 | @Suppress("SpreadOperator") 385 | DirectionsApiRequest(geoApiContext) 386 | .alternatives(false) // не стоить альтернативные пути 387 | .origin(origin) 388 | .destination(destination) 389 | .waypoints(*wayPoints.toTypedArray()) 390 | .setCallback( 391 | object : PendingResult.Callback { 392 | override fun onResult(result: DirectionsResult) { 393 | continuation.resume(result) 394 | } 395 | 396 | override fun onFailure(e: Throwable) { 397 | continuation.resumeWithException(e) 398 | } 399 | } 400 | ) 401 | } 402 | } 403 | 404 | override suspend fun addMarker( 405 | image: ImageResource, 406 | latLng: dev.icerock.moko.geo.LatLng, 407 | rotation: Float, 408 | onClick: (() -> Unit)? 409 | ): dev.icerock.moko.maps.Marker { 410 | @Suppress("MagicNumber") 411 | val markerOptions = MarkerOptions() 412 | .position(latLng.toAndroidLatLng()) 413 | .icon(BitmapDescriptorFactory.fromResource(image.drawableResId)) 414 | .rotation(rotation) 415 | .anchor(0.5f, 0.5f) 416 | 417 | val marker = mapHolder.get().addMarker(markerOptions) 418 | marker.tag = onClick 419 | return GoogleMarker(marker) 420 | } 421 | 422 | actual suspend fun readUiSettings(): UiSettings { 423 | val settings = mapHolder.get().uiSettings 424 | return UiSettings( 425 | compassEnabled = settings.isCompassEnabled, 426 | myLocationButtonEnabled = settings.isMyLocationButtonEnabled, 427 | indoorLevelPickerEnabled = settings.isIndoorLevelPickerEnabled, 428 | scrollGesturesEnabled = settings.isScrollGesturesEnabled, 429 | zoomGesturesEnabled = settings.isZoomGesturesEnabled, 430 | tiltGesturesEnabled = settings.isTiltGesturesEnabled, 431 | rotateGesturesEnabled = settings.isRotateGesturesEnabled, 432 | scrollGesturesDuringRotateOrZoomEnabled = settings.isScrollGesturesEnabledDuringRotateOrZoom 433 | ) 434 | } 435 | 436 | actual fun writeUiSettings(settings: UiSettings) { 437 | mapHolder.doWith { 438 | with(it.uiSettings) { 439 | isCompassEnabled = settings.compassEnabled 440 | isMyLocationButtonEnabled = settings.myLocationButtonEnabled 441 | isIndoorLevelPickerEnabled = settings.indoorLevelPickerEnabled 442 | isScrollGesturesEnabled = settings.scrollGesturesEnabled 443 | isZoomControlsEnabled = settings.zoomGesturesEnabled 444 | isTiltGesturesEnabled = settings.tiltGesturesEnabled 445 | isRotateGesturesEnabled = settings.rotateGesturesEnabled 446 | isScrollGesturesEnabledDuringRotateOrZoom = 447 | settings.scrollGesturesDuringRotateOrZoomEnabled 448 | } 449 | 450 | it.isMyLocationEnabled = settings.myLocationButtonEnabled || settings.myLocationEnabled 451 | } 452 | } 453 | 454 | private companion object { 455 | const val SINGLE_RESULT_ADDRESS = 1 456 | const val BOUNDS_PADDING = 250 457 | const val DIVIDER_BOUND_LATITUDE_PADDING = 2 458 | const val WIDTH_POLYLINE = 12.0f 459 | const val DASHED_SOLID_SIZE = 8.0f 460 | const val DASHED_EMPTY_SIZE = 4.0f 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/kotlin/dev/icerock/moko/maps/google/GoogleMarker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import android.animation.ValueAnimator 8 | import dev.icerock.moko.geo.LatLng 9 | import dev.icerock.moko.maps.Marker 10 | import kotlin.time.Duration 11 | import kotlin.time.ExperimentalTime 12 | 13 | actual class GoogleMarker( 14 | private val gmsMarker: com.google.android.gms.maps.model.Marker 15 | ) : Marker { 16 | 17 | override var position: LatLng 18 | get() = gmsMarker.position.toGeoLatLng() 19 | set(value) { 20 | gmsMarker.position = value.toAndroidLatLng() 21 | } 22 | 23 | override var rotation: Float 24 | get() = gmsMarker.rotation 25 | set(value) { 26 | gmsMarker.rotation = value 27 | } 28 | 29 | override fun delete() { 30 | gmsMarker.remove() 31 | } 32 | 33 | @OptIn(ExperimentalTime::class) 34 | override fun move(position: LatLng, rotation: Float, duration: Duration) { 35 | val currentPosition = gmsMarker.position 36 | val newPosition = position.toAndroidLatLng() 37 | 38 | val latDiff = newPosition.latitude - currentPosition.latitude 39 | val lngDiff = newPosition.longitude - currentPosition.longitude 40 | 41 | val currentRotation = gmsMarker.rotation 42 | 43 | val rotationDiff = rotation - currentRotation 44 | 45 | val animator = ValueAnimator.ofFloat(0.0f, 1.0f) 46 | animator.addUpdateListener { valueAnimator -> 47 | val value = valueAnimator.animatedValue as Float 48 | gmsMarker.position = com.google.android.gms.maps.model.LatLng( 49 | currentPosition.latitude + latDiff * value, 50 | currentPosition.longitude + lngDiff * value 51 | ) 52 | gmsMarker.rotation = currentRotation + rotationDiff * value 53 | } 54 | animator.duration = duration.inMilliseconds.toLong() 55 | animator.start() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/kotlin/dev/icerock/moko/maps/google/GooglePolygon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import com.google.android.gms.maps.model.Polygon 8 | import dev.icerock.moko.maps.MapElement 9 | 10 | data class GooglePolygon(val polygon: Polygon) : MapElement { 11 | override fun delete() { 12 | polygon.remove() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/kotlin/dev/icerock/moko/maps/google/GoogleRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import com.google.android.gms.maps.model.Marker 8 | import com.google.android.gms.maps.model.Polyline 9 | import dev.icerock.moko.maps.MapElement 10 | 11 | actual class GoogleRoute( 12 | private val points: List, 13 | private val polyline: Polyline 14 | ) : MapElement { 15 | override fun delete() { 16 | points.forEach { it.remove() } 17 | polyline.remove() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /maps-google/src/androidMain/kotlin/dev/icerock/moko/maps/google/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.geo.LatLng 8 | 9 | internal fun LatLng.toAndroidLatLng() = com.google.android.gms.maps.model.LatLng( 10 | latitude, 11 | longitude 12 | ) 13 | 14 | internal fun LatLng.toMapsLatLng() = com.google.maps.model.LatLng( 15 | latitude, 16 | longitude 17 | ) 18 | 19 | internal fun com.google.maps.model.LatLng.toAndroidLatLng() = com.google.android.gms.maps.model.LatLng( 20 | lat, 21 | lng 22 | ) 23 | 24 | internal fun com.google.maps.model.LatLng.toGeoLatLng() = LatLng( 25 | latitude = lat, 26 | longitude = lng 27 | ) 28 | 29 | internal fun com.google.android.gms.maps.model.LatLng.toGeoLatLng() = LatLng( 30 | latitude = latitude, 31 | longitude = longitude 32 | ) 33 | -------------------------------------------------------------------------------- /maps-google/src/commonMain/kotlin/dev/icerock/moko/maps/google/ColorUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.graphics.Color 8 | 9 | internal fun colorWithOpacity(color: Color, opacity: Float): Color { 10 | @Suppress("MagicNumber") 11 | val newAlpha = (color.alpha * opacity).toInt().let { 12 | if (it > 0xFF) 0xFF 13 | else it 14 | } 15 | return color.copy(alpha = newAlpha) 16 | } 17 | -------------------------------------------------------------------------------- /maps-google/src/commonMain/kotlin/dev/icerock/moko/maps/google/GoogleMapController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.maps.MapController 8 | 9 | expect class GoogleMapController : MapController { 10 | suspend fun readUiSettings(): UiSettings 11 | 12 | fun writeUiSettings(settings: UiSettings) 13 | 14 | var onCameraScrollStateChanged: ((scrolling: Boolean, isUserGesture: Boolean) -> Unit)? 15 | } 16 | -------------------------------------------------------------------------------- /maps-google/src/commonMain/kotlin/dev/icerock/moko/maps/google/GoogleMarker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.maps.Marker 8 | 9 | expect class GoogleMarker : Marker 10 | -------------------------------------------------------------------------------- /maps-google/src/commonMain/kotlin/dev/icerock/moko/maps/google/GoogleRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.maps.MapElement 8 | 9 | expect class GoogleRoute : MapElement 10 | -------------------------------------------------------------------------------- /maps-google/src/commonMain/kotlin/dev/icerock/moko/maps/google/UiSettings.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | data class UiSettings( 8 | val compassEnabled: Boolean = false, 9 | val myLocationEnabled: Boolean = false, 10 | val myLocationButtonEnabled: Boolean = false, 11 | val indoorLevelPickerEnabled: Boolean = false, 12 | val scrollGesturesEnabled: Boolean = true, 13 | val zoomGesturesEnabled: Boolean = true, 14 | val tiltGesturesEnabled: Boolean = true, 15 | val rotateGesturesEnabled: Boolean = true, 16 | val scrollGesturesDuringRotateOrZoomEnabled: Boolean = true 17 | ) 18 | -------------------------------------------------------------------------------- /maps-google/src/iosArm64Main: -------------------------------------------------------------------------------- 1 | iosX64Main -------------------------------------------------------------------------------- /maps-google/src/iosMain/kotlin/dev/icerock/moko/maps/google/Async.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import kotlinx.cinterop.staticCFunction 8 | import platform.Foundation.NSThread 9 | import platform.darwin.dispatch_get_main_queue 10 | import platform.darwin.dispatch_sync_f 11 | import kotlin.native.concurrent.Continuation2 12 | import kotlin.native.concurrent.callContinuation2 13 | 14 | @Suppress("NOTHING_TO_INLINE") 15 | internal inline fun mainContinuation( 16 | singleShot: Boolean = true, 17 | noinline block: (T1, T2) -> Unit 18 | ) = Continuation2( 19 | block = block, 20 | invoker = staticCFunction { invokerArg -> 21 | if (NSThread.isMainThread()) { 22 | invokerArg!!.callContinuation2() 23 | } else { 24 | dispatch_sync_f( 25 | queue = dispatch_get_main_queue(), 26 | context = invokerArg, 27 | work = staticCFunction { args -> 28 | args!!.callContinuation2() 29 | } 30 | ) 31 | } 32 | }, 33 | singleShot = singleShot 34 | ) 35 | -------------------------------------------------------------------------------- /maps-google/src/iosMain/kotlin/dev/icerock/moko/maps/google/GDirection.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | import platform.CoreLocation.CLLocationCoordinate2DMake 10 | 11 | @Serializable 12 | internal data class GDirection( 13 | val routes: List 14 | ) 15 | 16 | @Serializable 17 | internal data class GRoute( 18 | val legs: List, 19 | @SerialName("overview_polyline") 20 | val overviewPolyline: GPolyline 21 | ) 22 | 23 | @Serializable 24 | internal data class GLeg( 25 | @SerialName("start_location") 26 | val startLocation: GLocation, 27 | @SerialName("end_location") 28 | val endLocation: GLocation, 29 | val steps: List, 30 | @SerialName("via_waypoint") 31 | val viaWaypoint: List? 32 | ) 33 | 34 | @Serializable 35 | internal data class GStep( 36 | @SerialName("end_location") 37 | val endLocation: GLocation 38 | ) 39 | 40 | @Serializable 41 | internal data class GWayPoint( 42 | val location: GLocation, 43 | @SerialName("step_index") 44 | val stepIndex: Int 45 | ) 46 | 47 | @Serializable 48 | internal data class GPolyline( 49 | val points: String 50 | ) 51 | 52 | @Serializable 53 | internal data class GLocation( 54 | val lat: Double, 55 | val lng: Double 56 | ) { 57 | fun coord2D() = CLLocationCoordinate2DMake( 58 | latitude = lat, 59 | longitude = lng 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /maps-google/src/iosMain/kotlin/dev/icerock/moko/maps/google/Utils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import dev.icerock.moko.geo.LatLng 8 | import kotlinx.cinterop.CValue 9 | import kotlinx.cinterop.useContents 10 | import platform.CoreLocation.CLLocationCoordinate2D 11 | import platform.CoreLocation.CLLocationCoordinate2DMake 12 | import platform.Foundation.NSError 13 | 14 | internal fun NSError.asThrowable() = Throwable("[$domain] - $code - $localizedDescription") 15 | 16 | internal fun CValue.toLatLng(): LatLng = useContents { 17 | LatLng( 18 | latitude = latitude, 19 | longitude = longitude 20 | ) 21 | } 22 | 23 | internal fun LatLng.toCoord2D() = CLLocationCoordinate2DMake( 24 | latitude = latitude, 25 | longitude = longitude 26 | ) 27 | -------------------------------------------------------------------------------- /maps-google/src/iosX64Main/kotlin/dev/icerock/moko/maps/google/GoogleMapController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import cocoapods.GoogleMaps.GMSAddress 8 | import cocoapods.GoogleMaps.GMSCameraPosition 9 | import cocoapods.GoogleMaps.GMSCoordinateBounds 10 | import cocoapods.GoogleMaps.GMSGeocoder 11 | import cocoapods.GoogleMaps.GMSMapView 12 | import cocoapods.GoogleMaps.GMSMapViewDelegateProtocol 13 | import cocoapods.GoogleMaps.GMSMarker 14 | import cocoapods.GoogleMaps.GMSMutablePath 15 | import cocoapods.GoogleMaps.GMSPath 16 | import cocoapods.GoogleMaps.GMSPolygon 17 | import cocoapods.GoogleMaps.GMSPolyline 18 | import cocoapods.GoogleMaps.animateToCameraPosition 19 | import cocoapods.GoogleMaps.animateToZoom 20 | import cocoapods.GoogleMaps.create 21 | import cocoapods.GoogleMaps.kGMSMaxZoomLevel 22 | import cocoapods.GoogleMaps.kGMSMinZoomLevel 23 | import dev.icerock.moko.geo.LatLng 24 | import dev.icerock.moko.graphics.Color 25 | import dev.icerock.moko.graphics.toUIColor 26 | import dev.icerock.moko.maps.LineType 27 | import dev.icerock.moko.maps.MapAddress 28 | import dev.icerock.moko.maps.MapController 29 | import dev.icerock.moko.maps.MapElement 30 | import dev.icerock.moko.maps.Marker 31 | import dev.icerock.moko.maps.ZoomConfig 32 | import dev.icerock.moko.resources.ImageResource 33 | import io.ktor.client.HttpClient 34 | import io.ktor.client.call.ReceivePipelineException 35 | import io.ktor.client.request.HttpRequestBuilder 36 | import io.ktor.client.request.request 37 | import io.ktor.http.HttpMethod 38 | import io.ktor.http.takeFrom 39 | import kotlinx.cinterop.cValue 40 | import kotlinx.cinterop.readValue 41 | import kotlinx.cinterop.useContents 42 | import kotlinx.serialization.json.Json 43 | import platform.CoreLocation.CLLocation 44 | import platform.CoreLocation.CLLocationCoordinate2D 45 | import platform.CoreLocation.CLLocationCoordinate2DMake 46 | import platform.CoreLocation.CLLocationManager 47 | import platform.MapKit.MKCoordinateRegionMakeWithDistance 48 | import platform.MapKit.MKLocalSearch 49 | import platform.MapKit.MKLocalSearchRequest 50 | import platform.MapKit.MKMapItem 51 | import platform.UIKit.UIEdgeInsetsZero 52 | import platform.darwin.NSObject 53 | import kotlin.coroutines.resume 54 | import kotlin.coroutines.resumeWithException 55 | import kotlin.coroutines.suspendCoroutine 56 | import kotlin.native.ref.WeakReference 57 | 58 | @Suppress("TooManyFunctions") 59 | actual class GoogleMapController( 60 | mapView: GMSMapView, 61 | private val geoApiKey: String 62 | ) : MapController { 63 | private val httpClient = HttpClient() 64 | private val json = Json { 65 | ignoreUnknownKeys = true 66 | } 67 | 68 | private val geoCoder = GMSGeocoder() 69 | private val locationManager = CLLocationManager() 70 | private val delegate = MapDelegate(this) 71 | private val weakMapView = WeakReference(mapView) 72 | 73 | actual var onCameraScrollStateChanged: ((scrolling: Boolean, isUserGesture: Boolean) -> Unit)? = 74 | null 75 | 76 | init { 77 | weakMapView.get()?.delegate = delegate 78 | } 79 | 80 | override suspend fun getAddressByLatLng(latitude: Double, longitude: Double): String? { 81 | val gmsAddress: GMSAddress = suspendCoroutine { continuation -> 82 | val coords = cValue { 83 | this.latitude = latitude 84 | this.longitude = longitude 85 | } 86 | geoCoder.reverseGeocodeCoordinate(coords) { response, error -> 87 | if (error != null) { 88 | continuation.resumeWithException(error.asThrowable()) 89 | return@reverseGeocodeCoordinate 90 | } 91 | 92 | @Suppress("UNCHECKED_CAST") 93 | val addresses = response?.firstResult()?.lines as? List 94 | val firstAddress = addresses?.firstOrNull() 95 | 96 | if (firstAddress == null) { 97 | continuation.resumeWithException(IllegalStateException("empty results")) 98 | return@reverseGeocodeCoordinate 99 | } 100 | 101 | continuation.resume(firstAddress) 102 | } 103 | } 104 | 105 | return gmsAddress.toString() 106 | } 107 | 108 | override suspend fun getSimilarNearAddresses( 109 | text: String?, 110 | maxResults: Int, 111 | maxRadius: Int 112 | ): List { 113 | val location = getCurrentLocation() 114 | 115 | return suspendCoroutine { continuation -> 116 | val request = MKLocalSearchRequest() 117 | request.naturalLanguageQuery = text 118 | 119 | request.region = MKCoordinateRegionMakeWithDistance( 120 | centerCoordinate = CLLocationCoordinate2DMake( 121 | latitude = location.latitude, 122 | longitude = location.longitude 123 | ), 124 | latitudinalMeters = maxRadius.toDouble(), 125 | longitudinalMeters = maxRadius.toDouble() 126 | ) 127 | 128 | val search = MKLocalSearch(request = request) 129 | 130 | search.startWithCompletionHandler( 131 | mainContinuation { response, error -> 132 | if (error != null) { 133 | continuation.resumeWithException(Throwable(error.localizedDescription)) 134 | return@mainContinuation 135 | } 136 | 137 | var addresses = listOf() 138 | val items = response?.mapItems ?: listOf() 139 | 140 | for (item in items) { 141 | val mkItem = item as? MKMapItem 142 | var fullAddress = mkItem?.placemark?.name ?: "" 143 | 144 | val thoroughfare = mkItem?.placemark?.thoroughfare 145 | if (thoroughfare != null && !fullAddress.contains(thoroughfare)) { 146 | fullAddress = fullAddress.plus(", $thoroughfare") 147 | } 148 | 149 | val subThoroughfare = mkItem?.placemark?.subThoroughfare 150 | if (subThoroughfare != null && !fullAddress.contains(subThoroughfare)) { 151 | fullAddress = fullAddress.plus(", $subThoroughfare") 152 | } 153 | 154 | mkItem?.placemark?.coordinate?.useContents { 155 | 156 | val address = MapAddress( 157 | address = fullAddress, 158 | city = mkItem.placemark.locality, 159 | latLng = LatLng( 160 | latitude = this.latitude, 161 | longitude = this.longitude 162 | ), 163 | distance = maxRadius.toDouble() 164 | ) 165 | addresses = addresses.plus(address) 166 | } 167 | } 168 | continuation.resume(addresses.take(maxResults)) 169 | } 170 | ) 171 | } 172 | } 173 | 174 | private fun getCurrentLocation(): LatLng { 175 | val location: CLLocation = weakMapView.get()?.myLocation 176 | ?: locationManager.location 177 | ?: throw IllegalStateException("can't get location") 178 | 179 | return location.coordinate.toLatLng() 180 | } 181 | 182 | override suspend fun addMarker( 183 | image: ImageResource, 184 | latLng: LatLng, 185 | rotation: Float, 186 | onClick: (() -> Unit)? 187 | ): Marker { 188 | val marker = GMSMarker.markerWithPosition(position = latLng.toCoord2D()).also { 189 | it.icon = image.toUIImage() 190 | it.rotation = rotation.toDouble() 191 | it.map = weakMapView.get() 192 | it.tappable = true 193 | it.userData = onClick 194 | } 195 | return GoogleMarker(marker) 196 | } 197 | 198 | override suspend fun buildRoute( 199 | points: List, 200 | lineColor: Color, 201 | markersImage: ImageResource? 202 | ): MapElement { 203 | val builder = HttpRequestBuilder() 204 | builder.method = HttpMethod.Get 205 | 206 | val origin = points.first() 207 | val destination = points.last() 208 | 209 | var waypoints = "" 210 | 211 | if (points.count() > 2) { 212 | waypoints = "&waypoints=" 213 | for (point in points) { 214 | if (point != origin && point != destination) { 215 | waypoints = waypoints.plus("via:${point.latitude},${point.longitude}") 216 | } 217 | } 218 | } 219 | 220 | builder.url { 221 | val originStr = "origin=${origin.latitude},${origin.longitude}" 222 | val destinationStr = "destination=${destination.latitude},${destination.longitude}" 223 | val key = "key=$geoApiKey" 224 | takeFrom("https://maps.googleapis.com/maps/api/directions/json?$originStr&$destinationStr&$key$waypoints") 225 | } 226 | 227 | try { 228 | val result: String = httpClient.request(builder) 229 | return buildRoute(result, lineColor, markersImage) 230 | } catch (pipeline: ReceivePipelineException) { 231 | throw pipeline.cause 232 | } 233 | } 234 | 235 | override suspend fun drawPolygon( 236 | pointList: List, 237 | backgroundColor: Color, 238 | lineColor: Color, 239 | backgroundOpacity: Float, 240 | lineWidth: Float, 241 | lineOpacity: Float, 242 | lineType: LineType 243 | ): MapElement { 244 | val polygonPath = GMSMutablePath().apply { 245 | pointList.forEach { addCoordinate(it.toCoord2D()) } 246 | } 247 | val polygon = GMSPolygon().apply { 248 | path = polygonPath 249 | fillColor = colorWithOpacity(backgroundColor, backgroundOpacity).toUIColor() 250 | strokeColor = colorWithOpacity(lineColor, lineOpacity).toUIColor() 251 | strokeWidth = lineWidth.toDouble() 252 | tappable = false 253 | map = weakMapView.get() 254 | } 255 | if (lineType != LineType.SOLID) { 256 | println("WARNING: GMSPolygon not support line type $lineType") 257 | } 258 | return GooglePolygon(polygon) 259 | } 260 | 261 | private fun buildRoute( 262 | from: String, 263 | lineColor: Color, 264 | markersImage: ImageResource? 265 | ): MapElement { 266 | val direction = json.decodeFromString(GDirection.serializer(), from) 267 | 268 | val route = 269 | direction.routes.firstOrNull() ?: throw IllegalArgumentException("routes not found") 270 | 271 | val path = GMSPath.pathFromEncodedPath(route.overviewPolyline.points) 272 | val routeLine = GMSPolyline.polylineWithPath(path) 273 | 274 | routeLine.strokeColor = lineColor.toUIColor() 275 | routeLine.strokeWidth = ROUTE_STROKE_WIDTH 276 | routeLine.map = weakMapView.get() 277 | 278 | val startMarker = route.legs.firstOrNull()?.startLocation 279 | ?.takeIf { markersImage != null } 280 | ?.let { 281 | GMSMarker.markerWithPosition(it.coord2D()).apply { 282 | icon = markersImage!!.toUIImage() 283 | map = weakMapView.get() 284 | } 285 | } 286 | 287 | val endMarker = route.legs.lastOrNull()?.endLocation 288 | ?.takeIf { markersImage != null } 289 | ?.let { 290 | GMSMarker.markerWithPosition(it.coord2D()).apply { 291 | icon = markersImage!!.toUIImage() 292 | map = weakMapView.get() 293 | } 294 | } 295 | 296 | val firstLeg = route.legs.firstOrNull() 297 | val waypoints = firstLeg?.viaWaypoint 298 | ?.takeIf { markersImage != null } 299 | ?.let { points -> 300 | points.map { 301 | val step = firstLeg.steps[it.stepIndex] 302 | GMSMarker.markerWithPosition(step.endLocation.coord2D()).apply { 303 | icon = markersImage!!.toUIImage() 304 | map = weakMapView.get() 305 | } 306 | } 307 | } 308 | 309 | if (path != null) { 310 | val position = weakMapView.get()?.cameraForBounds( 311 | bounds = GMSCoordinateBounds.create(path = path), 312 | insets = UIEdgeInsetsZero.readValue() 313 | ) 314 | 315 | if (position != null) { 316 | weakMapView.get()?.animateToCameraPosition(position) 317 | } 318 | } 319 | 320 | return GoogleRoute( 321 | routeLine = routeLine, 322 | startMarker = startMarker, 323 | endMarker = endMarker, 324 | wayPointsMarkers = waypoints 325 | ) 326 | } 327 | 328 | override suspend fun getMapCenterLatLng(): LatLng { 329 | return weakMapView.get()?.camera?.target?.toLatLng() ?: LatLng( 330 | latitude = 0.0, 331 | longitude = 0.0 332 | ) 333 | } 334 | 335 | override fun showLocation(latLng: LatLng, zoom: Float, animation: Boolean) { 336 | val position = GMSCameraPosition( 337 | latitude = latLng.latitude, 338 | longitude = latLng.longitude, 339 | zoom = zoom 340 | ) 341 | if (animation) { 342 | weakMapView.get()?.animateToCameraPosition(position) 343 | } else { 344 | weakMapView.get()?.setCamera(position) 345 | } 346 | } 347 | 348 | override fun showMyLocation(zoom: Float) { 349 | val location = getCurrentLocation() 350 | val position = GMSCameraPosition( 351 | latitude = location.latitude, 352 | longitude = location.longitude, 353 | zoom = zoom 354 | ) 355 | weakMapView.get()?.animateToCameraPosition(position) 356 | } 357 | 358 | override suspend fun getCurrentZoom(): Float { 359 | return weakMapView.get()?.camera?.zoom ?: 0f 360 | } 361 | 362 | override suspend fun setCurrentZoom(zoom: Float) { 363 | weakMapView.get()?.animateToZoom(zoom) 364 | } 365 | 366 | override suspend fun getZoomConfig(): ZoomConfig { 367 | return ZoomConfig( 368 | min = weakMapView.get()?.minZoom, 369 | max = weakMapView.get()?.maxZoom 370 | ) 371 | } 372 | 373 | override suspend fun setZoomConfig(config: ZoomConfig) { 374 | weakMapView.get()?.setMinZoom( 375 | minZoom = config.min ?: kGMSMinZoomLevel, 376 | maxZoom = config.max ?: kGMSMaxZoomLevel 377 | ) 378 | } 379 | 380 | actual suspend fun readUiSettings(): UiSettings { 381 | val settings = weakMapView.get()?.settings 382 | return UiSettings( 383 | compassEnabled = settings?.compassButton ?: false, 384 | myLocationButtonEnabled = settings?.myLocationButton ?: false, 385 | indoorLevelPickerEnabled = settings?.indoorPicker ?: false, 386 | scrollGesturesEnabled = settings?.scrollGestures ?: false, 387 | zoomGesturesEnabled = settings?.zoomGestures ?: false, 388 | tiltGesturesEnabled = settings?.tiltGestures ?: false, 389 | rotateGesturesEnabled = settings?.rotateGestures ?: false, 390 | scrollGesturesDuringRotateOrZoomEnabled = settings?.allowScrollGesturesDuringRotateOrZoom 391 | ?: false 392 | ) 393 | } 394 | 395 | actual fun writeUiSettings(settings: UiSettings) { 396 | weakMapView.get()?.settings?.let { 397 | it.compassButton = settings.compassEnabled 398 | it.myLocationButton = settings.myLocationButtonEnabled 399 | it.indoorPicker = settings.indoorLevelPickerEnabled 400 | it.scrollGestures = settings.scrollGesturesEnabled 401 | it.zoomGestures = settings.zoomGesturesEnabled 402 | it.tiltGestures = settings.tiltGesturesEnabled 403 | it.rotateGestures = settings.rotateGesturesEnabled 404 | it.allowScrollGesturesDuringRotateOrZoom = 405 | settings.scrollGesturesDuringRotateOrZoomEnabled 406 | } 407 | weakMapView.get()?.myLocationEnabled = 408 | settings.myLocationButtonEnabled || settings.myLocationEnabled 409 | } 410 | 411 | @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") 412 | private class MapDelegate( 413 | mapController: GoogleMapController 414 | ) : NSObject(), GMSMapViewDelegateProtocol { 415 | private val mapController = WeakReference(mapController) 416 | 417 | @Suppress("RETURN_TYPE_MISMATCH_ON_OVERRIDE") 418 | override fun mapView(mapView: GMSMapView, didTapMarker: GMSMarker): Boolean { 419 | val marker: GMSMarker = didTapMarker 420 | 421 | @Suppress("UNCHECKED_CAST") 422 | (marker.userData as? (() -> Unit))?.invoke() 423 | 424 | return false // not show any info box 425 | } 426 | 427 | override fun mapView(mapView: GMSMapView, willMove: Boolean) { 428 | mapController.get()?.onCameraScrollStateChanged?.invoke(true, willMove) 429 | } 430 | 431 | override fun mapView(mapView: GMSMapView, idleAtCameraPosition: GMSCameraPosition) { 432 | mapController.get()?.onCameraScrollStateChanged?.invoke(false, false) 433 | } 434 | } 435 | 436 | private companion object { 437 | const val ROUTE_STROKE_WIDTH = 3.0 438 | } 439 | } 440 | -------------------------------------------------------------------------------- /maps-google/src/iosX64Main/kotlin/dev/icerock/moko/maps/google/GoogleMarker.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import cocoapods.GoogleMaps.GMSMarker 8 | import dev.icerock.moko.geo.LatLng 9 | import dev.icerock.moko.maps.Marker 10 | import platform.QuartzCore.CATransaction 11 | import kotlin.time.Duration 12 | import kotlin.time.ExperimentalTime 13 | 14 | actual class GoogleMarker( 15 | private val gmsMarker: GMSMarker 16 | ) : Marker { 17 | override var position: LatLng 18 | get() = gmsMarker.position.toLatLng() 19 | set(value) { 20 | gmsMarker.position = value.toCoord2D() 21 | } 22 | 23 | override var rotation: Float 24 | get() = gmsMarker.rotation.toFloat() 25 | set(value) { 26 | gmsMarker.rotation = value.toDouble() 27 | } 28 | 29 | override fun delete() { 30 | gmsMarker.map = null 31 | } 32 | 33 | @OptIn(ExperimentalTime::class) 34 | override fun move(position: LatLng, rotation: Float, duration: Duration) { 35 | CATransaction.begin() 36 | CATransaction.setAnimationDuration(duration.inSeconds) 37 | 38 | gmsMarker.position = position.toCoord2D() 39 | gmsMarker.rotation = rotation.toDouble() 40 | 41 | CATransaction.commit() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /maps-google/src/iosX64Main/kotlin/dev/icerock/moko/maps/google/GooglePolygon.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import cocoapods.GoogleMaps.GMSPolygon 8 | import dev.icerock.moko.maps.MapElement 9 | 10 | data class GooglePolygon(val polygon: GMSPolygon) : MapElement { 11 | override fun delete() { 12 | polygon.map = null 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /maps-google/src/iosX64Main/kotlin/dev/icerock/moko/maps/google/GoogleRoute.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.google 6 | 7 | import cocoapods.GoogleMaps.GMSMarker 8 | import cocoapods.GoogleMaps.GMSPolyline 9 | import dev.icerock.moko.maps.MapElement 10 | 11 | actual class GoogleRoute( 12 | val routeLine: GMSPolyline, 13 | val startMarker: GMSMarker?, 14 | val endMarker: GMSMarker?, 15 | val wayPointsMarkers: List? 16 | ) : MapElement { 17 | override fun delete() { 18 | wayPointsMarkers?.forEach { it.map = null } 19 | routeLine.map = null 20 | startMarker?.map = null 21 | endMarker?.map = null 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /maps-mapbox/build.gradle.kts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | plugins { 6 | id("multiplatform-library-convention") 7 | id("dev.icerock.mobile.multiplatform.android-manifest") 8 | id("publication-convention") 9 | id("kotlin-parcelize") 10 | id("dev.icerock.mobile.multiplatform.cocoapods") 11 | } 12 | 13 | dependencies { 14 | commonMainImplementation(libs.coroutines) 15 | 16 | commonMainApi(projects.maps) 17 | 18 | "androidMainImplementation"(libs.appCompat) 19 | "androidMainImplementation"(libs.lifecycle) 20 | "androidMainImplementation"(libs.playServicesLocation) 21 | "androidMainImplementation"(libs.mapboxAnnotation) 22 | "androidMainImplementation"(libs.mapboxServices) 23 | "androidMainApi"(libs.mapbox) 24 | "androidMainApi"(libs.mapboxNavigation) 25 | } 26 | 27 | cocoaPods { 28 | precompiledPod( 29 | scheme = "Mapbox" 30 | ) { podsDir -> 31 | listOf(File(podsDir, "Mapbox-iOS-SDK/dynamic")) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /maps-mapbox/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /maps-mapbox/src/androidMain/kotlin/dev/icerock/moko/maps/mapbox/MapboxController.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. 3 | */ 4 | 5 | package dev.icerock.moko.maps.mapbox 6 | 7 | import android.annotation.SuppressLint 8 | import android.content.Context 9 | import android.location.Geocoder 10 | import android.location.Location 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.LifecycleObserver 13 | import androidx.lifecycle.OnLifecycleEvent 14 | import com.google.android.gms.location.FusedLocationProviderClient 15 | import com.google.android.gms.location.LocationServices 16 | import com.mapbox.api.directions.v5.DirectionsCriteria 17 | import com.mapbox.api.directions.v5.DirectionsCriteria.GEOMETRY_POLYLINE 18 | import com.mapbox.api.directions.v5.MapboxDirections 19 | import com.mapbox.geojson.Feature 20 | import com.mapbox.geojson.Geometry 21 | import com.mapbox.geojson.LineString 22 | import com.mapbox.geojson.Point 23 | import com.mapbox.geojson.Polygon 24 | import com.mapbox.mapboxsdk.camera.CameraUpdateFactory 25 | import com.mapbox.mapboxsdk.location.LocationComponentActivationOptions 26 | import com.mapbox.mapboxsdk.maps.MapView 27 | import com.mapbox.mapboxsdk.maps.MapboxMap 28 | import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener.REASON_API_GESTURE 29 | import com.mapbox.mapboxsdk.maps.Style 30 | import com.mapbox.mapboxsdk.plugins.annotation.SymbolManager 31 | import com.mapbox.mapboxsdk.plugins.annotation.SymbolOptions 32 | import com.mapbox.mapboxsdk.style.layers.FillLayer 33 | import com.mapbox.mapboxsdk.style.layers.LineLayer 34 | import com.mapbox.mapboxsdk.style.layers.Property.LINE_CAP_ROUND 35 | import com.mapbox.mapboxsdk.style.layers.Property.LINE_JOIN_ROUND 36 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillColor 37 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.fillOpacity 38 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineCap 39 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineColor 40 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineDasharray 41 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineJoin 42 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineOpacity 43 | import com.mapbox.mapboxsdk.style.layers.PropertyFactory.lineWidth 44 | import com.mapbox.mapboxsdk.style.sources.GeoJsonSource 45 | import com.mapbox.mapboxsdk.utils.BitmapUtils 46 | import dev.icerock.moko.geo.LatLng 47 | import dev.icerock.moko.graphics.Color 48 | import dev.icerock.moko.graphics.colorInt 49 | import dev.icerock.moko.maps.LineType 50 | import dev.icerock.moko.maps.MapAddress 51 | import dev.icerock.moko.maps.MapController 52 | import dev.icerock.moko.maps.MapElement 53 | import dev.icerock.moko.maps.ZoomConfig 54 | import dev.icerock.moko.resources.ImageResource 55 | import kotlinx.coroutines.Dispatchers 56 | import kotlinx.coroutines.withContext 57 | import java.util.Locale 58 | import kotlin.coroutines.resume 59 | import kotlin.coroutines.resumeWithException 60 | import kotlin.coroutines.suspendCoroutine 61 | 62 | @SuppressLint("MissingPermission") 63 | @Suppress("TooManyFunctions") 64 | actual class MapboxController( 65 | private val accessToken: String 66 | ) : MapController { 67 | private val contextHolder = LifecycleHolder() 68 | private val mapHolder = LifecycleHolder() 69 | private val mapViewHolder = LifecycleHolder() 70 | private val styleHolder = LifecycleHolder