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
4 changes: 2 additions & 2 deletions docs/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ Run unit tests:

### Test Coverage

The project enforces a **minimum test coverage of 20%** for all code. Coverage is measured with the Android Gradle Plugin's built-in JaCoCo support.
The project enforces a **minimum test coverage of 75%** for all code. Coverage is measured with the Android Gradle Plugin's built-in JaCoCo support.

Generate coverage report:

Expand Down Expand Up @@ -216,7 +216,7 @@ The CI pipeline runs automatically on all pull requests and includes:
2. **Copyright Check** - Verifies MIT license headers
3. **Static Analysis** - Runs Android Lint
4. **Unit Tests** - Runs all JVM unit tests
5. **Test Coverage** - Ensures minimum 20% code coverage
5. **Test Coverage** - Ensures minimum 75% code coverage
6. **Build Library** - Builds the OpenMapView AAR
7. **Build Examples** - Builds all example applications

Expand Down
2 changes: 1 addition & 1 deletion docs/GITHUB_WORKFLOWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ instrumented-test (Phone + Automotive) runs in parallel
3. **lint** - Runs Android Lint
4. **test** - Runs unit tests (depends on format + copyright + lint)
5. **docs** - Builds API documentation with Dokka (depends on format + copyright + lint)
6. **coverage** - Checks test coverage meets 20% minimum (depends on format + copyright + lint)
6. **coverage** - Checks test coverage meets 75% minimum (depends on format + copyright + lint)
7. **build-library** - Builds library AAR (depends on all above, runs in parallel with build-examples)
8. **build-examples** - Builds example APKs (depends on all above, runs in parallel with build-library)
9. **instrumented-test** - Runs instrumentation tests on phone and automotive emulators (PR only)
Expand Down
39 changes: 26 additions & 13 deletions docs/TESTING_UNIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,30 +50,42 @@ The `--continue` flag ensures all tests run even if some fail, useful for gettin

## Test Structure

### Current Test Coverage (260+ tests across 21 test classes)
### Current Test Coverage (456 tests across 35 test classes)

