diff --git a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt index 45e2104..a934ae7 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapCircleBuilder.kt @@ -20,15 +20,38 @@ class MapCircleBuilder { } fun update( - circle: Circle, + prev: RNCircle, next: RNCircle, + circle: Circle, ) { - circle.center = next.center.toLatLng() - circle.radius = next.radius - circle.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f - circle.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK - circle.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT - circle.isClickable = next.pressable ?: false - circle.zIndex = next.zIndex?.toFloat() ?: 0f + if (prev.center.latitude != next.center.latitude || + prev.center.longitude != next.center.longitude + ) { + circle.center = next.center.toLatLng() + } + + if (prev.radius != next.radius) { + circle.radius = next.radius + } + + if (prev.strokeWidth != next.strokeWidth) { + circle.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f + } + + if (prev.strokeColor != next.strokeColor) { + circle.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK + } + + if (prev.fillColor != next.fillColor) { + circle.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT + } + + if (prev.pressable != next.pressable) { + circle.isClickable = next.pressable ?: false + } + + if (prev.zIndex != next.zIndex) { + circle.zIndex = next.zIndex?.toFloat() ?: 0f + } } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt index ddb48dd..6db1b9c 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapMarkerBuilder.kt @@ -54,33 +54,84 @@ class MapMarkerBuilder( } fun update( - marker: Marker, prev: RNMarker, next: RNMarker, + marker: Marker, ) { - marker.position = - next.coordinate.toLatLng() + if (prev.coordinate.latitude != next.coordinate.latitude || + prev.coordinate.longitude != next.coordinate.longitude + ) { + marker.position = next.coordinate.toLatLng() + } if (!prev.markerStyleEquals(next)) { buildIconAsync(marker.id, next) { icon -> marker.setIcon(icon) + if (prev.infoWindowAnchor?.x != next.infoWindowAnchor?.x || + prev.infoWindowAnchor?.y != next.infoWindowAnchor?.y + ) { + marker.setInfoWindowAnchor( + (next.infoWindowAnchor?.x ?: 0.5f).toFloat(), + (next.infoWindowAnchor?.y ?: 0f).toFloat(), + ) + } + + if (prev.anchor?.x != next.anchor?.x || + prev.anchor?.y != next.anchor?.y + ) { + marker.setAnchor( + (next.anchor?.x ?: 0.5f).toFloat(), + (next.anchor?.y ?: 1.0f).toFloat(), + ) + } } + } else { + if (prev.infoWindowAnchor?.x != next.infoWindowAnchor?.x || + prev.infoWindowAnchor?.y != next.infoWindowAnchor?.y + ) { + marker.setInfoWindowAnchor( + (next.infoWindowAnchor?.x ?: 0.5f).toFloat(), + (next.infoWindowAnchor?.y ?: 0f).toFloat(), + ) + } + + if (prev.anchor?.x != next.anchor?.x || + prev.anchor?.y != next.anchor?.y + ) { + marker.setAnchor( + (next.anchor?.x ?: 0.5f).toFloat(), + (next.anchor?.y ?: 1.0f).toFloat(), + ) + } + } + + if (prev.title != next.title) { + marker.title = next.title + } + + if (prev.snippet != next.snippet) { + marker.snippet = next.snippet + } + + if (prev.opacity != next.opacity) { + marker.alpha = next.opacity?.toFloat() ?: 1f + } + + if (prev.flat != next.flat) { + marker.isFlat = next.flat ?: false + } + + if (prev.draggable != next.draggable) { + marker.isDraggable = next.draggable ?: false + } + + if (prev.rotation != next.rotation) { + marker.rotation = next.rotation?.toFloat() ?: 0f + } + + if (prev.zIndex != next.zIndex) { + marker.zIndex = next.zIndex?.toFloat() ?: 0f } - marker.title = next.title - marker.snippet = next.snippet - marker.alpha = next.opacity?.toFloat() ?: 1f - marker.isFlat = next.flat ?: false - marker.isDraggable = next.draggable ?: false - marker.rotation = next.rotation?.toFloat() ?: 0f - marker.setInfoWindowAnchor( - (next.infoWindowAnchor?.x ?: 0.5).toFloat(), - (next.infoWindowAnchor?.y ?: 0).toFloat(), - ) - marker.setAnchor( - (next.anchor?.x ?: 0.5).toFloat(), - (next.anchor?.y ?: 1.0).toFloat(), - ) - marker.zIndex = next.zIndex?.toFloat() ?: 0f } fun buildIconAsync( diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt index a69e88e..9601d2c 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolygonBuilder.kt @@ -27,21 +27,60 @@ class MapPolygonBuilder { } fun update( - poly: Polygon, + prev: RNPolygon, next: RNPolygon, + poly: Polygon, ) { - poly.points = - next.coordinates.map { - it.toLatLng() - } - poly.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT - poly.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK - poly.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f - poly.isClickable = next.pressable ?: false - poly.isGeodesic = next.geodesic ?: false - poly.holes = next.holes?.map { hole -> - hole.coordinates.map { it.toLatLng() } - } ?: emptyList() - poly.zIndex = next.zIndex?.toFloat() ?: 0f + val coordsChanged = + prev.coordinates.size != next.coordinates.size || + !prev.coordinates.zip(next.coordinates).all { (a, b) -> + a.latitude == b.latitude && a.longitude == b.longitude + } + + if (coordsChanged) { + poly.points = next.coordinates.map { it.toLatLng() } + } + + val prevHoles = prev.holes?.toList() ?: emptyList() + val nextHoles = next.holes?.toList() ?: emptyList() + val holesChanged = + prevHoles.size != nextHoles.size || + !prevHoles.zip(nextHoles).all { (ha, hb) -> + ha.coordinates.size == hb.coordinates.size && + ha.coordinates.zip(hb.coordinates).all { (a, b) -> + a.latitude == b.latitude && a.longitude == b.longitude + } + } + + if (holesChanged) { + poly.holes = + nextHoles.map { hole -> + hole.coordinates.map { it.toLatLng() } + } + } + + if (prev.fillColor != next.fillColor) { + poly.fillColor = next.fillColor?.toColor() ?: Color.TRANSPARENT + } + + if (prev.strokeColor != next.strokeColor) { + poly.strokeColor = next.strokeColor?.toColor() ?: Color.BLACK + } + + if (prev.strokeWidth != next.strokeWidth) { + poly.strokeWidth = next.strokeWidth?.dpToPx() ?: 1f + } + + if (prev.pressable != next.pressable) { + poly.isClickable = next.pressable ?: false + } + + if (prev.geodesic != next.geodesic) { + poly.isGeodesic = next.geodesic ?: false + } + + if (prev.zIndex != next.zIndex) { + poly.zIndex = next.zIndex?.toFloat() ?: 0f + } } } diff --git a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt index d4bb76f..2802570 100644 --- a/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt +++ b/android/src/main/java/com/rngooglemapsplus/MapPolylineBuilder.kt.kt @@ -31,19 +31,52 @@ class MapPolylineBuilder { } fun update( - polyline: Polyline, + prev: RNPolyline, next: RNPolyline, + polyline: Polyline, ) { - polyline.points = next.coordinates.map { it.toLatLng() } - polyline.width = next.width?.dpToPx() ?: 1f - val cap = mapLineCap(next.lineCap ?: RNLineCapType.BUTT) - polyline.startCap = cap - polyline.endCap = cap - polyline.jointType = mapLineJoin(next.lineJoin ?: RNLineJoinType.MITER) - polyline.color = next.color?.toColor() ?: Color.BLACK - polyline.isClickable = next.pressable ?: false - polyline.isGeodesic = next.geodesic ?: false - polyline.zIndex = next.zIndex?.toFloat() ?: 0f + val coordsChanged = + prev.coordinates.size != next.coordinates.size || + !prev.coordinates.zip(next.coordinates).all { (a, b) -> + a.latitude == b.latitude && a.longitude == b.longitude + } + + if (coordsChanged) { + polyline.points = next.coordinates.map { it.toLatLng() } + } + + if (prev.width != next.width) { + polyline.width = next.width?.dpToPx() ?: 1f + } + + val newCap = mapLineCap(next.lineCap ?: RNLineCapType.BUTT) + val prevCap = mapLineCap(prev.lineCap ?: RNLineCapType.BUTT) + if (newCap != prevCap) { + polyline.startCap = newCap + polyline.endCap = newCap + } + + val newJoin = mapLineJoin(next.lineJoin ?: RNLineJoinType.MITER) + val prevJoin = mapLineJoin(prev.lineJoin ?: RNLineJoinType.MITER) + if (newJoin != prevJoin) { + polyline.jointType = newJoin + } + + if (prev.color != next.color) { + polyline.color = next.color?.toColor() ?: Color.BLACK + } + + if (prev.pressable != next.pressable) { + polyline.isClickable = next.pressable ?: false + } + + if (prev.geodesic != next.geodesic) { + polyline.isGeodesic = next.geodesic ?: false + } + + if (prev.zIndex != next.zIndex) { + polyline.zIndex = next.zIndex?.toFloat() ?: 0f + } } private fun mapLineCap(type: RNLineCapType?): Cap = diff --git a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt index 899dbf4..9d0df42 100644 --- a/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt +++ b/android/src/main/java/com/rngooglemapsplus/RNGoogleMapsPlusView.kt @@ -157,7 +157,7 @@ class RNGoogleMapsPlusView( } else if (!prev.markerEquals(next)) { view.updateMarker(id) { marker -> onUi { - markerBuilder.update(marker, next, prev) + markerBuilder.update(prev, next, marker) } } } @@ -181,7 +181,7 @@ class RNGoogleMapsPlusView( } else if (!prev.polylineEquals(next)) { view.updatePolyline(id) { polyline -> onUi { - polylineBuilder.update(polyline, next) + polylineBuilder.update(prev, next, polyline) } } } @@ -205,7 +205,7 @@ class RNGoogleMapsPlusView( view.addPolygon(id, polygonBuilder.build(next)) } else if (!prev.polygonEquals(next)) { view.updatePolygon(id) { polygon -> - onUi { polygonBuilder.update(polygon, next) } + onUi { polygonBuilder.update(prev, next, polygon) } } } } @@ -229,7 +229,7 @@ class RNGoogleMapsPlusView( } else if (!prev.circleEquals(next)) { view.updateCircle(id) { circle -> onUi { - circleBuilder.update(circle, next) + circleBuilder.update(prev, next, circle) } } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ff231e0..c901db2 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -1819,6 +1819,8 @@ PODS: - React-RCTFBReactNativeSpec - ReactCommon/turbomodule/core - SocketRocket + - react-native-clusterer (4.0.0): + - React-Core - react-native-safe-area-context (5.6.1): - boost - DoubleConversion @@ -2795,6 +2797,7 @@ DEPENDENCIES: - React-logger (from `../node_modules/react-native/ReactCommon/logger`) - React-Mapbuffer (from `../node_modules/react-native/ReactCommon`) - React-microtasksnativemodule (from `../node_modules/react-native/ReactCommon/react/nativemodule/microtasks`) + - react-native-clusterer (from `../node_modules/react-native-clusterer`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - React-NativeModulesApple (from `../node_modules/react-native/ReactCommon/react/nativemodule/core/platform/ios`) - React-oscompat (from `../node_modules/react-native/ReactCommon/oscompat`) @@ -2928,6 +2931,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native/ReactCommon" React-microtasksnativemodule: :path: "../node_modules/react-native/ReactCommon/react/nativemodule/microtasks" + react-native-clusterer: + :path: "../node_modules/react-native-clusterer" react-native-safe-area-context: :path: "../node_modules/react-native-safe-area-context" React-NativeModulesApple: @@ -3052,6 +3057,7 @@ SPEC CHECKSUMS: React-logger: 30adf849117e87cf86e88dca1824bb0f18f87e10 React-Mapbuffer: 2a5edca6905cb1b3a40fb7ed3f4496df4f1bc60e React-microtasksnativemodule: 6d775fdf71445f58dbedbd66ed9cb08b48ae2797 + react-native-clusterer: a9526b8fb1d6be10cd9a6d05d7d8b982da7c6abc react-native-safe-area-context: ee1e8e2a7abf737a8d4d9d1a5686a7f2e7466236 React-NativeModulesApple: b2ee5b48020439fd81d1fd9cba40ebf0c3af5636 React-oscompat: 80ca388c4831481cd03a6b45ecfc82739ca9a95e diff --git a/example/package.json b/example/package.json index 4c81cd0..5b9d365 100644 --- a/example/package.json +++ b/example/package.json @@ -16,6 +16,7 @@ "@react-navigation/stack": "7.4.9", "react": "19.1.1", "react-native": "0.82.0", + "react-native-clusterer": "4.0.0", "react-native-gesture-handler": "2.28.0", "react-native-google-maps-plus": "workspace:*", "react-native-nitro-modules": "0.30.0", diff --git a/example/src/App.tsx b/example/src/App.tsx index 30c2af2..310e45c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -24,6 +24,7 @@ import IndoorLevelMapScreen from './screens/IndoorLevelMapScreen'; import CameraTestScreen from './screens/CameraTestScreen'; import type { RootStackParamList } from './types/navigation'; import SnapshotTestScreen from './screens/SnaptshotTestScreen'; +import ClusteringScreen from './screens/ClsuteringScreen'; const Stack = createStackNavigator(); @@ -112,6 +113,11 @@ export default function App() { component={SnapshotTestScreen} options={{ title: 'Snapshot test' }} /> + (null); + const [coordinates] = useState( + Array.from({ length: 500 }, () => + randomCoordinates(37.7749, -122.4194, 0.2) + ) + ); + const [region, setRegion] = useState({ + center: { + latitude: 37.7749, + longitude: -122.4194, + }, + latitudeDelta: 0.4, + longitudeDelta: 0.4, + }); + + const mapDimensions = useMemo(() => ({ width: 400, height: 800 }), []); + + const data = useMemo< + Array< + Supercluster.PointFeature<{ + id: string; + svgIcon: RNMarkerSvg; + }> + > + >( + () => + coordinates.map((e, i) => { + return { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [e.longitude, e.latitude], + }, + properties: { + id: `sf-${i}`, + svgIcon: { + width: 32, + height: 44, + svgString: ` + + + + + + `, + }, + }, + }; + }), + [coordinates] + ); + + const clusterRegion = useMemo( + () => ({ + latitude: region.center.latitude, + longitude: region.center.longitude, + latitudeDelta: region.latitudeDelta, + longitudeDelta: region.longitudeDelta, + }), + [region] + ); + + const clusterOptions = useMemo( + () => ({ radius: 60, maxZoom: 16, minZoom: 0 }), + [] + ); + + const [points] = useClusterer( + data, + mapDimensions, + clusterRegion, + clusterOptions + ); + + const markers: RNMarker[] = useMemo(() => { + return points.map((feature, i) => { + const [lng, lat] = feature.geometry.coordinates as [number, number]; + const isCluster = 'cluster' in feature.properties; + // @ts-ignore + const count = feature.properties?.point_count ?? 0; + const icon = isCluster + ? { + width: 36, + height: 36, + svgString: ` + + + ${count} + + `, + } + : feature.properties.svgIcon; + + console.log(feature); + + return { + id: feature.id?.toString() ?? i.toString(), + coordinate: { latitude: lat, longitude: lng }, + iconSvg: icon, + } as RNMarker; + }); + }, [points]); + + return ( + + + + ); +} diff --git a/example/src/screens/HomeScreen.tsx b/example/src/screens/HomeScreen.tsx index 08aa896..94698d6 100644 --- a/example/src/screens/HomeScreen.tsx +++ b/example/src/screens/HomeScreen.tsx @@ -17,6 +17,7 @@ const screens = [ { name: 'IndoorLevelMap', title: 'Indoor Level Map' }, { name: 'Camera', title: 'Camera Test' }, { name: 'Snapshot', title: 'Snapshot Test' }, + { name: 'Clustering', title: 'Clustering' }, { name: 'Stress', title: 'Stress Test' }, ]; diff --git a/example/src/types/navigation.ts b/example/src/types/navigation.ts index bf29d2f..05f262b 100644 --- a/example/src/types/navigation.ts +++ b/example/src/types/navigation.ts @@ -13,6 +13,7 @@ export type RootStackParamList = { IndoorLevelMap: undefined; Camera: undefined; Snapshot: undefined; + Clustering: undefined; Stress: undefined; }; diff --git a/example/src/utils/mapGenerators.ts b/example/src/utils/mapGenerators.ts index 060d17f..0b47179 100644 --- a/example/src/utils/mapGenerators.ts +++ b/example/src/utils/mapGenerators.ts @@ -204,16 +204,13 @@ export function makeRandomMarkerForStressTest(id: number): RNMarker { id: id.toString(), zIndex: id, coordinate: randomCoordinates(37.7749, -122.4194, 0.2), - anchor: { x: 0.5, y: 1.0 }, draggable: false, - opacity: Math.random(), - flat: customIcon, - rotation: customIcon ? Math.random() * 180 : 0, - title: `Marker title id: ${id}`, - iconSvg: { - width: (64 / 100) * 50, - height: (88 / 100) * 50, - svgString: makeSvgIcon(64, 88), - }, + iconSvg: customIcon + ? { + width: (64 / 100) * 50, + height: (88 / 100) * 50, + svgString: makeSvgIcon(64, 88), + } + : undefined, }; } diff --git a/ios/MapCircleBuilder.swift b/ios/MapCircleBuilder.swift index 50377bd..148012f 100644 --- a/ios/MapCircleBuilder.swift +++ b/ios/MapCircleBuilder.swift @@ -14,14 +14,34 @@ final class MapCircleBuilder { return circle } - func update(_ next: RNCircle, _ c: GMSCircle) { - c.position = next.center.toCLLocationCoordinate2D() - c.radius = next.radius - c.fillColor = next.fillColor?.toUIColor() ?? nil - c.strokeColor = next.strokeColor?.toUIColor() ?? .black - c.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) - c.isTappable = next.pressable ?? false - c.zIndex = Int32(next.zIndex ?? 0) - } + func update(_ prev: RNCircle, _ next: RNCircle, _ c: GMSCircle) { + if prev.center.latitude != next.center.latitude + || prev.center.longitude != next.center.longitude { + c.position = next.center.toCLLocationCoordinate2D() + } + + if prev.radius != next.radius { + c.radius = next.radius ?? 0 + } + + if prev.fillColor != next.fillColor { + c.fillColor = next.fillColor?.toUIColor() ?? .clear + } + + if prev.strokeColor != next.strokeColor { + c.strokeColor = next.strokeColor?.toUIColor() ?? .black + } + if prev.strokeWidth != next.strokeWidth { + c.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + } + + if prev.pressable != next.pressable { + c.isTappable = next.pressable ?? false + } + + if prev.zIndex != next.zIndex { + c.zIndex = Int32(next.zIndex ?? 0) + } + } } diff --git a/ios/MapMarkerBuilder.swift b/ios/MapMarkerBuilder.swift index e5f7b27..6174158 100644 --- a/ios/MapMarkerBuilder.swift +++ b/ios/MapMarkerBuilder.swift @@ -44,31 +44,58 @@ final class MapMarkerBuilder { @MainActor func update(_ prev: RNMarker, _ next: RNMarker, _ m: GMSMarker) { - m.position = next.coordinate.toCLLocationCoordinate2D() - m.title = next.title - m.snippet = next.snippet - m.iconView?.alpha = CGFloat(next.opacity ?? 1) - m.isFlat = next.flat ?? false - m.isDraggable = next.draggable ?? false - m.rotation = next.rotation ?? 0 - m.infoWindowAnchor = CGPoint( - x: next.infoWindowAnchor?.x ?? 0.5, - y: next.infoWindowAnchor?.y ?? 0 - ) - m.zIndex = Int32(next.zIndex ?? 0) - m.groundAnchor = CGPoint( - x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 1 - ) + if prev.coordinate.latitude != next.coordinate.latitude + || prev.coordinate.longitude != next.coordinate.longitude { + m.position = next.coordinate.toCLLocationCoordinate2D() + } + + if prev.title != next.title { + m.title = next.title + } + + if prev.snippet != next.snippet { + m.snippet = next.snippet + } + + if prev.opacity != next.opacity { + let opacity = Float(next.opacity ?? 1) + m.opacity = opacity + m.iconView?.alpha = CGFloat(opacity) + } + + if prev.flat != next.flat { + m.isFlat = next.flat ?? false + } + + if prev.draggable != next.draggable { + m.isDraggable = next.draggable ?? false + } + + if prev.rotation != next.rotation { + m.rotation = next.rotation ?? 0 + } + + if prev.zIndex != next.zIndex { + m.zIndex = Int32(next.zIndex ?? 0) + } + if !prev.markerStyleEquals(next) { buildIconAsync(next.id, next) { img in m.tracksViewChanges = true m.icon = img - if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y { + if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y{ m.groundAnchor = CGPoint( x: next.anchor?.x ?? 0.5, - y: next.anchor?.y ?? 0.5 + y: next.anchor?.y ?? 1 + ) + } + + if prev.infoWindowAnchor?.x != next.infoWindowAnchor?.x + || prev.infoWindowAnchor?.y != next.infoWindowAnchor?.y { + m.infoWindowAnchor = CGPoint( + x: next.infoWindowAnchor?.x ?? 0.5, + y: next.infoWindowAnchor?.y ?? 0 ) } @@ -76,6 +103,21 @@ final class MapMarkerBuilder { m?.tracksViewChanges = false } } + } else { + if prev.anchor?.x != next.anchor?.x || prev.anchor?.y != next.anchor?.y{ + m.groundAnchor = CGPoint( + x: next.anchor?.x ?? 0.5, + y: next.anchor?.y ?? 1 + ) + } + + if prev.infoWindowAnchor?.x != next.infoWindowAnchor?.x + || prev.infoWindowAnchor?.y != next.infoWindowAnchor?.y { + m.infoWindowAnchor = CGPoint( + x: next.infoWindowAnchor?.x ?? 0.5, + y: next.infoWindowAnchor?.y ?? 0 + ) + } } } diff --git a/ios/MapPolygonBuilder.swift b/ios/MapPolygonBuilder.swift index c8c840b..0ef595e 100644 --- a/ios/MapPolygonBuilder.swift +++ b/ios/MapPolygonBuilder.swift @@ -28,26 +28,60 @@ final class MapPolygonBuilder { return pg } - func update(_ next: RNPolygon, _ pg: GMSPolygon) { - let path = GMSMutablePath() - next.coordinates.forEach { - path.add( - $0.toCLLocationCoordinate2D() - ) + func update(_ prev: RNPolygon, _ next: RNPolygon, _ pg: GMSPolygon) { + let coordsChanged = + prev.coordinates.count != next.coordinates.count + || !zip(prev.coordinates, next.coordinates).allSatisfy { + $0.latitude == $1.latitude && $0.longitude == $1.longitude + } + + if coordsChanged { + let path = GMSMutablePath() + next.coordinates.forEach { path.add($0.toCLLocationCoordinate2D()) } + pg.path = path } - pg.path = path - pg.fillColor = next.fillColor?.toUIColor() ?? .clear - pg.strokeColor = next.strokeColor?.toUIColor() ?? .black - pg.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) - pg.isTappable = next.pressable ?? false - pg.geodesic = next.geodesic ?? false - pg.holes = - next.holes?.map { hole in + let prevHoles = prev.holes ?? [] + let nextHoles = next.holes ?? [] + let holesChanged = + prevHoles.count != nextHoles.count + || !zip(prevHoles, nextHoles).allSatisfy { a, b in + a.coordinates.count == b.coordinates.count + && zip(a.coordinates, b.coordinates).allSatisfy { + $0.latitude == $1.latitude && $0.longitude == $1.longitude + } + } + + if holesChanged { + pg.holes = nextHoles.map { hole in let path = GMSMutablePath() hole.coordinates.forEach { path.add($0.toCLLocationCoordinate2D()) } return path - } ?? [] - pg.zIndex = Int32(next.zIndex ?? 0) + } + } + + if prev.fillColor != next.fillColor { + pg.fillColor = next.fillColor?.toUIColor() ?? .clear + } + + if prev.strokeColor != next.strokeColor { + pg.strokeColor = next.strokeColor?.toUIColor() ?? .black + } + + if prev.strokeWidth != next.strokeWidth { + pg.strokeWidth = CGFloat(next.strokeWidth ?? 1.0) + } + + if prev.pressable != next.pressable { + pg.isTappable = next.pressable ?? false + } + + if prev.geodesic != next.geodesic { + pg.geodesic = next.geodesic ?? false + } + + if prev.zIndex != next.zIndex { + pg.zIndex = Int32(next.zIndex ?? 0) + } } } diff --git a/ios/MapPolylineBuilder.swift b/ios/MapPolylineBuilder.swift index ba41dc2..da2f31d 100644 --- a/ios/MapPolylineBuilder.swift +++ b/ios/MapPolylineBuilder.swift @@ -22,21 +22,37 @@ final class MapPolylineBuilder { return pl } - func update(_ next: RNPolyline, _ pl: GMSPolyline) { - let path = GMSMutablePath() - next.coordinates.forEach { - path.add( - $0.toCLLocationCoordinate2D() - ) + func update(_ prev: RNPolyline, _ next: RNPolyline, _ pl: GMSPolyline) { + let coordsChanged = + prev.coordinates.count != next.coordinates.count + || !zip(prev.coordinates, next.coordinates).allSatisfy { + $0.latitude == $1.latitude && $0.longitude == $1.longitude + } + + if coordsChanged { + let path = GMSMutablePath() + next.coordinates.forEach { path.add($0.toCLLocationCoordinate2D()) } + pl.path = path + } + + if prev.width != next.width { + pl.strokeWidth = CGFloat(next.width ?? 1.0) + } + + if prev.color != next.color { + pl.strokeColor = next.color?.toUIColor() ?? .black + } + + if prev.pressable != next.pressable { + pl.isTappable = next.pressable ?? false + } + + if prev.geodesic != next.geodesic { + pl.geodesic = next.geodesic ?? false + } + + if prev.zIndex != next.zIndex { + pl.zIndex = Int32(next.zIndex ?? 0) } - pl.path = path - - /* lineCap not supported */ - /* lineJoin not supported */ - pl.strokeWidth = CGFloat(next.width ?? 1.0) - pl.strokeColor = next.color?.toUIColor() ?? .black - pl.isTappable = next.pressable ?? false - pl.geodesic = next.geodesic ?? false - pl.zIndex = Int32(next.zIndex ?? 0) } } diff --git a/ios/RNGoogleMapsPlusView.swift b/ios/RNGoogleMapsPlusView.swift index 0fd68ee..0d9f42b 100644 --- a/ios/RNGoogleMapsPlusView.swift +++ b/ios/RNGoogleMapsPlusView.swift @@ -169,7 +169,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.polylineEquals(next) { impl.updatePolyline(id: id) { pl in - self.polylineBuilder.update(next, pl) + self.polylineBuilder.update(prev, next, pl) } } } else { @@ -201,7 +201,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.polygonEquals(next) { impl.updatePolygon(id: id) { pg in - self.polygonBuilder.update(next, pg) + self.polygonBuilder.update(prev, next, pg) } } } else { @@ -230,7 +230,7 @@ final class RNGoogleMapsPlusView: HybridRNGoogleMapsPlusViewSpec { if let prev = prevById[id] { if !prev.circleEquals(next) { impl.updateCircle(id: id) { circle in - self.circleBuilder.update(next, circle) + self.circleBuilder.update(prev, next, circle) } } } else { diff --git a/yarn.lock b/yarn.lock index 7b4bb49..e2f5d7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2433,6 +2433,27 @@ __metadata: languageName: node linkType: hard +"@mapbox/geo-viewport@npm:^0.5.0": + version: 0.5.0 + resolution: "@mapbox/geo-viewport@npm:0.5.0" + dependencies: + "@mapbox/sphericalmercator": ^1.2.0 + checksum: 9cb990e177226acbdf7658f3367ed3158d8a77350afb0e1de71fa432e682a8aed4afe2f80d36872eb79666b54432fe6219797d50f2ab4152c51811235b7520a0 + languageName: node + linkType: hard + +"@mapbox/sphericalmercator@npm:^1.2.0": + version: 1.2.0 + resolution: "@mapbox/sphericalmercator@npm:1.2.0" + bin: + bbox: bin/bbox.js + to4326: bin/to4326.js + to900913: bin/to900913.js + xyz: bin/xyz.js + checksum: 515cd9fcadc6626d6352001f6d9dba073fcd10433ceee37d13d4be7fc9a48930b4163b79f53151b71ff9a4410da77bd11c5ff62e3b9d6119bce9031c4d7dabc1 + languageName: node + linkType: hard + "@napi-rs/wasm-runtime@npm:^0.2.11": version: 0.2.12 resolution: "@napi-rs/wasm-runtime@npm:0.2.12" @@ -3753,6 +3774,13 @@ __metadata: languageName: node linkType: hard +"@types/geojson@npm:^7946.0.8": + version: 7946.0.16 + resolution: "@types/geojson@npm:7946.0.16" + checksum: d66e5e023f43b3e7121448117af1930af7d06410a32a585a8bc9c6bb5d97e0d656cd93d99e31fa432976c32e98d4b780f82bf1fd1acd20ccf952eb6b8e39edf2 + languageName: node + linkType: hard + "@types/graceful-fs@npm:^4.1.3": version: 4.1.9 resolution: "@types/graceful-fs@npm:4.1.9" @@ -11624,6 +11652,19 @@ __metadata: languageName: node linkType: hard +"react-native-clusterer@npm:4.0.0": + version: 4.0.0 + resolution: "react-native-clusterer@npm:4.0.0" + dependencies: + "@mapbox/geo-viewport": ^0.5.0 + "@types/geojson": ^7946.0.8 + peerDependencies: + react: "*" + react-native: "*" + checksum: 4481c0e8aa92ce7fada43231137ea025de593169ab6fa943131666157ed72e64acce26a58334e9b7e67bdd8c9a07ff6ccd15728a474f4b0b574d7eddca5a6a10 + languageName: node + linkType: hard + "react-native-gesture-handler@npm:2.28.0": version: 2.28.0 resolution: "react-native-gesture-handler@npm:2.28.0" @@ -11658,6 +11699,7 @@ __metadata: react: 19.1.1 react-native: 0.82.0 react-native-builder-bob: 0.40.13 + react-native-clusterer: 4.0.0 react-native-gesture-handler: 2.28.0 react-native-google-maps-plus: "workspace:*" react-native-monorepo-config: 0.2.2