diff --git a/.idea/codeStyleSettings.xml b/.idea/codeStyleSettings.xml new file mode 100644 index 0000000000..b6cfecda91 --- /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 0000000000..a78536177b --- /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/.npmignore b/.npmignore index 33a9488b16..4ce7bc58bc 100644 --- a/.npmignore +++ b/.npmignore @@ -1 +1,2 @@ example +.babelrc diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c13e5ba07..5678d80526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,62 @@ # Change Log +## 0.17.0 (October 11, 2017) +* iOS: [#1527](https://github.com/airbnb/react-native-maps/pull/1527) Added [iOS / Google Maps] support for showsIndoorLevelPicker +* iOS/Android: [#1544](https://github.com/airbnb/react-native-maps/pull/1544) Adds support to animateToBearing and animateToViewingAngle ( IOS + Android ) +* JS: [#1503](https://github.com/airbnb/react-native-maps/pull/1503) Remove caret from "react": "^16.0.0-alpha.12 +* Android: [#1521](https://github.com/airbnb/react-native-maps/pull/1521) Fix rare android crashes when map size is 0 +* Common: [#1610](https://github.com/airbnb/react-native-maps/pull/1610) Added Typescript Definitions +* Android: [#1612](https://github.com/airbnb/react-native-maps/pull/1612) Remove legalNotice from android AirMapModule + +## 0.16.4 (September 13, 2017) +* Android: [#1643](https://github.com/airbnb/react-native-maps/pull/1643) [MapMarker] fix android release crash on custom marker + +## 0.16.3 (September 2, 2017) +* iOS: [#1603](https://github.com/airbnb/react-native-maps/pull/1603) Added missing satellite option for iOS Google Maps +* iOS: [#1579](https://github.com/airbnb/react-native-maps/pull/1579) Set initial region on view + +## 0.16.2 (August 17, 2017) +* Android: [#1563](https://github.com/airbnb/react-native-maps/pull/#1563) Add missing native method for setting initial region +* iOS: [#1187](https://github.com/airbnb/react-native-maps/pull/1187) Reverted due to build issues + +## 0.16.1 (August 15, 2017) +* Android: [#1428](https://github.com/airbnb/react-native-maps/pull/#1428) Add ability to load marker image from drawable +* iOS: [#1187](https://github.com/airbnb/react-native-maps/pull/1187) Improve marker performance +* iOS/Android: [#1458](https://github.com/airbnb/react-native-maps/pull/1458) Add Google Maps legalNotice constant +* JS: [#1546](https://github.com/airbnb/react-native-maps/pull/1546) Fix initial region native prop + +## 0.16.0 (August 9, 2017) +* Android: [#1481](https://github.com/airbnb/react-native-maps/pull/1481) Handle Android RN 0.47 breaking change +* iOS: [#1357](https://github.com/airbnb/react-native-maps/pull/1357) add MKTileOverlayRenderer +* iOS: [#1369](https://github.com/airbnb/react-native-maps/pull/1369) Add onMapReady callback +* Android/iOS/JS: [#1360](https://github.com/airbnb/react-native-maps/pull/1360) Add minZoom and maxZoom properties for android and ios +* JS: [#1479](https://github.com/airbnb/react-native-maps/pull/1479) Fix timing function used in AnimatedRegion.spring + +## 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 + +## 0.15.1 (May 19, 2017) + +* iOS: [#1341](https://github.com/airbnb/react-native-maps/pull/1341) Fix compile error in rn version >= 0.40 +* iOS: [#1194](https://github.com/airbnb/react-native-maps/pull/1194) Add onPress support for Google Maps Polyline +* iOS: [#1326](https://github.com/airbnb/react-native-maps/pull/1326) Add Marker rotation for Google Maps on iOS +* Android: [#1311](https://github.com/airbnb/react-native-maps/pull/1311) Fix overlay issue +* Common [#1313](https://github.com/airbnb/react-native-maps/pull/1313) Fix Android sourceDir for react-native-link + +## 0.15.0 (May 8, 2017) + +* iOS: [#1195](https://github.com/airbnb/react-native-maps/pull/1195) Rename project file to fix iOS build error +* Android: Update Google Play Services to version `10.2.4` + ## 0.14.0 (April 4, 2017) ## Enhancements diff --git a/README.md b/README.md index 41139a4a02..91b22377be 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 @@ -405,7 +405,7 @@ getInitialState() { takeSnapshot () { // 'takeSnapshot' takes a config object with the // following options - const snapshot = this.refs.map.takeSnapshot({ + const snapshot = this.map.takeSnapshot({ width: 300, // optional, when omitted the view-width is used height: 300, // optional, when omitted the view-height is used region: {..}, // iOS only, optional region to render @@ -421,7 +421,7 @@ takeSnapshot () { render() { return ( - + { this.map = map }}> @@ -494,7 +494,7 @@ Good: License -------- - Copyright (c) 2015 Airbnb + Copyright (c) 2017 Airbnb Licensed under the The MIT License (MIT) (the "License"); you may not use this file except in compliance with the License. diff --git a/build.gradle b/build.gradle index f89d2c5948..588e5cb996 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.3.0' + classpath 'com.android.tools.build:gradle:2.3.2' } } diff --git a/docs/circle.md b/docs/circle.md index 70a5e078e5..992ac4567d 100644 --- a/docs/circle.md +++ b/docs/circle.md @@ -10,8 +10,8 @@ | `strokeColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The stroke color to use for the path. | `fillColor` | `String` | `#000`, `rgba(r,g,b,0.5)` | The fill color to use for the path. | `zIndex` | `Number` | 0 | The order in which this tile overlay is drawn with respect to other overlays. An overlay with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays with the same z-index is arbitrary. The default zIndex is 0. (Android Only) -| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. -| `lineJoin` | `Array` | | The line join style to apply to corners of the path. +| `lineCap` | `String` | `round` | The line cap style to apply to the open ends of the path. Other values : `butt`, `square` +| `lineJoin` | `String` | | The line join style to apply to corners of the path. possible value: `miter`, `round`, `bevel` | `miterLimit` | `Number` | | The limiting value that helps avoid spikes at junctions between connected line segments. The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If the ratio of the miter length—that is, the diagonal length of the miter join—to the line thickness exceeds the miter limit, the joint is converted to a bevel join. The default miter limit is 10, which results in the conversion of miters whose angle at the joint is less than 11 degrees. | `geodesic` | `Boolean` | | Boolean to indicate whether to draw each segment of the line as a geodesic as opposed to straight lines on the Mercator projection. A geodesic is the shortest path between two points on the Earth's surface. The geodesic curve is constructed assuming the Earth is a sphere. | `lineDashPhase` | `Number` | `0` | (iOS only) The offset (in points) at which to start drawing the dash pattern. Use this property to start drawing a dashed line partway through a segment or gap. For example, a phase value of 6 for the patter 5-2-3-2 would cause drawing to begin in the middle of the first gap. diff --git a/docs/installation.md b/docs/installation.md index d3b6c331c1..ad9d4b3833 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -21,13 +21,13 @@ react-native link react-native-maps ### Option 1: CocoaPods - Same as the included AirMapsExplorer example -1. Setup your `Podfile` like the included [example/ios/Podfile](../example/ios/Podfile), replace all references to `AirMapExplorer` with your project name, and then run `pod install`. +1. Setup your `Podfile` like the included [example/ios/Podfile](../example/ios/Podfile), replace all references to `AirMapsExplorer` with your project name, and then run `pod install`. (If you do not need `GoogleMaps` support for iOS, then you can probably completely skip this step.) 1. Open your project in Xcode workspace 1. If you need `GoogleMaps` support also - - Drag this folder `node_modules/react-native-maps/ios/AirGoogleMaps/` into your project, and choose `Create groups` in the popup window. + - Drag this folder `node_modules/react-native-maps/lib/ios/AirGoogleMaps/` into your project, and choose `Create groups` in the popup window. - In `AppDelegate.m`, add `@import GoogleMaps;` before `@implementation AppDelegate`. In `- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions`, add `[GMSServices provideAPIKey:@"YOUR_GOOGLE_MAP_API_KEY"];` - - In your project's `Build Settings` > `Header Search Paths`, double click the value field. In the popup, add `$(SRCROOT)/../node_modules/react-native-maps/ios/AirMaps` and change `non-recursive` to `recursive`. (Dragging the folder `node_modules/react-native-maps/ios/AirMaps/` into your project introduces duplicate symbols. We should not do it.) + - In your project's `Build Settings` > `Header Search Paths`, double click the value field. In the popup, add `$(SRCROOT)/../node_modules/react-native-maps/lib/ios/AirMaps` and change `non-recursive` to `recursive`. (Dragging the folder `node_modules/react-native-maps/lib/ios/AirMaps/` into your project introduces duplicate symbols. We should not do it.) Note: We recommend using a version of React Native >= .40. Newer versions (>= .40) require `package.json` to be set to `"react-native-maps": "^0.13.0"`, while older versions require `"react-native-maps": "^0.12.4"`. @@ -52,7 +52,7 @@ After your `Podfile` is setup properly, run `pod install`. >This was already done for you if you ran "react-native link" 1. Open your project in Xcode, right click on `Libraries` and click `Add - Files to "Your Project Name"` Look under `node_modules/react-native-maps/ios` and add `AIRMaps.xcodeproj`. + Files to "Your Project Name"` Look under `node_modules/react-native-maps/lib/ios` and add `AIRMaps.xcodeproj`. 1. Add `libAIRMaps.a` to `Build Phases -> Link Binary With Libraries. 1. Click on `AIRMaps.xcodeproj` in `Libraries` and go the `Build Settings` tab. Double click the text to the right of `Header Search @@ -98,7 +98,7 @@ After your `Podfile` is setup properly, run `pod install`. ```groovy ... include ':react-native-maps' - project(':react-native-maps').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-maps/android') + project(':react-native-maps').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-maps/lib/android') ``` 1. Specify your Google Maps API Key: @@ -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: @@ -213,7 +215,7 @@ Enter the name of the API key and create it. 1. If you encounter `com.android.dex.DexException: Multiple dex files define Landroid/support/v7/appcompat/R$anim`, then clear build folder. ``` cd android - gradlew clean + ./gradlew clean cd .. ``` diff --git a/docs/mapview.md b/docs/mapview.md index 8e73a07738..2aa6a7086f 100644 --- a/docs/mapview.md +++ b/docs/mapview.md @@ -9,7 +9,7 @@ | `initialRegion` | `Region` | | The initial region to be displayed by the map. Use this prop instead of `region` only if you don't want to control the viewport of the map besides the initial region.