| Test Class | Tests | Description |
|------------|-------|-------------|
| **AttributionOverlayTest** | 4 | Attribution rendering and touch detection |
| **BitmapDescriptorFactoryTest** | 18 | Marker icon generation with colors and custom images |
| **CameraPositionTest** | 6 | Camera position validation and equality |
| **CameraUpdateFactoryTest** | 8 | Camera update creation (zoom, pan, position) |
| **BitmapDescriptorFactoryTest** | 15 | Marker icon generation with colors and custom images |
| **CameraPositionTest** | 7 | Camera position validation and equality |
| **CameraUpdateFactoryTest** | 11 | Camera update creation (zoom, pan, position) |
| **CircleTest** | 4 | Circle defaults, radius validation, identity equality |
| **DiskTileCacheTest** | 8 | Persistent disk cache with DiskLruCache, per-source namespaces |
| **GeoJsonParserTest** | 14 | GeoJSON parsing (Point, LineString, Polygon, Multi*, Features) |
| **IntentUtilsTest** | 4 | External app integration (geo: URI, OSM fallback) |
| **GeodesicUtilsTest** | 17 | Great-circle interpolation for geodesic shapes |
| **GroundOverlayTest** | 10 | Ground overlay position/bounds validation and equality |
| **IntentUtilsTest** | 7 | External app integration (geo: URI, OSM fallback) |
| **LatLngBoundsTest** | 13 | Lat/lng bounds calculations and contains checks |
| **MapControllerTest** | 63 | Zoom, pan, marker/polyline/polygon/circle management, touch detection, padding |
| **MapFeaturesTest** | 4 | Feature query methods (traffic, indoor, buildings, myLocation) |
| **MapTypeTest** | 9 | Map type switching and validation |
| **MarkerTest** | 24 | Marker creation, equality, properties, info windows, dragging |
| **PolygonTest** | 16 | Polygon creation, validation, holes, and styling |
| **PolylineTest** | 16 | Polyline creation, validation, and styling |
| **LatLngTest** | 11 | Latitude clamping and longitude wrapping |
| **MapControllerOverlaysTest** | 13 | Circles, ground and tile overlays, their touch detection, info windows, drawing all shape types |
| **MapControllerTest** | 103 | Zoom, pan, marker/polyline/polygon management, touch detection, padding, camera bounds, edge effects |
| **MapFeaturesTest** | 5 | Feature query methods (traffic, indoor, buildings, myLocation) |
| **MapProjectionTest** | 8 | Projection screen/LatLng conversion, pan offset, padding, visible region |
| **MapTypeTest** | 18 | Map type switching and validation |
| **MarkerIconFactoryTest** | 3 | Default marker icon generation and caching |
| **MarkerTest** | 23 | Marker creation, equality, properties, info windows, dragging |
| **OptionsBuildersTest** | 13 | Google-style *Options builders for markers, shapes and overlays |
| **PolygonTest** | 20 | Polygon creation, validation, holes, and styling |
| **PolylineTest** | 31 | Polyline creation, validation, and styling |
| **PredefinedTileProvidersTest** | 1 | URL templates of the predefined overlay tile providers |
| **ProjectionTest** | 12 | Web Mercator projection calculations |
| **SnapshotTest** | 8 | Map snapshot API and callbacks |
| **TileCacheTest** | 9 | Memory LRU cache behavior, clearing, namespaces, disk writes |
| **TileDownloaderTest** | 5 | HTTP tile downloading with mocked Ktor client, HTTP errors, cancellation |
| **TileOverlayTest** | 5 | Tile overlay defaults, transparency validation, copy helpers |
| **TileRetryTrackerTest** | 4 | Exponential backoff for failed tiles |
| **TileSourceTest** | 7 | Tile source URL generation and attribution |
| **UiSettingsTest** | 19 | UI settings (gestures, zoom controls, not-implemented features) |
| **TileTest** | 6 | Tile content equality, validation and toString |
| **UiSettingsTest** | 25 | UI settings (gestures, zoom controls, not-implemented features) |
| **UrlTileProviderTest** | 2 | Overlay tile downloading, HTTP 404 handling |
| **ViewportCalculatorTest** | 10 | Visible tile calculation |
| **VisibleRegionTest** | 4 | Visible region bounds and corners |
Expand Down Expand Up @@ -424,7 +436,8 @@ The test suite currently covers:
- Memory cache (LRU) behavior
- Disk cache (persistent storage)
- Viewport calculation
- MapController rendering logic
- MapController rendering logic for all shape types, ground and tile overlays
- Options builders and the public Projection class
- Touch gesture handling (marker and attribution hit detection)
- Zoom level validation and bounds
- Network tile downloading (with mocking)
Expand Down
285 changes: 0 additions & 285 deletions openmapview/src/main/kotlin/de/afarber/openmapview/MapController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1357,253 +1357,6 @@ class MapController(
}
}

private fun drawPolylines(
canvas: Canvas,
centerPixelX: Float,
centerPixelY: Float,
) {
val paint = Paint()
paint.style = Paint.Style.STROKE
paint.isAntiAlias = true

for (polyline in polylines) {
if (!polyline.visible || polyline.points.size < 2) continue

// Expand points with geodesic interpolation if needed
val points =
if (polyline.geodesic) {
GeodesicUtils.expandWithGeodesicPoints(polyline.points)
} else {
polyline.points
}

// Handle spans (multi-color segments) or single color
if (polyline.spans.isNotEmpty()) {
drawPolylineWithSpans(canvas, polyline, points, centerPixelX, centerPixelY)
} else {
paint.color = polyline.strokeColor.toArgb()
paint.strokeWidth = polyline.strokeWidth
paint.pathEffect = polyline.strokePattern?.asAndroidPathEffect()
paint.strokeCap = polyline.startCap.toAndroidCap() // Use startCap for full path
paint.strokeJoin = polyline.strokeJoin.toAndroidJoin()

val path = android.graphics.Path()
var isFirst = true

for (point in points) {
val (pixelX, pixelY) = ProjectionUtils.latLngToPixel(point, zoom.toInt())
val screenX = pixelX - centerPixelX + viewWidth / 2 - panOffsetX
val screenY = pixelY - centerPixelY + viewHeight / 2 - panOffsetY

if (isFirst) {
path.moveTo(screenX, screenY)
isFirst = false
} else {
path.lineTo(screenX, screenY)
}
}

canvas.drawPath(path, paint)
}
}
}

