Skip to content

Commit 8fc9e6c

Browse files
committed
Implement camera updates
1 parent 7a00581 commit 8fc9e6c

File tree

6 files changed

+363
-6
lines changed

6 files changed

+363
-6
lines changed

docs/PUBLIC_API.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -237,12 +237,12 @@ This method is an OpenMapView-specific feature not present in Google Maps API. S
237237
| `newLatLngZoom(LatLng, float)` | `CameraUpdate` | IMPLEMENTED | Move to location and zoom |
238238
| `newCameraPosition(CameraPosition)` | `CameraUpdate` | IMPLEMENTED | Move to camera position |
239239
| `zoomIn()` | `CameraUpdate` | IMPLEMENTED | Increment zoom by 1 |
240-
| `zoomOut()` | `CameraUpdate` | IMPLEMENTED | Decrement zoom by 1 |
241-
| `zoomTo(float)` | `CameraUpdate` | IMPLEMENTED | Set specific zoom level |
242-
| `zoomBy(float)` | `CameraUpdate` | IMPLEMENTED | Adjust zoom by amount |
243-
| `newLatLngBounds(LatLngBounds, int)` | `CameraUpdate` | NOT IMPLEMENTED | Planned for future release |
244-
| `newLatLngBounds(LatLngBounds, int, int, int)` | `CameraUpdate` | NOT IMPLEMENTED | Planned for future release |
245-
| `scrollBy(float, float)` | `CameraUpdate` | NOT IMPLEMENTED | Planned for future release |
240+
| `zoomOut()` | `CameraUpdate` | IMPLEMENTED | Decrement zoom by 1 |
241+
| `zoomTo(float)` | `CameraUpdate` | IMPLEMENTED | Set specific zoom level |
242+
| `zoomBy(float)` | `CameraUpdate` | IMPLEMENTED | Adjust zoom by amount |
243+
| `newLatLngBounds(LatLngBounds, int)` | `CameraUpdate` | IMPLEMENTED | Fit bounds with padding |
244+
| `newLatLngBounds(LatLngBounds, int, int, int)` | `CameraUpdate` | IMPLEMENTED | Fit bounds in specified dimensions |
245+
| `scrollBy(float, float)` | `CameraUpdate` | IMPLEMENTED | Scroll map by pixel offset |
246246

247247
---
248248

openmapview/src/main/kotlin/de/afarber/openmapview/CameraUpdate.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,33 @@ sealed class CameraUpdate {
6161
internal data class ZoomBy(
6262
val amount: Double,
6363
) : CameraUpdate()
64+
65+
/**
66+
* Scrolls the map by specified pixel amounts.
67+
* Positive x moves viewport right, positive y moves viewport down.
68+
*/
69+
internal data class ScrollBy(
70+
val xPixels: Float,
71+
val yPixels: Float,
72+
) : CameraUpdate()
73+
74+
/**
75+
* Moves camera to show the entire bounds with uniform padding.
76+
* Calculates appropriate zoom level automatically.
77+
*/
78+
internal data class NewLatLngBounds(
79+
val bounds: LatLngBounds,
80+
val padding: Int,
81+
) : CameraUpdate()
82+
83+
/**
84+
* Moves camera to show the entire bounds in specified viewport dimensions with padding.
85+
* Useful when map view hasn't been laid out yet.
86+
*/
87+
internal data class NewLatLngBoundsWithSize(
88+
val bounds: LatLngBounds,
89+
val width: Int,
90+
val height: Int,
91+
val padding: Int,
92+
) : CameraUpdate()
6493
}

