├── .gitignore ├── .idea ├── .gitignore ├── artifacts │ ├── example_js.xml │ └── maps_compose_web_js.xml ├── gradle.xml ├── kotlinc.xml ├── misc.xml ├── uiDesigner.xml └── vcs.xml ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── example ├── build.gradle.kts ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties └── src │ └── jsMain │ ├── kotlin │ ├── DrawingExample.kt │ ├── GoogleMapExample.kt │ ├── LayerExample.kt │ ├── Main.kt │ └── Utils.kt │ └── resources │ └── index.html ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jitpack.yml ├── kotlin-js-store └── yarn.lock ├── maps-compose-web ├── build.gradle.kts └── src │ └── jsMain │ └── kotlin │ └── com │ └── chihsuanwu │ └── maps │ └── compose │ └── web │ ├── CameraPositionState.kt │ ├── Coordinates.kt │ ├── Events.kt │ ├── GoogleMap.kt │ ├── MapApplier.kt │ ├── MapOptions.kt │ ├── MapUpdater.kt │ ├── drawing │ ├── Circle.kt │ ├── InfoWindow.kt │ ├── Marker.kt │ ├── OverlayView.kt │ ├── Polygon.kt │ ├── Polyline.kt │ └── Rectangle.kt │ ├── jsobject │ ├── Coordinates.kt │ ├── Events.kt │ ├── Map.kt │ ├── MapOptions.kt │ ├── drawing │ │ ├── Circle.kt │ │ ├── InfoWindow.kt │ │ ├── Marker.kt │ │ ├── OverlayView.kt │ │ ├── Polygon.kt │ │ ├── Polyline.kt │ │ └── Rectangle.kt │ └── layers │ │ ├── BicyclingLayer.kt │ │ ├── HeatmapLayer.kt │ │ ├── KMLLayer.kt │ │ ├── TrafficLayer.kt │ │ └── TransitLayer.kt │ └── layers │ ├── BicyclingLayer.kt │ ├── HeatmapLayer.kt │ ├── KMLLayer.kt │ ├── TrafficLayer.kt │ └── TransitLayer.kt └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/modules.xml 9 | .idea/jarRepositories.xml 10 | .idea/compiler.xml 11 | .idea/libraries/ 12 | *.iws 13 | *.iml 14 | *.ipr 15 | out/ 16 | !**/src/main/**/out/ 17 | !**/src/test/**/out/ 18 | 19 | ### Eclipse ### 20 | .apt_generated 21 | .classpath 22 | .factorypath 23 | .project 24 | .settings 25 | .springBeans 26 | .sts4-cache 27 | bin/ 28 | !**/src/main/**/bin/ 29 | !**/src/test/**/bin/ 30 | 31 | ### NetBeans ### 32 | /nbproject/private/ 33 | /nbbuild/ 34 | /dist/ 35 | /nbdist/ 36 | /.nb-gradle/ 37 | 38 | ### VS Code ### 39 | .vscode/ 40 | 41 | ### Mac OS ### 42 | .DS_Store -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/artifacts/example_js.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/example/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/maps_compose_web_js.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/maps-compose-web/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/uiDesigner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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 | # Google Maps Compose Web 2 | 3 | [![](https://jitpack.io/v/chihsuanwu/google-maps-compose-web.svg)](https://jitpack.io/#chihsuanwu/google-maps-compose-web) 4 | 5 | A library for using Google Maps in [Compose HTML](https://github.com/JetBrains/compose-jb). 6 | 7 | This library is inspired by [Maps Compose for Android](https://github.com/googlemaps/android-maps-compose). 8 | 9 | # Usage 10 | 11 | Adding a `GoogleMap` to your Compose UI as follows: 12 | 13 | ```kotlin 14 | val cameraPositionState = rememberCameraPositionState { 15 | position = CameraPosition( 16 | center = LatLng(23.2, 120.5), 17 | zoom = 8.0, 18 | ) 19 | } 20 | GoogleMap( 21 | apiKey = "YOUR_API_KEY", 22 | cameraPositionState = cameraPositionState, 23 | attrs = { 24 | style { 25 | width(100.percent) 26 | height(100.percent) 27 | } 28 | } 29 | ) 30 | ``` 31 | 32 |
33 | Configuring the map 34 | 35 | ## Configuring the map 36 | 37 | Configuring the map can be done by passing a `MapOptions` object to the `GoogleMap` composable. 38 | 39 | ```kotlin 40 | val mapOptions = remember { 41 | MapOptions( 42 | fullscreenControl = false, 43 | // ... 44 | ) 45 | } 46 | 47 | GoogleMap( 48 | // ... 49 | mapOptions = mapOptions, 50 | ) { 51 | // ... 52 | } 53 | ``` 54 | 55 |
56 | 57 |
58 | Handling map events 59 | 60 | ## Handling map events 61 | 62 | Map events can be handled by passing a lambda expression to the `GoogleMap` composable. 63 | 64 | ```kotlin 65 | GoogleMap( 66 | // ... 67 | onClick = { 68 | console.log("Map clicked!") 69 | }, 70 | onDrag = { 71 | console.log("Map dragged!") 72 | }, 73 | // Add more events here 74 | ) { 75 | // ... 76 | } 77 | ``` 78 | 79 |
80 | 81 |
82 | Drawing on the map 83 | 84 | ## Drawing on the map 85 | 86 | Adding child composable, such as `Marker`, to the `GoogleMap` composable. 87 | 88 | ```kotlin 89 | GoogleMap( 90 | // ... 91 | ) { 92 | Marker( 93 | state = MarkerState(position = LatLng(23.2, 120.5)), 94 | onClick = { 95 | console.log("Marker clicked!") 96 | }, 97 | // ... 98 | ) 99 | } 100 | ``` 101 | 102 | To display custom content on the map, use the `OverlayView` composable. 103 | 104 | ```kotlin 105 | GoogleMap( 106 | // ... 107 | ) { 108 | OverlayView( 109 | bounds = LatLngBounds( 110 | // ... 111 | ), 112 | content = { 113 | Div { 114 | Text("Overlay View") 115 | } 116 | } 117 | ) 118 | } 119 | ``` 120 | 121 | Currently, the following drawing composable are supported: 122 | - `Marker` 123 | - `Polygons` (polygon, polyline, rectangle, circle) 124 | - `InfoWindow` 125 | - `OverlayView` 126 | 127 |
128 | 129 |
130 | Marker's Info Window 131 | 132 | ## Marker's Info Window 133 | 134 | An info window can be added to a `Marker` directly by passing a lambda expression to the `infoContent` parameter. 135 | 136 | To show the info window, call `showInfoWindow()` on the `MarkerState`. 137 | 138 | ```kotlin 139 | state = rememberMarkerState() 140 | 141 | Marker( 142 | state = state, 143 | // ... 144 | infoContent = { 145 | Div { 146 | Span({ style { fontSize(20.px) } }) { 147 | Text("Info Window Title") 148 | } 149 | Text("Info Window Content") 150 | } 151 | } 152 | ) 153 | 154 | // show the info window 155 | state.showInfoWindow() 156 | ``` 157 | 158 |
159 | 160 |
161 | Map Layers 162 | 163 | ## Map Layers 164 | 165 | Map layers can be added to the `GoogleMap` composable. 166 | 167 | ```kotlin 168 | GoogleMap( 169 | // ... 170 | ) { 171 | if (showTrafficLayer) { 172 | TrafficLayer() 173 | } 174 | } 175 | ``` 176 | 177 | Currently, `TrafficLayer`, `TransitLayer`, `BicyclingLayer`, `HeatmapLayer` and `KmlLayer` are supported. 178 | 179 |
180 | 181 | # Setup 182 | 183 | Add the following to your `build.gradle.kts` file: 184 | 185 | ```kotlin 186 | repositories { 187 | maven("https://jitpack.io") 188 | } 189 | 190 | kotlin { 191 | sourceSets { 192 | val jsMain by getting { 193 | dependencies { 194 | implementation("com.github.chihsuanwu:google-maps-compose-web:") 195 | } 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | # Current State 202 | 203 | **This library is currently in alpha state and the API is subject to change.** 204 | 205 | There are still many advanced features that are not yet supported. 206 | However, if you are a user of Compose HTML and would like to use Google Maps in your web application, 207 | this library is still worth a try. 208 | 209 | Feedback and contributions are highly appreciated! Feel free to open an issue or submit a pull request. 210 | 211 | If you like this library, please consider starring this project, so we know that it is useful to you. 212 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 6 | } 7 | } 8 | 9 | plugins { 10 | kotlin("multiplatform") apply false 11 | id("org.jetbrains.compose") apply false 12 | } 13 | 14 | // The node version that kotlin 1.8.20 uses is too new for Jitpack. 15 | // So we fix the node version to 16.20.0. 16 | plugins.withType { 17 | configure { 18 | nodeVersion = "16.20.0" 19 | } 20 | } -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("org.jetbrains.compose") 4 | } 5 | 6 | fun kotlinw(target: String): String = "org.jetbrains.kotlin-wrappers:kotlin-$target" 7 | 8 | val kotlinWrappersVersion = "1.0.0-pre.624" 9 | 10 | kotlin { 11 | js(IR) { 12 | browser { 13 | testTask { 14 | testLogging.showStandardStreams = true 15 | useKarma { 16 | useChromeHeadless() 17 | useFirefox() 18 | } 19 | } 20 | } 21 | binaries.executable() 22 | } 23 | sourceSets { 24 | val jsMain by getting { 25 | dependencies { 26 | implementation(kotlin("stdlib-js")) 27 | implementation(kotlinw("js")) 28 | 29 | implementation(compose.html.core) 30 | implementation(compose.runtime) 31 | 32 | implementation("app.softwork:routing-compose:0.2.12") 33 | 34 | implementation(project(":maps-compose-web")) 35 | } 36 | } 37 | val jsTest by getting { 38 | dependencies { 39 | implementation(kotlin("test-js")) 40 | } 41 | } 42 | } 43 | } 44 | 45 | dependencies { 46 | "jsMainImplementation"(enforcedPlatform(kotlinw("wrappers-bom:$kotlinWrappersVersion"))) 47 | } -------------------------------------------------------------------------------- /example/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chihsuanwu/google-maps-compose-web/be5a689d2fc9ae62baf40169fae6727fd18f2ae9/example/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example/src/jsMain/kotlin/DrawingExample.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.* 2 | import com.chihsuanwu.maps.compose.web.* 3 | import com.chihsuanwu.maps.compose.web.drawing.* 4 | import org.jetbrains.compose.web.css.* 5 | import org.jetbrains.compose.web.dom.Button 6 | import org.jetbrains.compose.web.dom.Div 7 | import org.jetbrains.compose.web.dom.Span 8 | import org.jetbrains.compose.web.dom.Text 9 | import kotlin.random.Random 10 | 11 | 12 | private class State { 13 | var cameraPositionState: CameraPositionState = CameraPositionState( 14 | CameraPosition( 15 | center = LatLng(23.5, 120.8), 16 | zoom = 7.6, 17 | ) 18 | ) 19 | var polyline: List by mutableStateOf(emptyList()) 20 | var polygon: List by mutableStateOf(emptyList()) 21 | var markers: List by mutableStateOf(emptyList()) 22 | var infoWindowState: InfoWindowState by mutableStateOf( 23 | InfoWindowState( 24 | LatLng(23.47, 120.96), 25 | ) 26 | ) 27 | var overlayViewBounds: LatLngBounds? by mutableStateOf(null) 28 | var overlayViewLayer: MapPanes by mutableStateOf(MapPanes.OverlayLayer) 29 | } 30 | 31 | @Composable 32 | fun DrawingExample( 33 | apiKey: String, 34 | ) { 35 | val state = remember { State() } 36 | 37 | ToolBar(state) 38 | 39 | GoogleMap( 40 | apiKey = apiKey, 41 | cameraPositionState = state.cameraPositionState, 42 | extra = "libraries=geometry", // Required for decoding path from encoded string 43 | attrs = { 44 | style { 45 | width(100.percent) 46 | flex(1) // Fill the remaining height 47 | property("margin", "0 auto") // Center the map 48 | } 49 | } 50 | ) { 51 | if (state.polyline.isNotEmpty()) { 52 | Polyline( 53 | points = state.polyline, 54 | clickable = true, 55 | color = "#EE4411", 56 | icons = listOf( 57 | IconSequence( 58 | icon = MarkerIcon.Symbol( 59 | path = MarkerIcon.Symbol.Path.SymbolPath.Circle, 60 | scale = 5.0, 61 | strokeColor = "#33FF22", 62 | ), 63 | repeat = "100%" 64 | ), 65 | ), 66 | opacity = 0.8, 67 | onClick = { 68 | console.log("Polyline clicked!") 69 | }, 70 | onDoubleClick = { 71 | console.log("Polyline double clicked!, ${it.latLng.asString()}") 72 | }, 73 | ) 74 | } 75 | 76 | if (state.polygon.isNotEmpty()) { 77 | Polygon( 78 | points = state.polygon, 79 | fillColor = "#EE4411", 80 | fillOpacity = 0.38, 81 | strokeColor = "#DD8822", 82 | ) 83 | } 84 | 85 | state.markers.forEach { marker -> 86 | Marker( 87 | state = marker, 88 | animation = MarkerAnimation.BOUNCE, 89 | title = "Hello, Marker ${marker.position.asString()}!", 90 | // icon = MarkerIcon.URL(url = "https://developers.google.com/maps/documentation/javascript/examples/full/images/beachflag.png"), 91 | icon = MarkerIcon.Symbol( 92 | path = MarkerIcon.Symbol.Path.SymbolPath.BackwardClosedArrow, 93 | scale = 5.0, 94 | ), 95 | draggable = true, 96 | onClick = { 97 | console.log("Marker clicked at ${it.latLng.asString()}!") 98 | marker.showInfoWindow() 99 | }, 100 | onDragEnd = { 101 | console.log("Marker dragged! New position: ${it.latLng.asString()}") 102 | }, 103 | onDoubleClick = { 104 | console.log("Marker double clicked!") 105 | }, 106 | ) { 107 | Div( 108 | attrs = { 109 | style { 110 | backgroundColor(Color("#0066BB")) 111 | padding(5.px) 112 | borderRadius(5.px) 113 | } 114 | } 115 | ) { 116 | Span({ 117 | style { 118 | color(Color("#FFFFFF")) 119 | fontWeight("bold") 120 | } 121 | }) { 122 | Text("Hello, Marker ${marker.position.asString()}!") 123 | } 124 | 125 | Button({ 126 | style { 127 | color(Color("#FFCC00")) 128 | marginLeft(10.px) 129 | } 130 | onClick { 131 | marker.hideInfoWindow() 132 | } 133 | }) { 134 | Text("Close") 135 | } 136 | } 137 | } 138 | } 139 | 140 | InfoWindow( 141 | state = state.infoWindowState, 142 | maxWidth = 200, 143 | ) { 144 | Div( 145 | attrs = { 146 | style { 147 | backgroundColor(Color("#11CC44")) 148 | padding(5.px) 149 | borderRadius(5.px) 150 | display(DisplayStyle.Flex) 151 | flexDirection(FlexDirection.Column) 152 | justifyContent(JustifyContent.Center) 153 | alignItems(AlignItems.Center) 154 | } 155 | } 156 | ) { 157 | Span({ 158 | style { 159 | color(Color("#FFFFFF")) 160 | fontWeight("bold") 161 | } 162 | }) { 163 | Text("Taiwan's highest mountain") 164 | } 165 | 166 | Text("Mount Yu-Shan (also known as Jade Mountain) is the highest mountain in Taiwan, at 3,952 m (12,966 ft) above sea level.") 167 | } 168 | } 169 | 170 | state.overlayViewBounds?.let { 171 | OverlayView( 172 | bounds = it, 173 | mapPane = state.overlayViewLayer, 174 | ) { 175 | Div( 176 | attrs = { 177 | style { 178 | backgroundColor(Color("#FFCC88")) 179 | padding(15.px) 180 | borderRadius(15.px) 181 | display(DisplayStyle.Flex) 182 | flexDirection(FlexDirection.Column) 183 | justifyContent(JustifyContent.Center) 184 | alignItems(AlignItems.Center) 185 | width(100.percent) 186 | height(100.percent) 187 | } 188 | } 189 | ) { 190 | var clicked by remember { mutableStateOf(false) } 191 | Text("Hello, OverlayView! In ${state.overlayViewLayer}") 192 | Button({ 193 | onClick { 194 | clicked = !clicked 195 | } 196 | }) { 197 | Text("Click me!") 198 | } 199 | if (clicked) { 200 | Text("The content can be recomposed dynamically!") 201 | } 202 | } 203 | } 204 | } 205 | 206 | } 207 | } 208 | 209 | @Composable 210 | private fun ToolBar(state: State) { 211 | Div { 212 | Div( 213 | attrs = { 214 | style { 215 | margin(5.px) 216 | } 217 | } 218 | ) { 219 | if (state.polyline.isNotEmpty()) { 220 | Span( 221 | attrs = { 222 | style { 223 | color(Color("#EE4411")) 224 | fontWeight("bold") 225 | } 226 | } 227 | ) { 228 | Text("Taiwan High Speed Rail") 229 | } 230 | } 231 | Button( 232 | attrs = { 233 | style { 234 | marginLeft(10.px) 235 | marginRight(10.px) 236 | } 237 | onClick { 238 | if (state.polyline.isNotEmpty()) { 239 | state.polyline = emptyList() 240 | return@onClick 241 | } 242 | val path = 243 | "{d|wCkjfeVv^|rP`uEvgInChuo@l_g@zaa@fpf@l}h@pk_Bb{g@|mm@dcGh}Yzz]jbu@nhQllgBjdFzfm@_fC" 244 | state.polyline = path.decodePath() 245 | } 246 | } 247 | ) { 248 | Text(if (state.polyline.isEmpty()) "Draw Polyline" else "Clear Polyline") 249 | } 250 | 251 | if (state.polygon.isNotEmpty()) { 252 | Span( 253 | attrs = { 254 | style { 255 | color(Color("#EE4411")) 256 | fontWeight("bold") 257 | marginLeft(10.px) 258 | } 259 | } 260 | ) { 261 | Text("Taipei City") 262 | } 263 | } 264 | Button( 265 | attrs = { 266 | style { 267 | marginLeft(10.px) 268 | marginRight(10.px) 269 | } 270 | onClick { 271 | if (state.polygon.isNotEmpty()) { 272 | state.polygon = emptyList() 273 | return@onClick 274 | } 275 | val path = """ 276 | odwwCwmqeVhWtG{BhbEns@|nDp|ChRf`BcAze@o{Btm@jhCa`@vnCtHp_BoyBre@gZ~eBi{@ 277 | zi@_dAfBgfA~eBxkAzrBgj@~c@}lC`^yh@wbBevCwh@qcD~_Bap@vaEeqBfCgtDs{CykCyqB 278 | hWycB{wCs|CaaBqnBl~AgfBvlCsGd{@wdAdcEk}AjxBtt@h{A_oDbiE`l@|kCaO|eAslC~Oc~B 279 | """.trimIndent().replace("\n", "") 280 | state.polygon = path.decodePath() 281 | } 282 | } 283 | ) { 284 | Text(if (state.polygon.isEmpty()) "Draw Polygon" else "Clear Polygon") 285 | } 286 | 287 | Button( 288 | attrs = { 289 | style { 290 | marginLeft(10.px) 291 | } 292 | onClick { 293 | state.markers += listOf( 294 | MarkerState( 295 | position = LatLng( 296 | state.cameraPositionState.position.center.lat, 297 | state.cameraPositionState.position.center.lng, 298 | ) 299 | ) 300 | ) 301 | } 302 | } 303 | ) { 304 | Text("Add Marker At Camera Center") 305 | } 306 | 307 | Button( 308 | attrs = { 309 | style { 310 | marginLeft(10.px) 311 | } 312 | onClick { 313 | state.markers = emptyList() 314 | } 315 | } 316 | ) { 317 | Text("Clear Markers") 318 | } 319 | 320 | Button( 321 | attrs = { 322 | style { 323 | marginLeft(10.px) 324 | } 325 | onClick { 326 | state.infoWindowState.showInfoWindow() 327 | } 328 | } 329 | ) { 330 | Text("Show InfoWindow") 331 | } 332 | } 333 | 334 | Div( 335 | attrs = { 336 | style { 337 | margin(5.px) 338 | } 339 | } 340 | ) { 341 | if (state.overlayViewBounds != null) { 342 | Button( 343 | attrs = { 344 | style { 345 | marginLeft(10.px) 346 | } 347 | onClick { 348 | state.overlayViewBounds = LatLngBounds( 349 | east = Random.nextDouble(121.0, 122.0), 350 | north = Random.nextDouble(23.0, 24.0), 351 | south = Random.nextDouble(22.0, 23.0), 352 | west = Random.nextDouble(120.0, 121.0), 353 | ) 354 | } 355 | } 356 | ) { 357 | Text("Move OverlayView Randomly") 358 | } 359 | 360 | Button( 361 | attrs = { 362 | style { 363 | marginLeft(10.px) 364 | } 365 | onClick { 366 | val index = state.overlayViewLayer.ordinal + 1 367 | state.overlayViewLayer = MapPanes.values()[index % MapPanes.values().size] 368 | } 369 | } 370 | ) { 371 | Text("Change OverlayView Layer, Current: ${state.overlayViewLayer}") 372 | } 373 | 374 | Button( 375 | attrs = { 376 | style { 377 | marginLeft(10.px) 378 | } 379 | onClick { 380 | state.overlayViewBounds = null 381 | } 382 | } 383 | ) { 384 | Text("Hide OverlayView") 385 | } 386 | } else { 387 | Button( 388 | attrs = { 389 | style { 390 | marginLeft(10.px) 391 | } 392 | onClick { 393 | state.overlayViewBounds = LatLngBounds( 394 | east = Random.nextDouble(121.0, 122.0), 395 | north = Random.nextDouble(23.0, 24.0), 396 | south = Random.nextDouble(22.0, 23.0), 397 | west = Random.nextDouble(120.0, 121.0), 398 | ) 399 | } 400 | } 401 | ) { 402 | Text("Show OverlayView") 403 | } 404 | } 405 | } 406 | } 407 | } -------------------------------------------------------------------------------- /example/src/jsMain/kotlin/GoogleMapExample.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.* 2 | import com.chihsuanwu.maps.compose.web.* 3 | import js.core.jso 4 | import kotlinx.coroutines.delay 5 | import org.jetbrains.compose.web.ExperimentalComposeWebApi 6 | import org.jetbrains.compose.web.css.* 7 | import org.jetbrains.compose.web.dom.* 8 | import kotlin.random.Random 9 | 10 | 11 | private class MapListenerState { 12 | var boundsChanged: Int by mutableStateOf(0) 13 | var centerChanged: Int by mutableStateOf(0) 14 | var click: Int by mutableStateOf(0) 15 | var contextMenu: Int by mutableStateOf(0) 16 | var dblClick: Int by mutableStateOf(0) 17 | var drag: Int by mutableStateOf(0) 18 | var dragEnd: Int by mutableStateOf(0) 19 | var dragStart: Int by mutableStateOf(0) 20 | var headingChanged: Int by mutableStateOf(0) 21 | var idle: Int by mutableStateOf(0) 22 | var mapTypeIdChanged: Int by mutableStateOf(0) 23 | var mouseMove: Int by mutableStateOf(0) 24 | var mouseOut: Int by mutableStateOf(0) 25 | var mouseOver: Int by mutableStateOf(0) 26 | var projectionChanged: Int by mutableStateOf(0) 27 | var renderingTypeChanged: Int by mutableStateOf(0) 28 | var tilesLoaded: Int by mutableStateOf(0) 29 | var tiltChanged: Int by mutableStateOf(0) 30 | var zoomChanged: Int by mutableStateOf(0) 31 | } 32 | 33 | private enum class MapStyle { 34 | Default, 35 | Night, 36 | Retro, 37 | } 38 | 39 | @Composable 40 | fun MapExample( 41 | apiKey: String, 42 | ) { 43 | val mapListenerState: MapListenerState by remember { mutableStateOf(MapListenerState()) } 44 | 45 | val cameraPositionState = rememberCameraPositionState { 46 | position = CameraPosition( 47 | center = LatLng(40.71403, 285.99708), 48 | zoom = 13.0, 49 | ) 50 | } 51 | 52 | var mapStyle by remember { mutableStateOf(MapStyle.Default) } 53 | 54 | val mapOptions = remember(mapStyle) { 55 | MapOptions( 56 | fullscreenControlOptions = FullscreenControlOptions( 57 | position = ControlPosition.TopRight 58 | ), 59 | mapTypeControl = true, 60 | mapTypeControlOptions = MapTypeControlOptions( 61 | mapTypeIds = listOf( 62 | MapTypeId.Roadmap, 63 | MapTypeId.Satellite, 64 | MapTypeId.Hybrid, 65 | MapTypeId.Terrain, 66 | ), 67 | position = ControlPosition.TopCenter, 68 | style = MapTypeControlStyle.DropdownMenu, 69 | ), 70 | styles = when (mapStyle) { 71 | MapStyle.Default -> null 72 | MapStyle.Night -> NightStyle 73 | MapStyle.Retro -> MapTypeStyles.fromString(RetroStyleString) 74 | } 75 | ) 76 | } 77 | 78 | CameraRow(cameraPositionState) 79 | 80 | // MapLayerRow(mapLayerState) 81 | 82 | MapStyleRow(mapStyle) { 83 | mapStyle = it 84 | } 85 | 86 | Div({ 87 | style { 88 | display(DisplayStyle.Flex) 89 | flexDirection(FlexDirection.Row) 90 | flex(1) 91 | } 92 | }) { 93 | MapContent( 94 | apiKey = apiKey, 95 | mapOptions = mapOptions, 96 | cameraPositionState = cameraPositionState, 97 | listenerState = mapListenerState, 98 | ) 99 | 100 | ListenerColumn(mapListenerState) 101 | } 102 | } 103 | 104 | @Composable 105 | private fun CameraRow( 106 | state: CameraPositionState 107 | ) { 108 | Div({ 109 | style { margin(5.px) } 110 | }) { 111 | Span({ 112 | style { marginRight(10.px) } 113 | }) { 114 | Text("Camera Position: ${state.position.center.asString()}") 115 | } 116 | Button( 117 | attrs = { 118 | onClick { 119 | val currentCenter = state.position.center 120 | val randomRange = 0.5 121 | state.position = state.position.copy( 122 | center = LatLng( 123 | currentCenter.lat + Random.nextDouble(-randomRange, randomRange), 124 | currentCenter.lng + Random.nextDouble(-randomRange, randomRange), 125 | ) 126 | ) 127 | } 128 | } 129 | ) { 130 | Text("Move Camera Randomly") 131 | } 132 | } 133 | } 134 | 135 | @OptIn(ExperimentalComposeWebApi::class) 136 | @Composable 137 | private fun MapStyleRow( 138 | style: MapStyle, 139 | onStyleChange: (MapStyle) -> Unit, 140 | ) { 141 | Div({ 142 | style { margin(5.px) } 143 | }) { 144 | Span({ 145 | style { marginRight(10.px) } 146 | }) { 147 | Text("Map Style: ") 148 | } 149 | RadioGroup(checkedValue = style.name) { 150 | RadioInput(value = "Default") { 151 | onClick { onStyleChange(MapStyle.Default) } 152 | } 153 | Text("Default") 154 | RadioInput(value = "Night") { 155 | onClick { onStyleChange(MapStyle.Night) } 156 | } 157 | Text("Night") 158 | RadioInput(value = "Retro") { 159 | onClick { onStyleChange(MapStyle.Retro) } 160 | } 161 | Text("Retro") 162 | } 163 | } 164 | } 165 | 166 | @Composable 167 | private fun MapContent( 168 | apiKey: String, 169 | cameraPositionState: CameraPositionState, 170 | mapOptions: MapOptions, 171 | listenerState: MapListenerState, 172 | ) { 173 | GoogleMap( 174 | apiKey = apiKey, 175 | cameraPositionState = cameraPositionState, 176 | mapOptions = mapOptions, 177 | attrs = { 178 | style { 179 | width(90.percent) 180 | } 181 | }, 182 | onBoundsChanged = { listenerState.boundsChanged++ }, 183 | onCenterChanged = { listenerState.centerChanged++ }, 184 | onContextMenu = { listenerState.contextMenu++ }, 185 | onClick = { listenerState.click++ }, 186 | onDoubleClick = { listenerState.dblClick++ }, 187 | onDrag = { listenerState.drag++ }, 188 | onDragEnd = { listenerState.dragEnd++ }, 189 | onDragStart = { listenerState.dragStart++ }, 190 | onHeadingChanged = { listenerState.headingChanged++ }, 191 | onIdle = { listenerState.idle++ }, 192 | onMapTypeIdChanged = { listenerState.mapTypeIdChanged++ }, 193 | onMouseMove = { listenerState.mouseMove++ }, 194 | onMouseOut = { listenerState.mouseOut++ }, 195 | onMouseOver = { listenerState.mouseOver++ }, 196 | onProjectionChanged = { listenerState.projectionChanged++ }, 197 | onRenderingTypeChanged = { listenerState.renderingTypeChanged++ }, 198 | onTilesLoaded = { listenerState.tilesLoaded++ }, 199 | onTiltChanged = { listenerState.tiltChanged++ }, 200 | onZoomChanged = { listenerState.zoomChanged++ }, 201 | ) {} 202 | } 203 | 204 | @Composable 205 | private fun ListenerColumn( 206 | state: MapListenerState, 207 | ) { 208 | Div({ 209 | style { 210 | width(10.percent) 211 | display(DisplayStyle.Flex) 212 | flexDirection(FlexDirection.Column) 213 | justifyContent(JustifyContent.SpaceBetween) 214 | } 215 | }) { 216 | ListenerState("Bounds Changed", state.boundsChanged) 217 | ListenerState("Center Changed", state.centerChanged) 218 | ListenerState("Context Menu", state.contextMenu) 219 | ListenerState("Click", state.click) 220 | ListenerState("Double Click", state.dblClick) 221 | ListenerState("Drag", state.drag) 222 | ListenerState("Drag End", state.dragEnd) 223 | ListenerState("Drag Start", state.dragStart) 224 | ListenerState("Heading Changed", state.headingChanged) 225 | ListenerState("Idle", state.idle) 226 | ListenerState("Map Type Id Changed", state.mapTypeIdChanged) 227 | ListenerState("Mouse Move", state.mouseMove) 228 | ListenerState("Mouse Out", state.mouseOut) 229 | ListenerState("Mouse Over", state.mouseOver) 230 | ListenerState("Projection Changed", state.projectionChanged) 231 | ListenerState("Rendering Type Changed", state.renderingTypeChanged) 232 | ListenerState("Tiles Loaded", state.tilesLoaded) 233 | ListenerState("Tilt Changed", state.tiltChanged) 234 | ListenerState("Zoom Changed", state.zoomChanged) 235 | } 236 | } 237 | 238 | @Composable 239 | private fun ListenerState(event: String, count: Int) { 240 | var triggered by remember { mutableStateOf(false) } 241 | LaunchedEffect(count) { 242 | triggered = true 243 | delay(1000) 244 | triggered = false 245 | } 246 | Span({ 247 | style { 248 | if (triggered) { 249 | background("#ff0000") 250 | } 251 | } 252 | }) { 253 | Text(event) 254 | } 255 | } 256 | 257 | private val NightStyle = MapTypeStyles( 258 | listOf( 259 | MapTypeStyle( 260 | elementType = "geometry", 261 | stylers = listOf(jso { color = "#242f3e" }) 262 | ), 263 | MapTypeStyle( 264 | elementType = "labels.text.stroke", 265 | stylers = listOf(jso { color = "#242f3e" }) 266 | ), 267 | MapTypeStyle( 268 | elementType = "labels.text.fill", 269 | stylers = listOf(jso { color = "#746855" }) 270 | ), 271 | MapTypeStyle( 272 | featureType = "administrative.locality", 273 | elementType = "labels.text.fill", 274 | stylers = listOf(jso { color = "#d59563" }) 275 | ), 276 | MapTypeStyle( 277 | featureType = "poi", 278 | elementType = "labels.text.fill", 279 | stylers = listOf(jso { color = "#d59563" }) 280 | ), 281 | MapTypeStyle( 282 | featureType = "poi.park", 283 | elementType = "geometry", 284 | stylers = listOf(jso { color = "#263c3f" }) 285 | ), 286 | MapTypeStyle( 287 | featureType = "poi.park", 288 | elementType = "labels.text.fill", 289 | stylers = listOf(jso { color = "#6b9a76" }) 290 | ), 291 | MapTypeStyle( 292 | featureType = "road", 293 | elementType = "geometry", 294 | stylers = listOf(jso { color = "#38414e" }) 295 | ), 296 | MapTypeStyle( 297 | featureType = "road", 298 | elementType = "geometry.stroke", 299 | stylers = listOf(jso { color = "#212a37" }) 300 | ), 301 | MapTypeStyle( 302 | featureType = "road", 303 | elementType = "labels.text.fill", 304 | stylers = listOf(jso { color = "#9ca5b3" }) 305 | ), 306 | MapTypeStyle( 307 | featureType = "road.highway", 308 | elementType = "geometry", 309 | stylers = listOf(jso { color = "#746855" }) 310 | ), 311 | MapTypeStyle( 312 | featureType = "road.highway", 313 | elementType = "geometry.stroke", 314 | stylers = listOf(jso { color = "#1f2835" }) 315 | ), 316 | MapTypeStyle( 317 | featureType = "road.highway", 318 | elementType = "labels.text.fill", 319 | stylers = listOf(jso { color = "#f3d19c" }) 320 | ), 321 | MapTypeStyle( 322 | featureType = "transit", 323 | elementType = "geometry", 324 | stylers = listOf(jso { color = "#2f3948" }) 325 | ), 326 | MapTypeStyle( 327 | featureType = "transit.station", 328 | elementType = "labels.text.fill", 329 | stylers = listOf(jso { color = "#d59563" }) 330 | ), 331 | MapTypeStyle( 332 | featureType = "water", 333 | elementType = "geometry", 334 | stylers = listOf(jso { color = "#17263c" }) 335 | ), 336 | MapTypeStyle( 337 | featureType = "water", 338 | elementType = "labels.text.fill", 339 | stylers = listOf(jso { color = "#515c6d" }) 340 | ), 341 | MapTypeStyle( 342 | featureType = "water", 343 | elementType = "labels.text.stroke", 344 | stylers = listOf(jso { color = "#17263c" }) 345 | ) 346 | ) 347 | ) 348 | 349 | private const val RetroStyleString = """[ 350 | {"elementType": "geometry", "stylers": [{"color": "#ebe3cd"}]}, 351 | {"elementType": "labels.text.fill", "stylers": [{"color": "#523735"}]}, 352 | {"elementType": "labels.text.stroke", "stylers": [{"color": "#f5f1e6"}]}, 353 | { 354 | "featureType": "administrative", 355 | "elementType": "geometry.stroke", 356 | "stylers": [{"color": "#c9b2a6"}] 357 | }, 358 | { 359 | "featureType": "administrative.land_parcel", 360 | "elementType": "geometry.stroke", 361 | "stylers": [{"color": "#dcd2be"}] 362 | }, 363 | { 364 | "featureType": "administrative.land_parcel", 365 | "elementType": "labels.text.fill", 366 | "stylers": [{"color": "#ae9e90"}] 367 | }, 368 | { 369 | "featureType": "landscape.natural", 370 | "elementType": "geometry", 371 | "stylers": [{"color": "#dfd2ae"}] 372 | }, 373 | { 374 | "featureType": "poi", 375 | "elementType": "geometry", 376 | "stylers": [{"color": "#dfd2ae"}] 377 | }, 378 | { 379 | "featureType": "poi", 380 | "elementType": "labels.text.fill", 381 | "stylers": [{"color": "#93817c"}] 382 | }, 383 | { 384 | "featureType": "poi.park", 385 | "elementType": "geometry.fill", 386 | "stylers": [{"color": "#a5b076"}] 387 | }, 388 | { 389 | "featureType": "poi.park", 390 | "elementType": "labels.text.fill", 391 | "stylers": [{"color": "#447530"}] 392 | }, 393 | { 394 | "featureType": "road", 395 | "elementType": "geometry", 396 | "stylers": [{"color": "#f5f1e6"}] 397 | }, 398 | { 399 | "featureType": "road.arterial", 400 | "elementType": "geometry", 401 | "stylers": [{"color": "#fdfcf8"}] 402 | }, 403 | { 404 | "featureType": "road.highway", 405 | "elementType": "geometry", 406 | "stylers": [{"color": "#f8c967"}] 407 | }, 408 | { 409 | "featureType": "road.highway", 410 | "elementType": "geometry.stroke", 411 | "stylers": [{"color": "#e9bc62"}] 412 | }, 413 | { 414 | "featureType": "road.highway.controlled_access", 415 | "elementType": "geometry", 416 | "stylers": [{"color": "#e98d58"}] 417 | }, 418 | { 419 | "featureType": "road.highway.controlled_access", 420 | "elementType": "geometry.stroke", 421 | "stylers": [{"color": "#db8555"}] 422 | }, 423 | { 424 | "featureType": "road.local", 425 | "elementType": "labels.text.fill", 426 | "stylers": [{"color": "#806b63"}] 427 | }, 428 | { 429 | "featureType": "transit.line", 430 | "elementType": "geometry", 431 | "stylers": [{"color": "#dfd2ae"}] 432 | }, 433 | { 434 | "featureType": "transit.line", 435 | "elementType": "labels.text.fill", 436 | "stylers": [{"color": "#8f7d77"}] 437 | }, 438 | { 439 | "featureType": "transit.line", 440 | "elementType": "labels.text.stroke", 441 | "stylers": [{"color": "#ebe3cd"}] 442 | }, 443 | { 444 | "featureType": "transit.station", 445 | "elementType": "geometry", 446 | "stylers": [{"color": "#dfd2ae"}] 447 | }, 448 | { 449 | "featureType": "water", 450 | "elementType": "geometry.fill", 451 | "stylers": [{"color": "#b9d3c2"}] 452 | }, 453 | { 454 | "featureType": "water", 455 | "elementType": "labels.text.fill", 456 | "stylers": [{"color": "#92998d"}] 457 | }] 458 | """ -------------------------------------------------------------------------------- /example/src/jsMain/kotlin/LayerExample.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.* 2 | import com.chihsuanwu.maps.compose.web.CameraPosition 3 | import com.chihsuanwu.maps.compose.web.GoogleMap 4 | import com.chihsuanwu.maps.compose.web.LatLng 5 | import com.chihsuanwu.maps.compose.web.layers.* 6 | import com.chihsuanwu.maps.compose.web.rememberCameraPositionState 7 | import org.jetbrains.compose.web.css.margin 8 | import org.jetbrains.compose.web.css.px 9 | import org.jetbrains.compose.web.dom.Button 10 | import org.jetbrains.compose.web.dom.Div 11 | import org.jetbrains.compose.web.dom.Text 12 | 13 | private class MapLayerState { 14 | var bicyclingLayer: Boolean by mutableStateOf(false) 15 | var trafficLayer: Boolean by mutableStateOf(false) 16 | var transitLayer: Boolean by mutableStateOf(false) 17 | var heatmapLayer: Boolean by mutableStateOf(false) 18 | var kmlLayer: Boolean by mutableStateOf(false) 19 | } 20 | 21 | @Composable 22 | fun LayerExample(apiKey: String) { 23 | 24 | val layerState: MapLayerState by remember { mutableStateOf(MapLayerState()) } 25 | 26 | MapLayerRow(layerState) 27 | CustomLayerRow(layerState) 28 | 29 | GoogleMap( 30 | apiKey = apiKey, 31 | cameraPositionState = rememberCameraPositionState { 32 | position = CameraPosition( 33 | center = LatLng(37.76, -122.44), 34 | zoom = 11.0, 35 | ) 36 | }, 37 | extra = "libraries=visualization" 38 | ) { 39 | if (layerState.bicyclingLayer) { 40 | BicyclingLayer() 41 | } 42 | if (layerState.trafficLayer) { 43 | TrafficLayer() 44 | } 45 | if (layerState.transitLayer) { 46 | TransitLayer() 47 | } 48 | if (layerState.heatmapLayer) { 49 | HeatmapLayer( 50 | data = listOf( 51 | WeightedLocation( 52 | LatLng(37.782, -122.447), 0.5 53 | ), 54 | WeightedLocation( 55 | LatLng(37.782, -122.445), 0.7 56 | ), 57 | WeightedLocation( 58 | LatLng(37.782, -122.443), 0.8 59 | ), 60 | WeightedLocation( 61 | LatLng(37.782, -122.441), 0.9 62 | ), 63 | WeightedLocation( 64 | LatLng(37.782, -122.439), 1.0 65 | ), 66 | WeightedLocation( 67 | LatLng(37.782, -122.437), 0.7 68 | ), 69 | WeightedLocation( 70 | LatLng(37.782, -122.435), 0.8 71 | ), 72 | ), 73 | ) 74 | } 75 | if (layerState.kmlLayer) { 76 | KMLLayer( 77 | url = "https://api.flickr.com/services/feeds/geo/?g=322338@N20&lang=en-us&format=feed-georss", 78 | onClick = { 79 | console.log("KMLLayer clicked, ${it.featureData.author.name}") 80 | } 81 | ) 82 | } 83 | } 84 | } 85 | 86 | @Composable 87 | private fun MapLayerRow( 88 | state: MapLayerState 89 | ) { 90 | Div({ 91 | style { margin(5.px) } 92 | }) { 93 | MapLayerToggleButton( 94 | text = "BicyclingLayer", 95 | state = state.bicyclingLayer, 96 | onClick = { state.bicyclingLayer = !state.bicyclingLayer } 97 | ) 98 | MapLayerToggleButton( 99 | text = "TrafficLayer", 100 | state = state.trafficLayer, 101 | onClick = { state.trafficLayer = !state.trafficLayer } 102 | ) 103 | MapLayerToggleButton( 104 | text = "TransitLayer", 105 | state = state.transitLayer, 106 | onClick = { state.transitLayer = !state.transitLayer } 107 | ) 108 | } 109 | } 110 | 111 | @Composable 112 | private fun CustomLayerRow( 113 | state: MapLayerState 114 | ) { 115 | Div({ 116 | style { margin(5.px) } 117 | }) { 118 | MapLayerToggleButton( 119 | text = "HeatmapLayer", 120 | state = state.heatmapLayer, 121 | onClick = { state.heatmapLayer = !state.heatmapLayer } 122 | ) 123 | MapLayerToggleButton( 124 | text = "KMLLayer", 125 | state = state.kmlLayer, 126 | onClick = { state.kmlLayer = !state.kmlLayer } 127 | ) 128 | } 129 | } 130 | 131 | @Composable 132 | private fun MapLayerToggleButton( 133 | text: String, 134 | state: Boolean, 135 | onClick: () -> Unit 136 | ) { 137 | Button({ 138 | style { margin(5.px) } 139 | onClick { onClick() } 140 | }) { 141 | Text("$text: $state") 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /example/src/jsMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.* 2 | import app.softwork.routingcompose.HashRouter 3 | import app.softwork.routingcompose.Router 4 | import org.jetbrains.compose.web.attributes.InputType 5 | import org.jetbrains.compose.web.css.* 6 | import org.jetbrains.compose.web.dom.* 7 | import org.jetbrains.compose.web.renderComposable 8 | 9 | fun main() { 10 | renderComposable(rootElementId = "root") { 11 | MainPage() 12 | } 13 | } 14 | 15 | @Composable 16 | private fun MainPage() { 17 | var apiKey: String by remember { mutableStateOf("") } 18 | 19 | var setApiKeyClicked by remember { mutableStateOf(false) } 20 | 21 | Div({ 22 | style { 23 | textAlign("center") 24 | display(DisplayStyle.Flex) 25 | flexDirection(FlexDirection.Column) 26 | height(100.vh - 66.px) 27 | padding(25.px) 28 | } 29 | }) { 30 | Span( 31 | attrs = { 32 | style { 33 | color(Color("#0000ff")) 34 | fontSize(2.em) 35 | marginBottom(20.px) 36 | } 37 | } 38 | ) { 39 | Text("Hello, Google Map Compose Web!!") 40 | } 41 | 42 | HashRouter(initPath = "/") { 43 | if (setApiKeyClicked) { 44 | route("/") { 45 | Navigator() 46 | } 47 | route("/map_example") { 48 | MapExample(apiKey = apiKey) 49 | } 50 | route("/drawing_example") { 51 | DrawingExample(apiKey = apiKey) 52 | } 53 | route("/layer_example") { 54 | LayerExample(apiKey = apiKey) 55 | } 56 | } else { 57 | KeyRow( 58 | apiKey = apiKey, 59 | onInput = { apiKey = it }, 60 | onClick = { setApiKeyClicked = true } 61 | ) 62 | } 63 | } 64 | } 65 | } 66 | 67 | @Composable 68 | private fun KeyRow( 69 | apiKey: String, 70 | onInput : (String) -> Unit, 71 | onClick: () -> Unit 72 | ) { 73 | Div( 74 | attrs = { 75 | style { 76 | margin(10.px) 77 | } 78 | } 79 | ) { 80 | Input( 81 | type = InputType.Text, 82 | attrs = { 83 | attr("placeholder", "Enter your API key here") 84 | value(apiKey) 85 | onInput { event -> 86 | onInput(event.value) 87 | } 88 | style { 89 | display(DisplayStyle.Inline) 90 | padding(4.px) 91 | } 92 | } 93 | ) 94 | 95 | Button( 96 | attrs = { 97 | style { 98 | margin(10.px) 99 | } 100 | onClick { 101 | onClick() 102 | } 103 | } 104 | ) { 105 | Text("Set API Key") 106 | } 107 | } 108 | } 109 | 110 | @Composable 111 | private fun Navigator() { 112 | Div({ 113 | style { 114 | display(DisplayStyle.Flex) 115 | flexDirection(FlexDirection.Row) 116 | justifyContent(JustifyContent.Center) 117 | alignItems(AlignItems.Center) 118 | width(100.percent) 119 | height(100.percent) 120 | } 121 | }) { 122 | val router = Router.current 123 | NavCard("Google Map Example") { 124 | router.navigate("/map_example") 125 | } 126 | NavCard("Drawing Example") { 127 | router.navigate("/drawing_example") 128 | } 129 | NavCard("Layer Example") { 130 | router.navigate("/layer_example") 131 | } 132 | } 133 | } 134 | 135 | @Composable 136 | private fun NavCard( 137 | text: String, 138 | onClick: () -> Unit 139 | ) { 140 | Button({ 141 | style { 142 | width(240.px) 143 | height(160.px) 144 | margin(20.px) 145 | fontSize(1.5.em) 146 | backgroundColor(Color("#ffffff")) 147 | borderRadius(10.px) 148 | borderWidth(1.px) 149 | property("box-shadow", "0 0 5px 0 #222222") 150 | } 151 | onClick { 152 | onClick() 153 | } 154 | }) { 155 | Text(text) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /example/src/jsMain/kotlin/Utils.kt: -------------------------------------------------------------------------------- 1 | import com.chihsuanwu.maps.compose.web.LatLng 2 | 3 | fun LatLng.asString(): String { 4 | return "(${this.lat.format(5)}, ${this.lng.format(5)})" 5 | } 6 | 7 | fun String.decodePath(): List { 8 | val encodedPath = this 9 | val result = js("google.maps.geometry.encoding.decodePath(encodedPath)") as Array 10 | return result.map { LatLng(it.lat() as Double, it.lng() as Double) } 11 | } 12 | 13 | private fun Double.format(digits: Int) = this.asDynamic().toFixed(digits) as String 14 | -------------------------------------------------------------------------------- /example/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.js.webpack.major.version=5 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chihsuanwu/google-maps-compose-web/be5a689d2fc9ae62baf40169fae6727fd18f2ae9/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.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original 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 POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /jitpack.yml: -------------------------------------------------------------------------------- 1 | jdk: 2 | - openjdk11 3 | -------------------------------------------------------------------------------- /maps-compose-web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("org.jetbrains.compose") 4 | 5 | id("maven-publish") 6 | } 7 | 8 | group = "com.github.chihsuanwu" 9 | 10 | fun kotlinw(target: String): String = "org.jetbrains.kotlin-wrappers:kotlin-$target" 11 | 12 | val kotlinWrappersVersion = "1.0.0-pre.624" 13 | 14 | kotlin { 15 | jvmToolchain(11) 16 | 17 | js(IR) { 18 | browser { 19 | testTask { 20 | testLogging.showStandardStreams = true 21 | useKarma { 22 | useChromeHeadless() 23 | useFirefox() 24 | } 25 | } 26 | } 27 | binaries.executable() 28 | } 29 | sourceSets { 30 | val jsMain by getting { 31 | dependencies { 32 | implementation(kotlin("stdlib-js")) 33 | implementation(kotlinw("js")) 34 | 35 | implementation(compose.html.core) 36 | implementation(compose.runtime) 37 | } 38 | } 39 | val jsTest by getting { 40 | dependencies { 41 | implementation(kotlin("test-js")) 42 | } 43 | } 44 | } 45 | } 46 | 47 | dependencies { 48 | "jsMainImplementation"(enforcedPlatform(kotlinw("wrappers-bom:$kotlinWrappersVersion"))) 49 | } 50 | 51 | tasks.withType { 52 | // The value 'enforced-platform' is provided in the validation 53 | // error message you got 54 | suppressedValidationErrors.add("enforced-platform") 55 | } 56 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/CameraPositionState.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | import androidx.compose.runtime.* 4 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 5 | import com.chihsuanwu.maps.compose.web.jsobject.toCameraOptions 6 | 7 | /** 8 | * A [CameraPosition] describes the position of the camera on the map. 9 | * 10 | * @param center the center of the camera 11 | * @param zoom the zoom level of the camera 12 | * @param tilt the tilt of the camera 13 | * @param heading the heading of the camera 14 | */ 15 | data class CameraPosition( 16 | val center: LatLng, 17 | val zoom: Double, 18 | val tilt: Double = 0.0, 19 | val heading: Double = 0.0, 20 | ) 21 | 22 | /** 23 | * Create and [remember] a [CameraPositionState]. 24 | */ 25 | @Composable 26 | inline fun rememberCameraPositionState( 27 | crossinline init: CameraPositionState.() -> Unit = {}, 28 | ): CameraPositionState = remember { 29 | CameraPositionState().apply(init) 30 | } 31 | 32 | /** 33 | * State object that can be hoisted to control and observe the camera position of a [GoogleMap]. 34 | */ 35 | class CameraPositionState( 36 | position: CameraPosition = CameraPosition( 37 | center = LatLng(0.0, 0.0), 38 | zoom = 0.0, 39 | ) 40 | ) { 41 | /** 42 | * Whether the camera is currently moving or not. 43 | */ 44 | var isMoving: Boolean by mutableStateOf(false) 45 | internal set 46 | 47 | 48 | internal var rawPosition by mutableStateOf(position) 49 | 50 | /** 51 | * Current position of the camera on the map. 52 | */ 53 | var position: CameraPosition 54 | get() = rawPosition 55 | set(value) { 56 | val map = map 57 | if (map == null) { 58 | rawPosition = value 59 | } else { 60 | console.log("moveCamera to ${value.center.lat}, ${value.center.lng}}") 61 | map.moveCamera(value.toCameraOptions()) 62 | } 63 | } 64 | 65 | private var map: MapView? by mutableStateOf(null) 66 | 67 | internal fun setMap(map: MapView?) { 68 | if (this.map == null && map == null) return 69 | if (this.map != null && map != null) { 70 | error("CameraPositionState may only be associated with one GoogleMap at a time") 71 | } 72 | this.map = map 73 | if (map == null) { 74 | isMoving = false 75 | } else { 76 | map.moveCamera(position.toCameraOptions()) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/Coordinates.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | 4 | class LatLng( 5 | val lat: Double, 6 | val lng: Double, 7 | ) 8 | 9 | class LatLngBounds( 10 | val east: Double, 11 | val north: Double, 12 | val south: Double, 13 | val west: Double, 14 | ) 15 | 16 | class Point( 17 | val x: Double, 18 | val y: Double 19 | ) 20 | 21 | class Size( 22 | val height: Double, 23 | val width: Double 24 | ) 25 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/Events.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | import com.chihsuanwu.maps.compose.web.layers.KMLFeatureData 4 | 5 | 6 | open class MapMouseEvent( 7 | val latLng: LatLng 8 | ) 9 | 10 | class IconMouseEvent( 11 | latLng: LatLng, 12 | val placeId: String 13 | ) : MapMouseEvent(latLng) 14 | 15 | class PolyMouseEvent( 16 | latLng: LatLng, 17 | val edge: Int, 18 | val path: Int, 19 | val vertex: Int 20 | ) : MapMouseEvent(latLng) 21 | 22 | class KMLMouseEvent( 23 | latLng: LatLng, 24 | val featureData: KMLFeatureData, 25 | val pixelOffset: Size, 26 | ) : MapMouseEvent(latLng) 27 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/GoogleMap.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | import androidx.compose.runtime.* 4 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 5 | import com.chihsuanwu.maps.compose.web.jsobject.newMap 6 | import com.chihsuanwu.maps.compose.web.jsobject.toJsMapOptions 7 | import kotlinx.browser.document 8 | import kotlinx.browser.window 9 | import kotlinx.coroutines.awaitCancellation 10 | import org.jetbrains.compose.web.css.height 11 | import org.jetbrains.compose.web.css.percent 12 | import org.jetbrains.compose.web.css.width 13 | import org.jetbrains.compose.web.dom.AttrBuilderContext 14 | import org.jetbrains.compose.web.dom.Div 15 | import org.w3c.dom.HTMLDivElement 16 | 17 | /** 18 | * A compose container for a [MapView]. 19 | * 20 | * @param apiKey the API key to use for the map. 21 | * @param cameraPositionState the [CameraPositionState] to be used to control or observe the map's 22 | * camera state 23 | * @param mapOptions the [MapOptions] to be used to configure the map. 24 | * @param id The id of the element to be used as the map container. 25 | * @param extra The extra parameters to be appended to the Google Maps API URL. For example, you can 26 | * add `"libraries=geometry"` to load the geometry library. 27 | * @param attrs The attributes to be applied to the map container. 28 | * 29 | * @param onBoundsChanged Called when the viewport bounds have changed. 30 | * @param onCenterChanged Called when the map center property changes. 31 | * @param onClick The click listener to be applied to the map. 32 | * A [MapMouseEvent] will be passed to the listener unless a place icon was clicked, in which case an 33 | * [IconMouseEvent] will be passed to the listener. 34 | * @param onContextMenu Called when the DOM contextmenu event is fired on the map container. 35 | * @param onDoubleClick Called when the DOM dblclick event is fired on the map container. 36 | * @param onDrag Called repeatedly while the user drags the map. 37 | * @param onDragEnd Called when the user stops dragging the map. 38 | * @param onDragStart Called when the user starts dragging the map. 39 | * @param onHeadingChanged Called when the map heading property changes. 40 | * @param onIdle Called when the map becomes idle after panning or zooming. 41 | * @param onIsFractionalZoomEnabledChanged Called when the map isFractionalZoomEnabled property changes. 42 | * @param onMapTypeIdChanged Called when the map mapTypeId property changes. 43 | * @param onMouseMove Called when the user's mouse moves over the map container. 44 | * @param onMouseOut Called when the user's mouse exits the map container. 45 | * @param onMouseOver Called when the user's mouse enters the map container. 46 | * @param onProjectionChanged Called when the projection has changed. 47 | * @param onRenderingTypeChanged Called when the renderingType property changes. 48 | * @param onTilesLoaded Called when the visible tiles have finished loading. 49 | * @param onTiltChanged Called when the map tilt property changes. 50 | * @param onZoomChanged Called when the map zoom property changes. 51 | * 52 | * @param content the content of the map 53 | */ 54 | @Composable 55 | fun GoogleMap( 56 | apiKey: String?, 57 | cameraPositionState: CameraPositionState = rememberCameraPositionState(), 58 | mapOptions: MapOptions = MapOptions(), 59 | id: String = "map", 60 | extra: String? = null, 61 | attrs: AttrBuilderContext? = null, 62 | onBoundsChanged: () -> Unit = {}, 63 | onCenterChanged: () -> Unit = {}, 64 | onClick: (MapMouseEvent) -> Unit = {}, 65 | onContextMenu: (MapMouseEvent) -> Unit = {}, 66 | onDoubleClick: (MapMouseEvent) -> Unit = {}, 67 | onDrag: () -> Unit = {}, 68 | onDragEnd: () -> Unit = {}, 69 | onDragStart: () -> Unit = {}, 70 | onHeadingChanged: () -> Unit = {}, 71 | onIdle: () -> Unit = {}, 72 | onIsFractionalZoomEnabledChanged: () -> Unit = {}, 73 | onMapTypeIdChanged: () -> Unit = {}, 74 | onMouseMove: (MapMouseEvent) -> Unit = {}, 75 | onMouseOut: (MapMouseEvent) -> Unit = {}, 76 | onMouseOver: (MapMouseEvent) -> Unit = {}, 77 | onProjectionChanged: () -> Unit = {}, 78 | onRenderingTypeChanged: () -> Unit = {}, 79 | onTilesLoaded: () -> Unit = {}, 80 | onTiltChanged: () -> Unit = {}, 81 | onZoomChanged: () -> Unit = {}, 82 | content: @Composable (() -> Unit)? = null, 83 | ) { 84 | var map: MapView? by remember { mutableStateOf(null) } 85 | 86 | LaunchedEffect(Unit) { 87 | val script = document.createElement("script").apply { 88 | val src = StringBuilder("https://maps.googleapis.com/maps/api/js?") 89 | apiKey?.let { src.append("key=$it") } 90 | src.append("&callback=initMap") 91 | extra?.let { src.append("&$it") } 92 | this.asDynamic().src = src 93 | this.asDynamic().async = true 94 | } 95 | document.head?.appendChild(script) 96 | } 97 | 98 | window.asDynamic().initMap = { 99 | map = newMap(id = id, options = mapOptions.toJsMapOptions()) 100 | } 101 | 102 | val currentCameraPositionState by rememberUpdatedState(cameraPositionState) 103 | val currentMapOptions by rememberUpdatedState(mapOptions) 104 | 105 | val parentComposition = rememberCompositionContext() 106 | val currentContent by rememberUpdatedState(content) 107 | 108 | LaunchedEffect(map) { 109 | val currentMap = map 110 | if (currentMap != null) { 111 | disposingComposition { 112 | currentMap.newComposition(parentComposition) { 113 | MapUpdater( 114 | cameraPositionState = currentCameraPositionState, 115 | mapOptions = currentMapOptions, 116 | onBoundsChanged = onBoundsChanged, 117 | onCenterChanged = onCenterChanged, 118 | onClick = onClick, 119 | onContextMenu = onContextMenu, 120 | onDoubleClick = onDoubleClick, 121 | onDrag = onDrag, 122 | onDragEnd = onDragEnd, 123 | onDragStart = onDragStart, 124 | onHeadingChanged = onHeadingChanged, 125 | onIdle = onIdle, 126 | onIsFractionalZoomEnabledChanged = onIsFractionalZoomEnabledChanged, 127 | onMapTypeIdChanged = onMapTypeIdChanged, 128 | onMouseMove = onMouseMove, 129 | onMouseOut = onMouseOut, 130 | onMouseOver = onMouseOver, 131 | onProjectionChanged = onProjectionChanged, 132 | onRenderingTypeChanged = onRenderingTypeChanged, 133 | onTilesLoaded = onTilesLoaded, 134 | onTiltChanged = onTiltChanged, 135 | onZoomChanged = onZoomChanged, 136 | ) 137 | currentContent?.invoke() 138 | } 139 | } 140 | } 141 | } 142 | 143 | // The container for the map 144 | Div( 145 | attrs = { 146 | id(id) 147 | style { 148 | width(100.percent) 149 | height(100.percent) 150 | } 151 | attrs?.invoke(this) 152 | } 153 | ) 154 | } 155 | 156 | internal suspend inline fun disposingComposition(factory: () -> Composition) { 157 | val composition = factory() 158 | try { 159 | awaitCancellation() 160 | } finally { 161 | composition.dispose() 162 | } 163 | } 164 | 165 | private inline fun MapView.newComposition( 166 | parent: CompositionContext, 167 | noinline content: @Composable () -> Unit 168 | ): Composition { 169 | return Composition( 170 | MapApplier(this), parent 171 | ).apply { 172 | setContent(content) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/MapApplier.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | import androidx.compose.runtime.AbstractApplier 4 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 5 | 6 | internal interface MapNode { 7 | fun onAttached() {} 8 | fun onRemoved() {} 9 | fun onCleared() {} 10 | } 11 | 12 | private object MapNodeRoot : MapNode 13 | 14 | internal class MapApplier( 15 | val map: MapView, 16 | ): AbstractApplier(MapNodeRoot) { 17 | 18 | private val decorations = mutableListOf() 19 | 20 | override fun insertBottomUp(index: Int, instance: MapNode) { 21 | decorations.add(index, instance) 22 | instance.onAttached() 23 | } 24 | 25 | override fun insertTopDown(index: Int, instance: MapNode) { } 26 | 27 | override fun remove(index: Int, count: Int) { 28 | repeat(count) { 29 | decorations[index + it].onRemoved() 30 | } 31 | decorations.remove(index, count) 32 | } 33 | 34 | override fun move(from: Int, to: Int, count: Int) { 35 | decorations.move(from, to, count) 36 | } 37 | 38 | override fun onClear() { 39 | decorations.forEach { it.onCleared() } 40 | decorations.clear() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/MapOptions.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | /** 4 | * Data class for configuring [GoogleMap] options 5 | * 6 | * See [MapOptions](https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions) 7 | * for more details 8 | * 9 | * Note that some attributes that are duplicated in other controllers are not included here 10 | */ 11 | data class MapOptions( 12 | val backgroundColor: String? = null, 13 | val clickableIcons: Boolean? = null, 14 | val controlSize: Int? = null, 15 | val disableDefaultUI: Boolean? = null, 16 | val disableDoubleClickZoom: Boolean? = null, 17 | val draggableCursor: String? = null, 18 | val draggingCursor: String? = null, 19 | val fullscreenControl: Boolean? = null, 20 | val fullscreenControlOptions: FullscreenControlOptions? = null, 21 | val gestureHandling: String? = null, 22 | val isFractionalZoomEnabled: Boolean? = null, 23 | val keyboardShortcuts: Boolean? = null, 24 | val mapId: String? = null, 25 | val mapTypeControl: Boolean? = null, 26 | val mapTypeControlOptions: MapTypeControlOptions? = null, 27 | val mapTypeId: MapTypeId? = null, 28 | val maxZoom: Double? = null, 29 | val minZoom: Double? = null, 30 | val noClear: Boolean? = null, 31 | val panControl: Boolean? = null, 32 | val panControlOptions: PanControlOptions? = null, 33 | val restriction: MapRestriction? = null, 34 | val rotateControl: Boolean? = null, 35 | val rotateControlOptions: RotateControlOptions? = null, 36 | val scaleControl: Boolean? = null, 37 | val scaleControlOptions: dynamic = null, 38 | val scrollwheel: Boolean? = null, 39 | val streetView: dynamic = null, 40 | val streetViewControl: Boolean? = null, 41 | val streetViewControlOptions: StreetViewControlOptions? = null, 42 | val styles: MapTypeStyles? = null, 43 | val zoomControl: Boolean? = null, 44 | val zoomControlOptions: ZoomControlOptions? = null, 45 | ) 46 | 47 | /** 48 | * Wrapper class for [MapOptions.styles] to allow for easier JSON parsing. 49 | * 50 | * See [MapTypeStyle](https://developers.google.com/maps/documentation/javascript/reference/map#MapTypeStyle) 51 | * for more details 52 | */ 53 | class MapTypeStyles( 54 | internal val styles: List, 55 | ) { 56 | companion object { 57 | /** 58 | * Create a [MapTypeStyles] from a JSON string. 59 | */ 60 | fun fromString(jsonString: String): MapTypeStyles { 61 | val array = js("JSON.parse(jsonString)") 62 | return (array as Array).map { 63 | MapTypeStyle( 64 | elementType = it.elementType as? String, 65 | featureType = it.featureType as? String, 66 | stylers = (it.stylers as? Array<*>)?.asList(), 67 | ) 68 | }.let { MapTypeStyles(it) } 69 | } 70 | } 71 | } 72 | 73 | class MapTypeStyle( 74 | val elementType: String? = null, 75 | val featureType: String? = null, 76 | val stylers: List? = null, 77 | ) 78 | 79 | enum class MapTypeId { 80 | Hybrid, 81 | Roadmap, 82 | Satellite, 83 | Terrain, 84 | } 85 | 86 | 87 | /** 88 | * Options for the rendering of the fullscreen control. 89 | * 90 | * See [FullscreenControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#FullscreenControlOptions) 91 | * for more details 92 | */ 93 | class FullscreenControlOptions( 94 | val position: ControlPosition? = null, 95 | ) 96 | 97 | /** 98 | * Options for the rendering of the map type control. 99 | * 100 | * See [MapTypeControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlOptions) 101 | * for more details 102 | */ 103 | class MapTypeControlOptions( 104 | val mapTypeIds: List? = null, 105 | val position: ControlPosition? = null, 106 | val style: MapTypeControlStyle? = null, 107 | ) 108 | 109 | /** 110 | * See [MapTypeControlStyle](https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlStyle) 111 | * for more details 112 | */ 113 | enum class MapTypeControlStyle { 114 | Default, 115 | DropdownMenu, 116 | HorizontalBar, 117 | } 118 | 119 | /** 120 | * Options for the rendering of the pan control. 121 | * 122 | * See [PanControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#PanControlOptions) 123 | * for more details 124 | */ 125 | class PanControlOptions( 126 | val position: ControlPosition? = null, 127 | ) 128 | 129 | /** 130 | * Options for the rendering of the rotate control. 131 | * 132 | * See [RotateControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#RotateControlOptions) 133 | * for more details 134 | */ 135 | class RotateControlOptions( 136 | val position: ControlPosition? = null, 137 | ) 138 | 139 | /** 140 | * Options for the rendering of the street view pegman control on the map. 141 | * 142 | * See [StreetViewControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#StreetViewControlOptions) 143 | * for more details 144 | */ 145 | class StreetViewControlOptions( 146 | val position: ControlPosition? = null, 147 | ) 148 | 149 | /** 150 | * Options for the rendering of the zoom control. 151 | * 152 | * See [ZoomControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#ZoomControlOptions) 153 | * for more details 154 | */ 155 | class ZoomControlOptions( 156 | val position: ControlPosition? = null 157 | ) 158 | 159 | /** 160 | * Used to specify the position of the controls on the map. 161 | * 162 | * See [ControlPosition](https://developers.google.com/maps/documentation/javascript/reference/control#ControlPosition) 163 | * for more details 164 | */ 165 | enum class ControlPosition { 166 | BottomCenter, 167 | BottomLeft, 168 | BottomRight, 169 | LeftBottom, 170 | LeftCenter, 171 | LeftTop, 172 | RightBottom, 173 | RightCenter, 174 | RightTop, 175 | TopCenter, 176 | TopLeft, 177 | TopRight, 178 | } 179 | 180 | data class MapRestriction( 181 | val latLngBounds: LatLngBounds, 182 | val strictBounds: Boolean? = null, 183 | ) -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/MapUpdater.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.jsobject.* 7 | import com.chihsuanwu.maps.compose.web.jsobject.toLatLng 8 | 9 | 10 | internal class MapPropertiesNode( 11 | val map: MapView, 12 | cameraPositionState: CameraPositionState, 13 | var events: MutableMap, 14 | ) : MapNode { 15 | 16 | init { 17 | cameraPositionState.setMap(map) 18 | } 19 | 20 | var cameraPositionState = cameraPositionState 21 | set(value) { 22 | if (value == field) return 23 | field.setMap(null) 24 | field = value 25 | value.setMap(map) 26 | } 27 | 28 | override fun onAttached() { 29 | map.addListener("idle") { 30 | cameraPositionState.isMoving = false 31 | cameraPositionState.rawPosition = CameraPosition( 32 | map.getCenter().toLatLng(), 33 | map.getZoom(), 34 | ) 35 | } 36 | } 37 | 38 | override fun onRemoved() { 39 | cameraPositionState.setMap(null) 40 | } 41 | 42 | override fun onCleared() { 43 | cameraPositionState.setMap(null) 44 | } 45 | } 46 | 47 | @Composable 48 | internal fun MapUpdater( 49 | cameraPositionState: CameraPositionState, 50 | mapOptions: MapOptions, 51 | onBoundsChanged: () -> Unit, 52 | onCenterChanged: () -> Unit, 53 | onClick: (MapMouseEvent) -> Unit, 54 | onContextMenu: (MapMouseEvent) -> Unit, 55 | onDoubleClick: (MapMouseEvent) -> Unit, 56 | onDrag: () -> Unit, 57 | onDragEnd: () -> Unit, 58 | onDragStart: () -> Unit, 59 | onHeadingChanged: () -> Unit, 60 | onIdle: () -> Unit, 61 | onIsFractionalZoomEnabledChanged: () -> Unit, 62 | onMapTypeIdChanged: () -> Unit, 63 | onMouseMove: (MapMouseEvent) -> Unit, 64 | onMouseOut: (MapMouseEvent) -> Unit, 65 | onMouseOver: (MapMouseEvent) -> Unit, 66 | onProjectionChanged: () -> Unit, 67 | onRenderingTypeChanged: () -> Unit, 68 | onTilesLoaded: () -> Unit, 69 | onTiltChanged: () -> Unit, 70 | onZoomChanged: () -> Unit, 71 | ) { 72 | val map = (currentComposer.applier as MapApplier).map 73 | 74 | ComposeNode( 75 | factory = { 76 | MapPropertiesNode( 77 | map = map, 78 | cameraPositionState = cameraPositionState, 79 | events = mutableMapOf(), 80 | ) 81 | } 82 | ) { 83 | update(cameraPositionState) { this.cameraPositionState = it } 84 | update(mapOptions) { map.setOptions(it.toJsMapOptions()) } 85 | 86 | set(onBoundsChanged) { 87 | val eventName = "bounds_changed" 88 | this.events[eventName]?.remove() 89 | this.events[eventName] = map.addListener(eventName) { onBoundsChanged() } 90 | } 91 | set(onCenterChanged) { 92 | val eventName = "center_changed" 93 | this.events[eventName]?.remove() 94 | this.events[eventName] = map.addListener(eventName) { onCenterChanged() } 95 | } 96 | set(onClick) { 97 | val eventName = "click" 98 | this.events[eventName]?.remove() 99 | this.events[eventName] = map.addListener(eventName) { onClick((it as JsMapMouseEvent).toMouseEvent()) } 100 | } 101 | set(onContextMenu) { 102 | val eventName = "contextmenu" 103 | this.events[eventName]?.remove() 104 | this.events[eventName] = map.addListener(eventName) { onContextMenu((it as JsMapMouseEvent).toMouseEvent()) } 105 | } 106 | set(onDoubleClick) { 107 | val eventName = "dblclick" 108 | this.events[eventName]?.remove() 109 | this.events[eventName] = map.addListener(eventName) { onDoubleClick((it as JsMapMouseEvent).toMouseEvent()) } 110 | } 111 | set(onDrag) { 112 | val eventName = "drag" 113 | this.events[eventName]?.remove() 114 | this.events[eventName] = map.addListener(eventName) { onDrag() } 115 | } 116 | set(onDragEnd) { 117 | val eventName = "dragend" 118 | this.events[eventName]?.remove() 119 | this.events[eventName] = map.addListener(eventName) { onDragEnd() } 120 | } 121 | set(onDragStart) { 122 | val eventName = "dragstart" 123 | this.events[eventName]?.remove() 124 | this.events[eventName] = map.addListener(eventName) { onDragStart() } 125 | } 126 | set(onHeadingChanged) { 127 | val eventName = "heading_changed" 128 | this.events[eventName]?.remove() 129 | this.events[eventName] = map.addListener(eventName) { onHeadingChanged() } 130 | } 131 | set(onIdle) { 132 | val eventName = "idle" 133 | this.events[eventName]?.remove() 134 | this.events[eventName] = map.addListener(eventName) { onIdle() } 135 | } 136 | set(onIsFractionalZoomEnabledChanged) { 137 | val eventName = "isfractionalzoomenabled_changed" 138 | this.events[eventName]?.remove() 139 | this.events[eventName] = map.addListener(eventName) { onIsFractionalZoomEnabledChanged() } 140 | } 141 | set(onMapTypeIdChanged) { 142 | val eventName = "maptypeid_changed" 143 | this.events[eventName]?.remove() 144 | this.events[eventName] = map.addListener(eventName) { onMapTypeIdChanged() } 145 | } 146 | set(onMouseMove) { 147 | val eventName = "mousemove" 148 | this.events[eventName]?.remove() 149 | this.events[eventName] = map.addListener(eventName) { onMouseMove((it as JsMapMouseEvent).toMouseEvent()) } 150 | } 151 | set(onMouseOut) { 152 | val eventName = "mouseout" 153 | this.events[eventName]?.remove() 154 | this.events[eventName] = map.addListener(eventName) { onMouseOut((it as JsMapMouseEvent).toMouseEvent()) } 155 | } 156 | set(onMouseOver) { 157 | val eventName = "mouseover" 158 | this.events[eventName]?.remove() 159 | this.events[eventName] = map.addListener(eventName) { onMouseOver((it as JsMapMouseEvent).toMouseEvent()) } 160 | } 161 | set(onProjectionChanged) { 162 | val eventName = "projection_changed" 163 | this.events[eventName]?.remove() 164 | this.events[eventName] = map.addListener(eventName) { onProjectionChanged() } 165 | } 166 | set(onRenderingTypeChanged) { 167 | val eventName = "renderingtype_changed" 168 | this.events[eventName]?.remove() 169 | this.events[eventName] = map.addListener(eventName) { onRenderingTypeChanged() } 170 | } 171 | set(onTilesLoaded) { 172 | val eventName = "tilesloaded" 173 | this.events[eventName]?.remove() 174 | this.events[eventName] = map.addListener(eventName) { onTilesLoaded() } 175 | } 176 | set(onTiltChanged) { 177 | val eventName = "tilt_changed" 178 | this.events[eventName]?.remove() 179 | this.events[eventName] = map.addListener(eventName) { onTiltChanged() } 180 | } 181 | set(onZoomChanged) { 182 | val eventName = "zoom_changed" 183 | this.events[eventName]?.remove() 184 | this.events[eventName] = map.addListener(eventName) { onZoomChanged() } 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/Circle.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.LatLng 7 | import com.chihsuanwu.maps.compose.web.MapApplier 8 | import com.chihsuanwu.maps.compose.web.MapMouseEvent 9 | import com.chihsuanwu.maps.compose.web.MapNode 10 | import com.chihsuanwu.maps.compose.web.jsobject.* 11 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JsCircle 12 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newCircle 13 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.toJs 14 | import js.core.jso 15 | 16 | internal class CircleNode( 17 | val circle: JsCircle, 18 | var events: MutableMap 19 | ) : MapNode { 20 | override fun onRemoved() { 21 | circle.setMap(null) 22 | } 23 | } 24 | 25 | /** 26 | * A composable for a circle on the map. 27 | * 28 | * @param center the center of the circle 29 | * @param clickable Indicates whether this Circle handles mouse events. 30 | * @param draggable If set to true, the user can drag this shape over the map. 31 | * @param editable If set to true, the user can edit this circle by dragging the control points shown at the center 32 | * and around the circumference of the circle. 33 | * @param fillColor The fill color. All CSS3 colors are supported except for extended named colors. 34 | * @param fillOpacity The fill opacity between 0.0 and 1.0. 35 | * @param strokeColor The stroke color. All CSS3 colors are supported except for extended named colors. 36 | * @param strokeOpacity The stroke opacity between 0.0 and 1.0. 37 | * @param strokeWidth The stroke width in pixels. 38 | * @param strokePosition The stroke position. Defaults to CENTER. 39 | * @param visible Whether this circle is visible on the map. 40 | * @param zIndex The zIndex compared to other circles. 41 | * 42 | * @param onCenterChanged A callback to be invoked when the circle center is changed. 43 | * @param onClick A callback to be invoked when the circle is clicked. 44 | * @param onDoubleClick A callback to be invoked when the circle is double-clicked. 45 | * @param onDrag A callback to be invoked repeatedly while the user drags the circle. 46 | * @param onDragEnd A callback to be invoked when the user stops dragging the circle. 47 | * @param onDragStart A callback to be invoked when the user starts dragging the circle. 48 | * @param onMouseDown A callback to be invoked when the DOM mousedown event is fired on the circle. 49 | * @param onMouseMove A callback to be invoked when the DOM mousemove event is fired on the circle. 50 | * @param onMouseOut A callback to be invoked when the mouseout event is fired on the circle. 51 | * @param onMouseOver A callback to be invoked when the mouseover event is fired on the circle. 52 | * @param onMouseUp A callback to be invoked when the DOM mouseup event is fired on the circle. 53 | * @param onRadiusChanged A callback to be invoked when the circle radius is changed. 54 | * @param onRightClick A callback to be invoked when the circle is right-clicked. 55 | */ 56 | @Composable 57 | fun Circle( 58 | center: LatLng, 59 | clickable: Boolean = true, 60 | draggable: Boolean = false, 61 | editable: Boolean = false, 62 | fillColor: String = "#000000", 63 | fillOpacity: Double = 1.0, 64 | radius: Double = 0.0, 65 | strokeColor: String = "#000000", 66 | strokeOpacity: Double = 1.0, 67 | strokeWidth: Int = 5, 68 | strokePosition: StrokePosition = StrokePosition.CENTER, 69 | visible: Boolean = true, 70 | zIndex: Double? = null, 71 | onCenterChanged: () -> Unit = {}, 72 | onClick: (MapMouseEvent) -> Unit = {}, 73 | onDoubleClick: (MapMouseEvent) -> Unit = {}, 74 | onDrag: (MapMouseEvent) -> Unit = {}, 75 | onDragEnd: (MapMouseEvent) -> Unit = {}, 76 | onDragStart: (MapMouseEvent) -> Unit = {}, 77 | onMouseDown: (MapMouseEvent) -> Unit = {}, 78 | onMouseMove: (MapMouseEvent) -> Unit = {}, 79 | onMouseOut: (MapMouseEvent) -> Unit = {}, 80 | onMouseOver: (MapMouseEvent) -> Unit = {}, 81 | onMouseUp: (MapMouseEvent) -> Unit = {}, 82 | onRadiusChanged: () -> Unit = {}, 83 | onRightClick: (MapMouseEvent) -> Unit = {}, 84 | ) { 85 | val mapApplier = currentComposer.applier as MapApplier? 86 | ComposeNode( 87 | factory = { 88 | val circle = newCircle( 89 | jso { 90 | this.center = center.toJsLatLngLiteral() 91 | this.clickable = clickable 92 | this.draggable = draggable 93 | this.editable = editable 94 | this.fillColor = fillColor 95 | this.fillOpacity = fillOpacity 96 | this.map = mapApplier?.map 97 | this.radius = radius 98 | this.strokeColor = strokeColor 99 | this.strokeOpacity = strokeOpacity 100 | this.strokeWeight = strokeWidth 101 | this.strokePosition = strokePosition.toJs() 102 | this.visible = visible 103 | this.zIndex = zIndex 104 | } 105 | ) 106 | CircleNode(circle, mutableMapOf()) 107 | }, 108 | update = { 109 | set(center) { circle.setOptions(jso { this.center = center.toJsLatLngLiteral() }) } 110 | set(clickable) { circle.setOptions(jso { this.clickable = clickable }) } 111 | set(draggable) { circle.setOptions(jso { this.draggable = draggable }) } 112 | set(editable) { circle.setOptions(jso { this.editable = editable }) } 113 | set(fillColor) { circle.setOptions(jso { this.fillColor = fillColor }) } 114 | set(fillOpacity) { circle.setOptions(jso { this.fillOpacity = fillOpacity }) } 115 | set(radius) { circle.setOptions(jso { this.radius = radius }) } 116 | set(strokeColor) { circle.setOptions(jso { this.strokeColor = strokeColor }) } 117 | set(strokeOpacity) { circle.setOptions(jso { this.strokeOpacity = strokeOpacity }) } 118 | set(strokeWidth) { circle.setOptions(jso { this.strokeWeight = strokeWidth }) } 119 | set(strokePosition) { circle.setOptions(jso { this.strokePosition = strokePosition.toJs() }) } 120 | set(visible) { circle.setOptions(jso { this.visible = visible }) } 121 | set(zIndex) { circle.setOptions(jso { this.zIndex = zIndex }) } 122 | 123 | set(onCenterChanged) { 124 | val eventName = "center_changed" 125 | events[eventName]?.remove() 126 | events[eventName] = circle.addListener(eventName) { onCenterChanged() } 127 | } 128 | set(onClick) { 129 | val eventName = "click" 130 | events[eventName]?.remove() 131 | events[eventName] = circle.addListener(eventName) { onClick((it as JsMapMouseEvent).toMouseEvent()) } 132 | } 133 | set(onDoubleClick) { 134 | val eventName = "dblclick" 135 | events[eventName]?.remove() 136 | events[eventName] = circle.addListener(eventName) { onDoubleClick((it as JsMapMouseEvent).toMouseEvent()) } 137 | } 138 | set(onDrag) { 139 | val eventName = "drag" 140 | events[eventName]?.remove() 141 | events[eventName] = circle.addListener(eventName) { onDrag((it as JsMapMouseEvent).toMouseEvent()) } 142 | } 143 | set(onDragEnd) { 144 | val eventName = "dragend" 145 | events[eventName]?.remove() 146 | events[eventName] = circle.addListener(eventName) { onDragEnd((it as JsMapMouseEvent).toMouseEvent()) } 147 | } 148 | set(onDragStart) { 149 | val eventName = "dragstart" 150 | events[eventName]?.remove() 151 | events[eventName] = circle.addListener(eventName) { onDragStart((it as JsMapMouseEvent).toMouseEvent()) } 152 | } 153 | set(onMouseDown) { 154 | val eventName = "mousedown" 155 | events[eventName]?.remove() 156 | events[eventName] = circle.addListener(eventName) { onMouseDown((it as JsMapMouseEvent).toMouseEvent()) } 157 | } 158 | set(onMouseMove) { 159 | val eventName = "mousemove" 160 | events[eventName]?.remove() 161 | events[eventName] = circle.addListener(eventName) { onMouseMove((it as JsMapMouseEvent).toMouseEvent()) } 162 | } 163 | set(onMouseOut) { 164 | val eventName = "mouseout" 165 | events[eventName]?.remove() 166 | events[eventName] = circle.addListener(eventName) { onMouseOut((it as JsMapMouseEvent).toMouseEvent()) } 167 | } 168 | set(onMouseOver) { 169 | val eventName = "mouseover" 170 | events[eventName]?.remove() 171 | events[eventName] = circle.addListener(eventName) { onMouseOver((it as JsMapMouseEvent).toMouseEvent()) } 172 | } 173 | set(onMouseUp) { 174 | val eventName = "mouseup" 175 | events[eventName]?.remove() 176 | events[eventName] = circle.addListener(eventName) { onMouseUp((it as JsMapMouseEvent).toMouseEvent()) } 177 | } 178 | set(onRadiusChanged) { 179 | val eventName = "radius_changed" 180 | events[eventName]?.remove() 181 | events[eventName] = circle.addListener(eventName) { onRadiusChanged() } 182 | } 183 | set(onRightClick) { 184 | val eventName = "rightclick" 185 | events[eventName]?.remove() 186 | events[eventName] = circle.addListener(eventName) { onRightClick((it as JsMapMouseEvent).toMouseEvent()) } 187 | } 188 | } 189 | ) 190 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/InfoWindow.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.* 4 | import com.chihsuanwu.maps.compose.web.LatLng 5 | import com.chihsuanwu.maps.compose.web.MapApplier 6 | import com.chihsuanwu.maps.compose.web.MapNode 7 | import com.chihsuanwu.maps.compose.web.Size 8 | import com.chihsuanwu.maps.compose.web.jsobject.* 9 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JsInfoWindow 10 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newInfoWindow 11 | import com.chihsuanwu.maps.compose.web.jsobject.toJsLatLngLiteral 12 | import js.core.jso 13 | import kotlinx.browser.document 14 | import org.jetbrains.compose.web.renderComposable 15 | 16 | 17 | internal class InfoWindowNode( 18 | val infoWindow: JsInfoWindow, 19 | val infoWindowState: InfoWindowState, 20 | val map: MapView?, 21 | var events: MutableMap, 22 | ) : MapNode { 23 | override fun onAttached() { 24 | infoWindowState.infoWindow = infoWindow 25 | infoWindowState.map = map 26 | } 27 | 28 | override fun onRemoved() { 29 | infoWindowState.infoWindow = null 30 | infoWindow.close() 31 | } 32 | 33 | override fun onCleared() { 34 | infoWindowState.infoWindow = null 35 | infoWindow.close() 36 | } 37 | } 38 | 39 | 40 | /** 41 | * A state object that can be hoisted to control and observe the info window state. 42 | * 43 | * @param position the initial info window position 44 | */ 45 | class InfoWindowState( 46 | position: LatLng = LatLng(0.0, 0.0) 47 | ) { 48 | var position: LatLng by mutableStateOf(position) 49 | 50 | internal var infoWindow: JsInfoWindow? = null 51 | internal var map: MapView? = null 52 | 53 | fun showInfoWindow() { 54 | // val map = map ?: return 55 | // Above code will cause compile error: Type inference failed. Expected type mismatch 56 | // Don't know why, following code works fine.: 57 | if (map == null) return 58 | val map = map!! 59 | 60 | infoWindow?.open( 61 | jso { 62 | this.map = map 63 | } 64 | ) 65 | } 66 | 67 | fun hideInfoWindow() { 68 | infoWindow?.close() 69 | } 70 | } 71 | 72 | /** 73 | * A composable that represents a Google Maps InfoWindow. 74 | * 75 | * @param state The [InfoWindowState] object that controls and observes the state of the InfoWindow. 76 | * @param ariaLabel AriaLabel to assign to the InfoWindow. 77 | * @param disableAutoPan Disable panning the map to make the InfoWindow fully visible when it opens. 78 | * @param maxWidth Maximum width of the InfoWindow, in pixels. 79 | * @param minWidth Minimum width of the InfoWindow, in pixels. 80 | * @param pixelOffset Offset, in pixels, of the tip of the InfoWindow from the point on the map 81 | * at whose geographical coordinates the InfoWindow is anchored. 82 | * @param zIndex The zIndex compared to other windows. 83 | * 84 | * 85 | * 86 | * @param content The content of the InfoWindow. 87 | */ 88 | @Composable 89 | fun InfoWindow( 90 | state: InfoWindowState, 91 | ariaLabel: String? = null, 92 | disableAutoPan: Boolean = false, 93 | maxWidth: Int? = null, 94 | minWidth: Int? = null, 95 | pixelOffset: Size? = null, 96 | zIndex: Double? = null, 97 | onCloseClick: () -> Unit = {}, 98 | onContentChanged: () -> Unit = {}, 99 | onDOMReady: () -> Unit = {}, 100 | onPositionChanged: () -> Unit = {}, 101 | onVisible: () -> Unit = {}, 102 | onZIndexChanged: () -> Unit = {}, 103 | content: @Composable () -> Unit, 104 | ) { 105 | val mapApplier = currentComposer.applier as MapApplier? 106 | ComposeNode( 107 | factory = { 108 | val root = document.createElement("div") 109 | renderComposable(root) { 110 | content() 111 | } 112 | val infoWindow = newInfoWindow( 113 | jso { 114 | this.position = state.position.toJsLatLngLiteral() 115 | this.ariaLabel = ariaLabel 116 | this.disableAutoPan = disableAutoPan 117 | this.maxWidth = maxWidth 118 | this.minWidth = minWidth 119 | this.pixelOffset = pixelOffset?.toJsSize() 120 | this.zIndex = zIndex 121 | } 122 | ) 123 | infoWindow.setContent(root) 124 | InfoWindowNode(infoWindow, state, mapApplier?.map, mutableMapOf()) 125 | }, 126 | update = { 127 | set(state.position) { infoWindow.setOptions(jso { this.position = state.position.toJsLatLngLiteral() }) } 128 | set(ariaLabel) { infoWindow.setOptions(jso { this.ariaLabel = ariaLabel }) } 129 | set(disableAutoPan) { infoWindow.setOptions(jso { this.disableAutoPan = disableAutoPan }) } 130 | set(maxWidth) { infoWindow.setOptions(jso { this.maxWidth = maxWidth }) } 131 | set(minWidth) { infoWindow.setOptions(jso { this.minWidth = minWidth }) } 132 | set(pixelOffset) { infoWindow.setOptions(jso { this.pixelOffset = pixelOffset?.toJsSize() }) } 133 | set(zIndex) { infoWindow.setOptions(jso { this.zIndex = zIndex }) } 134 | 135 | set(onCloseClick) { 136 | val eventName = "closeclick" 137 | events[eventName]?.remove() 138 | events[eventName] = infoWindow.addListener(eventName) { onCloseClick() } 139 | } 140 | set(onContentChanged) { 141 | val eventName = "content_changed" 142 | events[eventName]?.remove() 143 | events[eventName] = infoWindow.addListener(eventName) { onContentChanged() } 144 | } 145 | set(onDOMReady) { 146 | val eventName = "domready" 147 | events[eventName]?.remove() 148 | events[eventName] = infoWindow.addListener(eventName) { onDOMReady() } 149 | } 150 | set(onPositionChanged) { 151 | val eventName = "position_changed" 152 | events[eventName]?.remove() 153 | events[eventName] = infoWindow.addListener(eventName) { onPositionChanged() } 154 | } 155 | set(onVisible) { 156 | val eventName = "visible" 157 | events[eventName]?.remove() 158 | events[eventName] = infoWindow.addListener(eventName) { onVisible() } 159 | } 160 | set(onZIndexChanged) { 161 | val eventName = "zindex_changed" 162 | events[eventName]?.remove() 163 | events[eventName] = infoWindow.addListener(eventName) { onZIndexChanged() } 164 | } 165 | } 166 | ) 167 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/OverlayView.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.LatLng 7 | import com.chihsuanwu.maps.compose.web.LatLngBounds 8 | import com.chihsuanwu.maps.compose.web.MapApplier 9 | import com.chihsuanwu.maps.compose.web.MapNode 10 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 11 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JSOverlayView 12 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.getPane 13 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newOverlayView 14 | import com.chihsuanwu.maps.compose.web.jsobject.toJsLatLngLiteral 15 | import kotlinx.browser.document 16 | import org.jetbrains.compose.web.renderComposable 17 | import org.w3c.dom.HTMLDivElement 18 | 19 | 20 | /** 21 | * The panes that are available to attach overlays. 22 | */ 23 | enum class MapPanes { 24 | /** 25 | * This pane contains the info window. It is above all map overlays. (Pane 4). 26 | */ 27 | FloatPane, 28 | /** 29 | * This pane is the lowest pane and is above the tiles. It does not receive DOM events. (Pane 0). 30 | */ 31 | MapPane, 32 | /** 33 | * This pane contains markers. It does not receive DOM events. (Pane 2). 34 | */ 35 | MarkerLayer, 36 | /** 37 | * This pane contains polylines, polygons, ground overlays and tile layer overlays. 38 | * It does not receive DOM events. (Pane 1). 39 | */ 40 | OverlayLayer, 41 | /** 42 | * This pane contains elements that receive DOM events. (Pane 3). 43 | */ 44 | OverlayMouseTarget 45 | } 46 | 47 | internal class OverlayViewNode( 48 | val overlayView: JSOverlayView, 49 | val root: HTMLDivElement, 50 | var mapPane: MapPanes, 51 | ) : MapNode { 52 | 53 | override fun onRemoved() { 54 | overlayView.setMap(null) 55 | } 56 | } 57 | 58 | /** 59 | * A composable that can be used to display custom overlays on the map. 60 | * 61 | * @param bounds the bounds of the overlay 62 | * @param mapPane the pane to which the overlay is attached 63 | * @param content the DOM content of the overlay 64 | */ 65 | @Composable 66 | fun OverlayView( 67 | bounds: LatLngBounds, 68 | mapPane: MapPanes = MapPanes.OverlayLayer, 69 | content: @Composable () -> Unit, 70 | ) { 71 | val mapApplier = currentComposer.applier as MapApplier? 72 | ComposeNode( 73 | factory = { 74 | val root = (document.createElement("div") as HTMLDivElement).apply { 75 | style.position = "absolute" 76 | } 77 | renderComposable(root) { 78 | content() 79 | } 80 | val overlayView = newOverlayView().apply { 81 | onAdd = { onAdd((js("this") as JSOverlayView), root, mapPane) } 82 | draw = { draw((js("this") as JSOverlayView), root, bounds) } 83 | onRemove = { root.parentNode?.removeChild(root) } 84 | setMap(mapApplier?.map) 85 | } 86 | OverlayViewNode(overlayView, root, mapPane) 87 | }, 88 | update = { 89 | update(mapPane) { 90 | this.mapPane = it 91 | this.overlayView.apply { 92 | onAdd = { onAdd((js("this") as JSOverlayView), root, it) } 93 | // Trigger a reattach to the new pane 94 | onRemove() 95 | onAdd() 96 | } 97 | } 98 | update(bounds) { 99 | this.overlayView.draw = { draw((js("this") as JSOverlayView), root, it) } 100 | // Trigger a redraw 101 | this.overlayView.draw() 102 | } 103 | } 104 | ) 105 | } 106 | 107 | private fun onAdd(view: JSOverlayView, root: HTMLDivElement, mapPane: MapPanes) { 108 | view.getPanes().getPane(mapPane).appendChild(root) 109 | } 110 | 111 | private fun draw(view: JSOverlayView, root: HTMLDivElement, bounds: LatLngBounds) { 112 | val overlayProjection = view.getProjection() 113 | val sw = overlayProjection.fromLatLngToDivPixel(LatLng(bounds.south, bounds.west).toJsLatLngLiteral()) 114 | val ne = overlayProjection.fromLatLngToDivPixel(LatLng(bounds.north, bounds.east).toJsLatLngLiteral()) 115 | root.style.apply { 116 | left = "${sw.x}px" 117 | top = "${ne.y}px" 118 | width = "${ne.x - sw.x}px" 119 | height = "${sw.y - ne.y}px" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/Polygon.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.* 7 | import com.chihsuanwu.maps.compose.web.MapApplier 8 | import com.chihsuanwu.maps.compose.web.MapNode 9 | import com.chihsuanwu.maps.compose.web.jsobject.* 10 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JsPolygon 11 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newPolygon 12 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.toJs 13 | import js.core.jso 14 | 15 | enum class StrokePosition { 16 | CENTER, 17 | INSIDE, 18 | OUTSIDE 19 | } 20 | 21 | internal class PolygonNode( 22 | val polygon: JsPolygon, 23 | var events: MutableMap, 24 | ) : MapNode { 25 | override fun onRemoved() { 26 | polygon.setMap(null) 27 | } 28 | } 29 | 30 | /** 31 | * A composable for a polygon on the map. 32 | * 33 | * @param points The points of the polygon. 34 | * 35 | * @param clickable Indicates whether this Polygon handles mouse events. 36 | * @param draggable If set to true, the user can drag this shape over the map. 37 | * The geodesic property defines the mode of dragging. 38 | * @param editable If set to true, the user can edit this shape by dragging the control points 39 | * shown at the vertices and on each segment. 40 | * @param fillColor The fill color. All CSS3 colors are supported except for extended named colors. 41 | * @param fillOpacity The fill opacity between 0.0 and 1.0. 42 | * @param geodesic When true, edges of the polygon are interpreted as geodesic and 43 | * will follow the curvature of the Earth. When false, edges of the polygon are 44 | * rendered as straight lines in screen space. 45 | * @param strokeColor The stroke color. All CSS3 colors are supported except for extended named colors. 46 | * @param strokeOpacity The stroke opacity between 0.0 and 1.0. 47 | * @param strokeWidth The stroke width in pixels. 48 | * @param strokePosition The stroke position. Defaults to CENTER. 49 | * @param visible Whether this polygon is visible on the map. 50 | * @param zIndex The zIndex compared to other polys. 51 | * 52 | * @param onClick A callback to be invoked when the polygon is clicked. 53 | * @param onContextMenu A callback to be invoked when the DOM contextmenu event is fired on the polygon. 54 | * @param onDoubleClick A callback to be invoked when the polygon is double-clicked. 55 | * @param onDrag A callback to be invoked repeatedly while the user drags the polygon. 56 | * @param onDragEnd A callback to be invoked when the user stops dragging the polygon. 57 | * @param onDragStart A callback to be invoked when the user stops dragging the polygon. 58 | * @param onMouseDown A callback to be invoked when the DOM mousedown event is fired on the polygon. 59 | * @param onMouseMove A callback to be invoked when the DOM mousemove event is fired on the polygon. 60 | * @param onMouseOut A callback to be invoked when the mouseout event is fired. 61 | * @param onMouseOver A callback to be invoked when the mouseover event is fired. 62 | * @param onMouseUp A callback to be invoked when the DOM mouseup event is fired. 63 | * 64 | */ 65 | @Composable 66 | fun Polygon( 67 | points: List, 68 | clickable: Boolean = true, 69 | draggable: Boolean = false, 70 | editable: Boolean = false, 71 | fillColor: String = "#000000", 72 | fillOpacity: Double = 1.0, 73 | geodesic: Boolean = false, 74 | strokeColor: String = "#000000", 75 | strokeOpacity: Double = 1.0, 76 | strokeWidth: Int = 5, 77 | strokePosition: StrokePosition = StrokePosition.CENTER, 78 | visible: Boolean = true, 79 | zIndex: Double? = null, 80 | onClick: (PolyMouseEvent) -> Unit = {}, 81 | onContextMenu: (PolyMouseEvent) -> Unit = {}, 82 | onDoubleClick: (PolyMouseEvent) -> Unit = {}, 83 | onDrag: (MapMouseEvent) -> Unit = {}, 84 | onDragEnd: (MapMouseEvent) -> Unit = {}, 85 | onDragStart: (MapMouseEvent) -> Unit = {}, 86 | onMouseDown: (PolyMouseEvent) -> Unit = {}, 87 | onMouseMove: (PolyMouseEvent) -> Unit = {}, 88 | onMouseOut: (PolyMouseEvent) -> Unit = {}, 89 | onMouseOver: (PolyMouseEvent) -> Unit = {}, 90 | onMouseUp: (PolyMouseEvent) -> Unit = {}, 91 | ) { 92 | val mapApplier = currentComposer.applier as MapApplier? 93 | ComposeNode( 94 | factory = { 95 | val polygon = newPolygon( 96 | jso { 97 | this.paths = points.toJsLatLngLiteralArray() 98 | this.clickable = clickable 99 | this.draggable = draggable 100 | this.editable = editable 101 | this.fillColor = fillColor 102 | this.fillOpacity = fillOpacity 103 | this.geodesic = geodesic 104 | this.map = mapApplier?.map 105 | this.strokeColor = strokeColor 106 | this.strokeOpacity = strokeOpacity 107 | this.strokeWeight = strokeWidth 108 | this.strokePosition = strokePosition.toJs() 109 | this.visible = visible 110 | this.zIndex = zIndex 111 | } 112 | ) 113 | PolygonNode(polygon, mutableMapOf()) 114 | }, 115 | update = { 116 | set(points) { polygon.setOptions(jso { this.paths = points.toJsLatLngLiteralArray() }) } 117 | set(clickable) { polygon.setOptions(jso { this.clickable = clickable }) } 118 | set(draggable) { polygon.setOptions(jso { this.draggable = draggable }) } 119 | set(editable) { polygon.setOptions(jso { this.editable = editable }) } 120 | set(fillColor) { polygon.setOptions(jso { this.fillColor = fillColor }) } 121 | set(fillOpacity) { polygon.setOptions(jso { this.fillOpacity = fillOpacity }) } 122 | set(geodesic) { polygon.setOptions(jso { this.geodesic = geodesic }) } 123 | set(strokeColor) { polygon.setOptions(jso { this.strokeColor = strokeColor }) } 124 | set(strokeOpacity) { polygon.setOptions(jso { this.strokeOpacity = strokeOpacity }) } 125 | set(strokeWidth) { polygon.setOptions(jso { this.strokeWeight = strokeWidth }) } 126 | set(strokePosition) { polygon.setOptions(jso { this.strokePosition = strokePosition.toJs() }) } 127 | set(visible) { polygon.setOptions(jso { this.visible = visible }) } 128 | set(zIndex) { polygon.setOptions(jso { this.zIndex = zIndex }) } 129 | 130 | set(onClick) { 131 | val eventName = "click" 132 | events[eventName]?.remove() 133 | events[eventName] = polygon.addListener(eventName) { onClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 134 | } 135 | set(onContextMenu) { 136 | val eventName = "contextmenu" 137 | events[eventName]?.remove() 138 | events[eventName] = polygon.addListener(eventName) { onContextMenu((it as JsPolyMouseEvent).toPolyMouseEvent()) } 139 | } 140 | set(onDoubleClick) { 141 | val eventName = "dblclick" 142 | events[eventName]?.remove() 143 | events[eventName] = polygon.addListener(eventName) { onDoubleClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 144 | } 145 | set(onDrag) { 146 | val eventName = "drag" 147 | events[eventName]?.remove() 148 | events[eventName] = polygon.addListener(eventName) { onDrag((it as JsMapMouseEvent).toMouseEvent()) } 149 | } 150 | set(onDragEnd) { 151 | val eventName = "dragend" 152 | events[eventName]?.remove() 153 | events[eventName] = polygon.addListener(eventName) { onDragEnd((it as JsMapMouseEvent).toMouseEvent()) } 154 | } 155 | set(onDragStart) { 156 | val eventName = "dragstart" 157 | events[eventName]?.remove() 158 | events[eventName] = polygon.addListener(eventName) { onDragStart((it as JsMapMouseEvent).toMouseEvent()) } 159 | } 160 | set(onMouseDown) { 161 | val eventName = "mousedown" 162 | events[eventName]?.remove() 163 | events[eventName] = polygon.addListener(eventName) { onMouseDown((it as JsPolyMouseEvent).toPolyMouseEvent()) } 164 | } 165 | set(onMouseMove) { 166 | val eventName = "mousemove" 167 | events[eventName]?.remove() 168 | events[eventName] = polygon.addListener(eventName) { onMouseMove((it as JsPolyMouseEvent).toPolyMouseEvent()) } 169 | } 170 | set(onMouseOut) { 171 | val eventName = "mouseout" 172 | events[eventName]?.remove() 173 | events[eventName] = polygon.addListener(eventName) { onMouseOut((it as JsPolyMouseEvent).toPolyMouseEvent()) } 174 | } 175 | set(onMouseOver) { 176 | val eventName = "mouseover" 177 | events[eventName]?.remove() 178 | events[eventName] = polygon.addListener(eventName) { onMouseOver((it as JsPolyMouseEvent).toPolyMouseEvent()) } 179 | } 180 | set(onMouseUp) { 181 | val eventName = "mouseup" 182 | events[eventName]?.remove() 183 | events[eventName] = polygon.addListener(eventName) { onMouseUp((it as JsPolyMouseEvent).toPolyMouseEvent()) } 184 | } 185 | } 186 | ) 187 | } 188 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/Polyline.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.* 7 | import com.chihsuanwu.maps.compose.web.MapApplier 8 | import com.chihsuanwu.maps.compose.web.MapNode 9 | import com.chihsuanwu.maps.compose.web.jsobject.* 10 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JsPolyline 11 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newPolyline 12 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.toJsIconSequenceArray 13 | import js.core.jso 14 | 15 | /** 16 | * [google.maps.IconSequence](https://developers.google.com/maps/documentation/javascript/reference/polygon#IconSequence) 17 | * 18 | * @param fixedRotation If true, each icon in the sequence will be rotated to match the angle of the edge. 19 | * @param icon The icon to render. 20 | * @param offset The distance from the start of the line at which an icon is to be rendered. 21 | * This distance may be expressed as a percentage of line's length (e.g. '50%') or in pixels (e.g. '50px'). 22 | * @param repeat The distance between consecutive icons on the line. 23 | * This distance may be expressed as a percentage of the line's length (e.g. '50%') or in pixels (e.g. '50px'). 24 | * To disable repeating of the icon, specify '0'. 25 | */ 26 | data class IconSequence( 27 | val fixedRotation: Boolean? = null, 28 | val icon: MarkerIcon.Symbol? = null, 29 | val offset: String? = "100%", 30 | val repeat: String? = "0" 31 | ) 32 | 33 | internal class PolylineNode( 34 | val polyline: JsPolyline, 35 | var events: MutableMap, 36 | ) : MapNode { 37 | override fun onRemoved() { 38 | polyline.setMap(null) 39 | } 40 | } 41 | 42 | /** 43 | * A composable for a polyline on the map. 44 | * 45 | * @param points The points of the polyline. 46 | * 47 | * @param clickable Indicates whether this Polyline handles mouse events. 48 | * @param color The stroke color. All CSS3 colors are supported except for extended named colors. 49 | * @param draggable If set to true, the user can drag this shape over the map. 50 | * The geodesic property defines the mode of dragging. 51 | * @param editable If set to true, the user can edit this shape by dragging the control points 52 | * shown at the vertices and on each segment. 53 | * @param geodesic When true, edges of the polygon are interpreted as geodesic and 54 | * will follow the curvature of the Earth. When false, edges of the polygon are 55 | * rendered as straight lines in screen space. 56 | * @param icons The icons to be rendered along the polyline. 57 | * @param opacity The opacity of the polyline between 0.0 and 1.0. 58 | * @param visible Whether this polyline is visible on the map. 59 | * @param width The width of the polyline in pixels. 60 | * @param zIndex The zIndex compared to other polys. 61 | * 62 | * @param onClick A callback to be invoked when the polyline is clicked. 63 | * @param onContextMenu A callback to be invoked when the DOM contextmenu event is fired on the polyline. 64 | * @param onDoubleClick A callback to be invoked when the polyline is double-clicked. 65 | * @param onDrag A callback to be invoked repeatedly while the user drags the polyline. 66 | * @param onDragEnd A callback to be invoked when the user stops dragging the polyline. 67 | * @param onDragStart A callback to be invoked when the user starts dragging the polyline. 68 | * @param onMouseDown A callback to be invoked when the DOM mousedown event is fired on the polyline. 69 | * @param onMouseMove A callback to be invoked when the DOM mousemove event is fired on the polyline. 70 | * @param onMouseOut A callback to be invoked when the mouseout event is fired. 71 | * @param onMouseOver A callback to be invoked when the mouseover event is fired. 72 | * @param onMouseUp A callback to be invoked when the DOM mouseup event is fired on the polyline. 73 | * 74 | */ 75 | @Composable 76 | fun Polyline( 77 | points: List, 78 | clickable: Boolean = true, 79 | color: String = "#000000", 80 | draggable: Boolean = false, 81 | editable: Boolean = false, 82 | geodesic: Boolean = false, 83 | icons: List? = null, 84 | opacity: Double = 1.0, 85 | visible: Boolean = true, 86 | width: Int = 5, 87 | zIndex: Double? = null, 88 | onClick: (PolyMouseEvent) -> Unit = {}, 89 | onContextMenu: (PolyMouseEvent) -> Unit = {}, 90 | onDoubleClick: (PolyMouseEvent) -> Unit = {}, 91 | onDrag: (MapMouseEvent) -> Unit = {}, 92 | onDragEnd: (MapMouseEvent) -> Unit = {}, 93 | onDragStart: (MapMouseEvent) -> Unit = {}, 94 | onMouseDown: (PolyMouseEvent) -> Unit = {}, 95 | onMouseMove: (PolyMouseEvent) -> Unit = {}, 96 | onMouseOut: (PolyMouseEvent) -> Unit = {}, 97 | onMouseOver: (PolyMouseEvent) -> Unit = {}, 98 | onMouseUp: (PolyMouseEvent) -> Unit = {}, 99 | ) { 100 | val mapApplier = currentComposer.applier as MapApplier? 101 | ComposeNode( 102 | factory = { 103 | val polyline = newPolyline( 104 | jso { 105 | this.path = points.toJsLatLngLiteralArray() 106 | this.clickable = clickable 107 | this.strokeColor = color 108 | this.draggable = draggable 109 | this.editable = editable 110 | this.geodesic = geodesic 111 | this.icons = icons?.toJsIconSequenceArray() 112 | this.map = mapApplier?.map 113 | this.strokeOpacity = opacity 114 | this.visible = visible 115 | this.strokeWeight = width 116 | this.zIndex = zIndex 117 | } 118 | ) 119 | PolylineNode(polyline, mutableMapOf()) 120 | }, 121 | update = { 122 | set(points) { polyline.setOptions(jso { this.path = points.toJsLatLngLiteralArray() }) } 123 | set(clickable) { polyline.setOptions(jso { this.clickable = clickable }) } 124 | set(color) { polyline.setOptions(jso { this.strokeColor = color }) } 125 | set(draggable) { polyline.setOptions(jso { this.draggable = draggable }) } 126 | set(editable) { polyline.setOptions(jso { this.editable = editable }) } 127 | set(geodesic) { polyline.setOptions(jso { this.geodesic = geodesic }) } 128 | set(icons) { polyline.setOptions(jso { this.icons = icons?.toJsIconSequenceArray() }) } 129 | set(opacity) { polyline.setOptions(jso { this.strokeOpacity = opacity }) } 130 | set(visible) { polyline.setOptions(jso { this.visible = visible }) } 131 | set(width) { polyline.setOptions(jso { this.strokeWeight = width }) } 132 | set(zIndex) { polyline.setOptions(jso { this.zIndex = zIndex }) } 133 | 134 | set(onClick) { 135 | val eventName = "click" 136 | events[eventName]?.remove() 137 | events[eventName] = polyline.addListener(eventName) { onClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 138 | } 139 | set(onContextMenu) { 140 | val eventName = "contextmenu" 141 | events[eventName]?.remove() 142 | events[eventName] = polyline.addListener(eventName) { onContextMenu((it as JsPolyMouseEvent).toPolyMouseEvent()) } 143 | } 144 | set(onDoubleClick) { 145 | val eventName = "dblclick" 146 | events[eventName]?.remove() 147 | events[eventName] = polyline.addListener(eventName) { onDoubleClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 148 | } 149 | set(onDrag) { 150 | val eventName = "drag" 151 | events[eventName]?.remove() 152 | events[eventName] = polyline.addListener(eventName) { onDrag((it as JsMapMouseEvent).toMouseEvent()) } 153 | } 154 | set(onDragEnd) { 155 | val eventName = "dragend" 156 | events[eventName]?.remove() 157 | events[eventName] = polyline.addListener(eventName) { onDragEnd((it as JsMapMouseEvent).toMouseEvent()) } 158 | } 159 | set(onDragStart) { 160 | val eventName = "dragstart" 161 | events[eventName]?.remove() 162 | events[eventName] = polyline.addListener(eventName) { onDragStart((it as JsMapMouseEvent).toMouseEvent()) } 163 | } 164 | set(onMouseDown) { 165 | val eventName = "mousedown" 166 | events[eventName]?.remove() 167 | events[eventName] = polyline.addListener(eventName) { onMouseDown((it as JsPolyMouseEvent).toPolyMouseEvent()) } 168 | } 169 | set(onMouseMove) { 170 | val eventName = "mousemove" 171 | events[eventName]?.remove() 172 | events[eventName] = polyline.addListener(eventName) { onMouseMove((it as JsPolyMouseEvent).toPolyMouseEvent()) } 173 | } 174 | set(onMouseOut) { 175 | val eventName = "mouseout" 176 | events[eventName]?.remove() 177 | events[eventName] = polyline.addListener(eventName) { onMouseOut((it as JsPolyMouseEvent).toPolyMouseEvent()) } 178 | } 179 | set(onMouseOver) { 180 | val eventName = "mouseover" 181 | events[eventName]?.remove() 182 | events[eventName] = polyline.addListener(eventName) { onMouseOver((it as JsPolyMouseEvent).toPolyMouseEvent()) } 183 | } 184 | set(onMouseUp) { 185 | val eventName = "mouseup" 186 | events[eventName]?.remove() 187 | events[eventName] = polyline.addListener(eventName) { onMouseUp((it as JsPolyMouseEvent).toPolyMouseEvent()) } 188 | } 189 | } 190 | ) 191 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/drawing/Rectangle.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.drawing 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.* 7 | import com.chihsuanwu.maps.compose.web.jsobject.* 8 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.JsRectangle 9 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.newRectangle 10 | import com.chihsuanwu.maps.compose.web.jsobject.drawing.toJs 11 | import js.core.jso 12 | 13 | 14 | internal class RectangleNode( 15 | val rectangle: JsRectangle, 16 | var events: MutableMap, 17 | ) : MapNode { 18 | override fun onRemoved() { 19 | rectangle.setMap(null) 20 | } 21 | } 22 | 23 | /** 24 | * A composable for a rectangle on the map. 25 | * 26 | * @param clickable Indicates whether this Rectangle handles mouse events. 27 | * @param draggable If set to true, the user can drag this shape over the map. 28 | * @param editable If set to true, the user can edit this rectangle by 29 | * dragging the control points shown at the corners and on each edge. 30 | * @param fillColor The fill color. All CSS3 colors are supported except for extended named colors. 31 | * @param fillOpacity The fill opacity between 0.0 and 1.0. 32 | * @param strokeColor The stroke color. All CSS3 colors are supported except for extended named colors. 33 | * @param strokeOpacity The stroke opacity between 0.0 and 1.0. 34 | * @param strokeWidth The stroke width in pixels. 35 | * @param strokePosition The stroke position. Defaults to CENTER. 36 | * @param visible Whether this rectangle is visible on the map. 37 | * @param zIndex The zIndex compared to other polys. 38 | * 39 | * @param onClick A callback to be invoked when the rectangle is clicked. 40 | * @param onContextMenu A callback to be invoked when the DOM contextmenu event is fired on the rectangle. 41 | * @param onDoubleClick A callback to be invoked when the rectangle is double-clicked. 42 | * @param onDrag A callback to be invoked repeatedly while the user drags the rectangle. 43 | * @param onDragEnd A callback to be invoked when the user stops dragging the rectangle. 44 | * @param onDragStart A callback to be invoked when the user stops dragging the rectangle. 45 | * @param onMouseDown A callback to be invoked when the DOM mousedown event is fired on the rectangle. 46 | * @param onMouseMove A callback to be invoked when the DOM mousemove event is fired on the rectangle. 47 | * @param onMouseOut A callback to be invoked when the mouseout event is fired. 48 | * @param onMouseOver A callback to be invoked when the mouseover event is fired. 49 | * @param onMouseUp A callback to be invoked when the DOM mouseup event is fired. 50 | */ 51 | @Composable 52 | fun Rectangle( 53 | bounds: LatLngBounds, 54 | clickable: Boolean = true, 55 | draggable: Boolean = false, 56 | editable: Boolean = false, 57 | fillColor: String = "#000000", 58 | fillOpacity: Double = 1.0, 59 | strokeColor: String = "#000000", 60 | strokeOpacity: Double = 1.0, 61 | strokeWidth: Int = 5, 62 | strokePosition: StrokePosition = StrokePosition.CENTER, 63 | visible: Boolean = true, 64 | zIndex: Double? = null, 65 | onClick: (PolyMouseEvent) -> Unit = {}, 66 | onContextMenu: (PolyMouseEvent) -> Unit = {}, 67 | onDoubleClick: (PolyMouseEvent) -> Unit = {}, 68 | onDrag: (MapMouseEvent) -> Unit = {}, 69 | onDragEnd: (MapMouseEvent) -> Unit = {}, 70 | onDragStart: (MapMouseEvent) -> Unit = {}, 71 | onMouseDown: (PolyMouseEvent) -> Unit = {}, 72 | onMouseMove: (PolyMouseEvent) -> Unit = {}, 73 | onMouseOut: (PolyMouseEvent) -> Unit = {}, 74 | onMouseOver: (PolyMouseEvent) -> Unit = {}, 75 | onMouseUp: (PolyMouseEvent) -> Unit = {}, 76 | ) { 77 | val mapApplier = currentComposer.applier as MapApplier? 78 | ComposeNode( 79 | factory = { 80 | val rectangle = newRectangle( 81 | jso { 82 | this.bounds = bounds.toJsLatLngBoundsLiteral() 83 | this.clickable = clickable 84 | this.draggable = draggable 85 | this.editable = editable 86 | this.fillColor = fillColor 87 | this.fillOpacity = fillOpacity 88 | this.map = mapApplier?.map 89 | this.strokeColor = strokeColor 90 | this.strokeOpacity = strokeOpacity 91 | this.strokeWeight = strokeWidth 92 | this.strokePosition = strokePosition.toJs() 93 | this.visible = visible 94 | this.zIndex = zIndex 95 | } 96 | ) 97 | RectangleNode(rectangle, mutableMapOf()) 98 | }, 99 | update = { 100 | set(bounds) { rectangle.setOptions(jso { this.bounds = bounds.toJsLatLngBoundsLiteral() }) } 101 | set(clickable) { rectangle.setOptions(jso { this.clickable = clickable }) } 102 | set(draggable) { rectangle.setOptions(jso { this.draggable = draggable }) } 103 | set(editable) { rectangle.setOptions(jso { this.editable = editable }) } 104 | set(fillColor) { rectangle.setOptions(jso { this.fillColor = fillColor }) } 105 | set(fillOpacity) { rectangle.setOptions(jso { this.fillOpacity = fillOpacity }) } 106 | set(strokeColor) { rectangle.setOptions(jso { this.strokeColor = strokeColor }) } 107 | set(strokeOpacity) { rectangle.setOptions(jso { this.strokeOpacity = strokeOpacity }) } 108 | set(strokeWidth) { rectangle.setOptions(jso { this.strokeWeight = strokeWidth }) } 109 | set(strokePosition) { rectangle.setOptions(jso { this.strokePosition = strokePosition.toJs() }) } 110 | set(visible) { rectangle.setOptions(jso { this.visible = visible }) } 111 | set(zIndex) { rectangle.setOptions(jso { this.zIndex = zIndex }) } 112 | 113 | set(onClick) { 114 | val eventName = "click" 115 | events[eventName]?.remove() 116 | events[eventName] = rectangle.addListener(eventName) { onClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 117 | } 118 | set(onContextMenu) { 119 | val eventName = "contextmenu" 120 | events[eventName]?.remove() 121 | events[eventName] = rectangle.addListener(eventName) { onContextMenu((it as JsPolyMouseEvent).toPolyMouseEvent()) } 122 | } 123 | set(onDoubleClick) { 124 | val eventName = "dblclick" 125 | events[eventName]?.remove() 126 | events[eventName] = rectangle.addListener(eventName) { onDoubleClick((it as JsPolyMouseEvent).toPolyMouseEvent()) } 127 | } 128 | set(onDrag) { 129 | val eventName = "drag" 130 | events[eventName]?.remove() 131 | events[eventName] = rectangle.addListener(eventName) { onDrag((it as JsMapMouseEvent).toMouseEvent()) } 132 | } 133 | set(onDragEnd) { 134 | val eventName = "dragend" 135 | events[eventName]?.remove() 136 | events[eventName] = rectangle.addListener(eventName) { onDragEnd((it as JsMapMouseEvent).toMouseEvent()) } 137 | } 138 | set(onDragStart) { 139 | val eventName = "dragstart" 140 | events[eventName]?.remove() 141 | events[eventName] = rectangle.addListener(eventName) { onDragStart((it as JsMapMouseEvent).toMouseEvent()) } 142 | } 143 | set(onMouseDown) { 144 | val eventName = "mousedown" 145 | events[eventName]?.remove() 146 | events[eventName] = rectangle.addListener(eventName) { onMouseDown((it as JsPolyMouseEvent).toPolyMouseEvent()) } 147 | } 148 | set(onMouseMove) { 149 | val eventName = "mousemove" 150 | events[eventName]?.remove() 151 | events[eventName] = rectangle.addListener(eventName) { onMouseMove((it as JsPolyMouseEvent).toPolyMouseEvent()) } 152 | } 153 | set(onMouseOut) { 154 | val eventName = "mouseout" 155 | events[eventName]?.remove() 156 | events[eventName] = rectangle.addListener(eventName) { onMouseOut((it as JsPolyMouseEvent).toPolyMouseEvent()) } 157 | } 158 | set(onMouseOver) { 159 | val eventName = "mouseover" 160 | events[eventName]?.remove() 161 | events[eventName] = rectangle.addListener(eventName) { onMouseOver((it as JsPolyMouseEvent).toPolyMouseEvent()) } 162 | } 163 | set(onMouseUp) { 164 | val eventName = "mouseup" 165 | events[eventName]?.remove() 166 | events[eventName] = rectangle.addListener(eventName) { onMouseUp((it as JsPolyMouseEvent).toPolyMouseEvent()) } 167 | } 168 | } 169 | ) 170 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/Coordinates.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject 2 | 3 | import com.chihsuanwu.maps.compose.web.LatLng 4 | import com.chihsuanwu.maps.compose.web.LatLngBounds 5 | import com.chihsuanwu.maps.compose.web.Point 6 | import com.chihsuanwu.maps.compose.web.Size 7 | import js.core.jso 8 | 9 | /** 10 | * A [google.maps.LatLng](https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLng) 11 | * object. 12 | */ 13 | internal external interface JsLatLng { 14 | var lat: () -> Double 15 | var lng: () -> Double 16 | } 17 | 18 | internal fun JsLatLng.toLatLng(): LatLng { 19 | return LatLng(lat(), lng()) 20 | } 21 | 22 | internal fun LatLng.toJsLatLng(): JsLatLng { 23 | val lat = this.lat 24 | val lng = this.lng 25 | return js("new google.maps.LatLng(lat, lng);") as JsLatLng 26 | } 27 | 28 | /** 29 | * A [google.maps.LatLngLiteral](https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngLiteral) 30 | * object. 31 | */ 32 | internal external interface JsLatLngLiteral { 33 | var lat: Double 34 | var lng: Double 35 | } 36 | 37 | internal fun LatLng.toJsLatLngLiteral(): JsLatLngLiteral { 38 | return jso { 39 | lat = this@toJsLatLngLiteral.lat 40 | lng = this@toJsLatLngLiteral.lng 41 | } 42 | } 43 | 44 | internal fun List.toJsLatLngLiteralArray(): Array { 45 | return map { it.toJsLatLngLiteral() }.toTypedArray() 46 | } 47 | 48 | /** 49 | * A [google.maps.LatLngBounds](https://developers.google.com/maps/documentation/javascript/reference/coordinates#LatLngBounds) 50 | * object. 51 | */ 52 | internal external interface JsLatLngBoundsLiteral { 53 | var east: Double 54 | var north: Double 55 | var south: Double 56 | var west: Double 57 | } 58 | 59 | internal fun LatLngBounds.toJsLatLngBoundsLiteral(): JsLatLngBoundsLiteral { 60 | return jso { 61 | east = this@toJsLatLngBoundsLiteral.east 62 | north = this@toJsLatLngBoundsLiteral.north 63 | south = this@toJsLatLngBoundsLiteral.south 64 | west = this@toJsLatLngBoundsLiteral.west 65 | } 66 | } 67 | 68 | /** 69 | * A [google.maps.Point](https://developers.google.com/maps/documentation/javascript/reference/coordinates#Point) 70 | */ 71 | internal external interface JsPoint { 72 | var x: Double 73 | var y: Double 74 | } 75 | 76 | internal fun Point.toJsPoint(): JsPoint { 77 | return jso { 78 | x = this@toJsPoint.x 79 | y = this@toJsPoint.y 80 | } 81 | } 82 | 83 | /** 84 | * A [google.maps.Size](https://developers.google.com/maps/documentation/javascript/reference/coordinates#Size) 85 | */ 86 | internal external interface JsSize { 87 | var height: Double 88 | var width: Double 89 | } 90 | 91 | internal fun Size.toJsSize(): JsSize { 92 | return jso { 93 | height = this@toJsSize.height 94 | width = this@toJsSize.width 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/Events.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject 2 | 3 | import com.chihsuanwu.maps.compose.web.IconMouseEvent 4 | import com.chihsuanwu.maps.compose.web.MapMouseEvent 5 | import com.chihsuanwu.maps.compose.web.PolyMouseEvent 6 | 7 | /** 8 | * [google.maps.MapsEventListener](https://developers.google.com/maps/documentation/javascript/reference/event#MapsEventListener) 9 | */ 10 | internal external interface MapsEventListener { 11 | fun remove() 12 | } 13 | 14 | internal external interface AddListener { 15 | /** 16 | * Adds the given listener function to the given event name. 17 | */ 18 | fun addListener(event: String, callback: (dynamic) -> Unit): MapsEventListener 19 | } 20 | 21 | /** 22 | * [google.maps.MapMouseEvent](https://developers.google.com/maps/documentation/javascript/reference/map#MapMouseEvent 23 | */ 24 | internal external interface JsMapMouseEvent { 25 | val latLng: JsLatLng 26 | } 27 | 28 | internal fun JsMapMouseEvent.toMouseEvent(): MapMouseEvent { 29 | return MapMouseEvent( 30 | latLng = latLng.toLatLng() 31 | ) 32 | } 33 | 34 | /** 35 | * [google.maps.IconMouseEvent](https://developers.google.com/maps/documentation/javascript/reference/map#IconMouseEvent) 36 | */ 37 | internal external interface JsIconMouseEvent : JsMapMouseEvent { 38 | val placeId: String 39 | } 40 | 41 | internal fun JsIconMouseEvent.toIconMouseEvent(): IconMouseEvent { 42 | return IconMouseEvent( 43 | latLng = latLng.toLatLng(), 44 | placeId = placeId 45 | ) 46 | } 47 | 48 | /** 49 | * [google.maps.PolyMouseEvent](https://developers.google.com/maps/documentation/javascript/reference/polygon#PolyMouseEvent) 50 | */ 51 | internal external interface JsPolyMouseEvent : JsMapMouseEvent { 52 | val edge: Int 53 | val path: Int 54 | val vertex: Int 55 | } 56 | 57 | internal fun JsPolyMouseEvent.toPolyMouseEvent(): PolyMouseEvent { 58 | return PolyMouseEvent( 59 | latLng = latLng.toLatLng(), 60 | edge = edge, 61 | path = path, 62 | vertex = vertex 63 | ) 64 | } 65 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/Map.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject 2 | 3 | import com.chihsuanwu.maps.compose.web.* 4 | import js.core.jso 5 | 6 | 7 | /** 8 | * A [google.maps.Map](https://developers.google.com/maps/documentation/javascript/reference/map) object. 9 | */ 10 | internal external interface MapView : AddListener { 11 | 12 | /** 13 | * Returns the position displayed at the center of the map. 14 | */ 15 | fun getCenter(): JsLatLng 16 | 17 | /** 18 | * Returns the zoom level of the map. 19 | */ 20 | fun getZoom(): Double 21 | 22 | /** 23 | * Immediately sets the map's camera to the target camera options, without animation. 24 | */ 25 | fun moveCamera(cameraOptions: CameraOptions) 26 | 27 | fun setCenter(center: JsLatLngLiteral) 28 | 29 | fun setOptions(options: JsMapOptions) 30 | 31 | fun setZoom(zoom: Double) 32 | } 33 | 34 | /** 35 | * Create a [MapView] object. 36 | * 37 | * @param id The id of the element to be used as the map container. 38 | */ 39 | internal fun newMap( 40 | id: String, 41 | options: JsMapOptions, 42 | ): MapView { 43 | return js("new google.maps.Map(document.getElementById(id), options);") as MapView 44 | } 45 | 46 | 47 | /** 48 | * A [google.maps.CameraOptions](https://developers.google.com/maps/documentation/javascript/reference/map#CameraOptions) 49 | * object. 50 | */ 51 | internal external interface CameraOptions { 52 | var center: JsLatLngLiteral 53 | var zoom: Double 54 | var tilt: Double 55 | var heading: Double 56 | } 57 | 58 | internal fun CameraPosition.toCameraOptions(): CameraOptions { 59 | return jso { 60 | center = this@toCameraOptions.center.toJsLatLngLiteral() 61 | zoom = this@toCameraOptions.zoom 62 | tilt = this@toCameraOptions.tilt 63 | heading = this@toCameraOptions.heading 64 | } 65 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/MapOptions.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject 2 | 3 | import com.chihsuanwu.maps.compose.web.* 4 | import js.core.jso 5 | 6 | 7 | /** 8 | * A [google.maps.MapOptions](https://developers.google.com/maps/documentation/javascript/reference/map#MapOptions) 9 | * object. 10 | * 11 | * Some properties that are duplicated in other controller classes are omitted here. 12 | */ 13 | internal external interface JsMapOptions { 14 | var backgroundColor: String 15 | var clickableIcons: Boolean 16 | var controlSize: Int 17 | var disableDefaultUI: Boolean 18 | var disableDoubleClickZoom: Boolean 19 | var draggableCursor: String 20 | var draggingCursor: String 21 | var fullscreenControl: Boolean 22 | var fullscreenControlOptions: JsFullscreenControlOptions? 23 | var gestureHandling: String 24 | var isFractionalZoomEnabled: Boolean 25 | var keyboardShortcuts: Boolean 26 | var mapId: String 27 | var mapTypeControl: Boolean 28 | var mapTypeControlOptions: JsMapTypeControlOptions? 29 | var mapTypeId: String 30 | var maxZoom: Double 31 | var minZoom: Double 32 | var noClear: Boolean 33 | var panControl: Boolean 34 | var panControlOptions: JsPanControlOptions? 35 | var restriction: JsMapRestriction? 36 | var rotateControl: Boolean 37 | var rotateControlOptions: JsRotateControlOptions? 38 | var scaleControl: Boolean 39 | var scaleControlOptions: dynamic 40 | var scrollwheel: Boolean 41 | var streetView: dynamic 42 | var streetViewControl: Boolean 43 | var streetViewControlOptions: JsStreetViewControlOptions? 44 | var styles: Array? 45 | var zoomControl: Boolean 46 | var zoomControlOptions: JsZoomControlOptions? 47 | } 48 | 49 | internal fun MapOptions.toJsMapOptions(): JsMapOptions { 50 | val opt = this 51 | return jso { 52 | opt.backgroundColor?.let { backgroundColor = it } 53 | opt.clickableIcons?.let { clickableIcons = it } 54 | opt.controlSize?.let { controlSize = it } 55 | opt.disableDefaultUI?.let { disableDefaultUI = it } 56 | opt.disableDoubleClickZoom?.let { disableDoubleClickZoom = it } 57 | opt.draggableCursor?.let { draggableCursor = it } 58 | opt.draggingCursor?.let { draggingCursor = it } 59 | opt.fullscreenControl?.let { fullscreenControl = it } 60 | fullscreenControlOptions = opt.fullscreenControlOptions?.toJsFullscreenControlOptions() 61 | opt.gestureHandling?.let { gestureHandling = it } 62 | opt.isFractionalZoomEnabled?.let { isFractionalZoomEnabled = it } 63 | opt.keyboardShortcuts?.let { keyboardShortcuts = it } 64 | opt.mapId?.let { mapId = it } 65 | opt.mapTypeControl?.let { mapTypeControl = it } 66 | mapTypeControlOptions = opt.mapTypeControlOptions?.toJsMapTypeControlOptions() 67 | opt.mapTypeId?.let { mapTypeId = it.toJsTypeIdString() } 68 | opt.maxZoom?.let { maxZoom = it } 69 | opt.minZoom?.let { minZoom = it } 70 | opt.noClear?.let { noClear = it } 71 | opt.panControl?.let { panControl = it } 72 | panControlOptions = opt.panControlOptions?.toJsPanControlOptions() 73 | restriction = opt.restriction?.toJsMapRestriction() 74 | opt.rotateControl?.let { rotateControl = it } 75 | rotateControlOptions = opt.rotateControlOptions?.toJsRotateControlOptions() 76 | opt.scaleControl?.let { scaleControl = it } 77 | scaleControlOptions = opt.scaleControlOptions 78 | opt.scrollwheel?.let { scrollwheel = it } 79 | streetView = opt.streetView 80 | opt.streetViewControl?.let { streetViewControl = it } 81 | streetViewControlOptions = opt.streetViewControlOptions?.toJsStreetViewControlOptions() 82 | styles = opt.styles?.styles?.map { it.toJsMapTypeStyle() }?.toTypedArray() 83 | opt.zoomControl?.let { zoomControl = it } 84 | zoomControlOptions = opt.zoomControlOptions?.toJsZoomControlOptions() 85 | } 86 | } 87 | 88 | /** 89 | * [google.maps.MapTypeStyle](https://developers.google.com/maps/documentation/javascript/reference/map#MapTypeStyle) 90 | */ 91 | internal external interface JsMapTypeStyle { 92 | var elementType: String 93 | var featureType: String 94 | var stylers: Array 95 | } 96 | 97 | internal fun MapTypeStyle.toJsMapTypeStyle(): JsMapTypeStyle { 98 | val style = this 99 | return jso { 100 | style.elementType?.let { elementType = it } 101 | style.featureType?.let { featureType = it } 102 | style.stylers?.let { stylers = it.toTypedArray() } 103 | } 104 | } 105 | 106 | /** 107 | * See [google.maps.MapTypeId](https://developers.google.com/maps/documentation/javascript/reference/map#MapTypeId) 108 | */ 109 | internal fun MapTypeId.toJsTypeIdString(): String { 110 | return when (this) { 111 | MapTypeId.Hybrid -> "hybrid" 112 | MapTypeId.Roadmap -> "roadmap" 113 | MapTypeId.Satellite -> "satellite" 114 | MapTypeId.Terrain -> "terrain" 115 | } 116 | } 117 | 118 | /** 119 | * [google.maps.FullscreenControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#FullscreenControlOptions) 120 | */ 121 | internal external interface JsFullscreenControlOptions { 122 | var position: JsControlPosition 123 | } 124 | 125 | internal fun FullscreenControlOptions.toJsFullscreenControlOptions(): JsFullscreenControlOptions { 126 | val opt = this 127 | return jso { 128 | opt.position?.let { position = it.toJsControlPosition() } 129 | } 130 | } 131 | 132 | /** 133 | * [google.maps.MapTypeControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlOptions) 134 | */ 135 | internal external interface JsMapTypeControlOptions { 136 | var mapTypeIds: Array 137 | var position: JsControlPosition 138 | var style: JsMapTypeControlStyle 139 | } 140 | 141 | internal fun MapTypeControlOptions.toJsMapTypeControlOptions(): JsMapTypeControlOptions { 142 | val opt = this 143 | return jso { 144 | opt.mapTypeIds?.let { mapTypeIds = it.map { it.toJsTypeIdString() }.toTypedArray() } 145 | opt.position?.let { position = it.toJsControlPosition() } 146 | opt.style?.let { style = it.toJsMapTypeControlStyle() } 147 | } 148 | } 149 | 150 | /** 151 | * [google.maps.MapTypeControlStyle](https://developers.google.com/maps/documentation/javascript/reference/control#MapTypeControlStyle) 152 | */ 153 | internal external interface JsMapTypeControlStyle 154 | 155 | internal fun MapTypeControlStyle.toJsMapTypeControlStyle(): JsMapTypeControlStyle { 156 | return when (this) { 157 | MapTypeControlStyle.Default -> js("google.maps.MapTypeControlStyle.DEFAULT") 158 | MapTypeControlStyle.DropdownMenu -> js("google.maps.MapTypeControlStyle.DROPDOWN_MENU") 159 | MapTypeControlStyle.HorizontalBar -> js("google.maps.MapTypeControlStyle.HORIZONTAL_BAR") 160 | } 161 | } 162 | 163 | /** 164 | * [google.maps.PanControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#PanControlOptions) 165 | */ 166 | internal external interface JsPanControlOptions { 167 | var position: JsControlPosition 168 | } 169 | 170 | internal fun PanControlOptions.toJsPanControlOptions(): JsPanControlOptions { 171 | val opt = this 172 | return jso { 173 | opt.position?.let { position = it.toJsControlPosition() } 174 | } 175 | } 176 | 177 | /** 178 | * [google.maps.RotateControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#RotateControlOptions) 179 | */ 180 | internal external interface JsRotateControlOptions { 181 | var position: JsControlPosition 182 | } 183 | 184 | internal fun RotateControlOptions.toJsRotateControlOptions(): JsRotateControlOptions { 185 | val opt = this 186 | return jso { 187 | opt.position?.let { position = it.toJsControlPosition() } 188 | } 189 | } 190 | 191 | /** 192 | * [google.maps.ScaleControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#ScaleControlOptions) 193 | */ 194 | internal external interface JsStreetViewControlOptions { 195 | var position: JsControlPosition 196 | } 197 | 198 | internal fun StreetViewControlOptions.toJsStreetViewControlOptions(): JsStreetViewControlOptions { 199 | val opt = this 200 | return jso { 201 | opt.position?.let { position = it.toJsControlPosition() } 202 | } 203 | } 204 | 205 | /** 206 | * [google.maps.ZoomControlOptions](https://developers.google.com/maps/documentation/javascript/reference/control#ZoomControlOptions) 207 | */ 208 | internal external interface JsZoomControlOptions { 209 | var position: JsControlPosition 210 | } 211 | 212 | internal fun ZoomControlOptions.toJsZoomControlOptions(): JsZoomControlOptions { 213 | val opt = this 214 | return jso { 215 | opt.position?.let { position = it.toJsControlPosition() } 216 | } 217 | } 218 | 219 | /** 220 | * [google.maps.ControlPosition](https://developers.google.com/maps/documentation/javascript/reference/control#ControlPosition) 221 | */ 222 | internal external interface JsControlPosition 223 | 224 | internal fun ControlPosition.toJsControlPosition(): JsControlPosition { 225 | return when (this) { 226 | ControlPosition.BottomCenter -> js("google.maps.ControlPosition.BOTTOM_CENTER") 227 | ControlPosition.BottomLeft -> js("google.maps.ControlPosition.BOTTOM_LEFT") 228 | ControlPosition.BottomRight -> js("google.maps.ControlPosition.BOTTOM_RIGHT") 229 | ControlPosition.LeftBottom -> js("google.maps.ControlPosition.LEFT_BOTTOM") 230 | ControlPosition.LeftCenter -> js("google.maps.ControlPosition.LEFT_CENTER") 231 | ControlPosition.LeftTop -> js("google.maps.ControlPosition.LEFT_TOP") 232 | ControlPosition.RightBottom -> js("google.maps.ControlPosition.RIGHT_BOTTOM") 233 | ControlPosition.RightCenter -> js("google.maps.ControlPosition.RIGHT_CENTER") 234 | ControlPosition.RightTop -> js("google.maps.ControlPosition.RIGHT_TOP") 235 | ControlPosition.TopCenter -> js("google.maps.ControlPosition.TOP_CENTER") 236 | ControlPosition.TopLeft -> js("google.maps.ControlPosition.TOP_LEFT") 237 | ControlPosition.TopRight -> js("google.maps.ControlPosition.TOP_RIGHT") 238 | } 239 | } 240 | 241 | internal external interface JsMapRestriction { 242 | var latLngBounds: JsLatLngBoundsLiteral 243 | var strictBounds: Boolean 244 | } 245 | 246 | internal fun MapRestriction.toJsMapRestriction(): JsMapRestriction { 247 | val restriction = this 248 | return jso { 249 | restriction.latLngBounds.let { latLngBounds = it.toJsLatLngBoundsLiteral() } 250 | restriction.strictBounds?.let { strictBounds = it } 251 | } 252 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/Circle.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 4 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngLiteral 5 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 6 | 7 | /** 8 | * A [google.maps.Circle](https://developers.google.com/maps/documentation/javascript/reference/polygon#Polygon.constructor) object 9 | */ 10 | internal external interface JsCircle : AddListener { 11 | 12 | fun setMap(map: MapView?) 13 | 14 | fun setOptions(options: CircleOptions) 15 | } 16 | 17 | internal fun newCircle(options: CircleOptions): JsCircle { 18 | return js("new google.maps.Circle(options);") as JsCircle 19 | } 20 | 21 | internal external interface CircleOptions { 22 | var center: JsLatLngLiteral 23 | var clickable: Boolean 24 | var draggable: Boolean 25 | var editable: Boolean 26 | var fillColor: String 27 | var fillOpacity: Double 28 | var map: MapView? 29 | var radius: Double 30 | var strokeColor: String 31 | var strokeOpacity: Double 32 | var strokePosition: dynamic 33 | var strokeWeight: Int 34 | var visible: Boolean 35 | var zIndex: Double? 36 | } 37 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/InfoWindow.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 4 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLng 5 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngLiteral 6 | import com.chihsuanwu.maps.compose.web.jsobject.JsSize 7 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 8 | import org.w3c.dom.Element 9 | 10 | internal external interface JsInfoWindow : AddListener { 11 | fun setContent(content: Element) 12 | fun setOptions(options: InfoWindowOptions) 13 | fun open(options: InfoWindowOpenOptions) 14 | fun close() 15 | } 16 | 17 | internal fun newInfoWindow(options: InfoWindowOptions): JsInfoWindow { 18 | return js("new google.maps.InfoWindow(options);") as JsInfoWindow 19 | } 20 | 21 | internal external interface InfoWindowOptions { 22 | var ariaLabel: String? 23 | var disableAutoPan: Boolean 24 | var maxWidth: Int? 25 | var minWidth: Int? 26 | var pixelOffset: JsSize? 27 | var position: JsLatLngLiteral 28 | var zIndex: Double? 29 | } 30 | 31 | internal external interface InfoWindowOpenOptions { 32 | var anchor: JsLatLng? 33 | var map: MapView? 34 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/Marker.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.drawing.MarkerAnimation 4 | import com.chihsuanwu.maps.compose.web.drawing.MarkerIcon 5 | import com.chihsuanwu.maps.compose.web.drawing.MarkerLabel 6 | import com.chihsuanwu.maps.compose.web.drawing.MarkerShape 7 | import com.chihsuanwu.maps.compose.web.jsobject.* 8 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 9 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLng 10 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngLiteral 11 | import com.chihsuanwu.maps.compose.web.jsobject.JsPoint 12 | import com.chihsuanwu.maps.compose.web.jsobject.JsSize 13 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 14 | import js.core.jso 15 | 16 | 17 | /** 18 | * A [google.maps.Marker](https://developers.google.com/maps/documentation/javascript/reference/marker#Marker.constructor) object 19 | */ 20 | internal external interface JsMarker : JsLatLng, AddListener { 21 | 22 | fun setMap(map: MapView?) 23 | fun setOptions(options: MarkerOptions) 24 | fun getMap(): MapView 25 | fun getPosition(): JsLatLng 26 | } 27 | 28 | internal fun newMarker(options: MarkerOptions): JsMarker { 29 | return js("new google.maps.Marker(options);") as JsMarker 30 | } 31 | 32 | /** 33 | * [google.maps.MarkerOptions](https://developers.google.com/maps/documentation/javascript/reference/marker#MarkerOptions) 34 | */ 35 | internal external interface MarkerOptions { 36 | var anchorPoint: JsPoint? 37 | var animation: JsAnimation? 38 | var clickable: Boolean 39 | var crossOnDrag: Boolean 40 | var cursor: String? 41 | var draggable: Boolean 42 | var icon: JsMarkerIcon? 43 | var label: JsMarkerLabel? 44 | var map: MapView? 45 | var opacity: Double 46 | var optimized: Boolean? 47 | var position: JsLatLngLiteral 48 | var shape: JsMarkerShape? 49 | var title: String? 50 | var visible: Boolean 51 | var zIndex: Double? 52 | } 53 | 54 | internal external interface JsAnimation 55 | 56 | internal fun MarkerAnimation.toJsAnimation(): JsAnimation { 57 | return when (this) { 58 | MarkerAnimation.DROP -> js("google.maps.Animation.DROP") as JsAnimation 59 | MarkerAnimation.BOUNCE -> js("google.maps.Animation.BOUNCE") as JsAnimation 60 | } 61 | } 62 | 63 | internal external interface JsMarkerIcon 64 | 65 | internal external interface JsURLIcon : JsMarkerIcon 66 | 67 | internal external interface JsIcon : JsMarkerIcon { 68 | var url: String 69 | var anchor: JsPoint? 70 | var labelOrigin: JsPoint? 71 | var origin: JsPoint? 72 | var scaledSize: JsSize? 73 | var size: JsSize? 74 | } 75 | 76 | internal external interface JsSymbol : JsMarkerIcon { 77 | var path: JsPath 78 | var anchor: JsPoint? 79 | var fillColor: String? 80 | var fillOpacity: Double? 81 | var labelOrigin: JsPoint? 82 | var rotation: Double? 83 | var scale: Double? 84 | var strokeColor: String? 85 | var strokeOpacity: Double? 86 | var strokeWeight: Double? 87 | } 88 | 89 | internal external interface JsPath 90 | internal external interface JsStringPath : JsPath 91 | internal external interface JsSymbolPath : JsPath 92 | 93 | internal fun MarkerIcon.Icon.toJsIcon(): JsIcon { 94 | val icon = this 95 | return jso { 96 | url = icon.url 97 | anchor = icon.anchor?.toJsPoint() 98 | labelOrigin = icon.labelOrigin?.toJsPoint() 99 | origin = icon.origin?.toJsPoint() 100 | scaledSize = icon.scaledSize?.toJsSize() 101 | size = icon.size?.toJsSize() 102 | } 103 | } 104 | 105 | internal fun MarkerIcon.Symbol.toJsSymbol(): JsSymbol { 106 | val symbol = this 107 | return jso { 108 | path = symbol.path.toJsPath() 109 | anchor = symbol.anchor?.toJsPoint() 110 | fillColor = symbol.fillColor 111 | fillOpacity = symbol.fillOpacity 112 | labelOrigin = symbol.labelOrigin?.toJsPoint() 113 | rotation = symbol.rotation 114 | scale = symbol.scale 115 | strokeColor = symbol.strokeColor 116 | strokeOpacity = symbol.strokeOpacity 117 | strokeWeight = symbol.strokeWeight 118 | } 119 | } 120 | 121 | internal fun MarkerIcon.Symbol.Path.toJsPath(): JsPath { 122 | return when (this) { 123 | is MarkerIcon.Symbol.Path.StringPath -> this.path as JsStringPath 124 | is MarkerIcon.Symbol.Path.SymbolPath -> when (this) { 125 | MarkerIcon.Symbol.Path.SymbolPath.BackwardClosedArrow -> js("google.maps.SymbolPath.BACKWARD_CLOSED_ARROW") as JsSymbolPath 126 | MarkerIcon.Symbol.Path.SymbolPath.BackwardOpenArrow -> js("google.maps.SymbolPath.BACKWARD_OPEN_ARROW") as JsSymbolPath 127 | MarkerIcon.Symbol.Path.SymbolPath.Circle -> js("google.maps.SymbolPath.CIRCLE") as JsSymbolPath 128 | MarkerIcon.Symbol.Path.SymbolPath.ForwardClosedArrow -> js("google.maps.SymbolPath.FORWARD_CLOSED_ARROW") as JsSymbolPath 129 | MarkerIcon.Symbol.Path.SymbolPath.ForwardOpenArrow -> js("google.maps.SymbolPath.FORWARD_OPEN_ARROW") as JsSymbolPath 130 | } 131 | } 132 | } 133 | 134 | internal fun MarkerIcon.toJsMarkerIcon(): JsMarkerIcon { 135 | return when (this) { 136 | is MarkerIcon.URL -> this.url as JsURLIcon 137 | is MarkerIcon.Icon -> toJsIcon() 138 | is MarkerIcon.Symbol -> toJsSymbol() 139 | } 140 | } 141 | 142 | internal external interface JsMarkerLabel 143 | 144 | internal external interface JsText : JsMarkerLabel 145 | 146 | internal external interface JsLabel : JsMarkerLabel { 147 | var text: String 148 | var className: String? 149 | var color: String? 150 | var fontFamily: String? 151 | var fontSize: String? 152 | var fontWeight: String? 153 | } 154 | 155 | internal fun MarkerLabel.Label.toJsLabel(): JsLabel { 156 | val label = this 157 | return jso { 158 | text = label.text 159 | className = label.className 160 | color = label.color 161 | fontFamily = label.fontFamily 162 | fontSize = label.fontSize 163 | fontWeight = label.fontWeight 164 | } 165 | } 166 | 167 | internal fun MarkerLabel.toJsMarkerLabel(): JsMarkerLabel { 168 | return when (this) { 169 | is MarkerLabel.Text -> this.text as JsText 170 | is MarkerLabel.Label -> toJsLabel() 171 | } 172 | } 173 | 174 | internal external interface JsMarkerShape { 175 | var coords: Array 176 | var type: String 177 | } 178 | 179 | internal fun MarkerShape.toJsMarkerShape(): JsMarkerShape { 180 | return jso { 181 | coords = this@toJsMarkerShape.coords.toTypedArray() 182 | type = when(this@toJsMarkerShape.type) { 183 | MarkerShape.Type.Circle -> "circle" 184 | MarkerShape.Type.Poly -> "poly" 185 | MarkerShape.Type.Rect -> "rect" 186 | } 187 | } 188 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/OverlayView.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.drawing.MapPanes 4 | import com.chihsuanwu.maps.compose.web.jsobject.* 5 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLng 6 | import com.chihsuanwu.maps.compose.web.jsobject.JsPoint 7 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 8 | import org.w3c.dom.Element 9 | 10 | internal external interface JSOverlayView { 11 | fun setMap(map: MapView?) 12 | fun getPanes(): JSMapPanes 13 | fun getProjection(): JSProjection 14 | 15 | var onAdd: () -> Unit 16 | var draw: () -> Unit 17 | var onRemove: () -> Unit 18 | } 19 | 20 | internal fun newOverlayView(): JSOverlayView { 21 | return js("new google.maps.OverlayView();") as JSOverlayView 22 | } 23 | 24 | internal external interface JSMapPanes { 25 | var floatPane: Element 26 | var mapPane: Element 27 | var markerLayer: Element 28 | var overlayLayer: Element 29 | var overlayMouseTarget: Element 30 | } 31 | 32 | internal fun JSMapPanes.getPane(pane: MapPanes): Element { 33 | return when (pane) { 34 | MapPanes.FloatPane -> floatPane 35 | MapPanes.MapPane -> mapPane 36 | MapPanes.MarkerLayer -> markerLayer 37 | MapPanes.OverlayLayer -> overlayLayer 38 | MapPanes.OverlayMouseTarget -> overlayMouseTarget 39 | } 40 | } 41 | 42 | internal external interface JSProjection { 43 | fun fromContainerPixelToLatLng(pixel: JsPoint): JsLatLng 44 | fun fromDivPixelToLatLng(pixel: JsPoint): JsLatLng 45 | fun fromLatLngToContainerPixel(latLng: JsLatLngLiteral): JsPoint 46 | fun fromLatLngToDivPixel(latLng: JsLatLngLiteral): JsPoint 47 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/Polygon.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.drawing.StrokePosition 4 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 5 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngLiteral 6 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 7 | 8 | /** 9 | * A [google.maps.Polygon](https://developers.google.com/maps/documentation/javascript/reference/polygon#Polygon.constructor) object 10 | */ 11 | internal external interface JsPolygon : AddListener { 12 | 13 | fun setMap(map: MapView?) 14 | 15 | fun setOptions(options: PolygonOptions) 16 | } 17 | 18 | internal fun newPolygon(options: PolygonOptions): JsPolygon { 19 | return js("new google.maps.Polygon(options);") as JsPolygon 20 | } 21 | 22 | internal external interface PolygonOptions { 23 | var clickable: Boolean 24 | var draggable: Boolean 25 | var editable: Boolean 26 | var fillColor: String 27 | var fillOpacity: Double 28 | var geodesic: Boolean 29 | var map: MapView? 30 | var paths: Array 31 | var strokeColor: String 32 | var strokeOpacity: Double 33 | var strokePosition: dynamic 34 | var strokeWeight: Int 35 | var visible: Boolean 36 | var zIndex: Double? 37 | } 38 | 39 | internal fun StrokePosition.toJs(): dynamic { 40 | return when (this) { 41 | StrokePosition.CENTER -> js("google.maps.StrokePosition.CENTER") 42 | StrokePosition.INSIDE -> js("google.maps.StrokePosition.INSIDE") 43 | StrokePosition.OUTSIDE -> js("google.maps.StrokePosition.OUTSIDE") 44 | } 45 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/Polyline.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.drawing.IconSequence 4 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 5 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngLiteral 6 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 7 | import js.core.jso 8 | 9 | /** 10 | * A [google.maps.Polyline](https://developers.google.com/maps/documentation/javascript/reference/polygon#Polyline.constructor) object 11 | */ 12 | internal external interface JsPolyline : AddListener { 13 | 14 | fun setMap(map: MapView?) 15 | 16 | fun setOptions(options: PolylineOptions) 17 | } 18 | 19 | internal fun newPolyline(options: PolylineOptions): JsPolyline { 20 | return js("new google.maps.Polyline(options);") as JsPolyline 21 | } 22 | 23 | /** 24 | * [google.maps.PolylineOptions](https://developers.google.com/maps/documentation/javascript/reference/polygon#PolylineOptions) 25 | */ 26 | internal external interface PolylineOptions { 27 | var clickable: Boolean 28 | var draggable: Boolean 29 | var editable: Boolean 30 | var geodesic: Boolean 31 | var icons: Array? 32 | var map: MapView? 33 | var path: Array 34 | var strokeColor: String 35 | var strokeOpacity: Double 36 | var strokeWeight: Int 37 | var visible: Boolean 38 | var zIndex: Double? 39 | } 40 | 41 | /** 42 | * [google.maps.IconSequence](https://developers.google.com/maps/documentation/javascript/reference/polygon#IconSequence) 43 | */ 44 | internal external interface JsIconSequence { 45 | var fixedRotation: Boolean? 46 | var icon: JsSymbol? 47 | var offset: String? 48 | var repeat: String? 49 | } 50 | 51 | internal fun IconSequence.toJsIconSequence(): JsIconSequence { 52 | val sequence = this 53 | return jso { 54 | fixedRotation = sequence.fixedRotation 55 | icon = sequence.icon?.toJsSymbol() 56 | offset = sequence.offset 57 | repeat = sequence.repeat 58 | } 59 | } 60 | 61 | internal fun List.toJsIconSequenceArray(): Array { 62 | return map { it.toJsIconSequence() }.toTypedArray() 63 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/drawing/Rectangle.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.drawing 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 4 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLngBoundsLiteral 5 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 6 | 7 | /** 8 | * A [google.maps.Rectangle](https://developers.google.com/maps/documentation/javascript/reference/polygon#Rectangle) object. 9 | */ 10 | internal external interface JsRectangle : AddListener { 11 | 12 | fun setMap(map: MapView?) 13 | 14 | fun setOptions(options: RectangleOptions) 15 | } 16 | 17 | internal fun newRectangle(options: RectangleOptions): JsRectangle { 18 | return js("new google.maps.Rectangle(options);") as JsRectangle 19 | } 20 | 21 | internal external interface RectangleOptions { 22 | var bounds: JsLatLngBoundsLiteral 23 | var clickable: Boolean 24 | var draggable: Boolean 25 | var editable: Boolean 26 | var fillColor: String 27 | var fillOpacity: Double 28 | var map: MapView? 29 | var strokeColor: String 30 | var strokeOpacity: Double 31 | var strokePosition: dynamic 32 | var strokeWeight: Int 33 | var visible: Boolean 34 | var zIndex: Double? 35 | } 36 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/layers/BicyclingLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.layers 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 4 | 5 | /** 6 | * [google.maps.BicyclingLayer](https://developers.google.com/maps/documentation/javascript/reference/map#BicyclingLayer) 7 | */ 8 | internal external interface JsBicyclingLayer { 9 | fun setMap(map: MapView?) 10 | } 11 | 12 | internal fun newBicyclingLayer(): JsBicyclingLayer { 13 | return js("new google.maps.BicyclingLayer();") as JsBicyclingLayer 14 | } 15 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/layers/HeatmapLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.layers 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.JsLatLng 4 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 5 | import com.chihsuanwu.maps.compose.web.jsobject.toJsLatLng 6 | import com.chihsuanwu.maps.compose.web.layers.WeightedLocation 7 | import js.core.jso 8 | 9 | /** 10 | * [google.maps.visualization.HeatmapLayer](https://developers.google.com/maps/documentation/javascript/reference/visualization#HeatmapLayer) 11 | */ 12 | internal external interface JsHeatmapLayer { 13 | fun setMap(map: Any?) 14 | fun setOptions(options: JsHeatmapLayerOptions) 15 | } 16 | 17 | internal fun newHeatmapLayer(options: JsHeatmapLayerOptions): JsHeatmapLayer { 18 | return js("new google.maps.visualization.HeatmapLayer(options);") as JsHeatmapLayer 19 | } 20 | 21 | internal external interface JsHeatmapLayerOptions { 22 | var data: Array 23 | var dissipating: Boolean 24 | var gradient: Array? 25 | var map: MapView? 26 | var maxIntensity: Int? 27 | var opacity: Double 28 | var radius: Int? 29 | } 30 | 31 | internal external interface JsWeightedLocation { 32 | var location: JsLatLng 33 | var weight: Number 34 | } 35 | 36 | internal fun WeightedLocation.toJsWeightedLocation(): JsWeightedLocation { 37 | return jso { 38 | location = this@toJsWeightedLocation.location.toJsLatLng() 39 | weight = this@toJsWeightedLocation.weight 40 | } 41 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/layers/KMLLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.layers 2 | 3 | import com.chihsuanwu.maps.compose.web.KMLMouseEvent 4 | import com.chihsuanwu.maps.compose.web.PolyMouseEvent 5 | import com.chihsuanwu.maps.compose.web.Size 6 | import com.chihsuanwu.maps.compose.web.jsobject.* 7 | import com.chihsuanwu.maps.compose.web.jsobject.AddListener 8 | import com.chihsuanwu.maps.compose.web.jsobject.JsMapMouseEvent 9 | import com.chihsuanwu.maps.compose.web.jsobject.JsPolyMouseEvent 10 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 11 | import com.chihsuanwu.maps.compose.web.jsobject.toLatLng 12 | import com.chihsuanwu.maps.compose.web.layers.KMLAuthor 13 | import com.chihsuanwu.maps.compose.web.layers.KMLFeatureData 14 | 15 | /** 16 | * [google.maps.KmlLayer](https://developers.google.com/maps/documentation/javascript/reference/kml#KmlLayer) 17 | */ 18 | internal external interface JsKMLLayer : AddListener { 19 | fun setMap(map: Any?) 20 | fun setOptions(options: JsKMLLayerOptions) 21 | } 22 | 23 | internal fun newKMLLayer(options: JsKMLLayerOptions): JsKMLLayer { 24 | return js("new google.maps.KmlLayer(options);") as JsKMLLayer 25 | } 26 | 27 | internal external interface JsKMLLayerOptions { 28 | var clickable: Boolean 29 | var map: MapView? 30 | var preserveViewport: Boolean 31 | var screenOverlays: Boolean 32 | var suppressInfoWindows: Boolean 33 | var url: String? 34 | var zIndex: Double? 35 | } 36 | 37 | /** 38 | * [google.maps.KmlMouseEvent](https://developers.google.com/maps/documentation/javascript/reference/kml#KmlMouseEvent) 39 | */ 40 | internal external interface JsKMLMouseEvent : JsMapMouseEvent { 41 | val featureData: dynamic 42 | val pixelOffset: JsSize 43 | } 44 | 45 | internal fun JsKMLMouseEvent.toKMLMouseEvent(): KMLMouseEvent { 46 | return KMLMouseEvent( 47 | featureData = KMLFeatureData( 48 | author = KMLAuthor( 49 | email = featureData.author.email as? String, 50 | name = featureData.author.name as? String, 51 | uri = featureData.author.uri as? String 52 | ), 53 | description = featureData.description as? String ?: "", 54 | id = featureData.id as? String ?: "", 55 | infoWindowHtml = featureData.infoWindowHtml as? String ?: "", 56 | name = featureData.name as? String ?: "", 57 | snippet = featureData.snippet as? String ?: "" 58 | ), 59 | latLng = latLng.toLatLng(), 60 | pixelOffset = Size(pixelOffset.width, pixelOffset.height) 61 | ) 62 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/layers/TrafficLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.layers 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 4 | 5 | /** 6 | * [google.maps.TrafficLayer](https://developers.google.com/maps/documentation/javascript/reference/map#TrafficLayer) 7 | */ 8 | internal external interface JsTrafficLayer { 9 | fun setMap(map: MapView?) 10 | fun setOptions(options: JsTrafficLayerOptions) 11 | } 12 | 13 | internal fun newTrafficLayer(options: JsTrafficLayerOptions): JsTrafficLayer { 14 | return js("new google.maps.TrafficLayer(options);") as JsTrafficLayer 15 | } 16 | 17 | internal external interface JsTrafficLayerOptions { 18 | var autoRefresh: Boolean? 19 | var map: MapView? 20 | } 21 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/jsobject/layers/TransitLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.jsobject.layers 2 | 3 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 4 | 5 | /** 6 | * [google.maps.TransitLayer](https://developers.google.com/maps/documentation/javascript/reference/map#TransitLayer) 7 | */ 8 | internal external interface JsTransitLayer { 9 | fun setMap(map: MapView?) 10 | } 11 | 12 | internal fun newTransitLayer(): JsTransitLayer { 13 | return js("new google.maps.TransitLayer();") as JsTransitLayer 14 | } 15 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/layers/BicyclingLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.layers 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.MapApplier 7 | import com.chihsuanwu.maps.compose.web.MapNode 8 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsBicyclingLayer 9 | import com.chihsuanwu.maps.compose.web.jsobject.layers.newBicyclingLayer 10 | 11 | internal class BicyclingLayerNode( 12 | val bicyclingLayer: JsBicyclingLayer 13 | ) : MapNode { 14 | override fun onRemoved() { 15 | bicyclingLayer.setMap(null) 16 | } 17 | } 18 | 19 | /** 20 | * A composable that adds a bicycling layer to the map. 21 | */ 22 | @Composable 23 | fun BicyclingLayer() { 24 | val mapApplier = currentComposer.applier as MapApplier? 25 | ComposeNode( 26 | factory = { 27 | val layer = newBicyclingLayer().apply { 28 | setMap(mapApplier?.map) 29 | } 30 | BicyclingLayerNode(layer) 31 | }, 32 | update = {} 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/layers/HeatmapLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.layers 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.GoogleMap 7 | import com.chihsuanwu.maps.compose.web.LatLng 8 | import com.chihsuanwu.maps.compose.web.MapApplier 9 | import com.chihsuanwu.maps.compose.web.MapNode 10 | import com.chihsuanwu.maps.compose.web.jsobject.MapView 11 | import com.chihsuanwu.maps.compose.web.jsobject.layers.* 12 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsHeatmapLayer 13 | import com.chihsuanwu.maps.compose.web.jsobject.layers.newHeatmapLayer 14 | import js.core.jso 15 | 16 | 17 | internal class HeatmapLayerNode( 18 | val heatmapLayer: JsHeatmapLayer 19 | ) : MapNode { 20 | override fun onRemoved() { 21 | heatmapLayer.setMap(null) 22 | } 23 | } 24 | 25 | /** 26 | * A composable that adds a heatmap layer to the map. 27 | * Note that the heatmap layer is part of the visualization library. You must include the visualization library in your 28 | * by adding 'libraries=visualization' to the 'extra' parameter of the [GoogleMap] composable. 29 | * 30 | * @param data The data points to display. 31 | * @param dissipating Whether heatmaps dissipate on zoom. By default, the radius of influence of a data point 32 | * is specified by the radius option only. 33 | * @param gradient The color gradient of the heatmap, specified as an array of CSS color strings. 34 | * @param maxIntensity The maximum intensity of the heatmap. 35 | * @param opacity The opacity of the heatmap. Defaults to 0.6. 36 | * @param radius The radius of influence for each data point, in pixels. 37 | */ 38 | @Composable 39 | fun HeatmapLayer( 40 | data: List, 41 | dissipating: Boolean = true, 42 | gradient: List? = null, 43 | maxIntensity: Int? = null, 44 | opacity: Double = 0.6, 45 | radius: Int? = null, 46 | ) { 47 | val mapApplier = currentComposer.applier as MapApplier? 48 | ComposeNode( 49 | factory = { 50 | val layer = newHeatmapLayer( 51 | jso { 52 | this.data = data.map { it.toJsWeightedLocation() }.toTypedArray() 53 | this.dissipating = dissipating 54 | this.gradient = gradient?.toTypedArray() 55 | this.map = mapApplier?.map 56 | this.maxIntensity = maxIntensity 57 | this.opacity = opacity 58 | this.radius = radius 59 | } 60 | ) 61 | HeatmapLayerNode(layer) 62 | }, 63 | update = { 64 | set(data) { 65 | heatmapLayer.setOptions(jso { this.data = data.map { it.toJsWeightedLocation() }.toTypedArray() }) 66 | } 67 | set(dissipating) { 68 | heatmapLayer.setOptions(jso { this.dissipating = dissipating }) 69 | } 70 | set(gradient) { 71 | heatmapLayer.setOptions(jso { this.gradient = gradient?.toTypedArray() }) 72 | } 73 | set(maxIntensity) { 74 | heatmapLayer.setOptions(jso { this.maxIntensity = maxIntensity }) 75 | } 76 | set(opacity) { 77 | heatmapLayer.setOptions(jso { this.opacity = opacity }) 78 | } 79 | set(radius) { 80 | heatmapLayer.setOptions(jso { this.radius = radius }) 81 | } 82 | } 83 | ) 84 | } 85 | 86 | class WeightedLocation( 87 | val location: LatLng, 88 | val weight: Double = 1.0, 89 | ) -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/layers/KMLLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.layers 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.KMLMouseEvent 7 | import com.chihsuanwu.maps.compose.web.MapApplier 8 | import com.chihsuanwu.maps.compose.web.MapNode 9 | import com.chihsuanwu.maps.compose.web.jsobject.MapsEventListener 10 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsKMLLayer 11 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsKMLMouseEvent 12 | import com.chihsuanwu.maps.compose.web.jsobject.layers.newKMLLayer 13 | import com.chihsuanwu.maps.compose.web.jsobject.layers.toKMLMouseEvent 14 | import js.core.jso 15 | 16 | class KMLFeatureData( 17 | val author: KMLAuthor, 18 | val description: String, 19 | val id: String, 20 | val infoWindowHtml: String, 21 | val name: String, 22 | val snippet: String 23 | ) 24 | 25 | class KMLAuthor( 26 | val email: String?, 27 | val name: String?, 28 | val uri: String? 29 | ) 30 | 31 | internal class KMLLayerNode( 32 | val kmlLayer: JsKMLLayer, 33 | var events: MutableMap 34 | ) : MapNode { 35 | override fun onRemoved() { 36 | kmlLayer.setMap(null) 37 | } 38 | } 39 | 40 | /** 41 | * A composable that adds a KML layer to the map. 42 | * 43 | * @param url The URL of the KML document to display. 44 | * @param clickable Whether the layer is clickable. 45 | * @param preserveViewport If this option is set to true or if the map's center and zoom were never set, 46 | * the input map is centered and zoomed to the bounding box of the contents of the layer. 47 | * @param screenOverlays Whether the layer should render screen overlays. 48 | * @param suppressInfoWindows Whether the layer should suppress info windows. 49 | * @param zIndex The zIndex of the layer. 50 | * 51 | * @param onClick Called when the user clicks on a feature. 52 | * @param onDefaultViewportChanged Called when the default viewport of the layer has changed. 53 | * @param onStatusChanged Called when the status of the layer has changed. 54 | */ 55 | @Composable 56 | fun KMLLayer( 57 | url: String, 58 | clickable: Boolean = true, 59 | preserveViewport: Boolean = false, 60 | screenOverlays: Boolean = true, 61 | suppressInfoWindows: Boolean = false, 62 | zIndex: Double? = null, 63 | onClick: (KMLMouseEvent) -> Unit = {}, 64 | onDefaultViewportChanged: () -> Unit = {}, 65 | onStatusChanged: () -> Unit = {}, 66 | ) { 67 | val mapApplier = currentComposer.applier as MapApplier? 68 | ComposeNode( 69 | factory = { 70 | val layer = newKMLLayer( 71 | jso { 72 | this.clickable = clickable 73 | this.map = mapApplier?.map 74 | this.preserveViewport = preserveViewport 75 | this.screenOverlays = screenOverlays 76 | this.suppressInfoWindows = suppressInfoWindows 77 | this.url = url 78 | this.zIndex = zIndex 79 | } 80 | ) 81 | KMLLayerNode(layer, mutableMapOf()) 82 | }, 83 | update = { 84 | set(clickable) { 85 | kmlLayer.setOptions(jso { this.clickable = clickable }) 86 | } 87 | set(preserveViewport) { 88 | kmlLayer.setOptions(jso { this.preserveViewport = preserveViewport }) 89 | } 90 | set(screenOverlays) { 91 | kmlLayer.setOptions(jso { this.screenOverlays = screenOverlays }) 92 | } 93 | set(suppressInfoWindows) { 94 | kmlLayer.setOptions(jso { this.suppressInfoWindows = suppressInfoWindows }) 95 | } 96 | set(url) { 97 | kmlLayer.setOptions(jso { this.url = url }) 98 | } 99 | set(zIndex) { 100 | kmlLayer.setOptions(jso { this.zIndex = zIndex }) 101 | } 102 | set(onClick) { 103 | val eventName = "click" 104 | events[eventName]?.remove() 105 | events[eventName] = kmlLayer.addListener(eventName) { onClick((it as JsKMLMouseEvent).toKMLMouseEvent()) } 106 | } 107 | set(onDefaultViewportChanged) { 108 | val eventName = "defaultviewport_changed" 109 | events[eventName]?.remove() 110 | events[eventName] = kmlLayer.addListener(eventName) { onDefaultViewportChanged() } 111 | } 112 | set(onStatusChanged) { 113 | val eventName = "status_changed" 114 | events[eventName]?.remove() 115 | events[eventName] = kmlLayer.addListener(eventName) { onStatusChanged() } 116 | } 117 | } 118 | ) 119 | } -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/layers/TrafficLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.layers 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.MapApplier 7 | import com.chihsuanwu.maps.compose.web.MapNode 8 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsTrafficLayer 9 | import com.chihsuanwu.maps.compose.web.jsobject.layers.newTrafficLayer 10 | import js.core.jso 11 | 12 | 13 | internal class TrafficLayerNode( 14 | val trafficLayer: JsTrafficLayer 15 | ) : MapNode { 16 | override fun onRemoved() { 17 | trafficLayer.setMap(null) 18 | } 19 | } 20 | 21 | /** 22 | * A composable that adds a traffic layer to the map. 23 | * 24 | * @param autoRefresh Whether the layer will update automatically. 25 | */ 26 | @Composable 27 | fun TrafficLayer( 28 | autoRefresh: Boolean = true, 29 | ) { 30 | val mapApplier = currentComposer.applier as MapApplier? 31 | ComposeNode( 32 | factory = { 33 | val layer = newTrafficLayer( 34 | jso { 35 | this.autoRefresh = autoRefresh 36 | this.map = mapApplier?.map 37 | } 38 | ) 39 | TrafficLayerNode(layer) 40 | }, 41 | update = { 42 | set(autoRefresh) { 43 | trafficLayer.setOptions(jso { this.autoRefresh = autoRefresh }) 44 | } 45 | } 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /maps-compose-web/src/jsMain/kotlin/com/chihsuanwu/maps/compose/web/layers/TransitLayer.kt: -------------------------------------------------------------------------------- 1 | package com.chihsuanwu.maps.compose.web.layers 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.ComposeNode 5 | import androidx.compose.runtime.currentComposer 6 | import com.chihsuanwu.maps.compose.web.MapApplier 7 | import com.chihsuanwu.maps.compose.web.MapNode 8 | import com.chihsuanwu.maps.compose.web.jsobject.layers.JsTransitLayer 9 | import com.chihsuanwu.maps.compose.web.jsobject.layers.newTransitLayer 10 | 11 | 12 | internal class TransitLayerNode( 13 | val transitLayer: JsTransitLayer, 14 | ) : MapNode { 15 | override fun onRemoved() { 16 | transitLayer.setMap(null) 17 | } 18 | } 19 | 20 | /** 21 | * A composable that adds a transit layer to the map. 22 | */ 23 | @Composable 24 | fun TransitLayer() { 25 | val mapApplier = currentComposer.applier as MapApplier? 26 | ComposeNode( 27 | factory = { 28 | val layer = newTransitLayer().apply { 29 | setMap(mapApplier?.map) 30 | } 31 | TransitLayerNode(layer) 32 | }, 33 | update = {} 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 7 | } 8 | 9 | plugins { 10 | kotlin("multiplatform") version "1.9.10" 11 | id("org.jetbrains.compose") version "1.5.1" 12 | } 13 | } 14 | 15 | rootProject.name = "google-maps-compose-web" 16 | 17 | include(":maps-compose-web", ":example") 18 | --------------------------------------------------------------------------------