Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 123 additions & 1 deletion .gemini/skills/android-maps-utils/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: maps-utils-android
description: Guide for integrating the Google Maps Utility Library for Android (android-maps-utils) into an application. Use when users ask to add features like Marker Clustering, Heatmaps, GeoJSON, KML, or Polyline encoding/decoding.
description: Guide for integrating the Google Maps Utility Library for Android (android-maps-utils) into an application. Use when users ask to add features like Marker Clustering, Heatmaps, GeoJSON, KML, Polyline encoding/decoding, or Units (distance and elevation conversion and formatting).
---

# Google Maps Utility Library for Android Integration
Expand Down Expand Up @@ -45,6 +45,128 @@ Used for server-client coordinate compression and distance calculations.
* **Decoding:** `PolyUtil.decode(encodedPathString)`
* **Distance:** `SphericalUtil.computeDistanceBetween(latLng1, latLng2)`

### Units & Logical Distance/Elevation Formatting
Used to represent measurements with the type-safe `Meters` inline value class and format distances and elevations in Metric (`m`, `km`) or Imperial (`ft`, `mi`) units based on the device's `Locale` or `Resources` (`US`, `LR`, and `MM` default to `IMPERIAL` for both; `GB` defaults to `IMPERIAL` road distances + `METRIC` elevations; all others default to `METRIC`), or an explicit `UnitSystem` / formatter override.

* **Logical Precision Rules:**
* **Small distances** (`< 1 km` metric or `< 0.25 mi` imperial) and **all elevations** format as whole integers with locale thousands grouping (`"450 m"`, `"528 ft"`, `"14,440 ft"`).
* **Medium distances** (`1.0` to `< 10` km, or `0.25` to `< 10` mi) format with 1 decimal digit, keeping trailing `.0` (`"0.3 mi"`, `"1.0 km"`, `"2.5 mi"`).
* **Large distances** (`>= 10` km or `>= 10` mi) round to whole integers (`"10 km"`, `"12 mi"`).

* **Kotlin Example:**
```kotlin
import com.google.maps.android.ImperialUnitsConverter
import com.google.maps.android.Meters
import com.google.maps.android.MetricUnitsConverter
import com.google.maps.android.UnitSystem
import com.google.maps.android.UnitsConverter
import com.google.maps.android.feet
import com.google.maps.android.getUnitsConverter
import com.google.maps.android.inFeet
import com.google.maps.android.inKilometers
import com.google.maps.android.kilometers
import com.google.maps.android.meters
import com.google.maps.android.miles
import com.google.maps.android.sphericalDistance
import com.google.maps.android.toDistanceString
import com.google.maps.android.toElevationString
import java.util.Locale

// Construct type-safe Meters and perform arithmetic or unit conversions
val summitElevation: Meters = 4401.2.meters
val elevationInFeet: Double = summitElevation.inFeet // 14439.6325...
val routeDistance: Meters = startLatLng.sphericalDistance(endLatLng).meters + 500.feet

// 1. Default formatting using device Locale, explicit Locale, or Android Resources
val defaultDistance: String = routeDistance.toDistanceString()
val localizedDistance: String = routeDistance.toDistanceString(context.resources)
val ukDistance: String = 1.miles.toDistanceString(Locale.UK) // "1.0 mi"
val ukElevation: String = 1345.meters.toElevationString(Locale.UK) // "1,345 m"

// 2. Override both distance and elevation to Metric or Imperial
val metric = getUnitsConverter(UnitSystem.METRIC)
val imperial = getUnitsConverter(UnitSystem.IMPERIAL)

val shortWalk = 450.meters.toDistanceString(metric) // "450 m"
val quarterMile = 0.25.miles.toDistanceString(imperial) // "0.3 mi"
val mediumDrive = 2.0.kilometers.toDistanceString(metric) // "2.0 km"
val longTrip = 12.4.kilometers.toDistanceString(metric) // "12 km"
val summit = summitElevation.toElevationString(imperial) // "14,440 ft"

// 3. Override only distance or only elevation
val usMetricElevation = getUnitsConverter(Locale.US, elevationUnits = UnitSystem.METRIC)
val ukImperialElevation = getUnitsConverter(Locale.UK).withElevationUnits(UnitSystem.IMPERIAL)
val hybridBySystem = UnitsConverter(
distanceUnits = UnitSystem.IMPERIAL,
elevationUnits = UnitSystem.METRIC,
)

// 4. Mix & match converters or supply custom MeasurementFormatter lambdas
val customConverter = UnitsConverter(
distanceFormatter = { meters, symbols ->
"${symbols.formatFixedDecimal(meters.inKilometers, decimals = 2)} km"
},
elevationConverter = MetricUnitsConverter,
)
val customElevation = getUnitsConverter().withElevationFormatter { meters, symbols ->
"${symbols.formatInteger(meters.value)} MASL"
}
```

