chore: add mobile app

This commit is contained in:
2026-02-07 22:33:02 +01:00
parent db34640acc
commit e89384ad4e
29327 changed files with 3886944 additions and 12 deletions
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Airbnb
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+666
View File
@@ -0,0 +1,666 @@
# react-native-maps [![npm version](https://img.shields.io/npm/v/react-native-maps.svg?style=flat)](https://www.npmjs.com/package/react-native-maps)
React Native Map components for iOS + Android
## Contributing
This project is being maintained by a small group of people, and any help with issues and pull requests are always appreciated. If you are able and willing to contribute, please read the [guidelines](./CONTRIBUTING.md).
## Installation
See [Installation Instructions](docs/installation.md).
See [Setup Instructions for the Included Example Project](docs/examples-setup.md).
## Compatibility
## React-Native Requirements
- **Version 1.14.0 and above**: Requires `react-native >= 0.74`.
- **Versions below 1.14.0**: Require `react-native >= 0.64.3`.
## Component API
[`<MapView />` Component API](docs/mapview.md)
[`<Marker />` Component API](docs/marker.md)
[`<Callout />` Component API](docs/callout.md)
[`<Polygon />` Component API](docs/polygon.md)
[`<Polyline />` Component API](docs/polyline.md)
[`<Circle />` Component API](docs/circle.md)
[`<Overlay />` Component API](docs/overlay.md)
[`<Heatmap />` Component API](docs/heatmap.md)
[`<Geojson />` Component API](docs/geojson.md)
## General Usage
```js
import MapView from 'react-native-maps';
```
or
```js
var MapView = require('react-native-maps');
```
This MapView component is built so that features on the map (such as Markers, Polygons, etc.) are
specified as children of the MapView itself. This provides an intuitive and react-like API for
declaratively controlling features on the map.
### Rendering a Map with an initial region
## MapView
```jsx
<MapView
initialRegion={{
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
}}
/>
```
### Using a MapView while controlling the region as state
```jsx
getInitialState() {
return {
region: {
latitude: 37.78825,
longitude: -122.4324,
latitudeDelta: 0.0922,
longitudeDelta: 0.0421,
},
};
}
onRegionChange(region) {
this.setState({ region });
}
render() {
return (
<MapView
region={this.state.region}
onRegionChange={this.onRegionChange}
/>
);
}
```
### Rendering a list of markers on a map
```jsx
import {Marker} from 'react-native-maps';
<MapView region={this.state.region} onRegionChange={this.onRegionChange}>
{this.state.markers.map((marker, index) => (
<Marker
key={index}
coordinate={marker.latlng}
title={marker.title}
description={marker.description}
/>
))}
</MapView>;
```
### Rendering a Marker with a custom image
1. You need to generate an `png` image with various resolution (lets call them `custom_pin`) - for more information go to [Android](https://developer.android.com/studio/write/resource-manager#import), [iOS](https://developer.apple.com/documentation/xcode/adding-images-to-your-xcode-project)
2. put all images in Android drawables and iOS assets dir
3. Now you can use the following code:
```jsx
<Marker
coordinate={{latitude: latitude, longitude: longitude}}
image={{uri: 'custom_pin'}}
/>
```
Note: You can also pass the image binary data like `image={require('custom_pin.png')}`, but this will not scale good with the different screen sizes.
### Rendering a Marker with a custom view
Note: This has performance implications, if you wish for a simpler solution go with a custom image (save your self the headache)
```jsx
<Marker coordinate={{latitude: latitude, longitude: longitude}}>
<MyCustomMarkerView {...marker} />
</Marker>
```
### Rendering a custom Marker with a custom Callout
```jsx
import {Callout} from 'react-native-maps';
<Marker coordinate={marker.latlng}>
<MyCustomMarkerView {...marker} />
<Callout>
<MyCustomCalloutView {...marker} />
</Callout>
</Marker>;
```
### Draggable Markers
```jsx
<MapView initialRegion={...}>
<Marker draggable
coordinate={this.state.x}
onDragEnd={(e) => this.setState({ x: e.nativeEvent.coordinate })}
/>
</MapView>
```
### Using a custom Tile Overlay
#### Tile Overlay using tile server
```jsx
import {UrlTile} from 'react-native-maps';
<MapView region={this.state.region} onRegionChange={this.onRegionChange}>
<UrlTile
/**
* The url template of the tile server. The patterns {x} {y} {z} will be replaced at runtime
* For example, http://c.tile.openstreetmap.org/{z}/{x}/{y}.png
*/
urlTemplate={this.state.urlTemplate}
/**
* The maximum zoom level for this tile overlay. Corresponds to the maximumZ setting in
* MKTileOverlay. iOS only.
*/
maximumZ={19}
/**
* flipY allows tiles with inverted y coordinates (origin at bottom left of map)
* to be used. Its default value is false.
*/
flipY={false}
/>
</MapView>;
```
For Android: add the following line in your AndroidManifest.xml
```xml
<uses-permission android:name="android.permission.INTERNET" />
```
For IOS: configure [App Transport Security](https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW33) in your app
## React Native Configuration for Fabric / New Architecture
This library works with Fabric using the [New Renderer Interop Layer](https://github.com/reactwg/react-native-new-architecture/discussions/135)
There is a warning message that those steps are not necessary; but we couldn't get the example working without them so far.
### Configuration Steps
1. **Open your configuration file**: Locate the `react-native-config` file in your project directory.
2. **Add the following configuration**: Include the `unstable_reactLegacyComponentNames` array for both Android and iOS platforms as shown below:
```javascript
module.exports = {
project: {
android: {
unstable_reactLegacyComponentNames: [
'AIRMap',
'AIRMapCallout',
'AIRMapCalloutSubview',
'AIRMapCircle',
'AIRMapHeatmap',
'AIRMapLocalTile',
'AIRMapMarker',
'AIRMapOverlay',
'AIRMapPolygon',
'AIRMapPolyline',
'AIRMapUrlTile',
'AIRMapWMSTile',
],
},
ios: {
unstable_reactLegacyComponentNames: [
'AIRMap',
'AIRMapCallout',
'AIRMapCalloutSubview',
'AIRMapCircle',
'AIRMapHeatmap',
'AIRMapLocalTile',
'AIRMapMarker',
'AIRMapOverlay',
'AIRMapPolygon',
'AIRMapPolyline',
'AIRMapUrlTile',
'AIRMapWMSTile',
],
},
},
};
```
checkout the example project to see it in action.
#### Tile Overlay using local tiles
Tiles can be stored locally within device using xyz tiling scheme and displayed as tile overlay as well. This is usefull especially for offline map usage when tiles are available for selected map region within device storage.
```jsx
import {LocalTile} from 'react-native-maps';
<MapView region={this.state.region} onRegionChange={this.onRegionChange}>
<LocalTile
/**
* The path template of the locally stored tiles. The patterns {x} {y} {z} will be replaced at runtime
* For example, /storage/emulated/0/mytiles/{z}/{x}/{y}.png
*/
pathTemplate={this.state.pathTemplate}
/**
* The size of provided local tiles (usually 256 or 512).
*/
tileSize={256}
/>
</MapView>;
```
For Android: LocalTile is still just overlay over original map tiles. It means that if device is online, underlying tiles will be still downloaded. If original tiles download/display is not desirable set mapType to 'none'. For example:
```
<MapView
mapType={Platform.OS == "android" ? "none" : "standard"}
>
```
See [OSM Wiki](https://wiki.openstreetmap.org/wiki/Category:Tile_downloading) for how to download tiles for offline usage.
### Overlaying other components on the map
Place components that you wish to overlay `MapView` underneath the `MapView` closing tag. Absolutely position these elements.
```jsx
render() {
return (
<MapView
region={this.state.region}
/>
<OverlayComponent
style={{position: "absolute", bottom: 50}}
/>
);
}
```
### Customizing the map style (Google Maps Only)
The `<MapView provider="google" googleMapId="yourStyledMapId" />` Google Maps on iOS and Android supports styling via google cloud platform, the styled maps are published under a googleMapId, by simply setting the property googleMapId to the MapView you can use that styled map
more info here: [google map id](https://developers.google.com/maps/documentation/get-map-id)
### MapView Events
The `<MapView />` component and its child components have several events that you can subscribe to.
This example displays some of them in a log as a demonstration.
![](http://i.giphy.com/3o6UBpncYQASu2WTW8.gif) ![](http://i.giphy.com/xT77YdviLqtjaecRYA.gif)
### Tracking Region / Location
![](http://i.giphy.com/3o6UBoPSLlIKQ2dv7q.gif) ![](http://i.giphy.com/xT77XWjqECvdgjx9oA.gif)
### Programmatically Changing Region
One can change the mapview's position using refs and component methods, or by passing in an updated
`region` prop. The component methods will allow one to animate to a given position like the native
API could.
![](http://i.giphy.com/3o6UB7poyB6YJ0KPWU.gif) ![](http://i.giphy.com/xT77Yc4wK3pzZusEbm.gif)
### Changing the style of the map
![](http://i.imgur.com/a9WqCL6.png)
### Arbitrary React Views as Markers
![](http://i.giphy.com/3o6UBcsCLoLQtksJxe.gif) ![](http://i.giphy.com/3o6UB1qGEM9jYni3KM.gif)
### Using the MapView with the Animated API
The `<MapView />` component can be made to work with the Animated API, having the entire `region` prop
be declared as an animated value. This allows one to animate the zoom and position of the MapView along
with other gestures, giving a nice feel.
Further, Marker views can use the animated API to enhance the effect.
![](http://i.giphy.com/xT77XMw9IwS6QAv0nC.gif) ![](http://i.giphy.com/3o6UBdGQdM1GmVoIdq.gif)
Issue: Since android needs to render its marker views as a bitmap, the animations APIs may not be
compatible with the Marker views. Not sure if this can be worked around yet or not.
Markers' coordinates can also be animated, as shown in this example:
![](http://i.giphy.com/xTcnTelp1OwGPu1Wh2.gif) ![](http://i.giphy.com/xTcnT6WVpwlCiQnFW8.gif)
### Polygon Creator
![](http://i.giphy.com/3o6UAZWqQBkOzs8HE4.gif) ![](http://i.giphy.com/xT77XVBRErNZl3zyWQ.gif)
### Other Overlays
So far, `<Circle />`, `<Polygon />`, and `<Polyline />` are available to pass in as children to the
`<MapView />` component.
![](http://i.giphy.com/xT77XZCH8JpEhzVcNG.gif) ![](http://i.giphy.com/xT77XZyA0aYeOX5jsA.gif)
### Gradient Polylines (iOS MapKit only)
Gradient polylines can be created using the `strokeColors` prop of the `<Polyline>` component.
![](https://i.imgur.com/P7UeqAm.png?1)
### Default Markers
Default markers will be rendered unless a custom marker is specified. One can optionally adjust the
color of the default marker by using the `pinColor` prop.
![](http://i.giphy.com/xT77Y0pWKmUUnguHK0.gif) ![](http://i.giphy.com/3o6UBfk3I58VIwZjVe.gif)
### Custom Callouts
Callouts to markers can be completely arbitrary react views, similar to markers. As a result, they
can be interacted with like any other view.
Additionally, you can fall back to the standard behavior of just having a title/description through
the `<Marker />`'s `title` and `description` props.
Custom callout views can be the entire tooltip bubble, or just the content inside of the system
default bubble.
To handle press on specific subview of callout use `<CalloutSubview />` with `onPress`.
See `Callouts.js` example.
![](http://i.giphy.com/xT77XNePGnMIIDpbnq.gif) ![](http://i.giphy.com/xT77YdU0HXryvoRqaQ.gif)
### Image-based Markers
Markers can be customized by just using images, and specified using the `image` prop.
![](http://i.imgur.com/mzrOjTR.png)
### Draggable Markers
Markers are draggable, and emit continuous drag events to update other UI during drags.
![](http://i.giphy.com/l2JImnZxdv1WbpQfC.gif) ![](http://i.giphy.com/l2JIhv4Jx6Ugx1EGI.gif)
### Lite Mode ( Android )
Enable lite mode on Android with `liteMode` prop. Ideal when having multiple maps in a View or ScrollView.
![](http://i.giphy.com/qZ2lAf18s89na.gif)
### On Poi Click (Google Maps Only)
Poi are clickable, you can catch the event to get its information (usually to get the full detail from Google Place using the placeId).
![](https://media.giphy.com/media/3480VsCKnHr31uCLU3/giphy.gif)
### Animated Region
The MapView can accept an `AnimatedRegion` value as its `region` prop. This allows you to utilize the Animated API to control the map's center and zoom.
```jsx
import MapView, { AnimatedRegion, Animated } from 'react-native-maps';
getInitialState() {
return {
region: new AnimatedRegion({
latitude: LATITUDE,
longitude: LONGITUDE,
latitudeDelta: LATITUDE_DELTA,
longitudeDelta: LONGITUDE_DELTA,
}),
};
}
onRegionChange(region) {
this.state.region.setValue(region);
}
render() {
return (
<Animated
region={this.state.region}
onRegionChange={this.onRegionChange}
/>
);
}
```
### Animated Marker Position
Markers can also accept an `AnimatedRegion` value as a coordinate.
```jsx
import MapView, { AnimatedRegion, MarkerAnimated } from 'react-native-maps';
getInitialState() {
return {
coordinate: new AnimatedRegion({
latitude: LATITUDE,
longitude: LONGITUDE,
}),
};
}
componentWillReceiveProps(nextProps) {
const duration = 500
if (this.props.coordinate !== nextProps.coordinate) {
if (Platform.OS === 'android') {
if (this.marker) {
this.marker.animateMarkerToCoordinate(
nextProps.coordinate,
duration
);
}
} else {
this.state.coordinate.timing({
...nextProps.coordinate,
useNativeDriver: true, // defaults to false if not passed explicitly
duration
}).start();
}
}
}
render() {
return (
<MapView initialRegion={...}>
<MarkerAnimated
ref={marker => { this.marker = marker }}
coordinate={this.state.coordinate}
/>
</MapView>
);
}
```
### Take Snapshot of map
```jsx
import MapView, { Marker } from 'react-native-maps';
getInitialState() {
return {
coordinate: {
latitude: LATITUDE,
longitude: LONGITUDE,
},
};
}
takeSnapshot () {
// 'takeSnapshot' takes a config object with the
// following options
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
format: 'png', // image formats: 'png', 'jpg' (default: 'png')
quality: 0.8, // image quality: 0..1 (only relevant for jpg, default: 1)
result: 'file' // result types: 'file', 'base64' (default: 'file')
});
snapshot.then((uri) => {
this.setState({ mapSnapshot: uri });
});
}
render() {
return (
<View>
<MapView initialRegion={...} ref={map => { this.map = map }}>
<Marker coordinate={this.state.coordinate} />
</MapView>
<Image source={{ uri: this.state.mapSnapshot.uri }} />
<TouchableOpacity onPress={this.takeSnapshot}>
Take Snapshot
</TouchableOpacity>
</View>
);
}
```
### Zoom to Specified Markers
Pass an array of marker identifiers to have the map re-focus.
![](http://i.giphy.com/3o7qEbOQnO0yoXqKJ2.gif) ![](http://i.giphy.com/l41YdrQZ7m6Dz4h0c.gif)
### Zoom to Specified Coordinates
Pass an array of coordinates to focus a map region on said coordinates.
![](https://cloud.githubusercontent.com/assets/1627824/18609960/da5d9e06-7cdc-11e6-811e-34e255093df9.gif)
### Troubleshooting
#### My map is blank
- Make sure that you have [properly installed](docs/installation.md) react-native-maps.
- Check in the logs if there is more informations about the issue.
- Try setting the style of the MapView to an absolute position with top, left, right and bottom values set.
- Make sure you have enabled Google Maps API in [Google developer console](https://console.developers.google.com/apis/library)
```javascript
const styles = StyleSheet.create({
map: {
...StyleSheet.absoluteFillObject,
},
});
```
```jsx
<MapView
style={styles.map}
// other props
/>
```
#### Inputs don't focus
- When inputs don't focus or elements don't respond to tap, look at the order of the view hierarchy, sometimes the issue could be due to ordering of rendered components, prefer putting MapView as the first component.
Bad:
```jsx
<View>
<TextInput />
<MapView />
</View>
```
Good:
```jsx
<View>
<MapView />
<TextInput />
</View>
```
#### Children Components Not Re-Rendering
Components that aren't declared by this library (Ex: Markers, Polyline) must not be children of the MapView component due to MapView's unique rendering methodology. Have your custom components / views outside the MapView component and position absolute to ensure they only re-render as needed.
Example:
Bad:
```jsx
<View style={StyleSheet.absoluteFillObject}>
<MapView style={StyleSheet.absoluteFillObject}>
<View style={{position: 'absolute', top: 100, left: 50}} />
</MapView>
</View>
```
Good:
```jsx
<View style={StyleSheet.absoluteFillObject}>
<MapView style={StyleSheet.absoluteFillObject} />
<View style={{position: 'absolute', top: 100, left: 50}} />
</View>
```
Source: https://github.com/react-native-maps/react-native-maps/issues/1901
#### Crashing with EXC_BAD_ACCESS on iOS when switching apps
`<MapView>` using Apple Maps in `mapType: "standard"` will sometimes crash when you background the app or switch into another app. This is only an issue in XCode using Metal API Validation, and won't happen in production. To eliminate this problem even while debugging in XCode, go to `Edit Scheme... -> Run (Debug) -> Diagnostics` and uncheck `Metal -> API Validation`. (h/t [@Simon-TechForm](https://github.com/Simon-TechForm)).
Source: https://github.com/react-native-maps/react-native-maps/issues/3957#issuecomment-924161121
#### onRegionChangeComplete() callback is called infinitely
If changing the state in `onRegionChangeComplete` is called infinitely, add a condition to limit these calls to occur only when the region change was done as a result of a user's action.
```javascript
onRegionChangeComplete={ (region, gesture) => {
// This fix only works on Google Maps because isGesture is NOT available on Apple Maps
if (!gesture.isGesture) {
return;
}
// You can use
dispatch({ type: "map_region", payload: { mapRegion: region }}); // if using useReducer
// setMapRegionState(region); // if using useState
}}
```
Source: https://github.com/react-native-maps/react-native-maps/issues/846#issuecomment-1210079461
## License
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.
You may obtain a copy of the License at
https://raw.githubusercontent.com/airbnb/react-native-maps/master/LICENSE
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+70
View File
@@ -0,0 +1,70 @@
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
buildscript {
// The Android Gradle plugin is only required when opening the android folder stand-alone.
// This avoids unnecessary downloads and potential conflicts when the library is included as a
// module dependency in an application project.
if (project == rootProject) {
repositories {
google()
mavenCentral()
}
dependencies {
classpath("com.android.tools.build:gradle:7.6.3")
}
}
}
apply plugin: 'com.android.library'
def isNewArchitectureEnabled() {
return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}
android {
namespace "com.rnmaps.maps"
def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
namespace "com.rnmaps.maps"
}
compileSdk safeExtGet('compileSdkVersion', 34)
defaultConfig {
minSdkVersion safeExtGet('minSdkVersion', 21)
targetSdkVersion safeExtGet('targetSdkVersion', 34)
buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
}
packagingOptions {
excludes = [
"META-INF",
"META-INF/**",
]
}
}
repositories {
mavenLocal()
mavenCentral()
maven {
// All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
url "$rootDir/../node_modules/react-native/android"
}
maven {
// Android JSC is installed from npm
url "$rootDir/../node_modules/jsc-android/dist"
}
google()
}
dependencies {
implementation 'com.facebook.react:react-native:+'
implementation "com.google.android.gms:play-services-base:${safeExtGet('playServicesVersion', '18.2.0')}"
implementation "com.google.android.gms:play-services-maps:${safeExtGet('playServicesVersion', '18.2.0')}"
implementation "com.google.android.gms:play-services-location:21.0.1"
implementation 'com.google.maps.android:android-maps-utils:3.8.2'
implementation "androidx.work:work-runtime:2.7.1"
}
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+252
View File
@@ -0,0 +1,252 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"
+94
View File
@@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,74 @@
package com.rnmaps.maps;
import android.content.Context;
import android.net.Uri;
import android.os.AsyncTask;
import com.facebook.common.logging.FLog;
import com.facebook.react.common.ReactConstants;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
public class FileUtil extends AsyncTask<String, Void, InputStream> {
private Context context;
public FileUtil(Context context) {
super();
this.context = context;
}
protected InputStream doInBackground(String... urls) {
try {
Uri fileContentUri = Uri.parse(urls[0]);
if (fileContentUri.getScheme().startsWith("http")) {
return getDownloadFileInputStream(context, fileContentUri);
}
return context.getContentResolver().openInputStream(fileContentUri);
} catch (Exception e) {
FLog.e(
ReactConstants.TAG,
"Could not retrieve file for contentUri " + urls[0],
e);
return null;
}
}
private InputStream getDownloadFileInputStream(Context context, Uri uri)
throws IOException {
final File outputDir = context.getApplicationContext().getCacheDir();
String NAME = "FileUtil";
String TEMP_FILE_SUFFIX = "temp";
final File file = File.createTempFile(NAME, TEMP_FILE_SUFFIX, outputDir);
file.deleteOnExit();
final URL url = new URL(uri.toString());
final InputStream is = url.openStream();
try {
final ReadableByteChannel channel = Channels.newChannel(is);
try {
final FileOutputStream stream = new FileOutputStream(file);
try {
stream.getChannel().transferFrom(channel, 0, Long.MAX_VALUE);
return new FileInputStream(file);
} finally {
stream.close();
}
} finally {
channel.close();
}
} finally {
is.close();
}
}
}
@@ -0,0 +1,75 @@
package com.rnmaps.maps;
import android.annotation.SuppressLint;
import android.content.Context;
import android.location.Location;
import android.os.Looper;
import com.google.android.gms.location.FusedLocationProviderClient;
import com.google.android.gms.location.LocationCallback;
import com.google.android.gms.location.LocationRequest;
import com.google.android.gms.location.LocationResult;
import com.google.android.gms.location.LocationServices;
import com.google.android.gms.location.Priority;
import com.google.android.gms.maps.LocationSource;
import com.google.android.gms.tasks.OnSuccessListener;
import java.lang.SecurityException;
public class FusedLocationSource implements LocationSource {
private final FusedLocationProviderClient fusedLocationClientProviderClient;
private final LocationRequest locationRequest;
private LocationCallback locationCallback;
public FusedLocationSource(Context context){
fusedLocationClientProviderClient =
LocationServices.getFusedLocationProviderClient(context);
locationRequest = LocationRequest.create();
locationRequest.setPriority(Priority.PRIORITY_HIGH_ACCURACY);
locationRequest.setInterval(5000);
}
public void setPriority(int priority){
locationRequest.setPriority(priority);
}
public void setInterval(int interval){
locationRequest.setInterval(interval);
}
public void setFastestInterval(int fastestInterval){
locationRequest.setFastestInterval(fastestInterval);
}
@SuppressLint("MissingPermission")
@Override
public void activate(final OnLocationChangedListener onLocationChangedListener) {
try {
fusedLocationClientProviderClient.getLastLocation().addOnSuccessListener(new OnSuccessListener<Location>() {
@Override
public void onSuccess(Location location) {
if (location != null) {
onLocationChangedListener.onLocationChanged(location);
}
}
});
locationCallback = new LocationCallback() {
@Override
public void onLocationResult(LocationResult locationResult) {
for (Location location : locationResult.getLocations()) {
onLocationChangedListener.onLocationChanged(location);
}
}
};
fusedLocationClientProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper());
} catch (SecurityException e) {
e.printStackTrace();
}
}
@Override
public void deactivate() {
fusedLocationClientProviderClient.removeLocationUpdates(locationCallback);
}
}
@@ -0,0 +1,15 @@
package com.rnmaps.maps;
import android.graphics.Bitmap;
import com.google.android.gms.maps.model.BitmapDescriptor;
public interface ImageReadable {
public void setIconBitmap(Bitmap bitmap);
public void setIconBitmapDescriptor(BitmapDescriptor bitmapDescriptor);
public void update();
}
@@ -0,0 +1,127 @@
package com.rnmaps.maps;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.Animatable;
import android.net.Uri;
import androidx.annotation.Nullable;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.CloseableStaticBitmap;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
public class ImageReader {
private final ImageReadable imp;
private final Context context;
private final Resources resources;
private final DraweeHolder<?> logoHolder;
private DataSource<CloseableReference<CloseableImage>> dataSource;
private final ControllerListener<ImageInfo> mLogoControllerListener =
new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(
String id,
@Nullable final ImageInfo imageInfo,
@Nullable Animatable animatable) {
CloseableReference<CloseableImage> imageReference = null;
try {
imageReference = dataSource.getResult();
if (imageReference != null) {
CloseableImage image = imageReference.get();
if (image instanceof CloseableStaticBitmap) {
CloseableStaticBitmap closeableStaticBitmap = (CloseableStaticBitmap) image;
Bitmap bitmap = closeableStaticBitmap.getUnderlyingBitmap();
if (bitmap != null) {
bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
imp.setIconBitmap(bitmap);
imp.setIconBitmapDescriptor(BitmapDescriptorFactory.fromBitmap(bitmap));
}
}
}
} finally {
dataSource.close();
if (imageReference != null) {
CloseableReference.closeSafely(imageReference);
}
}
imp.update();
}
};
public ImageReader(Context context, Resources resources, ImageReadable imp) {
this.context = context;
this.resources = resources;
this.imp = imp;
logoHolder = DraweeHolder.create(createDraweeHeirarchy(resources), context);
logoHolder.onAttach();
}
private GenericDraweeHierarchy createDraweeHeirarchy(Resources resources){
return new GenericDraweeHierarchyBuilder(resources)
.setActualImageScaleType(ScalingUtils.ScaleType.FIT_CENTER)
.setFadeDuration(0)
.build();
}
public void setImage(String uri) {
if (uri == null) {
imp.setIconBitmapDescriptor(null);
imp.update();
} else if (uri.startsWith("http://") || uri.startsWith("https://") ||
uri.startsWith("file://") || uri.startsWith("asset://") || uri.startsWith("data:")) {
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 {
BitmapDescriptor iconBitmapDescriptor = getBitmapDescriptorByName(uri);
imp.setIconBitmapDescriptor(iconBitmapDescriptor);
imp.setIconBitmap(BitmapFactory.decodeResource(this.resources, getDrawableResourceByName
(uri)));
imp.update();
}
}
private int getDrawableResourceByName(String name) {
return this.resources.getIdentifier(
name,
"drawable",
this.context.getPackageName());
}
private BitmapDescriptor getBitmapDescriptorByName(String name) {
return BitmapDescriptorFactory.fromResource(getDrawableResourceByName(name));
}
}
@@ -0,0 +1,26 @@
package com.rnmaps.maps;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.util.Base64;
import java.io.ByteArrayOutputStream;
public class ImageUtil {
public static Bitmap convert(String base64Str) throws IllegalArgumentException {
byte[] decodedBytes = Base64.decode(
base64Str.substring(base64Str.indexOf(",") + 1),
Base64.DEFAULT
);
return BitmapFactory.decodeByteArray(decodedBytes, 0, decodedBytes.length);
}
public static String convert(Bitmap bitmap) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream);
return Base64.encodeToString(outputStream.toByteArray(), Base64.DEFAULT);
}
}
@@ -0,0 +1,47 @@
package com.rnmaps.maps;
import com.google.android.gms.maps.model.LatLng;
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;
}
}
@@ -0,0 +1,23 @@
package com.rnmaps.maps;
import android.content.Context;
import com.facebook.react.views.view.ReactViewGroup;
public class MapCallout extends ReactViewGroup {
private boolean tooltip = false;
public int width;
public int height;
public MapCallout(Context context) {
super(context);
}
public void setTooltip(boolean tooltip) {
this.tooltip = tooltip;
}
public boolean getTooltip() {
return this.tooltip;
}
}
@@ -0,0 +1,56 @@
package com.rnmaps.maps;
import androidx.annotation.Nullable;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.Map;
public class MapCalloutManager extends ViewGroupManager<MapCallout> {
@Override
public String getName() {
return "AIRMapCallout";
}
@Override
public MapCallout createViewInstance(ThemedReactContext context) {
return new MapCallout(context);
}
@ReactProp(name = "tooltip", defaultBoolean = false)
public void setTooltip(MapCallout 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(MapCallout view, Object extraData) {
// This method is called from the shadow node with the width/height of the rendered
// marker view.
//noinspection unchecked
Map<String, Float> data = (Map<String, Float>) extraData;
float width = data.get("width");
float height = data.get("height");
view.width = (int) width;
view.height = (int) height;
}
}
@@ -0,0 +1,102 @@
package com.rnmaps.maps;
import android.content.Context;
import com.google.android.gms.maps.model.Circle;
import com.google.android.gms.maps.model.CircleOptions;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.collections.CircleManager;
public class MapCircle extends MapFeature {
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 MapCircle(Context context) {
super(context);
}
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 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 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 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(Object collection) {
CircleManager.Collection circleCollection = (CircleManager.Collection) collection;
circle = circleCollection.addCircle(getCircleOptions());
}
@Override
public void removeFromMap(Object collection) {
CircleManager.Collection circleCollection = (CircleManager.Collection) collection;
circleCollection.remove(circle);
}
}
@@ -0,0 +1,67 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.gms.maps.model.LatLng;
public class MapCircleManager extends ViewGroupManager<MapCircle> {
private final DisplayMetrics metrics;
public MapCircleManager(ReactApplicationContext reactContext) {
super();
metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapCircle";
}
@Override
public MapCircle createViewInstance(ThemedReactContext context) {
return new MapCircle(context);
}
@ReactProp(name = "center")
public void setCenter(MapCircle view, ReadableMap center) {
view.setCenter(new LatLng(center.getDouble("latitude"), center.getDouble("longitude")));
}
@ReactProp(name = "radius", defaultDouble = 0)
public void setRadius(MapCircle view, double radius) {
view.setRadius(radius);
}
@ReactProp(name = "strokeWidth", defaultFloat = 1f)
public void setStrokeWidth(MapCircle 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(MapCircle view, int color) {
view.setFillColor(color);
}
@ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color")
public void setStrokeColor(MapCircle view, int color) {
view.setStrokeColor(color);
}
@ReactProp(name = "zIndex", defaultFloat = 1.0f)
public void setZIndex(MapCircle view, float zIndex) {
view.setZIndex(zIndex);
}
}
@@ -0,0 +1,17 @@
package com.rnmaps.maps;
import android.content.Context;
import com.facebook.react.views.view.ReactViewGroup;
public abstract class MapFeature extends ReactViewGroup {
public MapFeature(Context context) {
super(context);
}
public abstract void addToMap(Object mapOrCollection);
public abstract void removeFromMap(Object mapOrCollection);
public abstract Object getFeature();
}
@@ -0,0 +1,343 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Shader;
import android.util.Log;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.TileProvider;
import com.google.maps.android.SphericalUtil;
import com.google.maps.android.geometry.Point;
import com.google.maps.android.projection.SphericalMercatorProjection;
import java.io.ByteArrayOutputStream;
import java.util.List;
/**
* Tile overlay used to display a colored polyline as a replacement for the
* non-existence of gradient polylines for google maps. Implementation borrowed
* from Dagothig/ColoredPolylineOverlay
* (https://gist.github.com/Dagothig/5f9cf0a4a7a42901a7b2)
*/
public class MapGradientPolyline extends MapFeature {
private List<LatLng> points;
private int[] colors;
private float zIndex;
private float width;
private GoogleMap map;
private TileOverlay tileOverlay;
protected final Context context;
public MapGradientPolyline(Context context) {
super(context);
this.context = context;
}
public void setCoordinates(List<LatLng> coordinates) {
this.points = coordinates;
if (tileOverlay != null) {
tileOverlay.remove();
}
if (map != null) {
tileOverlay = map.addTileOverlay(createTileOverlayOptions());
}
}
public void setStrokeColors(int[] colors) {
this.colors = colors;
if (tileOverlay != null) {
tileOverlay.remove();
}
if (map != null) {
tileOverlay = map.addTileOverlay(createTileOverlayOptions());
}
}
public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (tileOverlay != null) {
tileOverlay.setZIndex(zIndex);
}
}
public void setWidth(float width) {
this.width = width;
if (tileOverlay != null) {
tileOverlay.remove();
}
if (map != null) {
tileOverlay = map.addTileOverlay(createTileOverlayOptions());
}
}
private TileOverlayOptions createTileOverlayOptions() {
TileOverlayOptions options = new TileOverlayOptions();
options.zIndex(zIndex);
AirMapGradientPolylineProvider tileProvider = new AirMapGradientPolylineProvider(context, points, colors, width);
options.tileProvider(tileProvider);
return options;
}
public static int interpolateColor(int[] colors, float proportion) {
int rTotal = 0, gTotal = 0, bTotal = 0;
// We correct the ratio to colors.length - 1 so that
// for i == colors.length - 1 and p == 1, then the final ratio is 1 (see below)
float p = proportion * (colors.length - 1);
for (int i = 0; i < colors.length; i++) {
// The ratio mostly resides on the 1 - Math.abs(p - i) calculation :
// Since for p == i, then the ratio is 1 and for p == i + 1 or p == i -1, then the ratio is 0
// This calculation works BECAUSE p lies within [0, length - 1] and i lies within [0, length - 1] as well
float iRatio = Math.max(1 - Math.abs(p - i), 0.0f);
rTotal += (int) (Color.red(colors[i]) * iRatio);
gTotal += (int) (Color.green(colors[i]) * iRatio);
bTotal += (int) (Color.blue(colors[i]) * iRatio);
}
return Color.rgb(rTotal, gTotal, bTotal);
}
public class AirMapGradientPolylineProvider implements TileProvider {
public static final int BASE_TILE_SIZE = 256;
protected final List<LatLng> points;
protected final int[] colors;
protected final float width;
protected final float density;
protected final int tileDimension;
protected final SphericalMercatorProjection projection;
// Caching calculation-related stuff
protected LatLng[] trailLatLngs;
protected Point[] projectedPts;
protected Point[] projectedPtMids;
public AirMapGradientPolylineProvider(Context context, List<LatLng> points, int[] colors,
float width) {
super();
this.points = points;
this.colors = colors;
this.width = width;
density = context.getResources().getDisplayMetrics().density;
tileDimension = (int) (BASE_TILE_SIZE * density);
projection = new SphericalMercatorProjection(BASE_TILE_SIZE);
calculatePoints();
}
public void calculatePoints() {
trailLatLngs = new LatLng[points.size()];
projectedPts = new Point[points.size()];
projectedPtMids = new Point[Math.max(points.size() - 1, 0)];
for (int i = 0; i < points.size(); i++) {
LatLng latLng = points.get(i);
trailLatLngs[i] = latLng;
projectedPts[i] = projection.toPoint(latLng);
// Mids
if (i > 0) {
LatLng previousLatLng = points.get(i - 1);
LatLng latLngMid = SphericalUtil.interpolate(previousLatLng, latLng, 0.5);
projectedPtMids[i - 1] = projection.toPoint(latLngMid);
}
}
}
@Override
public Tile getTile(int x, int y, int zoom) {
// Because getTile can be called asynchronously by multiple threads, none of the info we keep in the class will be modified
// (getTile is essentially side-effect-less) :
// Instead, we create the bitmap, the canvas and the paints specifically for the call to getTile
Bitmap bitmap = Bitmap.createBitmap(tileDimension, tileDimension, Bitmap.Config.ARGB_8888);
// Normally, instead of the later calls for drawing being offset, we would offset them using scale() and translate() right here
// However, there seems to be funky issues related to float imprecisions that happen at large scales when using this method, so instead
// The points are offset properly when drawing
Canvas canvas = new Canvas(bitmap);
Matrix shaderMat = new Matrix();
Paint gradientPaint = new Paint();
gradientPaint.setStyle(Paint.Style.STROKE);
gradientPaint.setStrokeWidth(width);
gradientPaint.setStrokeCap(Paint.Cap.BUTT);
gradientPaint.setStrokeJoin(Paint.Join.ROUND);
gradientPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
gradientPaint.setShader(new LinearGradient(0, 0, 1, 0, colors, null,
Shader.TileMode.CLAMP));
gradientPaint.getShader().setLocalMatrix(shaderMat);
Paint colorPaint = new Paint();
colorPaint.setStyle(Paint.Style.STROKE);
colorPaint.setStrokeWidth(width);
colorPaint.setStrokeCap(Paint.Cap.BUTT);
colorPaint.setStrokeJoin(Paint.Join.ROUND);
colorPaint.setFlags(Paint.ANTI_ALIAS_FLAG);
// See https://developers.google.com/maps/documentation/android/views#zoom for handy info regarding what zoom is
float scale = (float) (Math.pow(2, zoom) * density);
renderTrail(canvas, shaderMat, gradientPaint, colorPaint, scale, x, y);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
return new Tile(tileDimension, tileDimension, baos.toByteArray());
}
public void renderTrail(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint,
float scale, int x, int y) {
MutPoint pt1 = new MutPoint(), pt2 = new MutPoint(), pt3 = new MutPoint(), pt1mid2 =
new MutPoint(), pt2mid3 = new MutPoint();
if (points.size() == 1) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(colors, 1));
canvas
.drawCircle((float) pt1.x, (float) pt1.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
return;
}
if (points.size() == 2) {
pt1.set(projectedPts[0], scale, x, y, tileDimension);
pt2.set(projectedPts[1], scale, x, y, tileDimension);
drawLine(canvas, colorPaint, pt1, pt2, 0);
return;
}
for (int i = 2; i < points.size(); i++) {
pt1.set(projectedPts[i - 2], scale, x, y, tileDimension);
pt2.set(projectedPts[i - 1], scale, x, y, tileDimension);
pt3.set(projectedPts[i], scale, x, y, tileDimension);
// Because we want to split the lines in two to ease over the corners, we need the middle points
pt1mid2.set(projectedPtMids[i - 2], scale, x, y, tileDimension);
pt2mid3.set(projectedPtMids[i - 1], scale, x, y, tileDimension);
float interp1 = ((float)i - 2) / points.size();
float interp2 = ((float)i - 1) / points.size();
float interp1to2 = (interp1 + interp2) / 2;
Log.d("AirMapGradientPolyline", String.valueOf(interp1to2));
// Circle for the corner (removes the weird empty corners that occur otherwise)
colorPaint.setStyle(Paint.Style.FILL);
colorPaint.setColor(interpolateColor(colors, interp1to2));
canvas
.drawCircle((float) pt2.x, (float) pt2.y, colorPaint.getStrokeWidth() / 2f, colorPaint);
colorPaint.setStyle(Paint.Style.STROKE);
// Corner
// Note that since for the very first point and the very last point we don't split it in two, we used them instead.
drawLine(canvas, shaderMat, gradientPaint, colorPaint, i - 2 == 0 ? pt1 : pt1mid2,
pt2, interp1, interp1to2);
drawLine(canvas, shaderMat, gradientPaint, colorPaint, pt2, i == points.size() - 1 ?
pt3 : pt2mid3, interp1to2, interp2);
}
}
/**
* Note: it is assumed the shader is 0, 0, 1, 0 (horizontal) so that it lines up with the rotation
* (rotations are usually setup so that the angle 0 points right)
*/
public void drawLine(Canvas canvas, Matrix shaderMat, Paint gradientPaint, Paint colorPaint,
MutPoint pt1, MutPoint pt2, float ratio1, float ratio2) {
// Degenerate case: both ratios are the same; we just handle it using the colorPaint (handling it using the shader is just messy and ineffective)
if (ratio1 == ratio2) {
drawLine(canvas, colorPaint, pt1, pt2, ratio1);
return;
}
shaderMat.reset();
// PS: don't ask me why this specfic orders for calls works but other orders will fuck up
// Since every call is pre, this is essentially ordered as (or my understanding is that it is):
// ratio translate -> ratio scale -> scale to pt length -> translate to pt start -> rotate
// (my initial intuition was to use only post calls and to order as above, but it resulted in odd corruptions)
// Setup based on points:
// We translate the shader so that it is based on the first point, rotated towards the second and since the length of the
// gradient is 1, then scaling to the length of the distance between the points makes it exactly as long as needed
shaderMat.preRotate((float) Math.toDegrees(Math.atan2(pt2.y - pt1.y, pt2.x - pt1.x)),
(float) pt1.x, (float) pt1.y);
shaderMat.preTranslate((float) pt1.x, (float) pt1.y);
float scale = (float) Math.sqrt(Math.pow(pt2.x - pt1.x, 2) + Math.pow(pt2.y - pt1.y, 2));
shaderMat.preScale(scale, scale);
// Setup based on ratio
// By basing the shader to the first ratio, we ensure that the start of the gradient corresponds to it
// The inverse scaling of the shader means that it takes the full length of the call to go to the second ratio
// For instance; if d(ratio1, ratio2) is 0.5, then the shader needs to be twice as long so that an entire call (1)
// Results in only half of the gradient being used
shaderMat.preScale(1f / (ratio2 - ratio1), 1f / (ratio2 - ratio1));
shaderMat.preTranslate(-ratio1, 0);
gradientPaint.getShader().setLocalMatrix(shaderMat);
canvas.drawLine(
(float) pt1.x,
(float) pt1.y,
(float) pt2.x,
(float) pt2.y,
gradientPaint
);
}
public void drawLine(Canvas canvas, Paint colorPaint, MutPoint pt1, MutPoint pt2, float ratio) {
colorPaint.setColor(interpolateColor(colors, ratio));
canvas.drawLine(
(float) pt1.x,
(float) pt1.y,
(float) pt2.x,
(float) pt2.y,
colorPaint
);
}
}
@Override
public Object getFeature() {
return tileOverlay;
}
@Override
public void addToMap(Object map) {
this.map = (GoogleMap) map;
this.tileOverlay = this.map.addTileOverlay(createTileOverlayOptions());
}
@Override
public void removeFromMap(Object map) {
tileOverlay.remove();
}
public static class MutPoint {
public double x, y;
public MutPoint set(Point point, float scale, int x, int y, int tileDimension) {
this.x = point.x * scale - x * tileDimension;
this.y = point.y * scale - y * tileDimension;
return this;
}
}
}
@@ -0,0 +1,83 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.gms.maps.model.LatLng;
import java.util.List;
import java.util.ArrayList;
public class MapGradientPolylineManager extends ViewGroupManager<MapGradientPolyline> {
private final DisplayMetrics metrics;
public MapGradientPolylineManager(ReactApplicationContext reactContext) {
super();
metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapGradientPolyline";
}
@Override
public MapGradientPolyline createViewInstance(ThemedReactContext context) {
return new MapGradientPolyline(context);
}
@ReactProp(name = "coordinates")
public void setCoordinates(MapGradientPolyline view, ReadableArray coordinates) {
List<LatLng> p = new ArrayList<LatLng>();
for (int i = 0; i < coordinates.size(); i++) {
ReadableMap point = coordinates.getMap(i);
LatLng latLng = new LatLng(point.getDouble("latitude"), point.getDouble("longitude"));
p.add(latLng);
}
view.setCoordinates(p);
}
@ReactProp(name = "strokeColors", customType = "ColorArray")
public void setStrokeColors(MapGradientPolyline view, ReadableArray colors) {
if (colors != null) {
if (colors.size() == 0) {
int[] colorValues = {0,0};
view.setStrokeColors(colorValues);
} else if (colors.size() == 1) {
int[] colorValues = { colors.getInt(0), colors.getInt(0) };
view.setStrokeColors(colorValues);
} else {
int[] colorValues = new int[colors.size()];
for (int i = 0; i < colors.size(); i++) {
colorValues[i] = colors.getInt(i);
}
view.setStrokeColors(colorValues);
}
} else {
int[] colorValues = {0,0};
view.setStrokeColors(colorValues);
}
}
@ReactProp(name = "zIndex", defaultFloat = 1.0f)
public void setZIndex(MapGradientPolyline view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "strokeWidth", defaultFloat = 1f)
public void setStrokeWidth(MapGradientPolyline view, float widthInPoints) {
float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS
view.setWidth(widthInScreenPx);
}
}
@@ -0,0 +1,113 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.Log;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.maps.android.heatmaps.HeatmapTileProvider;
import com.google.maps.android.heatmaps.WeightedLatLng;
import com.google.maps.android.heatmaps.Gradient;
import java.util.Arrays;
import java.util.List;
public class MapHeatmap extends MapFeature {
private TileOverlayOptions heatmapOptions;
private TileOverlay heatmap;
private HeatmapTileProvider heatmapTileProvider;
private List<WeightedLatLng> points;
private Gradient gradient;
private Double opacity;
private Integer radius;
public MapHeatmap(Context context) {
super(context);
}
public void setPoints(WeightedLatLng[] points) {
this.points = Arrays.asList(points);
if (heatmapTileProvider != null) {
heatmapTileProvider.setWeightedData(this.points);
}
if (heatmap != null) {
heatmap.clearTileCache();
}
}
public void setGradient(Gradient gradient) {
this.gradient = gradient;
if (heatmapTileProvider != null) {
heatmapTileProvider.setGradient(gradient);
}
if (heatmap != null) {
heatmap.clearTileCache();
}
}
public void setOpacity(double opacity) {
this.opacity = opacity;
if (heatmapTileProvider != null) {
heatmapTileProvider.setOpacity(opacity);
}
if (heatmap != null) {
heatmap.clearTileCache();
}
}
public void setRadius(int radius) {
this.radius = radius;
if (heatmapTileProvider != null) {
heatmapTileProvider.setRadius(radius);
}
if (heatmap != null) {
heatmap.clearTileCache();
}
}
public TileOverlayOptions getHeatmapOptions() {
if (heatmapOptions == null) {
heatmapOptions = createHeatmapOptions();
}
return heatmapOptions;
}
private TileOverlayOptions createHeatmapOptions() {
TileOverlayOptions options = new TileOverlayOptions();
if (heatmapTileProvider == null) {
HeatmapTileProvider.Builder builder =
new HeatmapTileProvider.Builder().weightedData(this.points);
if (radius != null) {
builder.radius(radius);
}
if (opacity != null) {
builder.opacity(opacity);
}
if (gradient != null) {
builder.gradient(gradient);
}
heatmapTileProvider = builder.build();
}
options.tileProvider(heatmapTileProvider);
return options;
}
@Override
public Object getFeature() {
return heatmap;
}
@Override
public void addToMap(Object map) {
heatmap = ((GoogleMap) map).addTileOverlay(getHeatmapOptions());
}
@Override
public void removeFromMap(Object map) {
heatmap.remove();
}
}
@@ -0,0 +1,74 @@
package com.rnmaps.maps;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.google.android.gms.maps.model.LatLng;
import com.google.maps.android.heatmaps.WeightedLatLng;
import com.google.maps.android.heatmaps.Gradient;
public class MapHeatmapManager extends ViewGroupManager<MapHeatmap> {
@Override
public String getName() {
return "AIRMapHeatmap";
}
@Override
public MapHeatmap createViewInstance(ThemedReactContext context) {
return new MapHeatmap(context);
}
@ReactProp(name = "points")
public void setPoints(MapHeatmap view, ReadableArray points) {
WeightedLatLng[] p = new WeightedLatLng[points.size()];
for (int i = 0; i < points.size(); i++) {
ReadableMap point = points.getMap(i);
WeightedLatLng weightedLatLng;
LatLng latLng = new LatLng(point.getDouble("latitude"), point.getDouble("longitude"));
if (point.hasKey("weight")) {
weightedLatLng = new WeightedLatLng(latLng, point.getDouble("weight"));
} else {
weightedLatLng = new WeightedLatLng(latLng);
}
p[i] = weightedLatLng;
}
view.setPoints(p);
}
@ReactProp(name = "gradient")
public void setGradient(MapHeatmap view, ReadableMap gradient) {
ReadableArray srcColors = gradient.getArray("colors");
int[] colors = new int[srcColors.size()];
for (int i = 0; i < srcColors.size(); i++) {
colors[i] = srcColors.getInt(i);
}
ReadableArray srcStartPoints = gradient.getArray("startPoints");
float[] startPoints = new float[srcStartPoints.size()];
for (int i = 0; i < srcStartPoints.size(); i++) {
startPoints[i] = (float)srcStartPoints.getDouble(i);
}
if (gradient.hasKey("colorMapSize")) {
int colorMapSize = gradient.getInt("colorMapSize");
view.setGradient(new Gradient(colors, startPoints, colorMapSize));
} else {
view.setGradient(new Gradient(colors, startPoints));
}
}
@ReactProp(name = "opacity")
public void setOpacity(MapHeatmap view, double opacity) {
view.setOpacity(opacity);
}
@ReactProp(name = "radius")
public void setRadius(MapHeatmap view, int radius) {
view.setRadius(radius);
}
}
@@ -0,0 +1,151 @@
package com.rnmaps.maps;
import android.content.Context;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.TileProvider;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class MapLocalTile extends MapFeature {
class AIRMapLocalTileProvider implements TileProvider {
private static final int BUFFER_SIZE = 16 * 1024;
private int tileSize;
private String pathTemplate;
private final boolean useAssets;
public AIRMapLocalTileProvider(int tileSizet, String pathTemplate, boolean useAssets) {
this.tileSize = tileSizet;
this.pathTemplate = pathTemplate;
this.useAssets = useAssets;
}
@Override
public Tile getTile(int x, int y, int zoom) {
byte[] image = readTileImage(x, y, zoom);
return image == null ? TileProvider.NO_TILE : new Tile(this.tileSize, this.tileSize, image);
}
public void setPathTemplate(String pathTemplate) {
this.pathTemplate = pathTemplate;
}
public void setTileSize(int tileSize) {
this.tileSize = tileSize;
}
private byte[] readTileImage(int x, int y, int zoom) {
InputStream in = null;
ByteArrayOutputStream buffer = null;
String tileFilename = getTileFilename(x, y, zoom);
try {
in = useAssets ? getContext().getAssets().open(tileFilename) : new FileInputStream(tileFilename);
buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[BUFFER_SIZE];
while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return null;
} finally {
if (in != null) try { in.close(); } catch (Exception ignored) {}
if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
}
}
private String getTileFilename(int x, int y, int zoom) {
String s = this.pathTemplate
.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoom));
return s;
}
}
private TileOverlayOptions tileOverlayOptions;
private TileOverlay tileOverlay;
private MapLocalTile.AIRMapLocalTileProvider tileProvider;
private String pathTemplate;
private float tileSize;
private float zIndex;
private boolean useAssets;
public MapLocalTile(Context context) {
super(context);
}
public void setPathTemplate(String pathTemplate) {
this.pathTemplate = pathTemplate;
if (tileProvider != null) {
tileProvider.setPathTemplate(pathTemplate);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (tileOverlay != null) {
tileOverlay.setZIndex(zIndex);
}
}
public void setTileSize(float tileSize) {
this.tileSize = tileSize;
if (tileProvider != null) {
tileProvider.setTileSize((int)tileSize);
}
}
public void setUseAssets(boolean useAssets) {
this.useAssets = useAssets;
}
public TileOverlayOptions getTileOverlayOptions() {
if (tileOverlayOptions == null) {
tileOverlayOptions = createTileOverlayOptions();
}
return tileOverlayOptions;
}
private TileOverlayOptions createTileOverlayOptions() {
TileOverlayOptions options = new TileOverlayOptions();
options.zIndex(zIndex);
this.tileProvider = new MapLocalTile.AIRMapLocalTileProvider((int)this.tileSize, this.pathTemplate, this.useAssets);
options.tileProvider(this.tileProvider);
return options;
}
@Override
public Object getFeature() {
return tileOverlay;
}
@Override
public void addToMap(Object map) {
this.tileOverlay = ((GoogleMap) map).addTileOverlay(getTileOverlayOptions());
}
@Override
public void removeFromMap(Object map) {
tileOverlay.remove();
}
}
@@ -0,0 +1,54 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
/**
* Created by zavadpe on 30/11/2017.
*/
public class MapLocalTileManager extends ViewGroupManager<MapLocalTile> {
public MapLocalTileManager(ReactApplicationContext reactContext) {
super();
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapLocalTile";
}
@Override
public MapLocalTile createViewInstance(ThemedReactContext context) {
return new MapLocalTile(context);
}
@ReactProp(name = "pathTemplate")
public void setPathTemplate(MapLocalTile view, String pathTemplate) {
view.setPathTemplate(pathTemplate);
}
@ReactProp(name = "tileSize", defaultFloat = 256f)
public void setTileSize(MapLocalTile view, float tileSize) {
view.setTileSize(tileSize);
}
@ReactProp(name = "zIndex", defaultFloat = -1.0f)
public void setZIndex(MapLocalTile view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "useAssets", defaultBoolean = false)
public void setUseAssets(MapLocalTile view, boolean useAssets) {
view.setUseAssets(useAssets);
}
}
@@ -0,0 +1,504 @@
package com.rnmaps.maps;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.R;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ReactStylesDiffMap;
import com.facebook.react.uimanager.StateWrapper;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.android.gms.location.Priority;
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.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import java.util.Map;
public class MapManager extends ViewGroupManager<MapView> {
private static final String REACT_CLASS = "AIRMap";
private final Map<String, Integer> 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 Map<String, Integer> MY_LOCATION_PRIORITY = MapBuilder.of(
"balanced", Priority.PRIORITY_BALANCED_POWER_ACCURACY,
"high", Priority.PRIORITY_HIGH_ACCURACY,
"low", Priority.PRIORITY_LOW_POWER,
"passive", Priority.PRIORITY_PASSIVE
);
private final ReactApplicationContext appContext;
private MapMarkerManager markerManager;
protected GoogleMapOptions googleMapOptions;
protected MapsInitializer.Renderer renderer;
public MapManager(ReactApplicationContext context) {
this.appContext = context;
}
public MapMarkerManager getMarkerManager() {
return this.markerManager;
}
public void setMarkerManager(MapMarkerManager markerManager) {
this.markerManager = markerManager;
}
@Override
public String getName() {
return REACT_CLASS;
}
@Override
protected MapView createViewInstance(@NonNull ThemedReactContext context) {
return new MapView(context, this.appContext, this, googleMapOptions);
}
@Override
protected MapView createViewInstance(int reactTag, @NonNull ThemedReactContext reactContext, @Nullable ReactStylesDiffMap initialProps, @Nullable StateWrapper stateWrapper) {
this.googleMapOptions = new GoogleMapOptions();
if (initialProps != null) {
if (initialProps.getString("googleMapId") != null) {
googleMapOptions.mapId(initialProps.getString("googleMapId"));
}
if (initialProps.hasKey("liteMode")) {
googleMapOptions.liteMode(initialProps.getBoolean("liteMode", false));
}
if (initialProps.hasKey("initialCamera")) {
CameraPosition position = MapView.cameraPositionFromMap(initialProps.getMap("initialCamera"));
if (position != null) {
googleMapOptions.camera(position);
}
} else if (initialProps.hasKey("camera")) {
CameraPosition position = MapView.cameraPositionFromMap(initialProps.getMap("camera"));
if (position != null) {
googleMapOptions.camera(position);
}
}
if (initialProps.hasKey("googleRenderer") && "LEGACY".equals(initialProps.getString("googleRenderer"))) {
renderer = MapsInitializer.Renderer.LEGACY;
} else {
renderer = MapsInitializer.Renderer.LATEST;
}
}
return super.createViewInstance(reactTag, reactContext, initialProps, stateWrapper);
}
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(MapView view, ReadableMap region) {
view.setRegion(region);
}
@ReactProp(name = "googleRenderer")
public void setGoogleRenderer(MapView view, @Nullable String googleRenderer) {
// do nothing, passed as part of the InitialProps
}
@ReactProp(name = "liteMode", defaultBoolean = false)
public void setLiteMode(MapView view, boolean liteMode) {
googleMapOptions.liteMode(liteMode);
}
@ReactProp(name = "googleMapId")
public void setGoogleMapId(MapView view, @Nullable String googleMapId) {
if (googleMapId != null) {
googleMapOptions.mapId(googleMapId);
}
}
@ReactProp(name = "initialRegion")
public void setInitialRegion(MapView view, ReadableMap initialRegion) {
view.setInitialRegion(initialRegion);
}
@ReactProp(name = "camera")
public void setCamera(MapView view, ReadableMap camera) {
view.setCamera(camera);
}
@ReactProp(name = "initialCamera")
public void setInitialCamera(MapView view, ReadableMap initialCamera) {
view.setInitialCamera(initialCamera);
}
@ReactProp(name = "mapType")
public void setMapType(MapView view, @Nullable String mapType) {
int typeId = MAP_TYPES.get(mapType);
view.map.setMapType(typeId);
}
@ReactProp(name = "customMapStyleString")
public void setMapStyle(MapView view, @Nullable String customMapStyleString) {
view.setMapStyle(customMapStyleString);
}
@ReactProp(name = "mapPadding")
public void setMapPadding(MapView view, @Nullable ReadableMap padding) {
int left = 0;
int top = 0;
int right = 0;
int bottom = 0;
double density = (double) view.getResources().getDisplayMetrics().density;
if (padding != null) {
if (padding.hasKey("left")) {
left = (int) (padding.getDouble("left") * density);
}
if (padding.hasKey("top")) {
top = (int) (padding.getDouble("top") * density);
}
if (padding.hasKey("right")) {
right = (int) (padding.getDouble("right") * density);
}
if (padding.hasKey("bottom")) {
bottom = (int) (padding.getDouble("bottom") * density);
}
}
view.applyBaseMapPadding(left, top, right, bottom);
view.map.setPadding(left, top, right, bottom);
}
@ReactProp(name = "showsUserLocation", defaultBoolean = false)
public void setShowsUserLocation(MapView view, boolean showUserLocation) {
view.setShowsUserLocation(showUserLocation);
}
@ReactProp(name = "userLocationPriority")
public void setUserLocationPriority(MapView view, @Nullable String accuracy) {
view.setUserLocationPriority(MY_LOCATION_PRIORITY.get(accuracy));
}
@ReactProp(name = "userLocationUpdateInterval", defaultInt = 5000)
public void setUserLocationUpdateInterval(MapView view, int updateInterval) {
view.setUserLocationUpdateInterval(updateInterval);
}
@ReactProp(name = "userLocationFastestInterval", defaultInt = 5000)
public void setUserLocationFastestInterval(MapView view, int fastestInterval) {
view.setUserLocationFastestInterval(fastestInterval);
}
@ReactProp(name = "showsMyLocationButton", defaultBoolean = true)
public void setShowsMyLocationButton(MapView view, boolean showMyLocationButton) {
view.setShowsMyLocationButton(showMyLocationButton);
}
@ReactProp(name = "toolbarEnabled", defaultBoolean = true)
public void setToolbarEnabled(MapView 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(MapView view, boolean handlePanDrag) {
view.setHandlePanDrag(handlePanDrag);
}
@ReactProp(name = "showsTraffic", defaultBoolean = false)
public void setShowTraffic(MapView view, boolean showTraffic) {
view.map.setTrafficEnabled(showTraffic);
}
@ReactProp(name = "showsBuildings", defaultBoolean = false)
public void setShowBuildings(MapView view, boolean showBuildings) {
view.map.setBuildingsEnabled(showBuildings);
}
@ReactProp(name = "showsIndoors", defaultBoolean = false)
public void setShowIndoors(MapView view, boolean showIndoors) {
view.map.setIndoorEnabled(showIndoors);
}
@ReactProp(name = "showsIndoorLevelPicker", defaultBoolean = false)
public void setShowsIndoorLevelPicker(MapView view, boolean showsIndoorLevelPicker) {
view.map.getUiSettings().setIndoorLevelPickerEnabled(showsIndoorLevelPicker);
}
@ReactProp(name = "showsCompass", defaultBoolean = false)
public void setShowsCompass(MapView view, boolean showsCompass) {
view.map.getUiSettings().setCompassEnabled(showsCompass);
}
@ReactProp(name = "scrollEnabled", defaultBoolean = false)
public void setScrollEnabled(MapView view, boolean scrollEnabled) {
view.map.getUiSettings().setScrollGesturesEnabled(scrollEnabled);
}
@ReactProp(name = "zoomEnabled", defaultBoolean = false)
public void setZoomEnabled(MapView view, boolean zoomEnabled) {
view.map.getUiSettings().setZoomGesturesEnabled(zoomEnabled);
}
@ReactProp(name = "zoomControlEnabled", defaultBoolean = true)
public void setZoomControlEnabled(MapView view, boolean zoomControlEnabled) {
view.map.getUiSettings().setZoomControlsEnabled(zoomControlEnabled);
}
@ReactProp(name = "rotateEnabled", defaultBoolean = false)
public void setRotateEnabled(MapView view, boolean rotateEnabled) {
view.map.getUiSettings().setRotateGesturesEnabled(rotateEnabled);
}
@ReactProp(name = "scrollDuringRotateOrZoomEnabled", defaultBoolean = true)
public void setScrollDuringRotateOrZoomEnabled(MapView view, boolean scrollDuringRotateOrZoomEnabled) {
view.map.getUiSettings().setScrollGesturesEnabledDuringRotateOrZoom(scrollDuringRotateOrZoomEnabled);
}
@ReactProp(name = "cacheEnabled", defaultBoolean = false)
public void setCacheEnabled(MapView view, boolean cacheEnabled) {
view.setCacheEnabled(cacheEnabled);
}
@ReactProp(name = "poiClickEnabled", defaultBoolean = true)
public void setPoiClickEnabled(MapView view, boolean poiClickEnabled) {
view.setPoiClickEnabled(poiClickEnabled);
}
@ReactProp(name = "loadingEnabled", defaultBoolean = false)
public void setLoadingEnabled(MapView view, boolean loadingEnabled) {
view.enableMapLoading(loadingEnabled);
}
@ReactProp(name = "moveOnMarkerPress", defaultBoolean = true)
public void setMoveOnMarkerPress(MapView view, boolean moveOnPress) {
view.setMoveOnMarkerPress(moveOnPress);
}
@ReactProp(name = "loadingBackgroundColor", customType = "Color")
public void setLoadingBackgroundColor(MapView view, @Nullable Integer loadingBackgroundColor) {
view.setLoadingBackgroundColor(loadingBackgroundColor);
}
@ReactProp(name = "loadingIndicatorColor", customType = "Color")
public void setLoadingIndicatorColor(MapView view, @Nullable Integer loadingIndicatorColor) {
view.setLoadingIndicatorColor(loadingIndicatorColor);
}
@ReactProp(name = "pitchEnabled", defaultBoolean = false)
public void setPitchEnabled(MapView view, boolean pitchEnabled) {
view.map.getUiSettings().setTiltGesturesEnabled(pitchEnabled);
}
@ReactProp(name = "minZoomLevel")
public void setMinZoomLevel(MapView view, float minZoomLevel) {
view.map.setMinZoomPreference(minZoomLevel);
}
@ReactProp(name = "maxZoomLevel")
public void setMaxZoomLevel(MapView view, float maxZoomLevel) {
view.map.setMaxZoomPreference(maxZoomLevel);
}
@ReactProp(name = "kmlSrc")
public void setKmlSrc(MapView view, String kmlUrl) {
if (kmlUrl != null) {
view.setKmlSrc(kmlUrl);
}
}
@ReactProp(name = "accessibilityLabel")
public void setAccessibilityLabel(MapView view, @Nullable String accessibilityLabel) {
view.setTag(R.id.accessibility_label, accessibilityLabel);
}
@Override
public void receiveCommand(@NonNull MapView view, String commandId, @Nullable ReadableArray args) {
int duration;
double lat;
double lng;
double lngDelta;
double latDelta;
ReadableMap region;
ReadableMap camera;
switch (commandId) {
case "setCamera":
if (args == null) {
break;
}
camera = args.getMap(0);
view.animateToCamera(camera, 0);
break;
case "animateCamera":
if (args == null) {
break;
}
camera = args.getMap(0);
duration = args.getInt(1);
view.animateToCamera(camera, duration);
break;
case "animateToRegion":
if (args == null) {
break;
}
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 "fitToElements":
if (args == null) {
break;
}
view.fitToElements(args.getMap(0), args.getBoolean(1));
break;
case "fitToSuppliedMarkers":
if (args == null) {
break;
}
view.fitToSuppliedMarkers(args.getArray(0), args.getMap(1), args.getBoolean(2));
break;
case "fitToCoordinates":
if (args == null) {
break;
}
view.fitToCoordinates(args.getArray(0), args.getMap(1), args.getBoolean(2));
break;
case "setMapBoundaries":
if (args == null) {
break;
}
view.setMapBoundaries(args.getMap(0), args.getMap(1));
break;
case "setIndoorActiveLevelIndex":
if (args == null) {
break;
}
view.setIndoorActiveLevelIndex(args.getInt(0));
break;
}
}
@Override
@Nullable
public Map getExportedCustomDirectEventTypeConstants() {
Map<String, Map<String, String>> map = MapBuilder.of(
"onMapReady", MapBuilder.of("registrationName", "onMapReady"),
"onPress", MapBuilder.of("registrationName", "onPress"),
"onLongPress", MapBuilder.of("registrationName", "onLongPress"),
"onMarkerPress", MapBuilder.of("registrationName", "onMarkerPress"),
"onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress")
);
map.putAll(MapBuilder.of(
"onUserLocationChange", MapBuilder.of("registrationName", "onUserLocationChange"),
"onMarkerDragStart", MapBuilder.of("registrationName", "onMarkerDragStart"),
"onMarkerDrag", MapBuilder.of("registrationName", "onMarkerDrag"),
"onMarkerDragEnd", MapBuilder.of("registrationName", "onMarkerDragEnd"),
"onPanDrag", MapBuilder.of("registrationName", "onPanDrag"),
"onKmlReady", MapBuilder.of("registrationName", "onKmlReady"),
"onPoiClick", MapBuilder.of("registrationName", "onPoiClick")
));
map.putAll(MapBuilder.of(
"onIndoorLevelActivated", MapBuilder.of("registrationName", "onIndoorLevelActivated"),
"onIndoorBuildingFocused", MapBuilder.of("registrationName", "onIndoorBuildingFocused"),
"onDoublePress", MapBuilder.of("registrationName", "onDoublePress"),
"onMapLoaded", MapBuilder.of("registrationName", "onMapLoaded"),
"onMarkerSelect", MapBuilder.of("registrationName", "onMarkerSelect"),
"onMarkerDeselect", MapBuilder.of("registrationName", "onMarkerDeselect"),
"onRegionChangeStart", MapBuilder.of("registrationName", "onRegionChangeStart")
));
return map;
}
@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(MapView parent, View child, int index) {
parent.addFeature(child, index);
}
@Override
public int getChildCount(MapView view) {
return view.getFeatureCount();
}
@Override
public View getChildAt(MapView view, int index) {
return view.getFeatureAt(index);
}
@Override
public void removeViewAt(MapView parent, int index) {
parent.removeFeatureAt(index);
}
@Override
public void updateExtraData(MapView view, Object extraData) {
view.updateExtraData(extraData);
}
void pushEvent(ThemedReactContext context, View view, String name, WritableMap data) {
context
.getReactApplicationContext()
.getJSModule(RCTEventEmitter.class)
.receiveEvent(view.getId(), name, data);
}
@Override
public void onDropViewInstance(MapView view) {
view.doDestroy();
super.onDropViewInstance(view);
}
}
@@ -0,0 +1,625 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Animatable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.view.View;
import android.widget.LinearLayout;
import android.animation.ObjectAnimator;
import android.util.Property;
import android.animation.TypeEvaluator;
import androidx.annotation.Nullable;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
import com.facebook.drawee.backends.pipeline.Fresco;
import com.facebook.drawee.controller.BaseControllerListener;
import com.facebook.drawee.controller.ControllerListener;
import com.facebook.drawee.drawable.ScalingUtils;
import com.facebook.drawee.generic.GenericDraweeHierarchy;
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
import com.facebook.drawee.interfaces.DraweeController;
import com.facebook.drawee.view.DraweeHolder;
import com.facebook.imagepipeline.core.ImagePipeline;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.image.CloseableStaticBitmap;
import com.facebook.imagepipeline.image.ImageInfo;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import com.facebook.react.bridge.ReadableMap;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.MarkerOptions;
import com.google.maps.android.collections.MarkerManager;
public class MapMarker extends MapFeature {
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 MapCallout 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 tracksViewChanges = true;
private boolean tracksViewChangesActive = false;
private boolean hasCustomMarkerView = false;
private final MapMarkerManager markerManager;
private String imageUri;
private final DraweeHolder<?> logoHolder;
private DataSource<CloseableReference<CloseableImage>> dataSource;
private final ControllerListener<ImageInfo> mLogoControllerListener =
new BaseControllerListener<ImageInfo>() {
@Override
public void onFinalImageSet(
String id,
@Nullable final ImageInfo imageInfo,
@Nullable Animatable animatable) {
CloseableReference<CloseableImage> imageReference = null;
try {
imageReference = dataSource.getResult();
if (imageReference != null) {
CloseableImage image = imageReference.get();
if (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);
}
}
if (MapMarker.this.markerManager != null && MapMarker.this.imageUri != null) {
MapMarker.this.markerManager.getSharedIcon(MapMarker.this.imageUri)
.updateIcon(iconBitmapDescriptor, iconBitmap);
}
update(true);
}
};
public MapMarker(Context context, MapMarkerManager markerManager) {
super(context);
this.context = context;
this.markerManager = markerManager;
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
logoHolder.onAttach();
}
public MapMarker(Context context, MarkerOptions options, MapMarkerManager markerManager) {
super(context);
this.context = context;
this.markerManager = markerManager;
logoHolder = DraweeHolder.create(createDraweeHierarchy(), context);
logoHolder.onAttach();
position = options.getPosition();
setAnchor(options.getAnchorU(), options.getAnchorV());
setCalloutAnchor(options.getInfoWindowAnchorU(), options.getInfoWindowAnchorV());
setTitle(options.getTitle());
setSnippet(options.getSnippet());
setRotation(options.getRotation());
setFlat(options.isFlat());
setDraggable(options.isDraggable());
setZIndex(Math.round(options.getZIndex()));
setAlpha(options.getAlpha());
iconBitmapDescriptor = options.getIcon();
}
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(false);
}
public void setIdentifier(String identifier) {
this.identifier = identifier;
update(false);
}
public String getIdentifier() {
return this.identifier;
}
public void setTitle(String title) {
this.title = title;
if (marker != null) {
marker.setTitle(title);
}
update(false);
}
public void setSnippet(String snippet) {
this.snippet = snippet;
if (marker != null) {
marker.setSnippet(snippet);
}
update(false);
}
public void setRotation(float rotation) {
this.rotation = rotation;
if (marker != null) {
marker.setRotation(rotation);
}
update(false);
}
public void setFlat(boolean flat) {
this.flat = flat;
if (marker != null) {
marker.setFlat(flat);
}
update(false);
}
public void setDraggable(boolean draggable) {
this.draggable = draggable;
if (marker != null) {
marker.setDraggable(draggable);
}
update(false);
}
public void setZIndex(int zIndex) {
this.zIndex = zIndex;
if (marker != null) {
marker.setZIndex(zIndex);
}
update(false);
}
public void setOpacity(float opacity) {
this.opacity = opacity;
if (marker != null) {
marker.setAlpha(opacity);
}
update(false);
}
public void setMarkerHue(float markerHue) {
this.markerHue = markerHue;
update(false);
}
public void setAnchor(double x, double y) {
anchorIsSet = true;
anchorX = (float) x;
anchorY = (float) y;
if (marker != null) {
marker.setAnchor(anchorX, anchorY);
}
update(false);
}
public void setCalloutAnchor(double x, double y) {
calloutAnchorIsSet = true;
calloutAnchorX = (float) x;
calloutAnchorY = (float) y;
if (marker != null) {
marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY);
}
update(false);
}
public void setTracksViewChanges(boolean tracksViewChanges) {
this.tracksViewChanges = tracksViewChanges;
updateTracksViewChanges();
}
private void updateTracksViewChanges() {
boolean shouldTrack = tracksViewChanges && hasCustomMarkerView && marker != null;
if (shouldTrack == tracksViewChangesActive) return;
tracksViewChangesActive = shouldTrack;
if (shouldTrack) {
ViewChangesTracker.getInstance().addMarker(this);
} else {
ViewChangesTracker.getInstance().removeMarker(this);
// Let it render one more time to avoid race conditions.
// i.e. Image onLoad ->
// ViewChangesTracker may not get a chance to render ->
// setState({ tracksViewChanges: false }) ->
// image loaded but not rendered.
updateMarkerIcon();
}
}
public LatLng getPosition() {
return position;
}
public boolean updateCustomForTracking() {
if (!tracksViewChangesActive)
return false;
updateMarkerIcon();
return true;
}
public void updateMarkerIcon() {
if (marker == null) return;
marker.setIcon(getIcon());
}
public LatLng interpolate(float fraction, LatLng a, LatLng b) {
double lat = (b.latitude - a.latitude) * fraction + a.latitude;
double lng = (b.longitude - a.longitude) * fraction + a.longitude;
return new LatLng(lat, lng);
}
public void animateToCoodinate(LatLng finalPosition, Integer duration) {
TypeEvaluator<LatLng> typeEvaluator = new TypeEvaluator<LatLng>() {
@Override
public LatLng evaluate(float fraction, LatLng startValue, LatLng endValue) {
return interpolate(fraction, startValue, endValue);
}
};
Property<Marker, LatLng> property = Property.of(Marker.class, LatLng.class, "position");
ObjectAnimator animator = ObjectAnimator.ofObject(
marker,
property,
typeEvaluator,
finalPosition);
animator.setDuration(duration);
animator.start();
}
public void setImage(String uri) {
boolean shouldLoadImage = true;
if (this.markerManager != null) {
// remove marker from previous shared icon if needed, to avoid future updates from it.
// remove the shared icon completely if no markers on it as well.
// this is to avoid memory leak due to orphan bitmaps.
//
// However in case where client want to update all markers from icon A to icon B
// and after some time to update back from icon B to icon A
// it may be better to keep it though. We assume that is rare.
if (this.imageUri != null) {
this.markerManager.getSharedIcon(this.imageUri).removeMarker(this);
this.markerManager.removeSharedIconIfEmpty(this.imageUri);
}
if (uri != null) {
// listening for marker bitmap descriptor update, as well as check whether to load the image.
MapMarkerManager.AirMapMarkerSharedIcon sharedIcon = this.markerManager.getSharedIcon(uri);
sharedIcon.addMarker(this);
shouldLoadImage = sharedIcon.shouldLoadImage();
}
}
this.imageUri = uri;
if (!shouldLoadImage) {return;}
if (uri == null) {
iconBitmapDescriptor = null;
update(true);
} else if (uri.startsWith("http://") || uri.startsWith("https://") ||
uri.startsWith("file://") || uri.startsWith("asset://") || uri.startsWith("data:")) {
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);
int drawableId = getDrawableResourceByName(uri);
iconBitmap = BitmapFactory.decodeResource(getResources(), drawableId);
if (iconBitmap == null) { // VectorDrawable or similar
Drawable drawable = getResources().getDrawable(drawableId);
iconBitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
Canvas canvas = new Canvas(iconBitmap);
drawable.draw(canvas);
}
if (this.markerManager != null) {
this.markerManager.getSharedIcon(uri).updateIcon(iconBitmapDescriptor, iconBitmap);
}
update(true);
}
}
public void setIconBitmapDescriptor(BitmapDescriptor bitmapDescriptor, Bitmap bitmap) {
this.iconBitmapDescriptor = bitmapDescriptor;
this.iconBitmap = bitmap;
this.update(true);
}
public void setIconBitmap(Bitmap bitmap) {
this.iconBitmap = bitmap;
}
public MarkerOptions getMarkerOptions() {
if (markerOptions == null) {
markerOptions = new MarkerOptions();
}
fillMarkerOptions(markerOptions);
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 MapCallout)) {
hasCustomMarkerView = true;
updateTracksViewChanges();
}
update(true);
}
@Override
public void requestLayout() {
super.requestLayout();
if (getChildCount() == 0) {
if (hasCustomMarkerView) {
hasCustomMarkerView = false;
clearDrawableCache();
updateTracksViewChanges();
update(true);
}
}
}
@Override
public Object getFeature() {
return marker;
}
@Override
public void addToMap(Object collection) {
MarkerManager.Collection markerCollection = (MarkerManager.Collection) collection;
marker = markerCollection.addMarker(getMarkerOptions());
updateTracksViewChanges();
}
@Override
public void removeFromMap(Object collection) {
if (marker == null) {
return;
}
MarkerManager.Collection markerCollection = (MarkerManager.Collection) collection;
markerCollection.remove(marker);
marker = null;
updateTracksViewChanges();
}
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 fillMarkerOptions(MarkerOptions options) {
options.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(boolean updateIcon) {
if (marker == null) {
return;
}
if (updateIcon)
updateMarkerIcon();
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(true);
}
private Bitmap mLastBitmapCreated = null;
private void clearDrawableCache() {
mLastBitmapCreated = null;
}
private Bitmap createDrawable() {
int width = this.width <= 0 ? 100 : this.width;
int height = this.height <= 0 ? 100 : this.height;
this.buildDrawingCache();
// Do not create the doublebuffer-bitmap each time. reuse it to save memory.
Bitmap bitmap = mLastBitmapCreated;
if (bitmap == null ||
bitmap.isRecycled() ||
bitmap.getWidth() != width ||
bitmap.getHeight() != height) {
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mLastBitmapCreated = bitmap;
} else {
bitmap.eraseColor(Color.TRANSPARENT);
}
Canvas canvas = new Canvas(bitmap);
this.draw(canvas);
return bitmap;
}
public void setCalloutView(MapCallout view) {
this.calloutView = view;
}
public MapCallout 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;
}
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
));
LL.addView(LL2);
LL2.addView(this.calloutView);
this.wrappedCalloutView = LL;
}
private int getDrawableResourceByName(String name) {
return getResources().getIdentifier(
name,
"drawable",
getContext().getPackageName());
}
private BitmapDescriptor getBitmapDescriptorByName(String name) {
return BitmapDescriptorFactory.fromResource(getDrawableResourceByName(name));
}
}
@@ -0,0 +1,368 @@
package com.rnmaps.maps;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.facebook.react.R;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.gms.maps.model.Marker;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.LatLng;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
public class MapMarkerManager extends ViewGroupManager<MapMarker> {
public static class AirMapMarkerSharedIcon {
private BitmapDescriptor iconBitmapDescriptor;
private Bitmap bitmap;
private final Map<MapMarker, Boolean> markers;
private boolean loadImageStarted;
public AirMapMarkerSharedIcon() {
this.markers = new WeakHashMap<>();
this.loadImageStarted = false;
}
/**
* check whether the load image process started.
* caller AirMapMarker will only need to load it when this returns true.
*
* @return true if it is not started, false otherwise.
*/
public synchronized boolean shouldLoadImage() {
if (!this.loadImageStarted) {
this.loadImageStarted = true;
return true;
}
return false;
}
/**
* subscribe icon update for given marker.
* <p>
* The marker is wrapped in weakReference, so no need to remove it explicitly.
*
* @param marker
*/
public synchronized void addMarker(MapMarker marker) {
this.markers.put(marker, true);
if (this.iconBitmapDescriptor != null) {
marker.setIconBitmapDescriptor(this.iconBitmapDescriptor, this.bitmap);
}
}
/**
* Remove marker from this shared icon.
* <p>
* Marker will only need to call it when the marker receives a different marker image uri.
*
* @param marker
*/
public synchronized void removeMarker(MapMarker marker) {
this.markers.remove(marker);
}
/**
* check if there is markers still listening on this icon.
* when there are not markers listen on it, we can remove it.
*
* @return true if there is, false otherwise
*/
public synchronized boolean hasMarker() {
return this.markers.isEmpty();
}
/**
* Update the bitmap descriptor and bitmap for the image uri.
* And notify all subscribers about the update.
*
* @param bitmapDescriptor
* @param bitmap
*/
public synchronized void updateIcon(BitmapDescriptor bitmapDescriptor, Bitmap bitmap) {
this.iconBitmapDescriptor = bitmapDescriptor;
this.bitmap = bitmap.copy(Bitmap.Config.ARGB_8888, true);
if (this.markers.isEmpty()) {
return;
}
for (Map.Entry<MapMarker, Boolean> markerEntry : markers.entrySet()) {
if (markerEntry.getKey() != null) {
markerEntry.getKey().setIconBitmapDescriptor(bitmapDescriptor, bitmap);
}
}
}
}
private final Map<String, AirMapMarkerSharedIcon> sharedIcons = new ConcurrentHashMap<>();
/**
* get the shared icon object, if not existed, create a new one and store it.
*
* @param uri
* @return the icon object for the given uri.
*/
public AirMapMarkerSharedIcon getSharedIcon(String uri) {
AirMapMarkerSharedIcon icon = this.sharedIcons.get(uri);
if (icon == null) {
synchronized (this) {
if ((icon = this.sharedIcons.get(uri)) == null) {
icon = new AirMapMarkerSharedIcon();
this.sharedIcons.put(uri, icon);
}
}
}
return icon;
}
/**
* Remove the share icon object from our sharedIcons map when no markers are listening for it.
*
* @param uri
*/
public void removeSharedIconIfEmpty(String uri) {
AirMapMarkerSharedIcon icon = this.sharedIcons.get(uri);
if (icon == null) {
return;
}
if (!icon.hasMarker()) {
synchronized (this) {
if ((icon = this.sharedIcons.get(uri)) != null && !icon.hasMarker()) {
this.sharedIcons.remove(uri);
}
}
}
}
public MapMarkerManager() {
}
@Override
public String getName() {
return "AIRMapMarker";
}
@Override
public MapMarker createViewInstance(ThemedReactContext context) {
return new MapMarker(context, this);
}
@ReactProp(name = "coordinate")
public void setCoordinate(MapMarker view, ReadableMap map) {
view.setCoordinate(map);
}
@ReactProp(name = "title")
public void setTitle(MapMarker view, String title) {
view.setTitle(title);
}
@ReactProp(name = "identifier")
public void setIdentifier(MapMarker view, String identifier) {
view.setIdentifier(identifier);
}
@ReactProp(name = "description")
public void setDescription(MapMarker 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) {
//
// }
//
// @ReactProp(name = "calloutOffset")
// public void setCalloutOffset(AirMapMarker view, ReadableMap map) {
//
// }
@ReactProp(name = "anchor")
public void setAnchor(MapMarker 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(MapMarker 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(MapMarker view, @Nullable String source) {
view.setImage(source);
}
// public void setImage(AirMapMarker view, ReadableMap image) {
// view.setImage(image);
// }
@ReactProp(name = "icon")
public void setIcon(MapMarker view, @Nullable String source) {
view.setImage(source);
}
@ReactProp(name = "pinColor", defaultInt = Color.RED, customType = "Color")
public void setPinColor(MapMarker 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(MapMarker view, float rotation) {
view.setRotation(rotation);
}
@ReactProp(name = "flat", defaultBoolean = false)
public void setFlat(MapMarker view, boolean flat) {
view.setFlat(flat);
}
@ReactProp(name = "draggable", defaultBoolean = false)
public void setDraggable(MapMarker view, boolean draggable) {
view.setDraggable(draggable);
}
@Override
@ReactProp(name = "zIndex", defaultFloat = 0.0f)
public void setZIndex(MapMarker 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(MapMarker view, float opacity) {
super.setOpacity(view, opacity);
view.setOpacity(opacity);
}
@ReactProp(name = "tracksViewChanges", defaultBoolean = true)
public void setTracksViewChanges(MapMarker view, boolean tracksViewChanges) {
view.setTracksViewChanges(tracksViewChanges);
}
@ReactProp(name = "accessibilityLabel")
public void setAccessibilityLabel(MapMarker view, @Nullable String accessibilityLabel) {
view.setTag(R.id.accessibility_label, accessibilityLabel);
}
@Override
public void addView(MapMarker parent, View child, int index) {
// if an <Callout /> component is a child, then it is a callout view, NOT part of the
// marker.
if (child instanceof MapCallout) {
parent.setCalloutView((MapCallout) child);
} else {
super.addView(parent, child, index);
parent.update(true);
}
}
@Override
public void removeViewAt(MapMarker parent, int index) {
super.removeViewAt(parent, index);
parent.update(true);
}
@Override
public void receiveCommand(@NonNull MapMarker view, String commandId, @Nullable ReadableArray args) {
int duration;
double lat;
double lng;
ReadableMap region;
switch (commandId) {
case "showCallout":
((Marker) view.getFeature()).showInfoWindow();
break;
case "hideCallout":
((Marker) view.getFeature()).hideInfoWindow();
break;
case "animateMarkerToCoordinate":
if (args == null) {
break;
}
region = args.getMap(0);
duration = args.getInt(1);
lng = region.getDouble("longitude");
lat = region.getDouble("latitude");
view.animateToCoodinate(new LatLng(lat, lng), duration);
break;
case "redraw":
view.updateMarkerIcon();
break;
}
}
@Override
@Nullable
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.<String, Map<String, String>>builder()
.put("onPress", MapBuilder.of("registrationName", "onPress"))
.put("onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress"))
.put("onDragStart", MapBuilder.of("registrationName", "onDragStart"))
.put("onDrag", MapBuilder.of("registrationName", "onDrag"))
.put("onDragEnd", MapBuilder.of("registrationName", "onDragEnd"))
.build();
}
@Override
@Nullable
public Map getExportedCustomBubblingEventTypeConstants() {
return MapBuilder.<String, Map<String, Object>>builder()
.put("onSelect", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onSelect")))
.put("onDeselect", MapBuilder.of("phasedRegistrationNames", MapBuilder.of("bubbled", "onDeselect")))
.build();
}
@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(MapMarker view, Object extraData) {
// This method is called from the shadow node with the width/height of the rendered
// marker view.
HashMap<String, Float> data = (HashMap<String, Float>) extraData;
float width = data.get("width");
float height = data.get("height");
view.update((int) width, (int) height);
}
}
@@ -0,0 +1,283 @@
package com.rnmaps.maps;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.util.Base64;
import android.util.DisplayMetrics;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.bridge.ReadableMap;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.module.annotations.ReactModule;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.CameraPosition;
import com.google.android.gms.maps.model.LatLng;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@ReactModule(name = MapModule.NAME)
public class MapModule extends ReactContextBaseJavaModule {
public static final String NAME = "AirMapModule";
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 MapModule(ReactApplicationContext reactContext) {
super(reactContext);
}
@Override
public String getName() {
return NAME;
}
@Override
public Map<String, Object> getConstants() {
final Map<String, Object> 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) {
// 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";
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
view.map.snapshot(new GoogleMap.SnapshotReadyCallback() {
public void onSnapshotReady(@Nullable Bitmap snapshot) {
// Convert image to requested width/height if necessary
if (snapshot == null) {
promise.reject("Failed to generate bitmap, snapshot = null");
return;
}
if ((width != 0) && (height != 0) &&
(width != snapshot.getWidth() || height != snapshot.getHeight())) {
snapshot = Bitmap.createScaledBitmap(snapshot, width, height, true);
}
// Save the snapshot to disk
if (result.equals(SNAPSHOT_RESULT_FILE)) {
File tempFile;
FileOutputStream outputStream;
try {
tempFile =
File.createTempFile("AirMapSnapshot", "." + format, context.getCacheDir());
outputStream = new FileOutputStream(tempFile);
} catch (Exception e) {
promise.reject(e);
return;
}
snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream);
closeQuietly(outputStream);
String uri = Uri.fromFile(tempFile).toString();
promise.resolve(uri);
} else if (result.equals(SNAPSHOT_RESULT_BASE64)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
snapshot.compress(compressFormat, (int) (100.0 * quality), outputStream);
closeQuietly(outputStream);
byte[] bytes = outputStream.toByteArray();
String data = Base64.encodeToString(bytes, Base64.NO_WRAP);
promise.resolve(data);
}
}
});
return null;
});
// Add UI-block so we can get a valid reference to the map-view
uiBlock.addToUIManager();
}
@ReactMethod
public void getCamera(final int tag, final Promise promise) {
final ReactApplicationContext context = getReactApplicationContext();
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
CameraPosition position = view.map.getCameraPosition();
WritableMap centerJson = new WritableNativeMap();
centerJson.putDouble("latitude", position.target.latitude);
centerJson.putDouble("longitude", position.target.longitude);
WritableMap cameraJson = new WritableNativeMap();
cameraJson.putMap("center", centerJson);
cameraJson.putDouble("heading", (double)position.bearing);
cameraJson.putDouble("zoom", (double)position.zoom);
cameraJson.putDouble("pitch", (double)position.tilt);
promise.resolve(cameraJson);
return null;
});
uiBlock.addToUIManager();
}
@ReactMethod
public void getAddressFromCoordinates(final int tag, final ReadableMap coordinate, final Promise promise) {
final ReactApplicationContext context = getReactApplicationContext();
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, mapView -> {
if (coordinate == null ||
!coordinate.hasKey("latitude") ||
!coordinate.hasKey("longitude")) {
promise.reject("Invalid coordinate format");
return null;
}
Geocoder geocoder = new Geocoder(context);
try {
List<Address> list =
geocoder.getFromLocation(coordinate.getDouble("latitude"),coordinate.getDouble("longitude"),1);
if (list.isEmpty()) {
promise.reject("Can not get address location");
return null;
}
Address address = list.get(0);
WritableMap addressJson = new WritableNativeMap();
addressJson.putString("name", address.getFeatureName());
addressJson.putString("locality", address.getLocality());
addressJson.putString("thoroughfare", address.getThoroughfare());
addressJson.putString("subThoroughfare", address.getSubThoroughfare());
addressJson.putString("subLocality", address.getSubLocality());
addressJson.putString("administrativeArea", address.getAdminArea());
addressJson.putString("subAdministrativeArea", address.getSubAdminArea());
addressJson.putString("postalCode", address.getPostalCode());
addressJson.putString("countryCode", address.getCountryCode());
addressJson.putString("country", address.getCountryName());
promise.resolve(addressJson);
} catch (IOException e) {
promise.reject("Can not get address location");
}
return null;
});
uiBlock.addToUIManager();
}
@ReactMethod
public void pointForCoordinate(final int tag, ReadableMap coordinate, final Promise promise) {
final ReactApplicationContext context = getReactApplicationContext();
final double density = (double) context.getResources().getDisplayMetrics().density;
final LatLng coord = new LatLng(
coordinate.hasKey("latitude") ? coordinate.getDouble("latitude") : 0.0,
coordinate.hasKey("longitude") ? coordinate.getDouble("longitude") : 0.0
);
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
Point pt = view.map.getProjection().toScreenLocation(coord);
WritableMap ptJson = new WritableNativeMap();
ptJson.putDouble("x", (double)pt.x / density);
ptJson.putDouble("y", (double)pt.y / density);
promise.resolve(ptJson);
return null;
});
uiBlock.addToUIManager();
}
@ReactMethod
public void coordinateForPoint(final int tag, ReadableMap point, final Promise promise) {
final ReactApplicationContext context = getReactApplicationContext();
final double density = (double) context.getResources().getDisplayMetrics().density;
final Point pt = new Point(
point.hasKey("x") ? (int)(point.getDouble("x") * density) : 0,
point.hasKey("y") ? (int)(point.getDouble("y") * density) : 0
);
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
LatLng coord = view.map.getProjection().fromScreenLocation(pt);
WritableMap coordJson = new WritableNativeMap();
coordJson.putDouble("latitude", coord.latitude);
coordJson.putDouble("longitude", coord.longitude);
promise.resolve(coordJson);
return null;
});
uiBlock.addToUIManager();
}
@ReactMethod
public void getMapBoundaries(final int tag, final Promise promise) {
final ReactApplicationContext context = getReactApplicationContext();
MapUIBlock uiBlock = new MapUIBlock(tag, promise, context, view -> {
double[][] boundaries = view.getMapBoundaries();
WritableMap coordinates = new WritableNativeMap();
WritableMap northEastHash = new WritableNativeMap();
WritableMap southWestHash = new WritableNativeMap();
northEastHash.putDouble("longitude", boundaries[0][0]);
northEastHash.putDouble("latitude", boundaries[0][1]);
southWestHash.putDouble("longitude", boundaries[1][0]);
southWestHash.putDouble("latitude", boundaries[1][1]);
coordinates.putMap("northEast", northEastHash);
coordinates.putMap("southWest", southWestHash);
promise.resolve(coordinates);
return null;
});
uiBlock.addToUIManager();
}
}
@@ -0,0 +1,166 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Bitmap;
import com.facebook.react.bridge.ReadableArray;
import com.google.android.gms.maps.model.BitmapDescriptor;
import com.google.android.gms.maps.model.BitmapDescriptorFactory;
import com.google.android.gms.maps.model.GroundOverlay;
import com.google.android.gms.maps.model.GroundOverlayOptions;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
import com.google.maps.android.collections.GroundOverlayManager;
public class MapOverlay extends MapFeature implements ImageReadable {
private GroundOverlayOptions groundOverlayOptions;
private GroundOverlay groundOverlay;
private LatLngBounds bounds;
private float bearing;
private BitmapDescriptor iconBitmapDescriptor;
private boolean tappable;
private float zIndex;
private float transparency;
private final ImageReader mImageReader;
private GroundOverlayManager.Collection groundOverlayCollection;
public MapOverlay(Context context) {
super(context);
this.mImageReader = new ImageReader(context, getResources(), this);
}
public void setBounds(ReadableArray bounds) {
LatLng sw = new LatLng(bounds.getArray(0).getDouble(0), bounds.getArray(0).getDouble(1));
LatLng ne = new LatLng(bounds.getArray(1).getDouble(0), bounds.getArray(1).getDouble(1));
this.bounds = new LatLngBounds(sw, ne);
if (this.groundOverlay != null) {
this.groundOverlay.setPositionFromBounds(this.bounds);
}
}
public void setBearing(float bearing){
this.bearing = bearing;
if (this.groundOverlay != null) {
this.groundOverlay.setBearing(bearing);
}
}
public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (this.groundOverlay != null) {
this.groundOverlay.setZIndex(zIndex);
}
}
public void setTransparency(float transparency) {
this.transparency = transparency;
if (groundOverlay != null) {
groundOverlay.setTransparency(transparency);
}
}
public void setImage(String uri) {
this.mImageReader.setImage(uri);
}
public void setTappable(boolean tapabble) {
this.tappable = tapabble;
if (groundOverlay != null) {
groundOverlay.setClickable(tappable);
}
}
public GroundOverlayOptions getGroundOverlayOptions() {
if (this.groundOverlayOptions == null) {
this.groundOverlayOptions = createGroundOverlayOptions();
}
return this.groundOverlayOptions;
}
private GroundOverlayOptions createGroundOverlayOptions() {
if (this.groundOverlayOptions != null) {
return this.groundOverlayOptions;
}
GroundOverlayOptions options = new GroundOverlayOptions();
if (this.iconBitmapDescriptor != null) {
options.image(iconBitmapDescriptor);
} else {
// add stub image to be able to instantiate the overlay
// and store a reference to it in MapView
options.image(BitmapDescriptorFactory.defaultMarker());
// hide overlay until real image gets added
options.visible(false);
}
options.positionFromBounds(bounds);
options.zIndex(zIndex);
options.bearing(bearing);
options.transparency(transparency);
return options;
}
@Override
public Object getFeature() {
return groundOverlay;
}
@Override
public void addToMap(Object collection) {
GroundOverlayManager.Collection groundOverlayCollection = (GroundOverlayManager.Collection) collection;
GroundOverlayOptions groundOverlayOptions = getGroundOverlayOptions();
if (groundOverlayOptions != null) {
groundOverlay = groundOverlayCollection.addGroundOverlay(groundOverlayOptions);
groundOverlay.setClickable(this.tappable);
} else {
this.groundOverlayCollection = groundOverlayCollection;
}
}
@Override
public void removeFromMap(Object collection) {
if (groundOverlay != null) {
GroundOverlayManager.Collection groundOverlayCollection = (GroundOverlayManager.Collection) collection;
groundOverlayCollection.remove(groundOverlay);
groundOverlay = null;
groundOverlayOptions = null;
}
groundOverlayCollection = null;
}
@Override
public void setIconBitmap(Bitmap bitmap) {
}
@Override
public void setIconBitmapDescriptor(
BitmapDescriptor iconBitmapDescriptor) {
this.iconBitmapDescriptor = iconBitmapDescriptor;
}
@Override
public void update() {
this.groundOverlay = getGroundOverlay();
if (this.groundOverlay != null) {
this.groundOverlay.setVisible(true);
this.groundOverlay.setImage(this.iconBitmapDescriptor);
this.groundOverlay.setTransparency(this.transparency);
this.groundOverlay.setClickable(this.tappable);
}
}
private GroundOverlay getGroundOverlay() {
if (this.groundOverlay != null) {
return this.groundOverlay;
}
if (this.groundOverlayCollection == null) {
return null;
}
GroundOverlayOptions groundOverlayOptions = getGroundOverlayOptions();
if (groundOverlayOptions != null) {
return this.groundOverlayCollection.addGroundOverlay(groundOverlayOptions);
}
return null;
}
}
@@ -0,0 +1,75 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.Map;
public class MapOverlayManager extends ViewGroupManager<MapOverlay> {
public MapOverlayManager(ReactApplicationContext reactContext) {
super();
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapOverlay";
}
@Override
public MapOverlay createViewInstance(ThemedReactContext context) {
return new MapOverlay(context);
}
@ReactProp(name = "bounds")
public void setBounds(MapOverlay view, ReadableArray bounds) {
view.setBounds(bounds);
}
@ReactProp(name = "bearing")
public void setBearing(MapOverlay view, float bearing){
view.setBearing(bearing);
}
@ReactProp(name = "zIndex", defaultFloat = 1.0f)
public void setZIndex(MapOverlay view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "opacity", defaultFloat = 1.0f)
public void setOpacity(MapOverlay view, float opacity) {
view.setTransparency(1 - opacity);
}
@ReactProp(name = "image")
public void setImage(MapOverlay view, @Nullable String source) {
view.setImage(source);
}
@ReactProp(name = "tappable", defaultBoolean = false)
public void setTappable(MapOverlay view, boolean tapabble) {
view.setTappable(tapabble);
}
@Override
@Nullable
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onPress", MapBuilder.of("registrationName", "onPress")
);
}
}
@@ -0,0 +1,194 @@
package com.rnmaps.maps;
import android.content.Context;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.google.android.gms.maps.model.Dash;
import com.google.android.gms.maps.model.Gap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.PatternItem;
import com.google.android.gms.maps.model.Polygon;
import com.google.android.gms.maps.model.PolygonOptions;
import com.google.maps.android.collections.PolygonManager;
import java.util.ArrayList;
import java.util.List;
public class MapPolygon extends MapFeature {
private PolygonOptions polygonOptions;
private Polygon polygon;
private List<LatLng> coordinates;
private List<List<LatLng>> holes;
private int strokeColor;
private int fillColor;
private float strokeWidth;
private boolean geodesic;
private boolean tappable;
private float zIndex;
private ReadableArray patternValues;
private List<PatternItem> pattern;
public MapPolygon(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")));
}
if (polygon != null) {
polygon.setPoints(this.coordinates);
}
}
public void setHoles(ReadableArray holes) {
if (holes == null) { return; }
this.holes = new ArrayList<>(holes.size());
for (int i = 0; i < holes.size(); i++) {
ReadableArray hole = holes.getArray(i);
if (hole.size() < 3) { continue; }
List<LatLng> coordinates = new ArrayList<>();
for (int j = 0; j < hole.size(); j++) {
ReadableMap coordinate = hole.getMap(j);
coordinates.add(new LatLng(
coordinate.getDouble("latitude"),
coordinate.getDouble("longitude")));
}
// If hole is triangle
if (coordinates.size() == 3) {
coordinates.add(coordinates.get(0));
}
this.holes.add(coordinates);
}
if (polygon != null) {
polygon.setHoles(this.holes);
}
}
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 setTappable(boolean tapabble) {
this.tappable = tapabble;
if (polygon != null) {
polygon.setClickable(tappable);
}
}
public void setGeodesic(boolean geodesic) {
this.geodesic = geodesic;
if (polygon != null) {
polygon.setGeodesic(geodesic);
}
}
public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (polygon != null) {
polygon.setZIndex(zIndex);
}
}
public void setLineDashPattern(ReadableArray patternValues) {
this.patternValues = patternValues;
this.applyPattern();
}
private void applyPattern() {
if(patternValues == null) {
return;
}
this.pattern = new ArrayList<>(patternValues.size());
for (int i = 0; i < patternValues.size(); i++) {
float patternValue = (float) patternValues.getDouble(i);
boolean isGap = i % 2 != 0;
if(isGap) {
this.pattern.add(new Gap(patternValue));
}else {
PatternItem patternItem;
patternItem = new Dash(patternValue);
this.pattern.add(patternItem);
}
}
if(polygon != null) {
polygon.setStrokePattern(this.pattern);
}
}
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);
options.strokePattern(this.pattern);
if (this.holes != null) {
for (int i = 0; i < holes.size(); i++) {
options.addHole(holes.get(i));
}
}
return options;
}
@Override
public Object getFeature() {
return polygon;
}
@Override
public void addToMap(Object collection) {
PolygonManager.Collection polygonCollection = (PolygonManager.Collection) collection;
polygon = polygonCollection.addPolygon(getPolygonOptions());
polygon.setClickable(this.tappable);
}
@Override
public void removeFromMap(Object collection) {
PolygonManager.Collection polygonCollection = (PolygonManager.Collection) collection;
polygonCollection.remove(polygon);
}
}
@@ -0,0 +1,93 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import java.util.Map;
public class MapPolygonManager extends ViewGroupManager<MapPolygon> {
private final DisplayMetrics metrics;
public MapPolygonManager(ReactApplicationContext reactContext) {
super();
metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapPolygon";
}
@Override
public MapPolygon createViewInstance(ThemedReactContext context) {
return new MapPolygon(context);
}
@ReactProp(name = "coordinates")
public void setCoordinate(MapPolygon view, ReadableArray coordinates) {
view.setCoordinates(coordinates);
}
@ReactProp(name = "holes")
public void setHoles(MapPolygon view, ReadableArray holes) {
view.setHoles(holes);
}
@ReactProp(name = "strokeWidth", defaultFloat = 1f)
public void setStrokeWidth(MapPolygon 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(MapPolygon view, int color) {
view.setFillColor(color);
}
@ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color")
public void setStrokeColor(MapPolygon view, int color) {
view.setStrokeColor(color);
}
@ReactProp(name = "tappable", defaultBoolean = false)
public void setTappable(MapPolygon view, boolean tapabble) {
view.setTappable(tapabble);
}
@ReactProp(name = "geodesic", defaultBoolean = false)
public void setGeodesic(MapPolygon view, boolean geodesic) {
view.setGeodesic(geodesic);
}
@ReactProp(name = "zIndex", defaultFloat = 1.0f)
public void setZIndex(MapPolygon view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "lineDashPattern")
public void setLineDashPattern(MapPolygon view, ReadableArray patternValues) {
view.setLineDashPattern(patternValues);
}
@Override
@Nullable
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onPress", MapBuilder.of("registrationName", "onPress")
);
}
}
@@ -0,0 +1,164 @@
package com.rnmaps.maps;
import android.content.Context;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.ReadableMap;
import com.google.android.gms.maps.model.Cap;
import com.google.android.gms.maps.model.Dash;
import com.google.android.gms.maps.model.Dot;
import com.google.android.gms.maps.model.Gap;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.PatternItem;
import com.google.android.gms.maps.model.Polyline;
import com.google.android.gms.maps.model.PolylineOptions;
import com.google.android.gms.maps.model.RoundCap;
import com.google.maps.android.collections.PolylineManager;
import java.util.ArrayList;
import java.util.List;
public class MapPolyline extends MapFeature {
private PolylineOptions polylineOptions;
private Polyline polyline;
private List<LatLng> coordinates;
private int color;
private float width;
private boolean tappable;
private boolean geodesic;
private float zIndex;
private Cap lineCap = new RoundCap();
private ReadableArray patternValues;
private List<PatternItem> pattern;
public MapPolyline(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);
}
}
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 setTappable(boolean tapabble) {
this.tappable = tapabble;
if (polyline != null) {
polyline.setClickable(tappable);
}
}
public void setGeodesic(boolean geodesic) {
this.geodesic = geodesic;
if (polyline != null) {
polyline.setGeodesic(geodesic);
}
}
public void setLineCap(Cap cap) {
this.lineCap = cap;
if (polyline != null) {
polyline.setStartCap(cap);
polyline.setEndCap(cap);
}
this.applyPattern();
}
public void setLineDashPattern(ReadableArray patternValues) {
this.patternValues = patternValues;
this.applyPattern();
}
private void applyPattern() {
if(patternValues == null) {
return;
}
this.pattern = new ArrayList<>(patternValues.size());
for (int i = 0; i < patternValues.size(); i++) {
float patternValue = (float) patternValues.getDouble(i);
boolean isGap = i % 2 != 0;
if(isGap) {
this.pattern.add(new Gap(patternValue));
}else {
PatternItem patternItem;
boolean isLineCapRound = this.lineCap instanceof RoundCap;
if(isLineCapRound) {
patternItem = new Dot();
}else {
patternItem = new Dash(patternValue);
}
this.pattern.add(patternItem);
}
}
if(polyline != null) {
polyline.setPattern(this.pattern);
}
}
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);
options.startCap(lineCap);
options.endCap(lineCap);
options.pattern(this.pattern);
return options;
}
@Override
public Object getFeature() {
return polyline;
}
@Override
public void addToMap(Object collection) {
PolylineManager.Collection polylineCollection = (PolylineManager.Collection) collection;
polyline = polylineCollection.addPolyline(getPolylineOptions());
polyline.setClickable(this.tappable);
}
@Override
public void removeFromMap(Object collection) {
PolylineManager.Collection polylineCollection = (PolylineManager.Collection) collection;
polylineCollection.remove(polyline);
}
}
@@ -0,0 +1,107 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Color;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import androidx.annotation.Nullable;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.common.MapBuilder;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.gms.maps.model.ButtCap;
import com.google.android.gms.maps.model.Cap;
import com.google.android.gms.maps.model.RoundCap;
import com.google.android.gms.maps.model.SquareCap;
import java.util.Map;
public class MapPolylineManager extends ViewGroupManager<MapPolyline> {
private final DisplayMetrics metrics;
public MapPolylineManager(ReactApplicationContext reactContext) {
super();
metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapPolyline";
}
@Override
public MapPolyline createViewInstance(ThemedReactContext context) {
return new MapPolyline(context);
}
@ReactProp(name = "coordinates")
public void setCoordinate(MapPolyline view, ReadableArray coordinates) {
view.setCoordinates(coordinates);
}
@ReactProp(name = "strokeWidth", defaultFloat = 1f)
public void setStrokeWidth(MapPolyline 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(MapPolyline view, int color) {
view.setColor(color);
}
@ReactProp(name = "tappable", defaultBoolean = false)
public void setTappable(MapPolyline view, boolean tapabble) {
view.setTappable(tapabble);
}
@ReactProp(name = "geodesic", defaultBoolean = false)
public void setGeodesic(MapPolyline view, boolean geodesic) {
view.setGeodesic(geodesic);
}
@ReactProp(name = "zIndex", defaultFloat = 1.0f)
public void setZIndex(MapPolyline view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "lineCap")
public void setlineCap(MapPolyline view, String lineCap) {
Cap cap = null;
switch (lineCap) {
case "butt":
cap = new ButtCap();
break;
case "round":
cap = new RoundCap();
break;
case "square":
cap = new SquareCap();
break;
default:
cap = new RoundCap();
break;
}
view.setLineCap(cap);
}
@ReactProp(name = "lineDashPattern")
public void setLineDashPattern(MapPolyline view, ReadableArray patternValues) {
view.setLineDashPattern(patternValues);
}
@Override
@Nullable
public Map getExportedCustomDirectEventTypeConstants() {
return MapBuilder.of(
"onPress", MapBuilder.of("registrationName", "onPress")
);
}
}
@@ -0,0 +1,494 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.Log;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.Future;
import java.util.List;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import androidx.work.Data;
import androidx.work.Constraints;
import androidx.work.NetworkType;
import androidx.work.ExistingWorkPolicy;
import androidx.work.Operation;
import androidx.work.WorkInfo;
import com.google.android.gms.maps.model.Tile;
import com.google.android.gms.maps.model.TileProvider;
import com.google.android.gms.maps.model.UrlTileProvider;
import java.lang.System;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class MapTileProvider implements TileProvider {
class AIRMapUrlTileProvider extends UrlTileProvider {
private String urlTemplate;
public AIRMapUrlTileProvider(int width, int height, String urlTemplate) {
super(width, height);
this.urlTemplate = urlTemplate;
}
@Override
public URL getTileUrl(int x, int y, int zoom) {
if (MapTileProvider.this.flipY) {
y = (1 << zoom) - y - 1;
}
String s = this.urlTemplate
.replace("{x}", Integer.toString(x))
.replace("{y}", Integer.toString(y))
.replace("{z}", Integer.toString(zoom));
URL url;
if(MapTileProvider.this.maximumZ > 0 && zoom > MapTileProvider.this.maximumZ) {
return null;
}
if(MapTileProvider.this.minimumZ > 0 && zoom < MapTileProvider.this.minimumZ) {
return null;
}
try {
url = new URL(s);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return url;
}
public void setUrlTemplate(String urlTemplate) {
this.urlTemplate = urlTemplate;
}
}
protected static final int BUFFER_SIZE = 16 * 1024;
protected static final int TARGET_TILE_SIZE = 512;
protected UrlTileProvider tileProvider;
protected String urlTemplate;
protected int tileSize;
protected boolean doubleTileSize;
protected int maximumZ;
protected int maximumNativeZ;
protected int minimumZ;
protected boolean flipY;
protected String tileCachePath;
protected int tileCacheMaxAge;
protected boolean offlineMode;
protected Context context;
protected boolean customMode;
public MapTileProvider(int tileSizet, boolean doubleTileSize, String urlTemplate,
int maximumZ, int maximumNativeZ, int minimumZ, boolean flipY, String tileCachePath,
int tileCacheMaxAge, boolean offlineMode, Context context, boolean customMode) {
this.tileProvider = new AIRMapUrlTileProvider(tileSizet, tileSizet, urlTemplate);
this.tileSize = tileSizet;
this.doubleTileSize = doubleTileSize;
this.urlTemplate = urlTemplate;
this.maximumZ = maximumZ;
this.maximumNativeZ = maximumNativeZ;
this.minimumZ = minimumZ;
this.flipY = flipY;
this.tileCachePath = tileCachePath;
this.tileCacheMaxAge = tileCacheMaxAge;
this.offlineMode = offlineMode;
this.context = context;
this.customMode = customMode;
}
@Override
public Tile getTile(int x, int y, int zoom) {
if (!this.customMode) return this.tileProvider.getTile(x, y, zoom);
byte[] image = null;
int maximumZ = this.maximumZ > 0 ? this.maximumZ : Integer.MAX_VALUE;
if (this.tileSize == 256 && this.doubleTileSize && zoom + 1 <= this.maximumNativeZ && zoom + 1 <= maximumZ) {
Log.d("urlTile", "pullTilesFromHigherZoom");
image = pullTilesFromHigherZoom(x, y, zoom);
}
if (zoom > this.maximumNativeZ) {
Log.d("urlTile", "scaleLowerZoomTile");
image = scaleLowerZoomTile(x, y, zoom, this.maximumNativeZ);
}
if (image == null && zoom <= maximumZ) {
Log.d("urlTile", "getTileImage");
image = getTileImage(x, y, zoom);
}
if (image == null && this.tileCachePath != null && this.offlineMode) {
Log.d("urlTile", "findLowerZoomTileForScaling");
int zoomLevelToStart = (zoom > this.maximumNativeZ) ? this.maximumNativeZ - 1 : zoom - 1;
int minimumZoomToSearch = Math.max(this.minimumZ, zoom - 3);
for (int tryZoom = zoomLevelToStart; tryZoom >= minimumZoomToSearch; tryZoom--) {
image = scaleLowerZoomTile(x, y, zoom, tryZoom);
if (image != null) {
break;
}
}
}
return image == null ? null : new Tile(this.tileSize, this.tileSize, image);
}
byte[] getTileImage(int x, int y, int zoom) {
byte[] image = null;
if (this.tileCachePath != null) {
image = readTileImage(x, y, zoom);
if (image != null) {
Log.d("urlTile", "tile cache HIT for " + zoom +
"/" + x + "/" + y);
} else {
Log.d("urlTile", "tile cache MISS for " + zoom +
"/" + x + "/" + y);
}
if (image != null && !this.offlineMode) {
checkForRefresh(x, y, zoom);
}
}
if (image == null && !this.offlineMode && this.tileCachePath != null) {
String fileName = getTileFilename(x, y, zoom);
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest tileRefreshWorkRequest = new OneTimeWorkRequest.Builder(MapTileWorker.class)
.setConstraints(constraints)
.addTag(fileName)
.setInputData(
new Data.Builder()
.putString("url", getTileUrl(x, y, zoom).toString())
.putString("filename", fileName)
.putInt("maxAge", -1)
.build()
)
.build();
WorkManager workManager = WorkManager.getInstance(this.context.getApplicationContext());
Operation fetchOperation = workManager
.enqueueUniqueWork(fileName, ExistingWorkPolicy.KEEP, tileRefreshWorkRequest);
Future<Operation.State.SUCCESS> operationFuture = fetchOperation.getResult();
try {
operationFuture.get(1L, TimeUnit.SECONDS);
Thread.sleep(500);
Future<List<WorkInfo>> fetchFuture = workManager.getWorkInfosByTag(fileName);
List<WorkInfo> workInfo = fetchFuture.get(1L, TimeUnit.SECONDS);
Log.d("urlTile: ", workInfo.get(0).toString());
if (this.tileCachePath != null) {
image = readTileImage(x, y, zoom);
if (image != null) {
Log.d("urlTile","tile cache fetch HIT for " + zoom +
"/" + x + "/" + y);
} else {
Log.d("urlTile","tile cache fetch MISS for " + zoom +
"/" + x + "/" + y);
}
}
} catch (Exception e) {
e.printStackTrace();
}
} else if (image == null && !this.offlineMode) {
Log.d("urlTile", "Normal fetch");
image = fetchTile(x, y, zoom);
if (image == null) {
Log.d("urlTile", "tile fetch TIMEOUT / FAIL for " + zoom +
"/" + x + "/" + y);
}
}
return image;
}
byte[] pullTilesFromHigherZoom(int x, int y, int zoom) {
byte[] data;
Bitmap image = getNewBitmap();
Canvas canvas = new Canvas(image);
Paint paint = new Paint();
x = x * 2;
y = y * 2;
byte[] leftTop = getTileImage(x, y, zoom + 1);
byte[] leftBottom = getTileImage(x, y + 1, zoom + 1);
byte[] rightTop = getTileImage(x + 1, y, zoom + 1);
byte[] rightBottom = getTileImage(x + 1, y + 1, zoom + 1);
if (leftTop == null || leftBottom == null || rightTop == null || rightBottom == null) {
return null;
}
Bitmap bitmap;
bitmap = BitmapFactory.decodeByteArray(leftTop, 0, leftTop.length);
canvas.drawBitmap(bitmap, 0, 0, paint);
bitmap.recycle();
bitmap = BitmapFactory.decodeByteArray(leftBottom, 0, leftBottom.length);
canvas.drawBitmap(bitmap, 0, 256, paint);
bitmap.recycle();
bitmap = BitmapFactory.decodeByteArray(rightTop, 0, rightTop.length);
canvas.drawBitmap(bitmap, 256, 0, paint);
bitmap.recycle();
bitmap = BitmapFactory.decodeByteArray(rightBottom, 0, rightBottom.length);
canvas.drawBitmap(bitmap, 256, 256, paint);
bitmap.recycle();
data = bitmapToByteArray(image);
image.recycle();
return data;
}
Bitmap getNewBitmap() {
Bitmap image = Bitmap.createBitmap(TARGET_TILE_SIZE, TARGET_TILE_SIZE, Bitmap.Config.ARGB_8888);
image.eraseColor(Color.TRANSPARENT);
return image;
}
byte[] bitmapToByteArray(Bitmap bm) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.PNG, 100, bos);
byte[] data = bos.toByteArray();
try {
bos.close();
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
byte[] scaleLowerZoomTile(int x, int y, int zoom, int maximumZoom) {
int overZoomLevel = zoom - maximumZoom;
int zoomFactor = 1 << overZoomLevel;
int xParent = x >> overZoomLevel;
int yParent = y >> overZoomLevel;
int zoomParent = zoom - overZoomLevel;
int xOffset = x % zoomFactor;
int yOffset = y % zoomFactor;
byte[] data;
Bitmap image = getNewBitmap();
Canvas canvas = new Canvas(image);
Paint paint = new Paint();
data = getTileImage(xParent, yParent, zoomParent);
if (data == null) return null;
Bitmap sourceImage;
sourceImage = BitmapFactory.decodeByteArray(data, 0, data.length);
int subTileSize = this.tileSize / zoomFactor;
Rect sourceRect = new Rect(xOffset * subTileSize, yOffset * subTileSize, xOffset * subTileSize + subTileSize , yOffset * subTileSize + subTileSize);
Rect targetRect = new Rect(0,0,TARGET_TILE_SIZE, TARGET_TILE_SIZE);
canvas.drawBitmap(sourceImage, sourceRect, targetRect, paint);
sourceImage.recycle();
data = bitmapToByteArray(image);
image.recycle();
return data;
}
void checkForRefresh(int x, int y, int zoom) {
String fileName = getTileFilename(x, y, zoom);
File file = new File(fileName);
long lastModified = file.lastModified();
long now = System.currentTimeMillis();
if ((now - lastModified) / 1000 > this.tileCacheMaxAge) {
Log.d("urlTile", "Refreshing");
Constraints constraints = new Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build();
OneTimeWorkRequest tileRefreshWorkRequest = new OneTimeWorkRequest.Builder(MapTileWorker.class)
.setConstraints(constraints)
.addTag(fileName)
.setInputData(
new Data.Builder()
.putString("url", getTileUrl(x, y, zoom).toString())
.putString("filename", fileName)
.putInt("maxAge", this.tileCacheMaxAge)
.build()
)
.build();
WorkManager.getInstance(this.context.getApplicationContext())
.enqueueUniqueWork(fileName, ExistingWorkPolicy.KEEP, tileRefreshWorkRequest);
}
}
byte[] fetchTile(int x, int y, int zoom) {
URL url = getTileUrl(x, y, zoom);
ByteArrayOutputStream buffer = null;
InputStream in = null;
try {
URLConnection conn = url.openConnection();
in = conn.getInputStream();
buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[BUFFER_SIZE];
while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return null;
} finally {
if (in != null) try { in.close(); } catch (Exception ignored) {}
if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
}
}
byte[] readTileImage(int x, int y, int zoom) {
InputStream in = null;
ByteArrayOutputStream buffer = null;
String fileName = getTileFilename(x, y, zoom);
if (fileName == null) {
return null;
}
File file = new File(fileName);
try {
in = new FileInputStream(file);
buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[BUFFER_SIZE];
while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
if (this.tileCacheMaxAge == 0) {
file.setLastModified(System.currentTimeMillis());
}
return buffer.toByteArray();
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return null;
} finally {
if (in != null) try { in.close(); } catch (Exception ignored) {}
if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
}
}
boolean writeTileImage(byte[] image, int x, int y, int zoom) {
OutputStream out = null;
String fileName = getTileFilename(x, y, zoom);
if (fileName == null) {
return false;
}
try {
File file = new File(fileName);
file.getParentFile().mkdirs();
out = new FileOutputStream(file);
out.write(image);
return true;
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return false;
} finally {
if (out != null) try { out.close(); } catch (Exception ignored) {}
}
}
String getTileFilename(int x, int y, int zoom) {
if (this.tileCachePath == null) {
return null;
}
return this.tileCachePath + '/' + zoom +
"/" + x + "/" + y;
}
protected URL getTileUrl(int x, int y, int zoom) {
return this.tileProvider.getTileUrl(x, y, zoom);
}
public void setUrlTemplate(String urlTemplate) {
if (this.urlTemplate != urlTemplate) {
this.tileProvider = new AIRMapUrlTileProvider(tileSize, tileSize, urlTemplate);
}
this.urlTemplate = urlTemplate;
}
public void setTileSize(int tileSize) {
if (this.tileSize != tileSize) {
this.tileProvider = new AIRMapUrlTileProvider(tileSize, tileSize, urlTemplate);
}
this.tileSize = tileSize;
}
public void setDoubleTileSize(boolean doubleTileSize) {
this.doubleTileSize = doubleTileSize;
}
public void setMaximumZ(int maximumZ) {
this.maximumZ = maximumZ;
}
public void setMaximumNativeZ(int maximumNativeZ) {
this.maximumNativeZ = maximumNativeZ;
}
public void setMinimumZ(int minimumZ) {
this.minimumZ = minimumZ;
}
public void setFlipY(boolean flipY) {
this.flipY = flipY;
}
public void setTileCachePath(String tileCachePath) {
this.tileCachePath = tileCachePath;
}
public void setTileCacheMaxAge(int tileCacheMaxAge) {
this.tileCacheMaxAge = tileCacheMaxAge;
}
public void setOfflineMode(boolean offlineMode) {
this.offlineMode = offlineMode;
}
public void setCustomMode() {
}
}
@@ -0,0 +1,115 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
public class MapTileWorker extends Worker {
private static final int BUFFER_SIZE = 16 * 1024;
public MapTileWorker(
@NonNull Context context,
@NonNull WorkerParameters params) {
super(context, params);
}
@Override
public Result doWork() {
byte[] image;
URL url;
String fileName = getInputData().getString("filename");
try {
int tileCacheMaxAge = getInputData().getInt("maxAge", 0);
if (tileCacheMaxAge >= 0) {
File file = new File(fileName);
long lastModified = file.lastModified();
long now = System.currentTimeMillis();
if ((now - lastModified) / 1000 < tileCacheMaxAge) return Result.failure();
}
} catch (Error e) {
return Result.failure();
}
try {
url = new URL(getInputData().getString("url"));
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
image = fetchTile(url);
if (image != null) {
boolean success = writeTileImage(image, fileName);
if (!success) {
return Result.failure();
}
} else {
return Result.retry();
}
// Indicate whether the work finished successfully with the Result
Log.d("urlTile", "Worker fetched " + fileName);
return Result.success();
}
private byte[] fetchTile(URL url) {
ByteArrayOutputStream buffer = null;
InputStream in = null;
try {
in = url.openStream();
buffer = new ByteArrayOutputStream();
int nRead;
byte[] data = new byte[BUFFER_SIZE];
while ((nRead = in.read(data, 0, BUFFER_SIZE)) != -1) {
buffer.write(data, 0, nRead);
}
buffer.flush();
return buffer.toByteArray();
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return null;
} finally {
if (in != null) try { in.close(); } catch (Exception ignored) {}
if (buffer != null) try { buffer.close(); } catch (Exception ignored) {}
}
}
private boolean writeTileImage(byte[] image, String fileName) {
OutputStream out = null;
if (fileName == null) {
return false;
}
try {
File file = new File(fileName);
file.getParentFile().mkdirs();
out = new FileOutputStream(file);
out.write(image);
return true;
} catch (IOException | OutOfMemoryError e) {
e.printStackTrace();
return false;
} finally {
if (out != null) try { out.close(); } catch (Exception ignored) {}
}
}
}
@@ -0,0 +1,64 @@
package com.rnmaps.maps;
import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.UIManager;
import com.facebook.react.fabric.FabricUIManager;
import com.facebook.react.fabric.interop.UIBlockViewResolver;
import com.facebook.react.uimanager.common.UIManagerType;
import com.facebook.react.uimanager.NativeViewHierarchyManager;
import com.facebook.react.uimanager.UIBlock;
import com.facebook.react.uimanager.UIManagerHelper;
import com.facebook.react.uimanager.UIManagerModule;
import java.util.function.Function;
public class MapUIBlock implements UIBlockInterface {
private int tag;
private Promise promise;
private ReactApplicationContext context;
private Function<MapView, Void> mapOperation;
public MapUIBlock(int tag, Promise promise, ReactApplicationContext context, Function<MapView, Void> mapOperation) {
this.tag = tag;
this.promise = promise;
this.context = context;
this.mapOperation = mapOperation;
}
@Override
public void execute(NativeViewHierarchyManager nvhm) {
executeImpl(nvhm, null);
}
@Override
public void execute(UIBlockViewResolver uiBlockViewResolver) {
executeImpl(null, uiBlockViewResolver);
}
private void executeImpl(NativeViewHierarchyManager nvhm, UIBlockViewResolver uiBlockViewResolver) {
MapView view = uiBlockViewResolver != null ? (MapView) uiBlockViewResolver.resolveView(tag) : (MapView) nvhm.resolveView(tag);
if (view == null) {
promise.reject("AirMapView not found");
return;
}
if (view.map == null) {
promise.reject("AirMapView.map is not valid");
return;
}
mapOperation.apply(view);
}
public void addToUIManager() {
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
UIManager uiManager = UIManagerHelper.getUIManager(context, UIManagerType.FABRIC);
((FabricUIManager) uiManager).addUIBlock(this);
} else {
UIManagerModule uiManager = context.getNativeModule(UIManagerModule.class);
uiManager.addUIBlock(this);
}
}
}
interface UIBlockInterface extends UIBlock, com.facebook.react.fabric.interop.UIBlock {}
@@ -0,0 +1,207 @@
package com.rnmaps.maps;
import android.util.Log;
import android.content.Context;
import com.google.android.gms.maps.GoogleMap;
import com.google.android.gms.maps.model.TileOverlay;
import com.google.android.gms.maps.model.TileOverlayOptions;
import java.net.MalformedURLException;
import java.net.URL;
public class MapUrlTile extends MapFeature {
protected TileOverlayOptions tileOverlayOptions;
protected TileOverlay tileOverlay;
protected MapTileProvider tileProvider;
protected String urlTemplate;
protected float zIndex;
protected float maximumZ;
protected float maximumNativeZ = 100;
protected float minimumZ;
protected boolean flipY = false;
protected float tileSize = 256;
protected boolean doubleTileSize = false;
protected String tileCachePath;
protected float tileCacheMaxAge;
protected boolean offlineMode = false;
protected float opacity = 1;
protected Context context;
protected boolean customTileProviderNeeded = false;
public MapUrlTile(Context context) {
super(context);
this.context = context;
}
public void setUrlTemplate(String urlTemplate) {
this.urlTemplate = urlTemplate;
if (tileProvider != null) {
tileProvider.setUrlTemplate(urlTemplate);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setZIndex(float zIndex) {
this.zIndex = zIndex;
if (tileOverlay != null) {
tileOverlay.setZIndex(zIndex);
}
}
public void setMaximumZ(float maximumZ) {
this.maximumZ = maximumZ;
if (tileProvider != null) {
tileProvider.setMaximumZ((int)maximumZ);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setMaximumNativeZ(float maximumNativeZ) {
this.maximumNativeZ = maximumNativeZ;
if (tileProvider != null) {
tileProvider.setMaximumNativeZ((int)maximumNativeZ);
}
setCustomTileProviderMode();
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setMinimumZ(float minimumZ) {
this.minimumZ = minimumZ;
if (tileProvider != null) {
tileProvider.setMinimumZ((int)minimumZ);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setFlipY(boolean flipY) {
this.flipY = flipY;
if (tileProvider != null) {
tileProvider.setFlipY(flipY);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setDoubleTileSize(boolean doubleTileSize) {
this.doubleTileSize = doubleTileSize;
if (tileProvider != null) {
tileProvider.setDoubleTileSize(doubleTileSize);
}
setCustomTileProviderMode();
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setTileSize(float tileSize) {
this.tileSize = tileSize;
if (tileProvider != null) {
tileProvider.setTileSize((int)tileSize);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setTileCachePath(String tileCachePath) {
if (tileCachePath == null || tileCachePath.isEmpty()) return;
try {
URL url = new URL(tileCachePath);
this.tileCachePath = url.getPath();
} catch (MalformedURLException e) {
this.tileCachePath = tileCachePath;
} catch (Exception e) {
return;
}
if (tileProvider != null) {
tileProvider.setTileCachePath(tileCachePath);
}
setCustomTileProviderMode();
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setTileCacheMaxAge(float tileCacheMaxAge) {
this.tileCacheMaxAge = tileCacheMaxAge;
if (tileProvider != null) {
tileProvider.setTileCacheMaxAge((int)tileCacheMaxAge);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setOfflineMode(boolean offlineMode) {
this.offlineMode = offlineMode;
if (tileProvider != null) {
tileProvider.setOfflineMode(offlineMode);
}
if (tileOverlay != null) {
tileOverlay.clearTileCache();
}
}
public void setOpacity(float opacity) {
this.opacity = opacity;
if (tileOverlay != null) {
tileOverlay.setTransparency(1 - opacity);
}
}
public TileOverlayOptions getTileOverlayOptions() {
if (tileOverlayOptions == null) {
tileOverlayOptions = createTileOverlayOptions();
}
return tileOverlayOptions;
}
protected void setCustomTileProviderMode() {
Log.d("urlTile ", "creating new mode TileProvider");
this.customTileProviderNeeded = true;
if (tileProvider != null) {
tileProvider.setCustomMode();
}
}
protected TileOverlayOptions createTileOverlayOptions() {
Log.d("urlTile ", "creating TileProvider");
TileOverlayOptions options = new TileOverlayOptions();
options.zIndex(zIndex);
options.transparency(1 - this.opacity);
this.tileProvider = new MapTileProvider((int)this.tileSize, this.doubleTileSize, this.urlTemplate,
(int)this.maximumZ, (int)this.maximumNativeZ, (int)this.minimumZ, this.flipY, this.tileCachePath,
(int)this.tileCacheMaxAge, this.offlineMode, this.context, this.customTileProviderNeeded);
options.tileProvider(this.tileProvider);
return options;
}
@Override
public Object getFeature() {
return tileOverlay;
}
@Override
public void addToMap(Object map) {
this.tileOverlay = ((GoogleMap) map).addTileOverlay(getTileOverlayOptions());
}
@Override
public void removeFromMap(Object map) {
tileOverlay.remove();
}
}
@@ -0,0 +1,91 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
public class MapUrlTileManager extends ViewGroupManager<MapUrlTile> {
public MapUrlTileManager(ReactApplicationContext reactContext) {
super();
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapUrlTile";
}
@Override
public MapUrlTile createViewInstance(ThemedReactContext context) {
return new MapUrlTile(context);
}
@ReactProp(name = "urlTemplate")
public void setUrlTemplate(MapUrlTile view, String urlTemplate) {
view.setUrlTemplate(urlTemplate);
}
@ReactProp(name = "zIndex", defaultFloat = -1.0f)
public void setZIndex(MapUrlTile view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "minimumZ", defaultFloat = 0.0f)
public void setMinimumZ(MapUrlTile view, float minimumZ) {
view.setMinimumZ(minimumZ);
}
@ReactProp(name = "maximumZ", defaultFloat = 100.0f)
public void setMaximumZ(MapUrlTile view, float maximumZ) {
view.setMaximumZ(maximumZ);
}
@ReactProp(name = "maximumNativeZ", defaultFloat = 100.0f)
public void setMaximumNativeZ(MapUrlTile view, float maximumNativeZ) {
view.setMaximumNativeZ(maximumNativeZ);
}
@ReactProp(name = "flipY", defaultBoolean = false)
public void setFlipY(MapUrlTile view, boolean flipY) {
view.setFlipY(flipY);
}
@ReactProp(name = "tileSize", defaultFloat = 256.0f)
public void setTileSize(MapUrlTile view, float tileSize) {
view.setTileSize(tileSize);
}
@ReactProp(name = "doubleTileSize", defaultBoolean = false)
public void setDoubleTileSize(MapUrlTile view, boolean doubleTileSize) {
view.setDoubleTileSize(doubleTileSize);
}
@ReactProp(name = "tileCachePath")
public void setTileCachePath(MapUrlTile view, String tileCachePath) {
view.setTileCachePath(tileCachePath);
}
@ReactProp(name = "tileCacheMaxAge", defaultFloat = 0.0f)
public void setTileCacheMaxAge(MapUrlTile view, float tileCacheMaxAge) {
view.setTileCacheMaxAge(tileCacheMaxAge);
}
@ReactProp(name = "offlineMode", defaultBoolean = false)
public void setOfflineMode(MapUrlTile view, boolean offlineMode) {
view.setOfflineMode(offlineMode);
}
@ReactProp(name = "opacity", defaultFloat = 1.0f)
public void setOpacity(MapUrlTile view, float opacity) {
view.setOpacity(opacity);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,94 @@
package com.rnmaps.maps;
import android.content.Context;
import com.google.android.gms.maps.model.TileOverlayOptions;
import com.google.android.gms.maps.model.UrlTileProvider;
import java.net.MalformedURLException;
import java.net.URL;
public class MapWMSTile extends MapUrlTile {
private static final double[] mapBound = {-20037508.34789244, 20037508.34789244};
private static final double FULL = 20037508.34789244 * 2;
class AIRMapGSUrlTileProvider extends MapTileProvider {
class AIRMapWMSTileProvider extends UrlTileProvider {
private String urlTemplate;
private final int tileSize;
public AIRMapWMSTileProvider(int width, int height, String urlTemplate) {
super(width, height);
this.urlTemplate = urlTemplate;
this.tileSize = width;
}
@Override
public URL getTileUrl(int x, int y, int zoom) {
if(MapWMSTile.this.maximumZ > 0 && zoom > maximumZ) {
return null;
}
if(MapWMSTile.this.minimumZ > 0 && zoom < minimumZ) {
return null;
}
double[] bb = getBoundingBox(x, y, zoom);
String s = this.urlTemplate
.replace("{minX}", Double.toString(bb[0]))
.replace("{minY}", Double.toString(bb[1]))
.replace("{maxX}", Double.toString(bb[2]))
.replace("{maxY}", Double.toString(bb[3]))
.replace("{width}", Integer.toString(this.tileSize))
.replace("{height}", Integer.toString(this.tileSize));
URL url = null;
try {
url = new URL(s);
} catch (MalformedURLException e) {
throw new AssertionError(e);
}
return url;
}
private double[] getBoundingBox(int x, int y, int zoom) {
double tile = FULL / Math.pow(2, zoom);
return new double[]{
mapBound[0] + x * tile,
mapBound[1] - (y + 1) * tile,
mapBound[0] + (x + 1) * tile,
mapBound[1] - y * tile
};
}
public void setUrlTemplate(String urlTemplate) {
this.urlTemplate = urlTemplate;
}
}
public AIRMapGSUrlTileProvider(int tileSizet, String urlTemplate,
int maximumZ, int maximumNativeZ, int minimumZ, String tileCachePath,
int tileCacheMaxAge, boolean offlineMode, Context context, boolean customMode) {
super(tileSizet, false, urlTemplate, maximumZ, maximumNativeZ, minimumZ, false,
tileCachePath, tileCacheMaxAge, offlineMode, context, customMode);
this.tileProvider = new AIRMapWMSTileProvider(tileSizet, tileSizet, urlTemplate);
}
}
public MapWMSTile(Context context) {
super(context);
}
@Override
protected TileOverlayOptions createTileOverlayOptions() {
TileOverlayOptions options = new TileOverlayOptions();
options.zIndex(zIndex);
options.transparency(1 - this.opacity);
AIRMapGSUrlTileProvider tileProvider = new AIRMapGSUrlTileProvider((int) this.tileSize, this.urlTemplate,
(int) this.maximumZ, (int) this.maximumNativeZ, (int) this.minimumZ, this.tileCachePath,
(int) this.tileCacheMaxAge, this.offlineMode, this.context, this.customTileProviderNeeded);
options.tileProvider(tileProvider);
return options;
}
}
@@ -0,0 +1,81 @@
package com.rnmaps.maps;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.WindowManager;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.ViewGroupManager;
import com.facebook.react.uimanager.annotations.ReactProp;
public class MapWMSTileManager extends ViewGroupManager<MapWMSTile> {
public MapWMSTileManager(ReactApplicationContext reactContext) {
super();
DisplayMetrics metrics = new DisplayMetrics();
((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE))
.getDefaultDisplay()
.getRealMetrics(metrics);
}
@Override
public String getName() {
return "AIRMapWMSTile";
}
@Override
public MapWMSTile createViewInstance(ThemedReactContext context) {
return new MapWMSTile(context);
}
@ReactProp(name = "urlTemplate")
public void setUrlTemplate(MapWMSTile view, String urlTemplate) {
view.setUrlTemplate(urlTemplate);
}
@ReactProp(name = "zIndex", defaultFloat = -1.0f)
public void setZIndex(MapWMSTile view, float zIndex) {
view.setZIndex(zIndex);
}
@ReactProp(name = "minimumZ", defaultFloat = 0.0f)
public void setMinimumZ(MapWMSTile view, float minimumZ) {
view.setMinimumZ(minimumZ);
}
@ReactProp(name = "maximumZ", defaultFloat = 100.0f)
public void setMaximumZ(MapWMSTile view, float maximumZ) {
view.setMaximumZ(maximumZ);
}
@ReactProp(name = "maximumNativeZ", defaultFloat = 100.0f)
public void setMaximumNativeZ(MapWMSTile view, float maximumNativeZ) {
view.setMaximumNativeZ(maximumNativeZ);
}
@ReactProp(name = "tileSize", defaultFloat = 256.0f)
public void setTileSize(MapWMSTile view, float tileSize) {
view.setTileSize(tileSize);
}
@ReactProp(name = "tileCachePath")
public void setTileCachePath(MapWMSTile view, String tileCachePath) {
view.setTileCachePath(tileCachePath);
}
@ReactProp(name = "tileCacheMaxAge", defaultFloat = 0.0f)
public void setTileCacheMaxAge(MapWMSTile view, float tileCacheMaxAge) {
view.setTileCacheMaxAge(tileCacheMaxAge);
}
@ReactProp(name = "offlineMode", defaultBoolean = false)
public void setOfflineMode(MapWMSTile view, boolean offlineMode) {
view.setOfflineMode(offlineMode);
}
@ReactProp(name = "opacity", defaultFloat = 1.0f)
public void setOpacity(MapWMSTile view, float opacity) {
view.setOpacity(opacity);
}
}
@@ -0,0 +1,45 @@
package com.rnmaps.maps;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;
import java.util.ArrayList;
import java.util.List;
public class MapsPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
modules.add(new MapModule(reactContext));
return modules;
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
MapManager mapManager = new MapManager(reactContext);
MapMarkerManager annotationManager = new MapMarkerManager();
mapManager.setMarkerManager(annotationManager);
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(mapManager);
viewManagers.add(annotationManager);
viewManagers.add(new MapCalloutManager());
viewManagers.add(new MapPolylineManager(reactContext));
viewManagers.add(new MapGradientPolylineManager(reactContext));
viewManagers.add(new MapPolygonManager(reactContext));
viewManagers.add(new MapCircleManager(reactContext));
viewManagers.add(new MapUrlTileManager(reactContext));
viewManagers.add(new MapWMSTileManager(reactContext));
viewManagers.add(new MapLocalTileManager(reactContext));
viewManagers.add(new MapOverlayManager(reactContext));
viewManagers.add(new MapHeatmapManager());
return viewManagers;
}
}
@@ -0,0 +1,48 @@
package com.rnmaps.maps;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.bridge.WritableNativeMap;
import com.facebook.react.uimanager.events.Event;
import com.facebook.react.uimanager.events.RCTEventEmitter;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.LatLngBounds;
public class RegionChangeEvent extends Event<RegionChangeEvent> {
private final LatLngBounds bounds;
private final boolean continuous;
private final boolean isGesture;
public RegionChangeEvent(int id, LatLngBounds bounds, boolean continuous, boolean isGesture) {
super(id);
this.bounds = bounds;
this.continuous = continuous;
this.isGesture = isGesture;
}
@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();
LatLng center = bounds.getCenter();
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);
event.putBoolean("isGesture", isGesture);
rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event);
}
}
@@ -0,0 +1,31 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.rnmaps.maps;
import com.facebook.react.uimanager.LayoutShadowNode;
import com.facebook.react.uimanager.UIViewOperationQueue;
import java.util.HashMap;
import java.util.Map;
// Custom LayoutShadowNode implementation used in conjunction with the AirMapManager
// 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);
Map<String, Float> data = new HashMap<>();
data.put("width", getLayoutWidth());
data.put("height", getLayoutHeight());
uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data);
}
}
@@ -0,0 +1,26 @@
package com.rnmaps.maps;
import android.content.Context;
import android.graphics.Rect;
import com.facebook.react.views.view.ReactViewGroup;
public class ViewAttacherGroup extends ReactViewGroup {
public ViewAttacherGroup(Context context) {
super(context);
this.setWillNotDraw(true);
this.setVisibility(VISIBLE);
this.setAlpha(0.0f);
this.setRemoveClippedSubviews(false);
this.setClipBounds(new Rect(0, 0, 0, 0));
this.setOverflow("hidden"); // Change to ViewProps.HIDDEN until RN 0.57 is base
}
// This should make it more performant, avoid trying to hard to overlap layers with opacity.
@Override
public boolean hasOverlappingRendering() {
return false;
}
}
@@ -0,0 +1,76 @@
package com.rnmaps.maps;
import android.os.Handler;
import android.os.Looper;
import java.util.LinkedList;
public class ViewChangesTracker {
private static ViewChangesTracker instance;
private final Handler handler;
private final LinkedList<MapMarker> markers = new LinkedList<>();
private boolean hasScheduledFrame = false;
private final Runnable updateRunnable;
private final long fps = 40;
private ViewChangesTracker() {
handler = new Handler(Looper.myLooper());
updateRunnable = new Runnable() {
@Override
public void run() {
update();
if (markers.size() > 0) {
handler.postDelayed(updateRunnable, fps);
} else {
hasScheduledFrame = false;
}
}
};
}
static ViewChangesTracker getInstance() {
if (instance == null) {
synchronized (ViewChangesTracker.class) {
instance = new ViewChangesTracker();
}
}
return instance;
}
public void addMarker(MapMarker marker) {
markers.add(marker);
if (!hasScheduledFrame) {
hasScheduledFrame = true;
handler.postDelayed(updateRunnable, fps);
}
}
public void removeMarker(MapMarker marker) {
markers.remove(marker);
}
public boolean containsMarker(MapMarker marker) {
return markers.contains(marker);
}
private final LinkedList<MapMarker> markersToRemove = new LinkedList<>();
public void update() {
for (MapMarker marker : markers) {
if (!marker.updateCustomForTracking()) {
markersToRemove.add(marker);
}
}
// Remove markers that are not active anymore
if (markersToRemove.size() > 0) {
markers.removeAll(markersToRemove);
markersToRemove.clear();
}
}
}
@@ -0,0 +1,18 @@
//
// AIRDummyView.h
// AirMapsExplorer
//
// Created by Gil Birman on 10/4/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
@interface AIRDummyView : UIView
@property (nonatomic, weak) UIView *view;
- (instancetype)initWithView:(UIView*)view;
@end
#endif
@@ -0,0 +1,23 @@
//
// AIRDummyView.m
// AirMapsExplorer
//
// Created by Gil Birman on 10/4/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import "AIRDummyView.h"
@implementation AIRDummyView
- (instancetype)initWithView:(UIView*)view
{
if ((self = [super init])) {
self.view = view;
}
return self;
}
@end
#endif
@@ -0,0 +1,29 @@
//
// AIRGMSMarker.h
// AirMaps
//
// Created by Gil Birman on 9/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import <React/UIView+React.h>
@class AIRGoogleMapMarker;
@interface AIRGMSMarker : GMSMarker
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, weak) AIRGoogleMapMarker *fakeMarker;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTDirectEventBlock onSelect;
@property (nonatomic, copy) RCTDirectEventBlock onDeselect;
@end
@protocol AIRGMSMarkerDelegate <NSObject>
@required
-(void)didTapMarker;
@end
#endif
@@ -0,0 +1,16 @@
//
// AIRGMSMarker.m
// AirMaps
//
// Created by Gil Birman on 9/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGMSMarker.h"
@implementation AIRGMSMarker
@end
#endif
@@ -0,0 +1,20 @@
//
// AIRGMSPolygon.h
// AirMaps
//
// Created by Gerardo Pacheco 02/05/2017.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import <React/UIView+React.h>
@class AIRGoogleMapPolygon;
@interface AIRGMSPolygon : GMSPolygon
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end
#endif
@@ -0,0 +1,16 @@
//
// AIRGMSPolygon.m
// AirMaps
//
// Created by Gerardo Pacheco 02/05/2017.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGMSPolygon.h"
@implementation AIRGMSPolygon
@end
#endif
@@ -0,0 +1,20 @@
//
// AIRGMSPolyline.h
// AirMaps
//
// Created by Guilherme Pontes 04/05/2017.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import <React/UIView+React.h>
@class AIRGoogleMapPolyline;
@interface AIRGMSPolyline : GMSPolyline
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end
#endif
@@ -0,0 +1,15 @@
//
// AIRGMSPolyline.m
// AirMaps
//
// Created by Guilherme Pontes 04/05/2017.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGMSPolyline.h"
@implementation AIRGMSPolyline
@end
#endif
@@ -0,0 +1,89 @@
//
// AIRGoogleMap.h
// AirMaps
//
// Created by Gil Birman on 9/1/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import <React/RCTComponent.h>
#import <React/RCTBridge.h>
#import <GoogleMaps/GoogleMaps.h>
#import <MapKit/MapKit.h>
#import "AIRGMSMarker.h"
#import "RCTConvert+AirMap.h"
@interface AIRGoogleMap : GMSMapView
// TODO: don't use MK region?
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, assign) MKCoordinateRegion initialRegion;
@property (nonatomic, assign) MKCoordinateRegion region;
@property (nonatomic, assign) GMSCameraPosition *cameraProp; // Because the base class already has a "camera" prop.
@property (nonatomic, strong) GMSCameraPosition *initialCamera;
@property (nonatomic, assign) NSString *customMapStyleString;
@property (nonatomic, assign) UIEdgeInsets mapPadding;
@property (nonatomic, assign) NSString *paddingAdjustmentBehaviorString;
@property (nonatomic, copy) RCTBubblingEventBlock onMapReady;
@property (nonatomic, copy) RCTBubblingEventBlock onMapLoaded;
@property (nonatomic, copy) RCTBubblingEventBlock onKmlReady;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, copy) RCTBubblingEventBlock onLongPress;
@property (nonatomic, copy) RCTBubblingEventBlock onPanDrag;
@property (nonatomic, copy) RCTBubblingEventBlock onUserLocationChange;
@property (nonatomic, copy) RCTBubblingEventBlock onMarkerPress;
@property (nonatomic, copy) RCTBubblingEventBlock onMarkerSelect;
@property (nonatomic, copy) RCTBubblingEventBlock onMarkerDeselect;
@property (nonatomic, copy) RCTBubblingEventBlock onChange;
@property (nonatomic, copy) RCTBubblingEventBlock onPoiClick;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChangeStart;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChange;
@property (nonatomic, copy) RCTDirectEventBlock onRegionChangeComplete;
@property (nonatomic, copy) RCTDirectEventBlock onIndoorLevelActivated;
@property (nonatomic, copy) RCTDirectEventBlock onIndoorBuildingFocused;
@property (nonatomic, strong) NSMutableArray *markers;
@property (nonatomic, strong) NSMutableArray *polygons;
@property (nonatomic, strong) NSMutableArray *polylines;
@property (nonatomic, strong) NSMutableArray *circles;
@property (nonatomic, strong) NSMutableArray *heatmaps;
@property (nonatomic, strong) NSMutableArray *tiles;
@property (nonatomic, strong) NSMutableArray *overlays;
@property (nonatomic, assign) BOOL showsBuildings;
@property (nonatomic, assign) BOOL showsTraffic;
@property (nonatomic, assign) BOOL showsCompass;
@property (nonatomic, assign) BOOL scrollEnabled;
@property (nonatomic, assign) BOOL zoomEnabled;
@property (nonatomic, assign) BOOL rotateEnabled;
@property (nonatomic, assign) BOOL scrollDuringRotateOrZoomEnabled;
@property (nonatomic, assign) BOOL pitchEnabled;
@property (nonatomic, assign) BOOL zoomTapEnabled;
@property (nonatomic, assign) BOOL showsUserLocation;
@property (nonatomic, assign) BOOL showsMyLocationButton;
@property (nonatomic, assign) BOOL showsIndoors;
@property (nonatomic, assign) BOOL showsIndoorLevelPicker;
@property (nonatomic, assign) NSString *kmlSrc;
- (void)didPrepareMap;
- (void)mapViewDidFinishTileRendering;
- (BOOL)didTapMarker:(GMSMarker *)marker;
- (void)didTapPolyline:(GMSPolyline *)polyline;
- (void)didTapPolygon:(GMSPolygon *)polygon;
- (void)didTapAtCoordinate:(CLLocationCoordinate2D)coordinate;
- (void)didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate;
- (void)willMove:(BOOL)gesture;
- (void)didChangeCameraPosition:(GMSCameraPosition *)position isGesture:(BOOL)isGesture;
- (void)idleAtCameraPosition:(GMSCameraPosition *)position isGesture:(BOOL)isGesture;
- (void)didTapPOIWithPlaceID:(NSString *)placeID name:(NSString *) name location:(CLLocationCoordinate2D) location;
- (NSArray *)getMapBoundaries;
+ (MKCoordinateRegion)makeGMSCameraPositionFromMap:(GMSMapView *)map andGMSCameraPosition:(GMSCameraPosition *)position;
+ (GMSCameraPosition*)makeGMSCameraPositionFromMap:(GMSMapView *)map andMKCoordinateRegion:(MKCoordinateRegion)region;
- (NSDictionary*) getMarkersFramesWithOnlyVisible:(BOOL)onlyVisible;
- (instancetype)initWithMapId:(NSString *)mapId initialCamera:(GMSCameraPosition*) camera backgroundColor:(UIColor *) backgroundColor andZoomTapEnabled:(BOOL)zoomTapEnabled;
@end
#endif
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,23 @@
//
// AIRGoogleMapCallout.h
// AirMaps
//
// Created by Gil Birman on 9/6/16.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
@interface AIRGoogleMapCallout : UIView
@property (nonatomic, assign) BOOL tooltip;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, assign) BOOL alphaHitTest;
- (BOOL) isPointInside:(CGPoint)pointInCallout;
@end
#endif
@@ -0,0 +1,39 @@
//
// AIRGoogleMapCallout.m
// AirMaps
//
// Created by Gil Birman on 9/6/16.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapCallout.h"
#import <React/RCTUtils.h>
#import <React/RCTView.h>
#import <React/RCTBridge.h>
@implementation AIRGoogleMapCallout
- (BOOL) isPointInside:(CGPoint)pointInCallout {
if (!self.alphaHitTest)
return TRUE;
CGFloat alpha = [self alphaOfPoint:pointInCallout];
return alpha >= 0.01;
}
- (CGFloat) alphaOfPoint:(CGPoint)point {
unsigned char pixel[4] = {0};
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(pixel, 1, 1, 8, 4, colorSpace, kCGBitmapAlphaInfoMask & kCGImageAlphaPremultipliedLast);
CGContextTranslateCTM(context, -point.x, -point.y);
[self.layer renderInContext:context];
CGContextRelease(context);
CGColorSpaceRelease(colorSpace);
return pixel[3]/255.0;
}
@end
#endif
@@ -0,0 +1,17 @@
//
// AIRGoogleMapCalloutManager.h
// AirMaps
//
// Created by Gil Birman on 9/6/16.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapCalloutManager : RCTViewManager
@end
#endif
@@ -0,0 +1,30 @@
//
// AIRGoogleMapCalloutManager.m
// AirMaps
//
// Created by Gil Birman on 9/6/16.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapCalloutManager.h"
#import "AIRGoogleMapCallout.h"
#import <React/RCTView.h>
@implementation AIRGoogleMapCalloutManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapCallout *callout = [AIRGoogleMapCallout new];
return callout;
}
RCT_EXPORT_VIEW_PROPERTY(tooltip, BOOL)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(alphaHitTest, BOOL)
@end
#endif
@@ -0,0 +1,18 @@
//
// AIRGoogleMapCalloutSubview.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import <React/RCTView.h>
@interface AIRGoogleMapCalloutSubview : UIView
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@end
#endif
@@ -0,0 +1,19 @@
//
// AIRGoogleMapCalloutSubview.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapCalloutSubview.h"
#import <React/RCTUtils.h>
#import <React/RCTView.h>
#import <React/RCTBridge.h>
@implementation AIRGoogleMapCalloutSubview
@end
#endif
@@ -0,0 +1,17 @@
//
// AIRGoogleMapCalloutSubviewManager.h
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapCalloutSubviewManager : RCTViewManager
@end
#endif
@@ -0,0 +1,28 @@
//
// AIRGoogleMapCalloutSubviewManager.m
// AirMaps
//
// Created by Denis Oblogin on 10/8/18.
//
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapCalloutSubviewManager.h"
#import "AIRGoogleMapCalloutSubview.h"
#import <React/RCTView.h>
@implementation AIRGoogleMapCalloutSubviewManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapCalloutSubview *calloutSubview = [AIRGoogleMapCalloutSubview new];
return calloutSubview;
}
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
@end
#endif
@@ -0,0 +1,24 @@
//
// AIRGoogleMapsCircle.h
//
// Created by Nick Italiano on 10/24/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import "AIRMapCoordinate.h"
@interface AIRGoogleMapCircle : UIView
@property (nonatomic, strong) GMSCircle *circle;
@property (nonatomic, assign) double radius;
@property (nonatomic, assign) CLLocationCoordinate2D centerCoordinate;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) double strokeWidth;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, assign) int zIndex;
@end
#endif
@@ -0,0 +1,88 @@
//
// AIRGoogleMapsCircle.m
//
// Created by Nick Italiano on 10/24/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import "AIRGoogleMapCircle.h"
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTUtils.h>
@implementation AIRGoogleMapCircle
{
BOOL _didMoveToWindow;
}
- (instancetype)init
{
if (self = [super init]) {
_didMoveToWindow = false;
_circle = [[GMSCircle alloc] init];
_circle.fillColor = _fillColor;
_circle.strokeColor = _strokeColor;
}
return self;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if(_didMoveToWindow) return;
_didMoveToWindow = true;
if(_fillColor) {
_circle.fillColor = _fillColor;
}
if(_strokeColor) {
_circle.strokeColor = _strokeColor;
}
if(_strokeWidth) {
_circle.strokeWidth = _strokeWidth;
}
}
- (void)setRadius:(double)radius
{
_radius = radius;
_circle.radius = radius;
}
- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate
{
_centerCoordinate = centerCoordinate;
_circle.position = centerCoordinate;
}
-(void)setStrokeColor:(UIColor *)strokeColor
{
_strokeColor = strokeColor;
if(_didMoveToWindow) {
_circle.strokeColor = strokeColor;
}
}
-(void)setStrokeWidth:(double)strokeWidth
{
_strokeWidth = strokeWidth;
if(_didMoveToWindow) {
_circle.strokeWidth = strokeWidth;
}
}
-(void)setFillColor:(UIColor *)fillColor
{
_fillColor = fillColor;
if(_didMoveToWindow) {
_circle.fillColor = fillColor;
}
}
-(void)setZIndex:(int)zIndex
{
_zIndex = zIndex;
_circle.zIndex = zIndex;
}
@end
#endif
@@ -0,0 +1,15 @@
//
// AIRGoogleMapCircleManager.h
//
// Created by Nick Italiano on 10/24/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapCircleManager : RCTViewManager
@end
#endif
@@ -0,0 +1,37 @@
//
// AIRGoogleMapCircleManager.m
//
// Created by Nick Italiano on 10/24/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapCircleManager.h"
#import "AIRGoogleMapCircle.h"
#import <React/RCTBridge.h>
#import <React/UIView+React.h>
@interface AIRGoogleMapCircleManager()
@end
@implementation AIRGoogleMapCircleManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapCircle *circle = [AIRGoogleMapCircle new];
return circle;
}
RCT_EXPORT_VIEW_PROPERTY(radius, double)
RCT_REMAP_VIEW_PROPERTY(center, centerCoordinate, CLLocationCoordinate2D)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, double)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(zIndex, int)
@end
#endif
@@ -0,0 +1,17 @@
//
// AIRGoogleMapHeatmap.h
//
// Created by David Cako on 29 April 2018.
//
#import "GMUHeatmapTileLayer.h"
@interface AIRGoogleMapHeatmap : UIView
@property (nonatomic, strong) GMUHeatmapTileLayer *heatmap;
@property (nonatomic, strong) NSMutableArray<GMUWeightedLatLng *> *points;
@property (nonatomic, assign) NSUInteger radius;
@property (nonatomic, assign) float opacity;
@property (nonatomic, assign) GMUGradient *gradient;
@end
@@ -0,0 +1,64 @@
//
// AIRGoogleMapHeatmap.m
//
// Created by David Cako on 29 April 2018.
//
#import <UIKit/UIKit.h>
#import "AIRGoogleMapHeatmap.h"
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
@implementation AIRGoogleMapHeatmap
- (instancetype)init
{
if (self = [super init]) {
_heatmap = [[GMUHeatmapTileLayer alloc] init];
}
return self;
}
- (void)setPoints:(NSArray<NSDictionary *> *)points
{
NSMutableArray<GMUWeightedLatLng *> *w = [NSMutableArray arrayWithCapacity:points.count];
for (int i = 0; i < points.count; i++) {
CLLocationCoordinate2D coord = [RCTConvert CLLocationCoordinate2D:points[i]];
float intensity = 1.0;
if (points[i][@"weight"] != nil) {
intensity = [RCTConvert float:points[i][@"weight"]];
}
[w addObject:[[GMUWeightedLatLng alloc] initWithCoordinate:coord intensity:intensity]];
}
_points = w;
[self.heatmap setWeightedData:w];
[self.heatmap clearTileCache];
[self.heatmap setMap:self.heatmap.map];
}
- (void)setRadius:(NSUInteger)radius
{
_radius = radius;
[self.heatmap setRadius:radius];
}
- (void)setOpacity:(float)opacity
{
_opacity = opacity;
[self.heatmap setOpacity:opacity];
}
- (void)setGradient:(NSDictionary *)gradient
{
NSArray<UIColor *> *colors = [RCTConvert UIColorArray:gradient[@"colors"]];
NSArray<NSNumber *> *colorStartPoints = [RCTConvert NSNumberArray:gradient[@"startPoints"]];
NSUInteger colorMapSize = [RCTConvert NSUInteger:gradient[@"colorMapSize"]];
GMUGradient *gmuGradient = [[GMUGradient alloc] initWithColors:colors
startPoints:colorStartPoints
colorMapSize:colorMapSize];
_gradient = gmuGradient;
[self.heatmap setGradient:gmuGradient];
}
@end
@@ -0,0 +1,11 @@
//
// AIRGoogleMapHeatmapManager.h
//
// Created by David Cako on 29 April 2018.
//
#import <React/RCTViewManager.h>
@interface AIRGoogleMapHeatmapManager : RCTViewManager
@end
@@ -0,0 +1,32 @@
//
// AIRGoogleMapHeatmapManager.m
//
// Created by David Cako on 29 April 2018.
//
#import "AIRGoogleMapHeatmapManager.h"
#import "AIRGoogleMapHeatmap.h"
#import "AIRGoogleMap.h"
#import <React/RCTBridge.h>
#import <React/UIView+React.h>
@interface AIRGoogleMapHeatmapManager()
@end
@implementation AIRGoogleMapHeatmapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapHeatmap *heatmap = [AIRGoogleMapHeatmap new];
return heatmap;
}
RCT_EXPORT_VIEW_PROPERTY(points, NSArray<NSDictionary *>)
RCT_EXPORT_VIEW_PROPERTY(radius, NSUInteger)
RCT_EXPORT_VIEW_PROPERTY(opacity, float)
RCT_EXPORT_VIEW_PROPERTY(gradient, NSDictionary *)
@end
@@ -0,0 +1,20 @@
//
// AIRGoogleMapManager.h
// AirMaps
//
// Created by Gil Birman on 9/1/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapManager : RCTViewManager
@property (nonatomic, strong) NSDictionary *initialProps;
@property (nonatomic) BOOL isGesture;
@end
#endif
@@ -0,0 +1,622 @@
//
// AIRGoogleMapManager.m
// AirMaps
//
// Created by Gil Birman on 9/1/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapManager.h"
#import <React/RCTViewManager.h>
#import <React/RCTBridge.h>
#import <React/RCTUIManager.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/RCTConvert.h>
#import <React/UIView+React.h>
#import "RCTConvert+GMSMapViewType.h"
#import "AIRGoogleMap.h"
#import "AIRMapMarker.h"
#import "AIRMapPolyline.h"
#import "AIRMapPolygon.h"
#import "AIRMapCircle.h"
#import "SMCalloutView.h"
#import "AIRGoogleMapMarker.h"
#import "RCTConvert+AirMap.h"
#import <MapKit/MapKit.h>
#import <QuartzCore/QuartzCore.h>
static NSString *const RCTMapViewKey = @"MapView";
@interface AIRGoogleMapManager() <GMSMapViewDelegate>
{
BOOL didCallOnMapReady;
}
@end
@implementation AIRGoogleMapManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
NSString* googleMapId = nil;
BOOL zoomTapEnabled = YES;
UIColor* backgroundColor = nil;
GMSCameraPosition* camera = nil;
if (self.initialProps){
if (self.initialProps[@"googleMapId"]){
googleMapId = self.initialProps[@"googleMapId"];
}
if (self.initialProps[@"zoomTapEnabled"]){
zoomTapEnabled = self.initialProps[@"zoomTapEnabled"];
}
if (self.initialProps[@"loadingBackgroundColor"]){
backgroundColor = [RCTConvert UIColor:self.initialProps[@"loadingBackgroundColor"]];
}
if (self.initialProps[@"initialCamera"]){
camera = [RCTConvert GMSCameraPositionWithDefaults:self.initialProps[@"initialCamera"] existingCamera:nil];
}
}
AIRGoogleMap *map = [[AIRGoogleMap alloc] initWithMapId:googleMapId initialCamera:camera backgroundColor:backgroundColor andZoomTapEnabled:zoomTapEnabled];
map.bridge = self.bridge;
map.delegate = self;
map.isAccessibilityElement = NO;
map.accessibilityElementsHidden = NO;
map.settings.consumesGesturesInView = NO;
UIPanGestureRecognizer *drag = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDrag:)];
[drag setMinimumNumberOfTouches:1];
[drag setMaximumNumberOfTouches:1];
[map addGestureRecognizer:drag];
UIPinchGestureRecognizer *pinch = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapDrag:)];
[map addGestureRecognizer:pinch];
return map;
}
RCT_EXPORT_VIEW_PROPERTY(isAccessibilityElement, BOOL)
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
RCT_EXPORT_VIEW_PROPERTY(googleMapId, NSString)
RCT_EXPORT_VIEW_PROPERTY(initialCamera, GMSCameraPosition)
RCT_REMAP_VIEW_PROPERTY(camera, cameraProp, GMSCameraPosition)
RCT_EXPORT_VIEW_PROPERTY(initialRegion, MKCoordinateRegion)
RCT_EXPORT_VIEW_PROPERTY(region, MKCoordinateRegion)
RCT_EXPORT_VIEW_PROPERTY(showsBuildings, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL)
//RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL) // Not supported by GoogleMaps
RCT_EXPORT_VIEW_PROPERTY(showsTraffic, BOOL)
RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(rotateEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(scrollDuringRotateOrZoomEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(pitchEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(zoomTapEnabled, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsMyLocationButton, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsIndoors, BOOL)
RCT_EXPORT_VIEW_PROPERTY(showsIndoorLevelPicker, BOOL)
RCT_EXPORT_VIEW_PROPERTY(customMapStyleString, NSString)
RCT_EXPORT_VIEW_PROPERTY(mapPadding, UIEdgeInsets)
RCT_REMAP_VIEW_PROPERTY(paddingAdjustmentBehavior, paddingAdjustmentBehaviorString, NSString)
RCT_EXPORT_VIEW_PROPERTY(onMapReady, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMapLoaded, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onKmlReady, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPanDrag, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onUserLocationChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onMarkerDeselect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChangeStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChange, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onRegionChangeComplete, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onPoiClick, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onIndoorLevelActivated, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onIndoorBuildingFocused, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(mapType, GMSMapViewType)
RCT_EXPORT_VIEW_PROPERTY(minZoomLevel, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(maxZoomLevel, CGFloat)
RCT_EXPORT_VIEW_PROPERTY(kmlSrc, NSString)
RCT_EXPORT_VIEW_PROPERTY(loadingBackgroundColor, UIColor)
RCT_EXPORT_METHOD(getCamera:(nonnull NSNumber *)reactTag
resolver: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
reject(@"Invalid argument", [NSString stringWithFormat:@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view], NULL);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
resolve(@{
@"center": @{
@"latitude": @(mapView.camera.target.latitude),
@"longitude": @(mapView.camera.target.longitude),
},
@"pitch": @(mapView.camera.viewingAngle),
@"heading": @(mapView.camera.bearing),
@"zoom": @(mapView.camera.zoom),
});
}
}];
}
RCT_EXPORT_METHOD(setCamera:(nonnull NSNumber *)reactTag
camera:(id)json)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
GMSCameraPosition *camera = [RCTConvert GMSCameraPositionWithDefaults:json existingCamera:[mapView cameraProp]];
[mapView setCameraProp:camera];
}
}];
}
RCT_EXPORT_METHOD(animateCamera:(nonnull NSNumber *)reactTag
withCamera:(id)json
withDuration:(CGFloat)duration)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *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;
GMSCameraPosition *camera = [RCTConvert GMSCameraPositionWithDefaults:json existingCamera:[mapView cameraProp]];
[mapView animateToCameraPosition:camera];
[CATransaction commit];
}
}];
}
RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag
withRegion:(MKCoordinateRegion)region
withDuration:(CGFloat)duration)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
// 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(fitToElements:(nonnull NSNumber *)reactTag
edgePadding:(nonnull NSDictionary *)edgePadding
animated:(BOOL)animated)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
CLLocationCoordinate2D myLocation = ((AIRGoogleMapMarker *)(mapView.markers.firstObject)).realMarker.position;
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation];
for (AIRGoogleMapMarker *marker in mapView.markers)
bounds = [bounds includingCoordinate:marker.realMarker.position];
GMSCameraUpdate* cameraUpdate;
if ([edgePadding count] != 0) {
// Set Map viewport
CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]];
CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]];
CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]];
CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]];
cameraUpdate = [GMSCameraUpdate fitBounds:bounds withEdgeInsets:UIEdgeInsetsMake(top, left, bottom, right)];
} else {
cameraUpdate = [GMSCameraUpdate fitBounds:bounds withPadding:55.0f];
}
if (animated) {
[mapView animateWithCameraUpdate: cameraUpdate];
} else {
[mapView moveCamera: cameraUpdate];
}
}
}];
}
RCT_EXPORT_METHOD(fitToSuppliedMarkers:(nonnull NSNumber *)reactTag
markers:(nonnull NSArray *)markers
edgePadding:(nonnull NSDictionary *)edgePadding
animated:(BOOL)animated)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
NSPredicate *filterMarkers = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
AIRGoogleMapMarker *marker = (AIRGoogleMapMarker *)evaluatedObject;
return [marker isKindOfClass:[AIRGoogleMapMarker class]] && [markers containsObject:marker.identifier];
}];
NSArray *filteredMarkers = [mapView.markers filteredArrayUsingPredicate:filterMarkers];
CLLocationCoordinate2D myLocation = ((AIRGoogleMapMarker *)(filteredMarkers.firstObject)).realMarker.position;
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation];
for (AIRGoogleMapMarker *marker in filteredMarkers)
bounds = [bounds includingCoordinate:marker.realMarker.position];
// Set Map viewport
CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]];
CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]];
CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]];
CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]];
GMSCameraUpdate* cameraUpdate = [GMSCameraUpdate fitBounds:bounds withEdgeInsets:UIEdgeInsetsMake(top, left, bottom, right)];
if (animated) {
[mapView animateWithCameraUpdate:cameraUpdate
];
} else {
[mapView moveCamera: cameraUpdate];
}
}
}];
}
RCT_EXPORT_METHOD(fitToCoordinates:(nonnull NSNumber *)reactTag
coordinates:(nonnull NSArray<AIRMapCoordinate *> *)coordinates
edgePadding:(nonnull NSDictionary *)edgePadding
animated:(BOOL)animated)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
CLLocationCoordinate2D myLocation = coordinates.firstObject.coordinate;
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:myLocation coordinate:myLocation];
for (AIRMapCoordinate *coordinate in coordinates)
bounds = [bounds includingCoordinate:coordinate.coordinate];
// Set Map viewport
CGFloat top = [RCTConvert CGFloat:edgePadding[@"top"]];
CGFloat right = [RCTConvert CGFloat:edgePadding[@"right"]];
CGFloat bottom = [RCTConvert CGFloat:edgePadding[@"bottom"]];
CGFloat left = [RCTConvert CGFloat:edgePadding[@"left"]];
GMSCameraUpdate *cameraUpdate = [GMSCameraUpdate fitBounds:bounds withEdgeInsets:UIEdgeInsetsMake(top, left, bottom, right)];
if (animated) {
[mapView animateWithCameraUpdate: cameraUpdate];
} else {
[mapView moveCamera: cameraUpdate];
}
}
}];
}
RCT_EXPORT_METHOD(takeSnapshot:(nonnull NSNumber *)reactTag
withWidth:(nonnull NSNumber *)width
withHeight:(nonnull NSNumber *)height
withRegion:(MKCoordinateRegion)region
format:(nonnull NSString *)format
quality:(nonnull NSNumber *)quality
result:(nonnull NSString *)result
withCallback:(RCTResponseSenderBlock)callback)
{
NSTimeInterval timeStamp = [[NSDate date] timeIntervalSince1970];
NSString *pathComponent = [NSString stringWithFormat:@"Documents/snapshot-%.20lf.%@", timeStamp, format];
NSString *filePath = [NSHomeDirectory() stringByAppendingPathComponent: pathComponent];
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
// TODO: currently we are ignoring width, height, region
UIGraphicsBeginImageContextWithOptions(mapView.frame.size, YES, 0.0f);
[mapView.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
NSData *data;
if ([format isEqualToString:@"png"]) {
data = UIImagePNGRepresentation(image);
}
else if([format isEqualToString:@"jpg"]) {
data = UIImageJPEGRepresentation(image, quality.floatValue);
}
if ([result isEqualToString:@"file"]) {
[data writeToFile:filePath atomically:YES];
callback(@[[NSNull null], filePath]);
}
else if ([result isEqualToString:@"base64"]) {
callback(@[[NSNull null], [data base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]]);
}
}
UIGraphicsEndImageContext();
}];
}
RCT_EXPORT_METHOD(pointForCoordinate:(nonnull NSNumber *)reactTag
coordinate:(NSDictionary *)coordinate
resolver: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
CLLocationCoordinate2D coord =
CLLocationCoordinate2DMake(
[coordinate[@"latitude"] doubleValue],
[coordinate[@"longitude"] doubleValue]
);
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
CGPoint touchPoint = [mapView.projection pointForCoordinate:coord];
resolve(@{
@"x": @(touchPoint.x),
@"y": @(touchPoint.y),
});
}
}];
}
RCT_EXPORT_METHOD(coordinateForPoint:(nonnull NSNumber *)reactTag
point:(NSDictionary *)point
resolver: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
CGPoint pt = CGPointMake(
[point[@"x"] doubleValue],
[point[@"y"] doubleValue]
);
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
CLLocationCoordinate2D coordinate = [mapView.projection coordinateForPoint:pt];
resolve(@{
@"latitude": @(coordinate.latitude),
@"longitude": @(coordinate.longitude),
});
}
}];
}
RCT_EXPORT_METHOD(getMarkersFrames:(nonnull NSNumber *)reactTag
onlyVisible:(BOOL)onlyVisible
resolver: (RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
resolve([mapView getMarkersFramesWithOnlyVisible:onlyVisible]);
}
}];
}
RCT_EXPORT_METHOD(getMapBoundaries:(nonnull NSNumber *)reactTag
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
NSArray *boundingBox = [view getMapBoundaries];
resolve(@{
@"northEast" : @{
@"longitude" : boundingBox[0][0],
@"latitude" : boundingBox[0][1]
},
@"southWest" : @{
@"longitude" : boundingBox[1][0],
@"latitude" : boundingBox[1][1]
}
});
}
}];
}
RCT_EXPORT_METHOD(setMapBoundaries:(nonnull NSNumber *)reactTag
northEast:(CLLocationCoordinate2D)northEast
southWest:(CLLocationCoordinate2D)southWest)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
GMSCoordinateBounds *bounds = [[GMSCoordinateBounds alloc] initWithCoordinate:northEast coordinate:southWest];
mapView.cameraTargetBounds = bounds;
}
}];
}
RCT_EXPORT_METHOD(setIndoorActiveLevelIndex:(nonnull NSNumber *)reactTag
levelIndex:(NSInteger) levelIndex)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMap class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRGoogleMap, got: %@", view);
} else {
AIRGoogleMap *mapView = (AIRGoogleMap *)view;
if (!mapView.indoorDisplay) {
return;
}
if ( levelIndex < [mapView.indoorDisplay.activeBuilding.levels count]) {
mapView.indoorDisplay.activeLevel = mapView.indoorDisplay.activeBuilding.levels[levelIndex];
}
}
}];
}
+ (BOOL)requiresMainQueueSetup {
return YES;
}
- (NSDictionary *)constantsToExport {
return @{ @"legalNotice": [GMSServices openSourceLicenseInfo] };
}
- (void)mapView:(GMSMapView *)mapView willMove:(BOOL)gesture {
self.isGesture = gesture;
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView willMove:gesture];
}
- (void)mapViewDidStartTileRendering:(GMSMapView *)mapView {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didPrepareMap];
}
- (void)mapViewDidFinishTileRendering:(GMSMapView *)mapView {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView mapViewDidFinishTileRendering];
}
- (BOOL)mapView:(GMSMapView *)mapView didTapMarker:(GMSMarker *)marker {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
return [googleMapView didTapMarker:marker];
}
- (void)mapView:(GMSMapView *)mapView didTapOverlay:(GMSPolygon *)polygon {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didTapPolygon:polygon];
}
- (void)mapView:(GMSMapView *)mapView didTapAtCoordinate:(CLLocationCoordinate2D)coordinate {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didTapAtCoordinate:coordinate];
}
- (void)mapView:(GMSMapView *)mapView didLongPressAtCoordinate:(CLLocationCoordinate2D)coordinate {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didLongPressAtCoordinate:coordinate];
}
- (void)mapView:(GMSMapView *)mapView didChangeCameraPosition:(GMSCameraPosition *)position {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didChangeCameraPosition:position isGesture:self.isGesture];
}
- (void)mapView:(GMSMapView *)mapView idleAtCameraPosition:(GMSCameraPosition *)position {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView idleAtCameraPosition:position isGesture:self.isGesture];
}
- (UIView *)mapView:(GMSMapView *)mapView markerInfoWindow:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
return [aMarker.fakeMarker markerInfoWindow];}
- (UIView *)mapView:(GMSMapView *)mapView markerInfoContents:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
return [aMarker.fakeMarker markerInfoContents];
}
- (void)mapView:(GMSMapView *)mapView didTapInfoWindowOfMarker:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
[aMarker.fakeMarker didTapInfoWindowOfMarker:aMarker];
}
- (void)mapView:(GMSMapView *)mapView didBeginDraggingMarker:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
[aMarker.fakeMarker didBeginDraggingMarker:aMarker];
}
- (void)mapView:(GMSMapView *)mapView didEndDraggingMarker:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
[aMarker.fakeMarker didEndDraggingMarker:aMarker];
}
- (void)mapView:(GMSMapView *)mapView didDragMarker:(GMSMarker *)marker {
AIRGMSMarker *aMarker = (AIRGMSMarker *)marker;
[aMarker.fakeMarker didDragMarker:aMarker];
}
- (void)mapView:(GMSMapView *)mapView
didTapPOIWithPlaceID:(NSString *)placeID
name:(NSString *)name
location:(CLLocationCoordinate2D)location {
AIRGoogleMap *googleMapView = (AIRGoogleMap *)mapView;
[googleMapView didTapPOIWithPlaceID:placeID name:name location:location];
}
#pragma mark Gesture Recognizer Handlers
- (void)handleMapDrag:(UIPanGestureRecognizer*)recognizer {
AIRGoogleMap *map = (AIRGoogleMap *)recognizer.view;
if (!map.onPanDrag) return;
CGPoint touchPoint = [recognizer locationInView:map];
CLLocationCoordinate2D coord = [map.projection coordinateForPoint:touchPoint];
map.onPanDrag(@{
@"coordinate": @{
@"latitude": @(coord.latitude),
@"longitude": @(coord.longitude),
},
@"position": @{
@"x": @(touchPoint.x),
@"y": @(touchPoint.y),
},
@"numberOfTouches": @(recognizer.numberOfTouches),
});
}
@end
#endif
@@ -0,0 +1,58 @@
//
// AIRGoogleMapMarker.h
// AirMaps
//
// Created by Gil Birman on 9/2/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTBridge.h>
#import "AIRGMSMarker.h"
#import "AIRGoogleMap.h"
#import "AIRGoogleMapCallout.h"
#import "AIRGoogleMapCalloutSubview.h"
@interface AIRGoogleMapMarker : UIView
@property (nonatomic, weak) RCTBridge *bridge;
@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;
@property (nonatomic, copy) RCTDirectEventBlock onDrag;
@property (nonatomic, copy) RCTDirectEventBlock onDragEnd;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, copy) NSString *iconSrc;
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) NSString *subtitle;
@property (nonatomic, strong) UIColor *pinColor;
@property (nonatomic, assign) CGPoint anchor;
@property (nonatomic, assign) CGPoint calloutAnchor;
@property (nonatomic, assign) NSInteger zIndex;
@property (nonatomic, assign) double opacity;
@property (nonatomic, assign) BOOL draggable;
@property (nonatomic, assign) BOOL tappable;
@property (nonatomic, assign) BOOL tracksViewChanges;
@property (nonatomic, assign) BOOL tracksInfoWindowChanges;
- (void)showCalloutView;
- (void)hideCalloutView;
- (void)redraw;
- (UIView *)markerInfoContents;
- (UIView *)markerInfoWindow;
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker;
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker point:(CGPoint)point frame:(CGRect)frame;
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker subview:(AIRGoogleMapCalloutSubview*)subview point:(CGPoint)point frame:(CGRect)frame;
- (void)didBeginDraggingMarker:(AIRGMSMarker *)marker;
- (void)didEndDraggingMarker:(AIRGMSMarker *)marker;
- (void)didDragMarker:(AIRGMSMarker *)marker;
- (id)makeEventData;
- (id)makeEventData:(NSString *)action;
@end
#endif
@@ -0,0 +1,456 @@
//
// AIRGoogleMapMarker.m
// AirMaps
//
// Created by Gil Birman on 9/2/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapMarker.h"
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import "AIRGMSMarker.h"
#import "AIRGoogleMapCallout.h"
#import "AIRDummyView.h"
CGRect unionRect(CGRect a, CGRect b) {
return CGRectMake(
MIN(a.origin.x, b.origin.x),
MIN(a.origin.y, b.origin.y),
MAX(a.size.width, b.size.width),
MAX(a.size.height, b.size.height));
}
@interface AIRGoogleMapMarker ()
@end
@implementation AIRGoogleMapMarker {
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
__weak UIImageView *_iconImageView;
UIView *_iconView;
}
- (instancetype)init
{
if ((self = [super init])) {
_realMarker = [[AIRGMSMarker alloc] init];
_realMarker.fakeMarker = self;
_realMarker.tracksViewChanges = true;
_realMarker.tracksInfoWindowChanges = false;
}
return self;
}
- (void)layoutSubviews {
float width = 0;
float height = 0;
for (UIView *v in [_iconView subviews]) {
float fw = v.frame.origin.x + v.frame.size.width;
float fh = v.frame.origin.y + v.frame.size.height;
width = MAX(fw, width);
height = MAX(fh, height);
}
[_iconView setFrame:CGRectMake(0, 0, width, height)];
}
- (void)iconViewInsertSubview:(UIView*)subview atIndex:(NSInteger)atIndex {
if (!_realMarker.iconView) {
_iconView = [[UIView alloc] init];
_realMarker.iconView = _iconView;
}
[_iconView insertSubview:subview atIndex:atIndex];
}
- (void)insertReactSubview:(id<RCTComponent>)subview atIndex:(NSInteger)atIndex {
if ([subview isKindOfClass:[AIRGoogleMapCallout class]]) {
self.calloutView = (AIRGoogleMapCallout *)subview;
} else { // a child view of the marker
[self iconViewInsertSubview:(UIView*)subview atIndex:atIndex+1];
}
AIRDummyView *dummySubview = [[AIRDummyView alloc] initWithView:(UIView *)subview];
[super insertReactSubview:(UIView*)dummySubview atIndex:atIndex];
}
- (void)removeReactSubview:(id<RCTComponent>)dummySubview {
UIView *subview = [dummySubview isKindOfClass:[AIRDummyView class]] ? ((AIRDummyView *)dummySubview).view : (UIView *)dummySubview;
if ([subview isKindOfClass:[AIRGoogleMapCallout class]]) {
self.calloutView = nil;
} else {
[subview removeFromSuperview];
}
[super removeReactSubview:(UIView*)dummySubview];
}
- (void)showCalloutView {
[_realMarker.map setSelectedMarker:_realMarker];
}
- (void)hideCalloutView {
[_realMarker.map setSelectedMarker:Nil];
}
- (void)redraw {
if (!_realMarker.iconView) return;
BOOL oldValue = _realMarker.tracksViewChanges;
if (oldValue == YES)
{
// Immediate refresh, like right now. Not waiting for next frame.
UIView *view = _realMarker.iconView;
_realMarker.iconView = nil;
_realMarker.iconView = view;
}
else
{
// Refresh according to docs
_realMarker.tracksViewChanges = YES;
_realMarker.tracksViewChanges = NO;
}
}
- (UIView *)markerInfoContents {
if (self.calloutView && !self.calloutView.tooltip) {
return self.calloutView;
}
return nil;
}
- (UIView *)markerInfoWindow {
if (self.calloutView && self.calloutView.tooltip) {
return self.calloutView;
}
return nil;
}
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker point:(CGPoint)point frame:(CGRect)frame {
if (self.calloutView && self.calloutView.onPress) {
//todo: why not 'callout-press' ?
id event = @{
@"action": @"marker-overlay-press",
@"id": self.identifier ?: @"unknown",
@"point": @{
@"x": @(point.x),
@"y": @(point.y),
},
@"frame": @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height),
}
};
self.calloutView.onPress(event);
}
}
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker {
[self didTapInfoWindowOfMarker:marker point:CGPointMake(-1, -1) frame:CGRectZero];
}
- (void)didTapInfoWindowOfMarker:(AIRGMSMarker *)marker subview:(AIRGoogleMapCalloutSubview*)subview point:(CGPoint)point frame:(CGRect)frame {
if (subview && subview.onPress) {
//todo: why not 'callout-inside-press' ?
id event = @{
@"action": @"marker-inside-overlay-press",
@"id": self.identifier ?: @"unknown",
@"point": @{
@"x": @(point.x),
@"y": @(point.y),
},
@"frame": @{
@"x": @(frame.origin.x),
@"y": @(frame.origin.y),
@"width": @(frame.size.width),
@"height": @(frame.size.height),
}
};
subview.onPress(event);
} else {
[self didTapInfoWindowOfMarker:marker point:point frame:frame];
}
}
- (void)didBeginDraggingMarker:(AIRGMSMarker *)marker {
if (!self.onDragStart) return;
self.onDragStart([self makeEventData]);
}
- (void)didEndDraggingMarker:(AIRGMSMarker *)marker {
if (!self.onDragEnd) return;
self.onDragEnd([self makeEventData]);
}
- (void)didDragMarker:(AIRGMSMarker *)marker {
if (!self.onDrag) return;
self.onDrag([self makeEventData]);
}
- (void)setCoordinate:(CLLocationCoordinate2D)coordinate {
_realMarker.position = coordinate;
}
- (CLLocationCoordinate2D)coordinate {
return _realMarker.position;
}
- (void)setRotation:(CLLocationDegrees)rotation {
_realMarker.rotation = rotation;
}
- (CLLocationDegrees)rotation {
return _realMarker.rotation;
}
- (void)setIdentifier:(NSString *)identifier {
_realMarker.identifier = identifier;
}
- (NSString *)identifier {
return _realMarker.identifier;
}
- (void)setOnPress:(RCTBubblingEventBlock)onPress {
_realMarker.onPress = onPress;
}
- (RCTBubblingEventBlock)onPress {
return _realMarker.onPress;
}
- (void)setOnSelect:(RCTDirectEventBlock)onSelect {
_realMarker.onSelect = onSelect;
}
- (RCTDirectEventBlock)onSelect {
return _realMarker.onSelect;
}
- (void)setOnDeselect:(RCTDirectEventBlock)onDeselect {
_realMarker.onDeselect = onDeselect;
}
- (RCTDirectEventBlock)onDeselect {
return _realMarker.onDeselect;
}
- (void)setOpacity:(double)opacity
{
_realMarker.opacity = opacity;
}
- (void)setImageSrc:(NSString *)imageSrc
{
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
if (!_imageSrc) {
if (_iconImageView) [_iconImageView removeFromSuperview];
return;
}
if (!_iconImageView) {
// prevent glitch with marker (cf. https://github.com/react-native-maps/react-native-maps/issues/738)
UIImageView *empyImageView = [[UIImageView alloc] init];
_iconImageView = empyImageView;
[self iconViewInsertSubview:_iconImageView atIndex:0];
}
_reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:self.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
// TODO(lmr): do something with the error?
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
// TODO(gil): This way allows different image sizes
if (self->_iconImageView) [self->_iconImageView removeFromSuperview];
// ... but this way is more efficient?
// if (_iconImageView) {
// [_iconImageView setImage:image];
// return;
// }
UIImageView *imageView = [[UIImageView alloc] initWithImage:image];
// TODO: w,h or pixel density could be a prop.
float density = 1;
float w = image.size.width/density;
float h = image.size.height/density;
CGRect bounds = CGRectMake(0, 0, w, h);
imageView.contentMode = UIViewContentModeScaleAspectFit;
[imageView setFrame:bounds];
// NOTE: sizeToFit doesn't work instead. Not sure why.
// TODO: Doing it this way is not ideal because it causes things to reshuffle
// when the image loads IF the image is larger than the UIView.
// Shouldn't required images have size info automatically via RN?
CGRect selfBounds = unionRect(bounds, self.bounds);
[self setFrame:selfBounds];
self->_iconImageView = imageView;
[self iconViewInsertSubview:imageView atIndex:0];
});
}];
}
- (void)setIconSrc:(NSString *)iconSrc
{
_iconSrc = iconSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
if (!_realMarker.icon) {
// prevent glitch with marker (cf. https://github.com/react-native-maps/react-native-maps/issues/3657)
UIImage *emptyImage = [[UIImage alloc] init];
_realMarker.icon = emptyImage;
}
_reloadImageCancellationBlock =
[[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_iconSrc]
size:self.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
// TODO(lmr): do something with the error?
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
self->_realMarker.icon = image;
});
}];
}
- (void)setTitle:(NSString *)title {
_realMarker.title = [title copy];
}
- (NSString *)title {
return _realMarker.title;
}
- (void)setSubtitle:(NSString *)subtitle {
_realMarker.snippet = subtitle;
}
- (NSString *)subtitle {
return _realMarker.snippet;
}
- (void)setPinColor:(UIColor *)pinColor {
_pinColor = pinColor;
_realMarker.icon = [GMSMarker markerImageWithColor:pinColor];
}
- (void)setAnchor:(CGPoint)anchor {
_anchor = anchor;
_realMarker.groundAnchor = anchor;
}
- (void)setCalloutAnchor:(CGPoint)calloutAnchor {
_calloutAnchor = calloutAnchor;
_realMarker.infoWindowAnchor = calloutAnchor;
}
- (void)setZIndex:(NSInteger)zIndex
{
_zIndex = zIndex;
_realMarker.zIndex = (int)zIndex;
}
- (void)setDraggable:(BOOL)draggable {
_realMarker.draggable = draggable;
}
- (BOOL)draggable {
return _realMarker.draggable;
}
- (void)setTappable:(BOOL)tappable {
_realMarker.tappable = tappable;
}
- (BOOL)tappable {
return _realMarker.tappable;
}
- (void)setFlat:(BOOL)flat {
_realMarker.flat = flat;
}
- (BOOL)flat {
return _realMarker.flat;
}
- (void)setTracksViewChanges:(BOOL)tracksViewChanges {
_realMarker.tracksViewChanges = tracksViewChanges;
}
- (BOOL)tracksViewChanges {
return _realMarker.tracksViewChanges;
}
- (void)setTracksInfoWindowChanges:(BOOL)tracksInfoWindowChanges {
_realMarker.tracksInfoWindowChanges = tracksInfoWindowChanges;
}
- (BOOL)tracksInfoWindowChanges {
return _realMarker.tracksInfoWindowChanges;
}
- (id)makeEventData:(NSString *)action {
CLLocationCoordinate2D coordinate = self.realMarker.position;
CGPoint position = [self.realMarker.map.projection pointForCoordinate:coordinate];
return @{
@"id": self.identifier ?: @"unknown",
@"position": @{
@"x": @(position.x),
@"y": @(position.y),
},
@"coordinate": @{
@"latitude": @(coordinate.latitude),
@"longitude": @(coordinate.longitude),
},
@"action": action,
};
}
- (id)makeEventData {
return [self makeEventData:@"unknown"];
}
@end
#endif
@@ -0,0 +1,16 @@
//
// AIRGoogleMapMarkerManager.h
// AirMaps
//
// Created by Gil Birman on 9/2/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapMarkerManager : RCTViewManager
@end
#endif
@@ -0,0 +1,117 @@
//
// AIRGoogleMapMarkerManager.m
// AirMaps
//
// Created by Gil Birman on 9/2/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapMarkerManager.h"
#import "AIRGoogleMapMarker.h"
#import <MapKit/MapKit.h>
#import <React/RCTUIManager.h>
#import "RCTConvert+AirMap.h"
@implementation AIRGoogleMapMarkerManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapMarker *marker = [AIRGoogleMapMarker new];
// UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_handleTap:)];
// // setting this to NO allows the parent MapView to continue receiving marker selection events
// tapGestureRecognizer.cancelsTouchesInView = NO;
// [marker addGestureRecognizer:tapGestureRecognizer];
marker.bridge = self.bridge;
marker.isAccessibilityElement = YES;
marker.accessibilityElementsHidden = NO;
return marker;
}
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_REMAP_VIEW_PROPERTY(icon, iconSrc, NSString)
RCT_EXPORT_VIEW_PROPERTY(title, NSString)
RCT_REMAP_VIEW_PROPERTY(testID, accessibilityIdentifier, NSString)
RCT_REMAP_VIEW_PROPERTY(description, subtitle, NSString)
RCT_EXPORT_VIEW_PROPERTY(pinColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(anchor, CGPoint)
RCT_EXPORT_VIEW_PROPERTY(calloutAnchor, CGPoint)
RCT_EXPORT_VIEW_PROPERTY(zIndex, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(draggable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tappable, BOOL)
RCT_EXPORT_VIEW_PROPERTY(flat, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tracksViewChanges, BOOL)
RCT_EXPORT_VIEW_PROPERTY(tracksInfoWindowChanges, BOOL)
RCT_EXPORT_VIEW_PROPERTY(opacity, double)
RCT_EXPORT_VIEW_PROPERTY(onDragStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDrag, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDragEnd, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onSelect, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onDeselect, RCTDirectEventBlock)
RCT_EXPORT_METHOD(showCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRGoogleMapMarker *) view showCalloutView];
}
}];
}
RCT_EXPORT_METHOD(hideCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRGoogleMapMarker *) view hideCalloutView];
}
}];
}
RCT_EXPORT_METHOD(redrawCallout:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
AIRGoogleMapMarker* marker = (AIRGoogleMapMarker *) view;
[NSTimer scheduledTimerWithTimeInterval:0.0
target:[NSBlockOperation blockOperationWithBlock:^{
[marker hideCalloutView];
[marker showCalloutView];
}]
selector:@selector(main)
userInfo:nil
repeats:NO
];
}
}];
}
RCT_EXPORT_METHOD(redraw:(nonnull NSNumber *)reactTag)
{
[self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary<NSNumber *, UIView *> *viewRegistry) {
id view = viewRegistry[reactTag];
if (![view isKindOfClass:[AIRGoogleMapMarker class]]) {
RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view);
} else {
[(AIRGoogleMapMarker *) view redraw];
}
}];
}
@end
#endif
@@ -0,0 +1,29 @@
//
// AIRGoogleMapOverlay.h
//
// Created by Taro Matsuzawa on 5/3/17.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTBridge.h>
#import "AIRMapCoordinate.h"
#import "AIRGoogleMap.h"
@interface AIRGoogleMapOverlay : UIView
@property (nonatomic, strong) GMSGroundOverlay *overlay;
@property (nonatomic, copy) NSString *imageSrc;
@property (nonatomic, strong, readonly) UIImage *overlayImage;
@property (nonatomic, copy) NSArray *boundsRect;
@property (nonatomic, assign) CGFloat opacity;
@property (nonatomic, readonly) GMSCoordinateBounds *overlayBounds;
@property (nonatomic, readonly) double bearing;
@property (nonatomic, weak) RCTBridge *bridge;
@end
#endif
@@ -0,0 +1,92 @@
//
// AIRGoogleMapOverlay.m
// Created by Nick Italiano on 3/5/17.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapOverlay.h"
#import <React/RCTEventDispatcher.h>
#import <React/RCTImageLoaderProtocol.h>
#import <React/RCTUtils.h>
#import <React/UIView+React.h>
@interface AIRGoogleMapOverlay()
@property (nonatomic, strong, readwrite) UIImage *overlayImage;
@property (nonatomic, readwrite) GMSCoordinateBounds *overlayBounds;
@property (nonatomic) CLLocationDirection bearing;
@end
@implementation AIRGoogleMapOverlay {
RCTImageLoaderCancellationBlock _reloadImageCancellationBlock;
CLLocationCoordinate2D _southWest;
CLLocationCoordinate2D _northEast;
}
- (instancetype)init
{
if ((self = [super init])) {
_overlay = [[GMSGroundOverlay alloc] init];
}
return self;
}
- (void)setImageSrc:(NSString *)imageSrc
{
NSLog(@">>> SET IMAGESRC: %@", imageSrc);
_imageSrc = imageSrc;
if (_reloadImageCancellationBlock) {
_reloadImageCancellationBlock();
_reloadImageCancellationBlock = nil;
}
__weak typeof(self) weakSelf = self;
_reloadImageCancellationBlock = [[_bridge moduleForName:@"ImageLoader"] loadImageWithURLRequest:[RCTConvert NSURLRequest:_imageSrc]
size:weakSelf.bounds.size
scale:RCTScreenScale()
clipped:YES
resizeMode:RCTResizeModeCenter
progressBlock:nil
partialLoadBlock:nil
completionBlock:^(NSError *error, UIImage *image) {
if (error) {
NSLog(@"%@", error);
}
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@">>> IMAGE: %@", image);
weakSelf.overlayImage = image;
weakSelf.overlay.icon = image;
});
}];
}
- (void)setBoundsRect:(NSArray *)boundsRect
{
_boundsRect = boundsRect;
_southWest = CLLocationCoordinate2DMake([boundsRect[1][0] doubleValue], [boundsRect[0][1] doubleValue]);
_northEast = CLLocationCoordinate2DMake([boundsRect[0][0] doubleValue], [boundsRect[1][1] doubleValue]);
_overlayBounds = [[GMSCoordinateBounds alloc] initWithCoordinate:_southWest
coordinate:_northEast];
_overlay.bounds = _overlayBounds;
}
- (void)setBearing:(double)bearing
{
_bearing = (double)bearing;
_overlay.bearing = _bearing;
}
- (void)setOpacity:(CGFloat)opacity
{
_overlay.opacity = opacity;
}
@end
#endif
@@ -0,0 +1,10 @@
//
// AIRGoogleMapOverlayManager.h
// Created by Taro Matsuzawa on 3/5/17.
//
#import <Foundation/Foundation.h>
#import <React/RCTViewManager.h>
@interface AIRGoogleMapOverlayManager : RCTViewManager
@end
@@ -0,0 +1,24 @@
#import "AIRGoogleMapOverlayManager.h"
#import "AIRGoogleMapOverlay.h"
@interface AIRGoogleMapOverlayManager()
@end
@implementation AIRGoogleMapOverlayManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapOverlay *overlay = [AIRGoogleMapOverlay new];
overlay.bridge = self.bridge;
return overlay;
}
RCT_REMAP_VIEW_PROPERTY(bounds, boundsRect, NSArray)
RCT_REMAP_VIEW_PROPERTY(bearing, bearing, double)
RCT_REMAP_VIEW_PROPERTY(image, imageSrc, NSString)
RCT_REMAP_VIEW_PROPERTY(opacity, opacity, CGFloat)
@end
@@ -0,0 +1,32 @@
//
// AIRGoogleMapPolygon.h
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTBridge.h>
#import "AIRGMSPolygon.h"
#import "AIRMapCoordinate.h"
@interface AIRGoogleMapPolygon : UIView
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, strong) AIRGMSPolygon *polygon;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, strong) NSArray<NSArray<AIRMapCoordinate *> *> *holes;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, strong) UIColor *fillColor;
@property (nonatomic, assign) double strokeWidth;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, assign) BOOL geodesic;
@property (nonatomic, assign) int zIndex;
@property (nonatomic, assign) BOOL tappable;
@end
#endif
@@ -0,0 +1,127 @@
//
// AIRGoogleMapPolygon.m
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapPolygon.h"
#import "AIRGMSPolygon.h"
#import <GoogleMaps/GoogleMaps.h>
@implementation AIRGoogleMapPolygon
{
BOOL _didMoveToWindow;
}
- (instancetype)init
{
if (self = [super init]) {
_didMoveToWindow = false;
_polygon = [[AIRGMSPolygon alloc] init];
_polygon.fillColor = _fillColor;
_polygon.strokeColor = _strokeColor;
}
return self;
}
- (void)didMoveToWindow {
[super didMoveToWindow];
if(_didMoveToWindow) return;
_didMoveToWindow = true;
if(_fillColor) {
_polygon.fillColor = _fillColor;
}
if(_strokeColor) {
_polygon.strokeColor = _strokeColor;
}
if(_strokeWidth) {
_polygon.strokeWidth = _strokeWidth;
}
}
- (void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates
{
_coordinates = coordinates;
GMSMutablePath *path = [GMSMutablePath path];
for(int i = 0; i < coordinates.count; i++)
{
[path addCoordinate:coordinates[i].coordinate];
}
_polygon.path = path;
}
- (void)setHoles:(NSArray<NSArray<AIRMapCoordinate *> *> *)holes
{
_holes = holes;
if (holes.count)
{
NSMutableArray<GMSMutablePath *> *interiorPolygons = [NSMutableArray array];
for(int h = 0; h < holes.count; h++)
{
GMSMutablePath *path = [GMSMutablePath path];
for(int i = 0; i < holes[h].count; i++)
{
[path addCoordinate:holes[h][i].coordinate];
}
[interiorPolygons addObject:path];
}
_polygon.holes = interiorPolygons;
}
}
-(void)setFillColor:(UIColor *)fillColor
{
_fillColor = fillColor;
if(_didMoveToWindow) {
_polygon.fillColor = fillColor;
}
}
-(void)setStrokeWidth:(double)strokeWidth
{
_strokeWidth = strokeWidth;
if(_didMoveToWindow) {
_polygon.strokeWidth = strokeWidth;
}
}
-(void)setStrokeColor:(UIColor *) strokeColor
{
_strokeColor = strokeColor;
if(_didMoveToWindow) {
_polygon.strokeColor = strokeColor;
}
}
-(void)setGeodesic:(BOOL)geodesic
{
_geodesic = geodesic;
_polygon.geodesic = geodesic;
}
-(void)setZIndex:(int)zIndex
{
_zIndex = zIndex;
_polygon.zIndex = zIndex;
}
-(void)setTappable:(BOOL)tappable
{
_tappable = tappable;
_polygon.tappable = tappable;
}
- (void)setOnPress:(RCTBubblingEventBlock)onPress {
_polygon.onPress = onPress;
}
@end
#endif
@@ -0,0 +1,15 @@
//
// AIRGoogleMapPolylgoneManager.h
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapPolygonManager : RCTViewManager
@end
#endif
@@ -0,0 +1,46 @@
//
// AIRGoogleMapPolylgoneManager.m
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapPolygonManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRGoogleMapPolygon.h"
@interface AIRGoogleMapPolygonManager()
@end
@implementation AIRGoogleMapPolygonManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapPolygon *polygon = [AIRGoogleMapPolygon new];
polygon.bridge = self.bridge;
return polygon;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(holes, AIRMapCoordinateArrayArray)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, double)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
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
#endif
@@ -0,0 +1,36 @@
//
// AIRGoogleMapPolyline.h
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTBridge.h>
#import "AIRGMSPolyline.h"
#import "AIRMapCoordinate.h"
#import "AIRGoogleMapMarker.h"
@interface AIRGoogleMapPolyline : UIView
@property (nonatomic, weak) RCTBridge *bridge;
@property (nonatomic, strong) NSString *identifier;
@property (nonatomic, strong) AIRGMSPolyline *polyline;
@property (nonatomic, strong) NSArray<AIRMapCoordinate *> *coordinates;
@property (nonatomic, copy) RCTBubblingEventBlock onPress;
@property (nonatomic, strong) GMSMapView *originalMap;
@property (nonatomic, strong) UIColor *strokeColor;
@property (nonatomic, strong) NSArray<UIColor *> *strokeColors;
@property (nonatomic, assign) double strokeWidth;
@property (nonatomic, assign) UIColor *fillColor;
@property (nonatomic, strong) NSArray<NSNumber *> *lineDashPattern;
@property (nonatomic, assign) BOOL geodesic;
@property (nonatomic, assign) NSString *title;
@property (nonatomic, assign) int zIndex;
@property (nonatomic, assign) BOOL tappable;
@end
#endif
@@ -0,0 +1,143 @@
//
// AIRGoogleMapPolyline.m
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <UIKit/UIKit.h>
#import "AIRGoogleMapPolyline.h"
#import "AIRGMSPolyline.h"
#import "AIRMapCoordinate.h"
#import "AIRGoogleMapMarker.h"
#import "AIRGoogleMapMarkerManager.h"
#import <GoogleMaps/GoogleMaps.h>
#import <React/RCTUtils.h>
@implementation AIRGoogleMapPolyline
- (instancetype)init
{
if (self = [super init]) {
_polyline = [[AIRGMSPolyline alloc] init];
_polyline.spans = @[[GMSStyleSpan spanWithColor:_strokeColor]];
_polyline.strokeColor = _strokeColor;
}
return self;
}
-(void)setCoordinates:(NSArray<AIRMapCoordinate *> *)coordinates
{
_coordinates = coordinates;
GMSMutablePath *path = [GMSMutablePath path];
if (!coordinates || coordinates.count == 0)
{
[path removeAllCoordinates];
return;
}
for (int i = 0; i < coordinates.count; i++) {
[path addCoordinate:coordinates[i].coordinate];
}
_polyline.path = path;
[self configureStyleSpansIfNeeded];
}
-(void)setStrokeColor:(UIColor *)strokeColor
{
_strokeColor = strokeColor;
_polyline.strokeColor = strokeColor;
[self configureStyleSpansIfNeeded];
}
-(void)setStrokeColors:(NSArray<UIColor *> *)strokeColors
{
NSMutableArray *spans = [NSMutableArray arrayWithCapacity:[strokeColors count]];
for (int i = 0; i < [strokeColors count]; i++)
{
GMSStrokeStyle *stroke;
if (i == 0) {
stroke = [GMSStrokeStyle solidColor:strokeColors[i]];
} else {
stroke = [GMSStrokeStyle gradientFromColor:strokeColors[i-1] toColor:strokeColors[i]];
}
[spans addObject:[GMSStyleSpan spanWithStyle:stroke]];
}
_strokeColors = strokeColors;
_polyline.spans = spans;
}
-(void)setStrokeWidth:(double)strokeWidth
{
_strokeWidth = strokeWidth;
_polyline.strokeWidth = strokeWidth;
}
-(void)setFillColor:(UIColor *)fillColor
{
_fillColor = fillColor;
_polyline.spans = @[[GMSStyleSpan spanWithColor:fillColor]];
}
- (void)setLineDashPattern:(NSArray<NSNumber *> *)lineDashPattern {
_lineDashPattern = lineDashPattern;
[self configureStyleSpansIfNeeded];
}
-(void)setGeodesic:(BOOL)geodesic
{
_geodesic = geodesic;
_polyline.geodesic = geodesic;
}
-(void)setTitle:(NSString *)title
{
_title = title;
_polyline.title = _title;
}
-(void) setZIndex:(int)zIndex
{
_zIndex = zIndex;
_polyline.zIndex = zIndex;
}
-(void)setTappable:(BOOL)tappable
{
_tappable = tappable;
_polyline.tappable = tappable;
}
- (void)setOnPress:(RCTBubblingEventBlock)onPress {
_polyline.onPress = onPress;
}
- (void)configureStyleSpansIfNeeded {
if (!_strokeColor || !_lineDashPattern || !_polyline.path) {
return;
}
BOOL isLine = YES;
NSMutableArray *styles = [[NSMutableArray alloc] init];
for (NSInteger i = 0; i < _lineDashPattern.count; i++) {
if (isLine) {
[styles addObject:[GMSStrokeStyle solidColor:_strokeColor]];
} else {
[styles addObject:[GMSStrokeStyle solidColor:[UIColor clearColor]]];
}
isLine = !isLine;
}
_polyline.spans = GMSStyleSpans(_polyline.path, styles, _lineDashPattern, kGMSLengthRhumb);
}
@end
#endif
@@ -0,0 +1,15 @@
//
// AIRGoogleMapPolylineManager.h
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <React/RCTViewManager.h>
@interface AIRGoogleMapPolylineManager : RCTViewManager
@end
#endif
@@ -0,0 +1,48 @@
//
// AIRGoogleMapPolylineManager.m
//
// Created by Nick Italiano on 10/22/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapPolylineManager.h"
#import <React/RCTBridge.h>
#import <React/RCTConvert.h>
#import <React/RCTConvert+CoreLocation.h>
#import <React/RCTEventDispatcher.h>
#import <React/RCTViewManager.h>
#import <React/UIView+React.h>
#import "RCTConvert+AirMap.h"
#import "AIRGoogleMapPolyline.h"
@interface AIRGoogleMapPolylineManager()
@end
@implementation AIRGoogleMapPolylineManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapPolyline *polyline = [AIRGoogleMapPolyline new];
polyline.bridge = self.bridge;
return polyline;
}
RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray)
RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor)
RCT_EXPORT_VIEW_PROPERTY(strokeColors, UIColorArray)
RCT_EXPORT_VIEW_PROPERTY(strokeWidth, double)
RCT_EXPORT_VIEW_PROPERTY(lineDashPattern, NSArray)
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
#endif
@@ -0,0 +1,33 @@
//
// AIRGoogleMapURLTileManager.m
// Created by Nick Italiano on 11/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapUrlTileManager.h"
#import "AIRGoogleMapUrlTile.h"
@interface AIRGoogleMapUrlTileManager()
@end
@implementation AIRGoogleMapUrlTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapUrlTile *tileLayer = [AIRGoogleMapUrlTile new];
return tileLayer;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(zIndex, int)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, NSInteger)
RCT_EXPORT_VIEW_PROPERTY(flipY, BOOL)
@end
#endif
@@ -0,0 +1,22 @@
//
// AIRGoogleMapURLTile.h
// Created by Nick Italiano on 11/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import <GoogleMaps/GoogleMaps.h>
@interface AIRGoogleMapUrlTile : UIView
@property (nonatomic, strong) GMSURLTileLayer *tileLayer;
@property (nonatomic, assign) NSString *urlTemplate;
@property (nonatomic, assign) int zIndex;
@property NSInteger *maximumZ;
@property NSInteger *minimumZ;
@property BOOL flipY;
@end
#endif
@@ -0,0 +1,56 @@
//
// AIRGoogleMapURLTile.m
// Created by Nick Italiano on 11/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapUrlTile.h"
@implementation AIRGoogleMapUrlTile
- (void)setZIndex:(int)zIndex
{
_zIndex = zIndex;
_tileLayer.zIndex = zIndex;
}
- (void)setUrlTemplate:(NSString *)urlTemplate
{
_urlTemplate = urlTemplate;
_tileLayer = [GMSURLTileLayer tileLayerWithURLConstructor:[self _getTileURLConstructor]];
_tileLayer.tileSize = [[UIScreen mainScreen] scale] * 256;
}
- (GMSTileURLConstructor)_getTileURLConstructor
{
NSString *urlTemplate = self.urlTemplate;
NSInteger *maximumZ = self.maximumZ;
NSInteger *minimumZ = self.minimumZ;
GMSTileURLConstructor urls = ^NSURL* _Nullable (NSUInteger x, NSUInteger y, NSUInteger zoom) {
if (self.flipY == YES) {
y = (1 << zoom) - y - 1;
}
NSString *url = urlTemplate;
url = [url stringByReplacingOccurrencesOfString:@"{x}" withString:[NSString stringWithFormat: @"%ld", (long)x]];
url = [url stringByReplacingOccurrencesOfString:@"{y}" withString:[NSString stringWithFormat: @"%ld", (long)y]];
url = [url stringByReplacingOccurrencesOfString:@"{z}" withString:[NSString stringWithFormat: @"%ld", (long)zoom]];
if(maximumZ && (long)zoom > (long)maximumZ) {
return nil;
}
if(minimumZ && (long)zoom < (long)minimumZ) {
return nil;
}
return [NSURL URLWithString:url];
};
return urls;
}
@end
#endif
@@ -0,0 +1,14 @@
//
// AIRGoogleMapURLTileManager.h
// Created by Nick Italiano on 11/5/16.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import <React/RCTViewManager.h>
@interface AIRGoogleMapUrlTileManager : RCTViewManager
@end
#endif
@@ -0,0 +1,33 @@
//
// AIRGoogleMapWMSTile.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import <GoogleMaps/GoogleMaps.h>
@interface WMSTileOverlay : GMSSyncTileLayer
@property (nonatomic) double MapX,MapY,FULL;
@property (nonatomic, strong) NSString *template;
@property (nonatomic, assign) NSInteger maximumZ;
@property (nonatomic, assign) NSInteger minimumZ;
@end
@interface AIRGoogleMapWMSTile : UIView
@property (nonatomic, strong) WMSTileOverlay *tileLayer;
@property (nonatomic, assign) NSString *urlTemplate;
@property (nonatomic, assign) int zIndex;
@property (nonatomic, assign) NSInteger maximumZ;
@property (nonatomic, assign) NSInteger minimumZ;
@property (nonatomic, assign) NSInteger tileSize;
@property (nonatomic, assign) float opacity;
@end
#endif
@@ -0,0 +1,125 @@
//
// AIRGoogleMapWMSTile.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapWMSTile.h"
@implementation AIRGoogleMapWMSTile
-(id) init
{
self = [super init];
_opacity = 1;
return self ;
}
- (void)setZIndex:(int)zIndex
{
_zIndex = zIndex;
_tileLayer.zIndex = zIndex;
}
- (void)setTileSize:(NSInteger)tileSize
{
_tileSize = tileSize;
if(self.tileLayer) {
self.tileLayer.tileSize = tileSize;
[self.tileLayer clearTileCache];
}
}
- (void)setMinimumZ:(NSInteger)minimumZ
{
_minimumZ = minimumZ;
if(self.tileLayer && _minimumZ) {
[self.tileLayer setMinimumZ: _minimumZ ];
[self.tileLayer clearTileCache];
}
}
- (void)setMaximumZ:(NSInteger)maximumZ
{
_maximumZ = maximumZ;
if(self.tileLayer && maximumZ) {
[self.tileLayer setMaximumZ: _maximumZ ];
[self.tileLayer clearTileCache];
}
}
- (void)setOpacity:(float)opacity
{
_opacity = opacity;
if(self.tileLayer ) {
[self.tileLayer setOpacity:opacity];
[self.tileLayer clearTileCache];
}
}
- (void)setUrlTemplate:(NSString *)urlTemplate
{
_urlTemplate = urlTemplate;
WMSTileOverlay *tile = [[WMSTileOverlay alloc] init];
[tile setTemplate:urlTemplate];
[tile setMaximumZ: _maximumZ];
[tile setMinimumZ: _minimumZ];
[tile setOpacity: _opacity];
[tile setTileSize: _tileSize];
[tile setZIndex: _zIndex];
_tileLayer = tile;
}
@end
@implementation WMSTileOverlay
-(id) init
{
self = [super init];
_MapX = -20037508.34789244;
_MapY = 20037508.34789244;
_FULL = 20037508.34789244 * 2;
return self ;
}
-(NSArray *)getBoundBox:(NSInteger)x yAxis:(NSInteger)y zoom:(NSInteger)zoom
{
double tile = _FULL / pow(2.0, (double)zoom);
NSArray *result =[[NSArray alloc] initWithObjects:
[NSNumber numberWithDouble:_MapX + (double)x * tile ],
[NSNumber numberWithDouble:_MapY - (double)(y+1) * tile ],
[NSNumber numberWithDouble:_MapX + (double)(x+1) * tile ],
[NSNumber numberWithDouble:_MapY - (double)y * tile ],
nil];
return result;
}
- (UIImage *)tileForX:(NSUInteger)x y:(NSUInteger)y zoom:(NSUInteger)zoom
{
NSInteger maximumZ = self.maximumZ;
NSInteger minimumZ = self.minimumZ;
if(maximumZ && (long)zoom > (long)maximumZ) {
return nil;
}
if(minimumZ && (long)zoom < (long)minimumZ) {
return nil;
}
NSArray *bb = [self getBoundBox:x yAxis:y zoom:zoom];
NSMutableString *url = [self.template mutableCopy];
[url replaceOccurrencesOfString: @"{minX}" withString:[NSString stringWithFormat:@"%@", bb[0]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{minY}" withString:[NSString stringWithFormat:@"%@", bb[1]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxX}" withString:[NSString stringWithFormat:@"%@", bb[2]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{maxY}" withString:[NSString stringWithFormat:@"%@", bb[3]] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{width}" withString:[NSString stringWithFormat:@"%d", (int)self.tileSize] options:0 range:NSMakeRange(0, url.length)];
[url replaceOccurrencesOfString: @"{height}" withString:[NSString stringWithFormat:@"%d", (int)self.tileSize] options:0 range:NSMakeRange(0, url.length)];
NSURL *uri = [NSURL URLWithString:url];
NSData *data = [NSData dataWithContentsOfURL:uri];
UIImage *img = [[UIImage alloc] initWithData:data];
return img;
}
@end
#endif
@@ -0,0 +1,17 @@
//
// AIRGoogleMapWMSTileManager.h
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#ifdef HAVE_GOOGLE_MAPS
#import <Foundation/Foundation.h>
#import <React/RCTViewManager.h>
@interface AIRGoogleMapWMSTileManager : RCTViewManager
@end
#endif
@@ -0,0 +1,37 @@
//
// AIRGoogleMapWMSTileManager.m
// AirMaps
//
// Created by nizam on 10/28/18.
// Copyright © 2018. All rights reserved.
//
#ifdef HAVE_GOOGLE_MAPS
#import "AIRGoogleMapWMSTileManager.h"
#import "AIRGoogleMapWMSTile.h"
@interface AIRGoogleMapWMSTileManager()
@end
@implementation AIRGoogleMapWMSTileManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
AIRGoogleMapWMSTile *tileLayer = [AIRGoogleMapWMSTile new];
return tileLayer;
}
RCT_EXPORT_VIEW_PROPERTY(urlTemplate, NSString)
RCT_EXPORT_VIEW_PROPERTY(zIndex, int)
RCT_EXPORT_VIEW_PROPERTY(maximumZ, int)
RCT_EXPORT_VIEW_PROPERTY(minimumZ, int)
RCT_EXPORT_VIEW_PROPERTY(tileSize, int)
RCT_EXPORT_VIEW_PROPERTY(opacity, float)
@end
#endif

Some files were not shown because too many files have changed in this diff Show More