@lugg/maps 1.0.0-beta.19 → 1.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +94 -13
- package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +44 -8
- package/ios/LuggMapView.mm +24 -14
- package/ios/core/AppleStaticMapProvider.mm +13 -0
- package/ios/core/GoogleStaticMapProvider.mm +6 -0
- package/ios/core/StaticMapProviderBase.h +5 -0
- package/ios/core/StaticMapProviderBase.mm +69 -1
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@ import com.luggmaps.extensions.findViewByTag
|
|
|
49
49
|
import java.net.URL
|
|
50
50
|
import kotlin.math.atan
|
|
51
51
|
import kotlin.math.ln
|
|
52
|
+
import kotlin.math.log2
|
|
52
53
|
import kotlin.math.pow
|
|
53
54
|
import kotlin.math.sin
|
|
54
55
|
import kotlin.math.sinh
|
|
@@ -866,13 +867,11 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
866
867
|
return
|
|
867
868
|
}
|
|
868
869
|
|
|
869
|
-
polylineView.polyline?.width = polylineView.strokeWidth.dpToPx()
|
|
870
|
-
polylineView.polyline?.zIndex = polylineView.zIndex
|
|
871
|
-
|
|
872
870
|
polylineAnimators[polylineView]?.apply {
|
|
873
871
|
coordinates = polylineView.coordinates
|
|
874
872
|
strokeColors = polylineView.strokeColors
|
|
875
873
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
874
|
+
zIndex = polylineView.zIndex
|
|
876
875
|
animatedOptions = polylineView.animatedOptions
|
|
877
876
|
animated = polylineView.animated && !staticMode
|
|
878
877
|
update()
|
|
@@ -902,6 +901,7 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
902
901
|
coordinates = polylineView.coordinates
|
|
903
902
|
strokeColors = polylineView.strokeColors
|
|
904
903
|
strokeWidth = polylineView.strokeWidth.dpToPx()
|
|
904
|
+
zIndex = polylineView.zIndex
|
|
905
905
|
animatedOptions = polylineView.animatedOptions
|
|
906
906
|
// Static maps render once; show the full polyline instead of animating
|
|
907
907
|
animated = polylineView.animated && !staticMode
|
|
@@ -1203,6 +1203,17 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1203
1203
|
// region Commands
|
|
1204
1204
|
|
|
1205
1205
|
override fun moveCamera(latitude: Double, longitude: Double, zoom: Double, duration: Int) {
|
|
1206
|
+
if (staticMode) {
|
|
1207
|
+
// Static maps don't animate; re-center at the final camera
|
|
1208
|
+
initialLatitude = latitude
|
|
1209
|
+
initialLongitude = longitude
|
|
1210
|
+
if (zoom > 0) initialZoom = zoom.toFloat()
|
|
1211
|
+
val map = googleMap ?: return
|
|
1212
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1213
|
+
positionLiveMarkers()
|
|
1214
|
+
return
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1206
1217
|
val map = googleMap ?: return
|
|
1207
1218
|
val position = LatLng(latitude, longitude)
|
|
1208
1219
|
val targetZoom = if (zoom > 0) zoom.toFloat() else map.cameraPosition.zoom
|
|
@@ -1232,15 +1243,20 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1232
1243
|
val latLongs = coordinates.filterIsInstance<LatLng>()
|
|
1233
1244
|
if (latLongs.isEmpty()) return
|
|
1234
1245
|
|
|
1235
|
-
val boundsBuilder = LatLngBounds.Builder()
|
|
1236
|
-
latLongs.forEach { boundsBuilder.include(it) }
|
|
1237
|
-
val bounds = boundsBuilder.build()
|
|
1238
|
-
|
|
1239
1246
|
val top = edgeInsetsTop.toFloat().dpToPx().toInt()
|
|
1240
1247
|
val left = edgeInsetsLeft.toFloat().dpToPx().toInt()
|
|
1241
1248
|
val bottom = edgeInsetsBottom.toFloat().dpToPx().toInt()
|
|
1242
1249
|
val right = edgeInsetsRight.toFloat().dpToPx().toInt()
|
|
1243
1250
|
|
|
1251
|
+
if (staticMode) {
|
|
1252
|
+
fitStaticCoordinates(latLongs, top, left, bottom, right)
|
|
1253
|
+
return
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
val boundsBuilder = LatLngBounds.Builder()
|
|
1257
|
+
latLongs.forEach { boundsBuilder.include(it) }
|
|
1258
|
+
val bounds = boundsBuilder.build()
|
|
1259
|
+
|
|
1244
1260
|
val combined = combinedEdgeInsets()
|
|
1245
1261
|
map.setPadding(
|
|
1246
1262
|
combined.left + left,
|
|
@@ -1341,14 +1357,79 @@ class GoogleMapProvider(private val context: Context) :
|
|
|
1341
1357
|
val offsetY = (edgeInsets.top - edgeInsets.bottom) / 2.0
|
|
1342
1358
|
if (offsetX == 0.0 && offsetY == 0.0) return LatLng(initialLatitude, initialLongitude)
|
|
1343
1359
|
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1360
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1361
|
+
return latLngFromMercator(
|
|
1362
|
+
mercatorX(initialLongitude) - offsetX / worldSize,
|
|
1363
|
+
mercatorY(initialLatitude) - offsetY / worldSize
|
|
1364
|
+
)
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
// A static camera can't fit with map padding (lite mode misrenders it);
|
|
1368
|
+
// compute the fitted camera in mercator space instead, shifting the
|
|
1369
|
+
// center for asymmetric padding like edge insets
|
|
1370
|
+
private fun fitStaticCoordinates(
|
|
1371
|
+
coordinates: List<LatLng>,
|
|
1372
|
+
top: Int,
|
|
1373
|
+
left: Int,
|
|
1374
|
+
bottom: Int,
|
|
1375
|
+
right: Int
|
|
1376
|
+
) {
|
|
1377
|
+
val map = googleMap ?: return
|
|
1378
|
+
val wrapper = wrapperView ?: return
|
|
1379
|
+
if (wrapper.width == 0 || wrapper.height == 0) return
|
|
1380
|
+
|
|
1381
|
+
var minX = Double.MAX_VALUE
|
|
1382
|
+
var minY = Double.MAX_VALUE
|
|
1383
|
+
var maxX = -Double.MAX_VALUE
|
|
1384
|
+
var maxY = -Double.MAX_VALUE
|
|
1385
|
+
for (coordinate in coordinates) {
|
|
1386
|
+
val x = mercatorX(coordinate.longitude)
|
|
1387
|
+
val y = mercatorY(coordinate.latitude)
|
|
1388
|
+
minX = minOf(minX, x)
|
|
1389
|
+
maxX = maxOf(maxX, x)
|
|
1390
|
+
minY = minOf(minY, y)
|
|
1391
|
+
maxY = maxOf(maxY, y)
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
val insets = combinedEdgeInsets()
|
|
1395
|
+
val availWidth = (wrapper.width - insets.left - insets.right - left - right).coerceAtLeast(1)
|
|
1396
|
+
val availHeight = (wrapper.height - insets.top - insets.bottom - top - bottom).coerceAtLeast(1)
|
|
1397
|
+
|
|
1398
|
+
// World size in px at which the bounds fit the padded viewport;
|
|
1399
|
+
// infinite for coincident coordinates - keep the current zoom then
|
|
1400
|
+
val fitWorldSize = minOf(availWidth / (maxX - minX), availHeight / (maxY - minY))
|
|
1401
|
+
if (fitWorldSize.isFinite()) {
|
|
1402
|
+
initialZoom = log2(fitWorldSize / 256f.dpToPx())
|
|
1403
|
+
.toFloat()
|
|
1404
|
+
.coerceIn(map.minZoomLevel, map.maxZoomLevel)
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
val worldSize = mercatorWorldSize(initialZoom)
|
|
1408
|
+
val center = latLngFromMercator(
|
|
1409
|
+
(minX + maxX) / 2.0 - (left - right) / 2.0 / worldSize,
|
|
1410
|
+
(minY + maxY) / 2.0 - (top - bottom) / 2.0 / worldSize
|
|
1411
|
+
)
|
|
1412
|
+
initialLatitude = center.latitude
|
|
1413
|
+
initialLongitude = center.longitude
|
|
1414
|
+
|
|
1415
|
+
map.moveCamera(CameraUpdateFactory.newLatLngZoom(staticCameraTarget(), initialZoom))
|
|
1416
|
+
positionLiveMarkers()
|
|
1350
1417
|
}
|
|
1351
1418
|
|
|
1419
|
+
// Web mercator world space in [0, 1]
|
|
1420
|
+
private fun mercatorX(longitude: Double): Double = (longitude + 180.0) / 360.0
|
|
1421
|
+
|
|
1422
|
+
private fun mercatorY(latitude: Double): Double {
|
|
1423
|
+
val sinLat = sin(Math.toRadians(latitude))
|
|
1424
|
+
return 0.5 - ln((1.0 + sinLat) / (1.0 - sinLat)) / (4.0 * Math.PI)
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
private fun latLngFromMercator(x: Double, y: Double): LatLng =
|
|
1428
|
+
LatLng(Math.toDegrees(atan(sinh(Math.PI * (1.0 - 2.0 * y)))), x * 360.0 - 180.0)
|
|
1429
|
+
|
|
1430
|
+
// World size in px at the given zoom
|
|
1431
|
+
private fun mercatorWorldSize(zoom: Float): Double = 256f.dpToPx().toDouble() * 2.0.pow(zoom.toDouble())
|
|
1432
|
+
|
|
1352
1433
|
private fun applyWatermarkTranslation(insets: EdgeInsets, duration: Int = 0) {
|
|
1353
1434
|
val view = mapView ?: return
|
|
1354
1435
|
view.findViewByTag("GoogleWatermark")?.let { watermark ->
|
|
@@ -16,12 +16,19 @@ class PolylineAnimator {
|
|
|
16
16
|
companion object {
|
|
17
17
|
private const val MAX_ANIMATION_SPANS = 16
|
|
18
18
|
private const val MAX_GRADIENT_SPANS = 512
|
|
19
|
+
|
|
20
|
+
// Cap polyline updates to ~60fps; every update crosses Binder into the
|
|
21
|
+
// maps renderer, and 120Hz displays double that churn (CONSUMER-APP-44Y)
|
|
22
|
+
private const val MIN_UPDATE_INTERVAL_NANOS = 16_000_000L
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
var polyline: Polyline? = null
|
|
22
26
|
var coordinates: List<LatLng> = emptyList()
|
|
23
27
|
set(value) {
|
|
24
|
-
|
|
28
|
+
// Structural equality: Fabric delivers a new list instance on every
|
|
29
|
+
// commit, so identity checks never match and the full route would be
|
|
30
|
+
// re-parceled across Binder even when unchanged
|
|
31
|
+
if (field == value) return
|
|
25
32
|
field = value
|
|
26
33
|
dirty = true
|
|
27
34
|
if (animated && animator != null) {
|
|
@@ -30,11 +37,22 @@ class PolylineAnimator {
|
|
|
30
37
|
}
|
|
31
38
|
var strokeColors: List<Int> = listOf(Color.BLACK)
|
|
32
39
|
set(value) {
|
|
33
|
-
if (field
|
|
40
|
+
if (field == value) return
|
|
34
41
|
field = value
|
|
35
42
|
dirty = true
|
|
36
43
|
}
|
|
37
44
|
var strokeWidth: Float = 1f
|
|
45
|
+
set(value) {
|
|
46
|
+
if (field == value) return
|
|
47
|
+
field = value
|
|
48
|
+
polyline?.width = value
|
|
49
|
+
}
|
|
50
|
+
var zIndex: Float = 0f
|
|
51
|
+
set(value) {
|
|
52
|
+
if (field == value) return
|
|
53
|
+
field = value
|
|
54
|
+
polyline?.zIndex = value
|
|
55
|
+
}
|
|
38
56
|
var animatedOptions: AnimatedOptions = AnimatedOptions()
|
|
39
57
|
set(value) {
|
|
40
58
|
if (field == value) return
|
|
@@ -58,6 +76,8 @@ class PolylineAnimator {
|
|
|
58
76
|
|
|
59
77
|
private var animator: ValueAnimator? = null
|
|
60
78
|
private var animationProgress: Float = 0f
|
|
79
|
+
private var lastUpdateNanos: Long = 0L
|
|
80
|
+
private var lastAppliedColor: Int? = null
|
|
61
81
|
private var cumulativeDistances: FloatArray = floatArrayOf()
|
|
62
82
|
private var totalLength: Float = 0f
|
|
63
83
|
private var dirty: Boolean = true
|
|
@@ -91,8 +111,11 @@ class PolylineAnimator {
|
|
|
91
111
|
|
|
92
112
|
if (strokeColors.size > 1) {
|
|
93
113
|
applyGradientSpans(poly)
|
|
114
|
+
lastAppliedColor = null
|
|
94
115
|
} else {
|
|
95
|
-
|
|
116
|
+
val color = strokeColors.firstOrNull() ?: Color.BLACK
|
|
117
|
+
poly.color = color
|
|
118
|
+
lastAppliedColor = color
|
|
96
119
|
}
|
|
97
120
|
}
|
|
98
121
|
|
|
@@ -111,6 +134,9 @@ class PolylineAnimator {
|
|
|
111
134
|
interpolator = LinearInterpolator()
|
|
112
135
|
addUpdateListener { animation ->
|
|
113
136
|
animationProgress = animation.animatedValue as Float
|
|
137
|
+
val now = System.nanoTime()
|
|
138
|
+
if (now - lastUpdateNanos < MIN_UPDATE_INTERVAL_NANOS) return@addUpdateListener
|
|
139
|
+
lastUpdateNanos = now
|
|
114
140
|
updateAnimatedPolyline()
|
|
115
141
|
}
|
|
116
142
|
start()
|
|
@@ -182,6 +208,7 @@ class PolylineAnimator {
|
|
|
182
208
|
private fun stopAnimation() {
|
|
183
209
|
animator?.cancel()
|
|
184
210
|
animator = null
|
|
211
|
+
lastUpdateNanos = 0L
|
|
185
212
|
}
|
|
186
213
|
|
|
187
214
|
fun pause() {
|
|
@@ -222,6 +249,7 @@ class PolylineAnimator {
|
|
|
222
249
|
if (headDist <= tailDist) {
|
|
223
250
|
val point = coordinates.firstOrNull() ?: LatLng(0.0, 0.0)
|
|
224
251
|
poly.color = strokeColors.firstOrNull() ?: Color.BLACK
|
|
252
|
+
lastAppliedColor = null
|
|
225
253
|
poly.points = listOf(point, point)
|
|
226
254
|
return
|
|
227
255
|
}
|
|
@@ -246,12 +274,13 @@ class PolylineAnimator {
|
|
|
246
274
|
|
|
247
275
|
if (reusablePoints.size < 2) return
|
|
248
276
|
|
|
249
|
-
// Clear spans before setting points to prevent IndexOutOfBoundsException
|
|
250
|
-
// when the new points list is shorter than what existing spans reference
|
|
251
|
-
poly.color = strokeColors.firstOrNull() ?: Color.BLACK
|
|
252
|
-
poly.points = reusablePoints
|
|
253
|
-
|
|
254
277
|
if (strokeColors.size > 1) {
|
|
278
|
+
// Clear spans before setting points to prevent IndexOutOfBoundsException
|
|
279
|
+
// when the new points list is shorter than what existing spans reference
|
|
280
|
+
poly.color = strokeColors.firstOrNull() ?: Color.BLACK
|
|
281
|
+
lastAppliedColor = null
|
|
282
|
+
poly.points = reusablePoints
|
|
283
|
+
|
|
255
284
|
val segmentCount = reusablePoints.size - 1
|
|
256
285
|
val spanCount = min(segmentCount, MAX_ANIMATION_SPANS)
|
|
257
286
|
val segmentsPerSpan = segmentCount.toDouble() / spanCount
|
|
@@ -263,6 +292,13 @@ class PolylineAnimator {
|
|
|
263
292
|
reusableSpans.add(StyleSpan(StrokeStyle.colorBuilder(color).build(), segmentsPerSpan))
|
|
264
293
|
}
|
|
265
294
|
poly.setSpans(reusableSpans)
|
|
295
|
+
} else {
|
|
296
|
+
val color = strokeColors.firstOrNull() ?: Color.BLACK
|
|
297
|
+
if (lastAppliedColor != color) {
|
|
298
|
+
poly.color = color
|
|
299
|
+
lastAppliedColor = color
|
|
300
|
+
}
|
|
301
|
+
poly.points = reusablePoints
|
|
266
302
|
}
|
|
267
303
|
}
|
|
268
304
|
|
package/ios/LuggMapView.mm
CHANGED
|
@@ -248,18 +248,26 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
|
|
|
248
248
|
: -(NSInteger)_theme;
|
|
249
249
|
|
|
250
250
|
// Camera is part of the key so a reused staticKey with a different
|
|
251
|
-
// location/zoom can't serve a stale snapshot
|
|
251
|
+
// location/zoom can't serve a stale snapshot. The provider's camera (when
|
|
252
|
+
// available) reflects imperative camera changes, so a re-rendered
|
|
253
|
+
// snapshot is keyed on what it actually shows.
|
|
252
254
|
const auto &viewProps =
|
|
253
255
|
*std::static_pointer_cast<LuggMapViewProps const>(_props);
|
|
256
|
+
double latitude = viewProps.initialCoordinate.latitude;
|
|
257
|
+
double longitude = viewProps.initialCoordinate.longitude;
|
|
258
|
+
double zoom = viewProps.initialZoom;
|
|
259
|
+
if ([_provider isKindOfClass:[StaticMapProviderBase class]]) {
|
|
260
|
+
StaticMapProviderBase *staticProvider = (StaticMapProviderBase *)_provider;
|
|
261
|
+
latitude = staticProvider.coordinate.latitude;
|
|
262
|
+
longitude = staticProvider.coordinate.longitude;
|
|
263
|
+
zoom = staticProvider.zoom;
|
|
264
|
+
}
|
|
254
265
|
|
|
255
266
|
return [NSString
|
|
256
267
|
stringWithFormat:@"%@|%d|%@|%d|%ld|%.0fx%.0f|%.6f,%.6f,%.2f|%@",
|
|
257
268
|
_staticKey, (int)_providerType, _mapId, (int)_mapType,
|
|
258
|
-
(long)style, size.width, size.height,
|
|
259
|
-
|
|
260
|
-
viewProps.initialCoordinate.longitude,
|
|
261
|
-
viewProps.initialZoom,
|
|
262
|
-
NSStringFromUIEdgeInsets(_edgeInsets)];
|
|
269
|
+
(long)style, size.width, size.height, latitude,
|
|
270
|
+
longitude, zoom, NSStringFromUIEdgeInsets(_edgeInsets)];
|
|
263
271
|
}
|
|
264
272
|
|
|
265
273
|
- (void)startStaticContent {
|
|
@@ -322,14 +330,6 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
|
|
|
322
330
|
_provider = google;
|
|
323
331
|
}
|
|
324
332
|
|
|
325
|
-
if (_staticMode) {
|
|
326
|
-
NSString *cacheKey = [self staticSnapshotCacheKey];
|
|
327
|
-
if (cacheKey) {
|
|
328
|
-
((StaticMapProviderBase *)_provider).cachedBaseImage =
|
|
329
|
-
[StaticSnapshotCache() objectForKey:cacheKey];
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
333
|
_provider.delegate = self;
|
|
334
334
|
_provider.staticMode = _staticMode;
|
|
335
335
|
|
|
@@ -341,6 +341,16 @@ static NSCache<NSString *, UIImage *> *StaticSnapshotCache(void) {
|
|
|
341
341
|
initialCoordinate:coordinate
|
|
342
342
|
initialZoom:viewProps.initialZoom];
|
|
343
343
|
|
|
344
|
+
// After initializeMapInView so the cache key reads the provider's camera;
|
|
345
|
+
// the base render is async and picks up the cached image
|
|
346
|
+
if (_staticMode) {
|
|
347
|
+
NSString *cacheKey = [self staticSnapshotCacheKey];
|
|
348
|
+
if (cacheKey) {
|
|
349
|
+
((StaticMapProviderBase *)_provider).cachedBaseImage =
|
|
350
|
+
[StaticSnapshotCache() objectForKey:cacheKey];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
344
354
|
// Apply cached props after map view is created
|
|
345
355
|
[self applyProps];
|
|
346
356
|
[_provider setEdgeInsets:_edgeInsets oldEdgeInsets:UIEdgeInsetsZero];
|
|
@@ -74,6 +74,19 @@ static MKMapRect LuggStaticFittedMapRect(CLLocationCoordinate2D center,
|
|
|
74
74
|
return pin;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// Inverse of LuggStaticFittedMapRect: the pre-fit rect is square in map
|
|
78
|
+
// points with side latitudeDelta * worldHeight / (360 * cos(lat)), and the
|
|
79
|
+
// aspect fit scales it to the view's short side
|
|
80
|
+
- (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
|
|
81
|
+
CGSize size = self.projectedSize;
|
|
82
|
+
double side = mapPointsPerPoint * MIN(size.width, size.height);
|
|
83
|
+
double latitudeDelta = side * 360.0 *
|
|
84
|
+
cos(self.coordinate.latitude * M_PI / 180.0) /
|
|
85
|
+
MKMapSizeWorld.height;
|
|
86
|
+
double zoom = 0.5 + log2(360.0 / latitudeDelta);
|
|
87
|
+
return MAX(MIN(zoom, 28.0), 0.0);
|
|
88
|
+
}
|
|
89
|
+
|
|
77
90
|
- (UITraitCollection *)snapshotTraitCollection {
|
|
78
91
|
UITraitCollection *base = self.wrapperView.traitCollection;
|
|
79
92
|
UIUserInterfaceStyle style;
|
|
@@ -74,6 +74,12 @@ static void EnqueueStaticMapView(NSString *mapId, GMSMapView *mapView) {
|
|
|
74
74
|
[[UIImageView alloc] initWithImage:[GMSMarker markerImageWithColor:nil]];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
// Inverse of mapRectForSize
|
|
78
|
+
- (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
|
|
79
|
+
double zoom = log2(MKMapSizeWorld.width / (256.0 * mapPointsPerPoint));
|
|
80
|
+
return MAX(MIN(zoom, (double)kGMSMaxZoomLevel), (double)kGMSMinZoomLevel);
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
- (void)renderBaseMap {
|
|
78
84
|
if (_warmupMapView) {
|
|
79
85
|
// Retry a swap that couldn't run (e.g. view was off-window)
|
|
@@ -56,6 +56,11 @@ CGPoint LuggStaticPointForCoordinate(MKMapRect mapRect, CGSize size,
|
|
|
56
56
|
/// Overlay view shown for markers without a custom child view.
|
|
57
57
|
- (UIView *)newDefaultMarkerOverlayView;
|
|
58
58
|
|
|
59
|
+
/// Zoom level at which the base map renders at the given scale (map points
|
|
60
|
+
/// per view point) for the current coordinate and view size; the inverse of
|
|
61
|
+
/// mapRectForSize. Used by fitCoordinates.
|
|
62
|
+
- (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint;
|
|
63
|
+
|
|
59
64
|
/// Starts rendering the base map for the current projection (guaranteed
|
|
60
65
|
/// projectionReady, no cached image). Called after mount (once initial
|
|
61
66
|
/// props have applied), after a resize, and from resumeAnimations to retry
|
|
@@ -386,6 +386,25 @@ static NSInteger shapeZIndex(UIView *shape) {
|
|
|
386
386
|
[self renderBaseMapIfNeeded];
|
|
387
387
|
}
|
|
388
388
|
|
|
389
|
+
// Camera commands invalidate any cached, displayed or in-flight base
|
|
390
|
+
// render and re-render at the new camera
|
|
391
|
+
- (void)rerenderBaseMap {
|
|
392
|
+
[self cancelBaseRender];
|
|
393
|
+
_baseRenderDone = NO;
|
|
394
|
+
self.cachedBaseImage = nil;
|
|
395
|
+
|
|
396
|
+
// Without a layout yet, the first layout renders with the new camera
|
|
397
|
+
if (!_projectionReady)
|
|
398
|
+
return;
|
|
399
|
+
|
|
400
|
+
[self updateProjection];
|
|
401
|
+
|
|
402
|
+
__weak StaticMapProviderBase *weakSelf = self;
|
|
403
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
404
|
+
[weakSelf renderBaseMapIfNeeded];
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
|
|
389
408
|
- (void)renderBaseMapIfNeeded {
|
|
390
409
|
if (_baseRenderDone || !_projectionReady || !_wrapperView)
|
|
391
410
|
return;
|
|
@@ -436,6 +455,10 @@ static NSInteger shapeZIndex(UIView *shape) {
|
|
|
436
455
|
return [[UIView alloc] init];
|
|
437
456
|
}
|
|
438
457
|
|
|
458
|
+
- (double)zoomForMapPointsPerPoint:(double)mapPointsPerPoint {
|
|
459
|
+
return _zoom;
|
|
460
|
+
}
|
|
461
|
+
|
|
439
462
|
- (void)renderBaseMap {
|
|
440
463
|
}
|
|
441
464
|
|
|
@@ -758,12 +781,17 @@ static NSInteger shapeZIndex(UIView *shape) {
|
|
|
758
781
|
|
|
759
782
|
#pragma mark - Commands
|
|
760
783
|
|
|
761
|
-
// Static maps render once
|
|
784
|
+
// Static maps don't animate; commands re-render once at the final camera
|
|
762
785
|
|
|
763
786
|
- (void)moveCamera:(double)latitude
|
|
764
787
|
longitude:(double)longitude
|
|
765
788
|
zoom:(double)zoom
|
|
766
789
|
duration:(double)duration {
|
|
790
|
+
_coordinate = CLLocationCoordinate2DMake(latitude, longitude);
|
|
791
|
+
if (zoom > 0) {
|
|
792
|
+
_zoom = zoom;
|
|
793
|
+
}
|
|
794
|
+
[self rerenderBaseMap];
|
|
767
795
|
}
|
|
768
796
|
|
|
769
797
|
- (void)fitCoordinates:(NSArray *)coordinates
|
|
@@ -772,6 +800,46 @@ static NSInteger shapeZIndex(UIView *shape) {
|
|
|
772
800
|
edgeInsetsBottom:(double)edgeInsetsBottom
|
|
773
801
|
edgeInsetsRight:(double)edgeInsetsRight
|
|
774
802
|
duration:(double)duration {
|
|
803
|
+
if (coordinates.count == 0 || !_projectionReady)
|
|
804
|
+
return;
|
|
805
|
+
|
|
806
|
+
MKMapRect boundsRect = MKMapRectNull;
|
|
807
|
+
for (NSDictionary *coordinate in coordinates) {
|
|
808
|
+
MKMapPoint point = MKMapPointForCoordinate(
|
|
809
|
+
CLLocationCoordinate2DMake([coordinate[@"latitude"] doubleValue],
|
|
810
|
+
[coordinate[@"longitude"] doubleValue]));
|
|
811
|
+
boundsRect =
|
|
812
|
+
MKMapRectUnion(boundsRect, MKMapRectMake(point.x, point.y, 0, 0));
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
double availWidth =
|
|
816
|
+
MAX(_projectedSize.width - _edgeInsets.left - _edgeInsets.right -
|
|
817
|
+
edgeInsetsLeft - edgeInsetsRight,
|
|
818
|
+
1.0);
|
|
819
|
+
double availHeight =
|
|
820
|
+
MAX(_projectedSize.height - _edgeInsets.top - _edgeInsets.bottom -
|
|
821
|
+
edgeInsetsTop - edgeInsetsBottom,
|
|
822
|
+
1.0);
|
|
823
|
+
|
|
824
|
+
// Map points per view point at which the bounds fit the padded viewport
|
|
825
|
+
double scale = MAX(boundsRect.size.width / availWidth,
|
|
826
|
+
boundsRect.size.height / availHeight);
|
|
827
|
+
MKMapPoint center = MKMapPointMake(MKMapRectGetMidX(boundsRect),
|
|
828
|
+
MKMapRectGetMidY(boundsRect));
|
|
829
|
+
if (scale > 0) {
|
|
830
|
+
_coordinate = MKCoordinateForMapPoint(center);
|
|
831
|
+
_zoom = [self zoomForMapPointsPerPoint:scale];
|
|
832
|
+
} else {
|
|
833
|
+
// Coincident coordinates; keep the current zoom
|
|
834
|
+
scale = _mapRect.size.width / _projectedSize.width;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// Asymmetric padding shifts the visible center, like edge insets
|
|
838
|
+
center.x -= (edgeInsetsLeft - edgeInsetsRight) / 2.0 * scale;
|
|
839
|
+
center.y -= (edgeInsetsTop - edgeInsetsBottom) / 2.0 * scale;
|
|
840
|
+
_coordinate = MKCoordinateForMapPoint(center);
|
|
841
|
+
|
|
842
|
+
[self rerenderBaseMap];
|
|
775
843
|
}
|
|
776
844
|
|
|
777
845
|
@end
|