Skip to content
Merged
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
2 changes: 1 addition & 1 deletion android-sdk-core
Submodule android-sdk-core updated 37 files
+114 −27 api/android-sdk-core.api
+200 −0 src/androidTest/java/com/mapconductor/core/OverlayOnAddedDeviceTest.kt
+281 −0 src/androidTest/java/com/mapconductor/core/polyline/PolylineTileRendererDeviceTest.kt
+381 −0 src/androidTest/java/com/mapconductor/core/polyline/PolylineTilerDeviceTest.kt
+93 −0 src/main/java/com/mapconductor/core/OverlayAddedTracker.kt
+86 −0 src/main/java/com/mapconductor/core/circle/Circle.kt
+8 −1 src/main/java/com/mapconductor/core/circle/CircleOverlay.kt
+13 −2 src/main/java/com/mapconductor/core/controller/BaseMapViewController.kt
+93 −0 src/main/java/com/mapconductor/core/groundimage/GroundImage.kt
+8 −1 src/main/java/com/mapconductor/core/groundimage/GroundImageOverlay.kt
+89 −0 src/main/java/com/mapconductor/core/marker/Marker.kt
+8 −3 src/main/java/com/mapconductor/core/marker/MarkerOverlay.kt
+80 −0 src/main/java/com/mapconductor/core/polygon/Polygon.kt
+8 −1 src/main/java/com/mapconductor/core/polygon/PolygonOverlay.kt
+10 −47 src/main/java/com/mapconductor/core/polygon/PolygonTileGeometry.kt
+75 −322 src/main/java/com/mapconductor/core/polygon/PolygonTileRenderer.kt
+29 −108 src/main/java/com/mapconductor/core/polygon/PolygonTileSet.kt
+19 −54 src/main/java/com/mapconductor/core/polygon/PolygonTiler.kt
+72 −0 src/main/java/com/mapconductor/core/polyline/Polyline.kt
+81 −3 src/main/java/com/mapconductor/core/polyline/PolylineController.kt
+68 −2 src/main/java/com/mapconductor/core/polyline/PolylineManager.kt
+29 −9 src/main/java/com/mapconductor/core/polyline/PolylineOverlay.kt
+192 −0 src/main/java/com/mapconductor/core/polyline/PolylineTileGeometry.kt
+103 −0 src/main/java/com/mapconductor/core/polyline/PolylineTileSet.kt
+90 −0 src/main/java/com/mapconductor/core/polyline/PolylineTiler.kt
+32 −0 src/main/java/com/mapconductor/core/polyline/PolylineTilingOptions.kt
+68 −0 src/main/java/com/mapconductor/core/raster/RasterLayer.kt
+8 −1 src/main/java/com/mapconductor/core/raster/RasterLayerOverlay.kt
+90 −0 src/main/java/com/mapconductor/core/shapetile/ShapeGridIndex.kt
+417 −0 src/main/java/com/mapconductor/core/shapetile/ShapeTileRenderer.kt
+166 −0 src/main/java/com/mapconductor/core/shapetile/ShapeTileSet.kt
+82 −0 src/main/java/com/mapconductor/core/shapetile/ShapeTiler.kt
+413 −0 src/test/java/com/mapconductor/core/OverlayOnAddedTest.kt
+53 −0 src/test/java/com/mapconductor/core/controller/OverlayStackingTest.kt
+169 −0 src/test/java/com/mapconductor/core/polyline/PolylineHitToleranceTest.kt
+144 −0 src/test/java/com/mapconductor/core/polyline/PolylineTilingTest.kt
+14 −7 src/test/java/com/mapconductor/core/raster/RasterLayerCoexistenceTest.kt
1 change: 0 additions & 1 deletion example-app/src/main/assets/boston/boston-buildings.json

