@lugg/maps 1.0.0-beta.9 → 1.1.0

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.
Files changed (64) hide show
  1. package/README.md +15 -100
  2. package/android/src/main/java/com/luggmaps/LuggCalloutView.kt +5 -0
  3. package/android/src/main/java/com/luggmaps/LuggMapView.kt +56 -2
  4. package/android/src/main/java/com/luggmaps/LuggMapViewManager.kt +18 -0
  5. package/android/src/main/java/com/luggmaps/LuggMapWrapperView.kt +36 -2
  6. package/android/src/main/java/com/luggmaps/LuggMarkerView.kt +7 -2
  7. package/android/src/main/java/com/luggmaps/LuggPolylineView.kt +2 -1
  8. package/android/src/main/java/com/luggmaps/core/GoogleMapProvider.kt +332 -42
  9. package/android/src/main/java/com/luggmaps/core/MapProviderDelegate.kt +6 -0
  10. package/android/src/main/java/com/luggmaps/core/PolylineAnimator.kt +61 -10
  11. package/ios/LuggMapView.mm +240 -10
  12. package/ios/LuggMapWrapperView.h +5 -0
  13. package/ios/LuggMapWrapperView.mm +18 -0
  14. package/ios/LuggMarkerView.mm +2 -0
  15. package/ios/core/AppleMapProvider.h +8 -0
  16. package/ios/core/AppleMapProvider.mm +63 -47
  17. package/ios/core/AppleStaticMapProvider.h +14 -0
  18. package/ios/core/AppleStaticMapProvider.mm +233 -0
  19. package/ios/core/GMSPolylineAnimator.m +40 -15
  20. package/ios/core/GoogleMapProvider.h +10 -0
  21. package/ios/core/GoogleMapProvider.mm +67 -54
  22. package/ios/core/GoogleStaticMapProvider.h +21 -0
  23. package/ios/core/GoogleStaticMapProvider.mm +217 -0
  24. package/ios/core/MKPolylineAnimator.m +67 -43
  25. package/ios/core/MapEdgeInsets.h +18 -0
  26. package/ios/core/MapProviderDelegate.h +11 -0
  27. package/ios/core/StaticMapProviderBase.h +80 -0
  28. package/ios/core/StaticMapProviderBase.mm +847 -0
  29. package/ios/events/CameraIdleEvent.h +4 -2
  30. package/ios/events/CameraMoveEvent.h +4 -2
  31. package/ios/events/CirclePressEvent.h +4 -2
  32. package/ios/events/GroundOverlayPressEvent.h +4 -2
  33. package/ios/events/LongPressEvent.h +4 -2
  34. package/ios/events/MarkerDragEvent.h +12 -6
  35. package/ios/events/MarkerPressEvent.h +4 -2
  36. package/ios/events/PolygonPressEvent.h +4 -2
  37. package/ios/events/PressEvent.h +4 -2
  38. package/ios/events/ReadyEvent.h +4 -2
  39. package/ios/events/TileOverlayPressEvent.h +4 -2
  40. package/ios/extensions/MKMapView+Zoom.h +20 -0
  41. package/ios/extensions/MKMapView+Zoom.m +28 -18
  42. package/ios/extensions/UIView+Snapshot.h +16 -0
  43. package/ios/extensions/UIView+Snapshot.m +27 -0
  44. package/lib/module/MapView.js +12 -0
  45. package/lib/module/MapView.js.map +1 -1
  46. package/lib/module/MapView.web.js +29 -10
  47. package/lib/module/MapView.web.js.map +1 -1
  48. package/lib/module/fabric/LuggMapViewNativeComponent.ts +10 -1
  49. package/lib/typescript/src/MapView.d.ts +1 -0
  50. package/lib/typescript/src/MapView.d.ts.map +1 -1
  51. package/lib/typescript/src/MapView.types.d.ts +32 -0
  52. package/lib/typescript/src/MapView.types.d.ts.map +1 -1
  53. package/lib/typescript/src/MapView.web.d.ts.map +1 -1
  54. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts +4 -0
  55. package/lib/typescript/src/fabric/LuggMapViewNativeComponent.d.ts.map +1 -1
  56. package/package.json +24 -14
  57. package/src/MapView.tsx +14 -0
  58. package/src/MapView.types.ts +32 -0
  59. package/src/MapView.web.tsx +39 -14
  60. package/src/fabric/LuggMapViewNativeComponent.ts +10 -1
  61. package/plugin/build/withMapsAndroid.d.ts +0 -5
  62. package/plugin/build/withMapsAndroid.js +0 -15
  63. package/plugin/build/withMapsIOS.d.ts +0 -5
  64. package/plugin/build/withMapsIOS.js +0 -27
