diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 000000000..b6cfecda9 --- /dev/null +++ b/.idea/codeStyleSettings.xml @@ -0,0 +1,265 @@ + + + + + + \ No newline at end of file diff --git a/.jscodeshiftignore b/.jscodeshiftignore new file mode 100644 index 000000000..a78536177 --- /dev/null +++ b/.jscodeshiftignore @@ -0,0 +1,9 @@ +# To run a codeshift on the react-native-maps library, cd to the root dir and run: +# jscodeshift -t PATH_TO_TRANSFORM . --ignore-config .jscodeshiftignore +.idea +android +docs +example +gradle +node_modules +scripts \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 5968b7e7b..50b2027bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 0.15.3 (June 27, 2017) + +* iOS: [#1362](https://github.com/airbnb/react-native-maps/pull/1362) Updates for React 0.43-0.45 and React 16. +* JS: [#1323](https://github.com/airbnb/react-native-maps/pull/1323) Updates for React 0.43-0.45 and React 16. +* Android/iOS/JS: [#1440](https://github.com/airbnb/react-native-maps/pull/1440) Updates for React 0.43-0.45 and React 16. +* iOS: [#1115](https://github.com/airbnb/react-native-maps/pull/1115) Fix animateToCoordinate and animateToRegion +* Android: [#1403](https://github.com/airbnb/react-native-maps/pull/1403) Fix an NPE + ## 0.15.2 (May 20, 2017) * iOS: [#1351](https://github.com/airbnb/react-native-maps/pull/1351) Fix file references diff --git a/README.md b/README.md index 49d65ac72..91b22377b 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ Then add the AirGoogleMaps directory: https://github.com/airbnb/react-native-maps/blob/1e71a21f39e7b88554852951f773c731c94680c9/docs/installation.md#ios -An unoffical step-by-step guide is also available at https://gist.github.com/heron2014/e60fa003e9b117ce80d56bb1d5bfe9e0 +An unofficial step-by-step guide is also available at https://gist.github.com/heron2014/e60fa003e9b117ce80d56bb1d5bfe9e0 ## Examples diff --git a/docs/installation.md b/docs/installation.md index 12842d7c6..ad9d4b383 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -123,6 +123,8 @@ Source: https://developers.google.com/maps/documentation/android-api/signup ## Troubleshooting +If you get the error `duplicate symbols for architecture x86_64` when building for iOS, you may need to reconfigure your linking and Podfile as [described in detail in this comment on issue #718](https://github.com/airbnb/react-native-maps/issues/718#issuecomment-295585410) + If you have a blank map issue, ([#118](https://github.com/airbnb/react-native-maps/issues/118), [#176](https://github.com/airbnb/react-native-maps/issues/176), [#684](https://github.com/airbnb/react-native-maps/issues/684)), try the following lines : ### On iOS: @@ -203,7 +205,7 @@ Enter the name of the API key and create it. 1. Clean the cache : ``` watchman watch-del-all - npm clean cache + npm cache clean ``` 1. When starting emulator, make sure you have enabled `Wipe user data`. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index c5bdebe4c..7a8e8b7dd 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -85,7 +85,7 @@ def enableProguardInReleaseBuilds = false android { compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion "25.0.3" defaultConfig { applicationId "com.airbnb.android.react.maps.example" @@ -127,8 +127,8 @@ android { } dependencies { - compile 'com.facebook.react:react-native:0.42.+' - compile 'com.android.support:appcompat-v7:25.1.1' - compile 'com.android.support:support-annotations:25.1.1' + compile 'com.facebook.react:react-native:0.45.+' + compile 'com.android.support:appcompat-v7:25.3.0' + compile 'com.android.support:support-annotations:25.3.0' compile project(':react-native-maps-lib') } diff --git a/example/android/app/src/main/java/com/airbnb/android/react/maps/example/MainActivity.java b/example/android/app/src/main/java/com/airbnb/android/react/maps/example/MainActivity.java index eec2349a4..12149c20e 100644 --- a/example/android/app/src/main/java/com/airbnb/android/react/maps/example/MainActivity.java +++ b/example/android/app/src/main/java/com/airbnb/android/react/maps/example/MainActivity.java @@ -4,14 +4,12 @@ public class MainActivity extends ReactActivity { - - /** - * Returns the name of the main component registered from JavaScript. - * This is used to schedule rendering of the component. - */ - @Override - protected String getMainComponentName() { - return "AirMapsExplorer"; - } - + /** + * Returns the name of the main component registered from JavaScript. + * This is used to schedule rendering of the component. + */ + @Override + protected String getMainComponentName() { + return "AirMapsExplorer"; + } } diff --git a/example/examples/DisplayLatLng.js b/example/examples/DisplayLatLng.js index 573617d41..aa2dde2ac 100644 --- a/example/examples/DisplayLatLng.js +++ b/example/examples/DisplayLatLng.js @@ -43,15 +43,25 @@ class DisplayLatLng extends React.Component { this.map.animateToRegion(this.randomRegion()); } - randomRegion() { - const { region } = this.state; + animateRandomCoordinate() { + this.map.animateToCoordinate(this.randomCoordinate()); + } + + randomCoordinate() { + const region = this.state.region; return { - ...this.state.region, latitude: region.latitude + ((Math.random() - 0.5) * (region.latitudeDelta / 2)), longitude: region.longitude + ((Math.random() - 0.5) * (region.longitudeDelta / 2)), }; } + randomRegion() { + return { + ...this.state.region, + ...this.randomCoordinate(), + }; + } + render() { return ( @@ -74,13 +84,19 @@ class DisplayLatLng extends React.Component { onPress={() => this.jumpRandom()} style={[styles.bubble, styles.button]} > - Jump + Jump this.animateRandom()} style={[styles.bubble, styles.button]} > - Animate + Animate (Region) + + this.animateRandomCoordinate()} + style={[styles.bubble, styles.button]} + > + Animate (Coordinate) @@ -112,16 +128,20 @@ const styles = StyleSheet.create({ alignItems: 'stretch', }, button: { - width: 80, - paddingHorizontal: 12, + width: 100, + paddingHorizontal: 8, alignItems: 'center', - marginHorizontal: 10, + justifyContent: 'center', + marginHorizontal: 5, }, buttonContainer: { flexDirection: 'row', marginVertical: 20, backgroundColor: 'transparent', }, + buttonText: { + textAlign: 'center', + }, }); module.exports = DisplayLatLng; diff --git a/example/examples/EventListener.js b/example/examples/EventListener.js index c8d3e937f..19a393063 100644 --- a/example/examples/EventListener.js +++ b/example/examples/EventListener.js @@ -7,7 +7,7 @@ import { ScrollView, } from 'react-native'; // eslint-disable-next-line max-len -import SyntheticEvent from 'react-native/Libraries/Renderer/src/renderers/shared/stack/event/SyntheticEvent'; +import SyntheticEvent from 'react-native/Libraries/Renderer/src/renderers/shared/shared/event/SyntheticEvent'; import MapView from 'react-native-maps'; import PriceMarker from './PriceMarker'; diff --git a/example/ios/Podfile b/example/ios/Podfile index 6cff9aae8..656f283e1 100644 --- a/example/ios/Podfile +++ b/example/ios/Podfile @@ -18,7 +18,8 @@ target 'AirMapsExplorer' do 'RCTSettings', 'RCTText', 'RCTVibration', - 'RCTWebSocket' + 'RCTWebSocket', + 'BatchedBridge' ] pod 'GoogleMaps' # <~~ remove this line if you do not want to support GoogleMaps on iOS diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ff6774aa2..3eec18fee 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,46 +4,49 @@ PODS: - GoogleMaps/Base (2.1.1) - GoogleMaps/Maps (2.1.1): - GoogleMaps/Base - - React (0.42.3): - - React/Core (= 0.42.3) - - react-native-google-maps (0.13.1): + - React (0.45.1): + - React/Core (= 0.45.1) + - react-native-google-maps (0.15.2): - GoogleMaps (= 2.1.1) - React - - react-native-maps (0.13.1): + - react-native-maps (0.15.2): - React - - React/Core (0.42.3): - - React/cxxreact - - Yoga (= 0.42.3.React) - - React/cxxreact (0.42.3): - - React/jschelpers - - React/jschelpers (0.42.3) - - React/RCTActionSheet (0.42.3): + - React/BatchedBridge (0.45.1): - React/Core - - React/RCTAnimation (0.42.3): + - React/cxxreact_legacy + - React/Core (0.45.1): + - Yoga (= 0.45.1.React) + - React/cxxreact_legacy (0.45.1): + - React/jschelpers_legacy + - React/jschelpers_legacy (0.45.1) + - React/RCTActionSheet (0.45.1): - React/Core - - React/RCTGeolocation (0.42.3): + - React/RCTAnimation (0.45.1): - React/Core - - React/RCTImage (0.42.3): + - React/RCTGeolocation (0.45.1): + - React/Core + - React/RCTImage (0.45.1): - React/Core - React/RCTNetwork - - React/RCTLinkingIOS (0.42.3): + - React/RCTLinkingIOS (0.45.1): - React/Core - - React/RCTNetwork (0.42.3): + - React/RCTNetwork (0.45.1): - React/Core - - React/RCTSettings (0.42.3): + - React/RCTSettings (0.45.1): - React/Core - - React/RCTText (0.42.3): + - React/RCTText (0.45.1): - React/Core - - React/RCTVibration (0.42.3): + - React/RCTVibration (0.45.1): - React/Core - - React/RCTWebSocket (0.42.3): + - React/RCTWebSocket (0.45.1): - React/Core - - Yoga (0.42.3.React) + - Yoga (0.45.1.React) DEPENDENCIES: - GoogleMaps - react-native-google-maps (from `../../`) - react-native-maps (from `../../`) + - React/BatchedBridge (from `../../node_modules/react-native`) - React/Core (from `../../node_modules/react-native`) - React/RCTActionSheet (from `../../node_modules/react-native`) - React/RCTAnimation (from `../../node_modules/react-native`) @@ -69,11 +72,11 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: GoogleMaps: a5b5bbe47734e2443bde781a6aa64e69fdb6d785 - React: 35e039680feacd0563677d49ba410112d2748559 - react-native-google-maps: b2668747ec289759993dc2411a7078afafa8adea - react-native-maps: 326ddbaaea8f6044b1817fb028c40950c71cc38a - Yoga: 86ce777665c8259b94ef8dbea76b84634237f4ea + React: 0c9191a8b0c843d7004f950ac6b5f6cba9d125c7 + react-native-google-maps: d0b8772eb76e1615ea32c73bc9d573360b8c0817 + react-native-maps: fe2e4680b4d3fcfd84d636ccedd470fe358d55e1 + Yoga: 89c8738d42a0b46a113acb4e574336d61cba2985 -PODFILE CHECKSUM: 222d08e48f834b6a3de650b72786105af7a9d331 +PODFILE CHECKSUM: 8b3eb68ef6553bf1fcb8a467e0e63000b37ec692 COCOAPODS: 1.2.0 diff --git a/examples/ios/bundler b/examples/ios/bundler new file mode 100755 index 000000000..905387619 --- /dev/null +++ b/examples/ios/bundler @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# This file was generated by Bundler. +# +# The application 'bundler' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("bundler", "bundler") diff --git a/examples/ios/fuzzy_match b/examples/ios/fuzzy_match new file mode 100755 index 000000000..f71547393 --- /dev/null +++ b/examples/ios/fuzzy_match @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# This file was generated by Bundler. +# +# The application 'fuzzy_match' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("fuzzy_match", "fuzzy_match") diff --git a/examples/ios/pod b/examples/ios/pod new file mode 100755 index 000000000..3c4a4d04c --- /dev/null +++ b/examples/ios/pod @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# This file was generated by Bundler. +# +# The application 'pod' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("cocoapods", "pod") diff --git a/examples/ios/sandbox-pod b/examples/ios/sandbox-pod new file mode 100755 index 000000000..c76cfd0a5 --- /dev/null +++ b/examples/ios/sandbox-pod @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# This file was generated by Bundler. +# +# The application 'sandbox-pod' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("cocoapods", "sandbox-pod") diff --git a/examples/ios/xcodeproj b/examples/ios/xcodeproj new file mode 100755 index 000000000..3c3452c17 --- /dev/null +++ b/examples/ios/xcodeproj @@ -0,0 +1,17 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true +# +# This file was generated by Bundler. +# +# The application 'xcodeproj' is installed as part of a gem, and +# this file is here to facilitate running it. +# + +require "pathname" +ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../../Gemfile", + Pathname.new(__FILE__).realpath) + +require "rubygems" +require "bundler/setup" + +load Gem.bin_path("xcodeproj", "xcodeproj") diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c21e471fc..e5fa88cc4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip diff --git a/lib/android/build.gradle b/lib/android/build.gradle index 434e63e72..326ab4bfa 100644 --- a/lib/android/build.gradle +++ b/lib/android/build.gradle @@ -3,7 +3,7 @@ apply from: 'gradle-maven-push.gradle' android { compileSdkVersion 25 - buildToolsVersion "25.0.2" + buildToolsVersion "25.0.3" defaultConfig { minSdkVersion 16 diff --git a/lib/android/gradle.properties b/lib/android/gradle.properties index 307005f30..e17f7611f 100644 --- a/lib/android/gradle.properties +++ b/lib/android/gradle.properties @@ -1,5 +1,5 @@ VERSION_CODE=4 -VERSION_NAME=0.15.2 +VERSION_NAME=0.15.3 GROUP=com.airbnb.android POM_DESCRIPTION=React Native Map view component for Android diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCallout.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCallout.java index c434c9c12..6f96e15da 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCallout.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCallout.java @@ -5,19 +5,19 @@ import com.facebook.react.views.view.ReactViewGroup; public class AirMapCallout extends ReactViewGroup { - private boolean tooltip = false; - public int width; - public int height; + private boolean tooltip = false; + public int width; + public int height; - public AirMapCallout(Context context) { - super(context); - } + public AirMapCallout(Context context) { + super(context); + } - public void setTooltip(boolean tooltip) { - this.tooltip = tooltip; - } + public void setTooltip(boolean tooltip) { + this.tooltip = tooltip; + } - public boolean getTooltip() { - return this.tooltip; - } + public boolean getTooltip() { + return this.tooltip; + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCalloutManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCalloutManager.java index 359e6847b..b63ea29ff 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCalloutManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCalloutManager.java @@ -12,45 +12,45 @@ public class AirMapCalloutManager extends ViewGroupManager { - @Override - public String getName() { - return "AIRMapCallout"; - } - - @Override - public AirMapCallout createViewInstance(ThemedReactContext context) { - return new AirMapCallout(context); - } - - @ReactProp(name = "tooltip", defaultBoolean = false) - public void setTooltip(AirMapCallout view, boolean tooltip) { - view.setTooltip(tooltip); - } - - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - return MapBuilder.of("onPress", MapBuilder.of("registrationName", "onPress")); - } - - @Override - public LayoutShadowNode createShadowNodeInstance() { - // we use a custom shadow node that emits the width/height of the view - // after layout with the updateExtraData method. Without this, we can't generate - // a bitmap of the appropriate width/height of the rendered view. - return new SizeReportingShadowNode(); - } - - @Override - public void updateExtraData(AirMapCallout view, Object extraData) { - // This method is called from the shadow node with the width/height of the rendered - // marker view. - //noinspection unchecked - Map data = (Map) extraData; - float width = data.get("width"); - float height = data.get("height"); - view.width = (int) width; - view.height = (int) height; - } + @Override + public String getName() { + return "AIRMapCallout"; + } + + @Override + public AirMapCallout createViewInstance(ThemedReactContext context) { + return new AirMapCallout(context); + } + + @ReactProp(name = "tooltip", defaultBoolean = false) + public void setTooltip(AirMapCallout view, boolean tooltip) { + view.setTooltip(tooltip); + } + + @Override + @Nullable + public Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of("onPress", MapBuilder.of("registrationName", "onPress")); + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + // we use a custom shadow node that emits the width/height of the view + // after layout with the updateExtraData method. Without this, we can't generate + // a bitmap of the appropriate width/height of the rendered view. + return new SizeReportingShadowNode(); + } + + @Override + public void updateExtraData(AirMapCallout view, Object extraData) { + // This method is called from the shadow node with the width/height of the rendered + // marker view. + //noinspection unchecked + Map data = (Map) extraData; + float width = data.get("width"); + float height = data.get("height"); + view.width = (int) width; + view.height = (int) height; + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircle.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircle.java index e428b04f6..a70d9146a 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircle.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircle.java @@ -9,92 +9,92 @@ public class AirMapCircle extends AirMapFeature { - private CircleOptions circleOptions; - private Circle circle; - - private LatLng center; - private double radius; - private int strokeColor; - private int fillColor; - private float strokeWidth; - private float zIndex; - - public AirMapCircle(Context context) { - super(context); + private CircleOptions circleOptions; + private Circle circle; + + private LatLng center; + private double radius; + private int strokeColor; + private int fillColor; + private float strokeWidth; + private float zIndex; + + public AirMapCircle(Context context) { + super(context); + } + + public void setCenter(LatLng center) { + this.center = center; + if (circle != null) { + circle.setCenter(this.center); } + } - public void setCenter(LatLng center) { - this.center = center; - if (circle != null) { - circle.setCenter(this.center); - } + public void setRadius(double radius) { + this.radius = radius; + if (circle != null) { + circle.setRadius(this.radius); } + } - public void setRadius(double radius) { - this.radius = radius; - if (circle != null) { - circle.setRadius(this.radius); - } + public void setFillColor(int color) { + this.fillColor = color; + if (circle != null) { + circle.setFillColor(color); } + } - public void setFillColor(int color) { - this.fillColor = color; - if (circle != null) { - circle.setFillColor(color); - } + public void setStrokeColor(int color) { + this.strokeColor = color; + if (circle != null) { + circle.setStrokeColor(color); } + } - public void setStrokeColor(int color) { - this.strokeColor = color; - if (circle != null) { - circle.setStrokeColor(color); - } + public void setStrokeWidth(float width) { + this.strokeWidth = width; + if (circle != null) { + circle.setStrokeWidth(width); } + } - public void setStrokeWidth(float width) { - this.strokeWidth = width; - if (circle != null) { - circle.setStrokeWidth(width); - } + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (circle != null) { + circle.setZIndex(zIndex); } + } - public void setZIndex(float zIndex) { - this.zIndex = zIndex; - if (circle != null) { - circle.setZIndex(zIndex); - } - } - - public CircleOptions getCircleOptions() { - if (circleOptions == null) { - circleOptions = createCircleOptions(); - } - return circleOptions; - } - - private CircleOptions createCircleOptions() { - CircleOptions options = new CircleOptions(); - options.center(center); - options.radius(radius); - options.fillColor(fillColor); - options.strokeColor(strokeColor); - options.strokeWidth(strokeWidth); - options.zIndex(zIndex); - return options; - } - - @Override - public Object getFeature() { - return circle; - } - - @Override - public void addToMap(GoogleMap map) { - circle = map.addCircle(getCircleOptions()); - } - - @Override - public void removeFromMap(GoogleMap map) { - circle.remove(); + public CircleOptions getCircleOptions() { + if (circleOptions == null) { + circleOptions = createCircleOptions(); } + return circleOptions; + } + + private CircleOptions createCircleOptions() { + CircleOptions options = new CircleOptions(); + options.center(center); + options.radius(radius); + options.fillColor(fillColor); + options.strokeColor(strokeColor); + options.strokeWidth(strokeWidth); + options.zIndex(zIndex); + return options; + } + + @Override + public Object getFeature() { + return circle; + } + + @Override + public void addToMap(GoogleMap map) { + circle = map.addCircle(getCircleOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + circle.remove(); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircleManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircleManager.java index c0eaf8f14..c8eabf2d1 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircleManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapCircleManager.java @@ -14,59 +14,59 @@ import com.google.android.gms.maps.model.LatLng; public class AirMapCircleManager extends ViewGroupManager { - private final DisplayMetrics metrics; + private final DisplayMetrics metrics; - public AirMapCircleManager(ReactApplicationContext reactContext) { - super(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - metrics = new DisplayMetrics(); - ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRealMetrics(metrics); - } else { - metrics = reactContext.getResources().getDisplayMetrics(); - } + public AirMapCircleManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); } + } - @Override - public String getName() { - return "AIRMapCircle"; - } + @Override + public String getName() { + return "AIRMapCircle"; + } - @Override - public AirMapCircle createViewInstance(ThemedReactContext context) { - return new AirMapCircle(context); - } + @Override + public AirMapCircle createViewInstance(ThemedReactContext context) { + return new AirMapCircle(context); + } - @ReactProp(name = "center") - public void setCenter(AirMapCircle view, ReadableMap center) { - view.setCenter(new LatLng(center.getDouble("latitude"), center.getDouble("longitude"))); - } + @ReactProp(name = "center") + public void setCenter(AirMapCircle view, ReadableMap center) { + view.setCenter(new LatLng(center.getDouble("latitude"), center.getDouble("longitude"))); + } - @ReactProp(name = "radius", defaultDouble = 0) - public void setRadius(AirMapCircle view, double radius) { - view.setRadius(radius); - } + @ReactProp(name = "radius", defaultDouble = 0) + public void setRadius(AirMapCircle view, double radius) { + view.setRadius(radius); + } - @ReactProp(name = "strokeWidth", defaultFloat = 1f) - public void setStrokeWidth(AirMapCircle view, float widthInPoints) { - float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS - view.setStrokeWidth(widthInScreenPx); - } + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapCircle view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setStrokeWidth(widthInScreenPx); + } - @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") - public void setFillColor(AirMapCircle view, int color) { - view.setFillColor(color); - } + @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") + public void setFillColor(AirMapCircle view, int color) { + view.setFillColor(color); + } - @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") - public void setStrokeColor(AirMapCircle view, int color) { - view.setStrokeColor(color); - } + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapCircle view, int color) { + view.setStrokeColor(color); + } - @ReactProp(name = "zIndex", defaultFloat = 1.0f) - public void setZIndex(AirMapCircle view, float zIndex) { - view.setZIndex(zIndex); - } + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapCircle view, float zIndex) { + view.setZIndex(zIndex); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapFeature.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapFeature.java index 1c15ade5f..70484c1a4 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapFeature.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapFeature.java @@ -6,13 +6,13 @@ import com.google.android.gms.maps.GoogleMap; public abstract class AirMapFeature extends ReactViewGroup { - public AirMapFeature(Context context) { - super(context); - } + public AirMapFeature(Context context) { + super(context); + } - public abstract void addToMap(GoogleMap map); + public abstract void addToMap(GoogleMap map); - public abstract void removeFromMap(GoogleMap map); + public abstract void removeFromMap(GoogleMap map); - public abstract Object getFeature(); + public abstract Object getFeature(); } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapLiteManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapLiteManager.java index 62495c5e4..619e36435 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapLiteManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapLiteManager.java @@ -5,16 +5,16 @@ public class AirMapLiteManager extends AirMapManager { - private static final String REACT_CLASS = "AIRMapLite"; + private static final String REACT_CLASS = "AIRMapLite"; - @Override - public String getName() { - return REACT_CLASS; - } + @Override + public String getName() { + return REACT_CLASS; + } - public AirMapLiteManager(ReactApplicationContext context) { - super(context); - this.googleMapOptions = new GoogleMapOptions().liteMode(true); - } + public AirMapLiteManager(ReactApplicationContext context) { + super(context); + this.googleMapOptions = new GoogleMapOptions().liteMode(true); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java index b6a975b77..e8f98a766 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapManager.java @@ -4,7 +4,6 @@ import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; @@ -17,7 +16,6 @@ import com.facebook.react.uimanager.events.RCTEventEmitter; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMapOptions; -import com.google.android.gms.maps.MapsInitializer; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; import com.google.android.gms.maps.model.MapStyleOptions; @@ -28,280 +26,290 @@ public class AirMapManager extends ViewGroupManager { - private static final String REACT_CLASS = "AIRMap"; - private static final int ANIMATE_TO_REGION = 1; - private static final int ANIMATE_TO_COORDINATE = 2; - private static final int FIT_TO_ELEMENTS = 3; - private static final int FIT_TO_SUPPLIED_MARKERS = 4; - private static final int FIT_TO_COORDINATES = 5; - - private final Map MAP_TYPES = MapBuilder.of( - "standard", GoogleMap.MAP_TYPE_NORMAL, - "satellite", GoogleMap.MAP_TYPE_SATELLITE, - "hybrid", GoogleMap.MAP_TYPE_HYBRID, - "terrain", GoogleMap.MAP_TYPE_TERRAIN, - "none", GoogleMap.MAP_TYPE_NONE - ); - - private final ReactApplicationContext appContext; - - protected GoogleMapOptions googleMapOptions; - - public AirMapManager(ReactApplicationContext context) { - this.appContext = context; - this.googleMapOptions = new GoogleMapOptions(); - } - - @Override - public String getName() { - return REACT_CLASS; - } - - @Override - protected AirMapView createViewInstance(ThemedReactContext context) { - return new AirMapView(context, this.appContext, this, googleMapOptions); - } - - private void emitMapError(ThemedReactContext context, String message, String type) { - WritableMap error = Arguments.createMap(); - error.putString("message", message); - error.putString("type", type); - - context - .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) - .emit("onError", error); - } - - @ReactProp(name = "region") - public void setRegion(AirMapView view, ReadableMap region) { - view.setRegion(region); - } - - @ReactProp(name = "mapType") - public void setMapType(AirMapView view, @Nullable String mapType) { - int typeId = MAP_TYPES.get(mapType); - view.map.setMapType(typeId); - } - - @ReactProp(name = "customMapStyleString") - public void setMapStyle(AirMapView view, @Nullable String customMapStyleString) { - view.map.setMapStyle(new MapStyleOptions(customMapStyleString)); - } - - @ReactProp(name = "showsUserLocation", defaultBoolean = false) - public void setShowsUserLocation(AirMapView view, boolean showUserLocation) { - view.setShowsUserLocation(showUserLocation); - } - - @ReactProp(name = "showsMyLocationButton", defaultBoolean = true) - public void setShowsMyLocationButton(AirMapView view, boolean showMyLocationButton) { - view.setShowsMyLocationButton(showMyLocationButton); - } - - @ReactProp(name = "toolbarEnabled", defaultBoolean = true) - public void setToolbarEnabled(AirMapView view, boolean toolbarEnabled) { - view.setToolbarEnabled(toolbarEnabled); - } - - // This is a private prop to improve performance of panDrag by disabling it when the callback is not set - @ReactProp(name = "handlePanDrag", defaultBoolean = false) - public void setHandlePanDrag(AirMapView view, boolean handlePanDrag) { - view.setHandlePanDrag(handlePanDrag); - } - - @ReactProp(name = "showsTraffic", defaultBoolean = false) - public void setShowTraffic(AirMapView view, boolean showTraffic) { - view.map.setTrafficEnabled(showTraffic); - } - - @ReactProp(name = "showsBuildings", defaultBoolean = false) - public void setShowBuildings(AirMapView view, boolean showBuildings) { - view.map.setBuildingsEnabled(showBuildings); - } - - @ReactProp(name = "showsIndoors", defaultBoolean = false) - public void setShowIndoors(AirMapView view, boolean showIndoors) { - view.map.setIndoorEnabled(showIndoors); - } - - @ReactProp(name = "showsIndoorLevelPicker", defaultBoolean = false) - public void setShowsIndoorLevelPicker(AirMapView view, boolean showsIndoorLevelPicker) { - view.map.getUiSettings().setIndoorLevelPickerEnabled(showsIndoorLevelPicker); - } - - @ReactProp(name = "showsCompass", defaultBoolean = false) - public void setShowsCompass(AirMapView view, boolean showsCompass) { - view.map.getUiSettings().setCompassEnabled(showsCompass); - } - - @ReactProp(name = "scrollEnabled", defaultBoolean = false) - public void setScrollEnabled(AirMapView view, boolean scrollEnabled) { - view.map.getUiSettings().setScrollGesturesEnabled(scrollEnabled); - } - - @ReactProp(name = "zoomEnabled", defaultBoolean = false) - public void setZoomEnabled(AirMapView view, boolean zoomEnabled) { - view.map.getUiSettings().setZoomGesturesEnabled(zoomEnabled); - } - - @ReactProp(name = "rotateEnabled", defaultBoolean = false) - public void setRotateEnabled(AirMapView view, boolean rotateEnabled) { - view.map.getUiSettings().setRotateGesturesEnabled(rotateEnabled); - } - - @ReactProp(name = "cacheEnabled", defaultBoolean = false) - public void setCacheEnabled(AirMapView view, boolean cacheEnabled) { - view.setCacheEnabled(cacheEnabled); - } - - @ReactProp(name = "loadingEnabled", defaultBoolean = false) - public void setLoadingEnabled(AirMapView view, boolean loadingEnabled) { - view.enableMapLoading(loadingEnabled); - } - - @ReactProp(name = "moveOnMarkerPress", defaultBoolean = true) - public void setMoveOnMarkerPress(AirMapView view, boolean moveOnPress) { - view.setMoveOnMarkerPress(moveOnPress); - } - - @ReactProp(name = "loadingBackgroundColor", customType = "Color") - public void setLoadingBackgroundColor(AirMapView view, @Nullable Integer loadingBackgroundColor) { - view.setLoadingBackgroundColor(loadingBackgroundColor); - } - - @ReactProp(name = "loadingIndicatorColor", customType = "Color") - public void setLoadingIndicatorColor(AirMapView view, @Nullable Integer loadingIndicatorColor) { - view.setLoadingIndicatorColor(loadingIndicatorColor); - } - - @ReactProp(name = "pitchEnabled", defaultBoolean = false) - public void setPitchEnabled(AirMapView view, boolean pitchEnabled) { - view.map.getUiSettings().setTiltGesturesEnabled(pitchEnabled); - } - - @Override - public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArray args) { - Integer duration; - Double lat; - Double lng; - Double lngDelta; - Double latDelta; - ReadableMap region; - - switch (commandId) { - case ANIMATE_TO_REGION: - region = args.getMap(0); - duration = args.getInt(1); - lng = region.getDouble("longitude"); - lat = region.getDouble("latitude"); - lngDelta = region.getDouble("longitudeDelta"); - latDelta = region.getDouble("latitudeDelta"); - LatLngBounds bounds = new LatLngBounds( - new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest - new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast - ); - view.animateToRegion(bounds, duration); - break; - - case ANIMATE_TO_COORDINATE: - region = args.getMap(0); - duration = args.getInt(1); - lng = region.getDouble("longitude"); - lat = region.getDouble("latitude"); - view.animateToCoordinate(new LatLng(lat, lng), duration); - break; - - case FIT_TO_ELEMENTS: - view.fitToElements(args.getBoolean(0)); - break; - - case FIT_TO_SUPPLIED_MARKERS: - view.fitToSuppliedMarkers(args.getArray(0), args.getBoolean(1)); - break; - case FIT_TO_COORDINATES: - view.fitToCoordinates(args.getArray(0), args.getMap(1), args.getBoolean(2)); - break; - } - } - - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - Map> map = MapBuilder.of( - "onMapReady", MapBuilder.of("registrationName", "onMapReady"), - "onPress", MapBuilder.of("registrationName", "onPress"), - "onLongPress", MapBuilder.of("registrationName", "onLongPress"), - "onMarkerPress", MapBuilder.of("registrationName", "onMarkerPress"), - "onMarkerSelect", MapBuilder.of("registrationName", "onMarkerSelect"), - "onMarkerDeselect", MapBuilder.of("registrationName", "onMarkerDeselect"), - "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress") - ); - - map.putAll(MapBuilder.of( - "onMarkerDragStart", MapBuilder.of("registrationName", "onMarkerDragStart"), - "onMarkerDrag", MapBuilder.of("registrationName", "onMarkerDrag"), - "onMarkerDragEnd", MapBuilder.of("registrationName", "onMarkerDragEnd"), - "onPanDrag", MapBuilder.of("registrationName", "onPanDrag") - )); - - return map; - } - - @Override - @Nullable - public Map getCommandsMap() { - return MapBuilder.of( - "animateToRegion", ANIMATE_TO_REGION, - "animateToCoordinate", ANIMATE_TO_COORDINATE, - "fitToElements", FIT_TO_ELEMENTS, - "fitToSuppliedMarkers", FIT_TO_SUPPLIED_MARKERS, - "fitToCoordinates", FIT_TO_COORDINATES + private static final String REACT_CLASS = "AIRMap"; + private static final int ANIMATE_TO_REGION = 1; + private static final int ANIMATE_TO_COORDINATE = 2; + private static final int FIT_TO_ELEMENTS = 3; + private static final int FIT_TO_SUPPLIED_MARKERS = 4; + private static final int FIT_TO_COORDINATES = 5; + + private final Map MAP_TYPES = MapBuilder.of( + "standard", GoogleMap.MAP_TYPE_NORMAL, + "satellite", GoogleMap.MAP_TYPE_SATELLITE, + "hybrid", GoogleMap.MAP_TYPE_HYBRID, + "terrain", GoogleMap.MAP_TYPE_TERRAIN, + "none", GoogleMap.MAP_TYPE_NONE + ); + + private final ReactApplicationContext appContext; + + protected GoogleMapOptions googleMapOptions; + + public AirMapManager(ReactApplicationContext context) { + this.appContext = context; + this.googleMapOptions = new GoogleMapOptions(); + } + + @Override + public String getName() { + return REACT_CLASS; + } + + @Override + protected AirMapView createViewInstance(ThemedReactContext context) { + return new AirMapView(context, this.appContext, this, googleMapOptions); + } + + private void emitMapError(ThemedReactContext context, String message, String type) { + WritableMap error = Arguments.createMap(); + error.putString("message", message); + error.putString("type", type); + + context + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onError", error); + } + + @ReactProp(name = "region") + public void setRegion(AirMapView view, ReadableMap region) { + view.setRegion(region); + } + + @ReactProp(name = "mapType") + public void setMapType(AirMapView view, @Nullable String mapType) { + int typeId = MAP_TYPES.get(mapType); + view.map.setMapType(typeId); + } + + @ReactProp(name = "customMapStyleString") + public void setMapStyle(AirMapView view, @Nullable String customMapStyleString) { + view.map.setMapStyle(new MapStyleOptions(customMapStyleString)); + } + + @ReactProp(name = "showsUserLocation", defaultBoolean = false) + public void setShowsUserLocation(AirMapView view, boolean showUserLocation) { + view.setShowsUserLocation(showUserLocation); + } + + @ReactProp(name = "showsMyLocationButton", defaultBoolean = true) + public void setShowsMyLocationButton(AirMapView view, boolean showMyLocationButton) { + view.setShowsMyLocationButton(showMyLocationButton); + } + + @ReactProp(name = "toolbarEnabled", defaultBoolean = true) + public void setToolbarEnabled(AirMapView view, boolean toolbarEnabled) { + view.setToolbarEnabled(toolbarEnabled); + } + + // This is a private prop to improve performance of panDrag by disabling it when the callback + // is not set + @ReactProp(name = "handlePanDrag", defaultBoolean = false) + public void setHandlePanDrag(AirMapView view, boolean handlePanDrag) { + view.setHandlePanDrag(handlePanDrag); + } + + @ReactProp(name = "showsTraffic", defaultBoolean = false) + public void setShowTraffic(AirMapView view, boolean showTraffic) { + view.map.setTrafficEnabled(showTraffic); + } + + @ReactProp(name = "showsBuildings", defaultBoolean = false) + public void setShowBuildings(AirMapView view, boolean showBuildings) { + view.map.setBuildingsEnabled(showBuildings); + } + + @ReactProp(name = "showsIndoors", defaultBoolean = false) + public void setShowIndoors(AirMapView view, boolean showIndoors) { + view.map.setIndoorEnabled(showIndoors); + } + + @ReactProp(name = "showsIndoorLevelPicker", defaultBoolean = false) + public void setShowsIndoorLevelPicker(AirMapView view, boolean showsIndoorLevelPicker) { + view.map.getUiSettings().setIndoorLevelPickerEnabled(showsIndoorLevelPicker); + } + + @ReactProp(name = "showsCompass", defaultBoolean = false) + public void setShowsCompass(AirMapView view, boolean showsCompass) { + view.map.getUiSettings().setCompassEnabled(showsCompass); + } + + @ReactProp(name = "scrollEnabled", defaultBoolean = false) + public void setScrollEnabled(AirMapView view, boolean scrollEnabled) { + view.map.getUiSettings().setScrollGesturesEnabled(scrollEnabled); + } + + @ReactProp(name = "zoomEnabled", defaultBoolean = false) + public void setZoomEnabled(AirMapView view, boolean zoomEnabled) { + view.map.getUiSettings().setZoomGesturesEnabled(zoomEnabled); + } + + @ReactProp(name = "rotateEnabled", defaultBoolean = false) + public void setRotateEnabled(AirMapView view, boolean rotateEnabled) { + view.map.getUiSettings().setRotateGesturesEnabled(rotateEnabled); + } + + @ReactProp(name = "cacheEnabled", defaultBoolean = false) + public void setCacheEnabled(AirMapView view, boolean cacheEnabled) { + view.setCacheEnabled(cacheEnabled); + } + + @ReactProp(name = "loadingEnabled", defaultBoolean = false) + public void setLoadingEnabled(AirMapView view, boolean loadingEnabled) { + view.enableMapLoading(loadingEnabled); + } + + @ReactProp(name = "moveOnMarkerPress", defaultBoolean = true) + public void setMoveOnMarkerPress(AirMapView view, boolean moveOnPress) { + view.setMoveOnMarkerPress(moveOnPress); + } + + @ReactProp(name = "loadingBackgroundColor", customType = "Color") + public void setLoadingBackgroundColor(AirMapView view, @Nullable Integer loadingBackgroundColor) { + view.setLoadingBackgroundColor(loadingBackgroundColor); + } + + @ReactProp(name = "loadingIndicatorColor", customType = "Color") + public void setLoadingIndicatorColor(AirMapView view, @Nullable Integer loadingIndicatorColor) { + view.setLoadingIndicatorColor(loadingIndicatorColor); + } + + @ReactProp(name = "pitchEnabled", defaultBoolean = false) + public void setPitchEnabled(AirMapView view, boolean pitchEnabled) { + view.map.getUiSettings().setTiltGesturesEnabled(pitchEnabled); + } + + @ReactProp(name = "minZoomLevel") + public void setMinZoomLevel(AirMapView view, float minZoomLevel) { + view.map.setMinZoomPreference(minZoomLevel); + } + + @ReactProp(name = "maxZoomLevel") + public void setMaxZoomLevel(AirMapView view, float maxZoomLevel) { + view.map.setMaxZoomPreference(maxZoomLevel); + } + + @Override + public void receiveCommand(AirMapView view, int commandId, @Nullable ReadableArray args) { + Integer duration; + Double lat; + Double lng; + Double lngDelta; + Double latDelta; + ReadableMap region; + + switch (commandId) { + case ANIMATE_TO_REGION: + region = args.getMap(0); + duration = args.getInt(1); + lng = region.getDouble("longitude"); + lat = region.getDouble("latitude"); + lngDelta = region.getDouble("longitudeDelta"); + latDelta = region.getDouble("latitudeDelta"); + LatLngBounds bounds = new LatLngBounds( + new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest + new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast ); - } - - @Override - public LayoutShadowNode createShadowNodeInstance() { - // A custom shadow node is needed in order to pass back the width/height of the map to the - // view manager so that it can start applying camera moves with bounds. - return new SizeReportingShadowNode(); - } - - @Override - public void addView(AirMapView parent, View child, int index) { - parent.addFeature(child, index); - } - - @Override - public int getChildCount(AirMapView view) { - return view.getFeatureCount(); - } - - @Override - public View getChildAt(AirMapView view, int index) { - return view.getFeatureAt(index); - } - - @Override - public void removeViewAt(AirMapView parent, int index) { - parent.removeFeatureAt(index); - } - - @Override - public void updateExtraData(AirMapView view, Object extraData) { - view.updateExtraData(extraData); - } - - void pushEvent(ThemedReactContext context, View view, String name, WritableMap data) { - context.getJSModule(RCTEventEmitter.class) - .receiveEvent(view.getId(), name, data); - } - - + view.animateToRegion(bounds, duration); + break; + + case ANIMATE_TO_COORDINATE: + region = args.getMap(0); + duration = args.getInt(1); + lng = region.getDouble("longitude"); + lat = region.getDouble("latitude"); + view.animateToCoordinate(new LatLng(lat, lng), duration); + break; + + case FIT_TO_ELEMENTS: + view.fitToElements(args.getBoolean(0)); + break; + + case FIT_TO_SUPPLIED_MARKERS: + view.fitToSuppliedMarkers(args.getArray(0), args.getBoolean(1)); + break; + case FIT_TO_COORDINATES: + view.fitToCoordinates(args.getArray(0), args.getMap(1), args.getBoolean(2)); + break; + } + } + + @Override + @Nullable + public Map getExportedCustomDirectEventTypeConstants() { + Map> map = MapBuilder.of( + "onMapReady", MapBuilder.of("registrationName", "onMapReady"), + "onPress", MapBuilder.of("registrationName", "onPress"), + "onLongPress", MapBuilder.of("registrationName", "onLongPress"), + "onMarkerPress", MapBuilder.of("registrationName", "onMarkerPress"), + "onMarkerSelect", MapBuilder.of("registrationName", "onMarkerSelect"), + "onMarkerDeselect", MapBuilder.of("registrationName", "onMarkerDeselect"), + "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress") + ); - @Override - public void onDropViewInstance(AirMapView view) { - view.doDestroy(); - super.onDropViewInstance(view); - } + map.putAll(MapBuilder.of( + "onMarkerDragStart", MapBuilder.of("registrationName", "onMarkerDragStart"), + "onMarkerDrag", MapBuilder.of("registrationName", "onMarkerDrag"), + "onMarkerDragEnd", MapBuilder.of("registrationName", "onMarkerDragEnd"), + "onPanDrag", MapBuilder.of("registrationName", "onPanDrag") + )); + + return map; + } + + @Override + @Nullable + public Map getCommandsMap() { + return MapBuilder.of( + "animateToRegion", ANIMATE_TO_REGION, + "animateToCoordinate", ANIMATE_TO_COORDINATE, + "fitToElements", FIT_TO_ELEMENTS, + "fitToSuppliedMarkers", FIT_TO_SUPPLIED_MARKERS, + "fitToCoordinates", FIT_TO_COORDINATES + ); + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + // A custom shadow node is needed in order to pass back the width/height of the map to the + // view manager so that it can start applying camera moves with bounds. + return new SizeReportingShadowNode(); + } + + @Override + public void addView(AirMapView parent, View child, int index) { + parent.addFeature(child, index); + } + + @Override + public int getChildCount(AirMapView view) { + return view.getFeatureCount(); + } + + @Override + public View getChildAt(AirMapView view, int index) { + return view.getFeatureAt(index); + } + + @Override + public void removeViewAt(AirMapView parent, int index) { + parent.removeFeatureAt(index); + } + + @Override + public void updateExtraData(AirMapView view, Object extraData) { + view.updateExtraData(extraData); + } + + void pushEvent(ThemedReactContext context, View view, String name, WritableMap data) { + context.getJSModule(RCTEventEmitter.class) + .receiveEvent(view.getId(), name, data); + } + + + @Override + public void onDropViewInstance(AirMapView view) { + view.doDestroy(); + super.onDropViewInstance(view); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java index 4041d91ac..52c4dcd08 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarker.java @@ -36,395 +36,395 @@ public class AirMapMarker extends AirMapFeature { - private MarkerOptions markerOptions; - private Marker marker; - private int width; - private int height; - private String identifier; - - private LatLng position; - private String title; - private String snippet; - - private boolean anchorIsSet; - private float anchorX; - private float anchorY; - - private AirMapCallout calloutView; - private View wrappedCalloutView; - private final Context context; - - private float markerHue = 0.0f; // should be between 0 and 360 - private BitmapDescriptor iconBitmapDescriptor; - private Bitmap iconBitmap; - - private float rotation = 0.0f; - private boolean flat = false; - private boolean draggable = false; - private int zIndex = 0; - private float opacity = 1.0f; - - private float calloutAnchorX; - private float calloutAnchorY; - private boolean calloutAnchorIsSet; - - private boolean hasCustomMarkerView = false; - - private final DraweeHolder logoHolder; - private DataSource> dataSource; - private final ControllerListener mLogoControllerListener = - new BaseControllerListener() { - @Override - public void onFinalImageSet( - String id, - @Nullable final ImageInfo imageInfo, - @Nullable Animatable animatable) { - CloseableReference imageReference = null; - try { - imageReference = dataSource.getResult(); - if (imageReference != null) { - CloseableImage image = imageReference.get(); - if (image != null && image instanceof CloseableStaticBitmap) { - CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) image; - Bitmap bitmap = closeableStaticBitmap.getUnderlyingBitmap(); - if (bitmap != null) { - bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); - iconBitmap = bitmap; - iconBitmapDescriptor = BitmapDescriptorFactory.fromBitmap(bitmap); - } - } - } - } finally { - dataSource.close(); - if (imageReference != null) { - CloseableReference.closeSafely(imageReference); - } - } - update(); + private MarkerOptions markerOptions; + private Marker marker; + private int width; + private int height; + private String identifier; + + private LatLng position; + private String title; + private String snippet; + + private boolean anchorIsSet; + private float anchorX; + private float anchorY; + + private AirMapCallout calloutView; + private View wrappedCalloutView; + private final Context context; + + private float markerHue = 0.0f; // should be between 0 and 360 + private BitmapDescriptor iconBitmapDescriptor; + private Bitmap iconBitmap; + + private float rotation = 0.0f; + private boolean flat = false; + private boolean draggable = false; + private int zIndex = 0; + private float opacity = 1.0f; + + private float calloutAnchorX; + private float calloutAnchorY; + private boolean calloutAnchorIsSet; + + private boolean hasCustomMarkerView = false; + + private final DraweeHolder logoHolder; + private DataSource> dataSource; + private final ControllerListener mLogoControllerListener = + new BaseControllerListener() { + @Override + public void onFinalImageSet( + String id, + @Nullable final ImageInfo imageInfo, + @Nullable Animatable animatable) { + CloseableReference imageReference = null; + try { + imageReference = dataSource.getResult(); + if (imageReference != null) { + CloseableImage image = imageReference.get(); + if (image != null && image instanceof CloseableStaticBitmap) { + CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) image; + Bitmap bitmap = closeableStaticBitmap.getUnderlyingBitmap(); + if (bitmap != null) { + bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true); + iconBitmap = bitmap; + iconBitmapDescriptor = BitmapDescriptorFactory.fromBitmap(bitmap); } - }; - - public AirMapMarker(Context context) { - super(context); - this.context = context; - logoHolder = DraweeHolder.create(createDraweeHierarchy(), context); - logoHolder.onAttach(); - } - - private GenericDraweeHierarchy createDraweeHierarchy() { - return new GenericDraweeHierarchyBuilder(getResources()) - .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) - .setFadeDuration(0) - .build(); - } - - public void setCoordinate(ReadableMap coordinate) { - position = new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude")); - if (marker != null) { - marker.setPosition(position); - } - update(); - } - - public void setIdentifier(String identifier) { - this.identifier = identifier; - update(); - } - - public String getIdentifier() { - return this.identifier; - } - - public void setTitle(String title) { - this.title = title; - if (marker != null) { - marker.setTitle(title); - } - update(); - } - - public void setSnippet(String snippet) { - this.snippet = snippet; - if (marker != null) { - marker.setSnippet(snippet); - } - update(); - } - - public void setRotation(float rotation) { - this.rotation = rotation; - if (marker != null) { - marker.setRotation(rotation); - } - update(); - } - - public void setFlat(boolean flat) { - this.flat = flat; - if (marker != null) { - marker.setFlat(flat); - } - update(); - } - - public void setDraggable(boolean draggable) { - this.draggable = draggable; - if (marker != null) { - marker.setDraggable(draggable); - } - update(); - } - - public void setZIndex(int zIndex) { - this.zIndex = zIndex; - if (marker != null) { - marker.setZIndex(zIndex); - } - update(); - } - - public void setOpacity(float opacity) { - this.opacity = opacity; - if (marker != null) { - marker.setAlpha(opacity); - } - update(); - } - - public void setMarkerHue(float markerHue) { - this.markerHue = markerHue; - update(); - } - - public void setAnchor(double x, double y) { - anchorIsSet = true; - anchorX = (float) x; - anchorY = (float) y; - if (marker != null) { - marker.setAnchor(anchorX, anchorY); - } - update(); - } - - public void setCalloutAnchor(double x, double y) { - calloutAnchorIsSet = true; - calloutAnchorX = (float) x; - calloutAnchorY = (float) y; - if (marker != null) { - marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); - } - update(); - } - - public void setImage(String uri) { - if (uri == null) { - iconBitmapDescriptor = null; - update(); - } else if (uri.startsWith("http://") || uri.startsWith("https://") || - uri.startsWith("file://")) { - ImageRequest imageRequest = ImageRequestBuilder - .newBuilderWithSource(Uri.parse(uri)) - .build(); - - ImagePipeline imagePipeline = Fresco.getImagePipeline(); - dataSource = imagePipeline.fetchDecodedImage(imageRequest, this); - DraweeController controller = Fresco.newDraweeControllerBuilder() - .setImageRequest(imageRequest) - .setControllerListener(mLogoControllerListener) - .setOldController(logoHolder.getController()) - .build(); - logoHolder.setController(controller); - } else { - iconBitmapDescriptor = getBitmapDescriptorByName(uri); - update(); - } - } - - public MarkerOptions getMarkerOptions() { - if (markerOptions == null) { - markerOptions = createMarkerOptions(); - } - return markerOptions; - } - - @Override - public void addView(View child, int index) { - super.addView(child, index); - // if children are added, it means we are rendering a custom marker - if (!(child instanceof AirMapCallout)) { - hasCustomMarkerView = true; - } - update(); - } - - @Override - public Object getFeature() { - return marker; - } - - @Override - public void addToMap(GoogleMap map) { - marker = map.addMarker(getMarkerOptions()); - } - - @Override - public void removeFromMap(GoogleMap map) { - marker.remove(); - marker = null; - } - - private BitmapDescriptor getIcon() { - if (hasCustomMarkerView) { - // creating a bitmap from an arbitrary view - if (iconBitmapDescriptor != null) { - Bitmap viewBitmap = createDrawable(); - int width = Math.max(iconBitmap.getWidth(), viewBitmap.getWidth()); - int height = Math.max(iconBitmap.getHeight(), viewBitmap.getHeight()); - Bitmap combinedBitmap = Bitmap.createBitmap(width, height, iconBitmap.getConfig()); - Canvas canvas = new Canvas(combinedBitmap); - canvas.drawBitmap(iconBitmap, 0, 0, null); - canvas.drawBitmap(viewBitmap, 0, 0, null); - return BitmapDescriptorFactory.fromBitmap(combinedBitmap); - } else { - return BitmapDescriptorFactory.fromBitmap(createDrawable()); + } } - } else if (iconBitmapDescriptor != null) { - // use local image as a marker - return iconBitmapDescriptor; - } else { - // render the default marker pin - return BitmapDescriptorFactory.defaultMarker(this.markerHue); - } - } - - private MarkerOptions createMarkerOptions() { - MarkerOptions options = new MarkerOptions().position(position); - if (anchorIsSet) options.anchor(anchorX, anchorY); - if (calloutAnchorIsSet) options.infoWindowAnchor(calloutAnchorX, calloutAnchorY); - options.title(title); - options.snippet(snippet); - options.rotation(rotation); - options.flat(flat); - options.draggable(draggable); - options.zIndex(zIndex); - options.alpha(opacity); - options.icon(getIcon()); - return options; - } - - public void update() { - if (marker == null) { - return; - } - - marker.setIcon(getIcon()); - - if (anchorIsSet) { - marker.setAnchor(anchorX, anchorY); - } else { - marker.setAnchor(0.5f, 1.0f); - } - - if (calloutAnchorIsSet) { - marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); - } else { - marker.setInfoWindowAnchor(0.5f, 0); - } - } - - public void update(int width, int height) { - this.width = width; - this.height = height; - update(); - } - - private Bitmap createDrawable() { - int width = this.width <= 0 ? 100 : this.width; - int height = this.height <= 0 ? 100 : this.height; - this.buildDrawingCache(); - Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); - - Canvas canvas = new Canvas(bitmap); - this.draw(canvas); - - return bitmap; - } - - public void setCalloutView(AirMapCallout view) { - this.calloutView = view; - } - - public AirMapCallout getCalloutView() { - return this.calloutView; - } - - public View getCallout() { - if (this.calloutView == null) return null; - - if (this.wrappedCalloutView == null) { - this.wrapCalloutView(); - } - - if (this.calloutView.getTooltip()) { - return this.wrappedCalloutView; - } else { - return null; - } - } - - public View getInfoContents() { - if (this.calloutView == null) return null; - - if (this.wrappedCalloutView == null) { - this.wrapCalloutView(); - } - - if (this.calloutView.getTooltip()) { - return null; - } else { - return this.wrappedCalloutView; + } finally { + dataSource.close(); + if (imageReference != null) { + CloseableReference.closeSafely(imageReference); + } + } + update(); } + }; + + public AirMapMarker(Context context) { + super(context); + this.context = context; + logoHolder = DraweeHolder.create(createDraweeHierarchy(), context); + logoHolder.onAttach(); + } + + private GenericDraweeHierarchy createDraweeHierarchy() { + return new GenericDraweeHierarchyBuilder(getResources()) + .setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER) + .setFadeDuration(0) + .build(); + } + + public void setCoordinate(ReadableMap coordinate) { + position = new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude")); + if (marker != null) { + marker.setPosition(position); + } + update(); + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + update(); + } + + public String getIdentifier() { + return this.identifier; + } + + public void setTitle(String title) { + this.title = title; + if (marker != null) { + marker.setTitle(title); + } + update(); + } + + public void setSnippet(String snippet) { + this.snippet = snippet; + if (marker != null) { + marker.setSnippet(snippet); + } + update(); + } + + public void setRotation(float rotation) { + this.rotation = rotation; + if (marker != null) { + marker.setRotation(rotation); + } + update(); + } + + public void setFlat(boolean flat) { + this.flat = flat; + if (marker != null) { + marker.setFlat(flat); + } + update(); + } + + public void setDraggable(boolean draggable) { + this.draggable = draggable; + if (marker != null) { + marker.setDraggable(draggable); + } + update(); + } + + public void setZIndex(int zIndex) { + this.zIndex = zIndex; + if (marker != null) { + marker.setZIndex(zIndex); + } + update(); + } + + public void setOpacity(float opacity) { + this.opacity = opacity; + if (marker != null) { + marker.setAlpha(opacity); + } + update(); + } + + public void setMarkerHue(float markerHue) { + this.markerHue = markerHue; + update(); + } + + public void setAnchor(double x, double y) { + anchorIsSet = true; + anchorX = (float) x; + anchorY = (float) y; + if (marker != null) { + marker.setAnchor(anchorX, anchorY); + } + update(); + } + + public void setCalloutAnchor(double x, double y) { + calloutAnchorIsSet = true; + calloutAnchorX = (float) x; + calloutAnchorY = (float) y; + if (marker != null) { + marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); + } + update(); + } + + public void setImage(String uri) { + if (uri == null) { + iconBitmapDescriptor = null; + update(); + } else if (uri.startsWith("http://") || uri.startsWith("https://") || + uri.startsWith("file://")) { + ImageRequest imageRequest = ImageRequestBuilder + .newBuilderWithSource(Uri.parse(uri)) + .build(); + + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + dataSource = imagePipeline.fetchDecodedImage(imageRequest, this); + DraweeController controller = Fresco.newDraweeControllerBuilder() + .setImageRequest(imageRequest) + .setControllerListener(mLogoControllerListener) + .setOldController(logoHolder.getController()) + .build(); + logoHolder.setController(controller); + } else { + iconBitmapDescriptor = getBitmapDescriptorByName(uri); + update(); + } + } + + public MarkerOptions getMarkerOptions() { + if (markerOptions == null) { + markerOptions = createMarkerOptions(); + } + return markerOptions; + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + // if children are added, it means we are rendering a custom marker + if (!(child instanceof AirMapCallout)) { + hasCustomMarkerView = true; + } + update(); + } + + @Override + public Object getFeature() { + return marker; + } + + @Override + public void addToMap(GoogleMap map) { + marker = map.addMarker(getMarkerOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + marker.remove(); + marker = null; + } + + private BitmapDescriptor getIcon() { + if (hasCustomMarkerView) { + // creating a bitmap from an arbitrary view + if (iconBitmapDescriptor != null) { + Bitmap viewBitmap = createDrawable(); + int width = Math.max(iconBitmap.getWidth(), viewBitmap.getWidth()); + int height = Math.max(iconBitmap.getHeight(), viewBitmap.getHeight()); + Bitmap combinedBitmap = Bitmap.createBitmap(width, height, iconBitmap.getConfig()); + Canvas canvas = new Canvas(combinedBitmap); + canvas.drawBitmap(iconBitmap, 0, 0, null); + canvas.drawBitmap(viewBitmap, 0, 0, null); + return BitmapDescriptorFactory.fromBitmap(combinedBitmap); + } else { + return BitmapDescriptorFactory.fromBitmap(createDrawable()); + } + } else if (iconBitmapDescriptor != null) { + // use local image as a marker + return iconBitmapDescriptor; + } else { + // render the default marker pin + return BitmapDescriptorFactory.defaultMarker(this.markerHue); + } + } + + private MarkerOptions createMarkerOptions() { + MarkerOptions options = new MarkerOptions().position(position); + if (anchorIsSet) options.anchor(anchorX, anchorY); + if (calloutAnchorIsSet) options.infoWindowAnchor(calloutAnchorX, calloutAnchorY); + options.title(title); + options.snippet(snippet); + options.rotation(rotation); + options.flat(flat); + options.draggable(draggable); + options.zIndex(zIndex); + options.alpha(opacity); + options.icon(getIcon()); + return options; + } + + public void update() { + if (marker == null) { + return; + } + + marker.setIcon(getIcon()); + + if (anchorIsSet) { + marker.setAnchor(anchorX, anchorY); + } else { + marker.setAnchor(0.5f, 1.0f); + } + + if (calloutAnchorIsSet) { + marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); + } else { + marker.setInfoWindowAnchor(0.5f, 0); + } + } + + public void update(int width, int height) { + this.width = width; + this.height = height; + update(); + } + + private Bitmap createDrawable() { + int width = this.width <= 0 ? 100 : this.width; + int height = this.height <= 0 ? 100 : this.height; + this.buildDrawingCache(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + this.draw(canvas); + + return bitmap; + } + + public void setCalloutView(AirMapCallout view) { + this.calloutView = view; + } + + public AirMapCallout getCalloutView() { + return this.calloutView; + } + + public View getCallout() { + if (this.calloutView == null) return null; + + if (this.wrappedCalloutView == null) { + this.wrapCalloutView(); + } + + if (this.calloutView.getTooltip()) { + return this.wrappedCalloutView; + } else { + return null; + } + } + + public View getInfoContents() { + if (this.calloutView == null) return null; + + if (this.wrappedCalloutView == null) { + this.wrapCalloutView(); + } + + if (this.calloutView.getTooltip()) { + return null; + } else { + return this.wrappedCalloutView; + } + } + + private void wrapCalloutView() { + // some hackery is needed to get the arbitrary infowindow view to render centered, and + // with only the width/height that it needs. + if (this.calloutView == null || this.calloutView.getChildCount() == 0) { + return; } - private void wrapCalloutView() { - // some hackery is needed to get the arbitrary infowindow view to render centered, and - // with only the width/height that it needs. - if (this.calloutView == null || this.calloutView.getChildCount() == 0) { - return; - } + LinearLayout LL = new LinearLayout(context); + LL.setOrientation(LinearLayout.VERTICAL); + LL.setLayoutParams(new LinearLayout.LayoutParams( + this.calloutView.width, + this.calloutView.height, + 0f + )); - LinearLayout LL = new LinearLayout(context); - LL.setOrientation(LinearLayout.VERTICAL); - LL.setLayoutParams(new LinearLayout.LayoutParams( - this.calloutView.width, - this.calloutView.height, - 0f - )); + LinearLayout LL2 = new LinearLayout(context); + LL2.setOrientation(LinearLayout.HORIZONTAL); + LL2.setLayoutParams(new LinearLayout.LayoutParams( + this.calloutView.width, + this.calloutView.height, + 0f + )); - LinearLayout LL2 = new LinearLayout(context); - LL2.setOrientation(LinearLayout.HORIZONTAL); - LL2.setLayoutParams(new LinearLayout.LayoutParams( - this.calloutView.width, - this.calloutView.height, - 0f - )); + LL.addView(LL2); + LL2.addView(this.calloutView); - LL.addView(LL2); - LL2.addView(this.calloutView); + this.wrappedCalloutView = LL; + } - this.wrappedCalloutView = LL; - } + private int getDrawableResourceByName(String name) { + return getResources().getIdentifier( + name, + "drawable", + getContext().getPackageName()); + } - private int getDrawableResourceByName(String name) { - return getResources().getIdentifier( - name, - "drawable", - getContext().getPackageName()); - } - - private BitmapDescriptor getBitmapDescriptorByName(String name) { - return BitmapDescriptorFactory.fromResource(getDrawableResourceByName(name)); - } + private BitmapDescriptor getBitmapDescriptorByName(String name) { + return BitmapDescriptorFactory.fromResource(getDrawableResourceByName(name)); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java index e035e1e14..3aef3148c 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapMarkerManager.java @@ -19,46 +19,46 @@ public class AirMapMarkerManager extends ViewGroupManager { - private static final int SHOW_INFO_WINDOW = 1; - private static final int HIDE_INFO_WINDOW = 2; - - public AirMapMarkerManager() { - } - - @Override - public String getName() { - return "AIRMapMarker"; - } - - @Override - public AirMapMarker createViewInstance(ThemedReactContext context) { - return new AirMapMarker(context); - } - - @ReactProp(name = "coordinate") - public void setCoordinate(AirMapMarker view, ReadableMap map) { - view.setCoordinate(map); - } - - @ReactProp(name = "title") - public void setTitle(AirMapMarker view, String title) { - view.setTitle(title); - } - - @ReactProp(name = "identifier") - public void setIdentifier(AirMapMarker view, String identifier) { - view.setIdentifier(identifier); - } - - @ReactProp(name = "description") - public void setDescription(AirMapMarker view, String description) { - view.setSnippet(description); - } - - // NOTE(lmr): - // android uses normalized coordinate systems for this, and is provided through the - // `anchor` property and `calloutAnchor` instead. Perhaps some work could be done - // to normalize iOS and android to use just one of the systems. + private static final int SHOW_INFO_WINDOW = 1; + private static final int HIDE_INFO_WINDOW = 2; + + public AirMapMarkerManager() { + } + + @Override + public String getName() { + return "AIRMapMarker"; + } + + @Override + public AirMapMarker createViewInstance(ThemedReactContext context) { + return new AirMapMarker(context); + } + + @ReactProp(name = "coordinate") + public void setCoordinate(AirMapMarker view, ReadableMap map) { + view.setCoordinate(map); + } + + @ReactProp(name = "title") + public void setTitle(AirMapMarker view, String title) { + view.setTitle(title); + } + + @ReactProp(name = "identifier") + public void setIdentifier(AirMapMarker view, String identifier) { + view.setIdentifier(identifier); + } + + @ReactProp(name = "description") + public void setDescription(AirMapMarker view, String description) { + view.setSnippet(description); + } + + // NOTE(lmr): + // android uses normalized coordinate systems for this, and is provided through the + // `anchor` property and `calloutAnchor` instead. Perhaps some work could be done + // to normalize iOS and android to use just one of the systems. // @ReactProp(name = "centerOffset") // public void setCenterOffset(AirMapMarker view, ReadableMap map) { // @@ -69,143 +69,143 @@ public void setDescription(AirMapMarker view, String description) { // // } - @ReactProp(name = "anchor") - public void setAnchor(AirMapMarker view, ReadableMap map) { - // should default to (0.5, 1) (bottom middle) - double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; - double y = map != null && map.hasKey("y") ? map.getDouble("y") : 1.0; - view.setAnchor(x, y); - } - - @ReactProp(name = "calloutAnchor") - public void setCalloutAnchor(AirMapMarker view, ReadableMap map) { - // should default to (0.5, 0) (top middle) - double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; - double y = map != null && map.hasKey("y") ? map.getDouble("y") : 0.0; - view.setCalloutAnchor(x, y); - } - - @ReactProp(name = "image") - public void setImage(AirMapMarker view, @Nullable String source) { - view.setImage(source); - } + @ReactProp(name = "anchor") + public void setAnchor(AirMapMarker view, ReadableMap map) { + // should default to (0.5, 1) (bottom middle) + double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; + double y = map != null && map.hasKey("y") ? map.getDouble("y") : 1.0; + view.setAnchor(x, y); + } + + @ReactProp(name = "calloutAnchor") + public void setCalloutAnchor(AirMapMarker view, ReadableMap map) { + // should default to (0.5, 0) (top middle) + double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; + double y = map != null && map.hasKey("y") ? map.getDouble("y") : 0.0; + view.setCalloutAnchor(x, y); + } + + @ReactProp(name = "image") + public void setImage(AirMapMarker view, @Nullable String source) { + view.setImage(source); + } // public void setImage(AirMapMarker view, ReadableMap image) { // view.setImage(image); // } - @ReactProp(name = "pinColor", defaultInt = Color.RED, customType = "Color") - public void setPinColor(AirMapMarker view, int pinColor) { - float[] hsv = new float[3]; - Color.colorToHSV(pinColor, hsv); - // NOTE: android only supports a hue - view.setMarkerHue(hsv[0]); - } - - @ReactProp(name = "rotation", defaultFloat = 0.0f) - public void setMarkerRotation(AirMapMarker view, float rotation) { - view.setRotation(rotation); - } - - @ReactProp(name = "flat", defaultBoolean = false) - public void setFlat(AirMapMarker view, boolean flat) { - view.setFlat(flat); - } - - @ReactProp(name = "draggable", defaultBoolean = false) - public void setDraggable(AirMapMarker view, boolean draggable) { - view.setDraggable(draggable); - } - - @Override - @ReactProp(name = "zIndex", defaultFloat = 0.0f) - public void setZIndex(AirMapMarker view, float zIndex) { - super.setZIndex(view, zIndex); - int integerZIndex = Math.round(zIndex); - view.setZIndex(integerZIndex); - } - - @Override - @ReactProp(name = "opacity", defaultFloat = 1.0f) - public void setOpacity(AirMapMarker view, float opacity) { - super.setOpacity(view, opacity); - view.setOpacity(opacity); - } - - @Override - public void addView(AirMapMarker parent, View child, int index) { - // if an component is a child, then it is a callout view, NOT part of the - // marker. - if (child instanceof AirMapCallout) { - parent.setCalloutView((AirMapCallout) child); - } else { - super.addView(parent, child, index); - parent.update(); - } - } - - @Override - public void removeViewAt(AirMapMarker parent, int index) { - super.removeViewAt(parent, index); - parent.update(); - } - - @Override - @Nullable - public Map getCommandsMap() { - return MapBuilder.of( - "showCallout", SHOW_INFO_WINDOW, - "hideCallout", HIDE_INFO_WINDOW - ); - } - - @Override - public void receiveCommand(AirMapMarker view, int commandId, @Nullable ReadableArray args) { - switch (commandId) { - case SHOW_INFO_WINDOW: - ((Marker) view.getFeature()).showInfoWindow(); - break; - - case HIDE_INFO_WINDOW: - ((Marker) view.getFeature()).hideInfoWindow(); - break; - } - } - - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - Map> map = MapBuilder.of( - "onPress", MapBuilder.of("registrationName", "onPress"), - "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress"), - "onDragStart", MapBuilder.of("registrationName", "onDragStart"), - "onDrag", MapBuilder.of("registrationName", "onDrag"), - "onDragEnd", MapBuilder.of("registrationName", "onDragEnd") - ); - - map.putAll(MapBuilder.of( - "onDragStart", MapBuilder.of("registrationName", "onDragStart"), - "onDrag", MapBuilder.of("registrationName", "onDrag"), - "onDragEnd", MapBuilder.of("registrationName", "onDragEnd") - )); - - return map; - } - - @Override - public LayoutShadowNode createShadowNodeInstance() { - // we use a custom shadow node that emits the width/height of the view - // after layout with the updateExtraData method. Without this, we can't generate - // a bitmap of the appropriate width/height of the rendered view. - return new SizeReportingShadowNode(); - } - - @Override - public void updateExtraData(AirMapMarker view, Object extraData) { - // This method is called from the shadow node with the width/height of the rendered - // marker view. - HashMap data = (HashMap) extraData; - float width = data.get("width"); - float height = data.get("height"); - view.update((int) width, (int) height); - } + @ReactProp(name = "pinColor", defaultInt = Color.RED, customType = "Color") + public void setPinColor(AirMapMarker view, int pinColor) { + float[] hsv = new float[3]; + Color.colorToHSV(pinColor, hsv); + // NOTE: android only supports a hue + view.setMarkerHue(hsv[0]); + } + + @ReactProp(name = "rotation", defaultFloat = 0.0f) + public void setMarkerRotation(AirMapMarker view, float rotation) { + view.setRotation(rotation); + } + + @ReactProp(name = "flat", defaultBoolean = false) + public void setFlat(AirMapMarker view, boolean flat) { + view.setFlat(flat); + } + + @ReactProp(name = "draggable", defaultBoolean = false) + public void setDraggable(AirMapMarker view, boolean draggable) { + view.setDraggable(draggable); + } + + @Override + @ReactProp(name = "zIndex", defaultFloat = 0.0f) + public void setZIndex(AirMapMarker view, float zIndex) { + super.setZIndex(view, zIndex); + int integerZIndex = Math.round(zIndex); + view.setZIndex(integerZIndex); + } + + @Override + @ReactProp(name = "opacity", defaultFloat = 1.0f) + public void setOpacity(AirMapMarker view, float opacity) { + super.setOpacity(view, opacity); + view.setOpacity(opacity); + } + + @Override + public void addView(AirMapMarker parent, View child, int index) { + // if an component is a child, then it is a callout view, NOT part of the + // marker. + if (child instanceof AirMapCallout) { + parent.setCalloutView((AirMapCallout) child); + } else { + super.addView(parent, child, index); + parent.update(); + } + } + + @Override + public void removeViewAt(AirMapMarker parent, int index) { + super.removeViewAt(parent, index); + parent.update(); + } + + @Override + @Nullable + public Map getCommandsMap() { + return MapBuilder.of( + "showCallout", SHOW_INFO_WINDOW, + "hideCallout", HIDE_INFO_WINDOW + ); + } + + @Override + public void receiveCommand(AirMapMarker view, int commandId, @Nullable ReadableArray args) { + switch (commandId) { + case SHOW_INFO_WINDOW: + ((Marker) view.getFeature()).showInfoWindow(); + break; + + case HIDE_INFO_WINDOW: + ((Marker) view.getFeature()).hideInfoWindow(); + break; + } + } + + @Override + @Nullable + public Map getExportedCustomDirectEventTypeConstants() { + Map> map = MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress"), + "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress"), + "onDragStart", MapBuilder.of("registrationName", "onDragStart"), + "onDrag", MapBuilder.of("registrationName", "onDrag"), + "onDragEnd", MapBuilder.of("registrationName", "onDragEnd") + ); + + map.putAll(MapBuilder.of( + "onDragStart", MapBuilder.of("registrationName", "onDragStart"), + "onDrag", MapBuilder.of("registrationName", "onDrag"), + "onDragEnd", MapBuilder.of("registrationName", "onDragEnd") + )); + + return map; + } + + @Override + public LayoutShadowNode createShadowNodeInstance() { + // we use a custom shadow node that emits the width/height of the view + // after layout with the updateExtraData method. Without this, we can't generate + // a bitmap of the appropriate width/height of the rendered view. + return new SizeReportingShadowNode(); + } + + @Override + public void updateExtraData(AirMapMarker view, Object extraData) { + // This method is called from the shadow node with the width/height of the rendered + // marker view. + HashMap data = (HashMap) extraData; + float width = data.get("width"); + float height = data.get("height"); + view.update((int) width, (int) height); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java index 2cd1ba686..1a096255e 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapModule.java @@ -1,128 +1,127 @@ package com.airbnb.android.react.maps; import android.app.Activity; -import android.util.DisplayMetrics; -import android.util.Base64; import android.graphics.Bitmap; import android.net.Uri; -import android.view.View; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.OutputStream; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Closeable; - -import javax.annotation.Nullable; +import android.util.Base64; +import android.util.DisplayMetrics; +import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; -import com.facebook.react.bridge.ReadableMap; -import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.uimanager.UIManagerModule; -import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.uimanager.NativeViewHierarchyManager; - +import com.facebook.react.uimanager.UIBlock; +import com.facebook.react.uimanager.UIManagerModule; import com.google.android.gms.maps.GoogleMap; -public class AirMapModule extends ReactContextBaseJavaModule { - - private static final String SNAPSHOT_RESULT_FILE = "file"; - private static final String SNAPSHOT_RESULT_BASE64 = "base64"; - private static final String SNAPSHOT_FORMAT_PNG = "png"; - private static final String SNAPSHOT_FORMAT_JPG = "jpg"; - - public AirMapModule(ReactApplicationContext reactContext) { - super(reactContext); - } +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; - @Override - public String getName() { - return "AirMapModule"; - } +import javax.annotation.Nullable; - public Activity getActivity() { - return getCurrentActivity(); - } +public class AirMapModule extends ReactContextBaseJavaModule { - public static void closeQuietly(Closeable closeable) { - if (closeable == null) return; - try { - closeable.close(); - } catch (IOException ignored) { - } + private static final String SNAPSHOT_RESULT_FILE = "file"; + private static final String SNAPSHOT_RESULT_BASE64 = "base64"; + private static final String SNAPSHOT_FORMAT_PNG = "png"; + private static final String SNAPSHOT_FORMAT_JPG = "jpg"; + + public AirMapModule(ReactApplicationContext reactContext) { + super(reactContext); + } + + @Override + public String getName() { + return "AirMapModule"; + } + + public Activity getActivity() { + return getCurrentActivity(); + } + + public static void closeQuietly(Closeable closeable) { + if (closeable == null) return; + try { + closeable.close(); + } catch (IOException ignored) { } + } - @ReactMethod - public void takeSnapshot(final int tag, final ReadableMap options, final Promise promise) { + @ReactMethod + public void takeSnapshot(final int tag, final ReadableMap options, final Promise promise) { - // Parse and verity options - final ReactApplicationContext context = getReactApplicationContext(); - final String format = options.hasKey("format") ? options.getString("format") : "png"; - final Bitmap.CompressFormat compressFormat = - format.equals(SNAPSHOT_FORMAT_PNG) ? Bitmap.CompressFormat.PNG : + // Parse and verity options + final ReactApplicationContext context = getReactApplicationContext(); + final String format = options.hasKey("format") ? options.getString("format") : "png"; + final Bitmap.CompressFormat compressFormat = + format.equals(SNAPSHOT_FORMAT_PNG) ? Bitmap.CompressFormat.PNG : format.equals(SNAPSHOT_FORMAT_JPG) ? Bitmap.CompressFormat.JPEG : null; - final double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0; - final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); - final Integer width = options.hasKey("width") ? (int)(displayMetrics.density * options.getDouble("width")) : 0; - final Integer height = options.hasKey("height") ? (int)(displayMetrics.density * options.getDouble("height")) : 0; - final String result = options.hasKey("result") ? options.getString("result") : "file"; - - // Add UI-block so we can get a valid reference to the map-view - UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); - uiManager.addUIBlock(new UIBlock() { - public void execute (NativeViewHierarchyManager nvhm) { - AirMapView view = (AirMapView) nvhm.resolveView(tag); - if (view == null) { - promise.reject("AirMapView not found"); - return; - } - if (view.map == null) { - promise.reject("AirMapView.map is not valid"); - return; - } - view.map.snapshot(new GoogleMap.SnapshotReadyCallback() { - public void onSnapshotReady(@Nullable Bitmap snapshot) { + final double quality = options.hasKey("quality") ? options.getDouble("quality") : 1.0; + final DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics(); + final Integer width = + options.hasKey("width") ? (int) (displayMetrics.density * options.getDouble("width")) : 0; + final Integer height = + options.hasKey("height") ? (int) (displayMetrics.density * options.getDouble("height")) : 0; + final String result = options.hasKey("result") ? options.getString("result") : "file"; + + // Add UI-block so we can get a valid reference to the map-view + UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class); + uiManager.addUIBlock(new UIBlock() { + public void execute(NativeViewHierarchyManager nvhm) { + AirMapView view = (AirMapView) nvhm.resolveView(tag); + if (view == null) { + promise.reject("AirMapView not found"); + return; + } + if (view.map == null) { + promise.reject("AirMapView.map is not valid"); + return; + } + view.map.snapshot(new GoogleMap.SnapshotReadyCallback() { + public void onSnapshotReady(@Nullable Bitmap snapshot) { - // Convert image to requested width/height if neccesary - if (snapshot == null) { - promise.reject("Failed to generate bitmap, snapshot = null"); - return; - } - if ((width != 0) && (height != 0) && (width != snapshot.getWidth() || height != snapshot.getHeight())) { - snapshot = Bitmap.createScaledBitmap(snapshot, width, height, true); - } + // Convert image to requested width/height if necessary + if (snapshot == null) { + promise.reject("Failed to generate bitmap, snapshot = null"); + return; + } + if ((width != 0) && (height != 0) && + (width != snapshot.getWidth() || height != snapshot.getHeight())) { + snapshot = Bitmap.createScaledBitmap(snapshot, width, height, true); + } - // Save the snapshot to disk - if (result.equals(SNAPSHOT_RESULT_FILE)) { - File tempFile; - FileOutputStream outputStream; - try { - tempFile = File.createTempFile("AirMapSnapshot", "." + format, context.getCacheDir()); - outputStream = new FileOutputStream(tempFile); - } - catch (Exception e) { - promise.reject(e); - return; - } - snapshot.compress(compressFormat, (int)(100.0 * quality), outputStream); - closeQuietly(outputStream); - String uri = Uri.fromFile(tempFile).toString(); - promise.resolve(uri); - } - else if (result.equals(SNAPSHOT_RESULT_BASE64)) { - ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); - snapshot.compress(compressFormat, (int)(100.0 * quality), outputStream); - closeQuietly(outputStream); - byte[] bytes = outputStream.toByteArray(); - String data = Base64.encodeToString(bytes, Base64.NO_WRAP); - promise.resolve(data); - } - } - }); + // Save the snapshot to disk + if (result.equals(SNAPSHOT_RESULT_FILE)) { + File tempFile; + FileOutputStream outputStream; + try { + tempFile = + File.createTempFile("AirMapSnapshot", "." + format, context.getCacheDir()); + outputStream = new FileOutputStream(tempFile); + } catch (Exception e) { + promise.reject(e); + return; + } + snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream); + closeQuietly(outputStream); + String uri = Uri.fromFile(tempFile).toString(); + promise.resolve(uri); + } else if (result.equals(SNAPSHOT_RESULT_BASE64)) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream); + closeQuietly(outputStream); + byte[] bytes = outputStream.toByteArray(); + String data = Base64.encodeToString(bytes, Base64.NO_WRAP); + promise.resolve(data); } + } }); - } + } + }); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygon.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygon.java index 226bc241c..41257b32e 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygon.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygon.java @@ -14,99 +14,99 @@ public class AirMapPolygon extends AirMapFeature { - private PolygonOptions polygonOptions; - private Polygon polygon; - - private List coordinates; - private int strokeColor; - private int fillColor; - private float strokeWidth; - private boolean geodesic; - private float zIndex; - - public AirMapPolygon(Context context) { - super(context); + private PolygonOptions polygonOptions; + private Polygon polygon; + + private List coordinates; + private int strokeColor; + private int fillColor; + private float strokeWidth; + private boolean geodesic; + private float zIndex; + + public AirMapPolygon(Context context) { + super(context); + } + + public void setCoordinates(ReadableArray coordinates) { + // it's kind of a bummer that we can't run map() or anything on the ReadableArray + this.coordinates = new ArrayList<>(coordinates.size()); + for (int i = 0; i < coordinates.size(); i++) { + ReadableMap coordinate = coordinates.getMap(i); + this.coordinates.add(i, + new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); } - - public void setCoordinates(ReadableArray coordinates) { - // it's kind of a bummer that we can't run map() or anything on the ReadableArray - this.coordinates = new ArrayList<>(coordinates.size()); - for (int i = 0; i < coordinates.size(); i++) { - ReadableMap coordinate = coordinates.getMap(i); - this.coordinates.add(i, - new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); - } - if (polygon != null) { - polygon.setPoints(this.coordinates); - } - } - - public void setFillColor(int color) { - this.fillColor = color; - if (polygon != null) { - polygon.setFillColor(color); - } - } - - public void setStrokeColor(int color) { - this.strokeColor = color; - if (polygon != null) { - polygon.setStrokeColor(color); - } - } - - public void setStrokeWidth(float width) { - this.strokeWidth = width; - if (polygon != null) { - polygon.setStrokeWidth(width); - } - } - - public void setGeodesic(boolean geodesic) { - this.geodesic = geodesic; - if (polygon != null) { - polygon.setGeodesic(geodesic); - } + if (polygon != null) { + polygon.setPoints(this.coordinates); } + } - public void setZIndex(float zIndex) { - this.zIndex = zIndex; - if (polygon != null) { - polygon.setZIndex(zIndex); - } + public void setFillColor(int color) { + this.fillColor = color; + if (polygon != null) { + polygon.setFillColor(color); } + } - public PolygonOptions getPolygonOptions() { - if (polygonOptions == null) { - polygonOptions = createPolygonOptions(); - } - return polygonOptions; + public void setStrokeColor(int color) { + this.strokeColor = color; + if (polygon != null) { + polygon.setStrokeColor(color); } + } - private PolygonOptions createPolygonOptions() { - PolygonOptions options = new PolygonOptions(); - options.addAll(coordinates); - options.fillColor(fillColor); - options.strokeColor(strokeColor); - options.strokeWidth(strokeWidth); - options.geodesic(geodesic); - options.zIndex(zIndex); - return options; + public void setStrokeWidth(float width) { + this.strokeWidth = width; + if (polygon != null) { + polygon.setStrokeWidth(width); } + } - @Override - public Object getFeature() { - return polygon; + public void setGeodesic(boolean geodesic) { + this.geodesic = geodesic; + if (polygon != null) { + polygon.setGeodesic(geodesic); } + } - @Override - public void addToMap(GoogleMap map) { - polygon = map.addPolygon(getPolygonOptions()); - polygon.setClickable(true); + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (polygon != null) { + polygon.setZIndex(zIndex); } + } - @Override - public void removeFromMap(GoogleMap map) { - polygon.remove(); + public PolygonOptions getPolygonOptions() { + if (polygonOptions == null) { + polygonOptions = createPolygonOptions(); } + return polygonOptions; + } + + private PolygonOptions createPolygonOptions() { + PolygonOptions options = new PolygonOptions(); + options.addAll(coordinates); + options.fillColor(fillColor); + options.strokeColor(strokeColor); + options.strokeWidth(strokeWidth); + options.geodesic(geodesic); + options.zIndex(zIndex); + return options; + } + + @Override + public Object getFeature() { + return polygon; + } + + @Override + public void addToMap(GoogleMap map) { + polygon = map.addPolygon(getPolygonOptions()); + polygon.setClickable(true); + } + + @Override + public void removeFromMap(GoogleMap map) { + polygon.remove(); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygonManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygonManager.java index 3eaa0e550..6f1605758 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygonManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolygonManager.java @@ -13,71 +13,71 @@ import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; -import java.util.HashMap; import java.util.Map; + import javax.annotation.Nullable; public class AirMapPolygonManager extends ViewGroupManager { - private final DisplayMetrics metrics; + private final DisplayMetrics metrics; - public AirMapPolygonManager(ReactApplicationContext reactContext) { - super(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - metrics = new DisplayMetrics(); - ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRealMetrics(metrics); - } else { - metrics = reactContext.getResources().getDisplayMetrics(); - } + public AirMapPolygonManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); } + } - @Override - public String getName() { - return "AIRMapPolygon"; - } + @Override + public String getName() { + return "AIRMapPolygon"; + } - @Override - public AirMapPolygon createViewInstance(ThemedReactContext context) { - return new AirMapPolygon(context); - } + @Override + public AirMapPolygon createViewInstance(ThemedReactContext context) { + return new AirMapPolygon(context); + } - @ReactProp(name = "coordinates") - public void setCoordinate(AirMapPolygon view, ReadableArray coordinates) { - view.setCoordinates(coordinates); - } + @ReactProp(name = "coordinates") + public void setCoordinate(AirMapPolygon view, ReadableArray coordinates) { + view.setCoordinates(coordinates); + } - @ReactProp(name = "strokeWidth", defaultFloat = 1f) - public void setStrokeWidth(AirMapPolygon view, float widthInPoints) { - float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS - view.setStrokeWidth(widthInScreenPx); - } + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapPolygon view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setStrokeWidth(widthInScreenPx); + } - @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") - public void setFillColor(AirMapPolygon view, int color) { - view.setFillColor(color); - } + @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") + public void setFillColor(AirMapPolygon view, int color) { + view.setFillColor(color); + } - @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") - public void setStrokeColor(AirMapPolygon view, int color) { - view.setStrokeColor(color); - } + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapPolygon view, int color) { + view.setStrokeColor(color); + } - @ReactProp(name = "geodesic", defaultBoolean = false) - public void setGeodesic(AirMapPolygon view, boolean geodesic) { - view.setGeodesic(geodesic); - } + @ReactProp(name = "geodesic", defaultBoolean = false) + public void setGeodesic(AirMapPolygon view, boolean geodesic) { + view.setGeodesic(geodesic); + } - @ReactProp(name = "zIndex", defaultFloat = 1.0f) - public void setZIndex(AirMapPolygon view, float zIndex) { - view.setZIndex(zIndex); - } + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapPolygon view, float zIndex) { + view.setZIndex(zIndex); + } - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - return MapBuilder.of( - "onPress", MapBuilder.of("registrationName", "onPress") - ); - } + @Override + @Nullable + public Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress") + ); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolyline.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolyline.java index 9a15fdc99..488e2972f 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolyline.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolyline.java @@ -14,89 +14,89 @@ public class AirMapPolyline extends AirMapFeature { - private PolylineOptions polylineOptions; - private Polyline polyline; - - private List coordinates; - private int color; - private float width; - private boolean geodesic; - private float zIndex; - - public AirMapPolyline(Context context) { - super(context); - } - - public void setCoordinates(ReadableArray coordinates) { - this.coordinates = new ArrayList<>(coordinates.size()); - for (int i = 0; i < coordinates.size(); i++) { - ReadableMap coordinate = coordinates.getMap(i); - this.coordinates.add(i, - new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); - } - if (polyline != null) { - polyline.setPoints(this.coordinates); - } - } - - public void setColor(int color) { - this.color = color; - if (polyline != null) { - polyline.setColor(color); - } + private PolylineOptions polylineOptions; + private Polyline polyline; + + private List coordinates; + private int color; + private float width; + private boolean geodesic; + private float zIndex; + + public AirMapPolyline(Context context) { + super(context); + } + + public void setCoordinates(ReadableArray coordinates) { + this.coordinates = new ArrayList<>(coordinates.size()); + for (int i = 0; i < coordinates.size(); i++) { + ReadableMap coordinate = coordinates.getMap(i); + this.coordinates.add(i, + new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); } - - public void setWidth(float width) { - this.width = width; - if (polyline != null) { - polyline.setWidth(width); - } - } - - public void setZIndex(float zIndex) { - this.zIndex = zIndex; - if (polyline != null) { - polyline.setZIndex(zIndex); - } - } - - public void setGeodesic(boolean geodesic) { - this.geodesic = geodesic; - if (polyline != null) { - polyline.setGeodesic(geodesic); - } + if (polyline != null) { + polyline.setPoints(this.coordinates); } + } - public PolylineOptions getPolylineOptions() { - if (polylineOptions == null) { - polylineOptions = createPolylineOptions(); - } - return polylineOptions; + public void setColor(int color) { + this.color = color; + if (polyline != null) { + polyline.setColor(color); } + } - private PolylineOptions createPolylineOptions() { - PolylineOptions options = new PolylineOptions(); - options.addAll(coordinates); - options.color(color); - options.width(width); - options.geodesic(geodesic); - options.zIndex(zIndex); - return options; + public void setWidth(float width) { + this.width = width; + if (polyline != null) { + polyline.setWidth(width); } + } - @Override - public Object getFeature() { - return polyline; + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (polyline != null) { + polyline.setZIndex(zIndex); } + } - @Override - public void addToMap(GoogleMap map) { - polyline = map.addPolyline(getPolylineOptions()); - polyline.setClickable(true); + public void setGeodesic(boolean geodesic) { + this.geodesic = geodesic; + if (polyline != null) { + polyline.setGeodesic(geodesic); } + } - @Override - public void removeFromMap(GoogleMap map) { - polyline.remove(); + public PolylineOptions getPolylineOptions() { + if (polylineOptions == null) { + polylineOptions = createPolylineOptions(); } + return polylineOptions; + } + + private PolylineOptions createPolylineOptions() { + PolylineOptions options = new PolylineOptions(); + options.addAll(coordinates); + options.color(color); + options.width(width); + options.geodesic(geodesic); + options.zIndex(zIndex); + return options; + } + + @Override + public Object getFeature() { + return polyline; + } + + @Override + public void addToMap(GoogleMap map) { + polyline = map.addPolyline(getPolylineOptions()); + polyline.setClickable(true); + } + + @Override + public void removeFromMap(GoogleMap map) { + polyline.remove(); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolylineManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolylineManager.java index c5afc61e0..be80acfbc 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolylineManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapPolylineManager.java @@ -13,66 +13,66 @@ import com.facebook.react.uimanager.ViewGroupManager; import com.facebook.react.uimanager.annotations.ReactProp; -import java.util.HashMap; import java.util.Map; + import javax.annotation.Nullable; public class AirMapPolylineManager extends ViewGroupManager { - private final DisplayMetrics metrics; + private final DisplayMetrics metrics; - public AirMapPolylineManager(ReactApplicationContext reactContext) { - super(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - metrics = new DisplayMetrics(); - ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRealMetrics(metrics); - } else { - metrics = reactContext.getResources().getDisplayMetrics(); - } + public AirMapPolylineManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); } + } - @Override - public String getName() { - return "AIRMapPolyline"; - } + @Override + public String getName() { + return "AIRMapPolyline"; + } - @Override - public AirMapPolyline createViewInstance(ThemedReactContext context) { - return new AirMapPolyline(context); - } + @Override + public AirMapPolyline createViewInstance(ThemedReactContext context) { + return new AirMapPolyline(context); + } - @ReactProp(name = "coordinates") - public void setCoordinate(AirMapPolyline view, ReadableArray coordinates) { - view.setCoordinates(coordinates); - } + @ReactProp(name = "coordinates") + public void setCoordinate(AirMapPolyline view, ReadableArray coordinates) { + view.setCoordinates(coordinates); + } - @ReactProp(name = "strokeWidth", defaultFloat = 1f) - public void setStrokeWidth(AirMapPolyline view, float widthInPoints) { - float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS - view.setWidth(widthInScreenPx); - } + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapPolyline view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setWidth(widthInScreenPx); + } - @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") - public void setStrokeColor(AirMapPolyline view, int color) { - view.setColor(color); - } + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapPolyline view, int color) { + view.setColor(color); + } - @ReactProp(name = "geodesic", defaultBoolean = false) - public void setGeodesic(AirMapPolyline view, boolean geodesic) { - view.setGeodesic(geodesic); - } + @ReactProp(name = "geodesic", defaultBoolean = false) + public void setGeodesic(AirMapPolyline view, boolean geodesic) { + view.setGeodesic(geodesic); + } - @ReactProp(name = "zIndex", defaultFloat = 1.0f) - public void setZIndex(AirMapPolyline view, float zIndex) { - view.setZIndex(zIndex); - } + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapPolyline view, float zIndex) { + view.setZIndex(zIndex); + } - @Override - @Nullable - public Map getExportedCustomDirectEventTypeConstants() { - return MapBuilder.of( - "onPress", MapBuilder.of("registrationName", "onPress") - ); - } + @Override + @Nullable + public Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress") + ); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTile.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTile.java index 691c2fd3c..ae51a63b4 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTile.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTile.java @@ -12,89 +12,90 @@ public class AirMapUrlTile extends AirMapFeature { - class AIRMapUrlTileProvider extends UrlTileProvider - { - private String urlTemplate; - public AIRMapUrlTileProvider(int width, int height, String urlTemplate) { - super(width, height); - this.urlTemplate = urlTemplate; - } - @Override - public synchronized URL getTileUrl(int x, int y, int zoom) { - - String s = this.urlTemplate - .replace("{x}", Integer.toString(x)) - .replace("{y}", Integer.toString(y)) - .replace("{z}", Integer.toString(zoom)); - URL url = null; - try { - url = new URL(s); - } catch (MalformedURLException e) { - throw new AssertionError(e); - } - return url; - } - - public void setUrlTemplate(String urlTemplate) { - this.urlTemplate = urlTemplate; - } - } - - private TileOverlayOptions tileOverlayOptions; - private TileOverlay tileOverlay; - private AIRMapUrlTileProvider tileProvider; - + class AIRMapUrlTileProvider extends UrlTileProvider { private String urlTemplate; - private float zIndex; - public AirMapUrlTile(Context context) { - super(context); + public AIRMapUrlTileProvider(int width, int height, String urlTemplate) { + super(width, height); + this.urlTemplate = urlTemplate; } - public void setUrlTemplate(String urlTemplate) { - this.urlTemplate = urlTemplate; - if (tileProvider != null) { - tileProvider.setUrlTemplate(urlTemplate); - } - if (tileOverlay != null) { - tileOverlay.clearTileCache(); - } + @Override + public synchronized URL getTileUrl(int x, int y, int zoom) { + + String s = this.urlTemplate + .replace("{x}", Integer.toString(x)) + .replace("{y}", Integer.toString(y)) + .replace("{z}", Integer.toString(zoom)); + URL url = null; + try { + url = new URL(s); + } catch (MalformedURLException e) { + throw new AssertionError(e); + } + return url; } - public void setZIndex(float zIndex) { - this.zIndex = zIndex; - if (tileOverlay != null) { - tileOverlay.setZIndex(zIndex); - } + public void setUrlTemplate(String urlTemplate) { + this.urlTemplate = urlTemplate; } + } - public TileOverlayOptions getTileOverlayOptions() { - if (tileOverlayOptions == null) { - tileOverlayOptions = createTileOverlayOptions(); - } - return tileOverlayOptions; - } + private TileOverlayOptions tileOverlayOptions; + private TileOverlay tileOverlay; + private AIRMapUrlTileProvider tileProvider; - private TileOverlayOptions createTileOverlayOptions() { - TileOverlayOptions options = new TileOverlayOptions(); - options.zIndex(zIndex); - this.tileProvider = new AIRMapUrlTileProvider(256, 256, this.urlTemplate); - options.tileProvider(this.tileProvider); - return options; - } + private String urlTemplate; + private float zIndex; - @Override - public Object getFeature() { - return tileOverlay; + public AirMapUrlTile(Context context) { + super(context); + } + + public void setUrlTemplate(String urlTemplate) { + this.urlTemplate = urlTemplate; + if (tileProvider != null) { + tileProvider.setUrlTemplate(urlTemplate); } + if (tileOverlay != null) { + tileOverlay.clearTileCache(); + } + } - @Override - public void addToMap(GoogleMap map) { - this.tileOverlay = map.addTileOverlay(getTileOverlayOptions()); + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (tileOverlay != null) { + tileOverlay.setZIndex(zIndex); } + } - @Override - public void removeFromMap(GoogleMap map) { - tileOverlay.remove(); + public TileOverlayOptions getTileOverlayOptions() { + if (tileOverlayOptions == null) { + tileOverlayOptions = createTileOverlayOptions(); } + return tileOverlayOptions; + } + + private TileOverlayOptions createTileOverlayOptions() { + TileOverlayOptions options = new TileOverlayOptions(); + options.zIndex(zIndex); + this.tileProvider = new AIRMapUrlTileProvider(256, 256, this.urlTemplate); + options.tileProvider(this.tileProvider); + return options; + } + + @Override + public Object getFeature() { + return tileOverlay; + } + + @Override + public void addToMap(GoogleMap map) { + this.tileOverlay = map.addTileOverlay(getTileOverlayOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + tileOverlay.remove(); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTileManager.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTileManager.java index 7ea0726dc..68bf07342 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTileManager.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapUrlTileManager.java @@ -11,38 +11,38 @@ import com.facebook.react.uimanager.annotations.ReactProp; public class AirMapUrlTileManager extends ViewGroupManager { - private DisplayMetrics metrics; - - public AirMapUrlTileManager(ReactApplicationContext reactContext) { - super(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - metrics = new DisplayMetrics(); - ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) - .getDefaultDisplay() - .getRealMetrics(metrics); - } else { - metrics = reactContext.getResources().getDisplayMetrics(); - } - } - - @Override - public String getName() { - return "AIRMapUrlTile"; - } - - @Override - public AirMapUrlTile createViewInstance(ThemedReactContext context) { - return new AirMapUrlTile(context); - } - - @ReactProp(name = "urlTemplate") - public void setUrlTemplate(AirMapUrlTile view, String urlTemplate) { - view.setUrlTemplate(urlTemplate); - } - - @ReactProp(name = "zIndex", defaultFloat = -1.0f) - public void setZIndex(AirMapUrlTile view, float zIndex) { - view.setZIndex(zIndex); + private DisplayMetrics metrics; + + public AirMapUrlTileManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); } + } + + @Override + public String getName() { + return "AIRMapUrlTile"; + } + + @Override + public AirMapUrlTile createViewInstance(ThemedReactContext context) { + return new AirMapUrlTile(context); + } + + @ReactProp(name = "urlTemplate") + public void setUrlTemplate(AirMapUrlTile view, String urlTemplate) { + view.setUrlTemplate(urlTemplate); + } + + @ReactProp(name = "zIndex", defaultFloat = -1.0f) + public void setZIndex(AirMapUrlTile view, float zIndex) { + view.setZIndex(zIndex); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java index a4610efff..f69e05295 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/AirMapView.java @@ -1,6 +1,5 @@ package com.airbnb.android.react.maps; -import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.ColorStateList; @@ -55,808 +54,816 @@ import static android.support.v4.content.PermissionChecker.checkSelfPermission; public class AirMapView extends MapView implements GoogleMap.InfoWindowAdapter, - GoogleMap.OnMarkerDragListener, OnMapReadyCallback { - public GoogleMap map; - private ProgressBar mapLoadingProgressBar; - private RelativeLayout mapLoadingLayout; - private ImageView cacheImageView; - private Boolean isMapLoaded = false; - private Integer loadingBackgroundColor = null; - private Integer loadingIndicatorColor = null; - private final int baseMapPadding = 50; - - private LatLngBounds boundsToMove; - private boolean showUserLocation = false; - private boolean isMonitoringRegion = false; - private boolean isTouchDown = false; - private boolean handlePanDrag = false; - private boolean moveOnMarkerPress = true; - private boolean cacheEnabled = false; - - private static final String[] PERMISSIONS = new String[] { - "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"}; - - private final List features = new ArrayList<>(); - private final Map markerMap = new HashMap<>(); - private final Map polylineMap = new HashMap<>(); - private final Map polygonMap = new HashMap<>(); - private final ScaleGestureDetector scaleDetector; - private final GestureDetectorCompat gestureDetector; - private final AirMapManager manager; - private LifecycleEventListener lifecycleListener; - private boolean paused = false; - private boolean destroyed = false; - private final ThemedReactContext context; - private final EventDispatcher eventDispatcher; - - private static boolean contextHasBug(Context context) { - return context == null || - context.getResources() == null || - context.getResources().getConfiguration() == null; - } - - // We do this to fix this bug: - // https://github.com/airbnb/react-native-maps/issues/271 - // - // which conflicts with another bug regarding the passed in context: - // https://github.com/airbnb/react-native-maps/issues/1147 - // - // Doing this allows us to avoid both bugs. - private static Context getNonBuggyContext(ThemedReactContext reactContext, - ReactApplicationContext appContext) { - Context superContext = reactContext; - if (!contextHasBug(appContext.getCurrentActivity())) { - superContext = appContext.getCurrentActivity(); - } else if (contextHasBug(superContext)) { - // we have the bug! let's try to find a better context to use - if (!contextHasBug(reactContext.getCurrentActivity())) { - superContext = reactContext.getCurrentActivity(); - } else if (!contextHasBug(reactContext.getApplicationContext())) { - superContext = reactContext.getApplicationContext(); - } else { - // ¯\_(ツ)_/¯ - } - } - return superContext; - } - - public AirMapView(ThemedReactContext reactContext, ReactApplicationContext appContext, AirMapManager manager, - GoogleMapOptions googleMapOptions) { - super(getNonBuggyContext(reactContext, appContext), googleMapOptions); - - this.manager = manager; - this.context = reactContext; - - super.onCreate(null); - // TODO(lmr): what about onStart???? - super.onResume(); - super.getMapAsync(this); - - final AirMapView view = this; - scaleDetector = - new ScaleGestureDetector(reactContext, new ScaleGestureDetector.SimpleOnScaleGestureListener() { - @Override - public boolean onScaleBegin(ScaleGestureDetector detector) { - view.startMonitoringRegion(); - return true; // stop recording this gesture. let mapview handle it. - } - }); - - gestureDetector = - new GestureDetectorCompat(reactContext, new GestureDetector.SimpleOnGestureListener() { - @Override - public boolean onDoubleTap(MotionEvent e) { - view.startMonitoringRegion(); - return false; - } - - @Override - public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, - float distanceY) { - if (handlePanDrag) { - onPanDrag(e2); - } - view.startMonitoringRegion(); - return false; - } - }); - - this.addOnLayoutChangeListener(new OnLayoutChangeListener() { - @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, - int oldLeft, int oldTop, int oldRight, int oldBottom) { - if (!paused) { - AirMapView.this.cacheView(); - } - } - }); - - eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); - } - - @Override - public void onMapReady(final GoogleMap map) { - if (destroyed) { - return; - } - this.map = map; - this.map.setInfoWindowAdapter(this); - this.map.setOnMarkerDragListener(this); - - manager.pushEvent(context, this, "onMapReady", new WritableNativeMap()); - - final AirMapView view = this; - - map.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { - @Override - public boolean onMarkerClick(Marker marker) { - WritableMap event; - AirMapMarker airMapMarker = markerMap.get(marker); - - event = makeClickEventData(marker.getPosition()); - event.putString("action", "marker-press"); - event.putString("id", airMapMarker.getIdentifier()); - manager.pushEvent(context, view, "onMarkerPress", event); - - event = makeClickEventData(marker.getPosition()); - event.putString("action", "marker-press"); - event.putString("id", airMapMarker.getIdentifier()); - manager.pushEvent(context, markerMap.get(marker), "onPress", event); - - // Return false to open the callout info window and center on the marker - // https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap.OnMarkerClickListener - if (view.moveOnMarkerPress) { - return false; - } else { - marker.showInfoWindow(); - return true; - } - } - }); - - map.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() { - @Override - public void onPolygonClick(Polygon polygon) { - WritableMap event = makeClickEventData(polygon.getPoints().get(0)); - event.putString("action", "polygon-press"); - manager.pushEvent(context, polygonMap.get(polygon), "onPress", event); - } - }); - - map.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() { - @Override - public void onPolylineClick(Polyline polyline) { - WritableMap event = makeClickEventData(polyline.getPoints().get(0)); - event.putString("action", "polyline-press"); - manager.pushEvent(context, polylineMap.get(polyline), "onPress", event); - } - }); - - map.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { - @Override - public void onInfoWindowClick(Marker marker) { - WritableMap event; - - event = makeClickEventData(marker.getPosition()); - event.putString("action", "callout-press"); - manager.pushEvent(context, view, "onCalloutPress", event); - - event = makeClickEventData(marker.getPosition()); - event.putString("action", "callout-press"); - AirMapMarker markerView = markerMap.get(marker); - manager.pushEvent(context, markerView, "onCalloutPress", event); - - event = makeClickEventData(marker.getPosition()); - event.putString("action", "callout-press"); - AirMapCallout infoWindow = markerView.getCalloutView(); - if (infoWindow != null) manager.pushEvent(context, infoWindow, "onPress", event); - } - }); - - map.setOnMapClickListener(new GoogleMap.OnMapClickListener() { - @Override - public void onMapClick(LatLng point) { - WritableMap event = makeClickEventData(point); - event.putString("action", "press"); - manager.pushEvent(context, view, "onPress", event); - } - }); - - map.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() { - @Override - public void onMapLongClick(LatLng point) { - WritableMap event = makeClickEventData(point); - event.putString("action", "long-press"); - manager.pushEvent(context, view, "onLongPress", makeClickEventData(point)); - } - }); - - map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { - @Override - public void onCameraChange(CameraPosition position) { - LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds; - LatLng center = position.target; - lastBoundsEmitted = bounds; - eventDispatcher.dispatchEvent(new RegionChangeEvent(getId(), bounds, center, isTouchDown)); - view.stopMonitoringRegion(); - } - }); - - map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { - @Override public void onMapLoaded() { - isMapLoaded = true; - AirMapView.this.cacheView(); - } - }); - - // We need to be sure to disable location-tracking when app enters background, in-case some - // other module - // has acquired a wake-lock and is controlling location-updates, otherwise, location-manager - // will be left - // updating location constantly, killing the battery, even though some other location-mgmt - // module may - // desire to shut-down location-services. - lifecycleListener = new LifecycleEventListener() { - @Override - public void onHostResume() { - if (hasPermissions()) { - //noinspection MissingPermission - map.setMyLocationEnabled(showUserLocation); - } - synchronized (AirMapView.this) { - AirMapView.this.onResume(); - paused = false; + GoogleMap.OnMarkerDragListener, OnMapReadyCallback { + public GoogleMap map; + private ProgressBar mapLoadingProgressBar; + private RelativeLayout mapLoadingLayout; + private ImageView cacheImageView; + private Boolean isMapLoaded = false; + private Integer loadingBackgroundColor = null; + private Integer loadingIndicatorColor = null; + private final int baseMapPadding = 50; + + private LatLngBounds boundsToMove; + private boolean showUserLocation = false; + private boolean isMonitoringRegion = false; + private boolean isTouchDown = false; + private boolean handlePanDrag = false; + private boolean moveOnMarkerPress = true; + private boolean cacheEnabled = false; + + private static final String[] PERMISSIONS = new String[]{ + "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION"}; + + private final List features = new ArrayList<>(); + private final Map markerMap = new HashMap<>(); + private final Map polylineMap = new HashMap<>(); + private final Map polygonMap = new HashMap<>(); + private final ScaleGestureDetector scaleDetector; + private final GestureDetectorCompat gestureDetector; + private final AirMapManager manager; + private LifecycleEventListener lifecycleListener; + private boolean paused = false; + private boolean destroyed = false; + private final ThemedReactContext context; + private final EventDispatcher eventDispatcher; + + private static boolean contextHasBug(Context context) { + return context == null || + context.getResources() == null || + context.getResources().getConfiguration() == null; + } + + // We do this to fix this bug: + // https://github.com/airbnb/react-native-maps/issues/271 + // + // which conflicts with another bug regarding the passed in context: + // https://github.com/airbnb/react-native-maps/issues/1147 + // + // Doing this allows us to avoid both bugs. + private static Context getNonBuggyContext(ThemedReactContext reactContext, + ReactApplicationContext appContext) { + Context superContext = reactContext; + if (!contextHasBug(appContext.getCurrentActivity())) { + superContext = appContext.getCurrentActivity(); + } else if (contextHasBug(superContext)) { + // we have the bug! let's try to find a better context to use + if (!contextHasBug(reactContext.getCurrentActivity())) { + superContext = reactContext.getCurrentActivity(); + } else if (!contextHasBug(reactContext.getApplicationContext())) { + superContext = reactContext.getApplicationContext(); + } else { + // ¯\_(ツ)_/¯ + } + } + return superContext; + } + + public AirMapView(ThemedReactContext reactContext, ReactApplicationContext appContext, + AirMapManager manager, + GoogleMapOptions googleMapOptions) { + super(getNonBuggyContext(reactContext, appContext), googleMapOptions); + + this.manager = manager; + this.context = reactContext; + + super.onCreate(null); + // TODO(lmr): what about onStart???? + super.onResume(); + super.getMapAsync(this); + + final AirMapView view = this; + scaleDetector = + new ScaleGestureDetector(reactContext, + new ScaleGestureDetector.SimpleOnScaleGestureListener() { + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + view.startMonitoringRegion(); + return true; // stop recording this gesture. let mapview handle it. + } + }); + + gestureDetector = + new GestureDetectorCompat(reactContext, new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onDoubleTap(MotionEvent e) { + view.startMonitoringRegion(); + return false; } - } - @Override - public void onHostPause() { - if (hasPermissions()) { - //noinspection MissingPermission - map.setMyLocationEnabled(false); - } - synchronized (AirMapView.this) { - AirMapView.this.onPause(); - paused = true; + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + if (handlePanDrag) { + onPanDrag(e2); } - } - - @Override - public void onHostDestroy() { - AirMapView.this.doDestroy(); - } - }; - - context.addLifecycleEventListener(lifecycleListener); - } - - private boolean hasPermissions() { - return checkSelfPermission(getContext(), PERMISSIONS[0]) == PackageManager.PERMISSION_GRANTED || - checkSelfPermission(getContext(), PERMISSIONS[1]) == PackageManager.PERMISSION_GRANTED; - } - - - - /* - onDestroy is final method so I can't override it. - */ - public synchronized void doDestroy() { - if (destroyed) { - return; - } - destroyed = true; + view.startMonitoringRegion(); + return false; + } + }); - if (lifecycleListener != null && context != null) { - context.removeLifecycleEventListener(lifecycleListener); - lifecycleListener = null; - } + this.addOnLayoutChangeListener(new OnLayoutChangeListener() { + @Override public void onLayoutChange(View v, int left, int top, int right, int bottom, + int oldLeft, int oldTop, int oldRight, int oldBottom) { if (!paused) { - onPause(); - paused = true; - } - onDestroy(); - } - - public void setRegion(ReadableMap region) { - if (region == null) return; - - Double lng = region.getDouble("longitude"); - Double lat = region.getDouble("latitude"); - Double lngDelta = region.getDouble("longitudeDelta"); - Double latDelta = region.getDouble("latitudeDelta"); - LatLngBounds bounds = new LatLngBounds( - new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest - new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast - ); - if (super.getHeight() <= 0 || super.getWidth() <= 0) { - // in this case, our map has not been laid out yet, so we save the bounds in a local - // variable, and make a guess of zoomLevel 10. Not to worry, though: as soon as layout - // occurs, we will move the camera to the saved bounds. Note that if we tried to move - // to the bounds now, it would trigger an exception. - map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10)); - boundsToMove = bounds; - } else { - map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); - boundsToMove = null; - } - } - - public void setShowsUserLocation(boolean showUserLocation) { - this.showUserLocation = showUserLocation; // hold onto this for lifecycle handling - if (hasPermissions()) { - //noinspection MissingPermission - map.setMyLocationEnabled(showUserLocation); - } - } - - public void setShowsMyLocationButton(boolean showMyLocationButton) { - if (hasPermissions()) { - map.getUiSettings().setMyLocationButtonEnabled(showMyLocationButton); - } - } - - public void setToolbarEnabled(boolean toolbarEnabled) { - if (hasPermissions()) { - map.getUiSettings().setMapToolbarEnabled(toolbarEnabled); - } - } - - public void setCacheEnabled(boolean cacheEnabled) { - this.cacheEnabled = cacheEnabled; - this.cacheView(); - } - - public void enableMapLoading(boolean loadingEnabled) { - if (loadingEnabled && !this.isMapLoaded) { - this.getMapLoadingLayoutView().setVisibility(View.VISIBLE); - } - } - - public void setMoveOnMarkerPress(boolean moveOnPress) { - this.moveOnMarkerPress = moveOnPress; - } - - public void setLoadingBackgroundColor(Integer loadingBackgroundColor) { - this.loadingBackgroundColor = loadingBackgroundColor; - - if (this.mapLoadingLayout != null) { - if (loadingBackgroundColor == null) { - this.mapLoadingLayout.setBackgroundColor(Color.WHITE); - } else { - this.mapLoadingLayout.setBackgroundColor(this.loadingBackgroundColor); - } - } - } - - public void setLoadingIndicatorColor(Integer loadingIndicatorColor) { - this.loadingIndicatorColor = loadingIndicatorColor; - if (this.mapLoadingProgressBar != null) { - Integer color = loadingIndicatorColor; - if (color == null) { - color = Color.parseColor("#606060"); - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - ColorStateList progressTintList = ColorStateList.valueOf(loadingIndicatorColor); - ColorStateList secondaryProgressTintList = ColorStateList.valueOf(loadingIndicatorColor); - ColorStateList indeterminateTintList = ColorStateList.valueOf(loadingIndicatorColor); - - this.mapLoadingProgressBar.setProgressTintList(progressTintList); - this.mapLoadingProgressBar.setSecondaryProgressTintList(secondaryProgressTintList); - this.mapLoadingProgressBar.setIndeterminateTintList(indeterminateTintList); - } else { - PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { - mode = PorterDuff.Mode.MULTIPLY; - } - if (this.mapLoadingProgressBar.getIndeterminateDrawable() != null) - this.mapLoadingProgressBar.getIndeterminateDrawable().setColorFilter(color, mode); - if (this.mapLoadingProgressBar.getProgressDrawable() != null) - this.mapLoadingProgressBar.getProgressDrawable().setColorFilter(color, mode); - } - } - } - - public void setHandlePanDrag(boolean handlePanDrag) { - this.handlePanDrag = handlePanDrag; - } - - public void addFeature(View child, int index) { - // Our desired API is to pass up annotations/overlays as children to the mapview component. - // This is where we intercept them and do the appropriate underlying mapview action. - - if (child instanceof AirMapMarker) { - AirMapMarker annotation = (AirMapMarker) child; - annotation.addToMap(map); - features.add(index, annotation); - Marker marker = (Marker) annotation.getFeature(); - markerMap.put(marker, annotation); - } else if (child instanceof AirMapPolyline) { - AirMapPolyline polylineView = (AirMapPolyline) child; - polylineView.addToMap(map); - features.add(index, polylineView); - Polyline polyline = (Polyline) polylineView.getFeature(); - polylineMap.put(polyline, polylineView); - } else if (child instanceof AirMapPolygon) { - AirMapPolygon polygonView = (AirMapPolygon) child; - polygonView.addToMap(map); - features.add(index, polygonView); - Polygon polygon = (Polygon) polygonView.getFeature(); - polygonMap.put(polygon, polygonView); - } else if (child instanceof AirMapCircle) { - AirMapCircle circleView = (AirMapCircle) child; - circleView.addToMap(map); - features.add(index, circleView); - } else if (child instanceof AirMapUrlTile) { - AirMapUrlTile urlTileView = (AirMapUrlTile) child; - urlTileView.addToMap(map); - features.add(index, urlTileView); - } else if (child instanceof AirMapCanvasUrlTile) { - AirMapCanvasUrlTile canvasUrlTileView = (AirMapCanvasUrlTile) child; - canvasUrlTileView.addToMap(map); - features.add(index, canvasUrlTileView); - } else if (child instanceof AirMapCanvasInteractionUrlTile) { - AirMapCanvasInteractionUrlTile canvasInteractionUrlTileView = (AirMapCanvasInteractionUrlTile) child; - canvasInteractionUrlTileView.addToMap(map); - features.add(index, canvasInteractionUrlTileView); - } else { - ViewGroup children = (ViewGroup) child; - for (int i = 0; i < children.getChildCount(); i++) { - addFeature(children.getChildAt(i), index); - } - } - } - - public int getFeatureCount() { - return features.size(); - } - - public View getFeatureAt(int index) { - return features.get(index); - } - - public void removeFeatureAt(int index) { - AirMapFeature feature = features.remove(index); - if (feature instanceof AirMapMarker) { - markerMap.remove(feature.getFeature()); + AirMapView.this.cacheView(); } - feature.removeFromMap(map); - } - - public WritableMap makeClickEventData(LatLng point) { - WritableMap event = new WritableNativeMap(); - - WritableMap coordinate = new WritableNativeMap(); - coordinate.putDouble("latitude", point.latitude); - coordinate.putDouble("longitude", point.longitude); - event.putMap("coordinate", coordinate); - - Projection projection = map.getProjection(); - Point screenPoint = projection.toScreenLocation(point); - - WritableMap position = new WritableNativeMap(); - position.putDouble("x", screenPoint.x); - position.putDouble("y", screenPoint.y); - event.putMap("position", position); - - return event; - } - - public void updateExtraData(Object extraData) { - // if boundsToMove is not null, we now have the MapView's width/height, so we can apply - // a proper camera move - if (boundsToMove != null) { - HashMap data = (HashMap) extraData; - float width = data.get("width"); - float height = data.get("height"); - map.moveCamera( - CameraUpdateFactory.newLatLngBounds( - boundsToMove, - (int) width, - (int) height, - 0 - ) - ); - boundsToMove = null; - } - } - - public void animateToRegion(LatLngBounds bounds, int duration) { - if (map != null) { - startMonitoringRegion(); - map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), duration, null); - } - } - - public void animateToCoordinate(LatLng coordinate, int duration) { - if (map != null) { - startMonitoringRegion(); - map.animateCamera(CameraUpdateFactory.newLatLng(coordinate), duration, null); - } - } - - public void fitToElements(boolean animated) { - LatLngBounds.Builder builder = new LatLngBounds.Builder(); + } + }); - boolean addedPosition = false; + eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + } - for (AirMapFeature feature : features) { - if (feature instanceof AirMapMarker) { - Marker marker = (Marker) feature.getFeature(); - builder.include(marker.getPosition()); - addedPosition = true; - } - // TODO(lmr): may want to include shapes / etc. - } - if (addedPosition) { - LatLngBounds bounds = builder.build(); - CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); - if (animated) { - startMonitoringRegion(); - map.animateCamera(cu); - } else { - map.moveCamera(cu); - } - } + @Override + public void onMapReady(final GoogleMap map) { + if (destroyed) { + return; } + this.map = map; + this.map.setInfoWindowAdapter(this); + this.map.setOnMarkerDragListener(this); - public void fitToSuppliedMarkers(ReadableArray markerIDsArray, boolean animated) { - LatLngBounds.Builder builder = new LatLngBounds.Builder(); - - String[] markerIDs = new String[markerIDsArray.size()]; - for (int i = 0; i < markerIDsArray.size(); i++) { - markerIDs[i] = markerIDsArray.getString(i); - } - - boolean addedPosition = false; + manager.pushEvent(context, this, "onMapReady", new WritableNativeMap()); - List markerIDList = Arrays.asList(markerIDs); + final AirMapView view = this; - for (AirMapFeature feature : features) { - if (feature instanceof AirMapMarker) { - String identifier = ((AirMapMarker)feature).getIdentifier(); - Marker marker = (Marker)feature.getFeature(); - if (markerIDList.contains(identifier)) { - builder.include(marker.getPosition()); - addedPosition = true; - } - } - } - - if (addedPosition) { - LatLngBounds bounds = builder.build(); - CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); - if (animated) { - startMonitoringRegion(); - map.animateCamera(cu); - } else { - map.moveCamera(cu); - } - } - } + map.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { + @Override + public boolean onMarkerClick(Marker marker) { + WritableMap event; + AirMapMarker airMapMarker = markerMap.get(marker); - public void fitToCoordinates(ReadableArray coordinatesArray, ReadableMap edgePadding, boolean animated) { - LatLngBounds.Builder builder = new LatLngBounds.Builder(); - - for (int i = 0; i < coordinatesArray.size(); i++) { - ReadableMap latLng = coordinatesArray.getMap(i); - Double lat = latLng.getDouble("latitude"); - Double lng = latLng.getDouble("longitude"); - builder.include(new LatLng(lat, lng)); - } - - LatLngBounds bounds = builder.build(); - CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); - - if (edgePadding != null) { - map.setPadding(edgePadding.getInt("left"), edgePadding.getInt("top"), edgePadding.getInt("right"), edgePadding.getInt("bottom")); - } + event = makeClickEventData(marker.getPosition()); + event.putString("action", "marker-press"); + event.putString("id", airMapMarker.getIdentifier()); + manager.pushEvent(context, view, "onMarkerPress", event); - if (animated) { - startMonitoringRegion(); - map.animateCamera(cu); + event = makeClickEventData(marker.getPosition()); + event.putString("action", "marker-press"); + event.putString("id", airMapMarker.getIdentifier()); + manager.pushEvent(context, markerMap.get(marker), "onPress", event); + + // Return false to open the callout info window and center on the marker + // https://developers.google.com/android/reference/com/google/android/gms/maps/GoogleMap + // .OnMarkerClickListener + if (view.moveOnMarkerPress) { + return false; } else { - map.moveCamera(cu); - } - map.setPadding(0, 0, 0, 0); // Without this, the Google logo is moved up by the value of edgePadding.bottom - } - - // InfoWindowAdapter interface - - @Override - public View getInfoWindow(Marker marker) { - AirMapMarker markerView = markerMap.get(marker); - return markerView.getCallout(); - } - - @Override - public View getInfoContents(Marker marker) { - AirMapMarker markerView = markerMap.get(marker); - return markerView.getInfoContents(); - } - - @Override - public boolean dispatchTouchEvent(MotionEvent ev) { - scaleDetector.onTouchEvent(ev); - gestureDetector.onTouchEvent(ev); - - int action = MotionEventCompat.getActionMasked(ev); - - switch (action) { - case (MotionEvent.ACTION_DOWN): - this.getParent().requestDisallowInterceptTouchEvent( - map != null && map.getUiSettings().isScrollGesturesEnabled()); - isTouchDown = true; - break; - case (MotionEvent.ACTION_MOVE): - startMonitoringRegion(); - break; - case (MotionEvent.ACTION_UP): - // Clear this regardless, since isScrollGesturesEnabled() may have been updated - this.getParent().requestDisallowInterceptTouchEvent(false); - isTouchDown = false; - break; - } - super.dispatchTouchEvent(ev); - return true; - } - - // Timer Implementation - - public void startMonitoringRegion() { - if (isMonitoringRegion) return; - timerHandler.postDelayed(timerRunnable, 100); - isMonitoringRegion = true; - } + marker.showInfoWindow(); + return true; + } + } + }); + + map.setOnPolygonClickListener(new GoogleMap.OnPolygonClickListener() { + @Override + public void onPolygonClick(Polygon polygon) { + WritableMap event = makeClickEventData(polygon.getPoints().get(0)); + event.putString("action", "polygon-press"); + manager.pushEvent(context, polygonMap.get(polygon), "onPress", event); + } + }); + + map.setOnPolylineClickListener(new GoogleMap.OnPolylineClickListener() { + @Override + public void onPolylineClick(Polyline polyline) { + WritableMap event = makeClickEventData(polyline.getPoints().get(0)); + event.putString("action", "polyline-press"); + manager.pushEvent(context, polylineMap.get(polyline), "onPress", event); + } + }); + + map.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { + @Override + public void onInfoWindowClick(Marker marker) { + WritableMap event; - public void stopMonitoringRegion() { - if (!isMonitoringRegion) return; - timerHandler.removeCallbacks(timerRunnable); - isMonitoringRegion = false; - } - - private LatLngBounds lastBoundsEmitted; - - Handler timerHandler = new Handler(); - Runnable timerRunnable = new Runnable() { - - @Override - public void run() { - - Projection projection = map.getProjection(); - VisibleRegion region = (projection != null) ? projection.getVisibleRegion() : null; - LatLngBounds bounds = (region != null) ? region.latLngBounds : null; - - if ((bounds != null) && - (lastBoundsEmitted == null || LatLngBoundsUtils.BoundsAreDifferent(bounds, lastBoundsEmitted))) { - LatLng center = map.getCameraPosition().target; - lastBoundsEmitted = bounds; - eventDispatcher.dispatchEvent(new RegionChangeEvent(getId(), bounds, center, true)); - } - - timerHandler.postDelayed(this, 100); - } - }; - - @Override - public void onMarkerDragStart(Marker marker) { - WritableMap event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, this, "onMarkerDragStart", event); - - AirMapMarker markerView = markerMap.get(marker); event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, markerView, "onDragStart", event); - } - - @Override - public void onMarkerDrag(Marker marker) { - WritableMap event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, this, "onMarkerDrag", event); + event.putString("action", "callout-press"); + manager.pushEvent(context, view, "onCalloutPress", event); - AirMapMarker markerView = markerMap.get(marker); event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, markerView, "onDrag", event); - } - - @Override - public void onMarkerDragEnd(Marker marker) { - WritableMap event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, this, "onMarkerDragEnd", event); - + event.putString("action", "callout-press"); AirMapMarker markerView = markerMap.get(marker); - event = makeClickEventData(marker.getPosition()); - manager.pushEvent(context, markerView, "onDragEnd", event); - } + manager.pushEvent(context, markerView, "onCalloutPress", event); - private ProgressBar getMapLoadingProgressBar() { - if (this.mapLoadingProgressBar == null) { - this.mapLoadingProgressBar = new ProgressBar(getContext()); - this.mapLoadingProgressBar.setIndeterminate(true); + event = makeClickEventData(marker.getPosition()); + event.putString("action", "callout-press"); + AirMapCallout infoWindow = markerView.getCalloutView(); + if (infoWindow != null) manager.pushEvent(context, infoWindow, "onPress", event); + } + }); + + map.setOnMapClickListener(new GoogleMap.OnMapClickListener() { + @Override + public void onMapClick(LatLng point) { + WritableMap event = makeClickEventData(point); + event.putString("action", "press"); + manager.pushEvent(context, view, "onPress", event); + } + }); + + map.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() { + @Override + public void onMapLongClick(LatLng point) { + WritableMap event = makeClickEventData(point); + event.putString("action", "long-press"); + manager.pushEvent(context, view, "onLongPress", makeClickEventData(point)); + } + }); + + map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { + @Override + public void onCameraChange(CameraPosition position) { + LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds; + LatLng center = position.target; + lastBoundsEmitted = bounds; + eventDispatcher.dispatchEvent(new RegionChangeEvent(getId(), bounds, center, isTouchDown)); + view.stopMonitoringRegion(); + } + }); + + map.setOnMapLoadedCallback(new GoogleMap.OnMapLoadedCallback() { + @Override public void onMapLoaded() { + isMapLoaded = true; + AirMapView.this.cacheView(); + } + }); + + // We need to be sure to disable location-tracking when app enters background, in-case some + // other module + // has acquired a wake-lock and is controlling location-updates, otherwise, location-manager + // will be left + // updating location constantly, killing the battery, even though some other location-mgmt + // module may + // desire to shut-down location-services. + lifecycleListener = new LifecycleEventListener() { + @Override + public void onHostResume() { + if (hasPermissions()) { + //noinspection MissingPermission + map.setMyLocationEnabled(showUserLocation); } - if (this.loadingIndicatorColor != null) { - this.setLoadingIndicatorColor(this.loadingIndicatorColor); + synchronized (AirMapView.this) { + AirMapView.this.onResume(); + paused = false; } - return this.mapLoadingProgressBar; - } - - private RelativeLayout getMapLoadingLayoutView() { - if (this.mapLoadingLayout == null) { - this.mapLoadingLayout = new RelativeLayout(getContext()); - this.mapLoadingLayout.setBackgroundColor(Color.LTGRAY); - this.addView(this.mapLoadingLayout, - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT)); + } - RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( - RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); - params.addRule(RelativeLayout.CENTER_IN_PARENT); - this.mapLoadingLayout.addView(this.getMapLoadingProgressBar(), params); - - this.mapLoadingLayout.setVisibility(View.INVISIBLE); + @Override + public void onHostPause() { + if (hasPermissions()) { + //noinspection MissingPermission + map.setMyLocationEnabled(false); } - this.setLoadingBackgroundColor(this.loadingBackgroundColor); - return this.mapLoadingLayout; - } - - private ImageView getCacheImageView() { - if (this.cacheImageView == null) { - this.cacheImageView = new ImageView(getContext()); - this.addView(this.cacheImageView, - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - this.cacheImageView.setVisibility(View.INVISIBLE); + synchronized (AirMapView.this) { + if (!destroyed) { + AirMapView.this.onPause(); + } + paused = true; } - return this.cacheImageView; - } + } - private void removeCacheImageView() { - if (this.cacheImageView != null) { - ((ViewGroup)this.cacheImageView.getParent()).removeView(this.cacheImageView); - this.cacheImageView = null; - } - } + @Override + public void onHostDestroy() { + AirMapView.this.doDestroy(); + } + }; - private void removeMapLoadingProgressBar() { - if (this.mapLoadingProgressBar != null) { - ((ViewGroup)this.mapLoadingProgressBar.getParent()).removeView(this.mapLoadingProgressBar); - this.mapLoadingProgressBar = null; - } - } + context.addLifecycleEventListener(lifecycleListener); + } + + private boolean hasPermissions() { + return checkSelfPermission(getContext(), PERMISSIONS[0]) == PackageManager.PERMISSION_GRANTED || + checkSelfPermission(getContext(), PERMISSIONS[1]) == PackageManager.PERMISSION_GRANTED; + } + + + /* + onDestroy is final method so I can't override it. + */ + public synchronized void doDestroy() { + if (destroyed) { + return; + } + destroyed = true; + + if (lifecycleListener != null && context != null) { + context.removeLifecycleEventListener(lifecycleListener); + lifecycleListener = null; + } + if (!paused) { + onPause(); + paused = true; + } + onDestroy(); + } + + public void setRegion(ReadableMap region) { + if (region == null) return; + + Double lng = region.getDouble("longitude"); + Double lat = region.getDouble("latitude"); + Double lngDelta = region.getDouble("longitudeDelta"); + Double latDelta = region.getDouble("latitudeDelta"); + LatLngBounds bounds = new LatLngBounds( + new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest + new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast + ); + if (super.getHeight() <= 0 || super.getWidth() <= 0) { + // in this case, our map has not been laid out yet, so we save the bounds in a local + // variable, and make a guess of zoomLevel 10. Not to worry, though: as soon as layout + // occurs, we will move the camera to the saved bounds. Note that if we tried to move + // to the bounds now, it would trigger an exception. + map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10)); + boundsToMove = bounds; + } else { + map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); + boundsToMove = null; + } + } + + public void setShowsUserLocation(boolean showUserLocation) { + this.showUserLocation = showUserLocation; // hold onto this for lifecycle handling + if (hasPermissions()) { + //noinspection MissingPermission + map.setMyLocationEnabled(showUserLocation); + } + } + + public void setShowsMyLocationButton(boolean showMyLocationButton) { + if (hasPermissions()) { + map.getUiSettings().setMyLocationButtonEnabled(showMyLocationButton); + } + } + + public void setToolbarEnabled(boolean toolbarEnabled) { + if (hasPermissions()) { + map.getUiSettings().setMapToolbarEnabled(toolbarEnabled); + } + } + + public void setCacheEnabled(boolean cacheEnabled) { + this.cacheEnabled = cacheEnabled; + this.cacheView(); + } + + public void enableMapLoading(boolean loadingEnabled) { + if (loadingEnabled && !this.isMapLoaded) { + this.getMapLoadingLayoutView().setVisibility(View.VISIBLE); + } + } + + public void setMoveOnMarkerPress(boolean moveOnPress) { + this.moveOnMarkerPress = moveOnPress; + } + + public void setLoadingBackgroundColor(Integer loadingBackgroundColor) { + this.loadingBackgroundColor = loadingBackgroundColor; + + if (this.mapLoadingLayout != null) { + if (loadingBackgroundColor == null) { + this.mapLoadingLayout.setBackgroundColor(Color.WHITE); + } else { + this.mapLoadingLayout.setBackgroundColor(this.loadingBackgroundColor); + } + } + } + + public void setLoadingIndicatorColor(Integer loadingIndicatorColor) { + this.loadingIndicatorColor = loadingIndicatorColor; + if (this.mapLoadingProgressBar != null) { + Integer color = loadingIndicatorColor; + if (color == null) { + color = Color.parseColor("#606060"); + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + ColorStateList progressTintList = ColorStateList.valueOf(loadingIndicatorColor); + ColorStateList secondaryProgressTintList = ColorStateList.valueOf(loadingIndicatorColor); + ColorStateList indeterminateTintList = ColorStateList.valueOf(loadingIndicatorColor); + + this.mapLoadingProgressBar.setProgressTintList(progressTintList); + this.mapLoadingProgressBar.setSecondaryProgressTintList(secondaryProgressTintList); + this.mapLoadingProgressBar.setIndeterminateTintList(indeterminateTintList); + } else { + PorterDuff.Mode mode = PorterDuff.Mode.SRC_IN; + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) { + mode = PorterDuff.Mode.MULTIPLY; + } + if (this.mapLoadingProgressBar.getIndeterminateDrawable() != null) + this.mapLoadingProgressBar.getIndeterminateDrawable().setColorFilter(color, mode); + if (this.mapLoadingProgressBar.getProgressDrawable() != null) + this.mapLoadingProgressBar.getProgressDrawable().setColorFilter(color, mode); + } + } + } + + public void setHandlePanDrag(boolean handlePanDrag) { + this.handlePanDrag = handlePanDrag; + } + + public void addFeature(View child, int index) { + // Our desired API is to pass up annotations/overlays as children to the mapview component. + // This is where we intercept them and do the appropriate underlying mapview action. + + if (child instanceof AirMapMarker) { + AirMapMarker annotation = (AirMapMarker) child; + annotation.addToMap(map); + features.add(index, annotation); + Marker marker = (Marker) annotation.getFeature(); + markerMap.put(marker, annotation); + } else if (child instanceof AirMapPolyline) { + AirMapPolyline polylineView = (AirMapPolyline) child; + polylineView.addToMap(map); + features.add(index, polylineView); + Polyline polyline = (Polyline) polylineView.getFeature(); + polylineMap.put(polyline, polylineView); + } else if (child instanceof AirMapPolygon) { + AirMapPolygon polygonView = (AirMapPolygon) child; + polygonView.addToMap(map); + features.add(index, polygonView); + Polygon polygon = (Polygon) polygonView.getFeature(); + polygonMap.put(polygon, polygonView); + } else if (child instanceof AirMapCircle) { + AirMapCircle circleView = (AirMapCircle) child; + circleView.addToMap(map); + features.add(index, circleView); + } else if (child instanceof AirMapUrlTile) { + AirMapUrlTile urlTileView = (AirMapUrlTile) child; + urlTileView.addToMap(map); + features.add(index, urlTileView); + } else if (child instanceof AirMapCanvasUrlTile) { + AirMapCanvasUrlTile canvasUrlTileView = (AirMapCanvasUrlTile) child; + canvasUrlTileView.addToMap(map); + features.add(index, canvasUrlTileView); + } else if (child instanceof AirMapCanvasInteractionUrlTile) { + AirMapCanvasInteractionUrlTile canvasInteractionUrlTileView = (AirMapCanvasInteractionUrlTile) child; + canvasInteractionUrlTileView.addToMap(map); + features.add(index, canvasInteractionUrlTileView); + } else { + ViewGroup children = (ViewGroup) child; + for (int i = 0; i < children.getChildCount(); i++) { + addFeature(children.getChildAt(i), index); + } + } + } + + public int getFeatureCount() { + return features.size(); + } + + public View getFeatureAt(int index) { + return features.get(index); + } + + public void removeFeatureAt(int index) { + AirMapFeature feature = features.remove(index); + if (feature instanceof AirMapMarker) { + markerMap.remove(feature.getFeature()); + } + feature.removeFromMap(map); + } + + public WritableMap makeClickEventData(LatLng point) { + WritableMap event = new WritableNativeMap(); + + WritableMap coordinate = new WritableNativeMap(); + coordinate.putDouble("latitude", point.latitude); + coordinate.putDouble("longitude", point.longitude); + event.putMap("coordinate", coordinate); + + Projection projection = map.getProjection(); + Point screenPoint = projection.toScreenLocation(point); + + WritableMap position = new WritableNativeMap(); + position.putDouble("x", screenPoint.x); + position.putDouble("y", screenPoint.y); + event.putMap("position", position); + + return event; + } + + public void updateExtraData(Object extraData) { + // if boundsToMove is not null, we now have the MapView's width/height, so we can apply + // a proper camera move + if (boundsToMove != null) { + HashMap data = (HashMap) extraData; + float width = data.get("width"); + float height = data.get("height"); + map.moveCamera( + CameraUpdateFactory.newLatLngBounds( + boundsToMove, + (int) width, + (int) height, + 0 + ) + ); + boundsToMove = null; + } + } + + public void animateToRegion(LatLngBounds bounds, int duration) { + if (map != null) { + startMonitoringRegion(); + map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), duration, null); + } + } + + public void animateToCoordinate(LatLng coordinate, int duration) { + if (map != null) { + startMonitoringRegion(); + map.animateCamera(CameraUpdateFactory.newLatLng(coordinate), duration, null); + } + } + + public void fitToElements(boolean animated) { + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + boolean addedPosition = false; + + for (AirMapFeature feature : features) { + if (feature instanceof AirMapMarker) { + Marker marker = (Marker) feature.getFeature(); + builder.include(marker.getPosition()); + addedPosition = true; + } + // TODO(lmr): may want to include shapes / etc. + } + if (addedPosition) { + LatLngBounds bounds = builder.build(); + CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); + if (animated) { + startMonitoringRegion(); + map.animateCamera(cu); + } else { + map.moveCamera(cu); + } + } + } + + public void fitToSuppliedMarkers(ReadableArray markerIDsArray, boolean animated) { + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + String[] markerIDs = new String[markerIDsArray.size()]; + for (int i = 0; i < markerIDsArray.size(); i++) { + markerIDs[i] = markerIDsArray.getString(i); + } + + boolean addedPosition = false; + + List markerIDList = Arrays.asList(markerIDs); + + for (AirMapFeature feature : features) { + if (feature instanceof AirMapMarker) { + String identifier = ((AirMapMarker) feature).getIdentifier(); + Marker marker = (Marker) feature.getFeature(); + if (markerIDList.contains(identifier)) { + builder.include(marker.getPosition()); + addedPosition = true; + } + } + } + + if (addedPosition) { + LatLngBounds bounds = builder.build(); + CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); + if (animated) { + startMonitoringRegion(); + map.animateCamera(cu); + } else { + map.moveCamera(cu); + } + } + } + + public void fitToCoordinates(ReadableArray coordinatesArray, ReadableMap edgePadding, + boolean animated) { + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + + for (int i = 0; i < coordinatesArray.size(); i++) { + ReadableMap latLng = coordinatesArray.getMap(i); + Double lat = latLng.getDouble("latitude"); + Double lng = latLng.getDouble("longitude"); + builder.include(new LatLng(lat, lng)); + } + + LatLngBounds bounds = builder.build(); + CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, baseMapPadding); + + if (edgePadding != null) { + map.setPadding(edgePadding.getInt("left"), edgePadding.getInt("top"), + edgePadding.getInt("right"), edgePadding.getInt("bottom")); + } + + if (animated) { + startMonitoringRegion(); + map.animateCamera(cu); + } else { + map.moveCamera(cu); + } + map.setPadding(0, 0, 0, + 0); // Without this, the Google logo is moved up by the value of edgePadding.bottom + } + + // InfoWindowAdapter interface - private void removeMapLoadingLayoutView() { - this.removeMapLoadingProgressBar(); - if (this.mapLoadingLayout != null) { - ((ViewGroup)this.mapLoadingLayout.getParent()).removeView(this.mapLoadingLayout); - this.mapLoadingLayout = null; - } + @Override + public View getInfoWindow(Marker marker) { + AirMapMarker markerView = markerMap.get(marker); + return markerView.getCallout(); + } + + @Override + public View getInfoContents(Marker marker) { + AirMapMarker markerView = markerMap.get(marker); + return markerView.getInfoContents(); + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + scaleDetector.onTouchEvent(ev); + gestureDetector.onTouchEvent(ev); + + int action = MotionEventCompat.getActionMasked(ev); + + switch (action) { + case (MotionEvent.ACTION_DOWN): + this.getParent().requestDisallowInterceptTouchEvent( + map != null && map.getUiSettings().isScrollGesturesEnabled()); + isTouchDown = true; + break; + case (MotionEvent.ACTION_MOVE): + startMonitoringRegion(); + break; + case (MotionEvent.ACTION_UP): + // Clear this regardless, since isScrollGesturesEnabled() may have been updated + this.getParent().requestDisallowInterceptTouchEvent(false); + isTouchDown = false; + break; } + super.dispatchTouchEvent(ev); + return true; + } + + // Timer Implementation - private void cacheView() { - if (this.cacheEnabled) { - final ImageView cacheImageView = this.getCacheImageView(); - final RelativeLayout mapLoadingLayout = this.getMapLoadingLayoutView(); - cacheImageView.setVisibility(View.INVISIBLE); - mapLoadingLayout.setVisibility(View.VISIBLE); - if (this.isMapLoaded) { - this.map.snapshot(new GoogleMap.SnapshotReadyCallback() { - @Override public void onSnapshotReady(Bitmap bitmap) { - cacheImageView.setImageBitmap(bitmap); - cacheImageView.setVisibility(View.VISIBLE); - mapLoadingLayout.setVisibility(View.INVISIBLE); - } - }); - } - } - else { - this.removeCacheImageView(); - if (this.isMapLoaded) { - this.removeMapLoadingLayoutView(); - } - } - } + public void startMonitoringRegion() { + if (isMonitoringRegion) return; + timerHandler.postDelayed(timerRunnable, 100); + isMonitoringRegion = true; + } + + public void stopMonitoringRegion() { + if (!isMonitoringRegion) return; + timerHandler.removeCallbacks(timerRunnable); + isMonitoringRegion = false; + } + + private LatLngBounds lastBoundsEmitted; + + Handler timerHandler = new Handler(); + Runnable timerRunnable = new Runnable() { - public void onPanDrag(MotionEvent ev) { - Point point = new Point((int) ev.getX(), (int) ev.getY()); - LatLng coords = this.map.getProjection().fromScreenLocation(point); - WritableMap event = makeClickEventData(coords); - manager.pushEvent(context, this, "onPanDrag", event); - } + @Override + public void run() { + + Projection projection = map.getProjection(); + VisibleRegion region = (projection != null) ? projection.getVisibleRegion() : null; + LatLngBounds bounds = (region != null) ? region.latLngBounds : null; + + if ((bounds != null) && + (lastBoundsEmitted == null || + LatLngBoundsUtils.BoundsAreDifferent(bounds, lastBoundsEmitted))) { + LatLng center = map.getCameraPosition().target; + lastBoundsEmitted = bounds; + eventDispatcher.dispatchEvent(new RegionChangeEvent(getId(), bounds, center, true)); + } + + timerHandler.postDelayed(this, 100); + } + }; + + @Override + public void onMarkerDragStart(Marker marker) { + WritableMap event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, this, "onMarkerDragStart", event); + + AirMapMarker markerView = markerMap.get(marker); + event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, markerView, "onDragStart", event); + } + + @Override + public void onMarkerDrag(Marker marker) { + WritableMap event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, this, "onMarkerDrag", event); + + AirMapMarker markerView = markerMap.get(marker); + event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, markerView, "onDrag", event); + } + + @Override + public void onMarkerDragEnd(Marker marker) { + WritableMap event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, this, "onMarkerDragEnd", event); + + AirMapMarker markerView = markerMap.get(marker); + event = makeClickEventData(marker.getPosition()); + manager.pushEvent(context, markerView, "onDragEnd", event); + } + + private ProgressBar getMapLoadingProgressBar() { + if (this.mapLoadingProgressBar == null) { + this.mapLoadingProgressBar = new ProgressBar(getContext()); + this.mapLoadingProgressBar.setIndeterminate(true); + } + if (this.loadingIndicatorColor != null) { + this.setLoadingIndicatorColor(this.loadingIndicatorColor); + } + return this.mapLoadingProgressBar; + } + + private RelativeLayout getMapLoadingLayoutView() { + if (this.mapLoadingLayout == null) { + this.mapLoadingLayout = new RelativeLayout(getContext()); + this.mapLoadingLayout.setBackgroundColor(Color.LTGRAY); + this.addView(this.mapLoadingLayout, + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + + RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams( + RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); + params.addRule(RelativeLayout.CENTER_IN_PARENT); + this.mapLoadingLayout.addView(this.getMapLoadingProgressBar(), params); + + this.mapLoadingLayout.setVisibility(View.INVISIBLE); + } + this.setLoadingBackgroundColor(this.loadingBackgroundColor); + return this.mapLoadingLayout; + } + + private ImageView getCacheImageView() { + if (this.cacheImageView == null) { + this.cacheImageView = new ImageView(getContext()); + this.addView(this.cacheImageView, + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + this.cacheImageView.setVisibility(View.INVISIBLE); + } + return this.cacheImageView; + } + + private void removeCacheImageView() { + if (this.cacheImageView != null) { + ((ViewGroup) this.cacheImageView.getParent()).removeView(this.cacheImageView); + this.cacheImageView = null; + } + } + + private void removeMapLoadingProgressBar() { + if (this.mapLoadingProgressBar != null) { + ((ViewGroup) this.mapLoadingProgressBar.getParent()).removeView(this.mapLoadingProgressBar); + this.mapLoadingProgressBar = null; + } + } + + private void removeMapLoadingLayoutView() { + this.removeMapLoadingProgressBar(); + if (this.mapLoadingLayout != null) { + ((ViewGroup) this.mapLoadingLayout.getParent()).removeView(this.mapLoadingLayout); + this.mapLoadingLayout = null; + } + } + + private void cacheView() { + if (this.cacheEnabled) { + final ImageView cacheImageView = this.getCacheImageView(); + final RelativeLayout mapLoadingLayout = this.getMapLoadingLayoutView(); + cacheImageView.setVisibility(View.INVISIBLE); + mapLoadingLayout.setVisibility(View.VISIBLE); + if (this.isMapLoaded) { + this.map.snapshot(new GoogleMap.SnapshotReadyCallback() { + @Override public void onSnapshotReady(Bitmap bitmap) { + cacheImageView.setImageBitmap(bitmap); + cacheImageView.setVisibility(View.VISIBLE); + mapLoadingLayout.setVisibility(View.INVISIBLE); + } + }); + } + } else { + this.removeCacheImageView(); + if (this.isMapLoaded) { + this.removeMapLoadingLayoutView(); + } + } + } + + public void onPanDrag(MotionEvent ev) { + Point point = new Point((int) ev.getX(), (int) ev.getY()); + LatLng coords = this.map.getProjection().fromScreenLocation(point); + WritableMap event = makeClickEventData(coords); + manager.pushEvent(context, this, "onPanDrag", event); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/LatLngBoundsUtils.java b/lib/android/src/main/java/com/airbnb/android/react/maps/LatLngBoundsUtils.java index ed1058c88..d32e0dc72 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/LatLngBoundsUtils.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/LatLngBoundsUtils.java @@ -4,44 +4,44 @@ import com.google.android.gms.maps.model.LatLngBounds; public class LatLngBoundsUtils { - public static boolean BoundsAreDifferent(LatLngBounds a, LatLngBounds b) { - LatLng centerA = a.getCenter(); - double latA = centerA.latitude; - double lngA = centerA.longitude; - double latDeltaA = a.northeast.latitude - a.southwest.latitude; - double lngDeltaA = a.northeast.longitude - a.southwest.longitude; - - LatLng centerB = b.getCenter(); - double latB = centerB.latitude; - double lngB = centerB.longitude; - double latDeltaB = b.northeast.latitude - b.southwest.latitude; - double lngDeltaB = b.northeast.longitude - b.southwest.longitude; - - double latEps = LatitudeEpsilon(a, b); - double lngEps = LongitudeEpsilon(a, b); - - return - different(latA, latB, latEps) || - different(lngA, lngB, lngEps) || - different(latDeltaA, latDeltaB, latEps) || - different(lngDeltaA, lngDeltaB, lngEps); - } - - private static boolean different(double a, double b, double epsilon) { - return Math.abs(a - b) > epsilon; - } - - private static double LatitudeEpsilon(LatLngBounds a, LatLngBounds b) { - double sizeA = a.northeast.latitude - a.southwest.latitude; // something mod 180? - double sizeB = b.northeast.latitude - b.southwest.latitude; // something mod 180? - double size = Math.min(Math.abs(sizeA), Math.abs(sizeB)); - return size / 2560; - } - - private static double LongitudeEpsilon(LatLngBounds a, LatLngBounds b) { - double sizeA = a.northeast.longitude - a.southwest.longitude; - double sizeB = b.northeast.longitude - b.southwest.longitude; - double size = Math.min(Math.abs(sizeA), Math.abs(sizeB)); - return size / 2560; - } + public static boolean BoundsAreDifferent(LatLngBounds a, LatLngBounds b) { + LatLng centerA = a.getCenter(); + double latA = centerA.latitude; + double lngA = centerA.longitude; + double latDeltaA = a.northeast.latitude - a.southwest.latitude; + double lngDeltaA = a.northeast.longitude - a.southwest.longitude; + + LatLng centerB = b.getCenter(); + double latB = centerB.latitude; + double lngB = centerB.longitude; + double latDeltaB = b.northeast.latitude - b.southwest.latitude; + double lngDeltaB = b.northeast.longitude - b.southwest.longitude; + + double latEps = LatitudeEpsilon(a, b); + double lngEps = LongitudeEpsilon(a, b); + + return + different(latA, latB, latEps) || + different(lngA, lngB, lngEps) || + different(latDeltaA, latDeltaB, latEps) || + different(lngDeltaA, lngDeltaB, lngEps); + } + + private static boolean different(double a, double b, double epsilon) { + return Math.abs(a - b) > epsilon; + } + + private static double LatitudeEpsilon(LatLngBounds a, LatLngBounds b) { + double sizeA = a.northeast.latitude - a.southwest.latitude; // something mod 180? + double sizeB = b.northeast.latitude - b.southwest.latitude; // something mod 180? + double size = Math.min(Math.abs(sizeA), Math.abs(sizeB)); + return size / 2560; + } + + private static double LongitudeEpsilon(LatLngBounds a, LatLngBounds b) { + double sizeA = a.northeast.longitude - a.southwest.longitude; + double sizeB = b.northeast.longitude - b.southwest.longitude; + double size = Math.min(Math.abs(sizeA), Math.abs(sizeB)); + return size / 2560; + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/MapsPackage.java b/lib/android/src/main/java/com/airbnb/android/react/maps/MapsPackage.java index 5284e1581..db17b99ed 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/MapsPackage.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/MapsPackage.java @@ -13,45 +13,43 @@ import java.util.List; public class MapsPackage implements ReactPackage { - public MapsPackage(Activity activity) { - } // backwards compatibility - - public MapsPackage() { - } - - @Override - public List createNativeModules(ReactApplicationContext reactContext) { - return Arrays.asList(new AirMapModule(reactContext)); - } - - @Override - public List> createJSModules() { - return Collections.emptyList(); - } - - @Override - public List createViewManagers(ReactApplicationContext reactContext) { - AirMapCalloutManager calloutManager = new AirMapCalloutManager(); - AirMapMarkerManager annotationManager = new AirMapMarkerManager(); - AirMapPolylineManager polylineManager = new AirMapPolylineManager(reactContext); - AirMapPolygonManager polygonManager = new AirMapPolygonManager(reactContext); - AirMapCircleManager circleManager = new AirMapCircleManager(reactContext); - AirMapManager mapManager = new AirMapManager(reactContext); - AirMapLiteManager mapLiteManager = new AirMapLiteManager(reactContext); - AirMapUrlTileManager tileManager = new AirMapUrlTileManager(reactContext); - AirMapCanvasUrlTileManager canvasTileManager = new AirMapCanvasUrlTileManager(reactContext); - AirMapCanvasInteractionUrlTileManager canvasInteractionTileManager = new AirMapCanvasInteractionUrlTileManager(reactContext); - - return Arrays.asList( - calloutManager, - annotationManager, - polylineManager, - polygonManager, - circleManager, - mapManager, - mapLiteManager, - tileManager, - canvasTileManager, - canvasInteractionTileManager); - } + public MapsPackage(Activity activity) { + } // backwards compatibility + + public MapsPackage() { + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new AirMapModule(reactContext)); + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + AirMapCalloutManager calloutManager = new AirMapCalloutManager(); + AirMapMarkerManager annotationManager = new AirMapMarkerManager(); + AirMapPolylineManager polylineManager = new AirMapPolylineManager(reactContext); + AirMapPolygonManager polygonManager = new AirMapPolygonManager(reactContext); + AirMapCircleManager circleManager = new AirMapCircleManager(reactContext); + AirMapManager mapManager = new AirMapManager(reactContext); + AirMapLiteManager mapLiteManager = new AirMapLiteManager(reactContext); + AirMapUrlTileManager tileManager = new AirMapUrlTileManager(reactContext); + AirMapCanvasInteractionUrlTileManager canvasInteractionTileManager = new AirMapCanvasInteractionUrlTileManager(reactContext); + + return Arrays.asList( + calloutManager, + annotationManager, + polylineManager, + polygonManager, + circleManager, + mapManager, + mapLiteManager, + tileManager, + canvasInteractionTileManager); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/RegionChangeEvent.java b/lib/android/src/main/java/com/airbnb/android/react/maps/RegionChangeEvent.java index 28a3b322b..43b1f678e 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/RegionChangeEvent.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/RegionChangeEvent.java @@ -8,40 +8,40 @@ import com.google.android.gms.maps.model.LatLngBounds; public class RegionChangeEvent extends Event { - private final LatLngBounds bounds; - private final LatLng center; - private final boolean continuous; - - public RegionChangeEvent(int id, LatLngBounds bounds, LatLng center, boolean continuous) { - super(id); - this.bounds = bounds; - this.center = center; - this.continuous = continuous; - } - - @Override - public String getEventName() { - return "topChange"; - } - - @Override - public boolean canCoalesce() { - return false; - } - - @Override - public void dispatch(RCTEventEmitter rctEventEmitter) { - - WritableMap event = new WritableNativeMap(); - event.putBoolean("continuous", continuous); - - WritableMap region = new WritableNativeMap(); - region.putDouble("latitude", center.latitude); - region.putDouble("longitude", center.longitude); - region.putDouble("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude); - region.putDouble("longitudeDelta", bounds.northeast.longitude - bounds.southwest.longitude); - event.putMap("region", region); - - rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event); - } + private final LatLngBounds bounds; + private final LatLng center; + private final boolean continuous; + + public RegionChangeEvent(int id, LatLngBounds bounds, LatLng center, boolean continuous) { + super(id); + this.bounds = bounds; + this.center = center; + this.continuous = continuous; + } + + @Override + public String getEventName() { + return "topChange"; + } + + @Override + public boolean canCoalesce() { + return false; + } + + @Override + public void dispatch(RCTEventEmitter rctEventEmitter) { + + WritableMap event = new WritableNativeMap(); + event.putBoolean("continuous", continuous); + + WritableMap region = new WritableNativeMap(); + region.putDouble("latitude", center.latitude); + region.putDouble("longitude", center.longitude); + region.putDouble("latitudeDelta", bounds.northeast.latitude - bounds.southwest.latitude); + region.putDouble("longitudeDelta", bounds.northeast.longitude - bounds.southwest.longitude); + event.putMap("region", region); + + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event); + } } diff --git a/lib/android/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java b/lib/android/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java index 40ace480f..bf6c29d65 100644 --- a/lib/android/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java +++ b/lib/android/src/main/java/com/airbnb/android/react/maps/SizeReportingShadowNode.java @@ -18,14 +18,14 @@ // which sends the width/height of the view after layout occurs. public class SizeReportingShadowNode extends LayoutShadowNode { - @Override - public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { - super.onCollectExtraUpdates(uiViewOperationQueue); + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + super.onCollectExtraUpdates(uiViewOperationQueue); - Map data = new HashMap<>(); - data.put("width", getLayoutWidth()); - data.put("height", getLayoutHeight()); + Map data = new HashMap<>(); + data.put("width", getLayoutWidth()); + data.put("height", getLayoutHeight()); - uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data); - } + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data); + } } diff --git a/lib/components/MapCallout.js b/lib/components/MapCallout.js index cf4bb9cef..226b52fe2 100644 --- a/lib/components/MapCallout.js +++ b/lib/components/MapCallout.js @@ -1,7 +1,8 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, StyleSheet, + ViewPropTypes, } from 'react-native'; import decorateMapComponent, { SUPPORTED, @@ -9,7 +10,7 @@ import decorateMapComponent, { } from './decorateMapComponent'; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, tooltip: PropTypes.bool, onPress: PropTypes.func, }; diff --git a/lib/components/MapCircle.js b/lib/components/MapCircle.js index aab1ba8f1..acf066b24 100644 --- a/lib/components/MapCircle.js +++ b/lib/components/MapCircle.js @@ -1,6 +1,7 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, + ViewPropTypes, } from 'react-native'; import decorateMapComponent, { USES_DEFAULT_IMPLEMENTATION, @@ -8,7 +9,7 @@ import decorateMapComponent, { } from './decorateMapComponent'; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, /** * The coordinate of the center of the circle diff --git a/lib/components/MapMarker.js b/lib/components/MapMarker.js index be54e42db..6e2b8585d 100644 --- a/lib/components/MapMarker.js +++ b/lib/components/MapMarker.js @@ -1,11 +1,12 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, StyleSheet, Platform, NativeModules, Animated, findNodeHandle, + ViewPropTypes, } from 'react-native'; import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; @@ -22,7 +23,7 @@ const viewConfig = { }; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, // TODO(lmr): get rid of these? identifier: PropTypes.string, diff --git a/lib/components/MapPolygon.js b/lib/components/MapPolygon.js index 6901979db..27f2aa322 100644 --- a/lib/components/MapPolygon.js +++ b/lib/components/MapPolygon.js @@ -1,6 +1,7 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, + ViewPropTypes, } from 'react-native'; import decorateMapComponent, { USES_DEFAULT_IMPLEMENTATION, @@ -8,7 +9,7 @@ import decorateMapComponent, { } from './decorateMapComponent'; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, /** * An array of coordinates to describe the polygon diff --git a/lib/components/MapPolyline.js b/lib/components/MapPolyline.js index aa79f096b..af1573eff 100644 --- a/lib/components/MapPolyline.js +++ b/lib/components/MapPolyline.js @@ -1,6 +1,7 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, + ViewPropTypes, } from 'react-native'; import decorateMapComponent, { USES_DEFAULT_IMPLEMENTATION, @@ -8,7 +9,7 @@ import decorateMapComponent, { } from './decorateMapComponent'; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, /** * An array of coordinates to describe the polygon diff --git a/lib/components/MapUrlTile.js b/lib/components/MapUrlTile.js index cf2770334..f61c1e0d3 100644 --- a/lib/components/MapUrlTile.js +++ b/lib/components/MapUrlTile.js @@ -1,7 +1,8 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { - View, + ViewPropTypes, } from 'react-native'; import decorateMapComponent, { @@ -10,7 +11,7 @@ import decorateMapComponent, { } from './decorateMapComponent'; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, /** * The url template of the tile server. The patterns {x} {y} {z} will be replaced at runtime diff --git a/lib/components/MapView.js b/lib/components/MapView.js index 1b416687b..7b0b6fb0f 100644 --- a/lib/components/MapView.js +++ b/lib/components/MapView.js @@ -1,13 +1,14 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { EdgeInsetsPropType, Platform, - View, Animated, requireNativeComponent, NativeModules, ColorPropType, findNodeHandle, + ViewPropTypes, } from 'react-native'; import MapMarker from './MapMarker'; import MapPolyline from './MapPolyline'; @@ -47,7 +48,7 @@ const viewConfig = { }; const propTypes = { - ...View.propTypes, + ...ViewPropTypes, /** * When provider is "google", we will use GoogleMaps. * Any value other than "google" will default to using @@ -61,7 +62,7 @@ const propTypes = { * Used to style and layout the `MapView`. See `StyleSheet.js` and * `ViewStylePropTypes.js` for more info. */ - style: View.propTypes.style, + style: ViewPropTypes.style, /** * A json object that describes the style of the map. This is transformed to a string @@ -386,6 +387,16 @@ const propTypes = { */ onMarkerDragEnd: PropTypes.func, + /** + * Minimum zoom value for the map, must be between 0 and 20 + */ + minZoomLevel: PropTypes.number, + + /** + * Maximum zoom value for the map, must be between 0 and 20 + */ + maxZoomLevel: PropTypes.number, + }; class MapView extends React.Component { diff --git a/lib/components/decorateMapComponent.js b/lib/components/decorateMapComponent.js index e655c4c33..168d4f122 100644 --- a/lib/components/decorateMapComponent.js +++ b/lib/components/decorateMapComponent.js @@ -1,4 +1,4 @@ -import { PropTypes } from 'react'; +import PropTypes from 'prop-types'; import { requireNativeComponent, NativeModules, diff --git a/lib/ios/AirGoogleMaps/AIRGMSPolyline.h b/lib/ios/AirGoogleMaps/AIRGMSPolyline.h index 64a3afb99..d7ee19783 100644 --- a/lib/ios/AirGoogleMaps/AIRGMSPolyline.h +++ b/lib/ios/AirGoogleMaps/AIRGMSPolyline.h @@ -6,7 +6,7 @@ // #import -#import "UIView+React.h" +#import @class AIRGoogleMapPolyline; diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMap.m b/lib/ios/AirGoogleMaps/AIRGoogleMap.m index ef73067b6..f87d34c63 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMap.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMap.m @@ -300,6 +300,13 @@ - (BOOL)showsMyLocationButton { return self.settings.myLocationButton; } +- (void)setMinZoomLevel:(CGFloat)minZoomLevel { + [self setMinZoom:minZoomLevel maxZoom:self.maxZoom ]; +} + +- (void)setMaxZoomLevel:(CGFloat)maxZoomLevel { + [self setMinZoom:self.minZoom maxZoom:maxZoomLevel ]; +} + (MKCoordinateRegion) makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position { // solution from here: http://stackoverflow.com/a/16587735/1102215 diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m index 785d58d4d..c482305b8 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapManager.m @@ -26,6 +26,7 @@ #import "RCTConvert+AirMap.h" #import +#import static NSString *const RCTMapViewKey = @"MapView"; @@ -65,6 +66,8 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onRegionChangeComplete, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(mapType, GMSMapViewType) +RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat) RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag withRegion:(MKCoordinateRegion)region @@ -75,10 +78,31 @@ - (UIView *)view if (![view isKindOfClass:[AIRGoogleMap class]]) { RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); } else { - [AIRGoogleMap animateWithDuration:duration/1000 animations:^{ - GMSCameraPosition* camera = [AIRGoogleMap makeGMSCameraPositionFromMap:(AIRGoogleMap *)view andMKCoordinateRegion:region]; - [(AIRGoogleMap *)view animateToCameraPosition:camera]; - }]; + // Core Animation must be used to control the animation's duration + // See http://stackoverflow.com/a/15663039/171744 + [CATransaction begin]; + [CATransaction setAnimationDuration:duration/1000]; + AIRGoogleMap *mapView = (AIRGoogleMap *)view; + GMSCameraPosition *camera = [AIRGoogleMap makeGMSCameraPositionFromMap:mapView andMKCoordinateRegion:region]; + [mapView animateToCameraPosition:camera]; + [CATransaction commit]; + } + }]; +} + +RCT_EXPORT_METHOD(animateToCoordinate:(nonnull NSNumber *)reactTag + withRegion:(CLLocationCoordinate2D)latlng + withDuration:(CGFloat)duration) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRGoogleMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view); + } else { + [CATransaction begin]; + [CATransaction setAnimationDuration:duration/1000]; + [(AIRGoogleMap *)view animateToLocation:latlng]; + [CATransaction commit]; } }]; } diff --git a/lib/ios/AirMaps/AIRMap.h b/lib/ios/AirMaps/AIRMap.h index 18c9a0bc0..4a88617e6 100644 --- a/lib/ios/AirMaps/AIRMap.h +++ b/lib/ios/AirMaps/AIRMap.h @@ -37,6 +37,8 @@ extern const CGFloat AIRMapZoomBoundBuffer; @property (nonatomic, assign) UIEdgeInsets legalLabelInsets; @property (nonatomic, strong) NSTimer *regionChangeObserveTimer; @property (nonatomic, assign) MKCoordinateRegion initialRegion; +@property (nonatomic, assign) CGFloat minZoomLevel; +@property (nonatomic, assign) CGFloat maxZoomLevel; @property (nonatomic, assign) CLLocationCoordinate2D pendingCenter; @property (nonatomic, assign) MKCoordinateSpan pendingSpan; diff --git a/lib/ios/AirMaps/AIRMapManager.h b/lib/ios/AirMaps/AIRMapManager.h index cc9a8c75b..29df98bfc 100644 --- a/lib/ios/AirMaps/AIRMapManager.h +++ b/lib/ios/AirMaps/AIRMapManager.h @@ -8,7 +8,23 @@ */ #import +#import "AIRMap.h" + +#define MERCATOR_RADIUS 85445659.44705395 +#define MERCATOR_OFFSET 268435456 +#define MAX_GOOGLE_LEVELS 20 @interface AIRMapManager : RCTViewManager + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + zoomLevel:(double)zoomLevel + animated:(BOOL)animated + mapView:(AIRMap *)mapView; + +- (MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView + centerCoordinate:(CLLocationCoordinate2D)centerCoordinate + andZoomLevel:(double)zoomLevel; +- (double) zoomLevel:(AIRMap *)mapView; + @end diff --git a/lib/ios/AirMaps/AIRMapManager.m b/lib/ios/AirMaps/AIRMapManager.m index 619142bd6..8ea247244 100644 --- a/lib/ios/AirMaps/AIRMapManager.m +++ b/lib/ios/AirMaps/AIRMapManager.m @@ -97,6 +97,9 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(initialRegion, MKCoordinateRegion) +RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat) + RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap) { @@ -625,11 +628,19 @@ - (void)mapView:(AIRMap *)mapView regionWillChangeAnimated:(__unused BOOL)animat - (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated { + CGFloat zoomLevel = [self zoomLevel:mapView]; [mapView.regionChangeObserveTimer invalidate]; mapView.regionChangeObserveTimer = nil; [self _regionChanged:mapView]; + if (mapView.minZoomLevel != nil && zoomLevel < mapView.minZoomLevel) { + [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.minZoomLevel animated:TRUE mapView:mapView]; + } + else if (mapView.maxZoomLevel != nil && zoomLevel > mapView.maxZoomLevel) { + [self setCenterCoordinate:[mapView centerCoordinate] zoomLevel:mapView.maxZoomLevel animated:TRUE mapView:mapView]; + } + // Don't send region did change events until map has // started rendering, as these won't represent the final location if (mapView.hasStartedRendering) { @@ -665,6 +676,7 @@ - (void)_regionChanged:(AIRMap *)mapView BOOL needZoom = NO; CGFloat newLongitudeDelta = 0.0f; MKCoordinateRegion region = mapView.region; + CGFloat zoomLevel = [self zoomLevel:mapView]; // On iOS 7, it's possible that we observe invalid locations during initialization of the map. // Filter those out. if (!CLLocationCoordinate2DIsValid(region.center)) { @@ -759,4 +771,161 @@ - (double)metersFromPixel:(NSUInteger)px atPoint:(CGPoint)pt forMap:(AIRMap *)ma return MKMetersBetweenMapPoints(MKMapPointForCoordinate(coordA), MKMapPointForCoordinate(coordB)); } ++ (double)longitudeToPixelSpaceX:(double)longitude +{ + return round(MERCATOR_OFFSET + MERCATOR_RADIUS * longitude * M_PI / 180.0); +} + ++ (double)latitudeToPixelSpaceY:(double)latitude +{ + if (latitude == 90.0) { + return 0; + } else if (latitude == -90.0) { + return MERCATOR_OFFSET * 2; + } else { + return round(MERCATOR_OFFSET - MERCATOR_RADIUS * logf((1 + sinf(latitude * M_PI / 180.0)) / (1 - sinf(latitude * M_PI / 180.0))) / 2.0); + } +} + ++ (double)pixelSpaceXToLongitude:(double)pixelX +{ + return ((round(pixelX) - MERCATOR_OFFSET) / MERCATOR_RADIUS) * 180.0 / M_PI; +} + ++ (double)pixelSpaceYToLatitude:(double)pixelY +{ + return (M_PI / 2.0 - 2.0 * atan(exp((round(pixelY) - MERCATOR_OFFSET) / MERCATOR_RADIUS))) * 180.0 / M_PI; +} + +#pragma mark - +#pragma mark Helper methods + +- (MKCoordinateSpan)coordinateSpanWithMapView:(AIRMap *)mapView + centerCoordinate:(CLLocationCoordinate2D)centerCoordinate + andZoomLevel:(double)zoomLevel +{ + // convert center coordiate to pixel space + double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude]; + double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude]; + + // determine the scale value from the zoom level + double zoomExponent = 20 - zoomLevel; + double zoomScale = pow(2, zoomExponent); + + // scale the map’s size in pixel space + CGSize mapSizeInPixels = mapView.bounds.size; + double scaledMapWidth = mapSizeInPixels.width * zoomScale; + double scaledMapHeight = mapSizeInPixels.height * zoomScale; + + // figure out the position of the top-left pixel + double topLeftPixelX = centerPixelX - (scaledMapWidth / 2); + double topLeftPixelY = centerPixelY - (scaledMapHeight / 2); + + // find delta between left and right longitudes + CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX]; + CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth]; + CLLocationDegrees longitudeDelta = maxLng - minLng; + + // find delta between top and bottom latitudes + CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY]; + CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:topLeftPixelY + scaledMapHeight]; + CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat); + + // create and return the lat/lng span + MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta); + return span; +} + +#pragma mark - +#pragma mark Public methods + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate + zoomLevel:(double)zoomLevel + animated:(BOOL)animated + mapView:(AIRMap *)mapView +{ + // clamp large numbers to 28 + zoomLevel = MIN(zoomLevel, 28); + + // use the zoom level to compute the region + MKCoordinateSpan span = [self coordinateSpanWithMapView:mapView centerCoordinate:centerCoordinate andZoomLevel:zoomLevel]; + MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span); + + // set the region like normal + [mapView setRegion:region animated:animated]; +} + +//KMapView cannot display tiles that cross the pole (as these would involve wrapping the map from top to bottom, something that a Mercator projection just cannot do). +-(MKCoordinateRegion)coordinateRegionWithMapView:(AIRMap *)mapView + centerCoordinate:(CLLocationCoordinate2D)centerCoordinate + andZoomLevel:(double)zoomLevel +{ + // clamp lat/long values to appropriate ranges + centerCoordinate.latitude = MIN(MAX(-90.0, centerCoordinate.latitude), 90.0); + centerCoordinate.longitude = fmod(centerCoordinate.longitude, 180.0); + + // convert center coordiate to pixel space + double centerPixelX = [AIRMapManager longitudeToPixelSpaceX:centerCoordinate.longitude]; + double centerPixelY = [AIRMapManager latitudeToPixelSpaceY:centerCoordinate.latitude]; + + // determine the scale value from the zoom level + double zoomExponent = 20 - zoomLevel; + double zoomScale = pow(2, zoomExponent); + + // scale the map’s size in pixel space + CGSize mapSizeInPixels = mapView.bounds.size; + double scaledMapWidth = mapSizeInPixels.width * zoomScale; + double scaledMapHeight = mapSizeInPixels.height * zoomScale; + + // figure out the position of the left pixel + double topLeftPixelX = centerPixelX - (scaledMapWidth / 2); + + // find delta between left and right longitudes + CLLocationDegrees minLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX]; + CLLocationDegrees maxLng = [AIRMapManager pixelSpaceXToLongitude:topLeftPixelX + scaledMapWidth]; + CLLocationDegrees longitudeDelta = maxLng - minLng; + + // if we’re at a pole then calculate the distance from the pole towards the equator + // as MKMapView doesn’t like drawing boxes over the poles + double topPixelY = centerPixelY - (scaledMapHeight / 2); + double bottomPixelY = centerPixelY + (scaledMapHeight / 2); + BOOL adjustedCenterPoint = NO; + if (topPixelY > MERCATOR_OFFSET * 2) { + topPixelY = centerPixelY - scaledMapHeight; + bottomPixelY = MERCATOR_OFFSET * 2; + adjustedCenterPoint = YES; + } + + // find delta between top and bottom latitudes + CLLocationDegrees minLat = [AIRMapManager pixelSpaceYToLatitude:topPixelY]; + CLLocationDegrees maxLat = [AIRMapManager pixelSpaceYToLatitude:bottomPixelY]; + CLLocationDegrees latitudeDelta = -1 * (maxLat - minLat); + + // create and return the lat/lng span + MKCoordinateSpan span = MKCoordinateSpanMake(latitudeDelta, longitudeDelta); + MKCoordinateRegion region = MKCoordinateRegionMake(centerCoordinate, span); + // once again, MKMapView doesn’t like drawing boxes over the poles + // so adjust the center coordinate to the center of the resulting region + if (adjustedCenterPoint) { + region.center.latitude = [AIRMapManager pixelSpaceYToLatitude:((bottomPixelY + topPixelY) / 2.0)]; + } + + return region; +} + +- (double) zoomLevel:(AIRMap *)mapView { + MKCoordinateRegion region = mapView.region; + + double centerPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude]; + double topLeftPixelX = [AIRMapManager longitudeToPixelSpaceX: region.center.longitude - region.span.longitudeDelta / 2]; + + double scaledMapWidth = (centerPixelX - topLeftPixelX) * 2; + CGSize mapSizeInPixels = mapView.bounds.size; + double zoomScale = scaledMapWidth / mapSizeInPixels.width; + double zoomExponent = log(zoomScale) / log(2); + double zoomLevel = 20 - zoomExponent; + + return zoomLevel; +} + @end diff --git a/package.json b/package.json index 772551c11..9ff2a7d83 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "description": "React Native Mapview component for iOS + Android", "main": "index.js", "author": "Leland Richardson ", - "version": "0.15.2", + "version": "0.15.3", "scripts": { "start": "node node_modules/react-native/local-cli/cli.js start", "run:packager": "./node_modules/react-native/packager/packager.sh", @@ -32,8 +32,9 @@ "mapkit" ], "peerDependencies": { - "react": ">=15.4.0", - "react-native": ">=0.40" + "react": ">=15.4.0 || ^16.0.0-alpha", + "react-native": ">=0.40", + "prop-types": "^15.5.10" }, "devDependencies": { "babel-eslint": "^6.1.2", @@ -48,8 +49,9 @@ "eslint-plugin-react": "^6.1.2", "gitbook-cli": "^2.3.0", "lodash": "^4.17.2", - "react": "~15.4.1", - "react-native": "^0.42.0" + "prop-types": "^15.5.10", + "react": "^16.0.0-alpha.12", + "react-native": "^0.45.1" }, "rnpm": { "android": {