Skip to content

Commit f6a532e

Browse files
committed
fix: improve HMS Maps location and coordinate handling
- add fused location updates to the HMS Maps backend - convert coordinates between WGS-84 and GCJ-02 - improve map initialization and loaded callback handling
1 parent 9a206ae commit f6a532e

4 files changed

Lines changed: 241 additions & 17 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* SPDX-FileCopyrightText: 2026 microG Project Team
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package org.microg.gms.utils;
7+
8+
/**
9+
* Conversion between the internationally accepted geographic coordinate system (WGS-84)
10+
* and the geographic coordinate system used in China (GCJ-02)
11+
*/
12+
public class CoordinateConverter {
13+
private static final double A = 6378245.0;
14+
private static final double EE = 0.006693421622965943;
15+
private static final double PI = Math.PI;
16+
private static final double EPSILON = 1e-6;
17+
private static final int MAX_ITERATIONS = 10;
18+
private static final double PI_OVER_180 = PI / 180.0;
19+
private static final double A_1_EE = A * (1 - EE);
20+
21+
// ---------- WGS-84 to GCJ-02 ----------
22+
23+
/**
24+
* Convert WGS-84 coordinates to GCJ-02 coordinates
25+
*
26+
* @param wgsLat latitude
27+
* @param wgsLon longitude
28+
* @return GCJ-02 coordinate array, index 0 is latitude, 1 is longitude
29+
*/
30+
public static double[] wgs84ToGcj02(double wgsLat, double wgsLon) {
31+
if (isOutOfChina(wgsLat, wgsLon)) {
32+
return new double[]{wgsLat, wgsLon};
33+
}
34+
35+
double dLat = transformLat(wgsLon - 105.0, wgsLat - 35.0);
36+
double dLon = transformLon(wgsLon - 105.0, wgsLat - 35.0);
37+
double radLat = wgsLat * PI_OVER_180;
38+
double magic = Math.sin(radLat);
39+
magic = 1 - EE * magic * magic;
40+
double sqrtMagic = Math.sqrt(magic);
41+
42+
dLat = (dLat * 180.0) / (A_1_EE / (magic * sqrtMagic) * PI);
43+
dLon = (dLon * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI);
44+
45+
return new double[]{wgsLat + dLat, wgsLon + dLon};
46+
}
47+
48+
// ---------- GCJ-02 to WGS-84 ----------
49+
50+
/**
51+
* Convert GCJ-02 coordinates to WGS-84 coordinates (iterative approximation method)
52+
*
53+
* @param gcjLat latitude
54+
* @param gcjLon longitude
55+
* @return WGS-84 coordinate array, index 0 is latitude, 1 is longitude
56+
*/
57+
public static double[] gcj02ToWgs84(double gcjLat, double gcjLon) {
58+
if (isOutOfChina(gcjLat, gcjLon)) {
59+
return new double[]{gcjLat, gcjLon};
60+
}
61+
62+
double[] result = {gcjLat, gcjLon};
63+
double[] delta = new double[2];
64+
int iteration = 0;
65+
66+
while (iteration++ < MAX_ITERATIONS) {
67+
double[] gcjGuess = wgs84ToGcj02(result[0], result[1]);
68+
delta[0] = gcjLat - gcjGuess[0];
69+
delta[1] = gcjLon - gcjGuess[1];
70+
71+
double h = 1e-4;
72+
double[] gradLat = gradient(result[0], result[1], h, 0);
73+
double[] gradLon = gradient(result[0], result[1], h, 1);
74+
75+
double det = gradLat[0] * gradLon[1] - gradLat[1] * gradLon[0];
76+
if (Math.abs(det) < 1e-12) break;
77+
78+
double stepLat = (delta[0] * gradLon[1] - delta[1] * gradLat[1]) / det;
79+
double stepLon = (delta[1] * gradLat[0] - delta[0] * gradLon[0]) / det;
80+
81+
result[0] += stepLat;
82+
result[1] += stepLon;
83+
84+
if (Math.abs(stepLat) < EPSILON && Math.abs(stepLon) < EPSILON) {
85+
break;
86+
}
87+
}
88+
return result;
89+
}
90+
91+
// numerical differentiation to compute gradients
92+
private static double[] gradient(double lat, double lon, double h, int axis) {
93+
double[] base = wgs84ToGcj02(lat, lon);
94+
double[] delta;
95+
if (axis == 0) {
96+
delta = wgs84ToGcj02(lat + h, lon);
97+
} else {
98+
delta = wgs84ToGcj02(lat, lon + h);
99+
}
100+
return new double[]{(delta[0] - base[0]) / h, (delta[1] - base[1]) / h};
101+
}
102+
103+
// determine whether the coordinates are outside China
104+
private static boolean isOutOfChina(double lat, double lon) {
105+
return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
106+
}
107+
108+
// latitude conversion formula
109+
private static double transformLat(double x, double y) {
110+
double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * Math.sqrt(Math.abs(x));
111+
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
112+
ret += (20.0 * Math.sin(y * PI) + 40.0 * Math.sin(y / 3.0 * PI)) * 2.0 / 3.0;
113+
ret += (160.0 * Math.sin(y / 12.0 * PI) + 320.0 * Math.sin(y * PI / 30.0)) * 2.0 / 3.0;
114+
return ret;
115+
}
116+
117+
// longitude conversion formula
118+
private static double transformLon(double x, double y) {
119+
double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * Math.sqrt(Math.abs(x));
120+
ret += (20.0 * Math.sin(6.0 * x * PI) + 20.0 * Math.sin(2.0 * x * PI)) * 2.0 / 3.0;
121+
ret += (20.0 * Math.sin(x * PI) + 40.0 * Math.sin(x / 3.0 * PI)) * 2.0 / 3.0;
122+
ret += (150.0 * Math.sin(x / 12.0 * PI) + 300.0 * Math.sin(x / 30.0 * PI)) * 2.0 / 3.0;
123+
return ret;
124+
}
125+
}