openmapview/src/main/kotlin/de/afarber/openmapview/CameraUpdateFactory.kt

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,55 @@ object CameraUpdateFactory {
7575
* @return A CameraUpdate for the relative zoom adjustment
7676
*/
7777
fun zoomBy(amount: Double): CameraUpdate = CameraUpdate.ZoomBy(amount)
78+
79+
/**
80+
* Returns a CameraUpdate that scrolls the map by the specified pixel amounts.
81+
*
82+
* Positive xPixels moves the viewport right (map content moves left).
83+
* Positive yPixels moves the viewport down (map content moves up).
84+
* The current zoom level is preserved.
85+
*
86+
* @param xPixels The horizontal scroll amount in pixels
87+
* @param yPixels The vertical scroll amount in pixels
88+
* @return A CameraUpdate for the pixel scroll
89+
*/
90+
fun scrollBy(
91+
xPixels: Float,
92+
yPixels: Float,
93+
): CameraUpdate = CameraUpdate.ScrollBy(xPixels, yPixels)
94+
95+
/**
96+
* Returns a CameraUpdate that moves the camera to show the entire bounds.
97+
*
98+
* The camera will be positioned at the center of the bounds, and the zoom level
99+
* will be calculated to fit the entire bounds within the viewport with the specified padding.
100+
*
101+
* @param bounds The geographic bounds to display
102+
* @param padding Padding in pixels to apply uniformly on all sides
103+
* @return A CameraUpdate to display the bounds
104+
*/
105+
fun newLatLngBounds(
106+
bounds: LatLngBounds,
107+
padding: Int,
108+
): CameraUpdate = CameraUpdate.NewLatLngBounds(bounds, padding)
109+
110+
/**
111+
* Returns a CameraUpdate that moves the camera to show the entire bounds
112+
* in a viewport of the specified dimensions.
113+
*
114+
* This overload is useful when the map view hasn't been laid out yet and you need
115+
* to calculate the camera position for specific viewport dimensions.
116+
*
117+
* @param bounds The geographic bounds to display
118+
* @param width The viewport width in pixels
119+
* @param height The viewport height in pixels
120+
* @param padding Padding in pixels to apply uniformly on all sides
121+
* @return A CameraUpdate to display the bounds
122+
*/
123+
fun newLatLngBounds(
124+
bounds: LatLngBounds,
125+
width: Int,
126+
height: Int,
127+
padding: Int,
128+
): CameraUpdate = CameraUpdate.NewLatLngBoundsWithSize(bounds, width, height, padding)
78129
}

openmapview/src/main/kotlin/de/afarber/openmapview/MapController.kt

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,44 @@ class MapController(
560560
)
561561
}
562562

563+
/**
564+
* Calculates the appropriate zoom level to fit bounds within the specified viewport dimensions.
565+
*
566+
* Iterates from maximum zoom down to minimum zoom, finding the largest zoom level
567+
* where the bounds fit entirely within the viewport.
568+
*
569+
* @param bounds The geographic bounds to fit
570+
* @param viewWidth The viewport width in pixels
571+
* @param viewHeight The viewport height in pixels
572+
* @return The calculated zoom level (between 2.0 and 19.0)
573+
*/
574+
private fun calculateZoomForBounds(
575+
bounds: LatLngBounds,
576+
viewWidth: Int,
577+
viewHeight: Int,
578+
): Double {
579+
// Ensure we have valid viewport dimensions
580+
if (viewWidth <= 0 || viewHeight <= 0) {
581+
return DEFAULT_MIN_ZOOM
582+
}
583+
584+
// Iterate from max zoom down to find the largest zoom that fits
585+
for (zoom in 19 downTo 2) {
586+
val (swX, swY) = ProjectionUtils.latLngToPixel(bounds.southwest, zoom)
587+
val (neX, neY) = ProjectionUtils.latLngToPixel(bounds.northeast, zoom)
588+
589+
val boundsWidth = kotlin.math.abs(neX - swX)
590+
val boundsHeight = kotlin.math.abs(swY - neY) // Y increases downward
591+
592+
if (boundsWidth <= viewWidth && boundsHeight <= viewHeight) {
593+
return zoom.toDouble()
594+
}
595+
}
596+
597+
// Fallback to minimum zoom
598+
return DEFAULT_MIN_ZOOM
599+
}
600+
563601
private fun calculateTargetPosition(
564602
cameraUpdate: CameraUpdate,
565603
currentPosition: CameraPosition,
@@ -599,6 +637,50 @@ class MapController(
599637
target = currentPosition.target,
600638
zoom = (currentPosition.zoom + cameraUpdate.amount).coerceIn(minZoomPreference, maxZoomPreference),
601639
)
640+
is CameraUpdate.ScrollBy -> {
641+
// Convert current center to pixel coordinates
642+
val (centerPixelX, centerPixelY) =
643+
ProjectionUtils.latLngToPixel(
644+
currentPosition.target,
645+
currentPosition.zoom.toInt(),
646+
)
647+
648+
// Apply pixel scroll offset
649+
val newPixelX = (centerPixelX + cameraUpdate.xPixels).toInt()
650+
val newPixelY = (centerPixelY + cameraUpdate.yPixels).toInt()
651+
652+
// Convert back to LatLng
653+
val newTarget = ProjectionUtils.pixelToLatLng(newPixelX, newPixelY, currentPosition.zoom.toInt())
654+
655+
CameraPosition(
656+
target = newTarget,
657+
zoom = currentPosition.zoom,
658+
)
659+
}
660+
is CameraUpdate.NewLatLngBounds -> {
661+
val zoom =
662+
calculateZoomForBounds(
663+
bounds = cameraUpdate.bounds,
664+
viewWidth = viewWidth - cameraUpdate.padding * 2,
665+
viewHeight = viewHeight - cameraUpdate.padding * 2,
666+
)
667+
CameraPosition(
668+
target = cameraUpdate.bounds.getCenter(),
669+
zoom = zoom.coerceIn(minZoomPreference, maxZoomPreference),
670+
)
671+
}
672+
is CameraUpdate.NewLatLngBoundsWithSize -> {
673+
val zoom =
674+
calculateZoomForBounds(
675+
bounds = cameraUpdate.bounds,
676+
viewWidth = cameraUpdate.width - cameraUpdate.padding * 2,
677+
viewHeight = cameraUpdate.height - cameraUpdate.padding * 2,
678+
)
679+
CameraPosition(
680+
target = cameraUpdate.bounds.getCenter(),
681+
zoom = zoom.coerceIn(minZoomPreference, maxZoomPreference),
682+
)
683+
}
602684
}
603685

