├── .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 |
5 |
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/#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 |
--------------------------------------------------------------------------------