This file was deleted.

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ 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.polyline.boston.BostonRoadsPage
import com.mapconductor.example.pages.rasterlayer.RasterHeaderProbePage
import com.mapconductor.example.pages.rasterlayer.RasterLayerMapPage
import com.mapconductor.example.pages.startup.StartUpPage
Expand Down Expand Up @@ -143,6 +144,7 @@ fun DemoAppScreen(initPage: String = "map") {
listOf(
SidebarItem(id = "polyline", title = "Polyline"),
SidebarItem(id = "polyline-click", title = "Polyline Click"),
SidebarItem(id = "boston-roads", title = "Boston Roads"),
),
),
SidebarSection(
Expand Down Expand Up @@ -340,6 +342,11 @@ fun DemoAppScreen(initPage: String = "map") {
onToggleSidebar = navigationViewModel::toggleSidebar,
)
}
"boston-roads" -> {
BostonRoadsPage(
onToggleSidebar = navigationViewModel::toggleSidebar,
)
}
"groundImage" -> {
GroundImageMapPage(
groundImageResources = groundImageResources,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ fun BostonBuildingsPage(
val tap = viewModel.tap.collectAsState()
val progress = viewModel.progress.collectAsState()

// Two frames after the list reaches Polygons(): the polygons are registered with the map by then.
// Two frames after the list reaches Polygons(), for the stats. The loading overlay waits for onAdded.
LaunchedEffect(states.value) {
val shown = states.value
withFrameNanos {}
Expand Down Expand Up @@ -147,18 +147,19 @@ fun BostonBuildingsPage(
if (dataset.value == Dataset.CITY_PARCELS) LandUseLegend(modifier = Modifier.padding(vertical = 4.dp))
stats.value?.let {
val frame = it.frameMillis?.let { ms -> ", next frame $ms ms" } ?: ""
Text("Load ${it.loadMillis} ms, states ${it.buildMillis} ms$frame")
val added = it.addedMillis?.let { ms -> ", on the map $ms ms" } ?: ""
Text("Load ${it.loadMillis} ms, states ${it.buildMillis} ms$frame$added")
}
tap.value?.let { tap ->
val building = tap.building
val use = building?.use
Text(
when {
building == null -> "No building here"
use != null -> "${building.name} · ${use.label}"
use != null -> "Parcel · ${use.label} · ${formatArea(areaSquareMetres(building))}"
else -> {
val height = building.height?.let { "$it m" } ?: "height unknown"
"${building.name ?: "Building"} · $height · ${building.id.take(8)}"
"${building.name ?: "Building"} · $height · ${formatArea(areaSquareMetres(building))}"
}
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class Building(
val rings: List<DoubleArray>,
/** A parcel's land use, from the City of Boston's Property Assessment. */
val use: LandUse? = null,
/** A road's class (the roads file): the index in [com.mapconductor.example.pages.polyline.boston.RoadClass]. */
val roadClass: Int? = null,
)

/** What the last tap hit: a building, or nothing (`building` null), and where it landed. */
Expand All @@ -49,6 +51,8 @@ data class BuildingStats(
val buildMillis: Long,
/** From handing the list to Polygons() until two frames later; null until then. */
val frameMillis: Long? = null,
/** From handing the list to Polygons() until the map had all of it (every onAdded); null until then. */
val addedMillis: Long? = null,
)

/**
Expand All @@ -61,7 +65,9 @@ data class BuildingStats(
* the tap; tapping elsewhere closes it and says nothing was hit. Every tap is
* also logged under `BostonTap`, which is how the hit testing is checked from
* outside. Parcels are coloured by land use. A loading overlay shows how far
* reading, decoding, building the states and drawing have got.
* reading, decoding, building the states and drawing have got; it stays up
* until every polygon shown has been added to the map ([PolygonState.onAdded]),
* not just until the list has been handed over.
*
* `camera` ("lat,lng,zoom"), `data` (downtown, buildings or parcels) and `area`
* (0, 1 or 2) come from the intent extras, so a measurement opens the same view
Expand Down Expand Up @@ -111,6 +117,36 @@ class BostonBuildingsViewModel(

private var shownAt = 0L

/**
* Which load the polygons' onAdded belongs to. A callback from an earlier
* dataset (its polygons may still be arriving when the next one is picked)
* is ignored.
*/
private var generation = 0

/**
* Which ids of this load have reached the map, from their states' onAdded.
* The loading overlay stays up until every id of the list now handed to the
* map is here.
*
* An id stays recorded when its building leaves the list (the area shrank).
* The SDK calls onAdded again only for a polygon it saw leave, and a list
* replaced before the SDK got to it is never seen: forgetting the id there
* left the overlay up for good when the area was switched mid-load. So the
* count is redone against each new list ([show]), and an id already on the
* map counts for it.
*/
private var addedIds = HashSet<String>()

/** The distinct ids of the list now handed to the map. */
private var listIds: Set<String> = emptySet()

/** How many of [listIds] are in [addedIds]. */
private var onMapCount = 0

/** True from handing this load's first list to the map until all of it is there. */
private var waitingForAdds = false

private var buildings: List<Building> = emptyList()
private var states: List<PolygonState> = emptyList()
private var loadMillis = 0L
Expand All @@ -129,6 +165,15 @@ class BostonBuildingsViewModel(

private fun load() {
val dataset = _dataset.value
val gen = ++generation
addedIds = HashSet()
listIds = emptySet()
onMapCount = 0
waitingForAdds = false
// Until this load's states are built there is nothing to show; an area
// switch meanwhile only records the area (see [setArea]).
buildings = emptyList()
states = emptyList()
_polygonStates.value = emptyList()
_stats.value = null
_loadError.value = null
Expand All @@ -151,9 +196,18 @@ class BostonBuildingsViewModel(
_attribution.value = loaded.attribution
_progress.value = LoadPhase.Build
val built = SystemClock.elapsedRealtime()
// One callback for the whole load; it knows which load it belongs to.
val onAdded: (PolygonState) -> Unit = { state -> onPolygonAdded(gen, state.id) }
val newStates =
withContext(Dispatchers.Default) {
loaded.buildings.map { toState(it, dataset, onAdded) }
}
// A newer load started meanwhile; its states are the ones to show.
if (gen != generation) return@launch
buildings = loaded.buildings
states = withContext(Dispatchers.Default) { loaded.buildings.map { toState(it, dataset) } }
states = newStates
buildMillis = SystemClock.elapsedRealtime() - built
waitingForAdds = true
show()
}
}
Expand All @@ -172,7 +226,15 @@ class BostonBuildingsViewModel(
onTap(null, null, point)
}

/**
* Hands the area's polygons to the map. While this load waits for its
* polygons ([waitingForAdds]) the loading overlay stays up until every id of
* this list has reported onAdded, whichever list it first arrived for.
*/
private fun show() {
// An area switch while the dataset is still being read or built: the
// load shows the area picked by then.
if (states.isEmpty()) return
val level = _area.value
val shown = states.filterIndexed { i, _ -> buildings[i].level <= level }
val vertices = shown.sumOf { state -> state.points.size + state.holes.sumOf { it.size } }
Expand All @@ -181,17 +243,52 @@ class BostonBuildingsViewModel(
TAP_LOG_TAG,
"shown dataset=${_dataset.value} polygons=${shown.size} vertices=$vertices load=$loadMillis build=$buildMillis",
)
if (_progress.value != null) _progress.value = LoadPhase.Draw
shownAt = SystemClock.elapsedRealtime()
// Counted against the new list from here on.
listIds = shown.mapTo(HashSet()) { it.id }
onMapCount = listIds.count { it in addedIds }
if (waitingForAdds && _progress.value != null) _progress.value = LoadPhase.Draw
_polygonStates.value = shown
finishIfAllAdded()
}

/** The page calls this two frames after the list reached Polygons(). */
// Runs on the main thread, once per polygon each time the map takes it in.
private fun onPolygonAdded(
gen: Int,
id: String,
) {
if (gen != generation || !addedIds.add(id)) return
// A building of a list since replaced: on the map, but not one waited for now.
if (id !in listIds) return
onMapCount++
finishIfAllAdded()
}

private fun finishIfAllAdded() {
if (!waitingForAdds || onMapCount < listIds.size) return
waitingForAdds = false
onAllAdded()
}

/** Every polygon shown is on the map: tiles handed over or native objects made. */
private fun onAllAdded() {
val shown = _polygonStates.value
_progress.value = null
if (shown.isEmpty()) return
val added = SystemClock.elapsedRealtime() - shownAt
_stats.value = _stats.value?.copy(addedMillis = added)
Log.i(TAP_LOG_TAG, "added dataset=${_dataset.value} polygons=${shown.size} added=$added")
}

/**
* The page calls this two frames after the list reached Polygons(). The
* loading overlay stays up: the map is still taking the polygons in
* (see [onAllAdded]).
*/
fun onDrawn(states: List<PolygonState>) {
if (states.isEmpty() || states !== _polygonStates.value) return
val frame = SystemClock.elapsedRealtime() - shownAt
_stats.value = _stats.value?.copy(frameMillis = frame)
_progress.value = null
Log.i(TAP_LOG_TAG, "drawn dataset=${_dataset.value} polygons=${states.size} frame=$frame")
}

Expand All @@ -215,6 +312,7 @@ class BostonBuildingsViewModel(
private fun toState(
building: Building,
dataset: Dataset,
onAdded: (PolygonState) -> Unit,
): PolygonState {
val rings = building.rings.map(::toPoints)
return PolygonState(
Expand All @@ -227,12 +325,13 @@ class BostonBuildingsViewModel(
if (dataset ==
Dataset.CITY_PARCELS
) {
LandUse.of(building.use?.key).fill
(building.use ?: LandUse.UNKNOWN).fill
} else {
fillForHeight(building.height)
},
extra = building.id,
onClick = { event -> onTap(event.state, building, event.clicked) },
onAdded = onAdded,
)
}
}
Expand Down
Loading