* **Java Example:**
```java
import static com.google.maps.android.Units.feet;
import static com.google.maps.android.Units.getUnitsConverter;
import static com.google.maps.android.Units.inFeet;
import static com.google.maps.android.Units.inKilometers;
import static com.google.maps.android.Units.kilometers;
import static com.google.maps.android.Units.meters;
import static com.google.maps.android.Units.miles;
import static com.google.maps.android.Units.toDistanceString;
import static com.google.maps.android.Units.toElevationString;

import com.google.maps.android.ImperialUnitsConverter;
import com.google.maps.android.MetricUnitsConverter;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.UnitSystem;
import com.google.maps.android.UnitsConverter;
import java.util.Locale;

// Express measurements in meters and convert between units
double twelveFeetInMeters = feet(12); // 3.6576 m
double elevationInFeet = inFeet(meters(4401.2)); // 14439.63 ft

// 1. Default formatting (Locale or Android Resources)
double distanceMeters = SphericalUtil.computeDistanceBetween(pointA, pointB);
String defaultFormatted = toDistanceString(distanceMeters);
String localizedFormatted = toDistanceString(distanceMeters, context.getResources());
String ukElevation = toElevationString(meters(1345), Locale.UK); // "1,345 m"

// 2. Override both distance and elevation
UnitsConverter imperial = getUnitsConverter(UnitSystem.IMPERIAL);
String quarterMile = imperial.toDistanceString(miles(0.25)); // "0.3 mi"
String mediumDrive = imperial.toDistanceString(kilometers(2.0)); // "1.2 mi"
String summit = imperial.toElevationString(meters(4401.2)); // "14,440 ft"

// 3. Override only distance or only elevation
UnitsConverter usMetricElevation = getUnitsConverter(Locale.US)
.withElevationUnits(UnitSystem.METRIC);
UnitsConverter hybridBySystem = UnitsConverter.create(
UnitSystem.IMPERIAL,
UnitSystem.METRIC
);

// 4. Mix & match converters or supply custom MeasurementFormatter lambdas
UnitsConverter ukConverter = UnitsConverter.create(
ImperialUnitsConverter.INSTANCE,
MetricUnitsConverter.INSTANCE
);
UnitsConverter customConverter = UnitsConverter.create(
(meters, symbols) -> symbols.formatFixedDecimal(inKilometers(meters), 2) + " km",
MetricUnitsConverter.INSTANCE::toElevationString
);
```

## 3. Best Practices
1. **Performance:** For massive datasets (e.g., parsing huge GeoJSON files), do not block the main thread. Parse on a background dispatcher and only call `layer.addLayerToMap()` on the UI thread.
2. **Custom Renderers:** If the user wants custom cluster icons, extend `DefaultClusterRenderer` and override `onBeforeClusterItemRendered` and `onBeforeClusterRendered`.
123 changes: 123 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ upgrading from 4.x, see the [migration guide](MIGRATION.md).
interoperability with Maps API web services
- **Spherical geometry** — for example: computeDistance, computeHeading,
computeArea
- **Units & logical distance/elevation formatting** — type-safe `Meters` value class, framework-agnostic `UnitSystem` / `UnitsConverter` selection (`MetricUnitsConverter` and `ImperialUnitsConverter`) defaulting to the device's locale, and order-of-magnitude distance formatting
- **Street View metadata** — checks if a Street View panorama exists at a given location
- **Reactive Kotlin Extensions & Builders** — coroutine suspensions (`awaitMap()`), reactive `Flow` observers (`mapClickEvents()`), and option builder DSLs (`addMarker { ... }`) consolidated directly into `com.google.maps.android.*`.

Expand Down Expand Up @@ -164,6 +165,128 @@ Full guides for using the utilities are published in

