diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c30fad06..3573815a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -33,7 +33,22 @@ "Bash(ls /Users/masashi/android-sdk/mapconductor-for-*/README.md /Users/masashi/android-sdk/mapconductor-for-*/docs/)", "Read(//Users/masashi/Desktop/ios-sdk/ios-for-googlemaps/**)", "Bash(xargs -I{} sh -c 'echo \"=== {} ===\" && cat \"{}\"')", - "Bash(xargs -I{} sh -c 'echo \"=== {} ===\" && grep -E \"name:|url:\" \"{}\" | head -5')" + "Bash(xargs -I{} sh -c 'echo \"=== {} ===\" && grep -E \"name:|url:\" \"{}\" | head -5')", + "Bash(xargs ls *)", + "Bash(unzip *)", + "Bash(/Users/masashi/.cargo/bin/cargo new *)", + "Bash(/Users/masashi/.cargo/bin/cargo test *)", + "Bash(~/.cargo/bin/rustup target *)", + "Bash(~/.cargo/bin/cargo build *)", + "Bash(codex --version)", + "Bash(swift build *)", + "Bash(xcodebuild -workspace /Users/masashi/Desktop/ios-sdk/ios-sdk-core/Package.swift -scheme MapConductorCore -destination generic/platform=iOS build)", + "Bash(xcodebuild -project /Users/masashi/Desktop/ios-sdk/.swiftpm/xcode/package.xcworkspace build ONLY_ACTIVE_ARCH=YES -destination 'generic/platform=iOS Simulator')", + "Bash(xcodebuild -sdk iphonesimulator -scheme \"mapconductor-for-arcgis\" -destination \"generic/platform=iOS Simulator\" build)", + "Bash(xcodebuild -scheme \"ios-sdk-core\" -destination \"generic/platform=iOS Simulator\" build)", + "Bash(xcodebuild -list)", + "Bash(xcodebuild -scheme MapConductorSampleApp -destination \"generic/platform=iOS Simulator\" build)", + "Bash(xcodebuild -workspace MapConductorSampleApp.xcworkspace -scheme MapConductorSampleApp -destination 'generic/platform=iOS Simulator' build)" ], "deny": [], "ask": [] diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 13bdc5db..a0b718bb 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -40,7 +40,10 @@ jobs: fi - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + with: + submodules: recursive + token: ${{ secrets.GITHUB_TOKEN }} - name: Write gradle.properties run: | @@ -54,15 +57,15 @@ jobs: echo "NDK_VERSION=${NDK_VERSION}" >> $GITHUB_ENV - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: 'temurin' java-version: 17 - - name: Install Android NDK $NDK_VERSION - uses: android-actions/setup-android@v3 - with: - packages: "build-tools;36.0.0 ndk;${{ env.NDK_VERSION }} platform-tools platforms;android-35" +# - name: Install Android NDK $NDK_VERSION +# uses: android-actions/setup-android@v3 +# with: +# packages: "build-tools;36.0.0 ndk;${{ env.NDK_VERSION }} platform-tools platforms;android-35" - name: Read module list from projects.properties id: define @@ -73,7 +76,7 @@ jobs: # echo "here_sdk_artifact=$here_sdk_name" >> $GITHUB_OUTPUT - name: Restore hash cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: key: lint-hash-cache-${{ github.base_ref || 'main' }} path: .lint-hash-cache/ @@ -168,7 +171,7 @@ jobs: - name: Save hash cache if: success() - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: key: lint-hash-cache-${{ github.base_ref || 'main' }}-${{ github.run_id }} path: .lint-hash-cache/ diff --git a/.github/workflows/publish-maven-central-only-bom.yml b/.github/workflows/publish-maven-central-only-bom.yml index c7108584..fd72393b 100644 --- a/.github/workflows/publish-maven-central-only-bom.yml +++ b/.github/workflows/publish-maven-central-only-bom.yml @@ -18,13 +18,13 @@ jobs: ANDROID_HOME: /usr/local/lib/android/sdk steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: submodules: recursive token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: '17' @@ -94,7 +94,7 @@ jobs: echo " Already published (remote version ${remote_version}); skipping." else echo " Will publish local version ${local_version} (remote='${remote_version}')." - publish_tasks+=(":${module}:publishReleasePublicationToCentralPortal") + publish_tasks+=(":${module}:publishAllPublicationsToCentralPortal") fi done diff --git a/.github/workflows/publish-maven-central.yml b/.github/workflows/publish-maven-central.yml.disabled similarity index 97% rename from .github/workflows/publish-maven-central.yml rename to .github/workflows/publish-maven-central.yml.disabled index 4cb608b2..338ff389 100644 --- a/.github/workflows/publish-maven-central.yml +++ b/.github/workflows/publish-maven-central.yml.disabled @@ -19,7 +19,7 @@ jobs: ANDROID_HOME: /usr/local/lib/android/sdk steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: submodules: recursive token: ${{ secrets.GITHUB_TOKEN }} @@ -82,6 +82,7 @@ jobs: android-icons android-marker-clustering android-heatmap + android-geojson-layer mapconductor-bom android-sdk-core android-for-arcgis @@ -134,7 +135,7 @@ jobs: echo " Already published (remote version ${remote_version}); skipping." else echo " Will publish local version ${local_version} (remote='${remote_version}')." - publish_tasks+=(":${module}:publishReleasePublicationToCentralPortal") + publish_tasks+=(":${module}:publishAllPublicationsToCentralPortal") fi done diff --git a/.gitmodules b/.gitmodules index 42262c8e..71aadc5b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -12,7 +12,7 @@ url = git@github.com:MapConductor/android-for-maplibre.git [submodule "android-for-mapbox"] path = android-for-mapbox - url = git@github.com:MapConductor/android-for-mapbox.git + url = git@github.com:MapConductor/android-for-mapbox.git [submodule "android-for-here"] path = android-for-here url = git@github.com:MapConductor/android-for-here.git @@ -25,3 +25,6 @@ [submodule "android-icons"] path = android-icons url = git@github.com:MapConductor/android-icons.git +[submodule "android-geojson-layer"] + path = android-geojson-layer + url = git@github.com:MapConductor/android-geojson-layer.git diff --git a/README.md b/README.md index 9632f877..f1d7cff9 100644 --- a/README.md +++ b/README.md @@ -11,18 +11,19 @@ A unified mapping library that provides a common API for multiple map providers ## Module Structure -| Module | Artifact | Description | -|---|---|---| -| `mapconductor-bom` | `com.mapconductor:mapconductor-bom` | Bill of Materials — aligns all module versions | -| `android-sdk-core` | `com.mapconductor:core` | Core abstractions, geometry types, overlay states | -| `android-for-googlemaps` | `com.mapconductor:for-googlemaps` | Google Maps implementation | -| `android-for-mapbox` | `com.mapconductor:for-mapbox` | Mapbox implementation | -| `android-for-here` | `com.mapconductor:for-here` | HERE Maps implementation | -| `android-for-arcgis` | `com.mapconductor:for-arcgis` | ArcGIS implementation | -| `android-for-maplibre` | `com.mapconductor:for-maplibre` | MapLibre implementation | -| `android-icons` | `com.mapconductor:icons` | Composable marker icons (CircleIcon, FlagIcon, info bubbles) | -| `android-heatmap` | `com.mapconductor:heatmap` | Map-provider-agnostic heatmap overlay | -| `android-marker-clustering` | `com.mapconductor:marker-clustering` | Automatic marker clustering across all providers | +| Module | Artifact | Description | +|-----------------------------|--------------------------------------|--------------------------------------------------------------| +| `mapconductor-bom` | `com.mapconductor:mapconductor-bom` | Bill of Materials — aligns all module versions | +| `android-sdk-core` | `com.mapconductor:core` | Core abstractions, geometry types, overlay states | +| `android-for-googlemaps` | `com.mapconductor:for-googlemaps` | Google Maps implementation | +| `android-for-mapbox` | `com.mapconductor:for-mapbox` | Mapbox implementation | +| `android-for-here` | `com.mapconductor:for-here` | HERE Maps implementation | +| `android-for-arcgis` | `com.mapconductor:for-arcgis` | ArcGIS implementation | +| `android-for-maplibre` | `com.mapconductor:for-maplibre` | MapLibre implementation | +| `android-icons` | `com.mapconductor:icons` | Composable marker icons (CircleIcon, FlagIcon, info bubbles) | +| `android-heatmap` | `com.mapconductor:heatmap` | Map-provider-agnostic heatmap overlay | +| `android-marker-clustering` | `com.mapconductor:marker-clustering` | Automatic marker clustering across all providers | +| `android-geojson-layer` | `com.mapconductor:geojson-layer` | Geojson layer across all providers | ## Quick Start diff --git a/android-for-arcgis b/android-for-arcgis index f54bfdb5..89496f9f 160000 --- a/android-for-arcgis +++ b/android-for-arcgis @@ -1 +1 @@ -Subproject commit f54bfdb55c61022843afeaa682eeaa0fcd8279c1 +Subproject commit 89496f9f4502025b38baedfba1cd2818face8fc2 diff --git a/android-for-googlemaps b/android-for-googlemaps index bb70695f..3791a7be 160000 --- a/android-for-googlemaps +++ b/android-for-googlemaps @@ -1 +1 @@ -Subproject commit bb70695f98f500512fc5bdc0fd0abdcf1857bd8c +Subproject commit 3791a7bed76d981252b8d988a698486a090d8a4d diff --git a/android-for-here b/android-for-here index c3915085..efbbee9a 160000 --- a/android-for-here +++ b/android-for-here @@ -1 +1 @@ -Subproject commit c3915085f057320a90428f83d6e0d1c25c4cc58d +Subproject commit efbbee9ab5a0cad79f034247ae247019e4ee4eeb diff --git a/android-for-mapbox b/android-for-mapbox index bc4f6899..ae4d80c4 160000 --- a/android-for-mapbox +++ b/android-for-mapbox @@ -1 +1 @@ -Subproject commit bc4f6899661896a81dbe4222eed8abb764c02c46 +Subproject commit ae4d80c4ca5d8766accc79df9cc2ef1bedfcc864 diff --git a/android-for-maplibre b/android-for-maplibre index fbe67094..411d63db 160000 --- a/android-for-maplibre +++ b/android-for-maplibre @@ -1 +1 @@ -Subproject commit fbe67094d70cb7160bcce2ab307da9fe762096df +Subproject commit 411d63db8a1b664bac5ea7372ef8e003fca64a48 diff --git a/android-geojson-layer b/android-geojson-layer new file mode 160000 index 00000000..e75c70ba --- /dev/null +++ b/android-geojson-layer @@ -0,0 +1 @@ +Subproject commit e75c70bae64ad828384fa2de6aa5ff6189107af4 diff --git a/android-heatmap b/android-heatmap index b2fca674..8da0aa6a 160000 --- a/android-heatmap +++ b/android-heatmap @@ -1 +1 @@ -Subproject commit b2fca6743dd17d9351574826d9d877ea1e65081c +Subproject commit 8da0aa6aceb7849b396d1c415c7c60b8bb3a04cf diff --git a/android-icons b/android-icons index b246d0fe..376149ed 160000 --- a/android-icons +++ b/android-icons @@ -1 +1 @@ -Subproject commit b246d0feb4da18fed680ef7876d5b35d8f70d6f2 +Subproject commit 376149ed016e2a583943397da65abc5a95ef8013 diff --git a/android-marker-clustering b/android-marker-clustering index e0a3e584..4d922f8b 160000 --- a/android-marker-clustering +++ b/android-marker-clustering @@ -1 +1 @@ -Subproject commit e0a3e58438be5f68248a9479bb4db0fd7b174e1f +Subproject commit 4d922f8b0afadf0935d6adac7494029010a7a27b diff --git a/android-sdk-core b/android-sdk-core index 6f9e34a7..7b97f149 160000 --- a/android-sdk-core +++ b/android-sdk-core @@ -1 +1 @@ -Subproject commit 6f9e34a7e225d0320cb70621525b5fa739e0c80b +Subproject commit 7b97f149e26d6663aacca140b215805dadf0d35a diff --git a/build.gradle.kts b/build.gradle.kts index d251c3f5..00a20bbf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { alias(libs.plugins.kotlin.jvm) apply false alias(libs.plugins.android.library) apply false alias(libs.plugins.jlleitschuh.ktlint) apply false - id("com.gradleup.nmcp") version "0.0.8" + id("com.gradleup.nmcp") version "1.5.0" apply false } buildscript { @@ -71,12 +71,16 @@ tasks.register("allLintChecks") { // Publishing tasks for all modules val publishableModules = listOf( "mapconductor-bom", - "android-core", + "android-sdk-core", + "android-icons", + "android-marker-clustering", + "android-heatmap", + "android-geojson-layer", "android-for-arcgis", "android-for-googlemaps", "android-for-here", "android-for-mapbox", - "android-icons" + "android-for-maplibre" ) tasks.register("publishAllLocal") { @@ -112,12 +116,3 @@ tasks.register("publishAllToMavenCentral") { dependsOn(publishTasks) } -// Central Portal configuration -nmcp { - publishAllProjectsProbablyBreakingProjectIsolation { - username = findProperty("ossrh.username") as String? ?: System.getenv("OSSRH_USERNAME") - password = findProperty("ossrh.password") as String? ?: System.getenv("OSSRH_PASSWORD") - // All publications from all subprojects will be published - } -} - diff --git a/camera-sync-map-app/src/main/assets/polygon_building.geojson.zip b/camera-sync-map-app/src/main/assets/polygon_building.geojson.zip deleted file mode 100644 index 8c7b75a0..00000000 Binary files a/camera-sync-map-app/src/main/assets/polygon_building.geojson.zip and /dev/null differ diff --git a/docs/astro.config.ts b/docs/astro.config.ts index 7e8259fc..bd8aba83 100644 --- a/docs/astro.config.ts +++ b/docs/astro.config.ts @@ -13,8 +13,14 @@ export default defineConfig({ integrations: [ postBuildIntegration(), starlight({ - title: 'MapConductor', - description: 'A unified map SDK for mobile developers', + title: { + en: "MapConductor — a unified Android maps SDK that lets you work with multiple map providers through a single API", + ja: "MapConductor(マップコンダクター) ♪ 複数の地図プロバイダを単一のAPIで扱えるAndroid向け統一地図SDK", + "es-419": "MapConductor — Un SDK de mapas unificado para Android que ofrece una API común para múltiples proveedores de mapas", + }, + markdown: { + headingLinks: false, + }, defaultLocale: 'root', locales: { root: { diff --git a/example-app/build.gradle.kts b/example-app/build.gradle.kts index 603a2fa8..94d0e106 100644 --- a/example-app/build.gradle.kts +++ b/example-app/build.gradle.kts @@ -1,6 +1,7 @@ -plugins { +import java.util.Properties + +plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) id("org.jlleitschuh.gradle.ktlint") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" @@ -46,13 +47,14 @@ android { buildTypes { - debug { + create("local") { + initWith(getByName("debug")) + isDebuggable = true isMinifyEnabled = false + } - proguardFiles( - getDefaultProguardFile("proguard-android.txt"), - "proguard-rules.pro", - ) + debug { + isMinifyEnabled = false } release { @@ -63,6 +65,7 @@ android { proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", + "proguard-rules-release.pro", ) buildConfigField("String", "BUILD_CONFIG_VERSION", "\"release\"") @@ -132,14 +135,13 @@ android { } } -kotlin { - compilerOptions { - jvmTarget.set( - org.jetbrains.kotlin.gradle.dsl.JvmTarget.fromTarget( - project.property("jvmTarget").toString(), - ), - ) - } +// Read libraryVersion from each module's gradle.properties for the 'local' build type +fun localVersion(moduleDir: String): String { + val props = Properties() + val propsFile = rootProject.file("$moduleDir/gradle.properties") + if (!propsFile.exists()) return "1.0.0" + propsFile.inputStream().use { stream -> props.load(stream) } + return props.getProperty("libraryVersion") ?: "1.0.0" } dependencies { @@ -157,7 +159,7 @@ dependencies { implementation(libs.androidx.appcompat) // Google Maps SDK - implementation(libs.play.services.maps) +// implementation(libs.play.services.maps) // Here Maps SDK implementation( @@ -169,34 +171,9 @@ dependencies { ), ) - // Mapbox SDK - implementation(libs.mapbox.android) - - // ArcGIS Maps for Kotlin - SDK dependency - implementation(libs.arcgis.maps.kotlin) - implementation(platform(libs.arcgis.maps.kotlin.toolkit.bom)) - implementation(libs.arcgis.maps.kotlin.toolkit.geoview.compose) - implementation(libs.arcgis.maps.kotlin.toolkit.authentication) - - // MapLibre SDK - implementation(libs.maplibre.sdk) - implementation(libs.maplibre.annotation) - implementation(libs.androidx.compose.ui.graphics) - implementation(libs.androidx.compose.foundation) - - // Map Conductor -// implementation("com.mapconductor:core") -// implementation("com.mapconductor:icons") -// implementation("com.mapconductor:for-googlemaps") -// implementation("com.mapconductor:for-here") -// implementation("com.mapconductor:for-mapbox") -// implementation("com.mapconductor:for-arcgis") -// implementation("com.mapconductor:marker-strategy") -// implementation("com.mapconductor:marker-native-strategy") - // Use project dependency for debug, Maven artifact for release // Align versions in release via the project BOM - releaseImplementation(platform(project(":mapconductor-bom"))) +// releaseImplementation(platform(project(":mapconductor-bom"))) releaseImplementation(libs.mapconductor.core) releaseImplementation(libs.mapconductor.icons) releaseImplementation(libs.mapconductor.googlemaps) @@ -207,16 +184,34 @@ dependencies { releaseImplementation(libs.mapconductor.marker.strategy) releaseImplementation(libs.mapconductor.marker.native.strategy) releaseImplementation(libs.mapconductor.marker.clustering) + releaseImplementation(libs.mapconductor.geojson) debugImplementation(project(":android-sdk-core")) debugImplementation(project(":android-icons")) debugImplementation(project(":android-for-googlemaps")) debugImplementation(project(":android-for-here")) debugImplementation(project(":android-for-mapbox")) - debugImplementation(project(":android-for-arcgis")) debugImplementation(project(":android-for-maplibre")) + debugImplementation(project(":android-for-arcgis")) debugImplementation(project(":android-marker-clustering")) debugImplementation(project(":android-heatmap")) + debugImplementation(project(":android-geojson-layer")) + + // local build type: uses MavenLocal published artifacts (published by publishAllLocal) + // Map SDKs must be declared explicitly because published AARs expose them as runtime-only scope + "localImplementation"(libs.play.services.maps) + "localImplementation"(libs.mapbox.android) + "localImplementation"(libs.maplibre.sdk) + "localImplementation"("com.mapconductor:core:${localVersion("android-sdk-core")}") + "localImplementation"("com.mapconductor:icons:${localVersion("android-icons")}") + "localImplementation"("com.mapconductor:for-googlemaps:${localVersion("android-for-googlemaps")}") + "localImplementation"("com.mapconductor:for-here:${localVersion("android-for-here")}") + "localImplementation"("com.mapconductor:for-mapbox:${localVersion("android-for-mapbox")}") + "localImplementation"("com.mapconductor:for-arcgis:${localVersion("android-for-arcgis")}") + "localImplementation"("com.mapconductor:for-maplibre:${localVersion("android-for-maplibre")}") + "localImplementation"("com.mapconductor:marker-clustering:${localVersion("android-marker-clustering")}") + "localImplementation"("com.mapconductor:heatmap:${localVersion("android-heatmap")}") + "localImplementation"("com.mapconductor:geojson:${localVersion("android-geojson-layer")}") implementation(libs.androidx.vectordrawable) testImplementation(libs.junit) diff --git a/example-app/proguard-rules-release.pro b/example-app/proguard-rules-release.pro new file mode 100644 index 00000000..831e4cc1 --- /dev/null +++ b/example-app/proguard-rules-release.pro @@ -0,0 +1,6 @@ +# Release-only rules: strip verbose logging +-assumenosideeffects class android.util.Log { + public static *** d(...); + public static *** v(...); + public static *** i(...); +} diff --git a/example-app/proguard-rules.pro b/example-app/proguard-rules.pro index 5631ff4e..169e3d05 100644 --- a/example-app/proguard-rules.pro +++ b/example-app/proguard-rules.pro @@ -56,10 +56,3 @@ java.lang.Object writeReplace(); java.lang.Object readResolve(); } - -# Remove logging in release builds --assumenosideeffects class android.util.Log { - public static *** d(...); - public static *** v(...); - public static *** i(...); -} diff --git a/example-app/src/main/assets/N02-22_GML.zip b/example-app/src/main/assets/N02-22_GML.zip new file mode 100644 index 00000000..7f106e55 Binary files /dev/null and b/example-app/src/main/assets/N02-22_GML.zip differ diff --git a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt index caf96282..9561417f 100644 --- a/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt +++ b/example-app/src/main/java/com/mapconductor/example/DemoAppScreen.kt @@ -12,6 +12,8 @@ import androidx.core.content.ContextCompat import com.mapconductor.core.marker.ImageIcon import com.mapconductor.example.navigation.NavigationViewModel import com.mapconductor.example.pages.circle.CircleMapPage +import com.mapconductor.example.pages.geojson.basic.BasicGeoJSONMapPage +import com.mapconductor.example.pages.geojson.layer.GeoJSONLayerMapPage import com.mapconductor.example.pages.groundimage.GroundImageMapPage import com.mapconductor.example.pages.groundimage.GroundImageResources import com.mapconductor.example.pages.heatmaplayer.HeatmapLayerPage @@ -19,11 +21,13 @@ import com.mapconductor.example.pages.infobubble.MultipleBubblesPage import com.mapconductor.example.pages.infobubble.RichContentBubblePage import com.mapconductor.example.pages.infobubble.SimpleTextBubblePage import com.mapconductor.example.pages.infobubble.StyledInfoBubblePage +import com.mapconductor.example.pages.map.arcgis2d.ArcGISMapView2DPage import com.mapconductor.example.pages.map.basic.StoreMapPage import com.mapconductor.example.pages.map.camerasync.CameraSyncPage import com.mapconductor.example.pages.map.design.MapDesignMapPage import com.mapconductor.example.pages.map.flyto.FlyToMapIcons import com.mapconductor.example.pages.map.flyto.FlyToMapPage +import com.mapconductor.example.pages.map.tilt.TiltMapPage import com.mapconductor.example.pages.map.visibleregion.VisibleRegionPage import com.mapconductor.example.pages.marker.animation.AnimationMapPage import com.mapconductor.example.pages.marker.icons.MarkerBasicPage @@ -32,6 +36,7 @@ import com.mapconductor.example.pages.marker.postofficecluster.MarkerClusterMapP import com.mapconductor.example.pages.polygon.basic.PolygonMapPage import com.mapconductor.example.pages.polygon.click.PolygonClickPage import com.mapconductor.example.pages.polygon.geodesic.PolygonGeodesicPage +import com.mapconductor.example.pages.polygon.hole.HolePolygonMapPage import com.mapconductor.example.pages.polyline.PolylineClickMapPage import com.mapconductor.example.pages.polyline.PolylineMapPage import com.mapconductor.example.pages.rasterlayer.RasterLayerMapPage @@ -80,6 +85,14 @@ fun DemoAppScreen(initPage: String = "map") { id = "map-basic", title = "Map", ), + SidebarItem( + id = "tilt-map", + title = "Tilt Map", + ), + SidebarItem( + id = "arcgis-map-2d", + title = "ArcGIS Map 2D", + ), SidebarItem( id = "simple-info-bubble", title = "Simple Text Bubble", @@ -156,6 +169,10 @@ fun DemoAppScreen(initPage: String = "map") { id = "polygon-geodesic", title = "Geodesic polygons", ), + SidebarItem( + id = "polygon-hole", + title = "Hole polygon", + ), SidebarItem( id = "raster-layer", title = "Raster Layer", @@ -164,6 +181,14 @@ fun DemoAppScreen(initPage: String = "map") { id = "heatmap-overlay", title = "Heatmap overlay", ), + SidebarItem( + id = "geojson-basic", + title = "Basic GeoJSON", + ), + SidebarItem( + id = "geojson-layer", + title = "GeoJSON Layer", + ), ) AppTheme { @@ -277,6 +302,11 @@ fun DemoAppScreen(initPage: String = "map") { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "polygon-hole" -> { + HolePolygonMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } "groundImage" -> { GroundImageMapPage( groundImageResources = groundImageResources, @@ -293,6 +323,26 @@ fun DemoAppScreen(initPage: String = "map") { onToggleSidebar = navigationViewModel::toggleSidebar, ) } + "geojson-basic" -> { + BasicGeoJSONMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } + "geojson-layer" -> { + GeoJSONLayerMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } + "tilt-map" -> { + TiltMapPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } + "arcgis-map-2d" -> { + ArcGISMapView2DPage( + onToggleSidebar = navigationViewModel::toggleSidebar, + ) + } } } diff --git a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt index e292c463..f1c7755c 100644 --- a/example-app/src/main/java/com/mapconductor/example/MainActivity.kt +++ b/example-app/src/main/java/com/mapconductor/example/MainActivity.kt @@ -3,6 +3,7 @@ package com.mapconductor.example import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import com.mapconductor.example.ui.theme.AppTheme import android.os.Bundle class MainActivity : ComponentActivity() { @@ -11,10 +12,12 @@ class MainActivity : ComponentActivity() { enableEdgeToEdge() setContent { - DemoAppScreen( -// initPage = "polygon-geodesic", - initPage = "startup", - ) + AppTheme { + DemoAppScreen( + // initPage = "map-basic", + initPage = "startup", + ) + } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt index 2af4105e..fae697e7 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/circle/CirclePageViewModel.kt @@ -64,7 +64,7 @@ class CirclePageViewModel : Color.LightGray, Color.Magenta, ) - private var tapIdx = 0 + private var tapIdx by mutableStateOf(0) override var fillOpacity by mutableStateOf(0.3f) override var strokeWidth by mutableStateOf(3.0f) @@ -130,10 +130,10 @@ class CirclePageViewModel : CircleState( id = "circle", center = circleCenter, - radiusMeters = radiusMeters, // Initial radius + radiusMeters = radiusMeters, strokeColor = Color.Blue.copy(alpha = 0.5f), strokeWidth = strokeWidth.dp, - fillColor = this.colors[0].copy(alpha = fillOpacity), + fillColor = colors[tapIdx].copy(alpha = fillOpacity), onClick = this::onCircleClick, ) @@ -156,18 +156,12 @@ class CirclePageViewModel : } override fun onCircleClick(event: CircleEvent) { - this.tapIdx = (this.tapIdx + 1) % this.colors.size - event.state.fillColor = this.colors[this.tapIdx].copy(alpha = fillOpacity) + tapIdx = (tapIdx + 1) % colors.size showToast("Circle clicked - Radius: ${radiusMeters.toInt()}m") } override fun onMarkerMove(dragged: MarkerState) { _edgeMarker.value.position = dragged.position - - // Update circle radius - circleState.radiusMeters = radiusMeters // haversineDistance(circleCenter, _edgeMarker.value.position) - -// showToast("Radius updated: ${radiusMeters.toInt()}m") } override fun showToast(text: String) { diff --git a/example-app/src/main/java/com/mapconductor/example/pages/geojson/basic/BasicGeoJSONMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/geojson/basic/BasicGeoJSONMapPage.kt new file mode 100644 index 00000000..aa29b284 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/geojson/basic/BasicGeoJSONMapPage.kt @@ -0,0 +1,111 @@ +package com.mapconductor.example.pages.geojson.basic + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewStateInterface +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.geojson.GeoJSONFeature +import com.mapconductor.geojson.GeoJSONLayer +import com.mapconductor.geojson.GeoJSONLayerState +import com.mapconductor.geojson.GeoJSONParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +@Composable +fun BasicGeoJSONMapPage(onToggleSidebar: () -> Unit = {}) { + val initCameraPosition = + remember { + MapCameraPosition( + position = GeoPoint.fromLongLat(55.3089185, 25.255377), + zoom = 13.0, + ) + } + var mapViewState by remember { mutableStateOf?>(null) } + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = { mapViewState = it }, + ) { + BasicGeoJSONMapComponent(mapViewState = mapViewState) + } +} + +@Composable +private fun BasicGeoJSONMapComponent(mapViewState: MapViewStateInterface<*>?) { + val layerState = + remember { + GeoJSONLayerState( + fillColor = android.graphics.Color.argb(127, 0x3b, 0xb2, 0xd0), + strokeColor = android.graphics.Color.argb(255, 0x1d, 0x70, 0x82), + strokeWidth = 2f, + ) + } + var features by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(Unit) { + features = + withContext(Dispatchers.IO) { + BASIC_GEOJSON.byteInputStream(Charsets.UTF_8).use(GeoJSONParser::parseStream) + } + } + + mapViewState?.let { state -> + MapViewContainer( + state = state, + ) { + GeoJSONLayer(state = layerState, features = features) + } + } +} + +private val BASIC_GEOJSON = + """ + { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [55.30122473231012, 25.26476622289597], + [55.29743486255916, 25.25827212207261], + [55.28978863411328, 25.251356725509737], + [55.300027931336984, 25.246425506635504], + [55.307474692951274, 25.244200378933655], + [55.31212891895635, 25.256408010450187], + [55.30774064871093, 25.26266169122738], + [55.301357710197806, 25.264946609615492], + [55.30122473231012, 25.26476622289597] + ], + [ + [55.30084858315658, 25.256531695820797], + [55.298280197635705, 25.252243254705405], + [55.30163885563897, 25.250501032248863], + [55.304059065092645, 25.254700192612702], + [55.30084858315658, 25.256531695820797] + ], + [ + [55.30173763969924, 25.262517391695198], + [55.301095543307355, 25.26122200491396], + [55.30396028103232, 25.259479911263526], + [55.30489872958182, 25.261132667394975], + [55.30173763969924, 25.262517391695198] + ] + ] + }, + "properties": {} + } + ] + } + """.trimIndent() diff --git a/example-app/src/main/java/com/mapconductor/example/pages/geojson/layer/GeoJSONLayerMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/geojson/layer/GeoJSONLayerMapPage.kt new file mode 100644 index 00000000..c39d94ac --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/geojson/layer/GeoJSONLayerMapPage.kt @@ -0,0 +1,193 @@ +package com.mapconductor.example.pages.geojson.layer + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.info.InfoBubble +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewStateInterface +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard +import com.mapconductor.geojson.GeoJSONFeature +import com.mapconductor.geojson.GeoJSONLayer +import com.mapconductor.geojson.GeoJSONLayerState +import com.mapconductor.geojson.GeoJSONParser +import com.mapconductor.utils.LoadingDialog +import java.util.zip.ZipInputStream +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +private const val GEOJSON_ASSET = "N02-22_GML.zip" + +@Composable +fun GeoJSONLayerMapPage(onToggleSidebar: () -> Unit = {}) { + val initCameraPosition = + remember { + MapCameraPosition( + position = GeoPoint(35.68, 139.77), + zoom = 13.0, + ) + } + var mapViewState by remember { mutableStateOf?>(null) } + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = { mapViewState = it }, + ) { paddingValues -> + GeoJSONLayerMapComponent(mapViewState = mapViewState) + + MessageCard( + title = "GeoJSON Layer", + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ), + ) { + Text("Tap a feature to inspect its properties.") + } + } +} + +@Composable +private fun GeoJSONLayerMapComponent(mapViewState: MapViewStateInterface<*>?) { + val context = LocalContext.current + var selectedFeature by remember { mutableStateOf(null) } + var tappedPosition by remember { mutableStateOf(null) } + var features by remember { mutableStateOf>(emptyList()) } + var isDataLoading by remember { mutableStateOf(true) } + + val layerState = + remember { + GeoJSONLayerState( + strokeColor = android.graphics.Color.argb(127, 250, 36, 29), + strokeWidth = 6f, + onClick = { feature, position -> + selectedFeature = feature + tappedPosition = GeoPoint.from(position) + }, + ) + } + + LaunchedEffect(Unit) { + isDataLoading = true + try { + features = + withContext(Dispatchers.IO) { + context.assets.open(GEOJSON_ASSET).use { input -> + ZipInputStream(input).use(::parseFirstGeoJSONEntry) + } + } + } finally { + isDataLoading = false + } + } + + mapViewState?.let { state -> + MapViewContainer( + state = state, + onMapClick = { clicked -> + if (!layerState.processClick(clicked)) { + tappedPosition = null + selectedFeature = null + } + }, + ) { + GeoJSONLayer(state = layerState, features = features) + + tappedPosition?.let { position -> + InfoBubble(position = position) { + selectedFeature?.properties?.let { properties -> + PropertyTable(properties) + } + } + } + } + } + + if (isDataLoading) { + LoadingDialog( + title = "Loading GeoJSON", + message = "Parsing $GEOJSON_ASSET...", + ) + } +} + +private fun parseFirstGeoJSONEntry(zipInputStream: ZipInputStream): List { + while (true) { + val entry = zipInputStream.nextEntry ?: return emptyList() + if (!entry.isDirectory && entry.name.endsWith(".geojson", ignoreCase = true)) { + return GeoJSONParser.parseStream(zipInputStream) + } + zipInputStream.closeEntry() + } +} + +@Composable +private fun PropertyTable(properties: Map) { + Column( + modifier = + Modifier + .width(400.dp) + .heightIn(max = 300.dp) + .verticalScroll(rememberScrollState()), + ) { + Row(modifier = Modifier.background(Color(0xFFE0E0E0))) { + PropertyTableCell(text = "Property", weight = 0.35f) + PropertyTableCell(text = "Value", weight = 0.65f) + } + + properties.forEach { (key, value) -> + Row(modifier = Modifier.fillMaxWidth()) { + PropertyTableCell(text = key, weight = 0.35f) + PropertyTableCell(text = value?.toString().orEmpty(), weight = 0.65f) + } + } + } +} + +@Composable +private fun RowScope.PropertyTableCell( + text: String, + weight: Float, +) { + Text( + text = text, + color = Color.Black, + modifier = + Modifier + .border(1.dp, Color.Gray) + .weight(weight) + .padding(8.dp), + ) +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt index b481cbfa..0261a6d0 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/groundimage/GroundImageMapPage.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.Slider -import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -16,7 +15,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.mapconductor.example.toast.ToastHost @@ -63,7 +61,7 @@ fun GroundImageMapPage( title = "GroundImage Example", ) { Column { - Text("opacity: ${"%.2f".format(sliderOpacity)}", color = Color.Black) + Text("opacity: ${"%.2f".format(sliderOpacity)}") Slider( value = sliderOpacity, onValueChange = { newValue -> @@ -85,14 +83,6 @@ fun GroundImageMapPage( }, valueRange = 0.0f..1.0f, // スライダー範囲 steps = 0, - colors = - SliderDefaults.colors( - thumbColor = Color.Black, // つまみの色 - activeTrackColor = Color.DarkGray, // 値までのトラック - inactiveTrackColor = Color.LightGray, // 残りのトラック - activeTickColor = Color.Black, // 有効ステップの目盛り - inactiveTickColor = Color.White, // 無効ステップの目盛り - ), ) } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt index c74c9382..567f1a34 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/MultipleBubblesPage.kt @@ -32,6 +32,7 @@ fun MultipleBubblesPage(onToggleSidebar: () -> Unit = {}) { MapCameraPosition( position = GeoPoint.fromLatLong(37.7749, -122.4194), zoom = 15.0, + tilt = 45.0, ) var selectedMarkers by remember { mutableStateOf(setOf()) } var mapViewState by remember { mutableStateOf?>(null) } @@ -81,6 +82,9 @@ fun MultipleBubblesPage(onToggleSidebar: () -> Unit = {}) { MapViewContainer( modifier = Modifier.fillMaxSize(), state = mapViewState, + onMapLoaded = { + selectedMarkers = markerStates.map { it.id }.toSet() + }, onMapClick = { selectedMarkers = emptySet() // Clear all selections }, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt index e5dfa0ed..23bb9fb6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/RichContentBubblePage.kt @@ -86,6 +86,9 @@ fun RichContentBubblePage(onToggleSidebar: () -> Unit = {}) { MapViewContainer( modifier = Modifier.fillMaxSize(), state = mapViewState, + onMapLoaded = { + selectedMarker = markerState + }, onMapClick = { selectedMarker = null }, ) { Marker(markerState) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt index 7a695126..0593a5a1 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/SimpleTextBubblePage.kt @@ -55,6 +55,9 @@ fun SimpleTextBubblePage(onToggleSidebar: () -> Unit = {}) { modifier = Modifier.fillMaxSize(), state = mapViewState, onMapClick = { selectedMarker = null }, + onMapLoaded = { + selectedMarker = markerState + }, ) { Marker(markerState) diff --git a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt index c573855f..7c7f5324 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/infobubble/StyledInfoBubblePage.kt @@ -104,7 +104,7 @@ fun StyledInfoBubblePage(onToggleSidebar: () -> Unit = {}) { modifier = Modifier.fillMaxSize(), state = mapViewState, onMapLoaded = { - println("地図の読み込みが完了しました") + selectedMarker = markerState1 }, onMapClick = { geoPoint -> selectedMarker = null // 地図クリックで選択解除 diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/arcgis2d/ArcGISMapView2DPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/arcgis2d/ArcGISMapView2DPage.kt new file mode 100644 index 00000000..8b393b47 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/arcgis2d/ArcGISMapView2DPage.kt @@ -0,0 +1,86 @@ +package com.mapconductor.example.pages.map.arcgis2d + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.mapconductor.arcgis.map.ArcGISDesign +import com.mapconductor.arcgis.map.ArcGISMapView2D +import com.mapconductor.arcgis.map.ArcGISMapViewState +import com.mapconductor.arcgis.map.rememberArcGISMapViewState +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.example.R +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.IconItem +import com.mapconductor.example.ui.MessageCard + +@Composable +fun ArcGISMapView2DPage(onToggleSidebar: () -> Unit = {}) { + val initCameraPosition = + remember { + MapCameraPosition( + position = GeoPoint(35.68162987878426, 139.76703394012318), + zoom = 0.0, + bearing = 127.0, + ) + } + + val arcGISState = + rememberArcGISMapViewState( + mapDesign = ArcGISDesign.Streets, + cameraPosition = initCameraPosition, + ) + + var mapState by remember { mutableStateOf(null) } + + val menuItems = + remember(arcGISState) { + listOf( + IconItem( + key = "arcgis2d", + label = "ArcGIS 2D", + lightIconResId = R.drawable.arcgis_logo_black, + darkIconResId = R.drawable.arcgis_logo_white, + value = arcGISState, + ), + ) + } + + DemoMapPageScaffold( + menuItems = menuItems, + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = { state -> + mapState = state as? ArcGISMapViewState + }, + ) { paddingValues -> + mapState?.let { state -> + ArcGISMapView2D( + state = state, + modifier = Modifier.fillMaxSize(), + ) + } + + MessageCard( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = 16.dp, + end = 16.dp, + ).sizeIn(maxWidth = 600.dp), + title = "ArcGIS MapView 2D", + ) { + Text("Flat 2D map view using ArcGIS MapView (no 3D tilt or elevation).") + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/camerasync/CameraSyncPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/camerasync/CameraSyncPage.kt index d400151d..601ebf4c 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/camerasync/CameraSyncPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/camerasync/CameraSyncPage.kt @@ -24,6 +24,7 @@ import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -207,362 +208,364 @@ fun CameraSyncPage(onToggleSidebar: () -> Unit = {}) { return target?.let { isCloseToTarget(camera, it) } ?: false } - Column(modifier = Modifier.fillMaxSize()) { - Card( - modifier = Modifier.fillMaxWidth().padding(10.dp), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), - ) { - Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + Scaffold { paddingValues -> + Column(modifier = Modifier.fillMaxSize().padding(paddingValues)) { + Card( + modifier = Modifier.fillMaxWidth().padding(10.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), ) { - Icon( - imageVector = Icons.Default.Menu, - contentDescription = "Toggle sidebar", - modifier = - Modifier - .clickable(onClick = onToggleSidebar) - .size(32.dp) - .padding(end = 4.dp), - tint = MaterialTheme.colorScheme.onSurface, - ) - - Text( - text = "Camera Sync", - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - - @OptIn(ExperimentalLayoutApi::class) - FlowRow( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 10.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - maxLines = 2, - ) { - locations.forEach { location -> - Button( - onClick = { - val now = SystemClock.uptimeMillis() - val position = - MapCameraPosition( - position = location.center, - zoom = location.zoom, - bearing = 0.0, - tilt = 0.0, - ) - currentLeftState.moveCameraTo(position, durationMillis = 1000) - currentRightState.moveCameraTo(position, durationMillis = 1000) - leftCameraPosition = position - rightCameraPosition = position - // Both maps will emit camera callbacks; treat them as programmatic during the animation. - markProgrammaticMove(ActiveMapPane.Left, position, now) - markProgrammaticMove(ActiveMapPane.Right, position, now) - programmaticLeftUntilMs = now + 1000L + programmaticTtlMs - programmaticRightUntilMs = now + 1000L + programmaticTtlMs - }, + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text(text = location.name, maxLines = 1, overflow = TextOverflow.Ellipsis) + Icon( + imageVector = Icons.Default.Menu, + contentDescription = "Toggle sidebar", + modifier = + Modifier + .clickable(onClick = onToggleSidebar) + .size(32.dp) + .padding(end = 4.dp), + tint = MaterialTheme.colorScheme.onSurface, + ) + + Text( + text = "Camera Sync", + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) } } - } - HorizontalDivider(modifier = Modifier.padding(top = 10.dp)) - - BoxWithConstraints(modifier = Modifier.fillMaxSize()) { - val stackVertically = maxHeight > maxWidth - - if (stackVertically) { - Column(modifier = Modifier.fillMaxSize()) { - CameraSyncMapPane( - modifier = Modifier.weight(1f).fillMaxWidth(), - menuContent = { - IconSelectMenu( - itemList = leftMenuItems, - selectedIndex = leftSelectedIndex, - onSelect = { index, _ -> leftSelectedIndex = index }, - ) + @OptIn(ExperimentalLayoutApi::class) + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + maxLines = 2, + ) { + locations.forEach { location -> + Button( + onClick = { + val now = SystemClock.uptimeMillis() + val position = + MapCameraPosition( + position = location.center, + zoom = location.zoom, + bearing = 0.0, + tilt = 0.0, + ) + currentLeftState.moveCameraTo(position, durationMillis = 1000) + currentRightState.moveCameraTo(position, durationMillis = 1000) + leftCameraPosition = position + rightCameraPosition = position + // Both maps will emit camera callbacks; treat them as programmatic during the animation. + markProgrammaticMove(ActiveMapPane.Left, position, now) + markProgrammaticMove(ActiveMapPane.Right, position, now) + programmaticLeftUntilMs = now + 1000L + programmaticTtlMs + programmaticRightUntilMs = now + 1000L + programmaticTtlMs }, - mapViewState = leftState, - label = "Source Camera", - cameraPosition = leftCameraPosition, - onCameraMove = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - // Ignore feedback from programmatic moves (but stop ignoring if the user deviates). - if (programmaticLeftKey != null) { - if (now > programmaticLeftUntilMs) { - clearProgrammaticMove(ActiveMapPane.Left) - } else { - val age = now - programmaticLeftSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Left, position, now) - ) { - leftCameraPosition = position - return@launch + ) { + Text(text = location.name, maxLines = 1, overflow = TextOverflow.Ellipsis) + } + } + } + + HorizontalDivider(modifier = Modifier.padding(top = 10.dp)) + + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val stackVertically = maxHeight > maxWidth + + if (stackVertically) { + Column(modifier = Modifier.fillMaxSize()) { + CameraSyncMapPane( + modifier = Modifier.weight(1f).fillMaxWidth(), + menuContent = { + IconSelectMenu( + itemList = leftMenuItems, + selectedIndex = leftSelectedIndex, + onSelect = { index, _ -> leftSelectedIndex = index }, + ) + }, + mapViewState = leftState, + label = "Source Camera", + cameraPosition = leftCameraPosition, + onCameraMove = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + // Ignore feedback from programmatic moves (but stop ignoring if the user deviates). + if (programmaticLeftKey != null) { + if (now > programmaticLeftUntilMs) { + clearProgrammaticMove(ActiveMapPane.Left) + } else { + val age = now - programmaticLeftSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Left, position, now) + ) { + leftCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Left) } - clearProgrammaticMove(ActiveMapPane.Left) } - } - if (now - lastLeftMoveSyncAtMs < moveSyncIntervalMs) return@launch - lastLeftMoveSyncAtMs = now + if (now - lastLeftMoveSyncAtMs < moveSyncIntervalMs) return@launch + lastLeftMoveSyncAtMs = now - leftCameraPosition = position - rightCameraPosition = position - markProgrammaticMove(ActiveMapPane.Right, position, now) - currentRightState.moveCameraTo(position, durationMillis = 0) - } - }, - onCameraMoveEnd = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticLeftKey != null) { - if (now > programmaticLeftUntilMs) { - clearProgrammaticMove(ActiveMapPane.Left) - } else { - val age = now - programmaticLeftSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Left, position, now) - ) { - leftCameraPosition = position - return@launch + leftCameraPosition = position + rightCameraPosition = position + markProgrammaticMove(ActiveMapPane.Right, position, now) + currentRightState.moveCameraTo(position, durationMillis = 0) + } + }, + onCameraMoveEnd = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticLeftKey != null) { + if (now > programmaticLeftUntilMs) { + clearProgrammaticMove(ActiveMapPane.Left) + } else { + val age = now - programmaticLeftSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Left, position, now) + ) { + leftCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Left) } - clearProgrammaticMove(ActiveMapPane.Left) } + leftCameraPosition = position + rightCameraPosition = position + markProgrammaticMove(ActiveMapPane.Right, position, now) + currentRightState.moveCameraTo(position, durationMillis = 0) } - leftCameraPosition = position - rightCameraPosition = position - markProgrammaticMove(ActiveMapPane.Right, position, now) - currentRightState.moveCameraTo(position, durationMillis = 0) - } - }, - boundsPolylines = boundsPolylines, - referenceRectangles = referenceRectangles, - ) - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .background(MaterialTheme.colorScheme.outline), - ) - - CameraSyncMapPane( - modifier = Modifier.weight(1f).fillMaxWidth(), - menuContent = { - IconSelectMenu( - itemList = rightMenuItems, - selectedIndex = rightSelectedIndex, - onSelect = { index, _ -> rightSelectedIndex = index }, - ) - }, - mapViewState = rightState, - label = "Synced Camera", - cameraPosition = rightCameraPosition, - onCameraMove = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticRightKey != null) { - if (now > programmaticRightUntilMs) { - clearProgrammaticMove(ActiveMapPane.Right) - } else { - val age = now - programmaticRightSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Right, position, now) - ) { - rightCameraPosition = position - return@launch + }, + boundsPolylines = boundsPolylines, + referenceRectangles = referenceRectangles, + ) + + Box( + modifier = + Modifier + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colorScheme.outline), + ) + + CameraSyncMapPane( + modifier = Modifier.weight(1f).fillMaxWidth(), + menuContent = { + IconSelectMenu( + itemList = rightMenuItems, + selectedIndex = rightSelectedIndex, + onSelect = { index, _ -> rightSelectedIndex = index }, + ) + }, + mapViewState = rightState, + label = "Synced Camera", + cameraPosition = rightCameraPosition, + onCameraMove = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticRightKey != null) { + if (now > programmaticRightUntilMs) { + clearProgrammaticMove(ActiveMapPane.Right) + } else { + val age = now - programmaticRightSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Right, position, now) + ) { + rightCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Right) } - clearProgrammaticMove(ActiveMapPane.Right) } - } - if (now - lastRightMoveSyncAtMs < moveSyncIntervalMs) return@launch - lastRightMoveSyncAtMs = now + if (now - lastRightMoveSyncAtMs < moveSyncIntervalMs) return@launch + lastRightMoveSyncAtMs = now - rightCameraPosition = position - leftCameraPosition = position - markProgrammaticMove(ActiveMapPane.Left, position, now) - currentLeftState.moveCameraTo(position, durationMillis = 0) - } - }, - onCameraMoveEnd = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticRightKey != null) { - if (now > programmaticRightUntilMs) { - clearProgrammaticMove(ActiveMapPane.Right) - } else { - val age = now - programmaticRightSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Right, position, now) - ) { - rightCameraPosition = position - return@launch + rightCameraPosition = position + leftCameraPosition = position + markProgrammaticMove(ActiveMapPane.Left, position, now) + currentLeftState.moveCameraTo(position, durationMillis = 0) + } + }, + onCameraMoveEnd = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticRightKey != null) { + if (now > programmaticRightUntilMs) { + clearProgrammaticMove(ActiveMapPane.Right) + } else { + val age = now - programmaticRightSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Right, position, now) + ) { + rightCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Right) } - clearProgrammaticMove(ActiveMapPane.Right) } + rightCameraPosition = position + leftCameraPosition = position + markProgrammaticMove(ActiveMapPane.Left, position, now) + currentLeftState.moveCameraTo(position, durationMillis = 0) } - rightCameraPosition = position - leftCameraPosition = position - markProgrammaticMove(ActiveMapPane.Left, position, now) - currentLeftState.moveCameraTo(position, durationMillis = 0) - } - }, - boundsPolylines = boundsPolylines, - referenceRectangles = referenceRectangles, - ) - } - } else { - Row(modifier = Modifier.fillMaxSize()) { - CameraSyncMapPane( - modifier = Modifier.weight(1f).fillMaxHeight(), - menuContent = { - IconSelectMenu( - itemList = leftMenuItems, - selectedIndex = leftSelectedIndex, - onSelect = { index, _ -> leftSelectedIndex = index }, - ) - }, - mapViewState = leftState, - label = "Source Camera", - cameraPosition = leftCameraPosition, - onCameraMove = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticLeftKey != null) { - if (now > programmaticLeftUntilMs) { - clearProgrammaticMove(ActiveMapPane.Left) - } else { - val age = now - programmaticLeftSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Left, position, now) - ) { - leftCameraPosition = position - return@launch + }, + boundsPolylines = boundsPolylines, + referenceRectangles = referenceRectangles, + ) + } + } else { + Row(modifier = Modifier.fillMaxSize()) { + CameraSyncMapPane( + modifier = Modifier.weight(1f).fillMaxHeight(), + menuContent = { + IconSelectMenu( + itemList = leftMenuItems, + selectedIndex = leftSelectedIndex, + onSelect = { index, _ -> leftSelectedIndex = index }, + ) + }, + mapViewState = leftState, + label = "Source Camera", + cameraPosition = leftCameraPosition, + onCameraMove = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticLeftKey != null) { + if (now > programmaticLeftUntilMs) { + clearProgrammaticMove(ActiveMapPane.Left) + } else { + val age = now - programmaticLeftSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Left, position, now) + ) { + leftCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Left) } - clearProgrammaticMove(ActiveMapPane.Left) } - } - if (now - lastLeftMoveSyncAtMs < moveSyncIntervalMs) return@launch - lastLeftMoveSyncAtMs = now + if (now - lastLeftMoveSyncAtMs < moveSyncIntervalMs) return@launch + lastLeftMoveSyncAtMs = now - leftCameraPosition = position - rightCameraPosition = position - markProgrammaticMove(ActiveMapPane.Right, position, now) - currentRightState.moveCameraTo(position, durationMillis = 0) - } - }, - onCameraMoveEnd = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticLeftKey != null) { - if (now > programmaticLeftUntilMs) { - clearProgrammaticMove(ActiveMapPane.Left) - } else { - val age = now - programmaticLeftSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Left, position, now) - ) { - leftCameraPosition = position - return@launch + leftCameraPosition = position + rightCameraPosition = position + markProgrammaticMove(ActiveMapPane.Right, position, now) + currentRightState.moveCameraTo(position, durationMillis = 0) + } + }, + onCameraMoveEnd = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticLeftKey != null) { + if (now > programmaticLeftUntilMs) { + clearProgrammaticMove(ActiveMapPane.Left) + } else { + val age = now - programmaticLeftSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Left, position, now) + ) { + leftCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Left) } - clearProgrammaticMove(ActiveMapPane.Left) } + leftCameraPosition = position + rightCameraPosition = position + markProgrammaticMove(ActiveMapPane.Right, position, now) + currentRightState.moveCameraTo(position, durationMillis = 0) } - leftCameraPosition = position - rightCameraPosition = position - markProgrammaticMove(ActiveMapPane.Right, position, now) - currentRightState.moveCameraTo(position, durationMillis = 0) - } - }, - boundsPolylines = boundsPolylines, - referenceRectangles = referenceRectangles, - ) - - Box( - modifier = - Modifier - .fillMaxHeight() - .width(1.dp) - .background(MaterialTheme.colorScheme.outline), - ) - - CameraSyncMapPane( - modifier = Modifier.weight(1f).fillMaxHeight(), - menuContent = { - IconSelectMenu( - itemList = rightMenuItems, - selectedIndex = rightSelectedIndex, - onSelect = { index, _ -> rightSelectedIndex = index }, - ) - }, - mapViewState = rightState, - label = "Synced Camera", - cameraPosition = rightCameraPosition, - onCameraMove = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticRightKey != null) { - if (now > programmaticRightUntilMs) { - clearProgrammaticMove(ActiveMapPane.Right) - } else { - val age = now - programmaticRightSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Right, position, now) - ) { - rightCameraPosition = position - return@launch + }, + boundsPolylines = boundsPolylines, + referenceRectangles = referenceRectangles, + ) + + Box( + modifier = + Modifier + .fillMaxHeight() + .width(1.dp) + .background(MaterialTheme.colorScheme.outline), + ) + + CameraSyncMapPane( + modifier = Modifier.weight(1f).fillMaxHeight(), + menuContent = { + IconSelectMenu( + itemList = rightMenuItems, + selectedIndex = rightSelectedIndex, + onSelect = { index, _ -> rightSelectedIndex = index }, + ) + }, + mapViewState = rightState, + label = "Synced Camera", + cameraPosition = rightCameraPosition, + onCameraMove = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticRightKey != null) { + if (now > programmaticRightUntilMs) { + clearProgrammaticMove(ActiveMapPane.Right) + } else { + val age = now - programmaticRightSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Right, position, now) + ) { + rightCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Right) } - clearProgrammaticMove(ActiveMapPane.Right) } - } - if (now - lastRightMoveSyncAtMs < moveSyncIntervalMs) return@launch - lastRightMoveSyncAtMs = now + if (now - lastRightMoveSyncAtMs < moveSyncIntervalMs) return@launch + lastRightMoveSyncAtMs = now - rightCameraPosition = position - leftCameraPosition = position - markProgrammaticMove(ActiveMapPane.Left, position, now) - currentLeftState.moveCameraTo(position, durationMillis = 0) - } - }, - onCameraMoveEnd = { position -> - mainScope.launch { - val now = SystemClock.uptimeMillis() - if (programmaticRightKey != null) { - if (now > programmaticRightUntilMs) { - clearProgrammaticMove(ActiveMapPane.Right) - } else { - val age = now - programmaticRightSinceMs - if (age <= programmaticGraceMs || - isProgrammaticMove(ActiveMapPane.Right, position, now) - ) { - rightCameraPosition = position - return@launch + rightCameraPosition = position + leftCameraPosition = position + markProgrammaticMove(ActiveMapPane.Left, position, now) + currentLeftState.moveCameraTo(position, durationMillis = 0) + } + }, + onCameraMoveEnd = { position -> + mainScope.launch { + val now = SystemClock.uptimeMillis() + if (programmaticRightKey != null) { + if (now > programmaticRightUntilMs) { + clearProgrammaticMove(ActiveMapPane.Right) + } else { + val age = now - programmaticRightSinceMs + if (age <= programmaticGraceMs || + isProgrammaticMove(ActiveMapPane.Right, position, now) + ) { + rightCameraPosition = position + return@launch + } + clearProgrammaticMove(ActiveMapPane.Right) } - clearProgrammaticMove(ActiveMapPane.Right) } + rightCameraPosition = position + leftCameraPosition = position + markProgrammaticMove(ActiveMapPane.Left, position, now) + currentLeftState.moveCameraTo(position, durationMillis = 0) } - rightCameraPosition = position - leftCameraPosition = position - markProgrammaticMove(ActiveMapPane.Left, position, now) - currentLeftState.moveCameraTo(position, durationMillis = 0) - } - }, - boundsPolylines = boundsPolylines, - referenceRectangles = referenceRectangles, - ) + }, + boundsPolylines = boundsPolylines, + referenceRectangles = referenceRectangles, + ) + } } } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/design/MapDesignMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/design/MapDesignMapPage.kt index 4da3621a..2adf889c 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/map/design/MapDesignMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/design/MapDesignMapPage.kt @@ -5,8 +5,8 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType.Companion.PrimaryNotEditable import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -123,7 +123,7 @@ fun MapDesignTypeSelector( onExpandedChange = { expanded = !expanded }, ) { TextField( - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable), + modifier = Modifier.menuAnchor(PrimaryNotEditable), value = selectedLabel, onValueChange = {}, readOnly = true, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPage.kt new file mode 100644 index 00000000..4ec0ba72 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPage.kt @@ -0,0 +1,188 @@ +package com.mapconductor.example.pages.map.tilt + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Card +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.tan +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@Composable +fun TiltMapPage(onToggleSidebar: () -> Unit = {}) { + val coroutineScope = rememberCoroutineScope() + val debounceMs = 80L + val viewModel = remember { TiltMapPageViewModel() } + + val tilt = viewModel.tilt + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState().value + + mapViewState?.let { + MapViewContainer( + state = mapViewState, + ) + } + + // Message Card + Card( + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ).sizeIn( + maxWidth = 600.dp, + ), + ) { + Column(modifier = Modifier.padding(6.dp)) { + TiltCameraDiagram( + tilt = tilt, + modifier = + Modifier + .fillMaxWidth() + .height(120.dp), + ) + Spacer(modifier = Modifier.height(6.dp)) + Row { + Text("tilt: ${"%.2f".format(tilt)}") + Slider( + value = viewModel.tilt.toFloat(), + onValueChange = { newValue -> + coroutineScope.launch { + delay(debounceMs) + viewModel.tilt = newValue.toDouble() + } + }, + onValueChangeFinished = { + }, + valueRange = -89.0f..89.0f, // スライダー範囲 + steps = 0, + ) + } + } + } + } +} + +@Composable +private fun TiltCameraDiagram( + tilt: Double, + modifier: Modifier = Modifier, +) { + Canvas(modifier = modifier) { + val width = size.width + val height = size.height + val groundY = height * 0.78f + val originX = width * 0.5f + val originY = groundY + val baseCameraY = height * 0.22f + val maxTilt = 90.0 + val tiltAbs = abs(tilt).coerceIn(0.0, maxTilt) + val tiltRad = (tiltAbs * PI / 180.0).toFloat() + val altitudePx = groundY - baseCameraY + val targetDistance = + (altitudePx * tan(tiltRad)) + .coerceAtMost(width * 0.44f) + val targetX = if (tilt < 0.0) originX - targetDistance else originX + val targetY = groundY + val cameraX = if (tilt > 0.0) originX + targetDistance else originX + val cameraY = baseCameraY + val sightEndX = + if (tilt == 0.0) { + cameraX + } else { + targetX + } + val sightEndY = targetY + + drawLine( + color = Color(0xFFE4E0EC), + start = Offset(width * 0.08f, groundY), + end = Offset(width * 0.94f, groundY), + strokeWidth = 3f, + cap = StrokeCap.Round, + ) + drawLine( + color = Color(0xFF8E879A), + start = Offset(cameraX, cameraY), + end = Offset(cameraX, groundY), + strokeWidth = 2f, + ) + drawCircle( + color = Color(0xFF5DA7FF), + radius = 8f, + center = Offset(cameraX, cameraY), + ) + drawCircle( + color = Color(0xFFFF6259), + radius = 7f, + center = Offset(targetX, targetY), + ) + drawLine( + color = Color(0xFFFFC857), + start = Offset(cameraX, cameraY), + end = Offset(sightEndX, sightEndY), + strokeWidth = 4f, + cap = StrokeCap.Round, + ) + + val cameraBody = + Path().apply { + moveTo(cameraX - 12f, cameraY - 8f) + lineTo(cameraX + 14f, cameraY - 4f) + lineTo(cameraX + 10f, cameraY + 10f) + lineTo(cameraX - 12f, cameraY + 8f) + close() + } + drawPath(cameraBody, Color(0xFF2F2A38)) + drawPath(cameraBody, Color.White.copy(alpha = 0.7f), style = Stroke(width = 1.5f)) + + drawCircle( + color = Color(0xFF8E879A), + radius = 3.5f, + center = Offset(originX, originY), + ) + drawLine( + color = Color(0xFFB8AFCA), + start = Offset(minOf(cameraX, targetX), groundY + 12f), + end = Offset(maxOf(cameraX, targetX), groundY + 12f), + strokeWidth = 2f, + cap = StrokeCap.Round, + ) + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPageViewModel.kt new file mode 100644 index 00000000..d1a68975 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/map/tilt/TiltMapPageViewModel.kt @@ -0,0 +1,95 @@ +package com.mapconductor.example.pages.map.tilt + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.graphics.Color +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewStateInterface +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface TiltMapPageViewModelInterface { + val initCameraPosition: MapCameraPosition + val cameraPosition: StateFlow + val disableSlider: StateFlow + var tilt: Double + val mapViewState: StateFlow?> + + fun onMapCameraMoveStart(point: GeoPoint) + + fun onMapCameraMoveEnd(point: GeoPoint) + + fun onMapViewChanged(state: MapViewStateInterface<*>) +} + +class DistanceColorPair( + val distance: Double, + val color: Color, +) + +class TiltMapPageViewModel : + ViewModel(), + TiltMapPageViewModelInterface { + override val initCameraPosition = + MapCameraPosition( + position = + GeoPoint( + latitude = 48.858140690309604, + longitude = 2.2945027576710344, + ), + zoom = 17.0, + bearing = 270.0, + ) + private var currentPosition: MapCameraPosition = initCameraPosition + + private var _disableSlider: MutableStateFlow = MutableStateFlow(false) + + override val disableSlider: StateFlow = _disableSlider.asStateFlow() + + private var _cameraPosition: MutableStateFlow = MutableStateFlow(currentPosition) + + override val cameraPosition: StateFlow = _cameraPosition.asStateFlow() + + private var _mapViewState: MutableStateFlow?> = MutableStateFlow(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override fun onMapViewChanged(state: MapViewStateInterface<*>) { + mapViewState.value?.cameraPosition?.let { + state.moveCameraTo(it) + } + this._mapViewState.value = state + } + + private var _tilt by mutableStateOf(initCameraPosition.tilt) + + override var tilt: Double + get(): Double = _tilt + + set(angle) { + if (_disableSlider.value) return + _tilt = angle + + currentPosition = + currentPosition.copy( + tilt = angle, + ) + _cameraPosition.value = currentPosition + + mapViewState.value?.moveCameraTo(currentPosition) + } + + override fun onMapCameraMoveStart(point: GeoPoint) { + _disableSlider.value = true + } + + override fun onMapCameraMoveEnd(point: GeoPoint) { + _disableSlider.value = false + _mapViewState.value?.let { + currentPosition = it.cameraPosition + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt index 1e9b542f..01094fa6 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficePage.kt @@ -3,7 +3,6 @@ package com.mapconductor.example.pages.marker.postoffice import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -54,10 +53,6 @@ fun PostOfficePage( val isMapLoaded = viewModel.isMapLoaded.collectAsState().value val isDataLoading = viewModel.isDataLoading.collectAsState().value - LaunchedEffect(Unit) { - viewModel.loadPostOfficeData() - } - DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficeViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficeViewModel.kt index db86539e..e50c9313 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficeViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/postoffice/PostOfficeViewModel.kt @@ -36,7 +36,7 @@ interface PostOfficeViewModelInterface { fun onInfoClick(postOffice: PostOffice) - fun loadPostOfficeData() + // fun loadPostOfficeData() } class PostOfficeViewModel( @@ -91,13 +91,13 @@ class PostOfficeViewModel( private var cameraPosition: MapCameraPosition = initCameraPosition - override fun loadPostOfficeData() { + fun loadPostOfficeData() { if (_markerList.value.isNotEmpty()) return coroutine.launch { + sleep(1000) _isDataLoading.value = true val postOffices = dataLoader.loadAllPostOffices() - sleep(1000) val markerStates = postOffices.map { it -> @@ -110,7 +110,6 @@ class PostOfficeViewModel( ) } _markerList.value = markerStates - sleep(6000) _isDataLoading.value = false } } @@ -136,6 +135,7 @@ class PostOfficeViewModel( _mapViewState.value?.moveCameraTo( cameraPosition = cameraPosition, ) + loadPostOfficeData() } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPage.kt index b38d89b6..354c7e5f 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPage.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPage.kt @@ -3,7 +3,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -105,10 +104,6 @@ fun MarkerClusterMapPage( val isMapLoaded = viewModel.isMapLoaded.collectAsState().value val isDataLoading = viewModel.isDataLoading.collectAsState().value - LaunchedEffect(Unit) { - viewModel.loadPostOfficeData() - } - DemoMapPageScaffold( menuItems = DefaultMapViewItems(viewModel.initCameraPosition), onToggleSidebar = onToggleSidebar, diff --git a/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPageViewModel.kt index 72ea2c62..c37c6898 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/marker/postofficecluster/MarkerClusterMapPageViewModel.kt @@ -36,8 +36,6 @@ interface MarkerClusterMapPageViewModelInterface { fun onMapLoaded(mapViewState: MapViewStateInterface<*>) fun onInfoClick(postOffice: PostOffice) - - fun loadPostOfficeData() } class MarkerClusterMapPageViewModel( @@ -78,13 +76,13 @@ class MarkerClusterMapPageViewModel( MutableStateFlow(null) override val renderingStrategy: StateFlow?> = _renderingStrategy.asStateFlow() - override fun loadPostOfficeData() { + fun loadPostOfficeData() { if (_markerList.value.isNotEmpty()) return viewModelScope.launch(Dispatchers.Default) { _isDataLoading.value = true try { - delay(3000) + delay(1000) val postOffices = dataLoader.loadAllPostOffices() val markerStates = @@ -98,7 +96,6 @@ class MarkerClusterMapPageViewModel( ) } _markerList.value = markerStates - delay(3000) } catch (t: Throwable) { _markerList.value = emptyList() } finally { @@ -128,6 +125,7 @@ class MarkerClusterMapPageViewModel( _mapViewState.value?.moveCameraTo( cameraPosition = cameraPosition, ) + loadPostOfficeData() } } diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPage.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPage.kt new file mode 100644 index 00000000..45ce6848 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPage.kt @@ -0,0 +1,96 @@ +package com.mapconductor.example.pages.polygon.hole + +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.polygon.Polygon +import com.mapconductor.core.polygon.PolygonState +import com.mapconductor.example.MapViewContainer +import com.mapconductor.example.ui.DefaultMapViewItems +import com.mapconductor.example.ui.DemoMapPageScaffold +import com.mapconductor.example.ui.MessageCard + +@Composable +fun HolePolygonMapPage(onToggleSidebar: () -> Unit = {}) { + val viewModel = remember { HolePolygonMapPageViewModel() } + + val polygonState = + remember { + PolygonState( + points = + listOf( + GeoPoint(85.0, 90.0), + GeoPoint(85.0, 0.1), + GeoPoint(85.0, -90.0), + GeoPoint(85.0, -179.9), + GeoPoint(0.0, -179.9), + GeoPoint(-85.0, -179.9), + GeoPoint(-85.0, -90.0), + GeoPoint(-85.0, 0.1), + GeoPoint(-85.0, 90.0), + GeoPoint(-85.0, 179.9), + GeoPoint(0.0, 179.9), + GeoPoint(85.0, 179.9), + ), + holes = + listOf( + listOf( + GeoPoint(43.10086924222251, 141.35290903949243), + GeoPoint(43.04444342582366, 141.4118953480885), + GeoPoint(43.05060149394299, 141.30656265416695), + ), + listOf( + GeoPoint(43.06035050410283, 141.31990479539704), + GeoPoint(43.038284739487004, 141.33324693662706), + GeoPoint(43.049062034871525, 141.28690055130158), + ), + ), + fillColor = Color(0xCC787880), + strokeColor = Color.Red, + strokeWidth = 2.dp, + ) + } + + DemoMapPageScaffold( + menuItems = DefaultMapViewItems(viewModel.initCameraPosition), + onToggleSidebar = onToggleSidebar, + onMapViewStateChanged = viewModel::onMapViewChanged, + ) { paddingValues -> + val mapViewState = viewModel.mapViewState.collectAsState() + + mapViewState.value?.let { + MapViewContainer(state = it) { + Polygon(polygonState) + } + } + + MessageCard( + title = "Hole Polygon Example", + modifier = + Modifier + .align(Alignment.BottomStart) + .padding( + bottom = paddingValues.calculateBottomPadding() + 16.dp, + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr) + 16.dp, + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr) + 16.dp, + ), + ) { + Text( + """ + A world-covering polygon with two triangular holes near Sapporo. + The grey overlay covers the entire map except the hole areas. + """.trimIndent(), + ) + } + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPageViewModel.kt new file mode 100644 index 00000000..611f15d3 --- /dev/null +++ b/example-app/src/main/java/com/mapconductor/example/pages/polygon/hole/HolePolygonMapPageViewModel.kt @@ -0,0 +1,33 @@ +package com.mapconductor.example.pages.polygon.hole + +import androidx.lifecycle.ViewModel +import com.mapconductor.core.features.GeoPoint +import com.mapconductor.core.map.MapCameraPosition +import com.mapconductor.core.map.MapViewStateInterface +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +interface HolePolygonMapPageViewModelInterface { + val initCameraPosition: MapCameraPosition + val mapViewState: StateFlow?> + + fun onMapViewChanged(state: MapViewStateInterface<*>) +} + +class HolePolygonMapPageViewModel : + ViewModel(), + HolePolygonMapPageViewModelInterface { + private val _mapViewState = MutableStateFlow?>(null) + override val mapViewState: StateFlow?> = _mapViewState.asStateFlow() + + override val initCameraPosition = + MapCameraPosition( + position = GeoPoint(43.06050568387817, 141.35374551567804), + zoom = 11.0, + ) + + override fun onMapViewChanged(state: MapViewStateInterface<*>) { + _mapViewState.value = state + } +} diff --git a/example-app/src/main/java/com/mapconductor/example/pages/rasterlayer/RasterLayerPageViewModel.kt b/example-app/src/main/java/com/mapconductor/example/pages/rasterlayer/RasterLayerPageViewModel.kt index 3a4597ca..6e30ad6c 100644 --- a/example-app/src/main/java/com/mapconductor/example/pages/rasterlayer/RasterLayerPageViewModel.kt +++ b/example-app/src/main/java/com/mapconductor/example/pages/rasterlayer/RasterLayerPageViewModel.kt @@ -51,7 +51,6 @@ class RasterLayerPageViewModel : tileSize = RasterLayerSource.DEFAULT_TILE_SIZE, ), opacity = opacity, - debug = true, ) private val _mapViewState = MutableStateFlow?>(null) diff --git a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt index 6047ddf9..1cfdc388 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/DemoMapPageScaffold.kt @@ -120,7 +120,7 @@ fun GetArcGISViewItem(initCameraPosition: MapCameraPositionInterface): IconItem< fun GetMapLibreViewItem(initCameraPosition: MapCameraPositionInterface): IconItem { val mapLibreMapState = rememberMapLibreMapViewState( - mapDesign = MapLibreDesign.DemoTiles, + mapDesign = MapLibreDesign.OsmBright, cameraPosition = initCameraPosition, ) return IconItem( diff --git a/example-app/src/main/java/com/mapconductor/example/ui/IconSelectMenu.kt b/example-app/src/main/java/com/mapconductor/example/ui/IconSelectMenu.kt index dd4e03a1..fe33ab64 100644 --- a/example-app/src/main/java/com/mapconductor/example/ui/IconSelectMenu.kt +++ b/example-app/src/main/java/com/mapconductor/example/ui/IconSelectMenu.kt @@ -10,9 +10,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuAnchorType.Companion.PrimaryEditable import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.Icon -import androidx.compose.material3.MenuAnchorType import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable @@ -38,7 +38,7 @@ data class IconItem( @Composable fun IconSelectMenu( itemList: List>, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.width(200.dp), selectedIndex: Int = 0, isDark: Boolean = isSystemInDarkTheme(), onSelect: (Int, IconItem<*>) -> Unit = { index, item -> @@ -80,7 +80,7 @@ fun IconSelectMenu( }, modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryEditable), + .menuAnchor(PrimaryEditable), ) ExposedDropdownMenu( diff --git a/gradle.properties b/gradle.properties index 639812a5..3f2df634 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -# Project-wide Gradle settings. +# Project-wide Gradle settings. # IDE (e.g. Android Studio) users: # Gradle settings configured through the IDE *will override* # any settings specified in this file. @@ -23,8 +23,8 @@ kotlin.code.style=official android.nonTransitiveRClass=true # Must match with the compose value in libs.version.toml kotlinCompilerExtensionVersion=1.8.1 -compileSdk=35 -targetSdk=35 +compileSdk=37 +targetSdk=37 minSdk=26 versionCode=1 versionName=1.0.0 @@ -34,7 +34,9 @@ isMinifyEnabled=true # R8 settings for handling duplicate classes android.enableR8.fullMode=true android.r8.ignoreAllowListBinaryFiles=true -android.enableJetifier=true # Enable support for flexible page sizes (e.g., 16KB pages on Android 15+) APP_SUPPORT_FLEXIBLE_PAGE_SIZES = true - +android.uniquePackageNames=false +android.dependency.useConstraints=true +android.r8.strictFullModeForKeepRules=false +android.generateSyncIssueWhenLibraryConstraintsAreEnabled=false diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bbcf2b10..1e1cfd71 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,10 +1,10 @@ [versions] -agp = "8.13.2" +agp = "9.2.1" core = "1.7.0" -kotlin = "2.3.20" +kotlin = "2.4.0" -coreKtx = "1.15.0" +coreKtx = "1.19.0" appcompat = "1.7.1" junit = "4.13.2" @@ -12,37 +12,33 @@ junitVersion = "1.3.0" espressoCore = "3.7.0" lifecycleRuntimeKtx = "2.10.0" -activityCompose = "1.10.0" +activityCompose = "1.13.0" lifecycleViewmodelCompose = "2.10.0" # https://developer.android.com/develop/ui/compose/bom/bom-mapping # gradle.properties?kotlinCompilerExtensionVersion?????? -compose = "1.10.6" -composeBom = "2026.03.01" +compose = "1.11.2" +composeBom = "2026.05.01" okhttp = "5.3.2" runner = "1.7.0" -mapboxAndroid = "11.14.3" +mapboxAndroid = "11.24.3" playServicesMaps = "20.0.0" arcgisMapsKotlin = "200.7.0" vectordrawable = "1.2.0" ktLint = "14.2.0" -uiToolingPreviewAndroid = "1.9.1" -uiToolingPreview = "1.9.1" -uiTooling = "1.9.1" geographiclib = "2.1" -material = "1.10.0" -mapLibreSdk = "13.0.2" +mapLibreSdk = "13.2.0" mapLibreAnnotation = "3.0.2" -runtime = "1.10.6" -runtimeVersion = "1.10.6" -materialVersion = "1.13.0" +runtime = "1.11.2" +runtimeVersion = "1.11.2" +materialVersion = "1.14.0" jts = "1.20.0" -coroutines = "1.10.2" -uiGraphics = "1.10.6" -foundation = "1.10.6" +coroutines = "1.11.0" +uiGraphics = "1.11.2" +foundation = "1.11.2" [libraries] # Android ?? @@ -80,6 +76,7 @@ mapconductor-maplibre = { module = "com.mapconductor:for-maplibre" } mapconductor-marker-strategy = { module = "com.mapconductor:marker-strategy" } mapconductor-marker-native-strategy = { module = "com.mapconductor:marker-native-strategy" } mapconductor-marker-clustering = { module = "com.mapconductor:marker-clustering" } +mapconductor-geojson = { module = "com.mapconductor:geojson" } # Mapbox mapbox-android = { module = "com.mapbox.maps:android-ndk27", version.ref = "mapboxAndroid" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index af9ba90b..311b1ea0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Fri Apr 11 01:40:57 PDT 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/mapconductor-bom/build.gradle.kts b/mapconductor-bom/build.gradle.kts index 0907440c..01e45bf2 100644 --- a/mapconductor-bom/build.gradle.kts +++ b/mapconductor-bom/build.gradle.kts @@ -2,6 +2,7 @@ plugins { id("java-platform") id("maven-publish") id("signing") + id("com.gradleup.nmcp") version "1.5.0" } // Publishing configuration @@ -24,6 +25,7 @@ val moduleInfo = mapOf( "for-maplibre" to getModuleVersion(":android-for-maplibre"), "icons" to getModuleVersion(":android-icons"), "heatmap" to getModuleVersion(":android-heatmap"), + "geojson" to getModuleVersion(":android-geojson-layer"), "marker-clustering" to getModuleVersion(":android-marker-clustering"), ) diff --git a/mapconductor-bom/gradle.properties b/mapconductor-bom/gradle.properties index e445bf5a..b3f2adcb 100644 --- a/mapconductor-bom/gradle.properties +++ b/mapconductor-bom/gradle.properties @@ -1 +1 @@ -libraryVersion=1.1.4 +libraryVersion=1.1.7 diff --git a/projects.properties b/projects.properties index 4f8d4e53..9adbf75e 100644 --- a/projects.properties +++ b/projects.properties @@ -5,9 +5,11 @@ modules=example-app,\ android-icons,\ android-marker-clustering,\ android-heatmap,\ + android-geojson-layer,\ mapconductor-bom,\ android-for-here,\ android-for-mapbox,\ android-for-googlemaps,\ android-for-arcgis,\ - android-for-maplibre + android-for-maplibre,\ + android-geojson-layer diff --git a/scripts/commit-all.sh b/scripts/commit-all.sh new file mode 100755 index 00000000..e6a19431 --- /dev/null +++ b/scripts/commit-all.sh @@ -0,0 +1,130 @@ +#!/bin/bash + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR=$(dirname "${SCRIPT_DIR}") + +FALSE=1 +TRUE=0 + +BLACK=$'\e[30m' +RED=$'\e[31m' +GREEN=$'\e[32m' +YELLOW=$'\e[33m' +BLUE=$'\e[34m' +MAGENTA=$'\e[35m' +CYAN=$'\e[36m' +WHITE=$'\e[37m' +RESET=$'\e[0m' +ASK_COLOR=$GREEN +SEPARATOR_COLOR=$BLUE + +git-claude-commit() { + if git diff --cached --quiet; then + echo "No changes staged. Please run 'git add' first." + return $FALSE + fi + + echo "Generating English commit message using Claude..." + + local prompt="Analyze the following git diff and generate a clear, concise git commit message in English following Conventional Commits format (e.g., feat: add login feature). Output ONLY the final commit message text. Absolutely NO explanations, NO greetings, and NO markdown code blocks." + local msg + msg=$(git diff --cached | claude -p "$prompt") + + if [ -n "$msg" ]; then + echo -e "\ncommit message:\n$msg\n" + git commit -m "$msg" + git push + else + echo "Error: Failed to generate commit message." + return $FALSE + fi +} + +no-any-change() { + local target_dir=$1 + local save_dir + save_dir=$(pwd) + + cd "$target_dir" + + if [[ -z "$(git status --porcelain)" ]]; then + cd "$save_dir" + else + cd "$save_dir" + return $FALSE + fi +} + +has-staged-change() { + ! git diff --cached --quiet +} + +has-unstaged-change() { + ! git diff --quiet || [[ -n "$(git ls-files --others --exclude-standard)" ]] +} + + +ask-to-commit() { + TARGET_DIR=$1 + SAVE_DIR=$(pwd) + if [[ -z "${TARGET_DIR}" ]]; then + return $FALSE + fi + if [ -f "${TARGET_DIR}" ]; then + TARGET_DIR=$(dirname "$TARGET_DIR") + fi + BASE_NAME=$(basename "$TARGET_DIR") + + if no-any-change "$TARGET_DIR"; then + echo "Nothing to commit at ${BASE_NAME}" + return $FALSE + fi + + cd "$TARGET_DIR" + echo + echo -e "${SEPARATOR_COLOR}---[ ${TARGET_DIR} ]--------------${RESET}" + git status + echo + + if has-unstaged-change; then + read -n1 -p "${ASK_COLOR}Git add all unstaged changes at ${BASE_NAME}? (y/N):${RESET}" yn + echo + if [[ $yn = [yY] ]]; then + git add -A + git status + echo + git-claude-commit + cd "$SAVE_DIR" + return + fi + fi + + if ! has-staged-change; then + echo "No staged changes at ${BASE_NAME}" + cd "$SAVE_DIR" + return $FALSE + fi + + read -n1 -p "${ASK_COLOR}Git commit at ${BASE_NAME} directory? (y/N):${RESET}" yn + echo + if [[ $yn = [yY] ]]; then + git-claude-commit + cd "$SAVE_DIR" + else + echo "skip committing" + echo $FALSE + cd "$SAVE_DIR" + fi +} + +ask-to-commit "${ROOT_DIR}/android-sdk-core" +ask-to-commit "${ROOT_DIR}/android-for-googlemaps" +ask-to-commit "${ROOT_DIR}/android-for-arcgis" +ask-to-commit "${ROOT_DIR}/android-for-here" +ask-to-commit "${ROOT_DIR}/android-for-maplibre" +ask-to-commit "${ROOT_DIR}/android-for-mapbox" +ask-to-commit "${ROOT_DIR}/android-heatmap" +ask-to-commit "${ROOT_DIR}/android-icons" +ask-to-commit "${ROOT_DIR}/android-geojson-layer" +ask-to-commit "${ROOT_DIR}/android-marker-clustering" +ask-to-commit "${ROOT_DIR}/" diff --git a/scripts/sync-module-branches.sh b/scripts/sync-module-branches.sh new file mode 100755 index 00000000..8e61ac2b --- /dev/null +++ b/scripts/sync-module-branches.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(dirname "${SCRIPT_DIR}")" + +TARGET_BRANCH="" +BASE_BRANCH="main" +FETCH=1 +PULL=0 +INCLUDE_ROOT=0 +DRY_RUN=0 + +usage() { + cat <<'USAGE' +Usage: + scripts/sync-module-branches.sh [branch] [options] + +Synchronize all git submodules to the same branch. + +By default, the target branch is the current branch of the parent repository. +If a submodule already has the target branch, the script switches to it. +If origin/ exists, the script creates/tracks the local branch from it. +Otherwise, the script creates the target branch from origin/main or local main. + +Options: + -b, --branch Target branch. Defaults to the parent repository branch. + --base Base branch for newly created branches. Defaults to main. + --no-fetch Do not fetch origin before checking remote branches. + --pull Fast-forward pull after switching to the target branch. + --include-root Also switch the parent repository. + --dry-run Print actions without changing branches. + -h, --help Show this help. + +Examples: + scripts/sync-module-branches.sh + scripts/sync-module-branches.sh feat/agp-9.2.1 --pull + scripts/sync-module-branches.sh --branch main --include-root +USAGE +} + +log() { + printf '%s\n' "$*" +} + +run() { + if [[ "${DRY_RUN}" -eq 1 ]]; then + printf ' DRY-RUN:' + printf ' %q' "$@" + printf '\n' + else + "$@" + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + -b|--branch) + TARGET_BRANCH="${2:-}" + if [[ -z "${TARGET_BRANCH}" ]]; then + log "Missing branch name for $1" + exit 1 + fi + shift 2 + ;; + --base) + BASE_BRANCH="${2:-}" + if [[ -z "${BASE_BRANCH}" ]]; then + log "Missing branch name for --base" + exit 1 + fi + shift 2 + ;; + --no-fetch) + FETCH=0 + shift + ;; + --pull) + PULL=1 + shift + ;; + --include-root) + INCLUDE_ROOT=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + -*) + log "Unknown option: $1" + usage + exit 1 + ;; + *) + if [[ -n "${TARGET_BRANCH}" ]]; then + log "Unexpected argument: $1" + usage + exit 1 + fi + TARGET_BRANCH="$1" + shift + ;; + esac + done +} + +current_parent_branch() { + git -C "${ROOT_DIR}" symbolic-ref --quiet --short HEAD +} + +ensure_clean_worktree() { + local repo_dir="$1" + local label="$2" + + if [[ -n "$(git -C "${repo_dir}" status --porcelain)" ]]; then + log "Refusing to switch ${label}: worktree has uncommitted changes." + git -C "${repo_dir}" status --short + exit 1 + fi +} + +has_local_branch() { + local repo_dir="$1" + local branch="$2" + git -C "${repo_dir}" show-ref --verify --quiet "refs/heads/${branch}" +} + +has_remote_branch() { + local repo_dir="$1" + local branch="$2" + git -C "${repo_dir}" show-ref --verify --quiet "refs/remotes/origin/${branch}" +} + +sync_repo_branch() { + local repo_dir="$1" + local label="$2" + + log "==> ${label}" + ensure_clean_worktree "${repo_dir}" "${label}" + + if [[ "${FETCH}" -eq 1 ]]; then + run git -C "${repo_dir}" fetch --prune origin + fi + + if has_local_branch "${repo_dir}" "${TARGET_BRANCH}"; then + run git -C "${repo_dir}" switch "${TARGET_BRANCH}" + elif has_remote_branch "${repo_dir}" "${TARGET_BRANCH}"; then + run git -C "${repo_dir}" switch --track -c "${TARGET_BRANCH}" "origin/${TARGET_BRANCH}" + elif has_remote_branch "${repo_dir}" "${BASE_BRANCH}"; then + run git -C "${repo_dir}" switch -c "${TARGET_BRANCH}" "origin/${BASE_BRANCH}" + elif has_local_branch "${repo_dir}" "${BASE_BRANCH}"; then + run git -C "${repo_dir}" switch -c "${TARGET_BRANCH}" "${BASE_BRANCH}" + else + log "Cannot create ${TARGET_BRANCH} in ${label}: base branch '${BASE_BRANCH}' was not found." + exit 1 + fi + + if [[ "${PULL}" -eq 1 ]]; then + run git -C "${repo_dir}" pull --ff-only + fi + + if [[ "${DRY_RUN}" -eq 1 ]]; then + log " target branch: ${TARGET_BRANCH}" + else + log " current branch: $(git -C "${repo_dir}" branch --show-current)" + fi +} + +submodule_paths() { + git -C "${ROOT_DIR}" config --file .gitmodules --get-regexp '^submodule\..*\.path$' | + awk '{print $2}' +} + +main() { + parse_args "$@" + + if [[ -z "${TARGET_BRANCH}" ]]; then + TARGET_BRANCH="$(current_parent_branch)" + fi + + if [[ -z "${TARGET_BRANCH}" ]]; then + log "Could not determine target branch. Pass one with --branch." + exit 1 + fi + + log "Target branch: ${TARGET_BRANCH}" + log "Base branch: ${BASE_BRANCH}" + + if [[ "${INCLUDE_ROOT}" -eq 1 ]]; then + sync_repo_branch "${ROOT_DIR}" "$(basename "${ROOT_DIR}")" + fi + + while IFS= read -r path; do + [[ -z "${path}" ]] && continue + sync_repo_branch "${ROOT_DIR}/${path}" "${path}" + done < <(submodule_paths) +} + +main "$@" + diff --git a/settings.gradle.kts b/settings.gradle.kts index 8ac6041a..97395d24 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -11,9 +11,13 @@ pluginManagement { gradlePluginPortal() } } +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} dependencyResolutionManagement { repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) repositories { + mavenLocal() google() mavenCentral() maven { @@ -25,14 +29,16 @@ dependencyResolutionManagement { url = uri("https://esri.jfrog.io/artifactory/arcgis") } - maven { - name = "GithubPackages" - url = uri("https://maven.pkg.github.com/mapconductor/android-sdk") - credentials { - username = System.getenv("GPR_USER") - password = System.getenv("GPR_TOKEN") + if (System.getenv("GPR_USER") != null && System.getenv("GPR_TOKEN") != null) { + maven { + name = "GithubPackages" + url = uri("https://maven.pkg.github.com/mapconductor/android-sdk") + credentials { + username = System.getenv("GPR_USER") + password = System.getenv("GPR_TOKEN") + } + content { includeGroup("com.mapconductor") } } - content { includeGroup("com.mapconductor") } } flatDir { dirs(rootDir.resolve("libs")) diff --git a/simple-map-app/build.gradle.kts b/simple-map-app/build.gradle.kts index 9f91fda0..49ecd837 100644 --- a/simple-map-app/build.gradle.kts +++ b/simple-map-app/build.gradle.kts @@ -1,6 +1,5 @@ plugins { alias(libs.plugins.android.application) - alias(libs.plugins.kotlin.android) alias(libs.plugins.kotlin.compose) id("org.jlleitschuh.gradle.ktlint") id("com.google.android.libraries.mapsplatform.secrets-gradle-plugin") version "2.0.1" @@ -16,12 +15,12 @@ ktlint { android { namespace = "com.mapconductor.simplemapapp" - compileSdk = 35 + compileSdk = 37 defaultConfig { applicationId = "com.mapconductor.simplemapapp" minSdk = 26 - targetSdk = 35 + targetSdk = 37 versionCode = 1 versionName = "1.0" @@ -42,7 +41,7 @@ android { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android.txt"), + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro", ) } @@ -100,15 +99,6 @@ android { } } -kotlin { - compilerOptions { - jvmTarget.set( - org.jetbrains.kotlin.gradle.dsl.JvmTarget - .fromTarget("11"), - ) - } -} - secrets { propertiesFileName = "secrets.properties" defaultPropertiesFileName = "local.defaults.properties" @@ -162,4 +152,6 @@ dependencies { ) implementation(project(":android-heatmap")) + + implementation(project(":android-geojson-layer")) } diff --git a/camera-sync-map-app/src/main/assets/tokyo-6-areas.zip b/simple-map-app/src/main/assets/tokyo-6-areas.zip similarity index 100% rename from camera-sync-map-app/src/main/assets/tokyo-6-areas.zip rename to simple-map-app/src/main/assets/tokyo-6-areas.zip diff --git a/simple-map-app/src/main/java/com/mapconductor/simplemapapp/MainActivity.kt b/simple-map-app/src/main/java/com/mapconductor/simplemapapp/MainActivity.kt index 031bc946..208eefaf 100644 --- a/simple-map-app/src/main/java/com/mapconductor/simplemapapp/MainActivity.kt +++ b/simple-map-app/src/main/java/com/mapconductor/simplemapapp/MainActivity.kt @@ -3,16 +3,11 @@ package com.mapconductor.simplemapapp import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -20,495 +15,153 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.core.content.ContextCompat -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel -import com.mapconductor.arcgis.map.ArcGISMapView -import com.mapconductor.arcgis.map.rememberArcGISMapViewState -import com.mapconductor.core.circle.Circle -import com.mapconductor.core.circle.CircleState import com.mapconductor.core.features.GeoPoint -import com.mapconductor.core.features.GeoRectBounds -import com.mapconductor.core.groundimage.GroundImage -import com.mapconductor.core.groundimage.GroundImageState import com.mapconductor.core.info.InfoBubble import com.mapconductor.core.map.MapCameraPosition -import com.mapconductor.core.marker.DefaultMarkerIcon import com.mapconductor.core.marker.Marker -import com.mapconductor.core.marker.MarkerAnimation -import com.mapconductor.core.marker.MarkerState -import com.mapconductor.core.marker.Markers -import com.mapconductor.core.polygon.Polygon -import com.mapconductor.core.polygon.PolygonState -import com.mapconductor.core.polyline.Polyline -import com.mapconductor.core.polyline.PolylineState -import com.mapconductor.googlemaps.GoogleMapDesign -import com.mapconductor.googlemaps.GoogleMapView -import com.mapconductor.googlemaps.rememberGoogleMapViewState -import com.mapconductor.heatmap.HeatmapOverlay -import com.mapconductor.heatmap.HeatmapPoints -import com.mapconductor.here.HereMapView -import com.mapconductor.here.rememberHereMapViewState -import com.mapconductor.mapbox.MapboxMapView -import com.mapconductor.mapbox.rememberMapboxMapViewState -import com.mapconductor.maplibre.MapLibreDesign +import com.mapconductor.geojson.GeoJSONFeature +import com.mapconductor.geojson.GeoJSONLayer +import com.mapconductor.geojson.GeoJSONLayerState +import com.mapconductor.geojson.GeoJSONParser import com.mapconductor.maplibre.MapLibreMapView import com.mapconductor.maplibre.rememberMapLibreMapViewState -import com.mapconductor.simplemapapp.postoffice.HeatmapLayerPageViewModel -import com.mapconductor.simplemapapp.postoffice.HeatmapLayerViewModelInterface -import com.mapconductor.simplemapapp.postoffice.PostOfficeDataLoader import com.mapconductor.simplemapapp.ui.theme.MapConductorSDKTheme -import android.graphics.drawable.Drawable +import java.util.zip.ZipInputStream import android.os.Bundle +import android.util.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() - val image = ContextCompat.getDrawable(this, R.drawable.overlayimg)!! + // val image = ContextCompat.getDrawable(this, R.drawable.overlayimg)!! setContent { MapConductorSDKTheme { Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> - TokyoExample( - modifier = - Modifier - .padding(innerPadding) - .fillMaxSize(), - ) + SimpleMap(modifier = Modifier.padding(innerPadding)) } } } } } + val HND_AIR_PORT = GeoPoint.fromLatLong(35.548852, 139.784086) val SFO_AIR_PORT = GeoPoint.fromLatLong(37.615223, -122.389979) -val HNL_AIR_PORT = GeoPoint.fromLatLong( 21.324513, -157.925074) -val airpots = listOf( - HND_AIR_PORT, - HNL_AIR_PORT, - SFO_AIR_PORT -) - - -val goryokaku = listOf( - GeoPoint.fromLatLong(41.79883, 140.75675), - GeoPoint.fromLatLong(41.799240000000005, 140.75875000000002), - GeoPoint.fromLatLong(41.797650000000004, 140.75905), - GeoPoint.fromLatLong(41.79637, 140.76018000000002), - GeoPoint.fromLatLong(41.79567, 140.75845), - GeoPoint.fromLatLong(41.794470000000004, 140.75714000000002), - GeoPoint.fromLatLong(41.795010000000005, 140.75611), - GeoPoint.fromLatLong(41.79477000000001, 140.75484), - GeoPoint.fromLatLong(41.79576, 140.75475), - GeoPoint.fromLatLong(41.796150000000004, 140.75364000000002), - GeoPoint.fromLatLong(41.79744, 140.75454000000002), - GeoPoint.fromLatLong(41.79909000000001, 140.75465), - GeoPoint.fromLatLong(41.79883, 140.75673), -) - -@Composable -fun TokyoExample(modifier: Modifier = Modifier) { - var selectedMarker by remember { mutableStateOf(null) } +val HNL_AIR_PORT = GeoPoint.fromLatLong(21.324513, -157.925074) +val airpots = + listOf( + HND_AIR_PORT, + HNL_AIR_PORT, + SFO_AIR_PORT, + ) - val center = GeoPoint( - latitude = 35.6762, - longitude = 139.6503, +val goryokaku = + listOf( + GeoPoint.fromLatLong(41.79883, 140.75675), + GeoPoint.fromLatLong(41.799240000000005, 140.75875000000002), + GeoPoint.fromLatLong(41.797650000000004, 140.75905), + GeoPoint.fromLatLong(41.79637, 140.76018000000002), + GeoPoint.fromLatLong(41.79567, 140.75845), + GeoPoint.fromLatLong(41.794470000000004, 140.75714000000002), + GeoPoint.fromLatLong(41.795010000000005, 140.75611), + GeoPoint.fromLatLong(41.79477000000001, 140.75484), + GeoPoint.fromLatLong(41.79576, 140.75475), + GeoPoint.fromLatLong(41.796150000000004, 140.75364000000002), + GeoPoint.fromLatLong(41.79744, 140.75454000000002), + GeoPoint.fromLatLong(41.79909000000001, 140.75465), + GeoPoint.fromLatLong(41.79883, 140.75673), ) +@Composable +fun SimpleMap(modifier: Modifier) { val mapViewState = - rememberHereMapViewState( + rememberMapLibreMapViewState( cameraPosition = MapCameraPosition( - position = center, - zoom = 2.0, + position = GeoPoint(35.68, 139.77), + zoom = 13.0, ), ) - val markerState = remember { MarkerState( - position = center, - icon = DefaultMarkerIcon().copy( - label = "Tokyo", - ), - onClick = { - selectedMarker = it - } - ) } - - HereMapView( - state = mapViewState, - modifier = modifier, - ) { - Marker(markerState) - - selectedMarker?.let { - InfoBubble( - marker = it, - ) { - Text("Hello, world!") - } - } - } -} - -@Composable -fun MapviewExample(modifier: Modifier = Modifier) { - val initCameraPosition = MapCameraPosition( - position = GeoPoint(37.422198, -122.085377), - zoom = 18.0, - tilt = 60.0, - bearing = 30.0, - ) - val mapViewState = rememberGoogleMapViewState( - cameraPosition = initCameraPosition, - ) - - GoogleMapView( + MapLibreMapView( state = mapViewState, modifier = modifier, - ) { - } -} - -@Composable -fun MarkerExample(modifier: Modifier = Modifier) { - var selectedMarker by remember { mutableStateOf(null) } - - val initCameraPosition = MapCameraPosition( - position = GeoPoint(37.422198, -122.085377), - zoom = 18.0, - ) - val mapViewState = rememberGoogleMapViewState( - cameraPosition = initCameraPosition, - ) - val markerState = remember { MarkerState( - position = GeoPoint(37.422198, -122.085377), - icon = DefaultMarkerIcon().copy( - label = "GoogleMaps" - ), - onClick = { - selectedMarker = it + onMapLoaded = { + Log.d("SimpleMap", "---->loaded") }, - ) } - - GoogleMapView( - state = mapViewState, - modifier = modifier, ) { -// selectedMarker?.let { -// InfoBubble( -// marker = it, -// ) { -// Text("Hello, world!") -// } -// } - - Marker(markerState) - } -} - -@Composable -fun PolylineExample(modifier: Modifier = Modifier) { - val initCameraPosition = MapCameraPosition( - position = GeoPoint(41.79,140.75), - zoom = 3.0, - ) - val mapViewState = rememberGoogleMapViewState( - cameraPosition = initCameraPosition, - ) - - val polylineState = remember { PolylineState( - points = airpots, - strokeColor = Color.Blue.copy(alpha = 0.5f), - strokeWidth = 4.dp, - geodesic = true, - ) } - - GoogleMapView(mapViewState) { - Polyline(polylineState) - } -} -@Composable -fun CirleExample(modifier: Modifier = Modifier) { - val initCameraPosition = MapCameraPosition( - position = GeoPoint(37.422198, -122.085377), - zoom = 17.0, - ) - val mapViewState = rememberGoogleMapViewState( - cameraPosition = initCameraPosition, - ) - val circleState = remember { CircleState( - center = GeoPoint(37.422198, -122.085377), - radiusMeters = 50.0, - fillColor = Color.Blue.copy(alpha = 0.5f), - ) - } - - GoogleMapView( - state = mapViewState, - modifier = modifier, - ) { - Circle(circleState) - } -} - -@Composable -fun PolygonExample(modifier: Modifier = Modifier) { - val initCameraPosition = MapCameraPosition( - position = GeoPoint(41.79,140.75), - zoom = 14.0, -// tilt = 60.0, -// bearing = 30.0, - ) - val mapViewState = rememberHereMapViewState( - cameraPosition = initCameraPosition, - ) - - val polygonState = remember { PolygonState( - points = goryokaku, - strokeColor = Color.Blue.copy(alpha = 0.5f), - fillColor = Color.Red.copy(alpha = 0.7f), - ) } - - HereMapView(mapViewState) { - Polygon(polygonState) + Marker( + position = GeoPoint(35.68, 139.77), + onClick = { + Log.d("SimpleMap", "--->clicked") + }, + ) } } @Composable -fun GroundImageExample(modifier: Modifier = Modifier, image: Drawable) { - val initCameraPosition = MapCameraPosition( - position = GeoPoint(51.511649,-0.100761), - zoom = 11.0, - ) - val mapViewState = rememberArcGISMapViewState( - cameraPosition = initCameraPosition, - ) - - val groundImageState = remember { GroundImageState( - bounds = GeoRectBounds( - southWest = GeoPoint.fromLatLong(51.476747, -0.167729), - northEast = GeoPoint.fromLatLong(51.546550,-0.033792), - ), - image = image, - opacity = 0.5f, - ) - } - - ArcGISMapView( - state = mapViewState, - modifier = modifier, - ) { - GroundImage(groundImageState) - } -} +fun GeoJsonLayerExample(modifier: Modifier = Modifier) { + val context = LocalContext.current -@Composable -fun HolePolygonExample(modifier: Modifier = Modifier) { val mapViewState = rememberMapLibreMapViewState( cameraPosition = MapCameraPosition( - position = GeoPoint(43.06050568387817, 141.35374551567804), - zoom = 11.0, + position = GeoPoint(35.68, 139.77), + zoom = 13.0, ), ) - val polygonState = + var selectedFeature by remember { mutableStateOf(null) } + var tappedPosition by remember { mutableStateOf(null) } + + val layerState = remember { - PolygonState( - points = - listOf( - GeoPoint(85.0, 90.0), - GeoPoint(85.0, 0.1), - GeoPoint(85.0, -90.0), - GeoPoint(85.0, -179.9), - GeoPoint(0.0, -179.9), - GeoPoint(-85.0, -179.9), - GeoPoint(-85.0, -90.0), - GeoPoint(-85.0, 0.1), - GeoPoint(-85.0, 90.0), - GeoPoint(-85.0, 179.9), - GeoPoint(0.0, 179.9), - GeoPoint(85.0, 179.9), - ), - holes = - listOf( - listOf( // 1 - GeoPoint(43.10086924222251, 141.35290903949243), - GeoPoint(43.04444342582366, 141.4118953480885), - GeoPoint(43.05060149394299, 141.30656265416695), - ), - listOf( // 2 - GeoPoint(43.06035050410283, 141.31990479539704), - GeoPoint(43.038284739487004, 141.33324693662706), - GeoPoint(43.049062034871525, 141.28690055130158), - ), - ), - fillColor = Color(0xCC787880), - strokeColor = Color.Red, - strokeWidth = 2.dp, + GeoJSONLayerState( + strokeColor = android.graphics.Color.argb(220, 30, 136, 229), + fillColor = android.graphics.Color.argb(60, 30, 136, 229), + strokeWidth = 1.5f, + onClick = { feature, position -> + selectedFeature = feature + tappedPosition = GeoPoint.from(position) + }, ) } -// val polygonState2 = remember { tokyoPolygonState } + var features by remember { mutableStateOf>(emptyList()) } + + LaunchedEffect(Unit) { + features = + withContext(Dispatchers.IO) { + ZipInputStream(context.assets.open("tokyo-6-areas.zip")).use { zis -> + zis.nextEntry ?: return@withContext emptyList() + GeoJSONParser.parseStream(zis) + } + } + } MapLibreMapView( state = mapViewState, modifier = modifier, + onMapClick = { + if (!layerState.processClick(it)) { + tappedPosition = null + selectedFeature = null + } + }, ) { - Polygon(polygonState) -// Polygon(polygonState2) - } -} + GeoJSONLayer(state = layerState, features = features) -// @Composable -// fun BasicGeoJSONExample( -// modifier: Modifier = Modifier, -// ) { -// val context = LocalContext.current -// val mapViewState = -// rememberMapLibreMapViewState( -// cameraPosition = -// MapCameraPosition( -// position = GeoPoint(35.691153, 139.756878), -// zoom = 10.0, -// ) -// ) -// // val geoJson = -// // remember { -// // // A small sample with MultiPoint, MultiLineString and MultiPolygon (lon/lat order). -// // """ -// // { -// // "type": "FeatureCollection", -// // "features": [ -// // { -// // "type": "Feature", -// // "id": "multipoint", -// // "properties": { "name": "MultiPoint sample" }, -// // "geometry": { -// // "type": "MultiPoint", -// // "coordinates": [ -// // [139.76669, 35.68049], -// // [139.75688, 35.69115], -// // [139.78100, 35.67350] -// // ] -// // } -// // }, -// // { -// // "type": "Feature", -// // "id": "multiline", -// // "properties": { "name": "MultiLineString sample" }, -// // "geometry": { -// // "type": "MultiLineString", -// // "coordinates": [ -// // [[139.74, 35.69], [139.77, 35.69], [139.80, 35.69]], -// // [[139.76, 35.66], [139.76, 35.68], [139.76, 35.70]] -// // ] -// // } -// // }, -// // { -// // "type": "Feature", -// // "id": "multipolygon", -// // "properties": { "name": "MultiPolygon sample" }, -// // "geometry": { -// // "type": "MultiPolygon", -// // "coordinates": [ -// // [ -// // [ -// // [139.7500, 35.6750], -// // [139.7800, 35.6750], -// // [139.7800, 35.6950], -// // [139.7500, 35.6950], -// // [139.7500, 35.6750] -// // ] -// // ] -// // ] -// // } -// // } -// // ] -// // } -// // """.trimIndent() -// // } -// val geoJsonInputStream = remember { -// context.assets.open("prefectures.geojson") -// } -// -// MapLibreMapView( -// modifier = modifier, -// state = mapViewState, -// markerTiling = MarkerTilingOptions.Default.copy( -// minMarkerCount = 0, -// debugTileOverlay = true, -// ), -// ) { -// GeoJsonLayer( -// geoJsonInputStream = geoJsonInputStream, -// geoJsonKey = geoJsonInputStream.hashCode(), -// style = -// GeoJsonLayerStyle( -// polylineStrokeColor = Color(0xFF1565C0), -// polylineStrokeWidth = 3.dp, -// polygonStrokeColor = Color(0xFF2E7D32), -// polygonStrokeWidth = 2.dp, -// polygonFillColor = Color(0x332E7D32), -// ), -// ) -// } -// } - -@Composable -fun HeatmapExample(modifier: Modifier = Modifier) { - val context = LocalContext.current - val dataLoader = remember { PostOfficeDataLoader(context) } - - val viewModel: HeatmapLayerViewModelInterface = - viewModel( - factory = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - if (modelClass.isAssignableFrom(HeatmapLayerPageViewModel::class.java)) { - @Suppress("UNCHECKED_CAST") - return HeatmapLayerPageViewModel( - dataLoader = dataLoader, - ) as T - } - throw IllegalArgumentException("Unknown ViewModel class") - } - }, - ) - LaunchedEffect(Unit) { - viewModel.loadPostOfficeData() - } - val points = viewModel.heatmapPoints.collectAsState().value - - val camera = - remember { - MapCameraPosition( - position = - GeoPoint.fromLatLong( - latitude = 35.68049, - longitude = 139.76669, - ), - zoom = 10.0, - bearing = 0.0, - tilt = 0.0, - paddings = null, - ) + tappedPosition?.let { pt -> + InfoBubble(position = pt) { + selectedFeature?.properties?.let { + TableView(it, 0.3f, 0.7f) + } + } } - val mapViewState = rememberHereMapViewState(cameraPosition = camera) - -// HereMapView( -// state = mapViewState, -// modifier = modifier, -// ) { -// HeatmapOverlay( -// trackPointUpdates = false, -// ) { -// HeatmapPoints(points) -// } -// } + } } diff --git a/simple-map-app/src/main/java/com/mapconductor/simplemapapp/Table.kt b/simple-map-app/src/main/java/com/mapconductor/simplemapapp/Table.kt index 96811ae8..1899352b 100644 --- a/simple-map-app/src/main/java/com/mapconductor/simplemapapp/Table.kt +++ b/simple-map-app/src/main/java/com/mapconductor/simplemapapp/Table.kt @@ -1,4 +1,65 @@ package com.mapconductor.simplemapapp -class Table { +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +private fun RowScope.TableCell( + text: String, + weight: Float, +) { + Text( + text = text, + Modifier + .border(1.dp, Color.Black) + .weight(weight) + .padding(8.dp), + ) +} + +@Composable +fun TableView( + tableData: Map, + column1Weight: Float, + column2Weight: Float, +) { + Column( + modifier = + Modifier + .width(400.dp) + .heightIn(max = 300.dp) + .padding(0.dp) + .verticalScroll(rememberScrollState()), + ) { + Row( + modifier = Modifier.background(Color.Gray), + ) { + TableCell(text = "Column 1", weight = column1Weight) + TableCell(text = "Column 2", weight = column2Weight) + } + + tableData.map { + val (id, text) = it + Row( + modifier = Modifier.fillMaxWidth(), + ) { + TableCell(text = id.toString(), weight = column1Weight) + TableCell(text = text.toString(), weight = column2Weight) + } + } + } }