604686
private fun interpolate(

openmapview/src/test/kotlin/de/afarber/openmapview/CameraUpdateFactoryTest.kt

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,44 @@ class CameraUpdateFactoryTest {
8686
assertTrue(update is CameraUpdate.ZoomBy)
8787
assertEquals(amount, (update as CameraUpdate.ZoomBy).amount, 0.0001)
8888
}
89+
90+
@Test
91+
fun `scrollBy creates ScrollBy update`() {
92+
val xPixels = 100f
93+
val yPixels = -50f
94+
val update = CameraUpdateFactory.scrollBy(xPixels, yPixels)
95+
96+
assertTrue(update is CameraUpdate.ScrollBy)
97+
val scrollUpdate = update as CameraUpdate.ScrollBy
98+
assertEquals(xPixels, scrollUpdate.xPixels, 0.01f)
99+
assertEquals(yPixels, scrollUpdate.yPixels, 0.01f)
100+
}
101+
102+
@Test
103+
fun `newLatLngBounds creates NewLatLngBounds update`() {
104+
val bounds = LatLngBounds(LatLng(51.46, 7.24), LatLng(51.47, 7.25))
105+
val padding = 100
106+
val update = CameraUpdateFactory.newLatLngBounds(bounds, padding)
107+
108+
assertTrue(update is CameraUpdate.NewLatLngBounds)
109+
val boundsUpdate = update as CameraUpdate.NewLatLngBounds
110+
assertEquals(bounds, boundsUpdate.bounds)
111+
assertEquals(padding, boundsUpdate.padding)
112+
}
113+
114+
@Test
115+
fun `newLatLngBounds with dimensions creates NewLatLngBoundsWithSize update`() {
116+
val bounds = LatLngBounds(LatLng(51.46, 7.24), LatLng(51.47, 7.25))
117+
val width = 1080
118+
val height = 1920
119+
val padding = 50
120+
val update = CameraUpdateFactory.newLatLngBounds(bounds, width, height, padding)
121+
122+
assertTrue(update is CameraUpdate.NewLatLngBoundsWithSize)
123+
val boundsUpdate = update as CameraUpdate.NewLatLngBoundsWithSize
124+
assertEquals(bounds, boundsUpdate.bounds)
125+
assertEquals(width, boundsUpdate.width)
126+
assertEquals(height, boundsUpdate.height)
127+
assertEquals(padding, boundsUpdate.padding)
128+
}
89129
}

0 commit comments

Comments
 (0)