Skip to content

feat(utils): add Compose wrappers for HeatmapTileOverlay, DataLayer, GeoJsonLayer, and KmlLayer - #1048

Open
dkhawk wants to merge 2 commits into
feat/maps-utils-6.1.0from
feat/maps-compose-utils-layers
Open

dkhawk wants to merge 2 commits into
feat/maps-utils-6.1.0from
feat/maps-compose-utils-layers

Conversation

@dkhawk

@dkhawk dkhawk commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Stack

Summary

  • HeatmapTileOverlay: Compose wrapper around HeatmapTileProvider and MapApplier tile overlays with reactive updates for data, weightedData, gradient, radius, opacity, and maxIntensity.
  • DataScene & DataLayer: Compose wrappers around the platform-agnostic com.google.maps.android.data.DataLayer, DataScene, and MapViewRenderer introduced in android-maps-utils 6.x.
  • GeoJsonLayer: Compose wrapper supporting raw GeoJSON resources, JSONObject, and InputStream parsing via DataLayerLoader.fromGeoJson.
  • KmlLayer: Compose wrapper supporting raw KML/KMZ resources and InputStream parsing via DataLayerLoader.fromKml / DataLayerLoader.fromKmz, including the KMZ zip-bomb security limits (maxKmzEntryCount, maxKmzUncompressedTotalSize) introduced in android-maps-utils 6.1.0 (fix(data): apply StyleMap styles and fix four KML and GeoJSON style bugs android-maps-utils#1809).
  • Add unit tests in HeatmapTileOverlayTest.kt and DataLayerTest.kt.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Coverage (unit tests)

Compared against db20d0d on main (#1056).

Module Line % Change Branch % Change
maps-compose 15.28% no change 3.76% no change
maps-compose-utils 12.12% 🔺 +6.03 6.68% 🔺 +2.21
maps-compose-widgets 0.00% no change 0.00% no change
TOTAL 13.60% 🔺 +1.19 4.53% 🔺 +0.74

Line and branch coverage from unit test reports. History is recorded on the coverage-history branch after each merge to main.

@googlemaps-bot

googlemaps-bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Coverage (instrumentation tests)

Overall Project 41.57% -4.91% 🍏
Files changed 0% ❌

Module Coverage
maps-app 36.72% -25.81% ❌
Files
Module File Coverage
maps-app DataLayer.kt 0% ❌
HeatmapTileOverlay.kt 0% ❌

@kikoso kikoso left a comment •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for adding these, @dkhawk ! Can you take a look at the comments? The description also mentions InputStream overloads and DataLayerLoader.fromKml/fromKmz that I can't find in the code.


val currentRenderer = renderer ?: return

DisposableEffect(currentRenderer, scene, useAdvancedMarkers) {

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MapViewRenderer.clear() cancels its rendererScope for good (maps-utils 6.1.0), and here we keep rendering with the same instance after scene or useAdvancedMarkers change. From then on the loadIcon coroutines never run, so remote KML/GeoJSON icons stop loading. Could we create a new renderer per scene, or change clear() in maps-utils so it doesn't cancel the scope?

@Composable
@GoogleMapComposable
@MapsComposeExperimentalApi
public fun DataLayer(

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds ten public composables and several overlap (DataLayer(assetName) vs KmlLayer(assetName), DataLayer(rawResourceId) vs the raw resource GeoJsonLayer/KmlLayer), and once released they are hard to remove. My two cents: let's start with DataScene and DataLayer(layer) plus a single loading path, and add the format specific ones when someone asks for them. Naming them like the model classes also forces users to import both in the same file, as happens here.

if (weightedData.isEmpty()) return

var holder by remember { mutableStateOf<HeatmapProviderHolder?>(null) }
val currentHolder = updateHeatmapProviderHolder(

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updateHeatmapProviderHolder mutates the provider and writes holder during composition, so a discarded composition would leave the provider with values that were never committed. What about keeping the provider in a remember and pushing the updates from a SideEffect? Also, Gradient has no equals, so an inline Gradient(...) would call setGradient and clearTileCache() on every recomposition.

*/
@Composable
@GoogleMapComposable
public fun HeatmapTileOverlay(

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the other new composables are @MapsComposeExperimentalApi, I would mark this one too (and the weighted overload) for consistency.

Suggested change
public fun HeatmapTileOverlay(
@MapsComposeExperimentalApi
public fun HeatmapTileOverlay(

onLayerLoaded: (DataLayer?) -> Unit = {},
) {
val onLayerLoadedState by rememberUpdatedState(onLayerLoaded)
var loadedLayer by remember(geoJsonData) { mutableStateOf<DataLayer?>(null) }

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSONObject doesn't override equals, so an object built during composition is parsed again on every recomposition. Resetting loadedLayer to null on a key change also makes the layer flicker until the new one is parsed (same in the other loaders), so I would keep the previous layer until the new one is ready.

import java.io.InputStream
import java.nio.charset.StandardCharsets

private const val DEFAULT_MAX_KMZ_ENTRY_COUNT = 200

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These copy the private defaults of KmzParser and can easily drift, shouldn't we expose them from maps-utils instead? The sniffing below also duplicates what DataLayerLoader already does.

maxKmzUncompressedTotalSize = maxKmzUncompressedTotalSize,
)
}
}.getOrNull()

@kikoso kikoso Oct 22, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With getOrNull() a missing resource, a malformed file and a KMZ rejected by the zip bomb limits all end up as onLayerLoaded(null), so apps can't tell them apart. I wonder if we could expose the error, e.g. through an onError callback.

@dkhawk
dkhawk force-pushed the feat/maps-compose-utils-layers branch 3 times, most recently from 37f6f7b to b1d9f6a Compare October 29, 2026 17:21
…GeoJsonLayer, and KmlLayer

- Add HeatmapTileOverlay composable wrapping HeatmapTileProvider and MapApplier tile overlays with reactive updates for data, weightedData, gradient, radius, opacity, and maxIntensity.
- Add DataScene and DataLayer composables wrapping the platform-agnostic DataLayer, DataScene, and MapViewRenderer introduced in android-maps-utils 6.x.
- Add GeoJsonLayer and KmlLayer composables supporting raw resources, JSONObject, InputStream, and KMZ archive security limits (maxKmzEntryCount, maxKmzUncompressedTotalSize).
- Add unit tests for HeatmapTileOverlay and DataLayer/GeoJsonLayer/KmlLayer.
@dkhawk
dkhawk force-pushed the feat/maps-compose-utils-layers branch from b1d9f6a to c0d81bc Compare October 29, 2026 21:25

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants