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
2 changes: 1 addition & 1 deletion android-sdk-core
Submodule android-sdk-core updated 23 files
+66 −11 api/android-sdk-core.api
+281 −0 src/androidTest/java/com/mapconductor/core/polyline/PolylineTileRendererDeviceTest.kt
+381 −0 src/androidTest/java/com/mapconductor/core/polyline/PolylineTilerDeviceTest.kt
+6 −0 src/main/java/com/mapconductor/core/controller/BaseMapViewController.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
+81 −3 src/main/java/com/mapconductor/core/polyline/PolylineController.kt
+68 −2 src/main/java/com/mapconductor/core/polyline/PolylineManager.kt
+27 −14 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
+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
+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
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 @@ -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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,22 @@ import android.content.Context
*
* The binary layout is the same as the React sample's (bostonData.ts), MCPG v2:
* 'MCPG' u32 version=2 u32 count u8 fields u8 digits (little-endian)
* fields: 1 = height, 2 = land use, 4 = area, 8 = name (what the file carries)
* fields: 1 = height, 2 = land use, 4 = area, 8 = name, 16 = road class (what the file carries),
* 32 = lines: the shapes are polylines, not polygons (the roads file, see BostonRoadsData)
* digits: coordinates are integers of 10^-digits degree (buildings 6, parcels 5)
* per polygon, only the fields present, in this order:
* height varint, 0 = unknown, else the height in 0.1 m plus 1
* land use u8, the index in [LandUse] (family … unknown)
* area u8, the smallest downtown area (0, 1 or 2) the building fits in
* name varint byte length (0 = none), UTF-8
* road class u8, the index in RoadClass (highway … path)
* varint ringCount, per ring: varint n, n x (svarint dLat, svarint dLng)
* varint is LEB128 (7 bits at a time, low first); svarint is a zigzag varint.
* Coordinates are integers of 10^-digits degree, each the difference from the
* previous vertex across the whole file. A ring's closing vertex (the same as its first)
* is not stored: n counts the others, and the reader closes the ring again.
* With the lines bit a "ring" is an open polyline: every vertex is stored and
* the reader does not close it.
*/
enum class Dataset(
val label: String,
Expand Down Expand Up @@ -128,8 +132,10 @@ private const val FIELD_HEIGHT = 1
private const val FIELD_USE = 2
private const val FIELD_AREA = 4
private const val FIELD_NAME = 8
private const val FIELD_CLASS = 16
private const val FIELD_LINES = 32

/** Decodes an MCPG v2 file (see the top of this file); polygon `i` gets the id "<idPrefix>-<i>". */
/** Decodes an MCPG v2 file (see the top of this file); shape `i` gets the id "<idPrefix>-<i>". */
internal fun decodeMcpg(
bytes: ByteArray,
idPrefix: String,
Expand All @@ -141,6 +147,7 @@ internal fun decodeMcpg(
val fields = bytes[12].toInt() and 0xFF
val scale = Math.pow(10.0, (bytes[13].toInt() and 0xFF).toDouble())
var offset = 14
val lines = (fields and FIELD_LINES) != 0

fun varint(): Int {
var value = 0
Expand All @@ -165,6 +172,7 @@ internal fun decodeMcpg(
var use: LandUse? = null
var level = 0
var name: String? = null
var roadClass: Int? = null
if ((fields and FIELD_HEIGHT) != 0) height = varint().takeIf { it > 0 }?.let { (it - 1) / 10.0 }
if ((fields and FIELD_USE) != 0) use = LandUse.entries[bytes[offset++].toInt() and 0xFF]
if ((fields and FIELD_AREA) != 0) level = bytes[offset++].toInt() and 0xFF
Expand All @@ -173,22 +181,25 @@ internal fun decodeMcpg(
if (length > 0) name = String(bytes, offset, length, Charsets.UTF_8)
offset += length
}
if ((fields and FIELD_CLASS) != 0) roadClass = bytes[offset++].toInt() and 0xFF
val rings =
List(varint()) {
val n = varint()
// The closing vertex is not stored; it is the first one again.
val ring = DoubleArray((n + 1) * 2)
// The closing vertex is not stored; it is the first one again. A line is open.
val ring = DoubleArray((if (lines) n else n + 1) * 2)
for (v in 0 until n) {
lat += svarint()
lng += svarint()
ring[v * 2] = lng / scale
ring[v * 2 + 1] = lat / scale
}
ring[n * 2] = ring[0]
ring[n * 2 + 1] = ring[1]
if (!lines) {
ring[n * 2] = ring[0]
ring[n * 2 + 1] = ring[1]
}
ring
}
Building(id = "$idPrefix-$i", level = level, height = height, name = name, rings = rings, use = use)
Building(id = "$idPrefix-$i", level = level, height = height, name = name, rings = rings, use = use, roadClass = roadClass)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ private const val MB = 1024.0 * 1024.0
fun LoadingOverlay(
phase: LoadPhase,
modifier: Modifier = Modifier,
/** What is loading, for the text: "polygons" or "roads". */
shapes: String = "polygons",
) {
var fraction: Float? = null
val text =
Expand All @@ -68,8 +70,8 @@ fun LoadingOverlay(
"Loading data… $loaded MB"
}
}
LoadPhase.Decode -> "Reading the polygons…"
LoadPhase.Build -> "Preparing the polygons…"
LoadPhase.Decode -> "Reading the $shapes…"
LoadPhase.Build -> "Preparing the $shapes…"
LoadPhase.Draw -> "Drawing the map…"
}
Box(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.mapconductor.example.pages.polyline.boston

import androidx.compose.ui.graphics.Color
import com.mapconductor.core.features.GeoPoint
import com.mapconductor.core.spherical.Spherical
import com.mapconductor.example.pages.polygon.boston.LoadPhase
import com.mapconductor.example.pages.polygon.boston.decodeMcpg
import android.content.Context

/**
* The roads the Boston roads page draws: every road of the city, from Overture
* Maps `transportation/segment` (road subtype), cut at the city boundary. 54,645
* polylines, 234,765 points, about 1 MB.
*
* Bundled as assets/boston/boston-city-roads.bin. The React sample's
* `examples/basic/tools/boston/` builds it (`pack.py roads`, see its README for
* the source, licence and why not Analyze Boston's street centerlines). The file
* carries no ids: this is a polyline demo, and a road's id on the map is its
* index in the file.
*
* The format is the polygons' MCPG v2 (see BostonData.kt) with two more header
* bits: 16, the road class (u8, after the name), and 32, lines: each record's
* one "ring" is an open polyline, every vertex stored, never closed by the
* reader. Coordinates are 1e-5 degree; points almost on a straight line (0.5 m)
* are left out, the ends kept.
*/
const val ROADS_FILE = "boston/boston-city-roads.bin"

const val ROADS_ATTRIBUTION =
"© OpenStreetMap contributors, TomTom — via Overture Maps Foundation (ODbL 1.0)"

/**
* A road's class (the file's index, grouped from Overture's segment classes) and
* how the page draws it: bigger roads wider, brighter and on top. The same table
* on the web and iOS.
*/
enum class RoadClass(
val label: String,
val color: Color,
/** dp. */
val width: Float,
val zIndex: Int,
) {
HIGHWAY("Highway", Color(0xFFE8590C), 5f, 6),
PRIMARY("Primary", Color(0xFFF08C00), 4f, 5),
SECONDARY("Secondary", Color(0xFFFAB005), 3.5f, 4),
TERTIARY("Tertiary", Color(0xFFFFD43B), 3f, 3),
LOCAL("Local", Color(0xFF868E96), 2f, 2),
SERVICE("Service", Color(0xFFADB5BD), 1.5f, 1),
PATH("Path", Color(0xFF74B816), 1f, 0),
}

class Road(
val id: String,
val roadClass: RoadClass,
val name: String?,
/** `[lng0, lat0, lng1, lat1, ...]`, open. */
val path: DoubleArray,
) {
val points: List<GeoPoint> get() = List(path.size / 2) { GeoPoint(path[it * 2 + 1], path[it * 2]) }
}

/** Decodes the roads file: MCPG v2 with the road class and lines bits; road `i` gets the id "road-<i>". */
fun decodeRoads(bytes: ByteArray): List<Road> =
decodeMcpg(bytes, "road").map { shape ->
Road(
id = shape.id,
roadClass = RoadClass.entries[shape.roadClass ?: RoadClass.LOCAL.ordinal],
name = shape.name,
path = shape.rings.single(),
)
}

fun loadBostonRoads(
context: Context,
onProgress: (LoadPhase) -> Unit = {},
): List<Road> {
val length = runCatching { context.assets.openFd(ROADS_FILE).use { it.length } }.getOrNull()
onProgress(LoadPhase.Read(0, length))
val bytes = context.assets.open(ROADS_FILE).use { it.readBytes() }
onProgress(LoadPhase.Read(bytes.size.toLong(), length))
onProgress(LoadPhase.Decode)
return decodeRoads(bytes)
}

/** The road's length in metres: the geodesic length of its points. */
fun lengthMetres(road: Road): Double = Spherical.computeLength(road.points)
Loading
Loading