- Polyline encoding and decoding [source](https://github.com/googlemaps/android-maps-utils/blob/main/library/src/main/java/com/google/maps/android/PolyUtil.kt), [encoding sample](https://github.com/googlemaps/android-maps-utils/blob/main/demo/src/main/java/com/google/maps/android/utils/demo/PolySimplifyDemoActivity.java), [decoding sample](https://github.com/googlemaps/android-maps-utils/blob/main/demo/src/main/java/com/google/maps/android/utils/demo/PolyDecodeDemoActivity.java)
- Spherical geometry [source](https://github.com/googlemaps/android-maps-utils/blob/main/library/src/main/java/com/google/maps/android/SphericalUtil.kt), [compute distance sample](https://github.com/googlemaps/android-maps-utils/blob/main/demo/src/main/java/com/google/maps/android/utils/demo/DistanceDemoActivity.java)
- Units & distance/elevation formatting [source](https://github.com/googlemaps/android-maps-utils/blob/main/library/src/main/java/com/google/maps/android/Units.kt), [sample code](https://github.com/googlemaps/android-maps-utils/blob/main/demo/src/main/java/com/google/maps/android/utils/demo/UnitsDemoActivity.kt)

</details>

<details>
<summary>Units & Logical Distance/Elevation Formatting</summary>

### Units & Logical Distance/Elevation Formatting

The `Units` API (`com.google.maps.android`) provides a type-safe `Meters` value class, locale- and `Resources`-aware `UnitSystem` resolution, and logical significant-digit formatting for distances and elevations:

- **Locale Defaults**:
- `US`, `LR`, and `MM` default to **Imperial** (`ft`, `mi`) for both road distances and elevations.
- `GB` (`Locale.UK`) defaults out-of-the-box to **Imperial road distances** (`ft`, `mi`) and **Metric elevations** (`m`).
- All other locales default to **Metric** (`m`, `km`) for both road distances and elevations.
- **Logical Precision Rules**:
- **Small distances** (`< 1 km` metric or `< 0.25 mi` imperial) and **all elevations** format as whole integers (`450 m`, `528 ft`, `14,440 ft`).
- **Medium distances** (`1.0` to `< 10` km, or `0.25` to `< 10` mi) format with 1 decimal digit, keeping trailing `.0` (`0.3 mi`, `1.0 km`, `2.5 mi`).
- **Large distances** (`>= 10` km or `>= 10` mi) round to whole numbers (`10 km`, `12 mi`).

```kotlin
import com.google.maps.android.ImperialUnitsConverter
import com.google.maps.android.MetricUnitsConverter
import com.google.maps.android.UnitSystem
import com.google.maps.android.UnitsConverter
import com.google.maps.android.getUnitsConverter
import com.google.maps.android.inFeet
import com.google.maps.android.inKilometers
import com.google.maps.android.kilometers
import com.google.maps.android.meters
import com.google.maps.android.miles
import com.google.maps.android.toDistanceString
import com.google.maps.android.toElevationString
import java.util.Locale

// 1. Default formatting using device Locale, explicit Locale, or Android Resources (for localized unit templates)
val distanceText = 450.meters.toDistanceString() // Uses Locale.getDefault()
val localizedText = 450.meters.toDistanceString(context.resources) // Uses localized R.string.amu_in_* templates
val ukDistance = 1.miles.toDistanceString(Locale.UK) // "1.0 mi"
val ukElevation = 1345.meters.toElevationString(Locale.UK) // "1,345 m" (GB defaults to miles/ft + metres)

// 2. Override both distance and elevation to Metric or Imperial
val imperial = getUnitsConverter(UnitSystem.IMPERIAL)
val metric = getUnitsConverter(UnitSystem.METRIC)

val summitFeet = 4401.2.meters.inFeet // 14439.6325...
val shortWalk = 450.meters.toDistanceString(metric) // "450 m"
val quarterMile = 0.25.miles.toDistanceString(imperial) // "0.3 mi"
val mediumDrive = 2.0.kilometers.toDistanceString(metric) // "2.0 km"
val longTrip = 12.4.kilometers.toDistanceString(metric) // "12 km"
val summit = 4401.2.meters.toElevationString(imperial) // "14,440 ft"

// 3. Override only distance or only elevation (via factory parameters, fluent copy methods, or UnitSystem pair)
val usMetricElevation = getUnitsConverter(Locale.US, elevationUnits = UnitSystem.METRIC)
val ukImperialElevation = getUnitsConverter(Locale.UK).withElevationUnits(UnitSystem.IMPERIAL)
val hybridBySystem = UnitsConverter(
distanceUnits = UnitSystem.IMPERIAL,
elevationUnits = UnitSystem.METRIC,
)

// 4. Mix & match converters or supply custom MeasurementFormatter lambdas
val customConverter = UnitsConverter(
distanceFormatter = { meters, symbols ->
"${symbols.formatFixedDecimal(meters.inKilometers, decimals = 2)} km"
},
elevationConverter = MetricUnitsConverter,
)
val customElevation = getUnitsConverter().withElevationFormatter { meters, symbols ->
"${symbols.formatInteger(meters.value)} MASL"
}
```

In Java, `Units` exposes static conversion and formatting methods (usable with `import static com.google.maps.android.Units.*;`):

```java
import static com.google.maps.android.Units.feet;
import static com.google.maps.android.Units.getUnitsConverter;
import static com.google.maps.android.Units.inFeet;
import static com.google.maps.android.Units.inKilometers;
import static com.google.maps.android.Units.kilometers;
import static com.google.maps.android.Units.meters;
import static com.google.maps.android.Units.miles;
import static com.google.maps.android.Units.toDistanceString;
import static com.google.maps.android.Units.toElevationString;

import com.google.maps.android.ImperialUnitsConverter;
import com.google.maps.android.MetricUnitsConverter;
import com.google.maps.android.UnitSystem;
import com.google.maps.android.UnitsConverter;
import java.util.Locale;

double twelveFeetInMeters = feet(12); // 3.6576 m
double elevationInFeet = inFeet(meters(4401.2)); // 14439.63 ft

// 1. Default formatting (Locale or Android Resources)
String defaultDistance = toDistanceString(miles(2.5)); // "2.5 mi" (in US/GB locale)
String localizedDistance = toDistanceString(miles(2.5), context.getResources());
String ukElevation = toElevationString(meters(1345), Locale.UK); // "1,345 m"

// 2. Override both distance and elevation
UnitsConverter imperial = getUnitsConverter(UnitSystem.IMPERIAL);
String distance = imperial.toDistanceString(miles(2.5)); // "2.5 mi"
String summit = imperial.toElevationString(meters(4401.2)); // "14,440 ft"

// 3. Override only distance or only elevation
UnitsConverter usMetricElevation = getUnitsConverter(Locale.US)
.withElevationUnits(UnitSystem.METRIC);
UnitsConverter hybridBySystem = UnitsConverter.create(
UnitSystem.IMPERIAL,
UnitSystem.METRIC
);

// 4. Mix & match converters or supply custom MeasurementFormatter lambdas
UnitsConverter ukConverter = UnitsConverter.create(
ImperialUnitsConverter.INSTANCE,
MetricUnitsConverter.INSTANCE
);
UnitsConverter customConverter = UnitsConverter.create(
(meters, symbols) -> symbols.formatFixedDecimal(inKilometers(meters), 2) + " km",
MetricUnitsConverter.INSTANCE::toElevationString
);
```

</details>

Expand Down
3 changes: 3 additions & 0 deletions demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@
<activity
android:name=".KtxExtensionsDemoActivity"
android:exported="true" />
<activity
android:name=".UnitsDemoActivity"
android:exported="true" />

</application>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.Units;
import com.google.maps.android.UnitsConverter;
import java.util.Arrays;

public class DistanceDemoActivity extends BaseDemoActivity
Expand All @@ -35,6 +37,7 @@ public class DistanceDemoActivity extends BaseDemoActivity
private Marker mMarkerA;
private Marker mMarkerB;
private Polyline mPolyline;
private UnitsConverter mUnitsConverter;

@Override
protected int getLayoutId() {
Expand All @@ -44,6 +47,7 @@ protected int getLayoutId() {
@Override
protected void startDemo(boolean isRestore) {
mTextView = findViewById(R.id.textView);
mUnitsConverter = Units.getUnitsConverter();

if (!isRestore) {
getMap().moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(-33.8256, 151.2395), 10));
Expand All @@ -62,30 +66,21 @@ protected void startDemo(boolean isRestore) {
showDistance();
}

// [START maps_android_utils_units_java]
@SuppressLint("SetTextI18n")
private void showDistance() {
double distance =
double distanceMeters =
SphericalUtil.computeDistanceBetween(mMarkerA.getPosition(), mMarkerB.getPosition());
mTextView.setText(getString(R.string.distance_format, formatNumber(distance)));
String formattedDistance = mUnitsConverter.toDistanceString(distanceMeters);
mTextView.setText(getString(R.string.distance_format, formattedDistance));
}

// [END maps_android_utils_units_java]

private void updatePolyline() {
mPolyline.setPoints(Arrays.asList(mMarkerA.getPosition(), mMarkerB.getPosition()));
}

private String formatNumber(double distance) {
String unit = getString(R.string.unit_m);
if (distance < 1) {
distance *= 1000;
unit = getString(R.string.unit_mm);
} else if (distance > 1000) {
distance /= 1000;
unit = getString(R.string.unit_km);
}

return String.format("%4.3f%s", distance, unit);
}

@Override
public void onMarkerDragEnd(@NonNull Marker marker) {
showDistance();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ internal fun demoGroups(): List<DemoGroup> = listOf(
Demo(R.string.demo_title_tile_provider, TileProviderAndProjectionDemo::class.java),
Demo(R.string.demo_title_animation_util, AnimationUtilDemoActivity::class.java),
Demo(R.string.demo_title_reactive_extensions, KtxExtensionsDemoActivity::class.java),
Demo(R.string.demo_title_units, UnitsDemoActivity::class.java),
),
),
DemoGroup(
Expand Down
Loading
Loading