private fun drawPolylineWithSpans(
canvas: Canvas,
polyline: Polyline,
points: List<LatLng>,
centerPixelX: Float,
centerPixelY: Float,
) {
val paint = Paint()
paint.style = Paint.Style.STROKE
paint.isAntiAlias = true
paint.strokeWidth = polyline.strokeWidth
paint.pathEffect = polyline.strokePattern?.asAndroidPathEffect()
paint.strokeJoin = polyline.strokeJoin.toAndroidJoin()

var segmentIndex = 0
val totalSegments = points.size - 1

for (span in polyline.spans) {
if (segmentIndex >= totalSegments) break

paint.color = span.color.toArgb()

for (i in 0 until span.segments) {
if (segmentIndex >= totalSegments) break

val isFirstSegment = segmentIndex == 0
val isLastSegment = segmentIndex == totalSegments - 1

// Apply appropriate cap
paint.strokeCap =
when {
isFirstSegment -> polyline.startCap.toAndroidCap()
isLastSegment -> polyline.endCap.toAndroidCap()
else -> Paint.Cap.BUTT
}

val from = points[segmentIndex]
val to = points[segmentIndex + 1]

val (fromPixelX, fromPixelY) = ProjectionUtils.latLngToPixel(from, zoom.toInt())
val fromScreenX = fromPixelX - centerPixelX + viewWidth / 2 - panOffsetX
val fromScreenY = fromPixelY - centerPixelY + viewHeight / 2 - panOffsetY

val (toPixelX, toPixelY) = ProjectionUtils.latLngToPixel(to, zoom.toInt())
val toScreenX = toPixelX - centerPixelX + viewWidth / 2 - panOffsetX
val toScreenY = toPixelY - centerPixelY + viewHeight / 2 - panOffsetY

canvas.drawLine(fromScreenX, fromScreenY, toScreenX, toScreenY, paint)
segmentIndex++
}
}

// Draw remaining segments with default color
if (segmentIndex < totalSegments) {
paint.color = polyline.strokeColor.toArgb()

while (segmentIndex < totalSegments) {
val isLastSegment = segmentIndex == totalSegments - 1
paint.strokeCap = if (isLastSegment) polyline.endCap.toAndroidCap() else Paint.Cap.BUTT

val from = points[segmentIndex]
val to = points[segmentIndex + 1]

val (fromPixelX, fromPixelY) = ProjectionUtils.latLngToPixel(from, zoom.toInt())
val fromScreenX = fromPixelX - centerPixelX + viewWidth / 2 - panOffsetX
val fromScreenY = fromPixelY - centerPixelY + viewHeight / 2 - panOffsetY

val (toPixelX, toPixelY) = ProjectionUtils.latLngToPixel(to, zoom.toInt())
val toScreenX = toPixelX - centerPixelX + viewWidth / 2 - panOffsetX
val toScreenY = toPixelY - centerPixelY + viewHeight / 2 - panOffsetY

canvas.drawLine(fromScreenX, fromScreenY, toScreenX, toScreenY, paint)
segmentIndex++
}
}
}