Changing this prop after the component has mounted will not result in a region change.

This is similar to the `initialValue` prop of a text input. | `liteMode` | `Boolean` | `false` | Enable lite mode. **Note**: Android only. | `mapType` | `String` | `"standard"` | The map type to be displayed.

- standard: standard road map (default)
- satellite: satellite view
- hybrid: satellite view with roads and points of interest overlayed
- terrain: (Android only) topographic view -| `customMapStyle` | `Array` | | Adds custom styling to the map component. See [README](https://github.com/airbnb/react-native-maps#customizing-the-map-style) for more information. +| `customMapStyle` | `Array` | | Adds custom styling to the map component. See [README](https://github.com/airbnb/react-native-maps#customizing-the-map-style) for more information. | `showsUserLocation` | `Boolean` | `false` | If `true` the app will ask for the user's location. **NOTE**: You need to add `NSLocationWhenInUseUsageDescription` key in Info.plist to enable geolocation, otherwise it is going to *fail silently*! | `userLocationAnnotationTitle` | `String` | | The title of the annotation for current user location. This only works if `showsUserLocation` is true. There is a default value `My Location` set by MapView. **Note**: iOS only. | `followsUserLocation` | `Boolean` | `false` | If `true` the map will focus on the user's location. This only works if `showsUserLocation` is true and the user has shared their location. **Note**: iOS only. @@ -20,8 +20,10 @@ | `showsBuildings` | `Boolean` | `true` | A Boolean indicating whether the map displays extruded building information. | `showsTraffic` | `Boolean` | `true` | A Boolean value indicating whether the map displays traffic information. | `showsIndoors` | `Boolean` | `true` | A Boolean indicating whether indoor maps should be enabled. -| `showsIndoorLevelPicker` | `Boolean` | `false` | A Boolean indicating whether indoor level picker should be enabled. **Note:** Android only. +| `showsIndoorLevelPicker` | `Boolean` | `false` | A Boolean indicating whether indoor level picker should be enabled. **Note:** Google Maps only (either Android or iOS with `PROVIDER_GOOGLE`). | `zoomEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/zoom the map. +| `minZoomLevel` | `Number` | `0` | Minimum zoom value for the map, must be between 0 and 20 +| `maxZoomLevel` | `Number` | `20` | Maximum zoom value for the map, must be between 0 and 20 | `rotateEnabled` | `Boolean` | `true` | If `false` the user won't be able to pinch/rotate the map. | `scrollEnabled` | `Boolean` | `true` | If `false` the user won't be able to change the map region being displayed. | `pitchEnabled` | `Boolean` | `true` | If `false` the user won't be able to adjust the camera’s pitch angle. @@ -36,8 +38,11 @@ ## Events +To access event data, you will need to use `e.nativeEvent`. For example, `onPress={e => console.log(e.nativeEvent)}` will log the entire event object to your console. + | Event Name | Returns | Notes |---|---|---| +| `onMapReady` | | Callback that is called once the map is fully loaded. | `onRegionChange` | `Region` | Callback that is called continuously when the region changes, such as when a user is dragging the map. | `onRegionChangeComplete` | `Region` | Callback that is called once when the region changes, such as when the user is done moving the map. | `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when user taps on the map. @@ -59,9 +64,11 @@ |---|---|---| | `animateToRegion` | `region: Region`, `duration: Number` | | `animateToCoordinate` | `coordinate: LatLng`, `duration: Number` | -| `fitToElements` | `animated: Boolean` | +| `animateToBearing` | `bearing: Number`, `duration: Number` | +| `animateToViewingAngle` | `angle: Number`, `duration: Number` | +| `fitToElements` | `animated: Boolean` | | `fitToSuppliedMarkers` | `markerIDs: String[]`, `animated: Boolean` | If you need to use this in `ComponentDidMount`, make sure you put it in a timeout or it will cause performance problems. -| `fitToCoordinates` | `coordinates: Array, options: { edgePadding: EdgePadding, animated: Boolean }` | If called in `ComponentDidMount` in android, it will cause an exception. It is recommended to call it from the MapView `onLayout` event. +| `fitToCoordinates` | `coordinates: Array, options: { edgePadding: EdgePadding, animated: Boolean }` | If called in `ComponentDidMount` in android, it will cause an exception. It is recommended to call it from the MapView `onLayout` event. diff --git a/docs/marker.md b/docs/marker.md index 4246befc36..4214e4d6e8 100644 --- a/docs/marker.md +++ b/docs/marker.md @@ -15,11 +15,13 @@ | `calloutAnchor` | `Point` | | Specifies the point in the marker image at which to anchor the callout when it is displayed. This is specified in the same coordinate system as the anchor. See the `anchor` prop for more details.

The default is the top middle of the image.

For ios, see the `calloutOffset` prop. | `flat` | `Boolean` | | Sets whether this marker should be flat against the map true or a billboard facing the camera false. | `identifier` | `String` | | An identifier used to reference this marker at a later date. -| `rotation` | `Float` | | A float number indicating marker's rotation angle. +| `rotation` | `Float` | | A float number indicating marker's rotation angle, in degrees. | `draggable` | `` | | This is a non-value based prop. Adding this allows the marker to be draggable (re-positioned). ## Events +To access event data, you will need to use `e.nativeEvent`. For example, `onPress={e => console.log(e.nativeEvent)}` will log the entire event object to your console. + | Event Name | Returns | Notes |---|---|---| | `onPress` | `{ coordinate: LatLng, position: Point }` | Callback that is called when the user presses on the marker diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index c5bdebe4cb..7a8e8b7dd7 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 eec2349a48..12149c20e1 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 573617d419..688427b35b 100644 --- a/example/examples/DisplayLatLng.js +++ b/example/examples/DisplayLatLng.js @@ -43,15 +43,37 @@ class DisplayLatLng extends React.Component { this.map.animateToRegion(this.randomRegion()); } - randomRegion() { - const { region } = this.state; + animateRandomCoordinate() { + this.map.animateToCoordinate(this.randomCoordinate()); + } + + animateToRandomBearing() { + this.map.animateToBearing(this.getRandomFloat(-360, 360)); + } + + animateToRandomViewingAngle() { + this.map.animateToViewingAngle(this.getRandomFloat(0, 90)); + } + + getRandomFloat(min, max) { + return (Math.random() * (max - min)) + min; + } + + 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 +96,31 @@ 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) + + this.animateToRandomBearing()} + style={[styles.bubble, styles.button]} + > + Animate (Bearing) + + this.animateToRandomViewingAngle()} + style={[styles.bubble, styles.button]} + > + Animate (View Angle)
@@ -112,16 +152,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 005894a0d5..19a3930637 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'; @@ -145,6 +145,7 @@ class EventListener extends React.Component { '../../node_modules/react-native/ReactCommon/yoga/Yoga.podspec' - pod 'React', path: '../../node_modules/react-native', :subspecs => [ + pod 'Yoga', path: "#{rn_path}/ReactCommon/yoga/Yoga.podspec" + pod 'React', path: rn_path, subspecs: [ 'Core', 'RCTActionSheet', 'RCTAnimation', @@ -18,22 +19,25 @@ target 'AirMapsExplorer' do 'RCTSettings', 'RCTText', 'RCTVibration', - 'RCTWebSocket' + 'RCTWebSocket', + 'BatchedBridge' ] - pod 'GoogleMaps' # <~~ remove this line if you do not want to support GoogleMaps on iOS + pod 'GoogleMaps' # Remove this line if you don't want to support GoogleMaps on iOS pod 'react-native-maps', path: '../../' - pod 'react-native-google-maps', path: '../../' # <~~ if you need GoogleMaps support on iOS - + pod 'react-native-google-maps', path: '../../' # If you need GoogleMaps support on iOS end - post_install do |installer| installer.pods_project.targets.each do |target| - if target.name == "react-native-google-maps" + if target.name == 'react-native-google-maps' target.build_configurations.each do |config| config.build_settings['CLANG_ENABLE_MODULES'] = 'No' end end + + if target.name == "React" + target.remove_from_project + end end end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index ff6774aa29..3eec18feec 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 0000000000..905387619f --- /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 0000000000..f715473935 --- /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 0000000000..3c4a4d04c1 --- /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 0000000000..c76cfd0a59 --- /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 0000000000..3c3452c175 --- /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 3b6c26cf77..e5fa88cc4c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Mar 02 17:03:11 PST 2017 +#Mon May 08 11:03:28 PDT 2017 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-4.0-all.zip diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000000..d885759b25 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,159 @@ +import * as React from 'react'; + +declare module "react-native-maps" { + + export type ProviderType = 'google'; + export type MapType = 'standard' | 'satellite' | 'hybrid' | 'terrain' | 'none'; + export type LineCapType = 'butt' | 'round' | 'square'; + export type LineJoinType = 'miter' | 'round' | 'bevel'; + + export interface MapViewProperties { + provider?: ProviderType; + style?: any; + customMapStyle?: any[]; + customMapStyleString?: string; + showsUserLocation?: boolean; + userLocationAnnotationTitle?: string; + showsMyLocationButton?: boolean; + followsUserLocation?: boolean; + showsPointsOfInterest?: boolean; + showsCompass?: boolean; + zoomEnabled?: boolean; + rotateEnabled?: boolean; + cacheEnabled?: boolean; + loadingEnabled?: boolean; + loadingBackgroundColor?: any; + loadingIndicatorColor?: any; + scrollEnabled?: boolean; + pitchEnabled?: boolean; + toolbarEnabled?: boolean; + moveOnMarkerPress?: boolean; + showsScale?: boolean; + showsBuildings?: boolean; + showsTraffic?: boolean; + showsIndoors?: boolean; + showsIndoorLevelPicker?: boolean; + mapType?: MapType; + region?: { latitude: number; longitude: number; latitudeDelta: number; longitudeDelta: number; }; + initialRegion?: { latitude: number; longitude: number; latitudeDelta: number; longitudeDelta: number; }; + liteMode?: boolean; + maxDelta?: number; + minDelta?: number; + legalLabelInsets?: any; + onChange?: Function; + onMapReady?: Function; + onRegionChange?: Function; + onRegionChangeComplete?: Function; + onPress?: Function; + onLayout?: Function; + onLongPress?: Function; + onPanDrag?: Function; + onMarkerPress?: Function; + onMarkerSelect?: Function; + onMarkerDeselect?: Function; + onCalloutPress?: Function; + onMarkerDragStart?: Function; + onMarkerDrag?: Function; + onMarkerDragEnd?: Function; + minZoomLevel?: number; + maxZoomLevel?: number; + } + + export interface MarkerProperties { + identifier?: string; + reuseIdentifier?: string; + title?: string; + description?: string; + image?: any; + opacity?: number; + pinColor?: string; + coordinate: { latitude: number; longitude: number }; + centerOffset?: { x: number; y: number }; + calloutOffset?: { x: number; y: number }; + anchor?: { x: number; y: number }; + calloutAnchor?: { x: number; y: number }; + flat?: boolean; + draggable?: boolean; + onPress?: Function; + onSelect?: Function; + onDeselect?: Function; + onCalloutPress?: Function; + onDragStart?: Function; + onDrag?: Function; + onDragEnd?: Function; + } + + export interface MapPolylineProperties { + coordinates?: { latitude: number; longitude: number; }[]; + onPress?: Function; + tappable?: boolean; + fillColor?: string; + strokeWidth?: number; + strokeColor?: string; + zIndex?: number; + lineCap?: LineCapType; + lineJoin?: LineJoinType; + miterLimit?: number; + geodesic?: boolean; + lineDashPhase?: number; + lineDashPattern?: number[]; + } + + export interface MapPolygonProperties { + coordinates?: { latitude: number; longitude: number; }[]; + holes?: { latitude: number; longitude: number; }[][]; + onPress?: Function; + tappable?: boolean; + strokeWidth?: number; + strokeColor?: string; + fillColor?: string; + zIndex?: number; + lineCap?: LineCapType; + lineJoin?: LineJoinType; + miterLimit?: number; + geodesic?: boolean; + lineDashPhase?: number; + lineDashPattern?: number[]; + } + + export interface MapCircleProperties { + center: { latitude: number; longitude: number }; + radius: number; + onPress?: Function; + strokeWidth?: number; + strokeColor?: string; + fillColor?: string; + zIndex?: number; + lineCap?: LineCapType; + lineJoin?: LineJoinType; + miterLimit?: number; + lineDashPhase?: number; + lineDashPattern?: number[]; + } + + export interface MapUrlTitleProperties { + urlTemplate: string; + zIndex?: number; + } + + export interface MapCalloutProperties { + tooltip?: boolean; + onPress?: Function; + } + + class MapView extends React.Component { + static Animated: any; + static AnimatedRegion: any; + } + + namespace MapView { + class Marker extends React.Component {} + class Polyline extends React.Component {} + class Polygon extends React.Component {} + class Circle extends React.Component {} + class UrlTile extends React.Component {} + class Callout extends React.Component {} + } + + export default MapView; +} diff --git a/lib/android/build.gradle b/lib/android/build.gradle index 85cb5b4d24..326ab4bfa5 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 @@ -35,6 +35,6 @@ android { dependencies { provided "com.facebook.react:react-native:+" - compile "com.google.android.gms:play-services-base:10.2.0" - compile "com.google.android.gms:play-services-maps:10.2.0" + compile "com.google.android.gms:play-services-base:10.2.4" + compile "com.google.android.gms:play-services-maps:10.2.4" } diff --git a/lib/android/gradle.properties b/lib/android/gradle.properties index 9a87aa52e5..7047e93ba3 100644 --- a/lib/android/gradle.properties +++ b/lib/android/gradle.properties @@ -1,5 +1,5 @@ VERSION_CODE=4 -VERSION_NAME=0.14.0 +VERSION_NAME=0.17.0 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 c434c9c123..6f96e15da7 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 359e6847b5..b63ea29ffa 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 e428b04f69..a70d9146ad 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 c0eaf8f149..c8eabf2d12 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 1c15ade5fa..70484c1a46 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 62495c5e42..619e36435a 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 3ac1f4de79..6749e9a859 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,313 @@ 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, 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 ANIMATE_TO_VIEWING_ANGLE = 3; + private static final int ANIMATE_TO_BEARING = 4; + private static final int FIT_TO_ELEMENTS = 5; + private static final int FIT_TO_SUPPLIED_MARKERS = 6; + private static final int FIT_TO_COORDINATES = 7; + + 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 = "initialRegion") + public void setInitialRegion(AirMapView view, ReadableMap initialRegion) { + view.setInitialRegion(initialRegion); + } + + @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; + float bearing; + float angle; + 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 ANIMATE_TO_VIEWING_ANGLE: + angle = (float)args.getDouble(0); + duration = args.getInt(1); + view.animateToViewingAngle(angle, duration); + break; + + case ANIMATE_TO_BEARING: + bearing = (float)args.getDouble(0); + duration = args.getInt(1); + view.animateToBearing(bearing, 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, + "animateToViewingAngle", ANIMATE_TO_VIEWING_ANGLE, + "animateToBearing", ANIMATE_TO_BEARING, + "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 4041d91ac5..cb0274bfa4 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 @@ -2,6 +2,7 @@ import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.drawable.Animatable; import android.net.Uri; @@ -36,395 +37,398 @@ 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); + if (iconBitmapDescriptor != null) { + iconBitmap = BitmapFactory.decodeResource(getResources(), getDrawableResourceByName(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 e035e1e144..3aef3148cd 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 2cd1ba6860..dfc71d6d9c 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,138 @@ 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; +import com.google.android.gms.common.GoogleApiAvailability; -public class AirMapModule extends ReactContextBaseJavaModule { +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; - 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"; +import java.util.Map; +import java.util.HashMap; - public AirMapModule(ReactApplicationContext reactContext) { - super(reactContext); - } +import javax.annotation.Nullable; - @Override - public String getName() { - return "AirMapModule"; - } +public class AirMapModule extends ReactContextBaseJavaModule { - public Activity getActivity() { - return getCurrentActivity(); + 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"; + } + + @Override + public Map getConstants() { + final Map constants = new HashMap<>(); + constants.put("legalNotice", "This license information is displayed in Settings > Google > Open Source on any device running Google Play services."); + return constants; + } + + 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) { - public static void closeQuietly(Closeable closeable) { - if (closeable == null) return; - try { - closeable.close(); - } catch (IOException ignored) { + // 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) { - @ReactMethod - public void takeSnapshot(final int tag, final ReadableMap options, final Promise promise) { + // 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); + } - // 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) { - - // 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); - } - - // 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 226bc241cb..41257b32e6 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 3eaa0e5508..6f16057585 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 9a15fdc99d..488e2972f4 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 c5afc61e0f..be80acfbc0 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 691c2fd3c9..ae51a63b4d 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 7ea0726dc8..68bf07342c 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 772627e4a6..836fe0e35b 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; @@ -22,6 +21,7 @@ import android.widget.RelativeLayout; import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; @@ -53,797 +53,840 @@ 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) { - Context superContext = reactContext; - - 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, AirMapManager manager, - GoogleMapOptions googleMapOptions) { - super(getNonBuggyContext(reactContext), 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); + 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 boolean initialRegionSet = 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; } - synchronized (AirMapView.this) { - AirMapView.this.onResume(); - paused = 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 { - 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); - } - } + eventDispatcher = reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + } - public void animateToCoordinate(LatLng coordinate, int duration) { - if (map != null) { - startMonitoringRegion(); - map.animateCamera(CameraUpdateFactory.newLatLng(coordinate), duration, null); - } + @Override + public void onMapReady(final GoogleMap map) { + if (destroyed) { + return; } + this.map = map; + this.map.setInfoWindowAdapter(this); + this.map.setOnMarkerDragListener(this); - public void fitToElements(boolean animated) { - LatLngBounds.Builder builder = new LatLngBounds.Builder(); + manager.pushEvent(context, this, "onMapReady", new WritableNativeMap()); - boolean addedPosition = false; + final AirMapView view = this; - 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); - } - } - } + 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; - } - - 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)); - } + 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; - 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); - } + event.putString("action", "callout-press"); + manager.pushEvent(context, view, "onCalloutPress", 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); - + 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) { + if (!destroyed) { + 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)); + @Override + public void onHostPause() { + if (hasPermissions()) { + //noinspection MissingPermission + map.setMyLocationEnabled(false); + } + synchronized (AirMapView.this) { + if (!destroyed) { + AirMapView.this.onPause(); + } + paused = true; + } + } - 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); + @Override + public void onHostDestroy() { + AirMapView.this.doDestroy(); + } + }; - this.mapLoadingLayout.setVisibility(View.INVISIBLE); - } - this.setLoadingBackgroundColor(this.loadingBackgroundColor); - return this.mapLoadingLayout; - } + context.addLifecycleEventListener(lifecycleListener); + } + + private boolean hasPermissions() { + return checkSelfPermission(getContext(), PERMISSIONS[0]) == PackageManager.PERMISSION_GRANTED || + checkSelfPermission(getContext(), PERMISSIONS[1]) == PackageManager.PERMISSION_GRANTED; + } - 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; - } - } + /* + 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 setInitialRegion(ReadableMap initialRegion) { + if (!initialRegionSet && initialRegion != null) { + setRegion(initialRegion); + initialRegionSet = true; + } + } + + 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 { + 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; + int width = data.get("width") == null ? 0 : data.get("width").intValue(); + int height = data.get("height") == null ? 0 : data.get("height").intValue(); + + //fix for https://github.com/airbnb/react-native-maps/issues/245, + //it's not guaranteed the passed-in height and width would be greater than 0. + if (width <= 0 || height <= 0) { + map.moveCamera(CameraUpdateFactory.newLatLngBounds(boundsToMove, 0)); + } else { + map.moveCamera(CameraUpdateFactory.newLatLngBounds(boundsToMove, width, 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 animateToViewingAngle(float angle, int duration) { + if (map != null) { + startMonitoringRegion(); + CameraPosition cameraPosition = new CameraPosition.Builder(map.getCameraPosition()) + .tilt(angle) + .build(); + map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), duration, null); + } + } + + public void animateToBearing(float bearing, int duration) { + if (map != null) { + startMonitoringRegion(); + CameraPosition cameraPosition = new CameraPosition.Builder(map.getCameraPosition()) + .bearing(bearing) + .build(); + map.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition), 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 removeMapLoadingProgressBar() { - if (this.mapLoadingProgressBar != null) { - ((ViewGroup)this.mapLoadingProgressBar.getParent()).removeView(this.mapLoadingProgressBar); - this.mapLoadingProgressBar = 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 removeMapLoadingLayoutView() { - this.removeMapLoadingProgressBar(); - if (this.mapLoadingLayout != null) { - ((ViewGroup)this.mapLoadingLayout.getParent()).removeView(this.mapLoadingLayout); - this.mapLoadingLayout = null; - } - } + public void startMonitoringRegion() { + if (map == null || isMonitoringRegion) return; + timerHandler.postDelayed(timerRunnable, 100); + isMonitoringRegion = true; + } + + public void stopMonitoringRegion() { + if (map == null || !isMonitoringRegion) return; + timerHandler.removeCallbacks(timerRunnable); + isMonitoringRegion = false; + } + + private LatLngBounds lastBoundsEmitted; + + Handler timerHandler = new Handler(); + Runnable timerRunnable = new Runnable() { - 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(); - } - } - } + @Override + public void run() { - 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); - } + if (map != null) { + 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 ed1058c88d..d32e0dc726 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 e76f63aa27..6ecc551658 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,41 +13,41 @@ 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); - - return Arrays.asList( - calloutManager, - annotationManager, - polylineManager, - polygonManager, - circleManager, - mapManager, - mapLiteManager, - tileManager); - } + public MapsPackage(Activity activity) { + } // backwards compatibility + + public MapsPackage() { + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new AirMapModule(reactContext)); + } + + // Deprecated RN 0.47 + 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); + + return Arrays.asList( + calloutManager, + annotationManager, + polylineManager, + polygonManager, + circleManager, + mapManager, + mapLiteManager, + tileManager); + } } 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 28a3b322b8..43b1f678e6 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 40ace480f5..bf6c29d652 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/AnimatedRegion.js b/lib/components/AnimatedRegion.js index 29356ca9e3..ad18c5e077 100644 --- a/lib/components/AnimatedRegion.js +++ b/lib/components/AnimatedRegion.js @@ -111,25 +111,25 @@ export default class AnimatedMapRegion extends AnimatedWithChildren { spring(config) { var animations = []; config.hasOwnProperty('latitude') && - animations.push(Animated.timing(this.latitude, { + animations.push(Animated.spring(this.latitude, { ...config, toValue: config.latitude })); config.hasOwnProperty('longitude') && - animations.push(Animated.timing(this.longitude, { + animations.push(Animated.spring(this.longitude, { ...config, toValue: config.longitude })); config.hasOwnProperty('latitudeDelta') && - animations.push(Animated.timing(this.latitudeDelta, { + animations.push(Animated.spring(this.latitudeDelta, { ...config, toValue: config.latitudeDelta })); config.hasOwnProperty('longitudeDelta') && - animations.push(Animated.timing(this.longitudeDelta, { + animations.push(Animated.spring(this.longitudeDelta, { ...config, toValue: config.longitudeDelta })); diff --git a/lib/components/MapCallout.js b/lib/components/MapCallout.js index cf4bb9cef8..226b52fe2d 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 aab1ba8f19..acf066b248 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 be54e42db3..b81bab87af 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, @@ -256,7 +257,7 @@ class MapMarker extends React.Component { let image; if (this.props.image) { image = resolveAssetSource(this.props.image) || {}; - image = image.uri; + image = image.uri || this.props.image; } const AIRMapMarker = this.getAirComponent(); diff --git a/lib/components/MapPolygon.js b/lib/components/MapPolygon.js index 6901979db2..27f2aa322a 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 35b4c60484..af1573eff4 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 @@ -26,6 +27,11 @@ const propTypes = { */ onPress: PropTypes.func, + /* Boolean to allow a polyline to be tappable and use the + * onPress function + */ + tappable: PropTypes.bool, + /** * The fill color to use for the path. */ diff --git a/lib/components/MapUrlTile.js b/lib/components/MapUrlTile.js index cf27703348..f61c1e0d36 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 f25003b843..123d34f60f 100644 --- a/lib/components/MapView.js +++ b/lib/components/MapView.js @@ -1,13 +1,15 @@ -import React, { PropTypes } from 'react'; +import PropTypes from 'prop-types'; +import React from 'react'; import { EdgeInsetsPropType, Platform, - View, Animated, requireNativeComponent, NativeModules, ColorPropType, findNodeHandle, + View, + ViewPropTypes, } from 'react-native'; import MapMarker from './MapMarker'; import MapPolyline from './MapPolyline'; @@ -44,8 +46,11 @@ const viewConfig = { }, }; +// if ViewPropTypes is not defined fall back to View.propType (to support RN < 0.44) +const viewPropTypes = ViewPropTypes || View.propTypes; + const propTypes = { - ...View.propTypes, + ...viewPropTypes, /** * When provider is "google", we will use GoogleMaps. * Any value other than "google" will default to using @@ -59,7 +64,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 @@ -317,6 +322,11 @@ const propTypes = { */ legalLabelInsets: EdgeInsetsPropType, + /** + * Callback that is called once the map is fully loaded. + */ + onMapReady: PropTypes.func, + /** * Callback that is called continuously when the user is dragging the map. */ @@ -384,6 +394,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 { @@ -430,24 +450,29 @@ class MapView extends React.Component { } _onMapReady() { - const { region, initialRegion } = this.props; + const { region, initialRegion, onMapReady } = this.props; if (region) { this.map.setNativeProps({ region }); } else if (initialRegion) { - this.map.setNativeProps({ region: initialRegion }); + this.map.setNativeProps({ initialRegion }); } this._updateStyle(); - this.setState({ isReady: true }); + this.setState({ isReady: true }, () => { + if (onMapReady) onMapReady(); + }); } _onLayout(e) { const { layout } = e.nativeEvent; if (!layout.width || !layout.height) return; if (this.state.isReady && !this.__layoutCalled) { - const region = this.props.region || this.props.initialRegion; + const { region, initialRegion } = this.props; if (region) { this.__layoutCalled = true; this.map.setNativeProps({ region }); + } else if (initialRegion) { + this.__layoutCalled = true; + this.map.setNativeProps({ initialRegion }); } } if (this.props.onLayout) { @@ -474,6 +499,14 @@ class MapView extends React.Component { this._runCommand('animateToCoordinate', [latLng, duration || 500]); } + animateToBearing(bearing, duration) { + this._runCommand('animateToBearing', [bearing, duration || 500]); + } + + animateToViewingAngle(angle, duration) { + this._runCommand('animateToViewingAngle', [angle, duration || 500]); + } + fitToElements(animated) { this._runCommand('fitToElements', [animated]); } diff --git a/lib/components/decorateMapComponent.js b/lib/components/decorateMapComponent.js index e655c4c33c..168d4f1226 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/AIRGMSPolygon.h b/lib/ios/AirGoogleMaps/AIRGMSPolygon.h index 3466b36915..d41c87d5e4 100644 --- a/lib/ios/AirGoogleMaps/AIRGMSPolygon.h +++ b/lib/ios/AirGoogleMaps/AIRGMSPolygon.h @@ -6,7 +6,7 @@ // #import -#import "UIView+React.h" +#import @class AIRGoogleMapPolygon; diff --git a/lib/ios/AirGoogleMaps/AIRGMSPolyline.h b/lib/ios/AirGoogleMaps/AIRGMSPolyline.h new file mode 100644 index 0000000000..d7ee197831 --- /dev/null +++ b/lib/ios/AirGoogleMaps/AIRGMSPolyline.h @@ -0,0 +1,16 @@ +// +// AIRGMSPolyline.h +// AirMaps +// +// Created by Guilherme Pontes 04/05/2017. +// + +#import +#import + +@class AIRGoogleMapPolyline; + +@interface AIRGMSPolyline : GMSPolyline +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; +@end diff --git a/lib/ios/AirGoogleMaps/AIRGMSPolyline.m b/lib/ios/AirGoogleMaps/AIRGMSPolyline.m new file mode 100644 index 0000000000..86e7ac0555 --- /dev/null +++ b/lib/ios/AirGoogleMaps/AIRGMSPolyline.m @@ -0,0 +1,11 @@ +// +// AIRGMSPolyline.m +// AirMaps +// +// Created by Guilherme Pontes 04/05/2017. +// + +#import "AIRGMSPolyline.h" + +@implementation AIRGMSPolyline +@end diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMap.h b/lib/ios/AirGoogleMaps/AIRGoogleMap.h index 9e6ae8b799..d2a8e3e6f3 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMap.h +++ b/lib/ios/AirGoogleMaps/AIRGoogleMap.h @@ -18,6 +18,7 @@ @property (nonatomic, assign) MKCoordinateRegion initialRegion; @property (nonatomic, assign) MKCoordinateRegion region; @property (nonatomic, assign) NSString *customMapStyleString; +@property (nonatomic, copy) RCTBubblingEventBlock onMapReady; @property (nonatomic, copy) RCTBubblingEventBlock onPress; @property (nonatomic, copy) RCTBubblingEventBlock onLongPress; @property (nonatomic, copy) RCTBubblingEventBlock onMarkerPress; @@ -39,8 +40,11 @@ @property (nonatomic, assign) BOOL pitchEnabled; @property (nonatomic, assign) BOOL showsUserLocation; @property (nonatomic, assign) BOOL showsMyLocationButton; +@property (nonatomic, assign) BOOL showsIndoorLevelPicker; +- (void)didFinishTileRendering; - (BOOL)didTapMarker:(GMSMarker *)marker; +- (void)didTapPolyline:(GMSPolyline *)polyline; - (void)didTapPolygon:(GMSPolygon *)polygon; - (void)didTapAtCoordinate:(CLLocationCoordinate2D)coordinate; - (void)didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate; diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMap.m b/lib/ios/AirGoogleMaps/AIRGoogleMap.m index ca15da04e5..f102385315 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMap.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMap.m @@ -155,6 +155,10 @@ - (void)setRegion:(MKCoordinateRegion)region { self.camera = [AIRGoogleMap makeGMSCameraPositionFromMap:self andMKCoordinateRegion:region]; } +- (void)didFinishTileRendering { + if (self.onMapReady) self.onMapReady(@{}); +} + - (BOOL)didTapMarker:(GMSMarker *)marker { AIRGMSMarker *airMarker = (AIRGMSMarker *)marker; @@ -170,6 +174,16 @@ - (BOOL)didTapMarker:(GMSMarker *)marker { return NO; } +- (void)didTapPolyline:(GMSOverlay *)polyline { + AIRGMSPolyline *airPolyline = (AIRGMSPolyline *)polyline; + + id event = @{@"action": @"polyline-press", + @"id": airPolyline.identifier ?: @"unknown", + }; + + if (airPolyline.onPress) airPolyline.onPress(event); +} + - (void)didTapPolygon:(GMSOverlay *)polygon { AIRGMSPolygon *airPolygon = (AIRGMSPolygon *)polygon; @@ -290,6 +304,21 @@ - (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 ]; +} + +- (void)setShowsIndoorLevelPicker:(BOOL)showsIndoorLevelPicker { + self.settings.indoorPicker = showsIndoorLevelPicker; +} + +- (BOOL)showsIndoorLevelPicker { + return self.settings.indoorPicker; +} + (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 785d58d4d4..12d63e1042 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"; @@ -57,7 +58,9 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL) RCT_EXPORT_VIEW_PROPERTY(showsMyLocationButton, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsIndoorLevelPicker, BOOL) RCT_EXPORT_VIEW_PROPERTY(customMapStyleString, NSString) +RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) @@ -65,6 +68,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 +80,67 @@ - (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]; + } + }]; +} + +RCT_EXPORT_METHOD(animateToViewingAngle:(nonnull NSNumber *)reactTag + withAngle:(double)angle + 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 *mapView = (AIRGoogleMap *)view; + [mapView animateToViewingAngle:angle]; + [CATransaction commit]; + } + }]; +} + +RCT_EXPORT_METHOD(animateToBearing:(nonnull NSNumber *)reactTag + withBearing:(CGFloat)bearing + 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 *mapView = (AIRGoogleMap *)view; + [mapView animateToBearing:bearing]; + [CATransaction commit]; } }]; } @@ -197,6 +259,14 @@ - (UIView *)view }]; } +- (NSDictionary *)constantsToExport { + return @{ @"legalNotice": [GMSServices openSourceLicenseInfo] }; +} + +- (void)mapViewDidFinishTileRendering:(GMSMapView *)mapView { + AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; + [googleMapView didFinishTileRendering]; +} - (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker { AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView; diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.h b/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.h index 36190e7cb6..298884f7d2 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.h +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.h @@ -17,6 +17,7 @@ @property (nonatomic, strong) AIRGoogleMapCallout *calloutView; @property (nonatomic, strong) NSString *identifier; @property (nonatomic, assign) CLLocationCoordinate2D coordinate; +@property (nonatomic, assign) CLLocationDegrees rotation; @property (nonatomic, strong) AIRGMSMarker* realMarker; @property (nonatomic, copy) RCTBubblingEventBlock onPress; @property (nonatomic, copy) RCTDirectEventBlock onDragStart; diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.m b/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.m index 2c6080e9ef..c41be55bbb 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapMarker.m @@ -157,6 +157,14 @@ - (CLLocationCoordinate2D)coordinate { return _realMarker.position; } +- (void)setRotation:(CLLocationDegrees)rotation { + _realMarker.rotation = rotation; +} + +- (CLLocationDegrees)rotation { + return _realMarker.rotation; +} + - (void)setIdentifier:(NSString *)identifier { _realMarker.identifier = identifier; } diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapMarkerManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapMarkerManager.m index 8be4ba53e9..b5572bf9d2 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapMarkerManager.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapMarkerManager.m @@ -28,6 +28,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(identifier, NSString) RCT_EXPORT_VIEW_PROPERTY(coordinate, CLLocationCoordinate2D) +RCT_EXPORT_VIEW_PROPERTY(rotation, CLLocationDegrees) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString) RCT_EXPORT_VIEW_PROPERTY(title, NSString) diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.h b/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.h index b127567a52..adebc40d6c 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.h +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.h @@ -5,18 +5,25 @@ // #import #import +#import +#import "AIRGMSPolyline.h" #import "AIRMapCoordinate.h" #import "AIRGoogleMapMarker.h" @interface AIRGoogleMapPolyline : UIView -@property (nonatomic, strong) GMSPolyline* polyline; +@property (nonatomic, weak) RCTBridge *bridge; +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, strong) AIRGMSPolyline *polyline; @property (nonatomic, strong) NSArray *coordinates; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; + @property (nonatomic, strong) UIColor *strokeColor; @property (nonatomic, assign) double strokeWidth; @property (nonatomic, assign) UIColor *fillColor; @property (nonatomic, assign) BOOL geodesic; @property (nonatomic, assign) NSString *title; @property (nonatomic, assign) int zIndex; +@property (nonatomic, assign) BOOL tappable; @end diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.m b/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.m index 009d90bfba..881f17be68 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapPolyline.m @@ -5,6 +5,7 @@ // #import #import "AIRGoogleMapPolyline.h" +#import "AIRGMSPolyline.h" #import "AIRMapCoordinate.h" #import "AIRGoogleMapMarker.h" #import "AIRGoogleMapMarkerManager.h" @@ -16,7 +17,7 @@ @implementation AIRGoogleMapPolyline - (instancetype)init { if (self = [super init]) { - _polyline = [[GMSPolyline alloc] init]; + _polyline = [[AIRGMSPolyline alloc] init]; } return self; } @@ -24,13 +25,13 @@ - (instancetype)init -(void)setCoordinates:(NSArray *)coordinates { _coordinates = coordinates; - + GMSMutablePath *path = [GMSMutablePath path]; for(int i = 0; i < coordinates.count; i++) { [path addCoordinate:coordinates[i].coordinate]; } - + _polyline.path = path; } @@ -70,4 +71,14 @@ -(void) setZIndex:(int)zIndex _polyline.zIndex = zIndex; } +-(void)setTappable:(BOOL)tappable +{ + _tappable = tappable; + _polyline.tappable = tappable; +} + +- (void)setOnPress:(RCTBubblingEventBlock)onPress { + _polyline.onPress = onPress; +} + @end diff --git a/lib/ios/AirGoogleMaps/AIRGoogleMapPolylineManager.m b/lib/ios/AirGoogleMaps/AIRGoogleMapPolylineManager.m index acad1631bd..a7515c207d 100644 --- a/lib/ios/AirGoogleMaps/AIRGoogleMapPolylineManager.m +++ b/lib/ios/AirGoogleMaps/AIRGoogleMapPolylineManager.m @@ -26,6 +26,7 @@ @implementation AIRGoogleMapPolylineManager - (UIView *)view { AIRGoogleMapPolyline *polyline = [AIRGoogleMapPolyline new]; + polyline.bridge = self.bridge; return polyline; } @@ -35,5 +36,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(strokeWidth, double) RCT_EXPORT_VIEW_PROPERTY(geodesic, BOOL) RCT_EXPORT_VIEW_PROPERTY(zIndex, int) +RCT_EXPORT_VIEW_PROPERTY(tappable, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) @end diff --git a/lib/ios/AirGoogleMaps/RCTConvert+GMSMapViewType.m b/lib/ios/AirGoogleMaps/RCTConvert+GMSMapViewType.m index bd339f30e1..cfceba5590 100644 --- a/lib/ios/AirGoogleMaps/RCTConvert+GMSMapViewType.m +++ b/lib/ios/AirGoogleMaps/RCTConvert+GMSMapViewType.m @@ -13,6 +13,7 @@ @implementation RCTConvert (GMSMapViewType) ( @{ @"standard": @(kGMSTypeNormal), + @"satellite": @(kGMSTypeSatellite), @"hybrid": @(kGMSTypeHybrid), @"terrain": @(kGMSTypeTerrain), @"none": @(kGMSTypeNone) diff --git a/lib/ios/AirMaps.xcodeproj/project.pbxproj b/lib/ios/AirMaps.xcodeproj/project.pbxproj index 39b1256e20..ac641aff72 100644 --- a/lib/ios/AirMaps.xcodeproj/project.pbxproj +++ b/lib/ios/AirMaps.xcodeproj/project.pbxproj @@ -21,7 +21,7 @@ 1125B2E51C4AD3DA007D0023 /* AIRMapPolyline.m in Sources */ = {isa = PBXBuildFile; fileRef = 1125B2D41C4AD3DA007D0023 /* AIRMapPolyline.m */; }; 1125B2E61C4AD3DA007D0023 /* AIRMapPolylineManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1125B2D61C4AD3DA007D0023 /* AIRMapPolylineManager.m */; }; 1125B2F21C4AD445007D0023 /* SMCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1125B2F11C4AD445007D0023 /* SMCalloutView.m */; }; - 19DABC7F1E7C9D3C00F41150 /* RCTConvert+MapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 19DABC7E1E7C9D3C00F41150 /* RCTConvert+MapKit.m */; }; + 19DABC7F1E7C9D3C00F41150 /* RCTConvert+AirMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 19DABC7E1E7C9D3C00F41150 /* RCTConvert+AirMap.m */; }; DA6C26381C9E2AFE0035349F /* AIRMapUrlTile.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6C26371C9E2AFE0035349F /* AIRMapUrlTile.m */; }; DA6C263E1C9E324A0035349F /* AIRMapUrlTileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6C263D1C9E324A0035349F /* AIRMapUrlTileManager.m */; }; /* End PBXBuildFile section */ @@ -68,8 +68,8 @@ 1125B2F01C4AD445007D0023 /* SMCalloutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SMCalloutView.h; path = AirMaps/Callout/SMCalloutView.h; sourceTree = SOURCE_ROOT; }; 1125B2F11C4AD445007D0023 /* SMCalloutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = SMCalloutView.m; path = AirMaps/Callout/SMCalloutView.m; sourceTree = SOURCE_ROOT; }; 11FA5C511C4A1296003AC2EE /* libAirMaps.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAirMaps.a; sourceTree = BUILT_PRODUCTS_DIR; }; - 19DABC7D1E7C9D3C00F41150 /* RCTConvert+MapKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+MapKit.h"; sourceTree = ""; }; - 19DABC7E1E7C9D3C00F41150 /* RCTConvert+MapKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+MapKit.m"; sourceTree = ""; }; + 19DABC7D1E7C9D3C00F41150 /* RCTConvert+AirMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+AirMap.h"; sourceTree = ""; }; + 19DABC7E1E7C9D3C00F41150 /* RCTConvert+AirMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+AirMap.m"; sourceTree = ""; }; DA6C26361C9E2AFE0035349F /* AIRMapUrlTile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapUrlTile.h; sourceTree = ""; }; DA6C26371C9E2AFE0035349F /* AIRMapUrlTile.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapUrlTile.m; sourceTree = ""; }; DA6C263C1C9E324A0035349F /* AIRMapUrlTileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapUrlTileManager.h; sourceTree = ""; }; @@ -134,8 +134,8 @@ 1125B2D61C4AD3DA007D0023 /* AIRMapPolylineManager.m */, 1125B2F01C4AD445007D0023 /* SMCalloutView.h */, 1125B2F11C4AD445007D0023 /* SMCalloutView.m */, - 19DABC7D1E7C9D3C00F41150 /* RCTConvert+MapKit.h */, - 19DABC7E1E7C9D3C00F41150 /* RCTConvert+MapKit.m */, + 19DABC7D1E7C9D3C00F41150 /* RCTConvert+AirMap.h */, + 19DABC7E1E7C9D3C00F41150 /* RCTConvert+AirMap.m */, DA6C26361C9E2AFE0035349F /* AIRMapUrlTile.h */, DA6C26371C9E2AFE0035349F /* AIRMapUrlTile.m */, DA6C263C1C9E324A0035349F /* AIRMapUrlTileManager.h */, @@ -206,7 +206,7 @@ 1125B2E01C4AD3DA007D0023 /* AIRMapManager.m in Sources */, 1125B2E61C4AD3DA007D0023 /* AIRMapPolylineManager.m in Sources */, 1125B2DD1C4AD3DA007D0023 /* AIRMapCircle.m in Sources */, - 19DABC7F1E7C9D3C00F41150 /* RCTConvert+MapKit.m in Sources */, + 19DABC7F1E7C9D3C00F41150 /* RCTConvert+AirMap.m in Sources */, 1125B2E51C4AD3DA007D0023 /* AIRMapPolyline.m in Sources */, DA6C263E1C9E324A0035349F /* AIRMapUrlTileManager.m in Sources */, 1125B2DA1C4AD3DA007D0023 /* AIRMap.m in Sources */, diff --git a/lib/ios/AirMaps/AIRMap.h b/lib/ios/AirMaps/AIRMap.h index 18c9a0bc05..1d5f92cdcc 100644 --- a/lib/ios/AirMaps/AIRMap.h +++ b/lib/ios/AirMaps/AIRMap.h @@ -17,6 +17,7 @@ extern const CLLocationDegrees AIRMapDefaultSpan; extern const NSTimeInterval AIRMapRegionChangeObserveInterval; extern const CGFloat AIRMapZoomBoundBuffer; +extern const NSInteger AIRMapMaxZoomLevel; @interface AIRMap: MKMapView @@ -37,6 +38,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; @@ -44,6 +47,7 @@ extern const CGFloat AIRMapZoomBoundBuffer; @property (nonatomic, assign) BOOL ignoreRegionChanges; +@property (nonatomic, copy) RCTBubblingEventBlock onMapReady; @property (nonatomic, copy) RCTBubblingEventBlock onChange; @property (nonatomic, copy) RCTBubblingEventBlock onPress; @property (nonatomic, copy) RCTBubblingEventBlock onPanDrag; diff --git a/lib/ios/AirMaps/AIRMap.m b/lib/ios/AirMaps/AIRMap.m index a9db64ab45..647be4b8a5 100644 --- a/lib/ios/AirMaps/AIRMap.m +++ b/lib/ios/AirMaps/AIRMap.m @@ -21,6 +21,7 @@ const CLLocationDegrees AIRMapDefaultSpan = 0.005; const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1; const CGFloat AIRMapZoomBoundBuffer = 0.01; +const NSInteger AIRMapMaxZoomLevel = 20; @interface MKMapView (UIGestureRecognizer) @@ -79,6 +80,9 @@ - (instancetype)init // be identical to the built-in callout view (which has a private API) self.calloutView = [SMCalloutView platformCalloutView]; self.calloutView.delegate = self; + + self.minZoomLevel = 0; + self.maxZoomLevel = AIRMapMaxZoomLevel; } return self; } diff --git a/lib/ios/AirMaps/AIRMapManager.h b/lib/ios/AirMaps/AIRMapManager.h index cc9a8c75b5..1d73b405e7 100644 --- a/lib/ios/AirMaps/AIRMapManager.h +++ b/lib/ios/AirMaps/AIRMapManager.h @@ -8,7 +8,22 @@ */ #import +#import "AIRMap.h" + +#define MERCATOR_RADIUS 85445659.44705395 +#define MERCATOR_OFFSET 268435456 @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 619142bd66..4838343693 100644 --- a/lib/ios/AirMaps/AIRMapManager.m +++ b/lib/ios/AirMaps/AIRMapManager.m @@ -85,6 +85,7 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType) +RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTBubblingEventBlock) RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) @@ -96,7 +97,20 @@ - (UIView *)view RCT_EXPORT_VIEW_PROPERTY(onMarkerDrag, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onMarkerDragEnd, RCTDirectEventBlock) RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock) -RCT_EXPORT_VIEW_PROPERTY(initialRegion, MKCoordinateRegion) +RCT_CUSTOM_VIEW_PROPERTY(initialRegion, MKCoordinateRegion, AIRMap) +{ + if (json == nil) return; + + // don't emit region change events when we are setting the initialRegion + BOOL originalIgnore = view.ignoreRegionChanges; + view.ignoreRegionChanges = YES; + [view setInitialRegion:[RCTConvert MKCoordinateRegion:json]]; + view.ignoreRegionChanges = originalIgnore; +} + +RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat) + RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap) { @@ -148,6 +162,48 @@ - (UIView *)view }]; } +RCT_EXPORT_METHOD(animateToViewingAngle:(nonnull NSNumber *)reactTag + withAngle:(double)angle + withDuration:(CGFloat)duration) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMap *mapView = (AIRMap *)view; + + MKMapCamera *mapCamera = [[mapView camera] copy]; + [mapCamera setPitch:angle]; + + [AIRMap animateWithDuration:duration/1000 animations:^{ + [mapView setCamera:mapCamera animated:YES]; + }]; + } + }]; +} + +RCT_EXPORT_METHOD(animateToBearing:(nonnull NSNumber *)reactTag + withBearing:(CGFloat)bearing + withDuration:(CGFloat)duration) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMap *mapView = (AIRMap *)view; + + MKMapCamera *mapCamera = [[mapView camera] copy]; + [mapCamera setHeading:bearing]; + + [AIRMap animateWithDuration:duration/1000 animations:^{ + [mapView setCamera:mapCamera animated:YES]; + }]; + } + }]; +} + RCT_EXPORT_METHOD(fitToElements:(nonnull NSNumber *)reactTag animated:(BOOL)animated) { @@ -478,6 +534,8 @@ - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id 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) { @@ -651,6 +717,8 @@ - (void)mapViewDidFinishRenderingMap:(AIRMap *)mapView fullyRendered:(BOOL)fully { [mapView finishLoading]; [mapView cacheViewIfNeeded]; + + mapView.onMapReady(@{}); } #pragma mark Private @@ -665,6 +733,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 +828,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 = AIRMapMaxZoomLevel - 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, AIRMapMaxZoomLevel); + + // 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 = AIRMapMaxZoomLevel - 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 = AIRMapMaxZoomLevel - zoomExponent; + + return zoomLevel; +} + @end diff --git a/package.json b/package.json index 9bbc2e1dd4..66e8be298c 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.14.0", + "version": "0.17.0", "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,12 +49,13 @@ "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": { - "sourceDir": "./android" + "sourceDir": "./lib/android" } } }