So the real issue is that you want to adjust your query, to filter out the ones you can't see on the map, so that you don't have to load them or iterate over them.
Since this doesn't have to have a lot of precision, you could store your marker points with a geohash, as well as their lat/long.
Geohash is really easy to use in a query to limit records, since it's a sequential string representation of the coordinates.
For instance, if you have two marker records with a geohash of 2j3l45d6 and 2j3lyt76, and your radius around the point is 60km, then you would set up your query something like:
SELECT * FROM points WHERE geohash LIKE "2j3%"
Geohash is not all that precise, for example, between precision 3 and 2 the visible radius jumps from 78km to 630km, but it's enough to limit the number of points you are querying, for most reasonable map camera zoom levels.
UPDATE:
I came here looking for a solution to a similar problem, and after writing this, went away and implemented it. This is a first crack, but I thought I'd share the code for anyone interested in trying it out.
There is a little extra code here, such as being able to specify the return radius in meters, which is not strictly required for this, but that I use elsewhere.
The first step was to choose a GeoHash library. Since I'm using Kotlin, I selected Android-Kotlin-Geohash, but pretty much any library will work.
Next I set up a couple of extensions for the VisibleRegion class, which is what you get back from the GoogleMap object on Android:
/**
* Returns a geohash based on the point, with precision great enough to cover the radius.
*/
fun VisibleRegion.visibleRadiusGeohash(point: LatLng): GeoHash? {
return GeoHash(
lat = point.latitude,
lon = point.longitude,
charsCount = when (calculateVisibleRadius(UnitOfMeasure.KILOMETER)) {
in 0.0..0.019 -> 8
in 0.02..0.076 -> 7
in 0.077..0.61 -> 6
in 0.62..2.4 -> 5
in 2.5..20.0 -> 4
in 21.0..78.0 -> 3
in 79.0..630.0 -> 2
else -> 1 // 2,500km and greater
}
)
}
/**
* @param units one of UnitOfMeasure.KILOMETER or UnitOfMeasure.METER.
* Any other unit will throw an IllegalArgumentException.
*
* This code adapted from https://stackoverflow.com/a/49413106/300236
*/
@Throws(IllegalArgumentException::class)
fun VisibleRegion.calculateVisibleRadius(units: UnitOfMeasure): Double {
val diameter = FloatArray(1)
Location.distanceBetween(
(farLeft.latitude + nearLeft.latitude) / 2,
farLeft.longitude,
(farRight.latitude + nearRight.latitude) / 2,
farRight.longitude,
diameter
)
// visible radius in meters is the diameter / 2, and /1000 for Km:
return when (units) {
UnitOfMeasure.KILOMETER -> (diameter[0] / 2 / 1000).toDouble()
UnitOfMeasure.METER -> (diameter[0] / 2).toDouble()
else -> throw IllegalArgumentException("Valid units are KILOMETER and METER only")
}
}
Since I'm using MVVM, I set up a view model with a few observable properties. One is the filter I'm going to use, the other is my list of points to draw on the map. Note that null is a perfectly valid value for the filter, and will cause all markers to be loaded:
val markerFilter: MutableLiveData<String?> = MutableLiveData<String?>(null)
val markers: LiveData<List<Mark>> =
Transformations.switchMap(markerFilter) { geoHash ->
liveData(Dispatchers.IO) {
emitSource(markRepo.loadMarkerList(geohash = geoHash))
}
}
If you are not familiar with ViewModel, this code essentially tells the "markers" list to update, whenever "markerFilter" changes.
Next, I need to know when the camera moved and the map viewport changes, so in my map view fragment, I listen for map movement events.
override fun onCameraMove() {
mapView?.let { map ->
val visibleRegion: VisibleRegion = map.projection.visibleRegion
val centreOfVisibleRegion = visibleRegion.latLngBounds.center
val geohash = visibleRegion.visibleRadiusGeohash(centreOfVisibleRegion)
model. markerFilter.postValue(geohash.toString())
}
}
(Looking at this code, I can see a nice refactoring that will simplify this, but this will work until I go back and do it)
... and of course, I need to observe my list of markers, so I can add them to the map when the list changes:
model.markers.observe(viewLifecycleOwner, Observer { list ->
list?.let {addMarkersToMap(it)}
})
The final bit is to set up the Repository to run the query, so my MarkerRepository class looks like this:
suspend fun loadMarkerList(geohash: String?): LiveData<List<Mark>> {
return withContext(io) {
dao.loadMarkerList(geohash ?: "")
}
}
Note that I don't pass a null filter value, if it's null, I set it to an empty string, so the resulting LIKE criteria will function.
... and my MarkerDao looks like:
@Query("SELECT * FROM marker WHERE geohash LIKE :filter || '%' ORDER BY geohash")
fun loadMarkerList(filter: String): LiveData<List<Mark>>
Hopefully, this gives someone else enough of a solution, to try it themselves.