private fun drawPolygons(
canvas: Canvas,
centerPixelX: Float,
centerPixelY: Float,
) {
val fillPaint = Paint()
fillPaint.style = Paint.Style.FILL
fillPaint.isAntiAlias = true

val strokePaint = Paint()
strokePaint.style = Paint.Style.STROKE
strokePaint.isAntiAlias = true

for (polygon in polygons) {
if (!polygon.visible || polygon.points.size < 3) continue

// Expand points with geodesic interpolation if needed
val points =
if (polygon.geodesic) {
GeodesicUtils.expandWithGeodesicPoints(polygon.points)
} else {
polygon.points
}

fillPaint.color = polygon.fillColor.toArgb()
strokePaint.color = polygon.strokeColor.toArgb()
strokePaint.strokeWidth = polygon.strokeWidth
strokePaint.pathEffect = polygon.strokePattern?.asAndroidPathEffect()
strokePaint.strokeCap = polygon.strokeCap.toAndroidCap()
strokePaint.strokeJoin = polygon.strokeJoin.toAndroidJoin()

val path = android.graphics.Path()
path.fillType = android.graphics.Path.FillType.EVEN_ODD

// Draw main polygon outline
var isFirst = true
for (point in points) {
val (pixelX, pixelY) = ProjectionUtils.latLngToPixel(point, zoom.toInt())
val screenX = pixelX - centerPixelX + viewWidth / 2 - panOffsetX
val screenY = pixelY - centerPixelY + viewHeight / 2 - panOffsetY

if (isFirst) {
path.moveTo(screenX, screenY)
isFirst = false
} else {
path.lineTo(screenX, screenY)
}
}
path.close()

// Draw holes
for (hole in polygon.holes) {
if (hole.size < 3) continue
// Expand hole points with geodesic interpolation if needed
val holePoints =
if (polygon.geodesic) {
GeodesicUtils.expandWithGeodesicPoints(hole)
} else {
hole
}
isFirst = true
for (point in holePoints) {
val (pixelX, pixelY) = ProjectionUtils.latLngToPixel(point, zoom.toInt())
val screenX = pixelX - centerPixelX + viewWidth / 2 - panOffsetX
val screenY = pixelY - centerPixelY + viewHeight / 2 - panOffsetY

if (isFirst) {
path.moveTo(screenX, screenY)
isFirst = false
} else {
path.lineTo(screenX, screenY)
}
}
path.close()
}

// Draw fill first, then stroke
canvas.drawPath(path, fillPaint)
canvas.drawPath(path, strokePaint)
}
}

private fun drawCircles(
canvas: Canvas,
centerPixelX: Float,
centerPixelY: Float,
) {
val fillPaint = Paint()
fillPaint.style = Paint.Style.FILL
fillPaint.isAntiAlias = true

val strokePaint = Paint()
strokePaint.style = Paint.Style.STROKE
strokePaint.isAntiAlias = true

for (circle in circles) {
if (!circle.visible || circle.radius <= 0) continue

fillPaint.color = circle.fillColor.toArgb()
strokePaint.color = circle.strokeColor.toArgb()
strokePaint.strokeWidth = circle.strokeWidth
strokePaint.pathEffect = circle.strokePattern?.asAndroidPathEffect()
strokePaint.strokeCap = circle.strokeCap.toAndroidCap()
strokePaint.strokeJoin = circle.strokeJoin.toAndroidJoin()

// Convert center to screen coordinates
val (pixelX, pixelY) = ProjectionUtils.latLngToPixel(circle.center, zoom.toInt())
val screenX = pixelX - centerPixelX + viewWidth / 2 - panOffsetX
val screenY = pixelY - centerPixelY + viewHeight / 2 - panOffsetY

// Convert radius from meters to pixels
val radiusInPixels = metersToPixels(circle.radius, circle.center.latitude, zoom)

// Draw fill first, then stroke
canvas.drawCircle(screenX, screenY, radiusInPixels, fillPaint)
canvas.drawCircle(screenX, screenY, radiusInPixels, strokePaint)
}
}

/**
* Converts a distance in meters to screen pixels at a given latitude and zoom level.
* Uses the Mercator projection formula.
Expand All @@ -1622,44 +1375,6 @@ class MapController(
return (meters / metersPerPixel).toFloat()
}

private fun drawMarkers(
canvas: Canvas,
centerPixelX: Float,
centerPixelY: Float,
) {
for (marker in markers) {
// Skip invisible markers
if (!marker.visible) continue

// Convert marker position to pixel coordinates
val (markerPixelX, markerPixelY) = ProjectionUtils.latLngToPixel(marker.position, zoom.toInt())

// Calculate screen position
val screenX = markerPixelX - centerPixelX + viewWidth / 2 - panOffsetX
val screenY = markerPixelY - centerPixelY + viewHeight / 2 - panOffsetY

// Get marker icon
val icon = loadBitmap(marker.icon)

// Apply anchor point
val anchorX = icon.width * marker.anchor.first
val anchorY = icon.height * marker.anchor.second

// Create paint with alpha if needed
val paint =
if (marker.alpha < 1.0f) {
Paint().apply {
alpha = (marker.alpha * 255).toInt()
}
} else {
null
}

// Draw the marker
canvas.drawBitmap(icon, screenX - anchorX, screenY - anchorY, paint)
}
}

private fun drawGroundOverlay(
canvas: Canvas,
centerPixelX: Float,
Expand Down
Loading
Loading