package/README.md CHANGED
@@ -2,82 +2,28 @@
2
2
 
3
3
  Universal maps for your React Native apps 📍
4
4
 
5
- <img alt="@lugg/maps" src="docs/preview.gif" width="720" />
5
+ <img alt="@lugg/maps" src="docs/public/preview.gif" width="720" />
6
6
 
7
- > [!IMPORTANT]
8
- > This library is currently under heavy development. APIs may change without notice.
7
+ ## Documentation
9
8
 
10
- ## Installation
9
+ Full documentation lives at **[maps.lodev09.com](https://maps.lodev09.com)**.
11
10
 
12
- ```sh
13
- npm install @lugg/maps
14
- ```
15
-
16
- ### Expo
17
-
18
- Add the plugin to your `app.json`:
19
-
20
- ```json
21
- {
22
- "expo": {
23
- "plugins": [
24
- [
25
- "@lugg/maps",
26
- {
27
- "iosGoogleMapsApiKey": "YOUR_IOS_API_KEY",
28
- "androidGoogleMapsApiKey": "YOUR_ANDROID_API_KEY"
29
- }
30
- ]
31
- ]
32
- }
33
- }
34
- ```
35
-
36
- ### Bare React Native
37
-
38
- #### iOS
39
-
40
- Add your Google Maps API key to `AppDelegate.swift`:
41
-
42
- ```swift
43
- import GoogleMaps
44
-
45
- // In application(_:didFinishLaunchingWithOptions:)
46
- GMSServices.provideAPIKey("YOUR_API_KEY")
47
- ```
48
-
49
- #### Android
11
+ - [Installation](https://maps.lodev09.com/installation) - Expo, bare React Native, and web setup
12
+ - [Usage](https://maps.lodev09.com/usage) - Render your first map
13
+ - [MapView](https://maps.lodev09.com/components/map-view) - Props, camera methods, events, static maps
14
+ - [Marker](https://maps.lodev09.com/components/marker) - Custom views, callouts, dragging
15
+ - [Polyline](https://maps.lodev09.com/components/polyline), [Polygon](https://maps.lodev09.com/components/polygon), [Circle](https://maps.lodev09.com/components/circle) - Shapes
16
+ - [GeoJson](https://maps.lodev09.com/components/geojson), [GroundOverlay](https://maps.lodev09.com/components/ground-overlay), [TileOverlay](https://maps.lodev09.com/components/tile-overlay) - Data and overlays
17
+ - [Types](https://maps.lodev09.com/types) - `Coordinate`, `Point`, `EdgeInsets`
50
18
 
51
- Add your Google Maps API key to `AndroidManifest.xml`:
19
+ ## Quick start
52
20
 
53
- ```xml
54
- <application>
55
- <meta-data
56
- android:name="com.google.android.geo.API_KEY"
57
- android:value="YOUR_API_KEY" />
58
- </application>
59
- ```
60
-
61
- ### Web
62
-
63
- Wrap your app with `MapProvider` and pass your Google Maps API key:
64
-
65
- ```tsx
66
- import { MapProvider } from '@lugg/maps';
67
-
68
- function App() {
69
- return (
70
- <MapProvider apiKey="YOUR_WEB_API_KEY">
71
- {/* Your app */}
72
- </MapProvider>
73
- );
74
- }
21
+ ```sh
22
+ npm install @lugg/maps
75
23
  ```
76
24
 
77
- ## Usage
78
-
79
25
  ```tsx
80
- import { MapView, Marker, Polyline, Polygon } from '@lugg/maps';
26
+ import { MapView, Marker } from '@lugg/maps';
81
27
 
82
28
  <MapView
83
29
  style={{ flex: 1 }}
@@ -89,41 +35,10 @@ import { MapView, Marker, Polyline, Polygon } from '@lugg/maps';
89
35
  coordinate={{ latitude: 37.7749, longitude: -122.4194 }}
90
36
  title="San Francisco"
91
37
  />
92
- <Polyline
93
- coordinates={[
94
- { latitude: 37.7749, longitude: -122.4194 },
95
- { latitude: 37.8049, longitude: -122.4094 },
96
- ]}
97
- strokeWidth={3}
98
- />
99
- <Polygon
100
- coordinates={[
101
- { latitude: 37.784, longitude: -122.428 },
102
- { latitude: 37.784, longitude: -122.422 },
103
- { latitude: 37.779, longitude: -122.422 },
104
- { latitude: 37.779, longitude: -122.428 },
105
- ]}
106
- fillColor="rgba(66, 133, 244, 0.3)"
107
- strokeColor="#4285F4"
108
- strokeWidth={2}
109
- />
110
38
  </MapView>
111
39
  ```
112
40
 
113
- ## Components
114
-
115
- - [MapView](docs/MAPVIEW.md) - Main map component
116
- - [Marker](docs/MARKER.md) - Map markers with callout support
117
- - [Polyline](docs/POLYLINE.md) - Draw lines on the map
118
- - [Polygon](docs/POLYGON.md) - Draw filled shapes on the map
119
- - [Circle](docs/CIRCLE.md) - Draw circular overlays on the map
120
- - [GeoJson](docs/GEOJSON.md) - Render GeoJSON data on the map
121
- - [GroundOverlay](docs/GROUND_OVERLAY.md) - Display images on the map
122
- - [TileOverlay](docs/TILE_OVERLAY.md) - Display custom tile layers on the map
123
-
124
- ## Types
125
-
126
- See [Types](docs/TYPES.md) for common type definitions (`Coordinate`, `Point`, `EdgeInsets`).
41
+ Google Maps needs an API key on every platform. See [Installation](https://maps.lodev09.com/installation) for Expo, iOS, Android, and web setup.
127
42
 
128
43
  ## Contributing
129
44
 
@@ -4,6 +4,7 @@ import android.content.Context
4
4
  import android.graphics.Bitmap
5
5
  import android.graphics.Canvas
6
6
  import android.view.View
7
+ import android.view.accessibility.AccessibilityEvent
7
8
  import androidx.core.graphics.createBitmap
8
9
  import androidx.core.view.isNotEmpty
9
10
  import com.facebook.react.uimanager.PointerEvents
@@ -51,6 +52,10 @@ class LuggCalloutView(context: Context) : ReactViewGroup(context) {
51
52
 
52
53
  override fun getChildAt(index: Int): View? = contentView.getChildAt(index)
53
54
 
55
+ // no-op: avoid accessibility traversal of detached callout children
56
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {}
57
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
58
+
54
59
  fun createContentBitmap(): Bitmap? {
55
60
  var maxWidth = 0
56
61
  var maxHeight = 0
@@ -1,6 +1,7 @@
1
1
  package com.luggmaps
2
2
 
3
3
  import android.annotation.SuppressLint
4
+ import android.content.res.Configuration
4
5
  import android.view.View
5
6
  import android.view.View.VISIBLE
6
7
  import com.facebook.react.uimanager.ThemedReactContext
@@ -64,6 +65,8 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
64
65
  private var scrollEnabled: Boolean = true
65
66
  private var rotateEnabled: Boolean = true
66
67
  private var pitchEnabled: Boolean = true
68
+ private var compassEnabled: Boolean = true
69
+ private var staticMode: Boolean = false
67
70
  private var userLocationEnabled: Boolean = false
68
71
  private var userLocationButtonEnabled: Boolean = false
69
72
  private var poiEnabled: Boolean = true
@@ -128,17 +131,22 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
128
131
 
129
132
  // region Provider Initialization
130
133
 
131
- private fun initializeProvider() {
134
+ private fun initializeProvider(latitude: Double = initialLatitude, longitude: Double = initialLongitude, zoom: Float = initialZoom) {
132
135
  if (provider != null || mapWrapperView == null) return
133
136
 
137
+ if (staticMode) {
138
+ applyStaticPlaceholder()
139
+ }
140
+
134
141
  val google = GoogleMapProvider(context)
135
142
  google.mapId = mapId
143
+ google.staticMode = staticMode
136
144
  google.delegate = this
137
145
  provider = google
138
146
 
139
147
  applyProps()
140
148
 
141
- google.initializeMap(mapWrapperView!!, initialLatitude, initialLongitude, initialZoom)
149
+ google.initializeMap(mapWrapperView!!, latitude, longitude, zoom)
142
150
 
143
151
  // Flush children mounted before provider was created
144
152
  for (i in 0 until childCount) {
@@ -153,6 +161,23 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
153
161
  }
154
162
  }
155
163
 
164
+ // Shown while the map loads, until the map covers it
165
+ private fun applyStaticPlaceholder() {
166
+ // A user-provided background acts as the placeholder
167
+ if (background != null) return
168
+
169
+ val isDark = when (theme) {
170
+ "dark" -> true
171
+
172
+ "light" -> false
173
+
174
+ else ->
175
+ (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) ==
176
+ Configuration.UI_MODE_NIGHT_YES
177
+ }
178
+ mapWrapperView?.setBackgroundColor(if (isDark) COLOR_PLACEHOLDER_DARK else COLOR_PLACEHOLDER_LIGHT)
179
+ }
180
+
156
181
  // endregion
157
182
 
158
183
  // region MapProviderDelegate
@@ -186,6 +211,7 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
186
211
  provider?.setScrollEnabled(scrollEnabled)
187
212
  provider?.setRotateEnabled(rotateEnabled)
188
213
  provider?.setPitchEnabled(pitchEnabled)
214
+ provider?.setCompassEnabled(compassEnabled)
189
215
  provider?.setUserLocationEnabled(userLocationEnabled)
190
216
  provider?.setUserLocationButtonEnabled(userLocationButtonEnabled)
191
217
  provider?.setMapType(mapType)
@@ -243,6 +269,16 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
243
269
  provider?.setPitchEnabled(enabled)
244
270
  }
245
271
 
272
+ fun setCompassEnabled(enabled: Boolean) {
273
+ if (compassEnabled == enabled) return
274
+ compassEnabled = enabled
275
+ provider?.setCompassEnabled(enabled)
276
+ }
277
+
278
+ fun setStaticMode(enabled: Boolean) {
279
+ staticMode = enabled
280
+ }
281
+
246
282
  fun setUserLocationEnabled(enabled: Boolean) {
247
283
  if (userLocationEnabled == enabled) return
248
284
  userLocationEnabled = enabled
@@ -335,5 +371,23 @@ class LuggMapView(private val reactContext: ThemedReactContext) :
335
371
  provider?.fitCoordinates(coordinates, edgeInsetsTop, edgeInsetsLeft, edgeInsetsBottom, edgeInsetsRight, duration)
336
372
  }
337
373
 
374
+ fun reload() {
375
+ val current = provider ?: return
376
+ val latitude = current.cameraLatitude
377
+ val longitude = current.cameraLongitude
378
+ val zoom = current.cameraZoom
379
+
380
+ current.destroy()
381
+ provider = null
382
+ if (isAttachedToWindow) {
383
+ initializeProvider(latitude, longitude, zoom)
384
+ }
385
+ }
386
+
338
387
  // endregion
388
+
389
+ companion object {
390
+ private const val COLOR_PLACEHOLDER_LIGHT = 0xFFF2F2F7.toInt()
391
+ private const val COLOR_PLACEHOLDER_DARK = 0xFF2C2C2E.toInt()
392
+ }
339
393
  }
@@ -132,6 +132,20 @@ class LuggMapViewManager :
132
132
  view.setPitchEnabled(value)
133
133
  }
134
134
 
135
+ @ReactProp(name = "compassEnabled", defaultBoolean = true)
136
+ override fun setCompassEnabled(view: LuggMapView, value: Boolean) {
137
+ view.setCompassEnabled(value)
138
+ }
139
+
140
+ @ReactProp(name = "staticMode", defaultBoolean = false)
141
+ override fun setStaticMode(view: LuggMapView, value: Boolean) {
142
+ view.setStaticMode(value)
143
+ }
144
+
145
+ // iOS only - lite mode doesn't need snapshot caching
146
+ @ReactProp(name = "staticKey")
147
+ override fun setStaticKey(view: LuggMapView, value: String?) = Unit
148
+
135
149
  @ReactProp(name = "userLocationEnabled", defaultBoolean = false)
136
150
  override fun setUserLocationEnabled(view: LuggMapView, value: Boolean) {
137
151
  view.setUserLocationEnabled(value)
@@ -259,6 +273,10 @@ class LuggMapViewManager :
259
273
  )
260
274
  }
261
275
 
276
+ override fun reload(view: LuggMapView) {
277
+ view.reload()
278
+ }
279
+
262
280
  companion object {
263
281
  const val NAME = "LuggMapView"
264
282
  }
@@ -2,6 +2,7 @@ package com.luggmaps
2
2
 
3
3
  import android.annotation.SuppressLint
4
4
  import android.view.MotionEvent
5
+ import android.view.View
5
6
  import com.facebook.react.uimanager.ThemedReactContext
6
7
  import com.facebook.react.views.view.ReactViewGroup
7
8
 
@@ -10,11 +11,38 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
10
11
 
11
12
  var touchEventHandler: ((MotionEvent) -> Unit)? = null
12
13
 
14
+ // React Native never runs an Android layout pass on Yoga-managed views, so
15
+ // a child's requestLayout (e.g. a lite mode MapView displaying its bitmap
16
+ // once loaded) would otherwise never result in a measure/layout. Live maps
17
+ // don't need this - their GL surface renders continuously once sized.
18
+ var relayoutChildOnRequest: Boolean = false
19
+
20
+ // Invoked on layout once the view has a non-zero size, so the map
21
+ // provider can create a map sized to the view
22
+ var onLayoutReady: (() -> Unit)? = null
23
+
24
+ private val measureAndLayoutChild = Runnable { layoutChild(width, height) }
25
+
13
26
  override fun dispatchTouchEvent(event: MotionEvent): Boolean {
14
27
  touchEventHandler?.invoke(event)
15
28
  return super.dispatchTouchEvent(event)
16
29
  }
17
30
 
31
+ override fun requestLayout() {
32
+ super.requestLayout()
33
+ if (relayoutChildOnRequest) {
34
+ post(measureAndLayoutChild)
35
+ }
36
+ }
37
+
38
+ override fun onViewAdded(child: View) {
39
+ super.onViewAdded(child)
40
+ // A replacement map needs sizing even when Yoga's layout is unchanged.
41
+ if (indexOfChild(child) == 0) {
42
+ layoutChild(width, height)
43
+ }
44
+ }
45
+
18
46
  override fun onLayout(
19
47
  changed: Boolean,
20
48
  left: Int,
@@ -23,8 +51,14 @@ class LuggMapWrapperView(context: ThemedReactContext) : ReactViewGroup(context)
23
51
  bottom: Int
24
52
  ) {
25
53
  super.onLayout(changed, left, top, right, bottom)
26
- val w = right - left
27
- val h = bottom - top
54
+ layoutChild(right - left, bottom - top)
55
+ if (right - left > 0 && bottom - top > 0) {
56
+ onLayoutReady?.invoke()
57
+ }
58
+ }
59
+
60
+ private fun layoutChild(w: Int, h: Int) {
61
+ if (w <= 0 || h <= 0) return
28
62
  getChildAt(0)?.let {
29
63
  it.measure(
30
64
  MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
@@ -3,12 +3,13 @@ package com.luggmaps
3
3
  import android.content.Context
4
4
  import android.graphics.Canvas
5
5
  import android.view.View
6
+ import android.view.accessibility.AccessibilityEvent
6
7
  import androidx.core.graphics.createBitmap
7
8
  import androidx.core.view.isNotEmpty
8
9
  import com.facebook.react.views.view.ReactViewGroup
9
- import com.google.android.gms.maps.model.AdvancedMarker
10
10
  import com.google.android.gms.maps.model.BitmapDescriptor
11
11
  import com.google.android.gms.maps.model.BitmapDescriptorFactory
12
+ import com.google.android.gms.maps.model.Marker
12
13
  import com.luggmaps.events.MarkerDragEvent
13
14
  import com.luggmaps.events.MarkerPressEvent
14
15
  import com.luggmaps.extensions.dispatchEvent
@@ -37,7 +38,7 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
37
38
  private set
38
39
 
39
40
  var delegate: LuggMarkerViewDelegate? = null
40
- var marker: AdvancedMarker? = null
41
+ var marker: Marker? = null
41
42
 
42
43
  var anchorX: Float = 0.5f
43
44
  private set
@@ -218,6 +219,10 @@ class LuggMarkerView(context: Context) : ReactViewGroup(context) {
218
219
  return null
219
220
  }
220
221
 
222
+ // no-op: avoid accessibility traversal of detached marker children
223
+ override fun addChildrenForAccessibility(outChildren: ArrayList<View>) {}
224
+ override fun dispatchPopulateAccessibilityEvent(event: AccessibilityEvent): Boolean = false
225
+
221
226
  override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
222
227
  super.onSizeChanged(w, h, oldw, oldh)
223
228
  didLayout = false
@@ -8,6 +8,7 @@ import com.google.android.gms.maps.model.Polyline
8
8
 
9
9
  interface LuggPolylineViewDelegate {
10
10
  fun polylineViewDidUpdate(polylineView: LuggPolylineView)
11
+ fun polylineViewDidDrop(polylineView: LuggPolylineView)
11
12
  }
12
13
 
13
14
  data class AnimatedOptions(val duration: Long = 2150L, val easing: String = "linear", val trailLength: Float = 1f, val delay: Long = 0L)
@@ -67,8 +68,8 @@ class LuggPolylineView(context: Context) : ReactViewGroup(context) {
67
68
  }
68
69
 
69
70
  fun onDropViewInstance() {
71
+ delegate?.polylineViewDidDrop(this)
70
72
  delegate = null
71
- polyline?.remove()
72
73
  polyline = null
73
74
  }
74
75
  }