play-services-maps/core/hms/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ apply plugin: 'kotlin-android'
99
dependencies {
1010
implementation project(':play-services-base-core')
1111
implementation project(':play-services-maps')
12+
implementation project(':play-services-location')
1213

1314
implementation 'com.huawei.hms:maps:6.9.0.300'
1415
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlinVersion"

play-services-maps/core/hms/src/main/kotlin/org/microg/gms/maps/hms/GoogleMap.kt

Lines changed: 109 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55

66
package org.microg.gms.maps.hms
77

8+
import android.Manifest
89
import android.content.Context
10+
import android.content.pm.PackageManager
911
import android.graphics.Bitmap
1012
import android.location.Location
1113
import android.os.*
@@ -21,16 +23,20 @@ import android.widget.RelativeLayout
2123
import androidx.annotation.IdRes
2224
import androidx.annotation.Keep
2325
import androidx.collection.LongSparseArray
26+
import androidx.core.app.ActivityCompat
2427
import com.google.android.gms.dynamic.IObjectWrapper
2528
import com.google.android.gms.dynamic.ObjectWrapper
2629
import com.google.android.gms.dynamic.unwrap
30+
import com.google.android.gms.location.LocationListener
31+
import com.google.android.gms.location.LocationServices
2732
import com.google.android.gms.maps.GoogleMap.MAP_TYPE_TERRAIN
2833
import com.google.android.gms.maps.GoogleMapOptions
2934
import com.google.android.gms.maps.internal.*
3035
import com.google.android.gms.maps.model.*
3136
import com.google.android.gms.maps.model.internal.*
3237
import com.huawei.hms.maps.CameraUpdate
3338
import com.huawei.hms.maps.HuaweiMap
39+
import com.huawei.hms.maps.LocationSource
3440
import com.huawei.hms.maps.MapView
3541
import com.huawei.hms.maps.MapsInitializer
3642
import com.huawei.hms.maps.OnMapReadyCallback
@@ -42,8 +48,11 @@ import com.huawei.hms.maps.internal.IOnPoiClickListener
4248
import com.huawei.hms.maps.model.Marker
4349
import org.microg.gms.maps.hms.model.*
4450
import org.microg.gms.maps.hms.utils.*
51+
import com.google.android.gms.location.LocationRequest
52+
import com.google.android.gms.location.Priority
4553
import java.util.concurrent.CopyOnWriteArrayList
4654
import java.util.concurrent.atomic.AtomicBoolean
55+
import com.google.android.gms.maps.model.LatLng
4756

4857

4958
private fun <T : Any> LongSparseArray<T>.values() = (0 until size()).mapNotNull { valueAt(it) }
@@ -102,6 +111,36 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
102111
private var projectionImpl: ProjectionImpl? = null
103112
private var inDeveloperAnimation = false
104113

114+
private var locationEnabled: Boolean = false
115+
private var isAddLocationCallback: Boolean = false
116+
private var lastLocation: Location? = null
117+
private var myLocationChangeListener: IOnMyLocationChangeListener? = null
118+
119+
private val locationService by lazy { LocationServices.getFusedLocationProviderClient(context) }
120+
private val locationCallback = LocationListener { location ->
121+
lastLocation = location
122+
try {
123+
myLocationChangeListener?.onMyLocationChanged(ObjectWrapper.wrap(location))
124+
} catch (e: RemoteException) {
125+
Log.w(TAG, "Failed to notify my-location listener", e)
126+
}
127+
val gcj02Location = Location(location).apply {
128+
val hmsLatLng = LatLng(location.latitude, location.longitude).toHms()
129+
latitude = hmsLatLng.latitude
130+
longitude = hmsLatLng.longitude
131+
}
132+
mLocationChangedListener?.onLocationChanged(gcj02Location)
133+
}
134+
private var mLocationChangedListener: LocationSource.OnLocationChangedListener? = null
135+
private var hwLocationSource: LocationSource = object : LocationSource {
136+
override fun activate(listener: LocationSource.OnLocationChangedListener) {
137+
mLocationChangedListener = listener
138+
}
139+
override fun deactivate() {
140+
mLocationChangedListener = null
141+
}
142+
}
143+
105144
init {
106145
BitmapDescriptorFactoryImpl.initialize(context.resources)
107146
runOnMainLooper {
@@ -340,7 +379,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
340379

341380
override fun addMarker(options: MarkerOptions): IMarkerDelegate {
342381
val marker = MarkerImpl(this, "m${markerId++}", options)
343-
if (map != null) {
382+
if (map != null && initialized) {
344383
marker.update()
345384
} else {
346385
markers[marker.id] = marker
@@ -425,16 +464,53 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
425464

426465
override fun setMyLocationEnabled(myLocation: Boolean) = afterInitialize {
427466
Log.d(TAG, "setMyLocationEnabled $myLocation")
428-
it.isMyLocationEnabled = myLocation
467+
synchronized(mapLock) {
468+
locationEnabled = myLocation
469+
try {
470+
setLocationSource(null)
471+
} catch (e: Exception) {
472+
Log.w(TAG, e)
473+
locationEnabled = false
474+
} finally {
475+
it.isMyLocationEnabled = locationEnabled
476+
}
477+
}
429478
}
430479

431-
override fun getMyLocation(): Location? {
432-
Log.d(TAG, "deprecated Method: getMyLocation")
433-
return null
434-
}
480+
override fun getMyLocation(): Location? = lastLocation
435481

436482
override fun setLocationSource(locationSource: ILocationSourceDelegate?) = afterInitialize {
437-
Log.d(TAG, "unimplemented Method: setLocationSource")
483+
synchronized(mapLock) {
484+
it.setLocationSource(hwLocationSource)
485+
updateLocationEngineListener(locationEnabled)
486+
}
487+
}
488+
489+
private fun updateLocationEngineListener(myLocation: Boolean) {
490+
if (ActivityCompat.checkSelfPermission(
491+
context, Manifest.permission.ACCESS_FINE_LOCATION
492+
) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(
493+
context, Manifest.permission.ACCESS_COARSE_LOCATION
494+
) == PackageManager.PERMISSION_GRANTED
495+
) {
496+
if (myLocation) {
497+
if (!isAddLocationCallback) {
498+
isAddLocationCallback = true
499+
locationService.requestLocationUpdates(
500+
LocationRequest.Builder(DEFAULT_LOCATION_INTERVAL_MILLIS)
501+
.setPriority(Priority.PRIORITY_HIGH_ACCURACY)
502+
.setMinUpdateIntervalMillis(DEFAULT_LOCATION_INTERVAL_MILLIS)
503+
.setMaxUpdateDelayMillis(DEFAULT_LOCATION_INTERVAL_MILLIS)
504+
.build(), locationCallback, Looper.getMainLooper()
505+
)
506+
}
507+
} else {
508+
if (isAddLocationCallback) {
509+
isAddLocationCallback = false
510+
locationService.removeLocationUpdates(locationCallback)
511+
}
512+
}
513+
}
438514
}
439515

440516
override fun setContentDescription(desc: String?) = afterInitialize {
@@ -507,7 +583,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
507583

508584
override fun setOnMarkerClickListener(listener: IOnMarkerClickListener?) = afterInitialize { hmap ->
509585
hmap.setOnMarkerClickListener {
510-
Log.d("GmsGoogleMap", "setOnMarkerClickListener marker id -> ${it.id}")
586+
Log.d(TAG, "setOnMarkerClickListener marker id -> ${it.id}")
511587
listener?.onMarkerClick(markers[it.id]) ?: false
512588
}
513589
}
@@ -557,6 +633,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
557633

558634
override fun setOnMyLocationChangeListener(listener: IOnMyLocationChangeListener?) = afterInitialize {
559635
Log.d(TAG, "deprecated Method: setOnMyLocationChangeListener")
636+
myLocationChangeListener = listener
560637
}
561638

562639
override fun setOnMyLocationButtonClickListener(listener: IOnMyLocationButtonClickListener?) = afterInitialize {
@@ -630,11 +707,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
630707
synchronized(mapLock) {
631708
if (loaded) {
632709
Log.d(TAG, "Invoking callback instantly, as map is loaded")
633-
try {
634-
scheduleExecute { callback.onMapLoaded() }
635-
} catch (e: Exception) {
636-
Log.w(TAG, e)
637-
}
710+
callback.scheduleExecute()
638711
} else {
639712
Log.d(TAG, "Delay callback invocation, as map is not yet loaded")
640713
loadedCallback = callback
@@ -847,8 +920,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
847920
}
848921
internalOnInitializedCallbackList.clear()
849922
fakeWatermark { Log.d(TAG_LOGO, "fakeWatermark success") }
850-
scheduleExecute { loadedCallback?.onMapLoaded() }
851-
923+
loadedCallback?.scheduleExecute()
852924
mapView?.visibility = View.VISIBLE
853925
}
854926

@@ -862,6 +934,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
862934
override fun onPause() = mapView?.onPause() ?: Unit
863935
override fun onDestroy() {
864936
Log.d(TAG, "onDestroy")
937+
locationService.removeLocationUpdates(locationCallback)
865938
initializedCallbackList.clear()
866939
internalOnInitializedCallbackList.clear()
867940
circles.map { it.value.remove() }
@@ -877,13 +950,15 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
877950
// TODO can crash?
878951
mapView?.onDestroy()
879952
mapView = null
953+
mLocationChangedListener = null
880954

881955
// Don't make it null; this object is not deleted immediately, and it may want to access map.* stuff
882956
//map = null
883957

884958
created = false
885959
initialized = false
886960
loaded = false
961+
isAddLocationCallback = false
887962
}
888963

889964
override fun onStart() {
@@ -929,6 +1004,16 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
9291004
}
9301005
}
9311006

1007+
private fun IOnMapLoadedCallback.scheduleExecute() {
1008+
Handler(Looper.getMainLooper()).postDelayed({
1009+
try {
1010+
this.onMapLoaded()
1011+
} catch (e: Exception) {
1012+
Log.w(TAG, e)
1013+
}
1014+
}, ON_MAP_LOADED_CALLBACK_DELAY)
1015+
}
1016+
9321017
private var isInvokingInitializedCallbacks = AtomicBoolean(false)
9331018
private fun tryRunUserInitializedCallbacks(tag: String = "") {
9341019

@@ -958,6 +1043,13 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
9581043
scheduleExecute { runCallbacks() }
9591044
}
9601045
if (!wasCallbackActive) isInvokingInitializedCallbacks.set(false)
1046+
} else if (mapView?.isShown == false) {
1047+
// Match the Mapbox backend: let applications configure the map while the
1048+
// backend is still initializing. Camera updates and other operations that
1049+
// require HuaweiMap are queued by their existing initialization guards.
1050+
runOnMainLooper(forceQueue = true) {
1051+
runCallbacks()
1052+
}
9611053
} else {
9621054
Log.d(
9631055
"$TAG:$tag",
@@ -987,5 +1079,7 @@ class GoogleMapImpl(private val context: Context, var options: GoogleMapOptions)
9871079

9881080
private const val TAG_LOGO = "fakeWatermark"
9891081
private const val ON_MAP_CALLBACK_DELAY = 300L
1082+
private const val ON_MAP_LOADED_CALLBACK_DELAY = 500L
1083+
private const val DEFAULT_LOCATION_INTERVAL_MILLIS = 1000L
9901084
}
9911085
}

0 commit comments

Comments
 (0)