|
3 | 3 | // found in the LICENSE file. |
4 | 4 |
|
5 | 5 | import 'dart:async'; |
| 6 | +import 'dart:math'; |
6 | 7 |
|
7 | 8 | import 'package:flutter/foundation.dart'; |
8 | 9 | import 'package:flutter/gestures.dart'; |
@@ -406,20 +407,134 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { |
406 | 407 | CameraUpdate cameraUpdate, |
407 | 408 | CameraUpdateAnimationConfiguration configuration, { |
408 | 409 | required int mapId, |
409 | | - }) { |
| 410 | + }) async { |
| 411 | + var effective = cameraUpdate; |
| 412 | + if (cameraUpdate is CameraUpdateNewLatLngBoundsWithEdgeInsets) { |
| 413 | + final CameraPosition pos = await _computeBoundsWithEdgeInsets( |
| 414 | + cameraUpdate.bounds, |
| 415 | + cameraUpdate.padding, |
| 416 | + mapId, |
| 417 | + ); |
| 418 | + effective = CameraUpdate.newCameraPosition(pos); |
| 419 | + } |
410 | 420 | return _hostApi(mapId).animateCamera( |
411 | | - _platformCameraUpdateFromCameraUpdate(cameraUpdate), |
| 421 | + _platformCameraUpdateFromCameraUpdate(effective), |
412 | 422 | configuration.duration?.inMilliseconds, |
413 | 423 | ); |
414 | 424 | } |
415 | 425 |
|
416 | 426 | @override |
417 | | - Future<void> moveCamera(CameraUpdate cameraUpdate, {required int mapId}) { |
| 427 | + Future<void> moveCamera( |
| 428 | + CameraUpdate cameraUpdate, { |
| 429 | + required int mapId, |
| 430 | + }) async { |
| 431 | + var effective = cameraUpdate; |
| 432 | + if (cameraUpdate is CameraUpdateNewLatLngBoundsWithEdgeInsets) { |
| 433 | + final CameraPosition pos = await _computeBoundsWithEdgeInsets( |
| 434 | + cameraUpdate.bounds, |
| 435 | + cameraUpdate.padding, |
| 436 | + mapId, |
| 437 | + ); |
| 438 | + effective = CameraUpdate.newCameraPosition(pos); |
| 439 | + } |
418 | 440 | return _hostApi( |
419 | 441 | mapId, |
420 | | - ).moveCamera(_platformCameraUpdateFromCameraUpdate(cameraUpdate)); |
| 442 | + ).moveCamera(_platformCameraUpdateFromCameraUpdate(effective)); |
| 443 | + } |
| 444 | + |
| 445 | + static const double _kMercatorTileSize = 256.0; |
| 446 | + |
| 447 | + /// Computes a [CameraPosition] that fits [bounds] within the map viewport |
| 448 | + /// minus [padding]. |
| 449 | + /// |
| 450 | + /// Uses Web Mercator projection to calculate the zoom level and offset |
| 451 | + /// center from the current map dimensions. |
| 452 | + Future<CameraPosition> _computeBoundsWithEdgeInsets( |
| 453 | + LatLngBounds bounds, |
| 454 | + EdgeInsets padding, |
| 455 | + int mapId, |
| 456 | + ) async { |
| 457 | + final double currentZoom = await getZoomLevel(mapId: mapId); |
| 458 | + final LatLngBounds region = await getVisibleRegion(mapId: mapId); |
| 459 | + final double scale = pow(2.0, currentZoom).toDouble(); |
| 460 | + |
| 461 | + double regionLngSpan = |
| 462 | + region.northeast.longitude - region.southwest.longitude; |
| 463 | + if (regionLngSpan <= 0) { |
| 464 | + regionLngSpan += 360; |
| 465 | + } |
| 466 | + final double mapWidthDp = |
| 467 | + regionLngSpan / 360.0 * _kMercatorTileSize * scale; |
| 468 | + |
| 469 | + final double regionNeY = _mercatorY(region.northeast.latitude); |
| 470 | + final double regionSwY = _mercatorY(region.southwest.latitude); |
| 471 | + final double mapHeightDp = |
| 472 | + (regionSwY - regionNeY) * _kMercatorTileSize * scale; |
| 473 | + |
| 474 | + final double availW = mapWidthDp - padding.left - padding.right; |
| 475 | + final double availH = mapHeightDp - padding.top - padding.bottom; |
| 476 | + |
| 477 | + final LatLng ne = bounds.northeast; |
| 478 | + final LatLng sw = bounds.southwest; |
| 479 | + double lngSpan = ne.longitude - sw.longitude; |
| 480 | + if (lngSpan <= 0) { |
| 481 | + lngSpan += 360; |
| 482 | + } |
| 483 | + |
| 484 | + final double neY = _mercatorY(ne.latitude); |
| 485 | + final double swY = _mercatorY(sw.latitude); |
| 486 | + final double latSpanMerc = (swY - neY).abs(); |
| 487 | + |
| 488 | + final double centerLat = (ne.latitude + sw.latitude) / 2; |
| 489 | + double centerLng = (ne.longitude + sw.longitude) / 2; |
| 490 | + if (ne.longitude < sw.longitude) { |
| 491 | + centerLng += 180; |
| 492 | + if (centerLng > 180) { |
| 493 | + centerLng -= 360; |
| 494 | + } |
| 495 | + } |
| 496 | + |
| 497 | + if (availW <= 0 || availH <= 0) { |
| 498 | + return CameraPosition( |
| 499 | + target: LatLng(centerLat, centerLng), |
| 500 | + zoom: currentZoom, |
| 501 | + ); |
| 502 | + } |
| 503 | + |
| 504 | + final double zoomLng = _log2( |
| 505 | + availW / (lngSpan / 360.0 * _kMercatorTileSize), |
| 506 | + ); |
| 507 | + final double zoomLat = _log2(availH / (latSpanMerc * _kMercatorTileSize)); |
| 508 | + final double zoom = min(zoomLng, zoomLat); |
| 509 | + |
| 510 | + final double targetScale = pow(2.0, zoom).toDouble(); |
| 511 | + final double lngPerDp = 360.0 / (_kMercatorTileSize * targetScale); |
| 512 | + final double offsetLng = (padding.right - padding.left) / 2 * lngPerDp; |
| 513 | + |
| 514 | + final double centerMercY = _mercatorY(centerLat); |
| 515 | + final double centerPxY = centerMercY * _kMercatorTileSize * targetScale; |
| 516 | + final double offsetPxY = centerPxY + (padding.bottom - padding.top) / 2; |
| 517 | + final double offsetLat = _inverseMercatorY( |
| 518 | + offsetPxY / (_kMercatorTileSize * targetScale), |
| 519 | + ); |
| 520 | + |
| 521 | + return CameraPosition( |
| 522 | + target: LatLng(offsetLat, centerLng + offsetLng), |
| 523 | + zoom: zoom, |
| 524 | + ); |
| 525 | + } |
| 526 | + |
| 527 | + static double _mercatorY(double lat) { |
| 528 | + final double latRad = lat * pi / 180; |
| 529 | + return (1 - log(tan(latRad) + 1 / cos(latRad)) / pi) / 2; |
| 530 | + } |
| 531 | + |
| 532 | + static double _inverseMercatorY(double y) { |
| 533 | + return (2 * atan(exp(pi * (1 - 2 * y))) - pi / 2) * 180 / pi; |
421 | 534 | } |
422 | 535 |
|
| 536 | + static double _log2(double x) => log(x) / ln2; |
| 537 | + |
423 | 538 | @override |
424 | 539 | Future<void> setMapStyle(String? mapStyle, {required int mapId}) async { |
425 | 540 | final bool success = await _hostApi(mapId).setStyle(mapStyle ?? ''); |
@@ -1008,9 +1123,11 @@ class GoogleMapsFlutterAndroid extends GoogleMapsFlutterPlatform { |
1008 | 1123 | ), |
1009 | 1124 | ); |
1010 | 1125 | case CameraUpdateType.newLatLngBoundsWithEdgeInsets: |
1011 | | - throw UnsupportedError( |
1012 | | - 'CameraUpdate.newLatLngBoundsWithEdgeInsets is not supported on Android. ' |
1013 | | - 'Use CameraUpdate.newLatLngBounds with uniform padding instead.', |
| 1126 | + // Requires async platform calls (getZoomLevel, getVisibleRegion) to |
| 1127 | + // compute the polyfill, so it is handled in moveCamera/animateCamera |
| 1128 | + // before reaching this static sync method. |
| 1129 | + throw StateError( |
| 1130 | + 'newLatLngBoundsWithEdgeInsets should be unreachable here.', |
1014 | 1131 | ); |
1015 | 1132 | } |
1016 | 1133 | } |
|
0 commit comments