From 4e58c9f985203e2da65e9b972e3db3590f7bdfbe Mon Sep 17 00:00:00 2001 From: Leland Richardson Date: Tue, 29 Dec 2015 12:00:37 -0800 Subject: [PATCH] Initial Commit --- .flowconfig | 63 ++ .gitignore | 34 + .watchmanconfig | 1 + android/app/app.iml | 127 +++ android/app/build.gradle | 83 ++ android/app/proguard-rules.pro | 60 ++ android/app/react.gradle | 87 ++ android/app/src/main/AndroidManifest.xml | 28 + .../java/com/rn_mapview/AirMapCallout.java | 21 + .../com/rn_mapview/AirMapCalloutManager.java | 40 + .../java/com/rn_mapview/AirMapCircle.java | 100 ++ .../com/rn_mapview/AirMapCircleManager.java | 72 ++ .../java/com/rn_mapview/AirMapFeature.java | 12 + .../java/com/rn_mapview/AirMapManager.java | 502 ++++++++++ .../java/com/rn_mapview/AirMapMarker.java | 270 ++++++ .../com/rn_mapview/AirMapMarkerManager.java | 174 ++++ .../rn_mapview/AirMapMarkerShadowNode.java | 29 + .../java/com/rn_mapview/AirMapPolygon.java | 110 +++ .../com/rn_mapview/AirMapPolygonManager.java | 70 ++ .../java/com/rn_mapview/AirMapPolyline.java | 100 ++ .../com/rn_mapview/AirMapPolylineManager.java | 65 ++ .../java/com/rn_mapview/AirMapShadowNode.java | 31 + .../main/java/com/rn_mapview/AirPackage.java | 47 + .../java/com/rn_mapview/MainActivity.java | 115 +++ .../com/rn_mapview/RegionChangeEvent.java | 44 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes android/app/src/main/res/values/strings.xml | 3 + android/app/src/main/res/values/styles.xml | 8 + android/build.gradle | 23 + android/gradle.properties | 20 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 52266 bytes .../gradle/wrapper/gradle-wrapper.properties | 5 + android/gradlew | 164 ++++ android/gradlew.bat | 90 ++ android/rn_mapview.iml | 19 + android/settings.gradle | 3 + components/AnimatedRegion.js | 171 ++++ components/MapCallout.js | 41 + components/MapCircle.js | 122 +++ components/MapMarker.js | 233 +++++ components/MapPolygon.js | 125 +++ components/MapPolyline.js | 113 +++ components/MapView.js | 286 ++++++ components/PriceMarker.js | 64 ++ examples/oldandroid.js | 167 ++++ examples/oldandroid2.js | 245 +++++ examples/oldandroid3.js | 259 ++++++ index.android.js | 290 ++++++ index.ios.js | 291 ++++++ ios/AIRMap.h | 47 + ios/AIRMap.m | 184 ++++ ios/AIRMapCallout.h | 15 + ios/AIRMapCallout.m | 12 + ios/AIRMapCalloutManager.h | 10 + ios/AIRMapCalloutManager.m | 37 + ios/AIRMapCircle.h | 40 + ios/AIRMapCircle.m | 155 ++++ ios/AIRMapCircleManager.h | 10 + ios/AIRMapCircleManager.m | 49 + ios/AIRMapCoordinate.h | 13 + ios/AIRMapCoordinate.m | 12 + ios/AIRMapManager.h | 14 + ios/AIRMapManager.m | 370 ++++++++ ios/AIRMapMarker.h | 45 + ios/AIRMapMarker.m | 97 ++ ios/AIRMapMarkerManager.h | 14 + ios/AIRMapMarkerManager.m | 110 +++ ios/AIRMapPolygon.h | 38 + ios/AIRMapPolygon.m | 143 +++ ios/AIRMapPolygonManager.h | 10 + ios/AIRMapPolygonManager.m | 48 + ios/AIRMapPolyline.h | 37 + ios/AIRMapPolyline.m | 91 ++ ios/AIRMapPolylineManager.h | 10 + ios/AIRMapPolylineManager.m | 48 + ios/Callout/SMCalloutView.h | 200 ++++ ios/Callout/SMCalloutView.m | 858 +++++++++++++++++ ios/RCTConvert+MoreMapKit.h | 12 + ios/RCTConvert+MoreMapKit.m | 27 + ios/rn_mapview.xcodeproj/project.pbxproj | 868 ++++++++++++++++++ .../xcschemes/rn_mapview.xcscheme | 112 +++ ios/rn_mapview/AppDelegate.h | 16 + ios/rn_mapview/AppDelegate.m | 57 ++ ios/rn_mapview/Base.lproj/LaunchScreen.xib | 42 + .../AppIcon.appiconset/Contents.json | 38 + ios/rn_mapview/Info.plist | 48 + ios/rn_mapview/main.m | 18 + ios/rn_mapviewTests/Info.plist | 24 + ios/rn_mapviewTests/rn_mapviewTests.m | 70 ++ package.json | 11 + 93 files changed, 9087 insertions(+) create mode 100644 .flowconfig create mode 100644 .gitignore create mode 100644 .watchmanconfig create mode 100644 android/app/app.iml create mode 100644 android/app/build.gradle create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/react.gradle create mode 100644 android/app/src/main/AndroidManifest.xml create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapCallout.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapCalloutManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapCircle.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapCircleManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapFeature.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapMarker.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapMarkerManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapMarkerShadowNode.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapPolygon.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapPolygonManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapPolyline.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapPolylineManager.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirMapShadowNode.java create mode 100644 android/app/src/main/java/com/rn_mapview/AirPackage.java create mode 100644 android/app/src/main/java/com/rn_mapview/MainActivity.java create mode 100644 android/app/src/main/java/com/rn_mapview/RegionChangeEvent.java create mode 100644 android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 android/app/src/main/res/values/strings.xml create mode 100644 android/app/src/main/res/values/styles.xml create mode 100644 android/build.gradle create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100644 android/gradle/wrapper/gradle-wrapper.properties create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/rn_mapview.iml create mode 100644 android/settings.gradle create mode 100644 components/AnimatedRegion.js create mode 100644 components/MapCallout.js create mode 100644 components/MapCircle.js create mode 100644 components/MapMarker.js create mode 100644 components/MapPolygon.js create mode 100644 components/MapPolyline.js create mode 100644 components/MapView.js create mode 100644 components/PriceMarker.js create mode 100644 examples/oldandroid.js create mode 100644 examples/oldandroid2.js create mode 100644 examples/oldandroid3.js create mode 100644 index.android.js create mode 100644 index.ios.js create mode 100644 ios/AIRMap.h create mode 100644 ios/AIRMap.m create mode 100644 ios/AIRMapCallout.h create mode 100644 ios/AIRMapCallout.m create mode 100644 ios/AIRMapCalloutManager.h create mode 100644 ios/AIRMapCalloutManager.m create mode 100644 ios/AIRMapCircle.h create mode 100644 ios/AIRMapCircle.m create mode 100644 ios/AIRMapCircleManager.h create mode 100644 ios/AIRMapCircleManager.m create mode 100644 ios/AIRMapCoordinate.h create mode 100644 ios/AIRMapCoordinate.m create mode 100644 ios/AIRMapManager.h create mode 100644 ios/AIRMapManager.m create mode 100644 ios/AIRMapMarker.h create mode 100644 ios/AIRMapMarker.m create mode 100644 ios/AIRMapMarkerManager.h create mode 100644 ios/AIRMapMarkerManager.m create mode 100644 ios/AIRMapPolygon.h create mode 100644 ios/AIRMapPolygon.m create mode 100644 ios/AIRMapPolygonManager.h create mode 100644 ios/AIRMapPolygonManager.m create mode 100644 ios/AIRMapPolyline.h create mode 100644 ios/AIRMapPolyline.m create mode 100644 ios/AIRMapPolylineManager.h create mode 100644 ios/AIRMapPolylineManager.m create mode 100644 ios/Callout/SMCalloutView.h create mode 100644 ios/Callout/SMCalloutView.m create mode 100644 ios/RCTConvert+MoreMapKit.h create mode 100644 ios/RCTConvert+MoreMapKit.m create mode 100644 ios/rn_mapview.xcodeproj/project.pbxproj create mode 100644 ios/rn_mapview.xcodeproj/xcshareddata/xcschemes/rn_mapview.xcscheme create mode 100644 ios/rn_mapview/AppDelegate.h create mode 100644 ios/rn_mapview/AppDelegate.m create mode 100644 ios/rn_mapview/Base.lproj/LaunchScreen.xib create mode 100644 ios/rn_mapview/Images.xcassets/AppIcon.appiconset/Contents.json create mode 100644 ios/rn_mapview/Info.plist create mode 100644 ios/rn_mapview/main.m create mode 100644 ios/rn_mapviewTests/Info.plist create mode 100644 ios/rn_mapviewTests/rn_mapviewTests.m create mode 100644 package.json diff --git a/.flowconfig b/.flowconfig new file mode 100644 index 0000000000..8eadd339f0 --- /dev/null +++ b/.flowconfig @@ -0,0 +1,63 @@ +[ignore] + +# We fork some components by platform. +.*/*.web.js +.*/*.android.js + +# Some modules have their own node_modules with overlap +.*/node_modules/node-haste/.* + +# Ugh +.*/node_modules/babel.* +.*/node_modules/babylon.* +.*/node_modules/invariant.* + +# Ignore react and fbjs where there are overlaps, but don't ignore +# anything that react-native relies on +.*/node_modules/fbjs-haste/.*/__tests__/.* +.*/node_modules/fbjs-haste/__forks__/Map.js +.*/node_modules/fbjs-haste/__forks__/Promise.js +.*/node_modules/fbjs-haste/__forks__/fetch.js +.*/node_modules/fbjs-haste/core/ExecutionEnvironment.js +.*/node_modules/fbjs-haste/core/isEmpty.js +.*/node_modules/fbjs-haste/crypto/crc32.js +.*/node_modules/fbjs-haste/stubs/ErrorUtils.js +.*/node_modules/react-haste/React.js +.*/node_modules/react-haste/renderers/dom/ReactDOM.js +.*/node_modules/react-haste/renderers/shared/event/eventPlugins/ResponderEventPlugin.js + +# Ignore commoner tests +.*/node_modules/commoner/test/.* + +# See https://github.com/facebook/flow/issues/442 +.*/react-tools/node_modules/commoner/lib/reader.js + +# Ignore jest +.*/node_modules/jest-cli/.* + +# Ignore Website +.*/website/.* + +[include] + +[libs] +node_modules/react-native/Libraries/react-native/react-native-interface.js + +[options] +module.system=haste + +munge_underscores=true + +module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' +module.name_mapper='^[./a-zA-Z0-9$_-]+\.png$' -> 'RelativeImageStub' + +suppress_type=$FlowIssue +suppress_type=$FlowFixMe +suppress_type=$FixMe + +suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(1[0-8]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) +suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(1[0-8]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)? #[0-9]+ +suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy + +[version] +0.18.1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..94fc86711d --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# OSX +# +.DS_Store + +# Xcode +# +build/ +*.pbxuser +!default.pbxuser +*.mode1v3 +!default.mode1v3 +*.mode2v3 +!default.mode2v3 +*.perspectivev3 +!default.perspectivev3 +xcuserdata +*.xccheckout +*.moved-aside +DerivedData +*.hmap +*.ipa +*.xcuserstate +project.xcworkspace + +# Android/IJ +# +.idea +.gradle +local.properties + +# node.js +# +node_modules/ +npm-debug.log diff --git a/.watchmanconfig b/.watchmanconfig new file mode 100644 index 0000000000..9e26dfeeb6 --- /dev/null +++ b/.watchmanconfig @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/android/app/app.iml b/android/app/app.iml new file mode 100644 index 0000000000..51e75e2221 --- /dev/null +++ b/android/app/app.iml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle new file mode 100644 index 0000000000..42b80765fc --- /dev/null +++ b/android/app/build.gradle @@ -0,0 +1,83 @@ +apply plugin: "com.android.application" + +/** + * The react.gradle file registers two tasks: bundleDebugJsAndAssets and bundleReleaseJsAndAssets. + * These basically call `react-native bundle` with the correct arguments during the Android build + * cycle. By default, bundleDebugJsAndAssets is skipped, as in debug/dev mode we prefer to load the + * bundle directly from the development server. Below you can see all the possible configurations + * and their defaults. If you decide to add a configuration block, make sure to add it before the + * `apply from: "react.gradle"` line. + * + * project.ext.react = [ + * // the name of the generated asset file containing your JS bundle + * bundleAssetName: "index.android.bundle", + * + * // the entry file for bundle generation + * entryFile: "index.android.js", + * + * // whether to bundle JS and assets in debug mode + * bundleInDebug: false, + * + * // whether to bundle JS and assets in release mode + * bundleInRelease: true, + * + * // the root of your project, i.e. where "package.json" lives + * root: "../../", + * + * // where to put the JS bundle asset in debug mode + * jsBundleDirDebug: "$buildDir/intermediates/assets/debug", + * + * // where to put the JS bundle asset in release mode + * jsBundleDirRelease: "$buildDir/intermediates/assets/release", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in debug mode + * resourcesDirDebug: "$buildDir/intermediates/res/merged/debug", + * + * // where to put drawable resources / React Native assets, e.g. the ones you use via + * // require('./image.png')), in release mode + * resourcesDirRelease: "$buildDir/intermediates/res/merged/release", + * + * // by default the gradle tasks are skipped if none of the JS files or assets change; this means + * // that we don't look at files in android/ or ios/ to determine whether the tasks are up to + * // date; if you have any other folders that you want to ignore for performance reasons (gradle + * // indexes the entire tree), add them here. Alternatively, if you have JS files in android/ + * // for example, you might want to remove it from here. + * inputExcludes: ["android/**", "ios/**"] + * ] + */ + +apply from: "react.gradle" + +android { + compileSdkVersion 23 + buildToolsVersion "23.0.1" + + defaultConfig { + applicationId "com.rn_mapview" + minSdkVersion 16 + targetSdkVersion 22 + versionCode 1 + versionName "1.0" + ndk { + abiFilters "armeabi-v7a", "x86" + } + } + buildTypes { + release { + minifyEnabled false // Set this to true to enable Proguard + proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" + } + } +} + +dependencies { + compile fileTree(dir: "libs", include: ["*.jar"]) + compile "com.android.support:appcompat-v7:23.0.1" + compile "com.facebook.react:react-native:0.16.+" + compile "com.google.android.gms:play-services-base:8.3.0" + compile 'com.google.android.gms:play-services-maps:8.3.0' + compile('com.mapbox.mapboxsdk:mapbox-android-sdk:2.3.0@aar') { + transitive = true + } +} diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000000..ffa8c9f64b --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,60 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Disabling obfuscation is useful if you collect stack traces from production crashes +# (unless you are using a system that supports de-obfuscate the stack traces). +-dontobfuscate + +# React Native + +# Keep our interfaces so they can be used by other ProGuard rules. +# See http://sourceforge.net/p/proguard/bugs/466/ +-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip +-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters + +# Do not strip any method/class that is annotated with @DoNotStrip +-keep @com.facebook.proguard.annotations.DoNotStrip class * +-keepclassmembers class * { + @com.facebook.proguard.annotations.DoNotStrip *; +} + +-keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * { + void set*(***); + *** get*(); +} + +-keep class * extends com.facebook.react.bridge.JavaScriptModule { *; } +-keep class * extends com.facebook.react.bridge.NativeModule { *; } +-keepclassmembers class * { @com.facebook.react.uimanager.UIProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.ReactProp ; } +-keepclassmembers class * { @com.facebook.react.uimanager.ReactPropGroup ; } + +# okhttp + +-keepattributes Signature +-keepattributes *Annotation* +-keep class com.squareup.okhttp.** { *; } +-keep interface com.squareup.okhttp.** { *; } +-dontwarn com.squareup.okhttp.** + +# okio + +-keep class sun.misc.Unsafe { *; } +-dontwarn java.nio.file.* +-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement +-dontwarn okio.** diff --git a/android/app/react.gradle b/android/app/react.gradle new file mode 100644 index 0000000000..1e08b00f13 --- /dev/null +++ b/android/app/react.gradle @@ -0,0 +1,87 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +def config = project.hasProperty("react") ? project.react : []; + +def bundleAssetName = config.bundleAssetName ?: "index.android.bundle" +def entryFile = config.entryFile ?: "index.android.js" + +// because elvis operator +def elvisFile(thing) { + return thing ? file(thing) : null; +} + +def reactRoot = elvisFile(config.root) ?: file("../../") +def jsBundleDirDebug = elvisFile(config.jsBundleDirDebug) ?: + file("$buildDir/intermediates/assets/debug") +def jsBundleDirRelease = elvisFile(config.jsBundleDirRelease) ?: + file("$buildDir/intermediates/assets/release") +def resourcesDirDebug = elvisFile(config.resourcesDirDebug) ?: + file("$buildDir/intermediates/res/merged/debug") +def resourcesDirRelease = elvisFile(config.resourcesDirRelease) ?: + file("$buildDir/intermediates/res/merged/release") +def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"] + +def jsBundleFileDebug = file("$jsBundleDirDebug/$bundleAssetName") +def jsBundleFileRelease = file("$jsBundleDirRelease/$bundleAssetName") + +task bundleDebugJsAndAssets(type: Exec) { + // create dirs if they are not there (e.g. the "clean" task just ran) + doFirst { + jsBundleDirDebug.mkdirs() + resourcesDirDebug.mkdirs() + } + + // set up inputs and outputs so gradle can cache the result + inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) + outputs.dir jsBundleDirDebug + outputs.dir resourcesDirDebug + + // set up the call to the react-native cli + workingDir reactRoot + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine "cmd", "/c", "react-native", "bundle", "--platform", "android", "--dev", "true", "--entry-file", + entryFile, "--bundle-output", jsBundleFileDebug, "--assets-dest", resourcesDirDebug + } else { + commandLine "react-native", "bundle", "--platform", "android", "--dev", "true", "--entry-file", + entryFile, "--bundle-output", jsBundleFileDebug, "--assets-dest", resourcesDirDebug + } + + enabled config.bundleInDebug ?: false +} + +task bundleReleaseJsAndAssets(type: Exec) { + // create dirs if they are not there (e.g. the "clean" task just ran) + doFirst { + jsBundleDirRelease.mkdirs() + resourcesDirRelease.mkdirs() + } + + // set up inputs and outputs so gradle can cache the result + inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) + outputs.dir jsBundleDirRelease + outputs.dir resourcesDirRelease + + // set up the call to the react-native cli + workingDir reactRoot + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + commandLine "cmd","/c", "react-native", "bundle", "--platform", "android", "--dev", "false", "--entry-file", + entryFile, "--bundle-output", jsBundleFileRelease, "--assets-dest", resourcesDirRelease + } else { + commandLine "react-native", "bundle", "--platform", "android", "--dev", "false", "--entry-file", + entryFile, "--bundle-output", jsBundleFileRelease, "--assets-dest", resourcesDirRelease + } + + enabled config.bundleInRelease ?: true +} + +gradle.projectsEvaluated { + // hook bundleDebugJsAndAssets into the android build process + bundleDebugJsAndAssets.dependsOn mergeDebugResources + bundleDebugJsAndAssets.dependsOn mergeDebugAssets + processDebugResources.dependsOn bundleDebugJsAndAssets + + // hook bundleReleaseJsAndAssets into the android build process + bundleReleaseJsAndAssets.dependsOn mergeReleaseResources + bundleReleaseJsAndAssets.dependsOn mergeReleaseAssets + processReleaseResources.dependsOn bundleReleaseJsAndAssets +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..05aaddfc99 --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/java/com/rn_mapview/AirMapCallout.java b/android/app/src/main/java/com/rn_mapview/AirMapCallout.java new file mode 100644 index 0000000000..a447c75bc8 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapCallout.java @@ -0,0 +1,21 @@ +package com.rn_mapview; + +import android.content.Context; + +import com.facebook.react.views.view.ReactViewGroup; + +public class AirMapCallout extends ReactViewGroup { + private boolean tooltip = false; + + public AirMapCallout(Context context) { + super(context); + } + + public void setTooltip(boolean tooltip) { + this.tooltip = tooltip; + } + + public boolean getTooltip() { + return this.tooltip; + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapCalloutManager.java b/android/app/src/main/java/com/rn_mapview/AirMapCalloutManager.java new file mode 100644 index 0000000000..61b2267485 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapCalloutManager.java @@ -0,0 +1,40 @@ +package com.rn_mapview; + +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.events.RCTEventEmitter; + +import java.util.Map; + +import javax.annotation.Nullable; + +public class AirMapCalloutManager extends ViewGroupManager { + + @Override + public String getName() { + return "AIRMapCallout"; + } + + @Override + public AirMapCallout createViewInstance(ThemedReactContext context) { + return new AirMapCallout(context); + } + + @ReactProp(name = "tooltip", defaultBoolean = false) + public void setTooltip(AirMapCallout view, boolean tooltip) { + view.setTooltip(tooltip); + } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress") + ); + } + +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapCircle.java b/android/app/src/main/java/com/rn_mapview/AirMapCircle.java new file mode 100644 index 0000000000..ffa6df8c38 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapCircle.java @@ -0,0 +1,100 @@ +package com.rn_mapview; + +import android.content.Context; + +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.CircleOptions; +import com.google.android.gms.maps.model.LatLng; + +public class AirMapCircle extends AirMapFeature { + + private CircleOptions circleOptions; + private Circle circle; + + private LatLng center; + private double radius; + private int strokeColor; + private int fillColor; + private float strokeWidth; + private float zIndex; + + public AirMapCircle(Context context) { + super(context); + } + + 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(GoogleMap map) { + circle = map.addCircle(getCircleOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + circle.remove(); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapCircleManager.java b/android/app/src/main/java/com/rn_mapview/AirMapCircleManager.java new file mode 100644 index 0000000000..5500869eda --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapCircleManager.java @@ -0,0 +1,72 @@ +package com.rn_mapview; + +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +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.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; +import com.google.android.gms.maps.model.LatLng; + +public class AirMapCircleManager extends ViewGroupManager { + private DisplayMetrics metrics; + + public AirMapCircleManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); + } + } + + @Override + public String getName() { + return "AIRMapCircle"; + } + + @Override + public AirMapCircle createViewInstance(ThemedReactContext context) { + return new AirMapCircle(context); + } + + @ReactProp(name = "center") + public void setCenter(AirMapCircle view, ReadableMap center) { + view.setCenter(new LatLng(center.getDouble("latitude"), center.getDouble("longitude"))); + } + + @ReactProp(name = "radius", defaultDouble = 0) + public void setRadius(AirMapCircle view, double radius) { + view.setRadius(radius); + } + + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapCircle view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setStrokeWidth(widthInScreenPx); + } + + @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") + public void setFillColor(AirMapCircle view, int color) { + view.setFillColor(color); + } + + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapCircle view, int color) { + view.setStrokeColor(color); + } + + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapCircle view, float zIndex) { + view.setZIndex(zIndex); + } + +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapFeature.java b/android/app/src/main/java/com/rn_mapview/AirMapFeature.java new file mode 100644 index 0000000000..ec21d01d8d --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapFeature.java @@ -0,0 +1,12 @@ +package com.rn_mapview; + +import com.facebook.react.views.view.ReactViewGroup; +import com.google.android.gms.maps.GoogleMap; +import android.content.Context; + +public abstract class AirMapFeature extends ReactViewGroup { + public AirMapFeature(Context context) { super(context); } + public abstract void addToMap(GoogleMap map); + public abstract void removeFromMap(GoogleMap map); + public abstract Object getFeature(); +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapManager.java b/android/app/src/main/java/com/rn_mapview/AirMapManager.java new file mode 100644 index 0000000000..b9fb55772d --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapManager.java @@ -0,0 +1,502 @@ +package com.rn_mapview; + +import android.graphics.Point; +import android.view.View; + +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.LifecycleEventListener; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.WritableNativeMap; +import com.facebook.react.common.MapBuilder; +import com.facebook.react.modules.core.DeviceEventManagerModule; +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.UIManagerModule; +import com.facebook.react.uimanager.ViewGroupManager; +import com.facebook.react.uimanager.events.EventDispatcher; +import com.facebook.react.uimanager.events.RCTEventEmitter; +import com.google.android.gms.maps.CameraUpdate; +import com.google.android.gms.maps.CameraUpdateFactory; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.GoogleMap.InfoWindowAdapter; +import com.google.android.gms.maps.MapView; +import com.google.android.gms.maps.MapsInitializer; +import com.google.android.gms.maps.Projection; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.Circle; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.Polyline; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + + +public class AirMapManager + extends ViewGroupManager + implements InfoWindowAdapter { + + public static final String REACT_CLASS = "AIRMap"; + + public static final int ANIMATE_TO_REGION = 1; + public static final int ANIMATE_TO_COORDINATE = 2; + public static final int FIT_TO_ELEMENTS = 3; + + private Map MAP_TYPES = MapBuilder.of( + "standard", GoogleMap.MAP_TYPE_NORMAL, + "satellite", GoogleMap.MAP_TYPE_SATELLITE, + "hybrid", GoogleMap.MAP_TYPE_HYBRID + ); + + // TODO(lmr): this is a bit of a hacky solution, and should be refactored. Under this + // implementation, basically only one can be shown at a time. This was done + // initially due to some annoyances with the fact that the GoogleMap class is different from the + // MapView class, and we really actually want the GoogleMap class. Ideally, we abstract this out + // into a `ReactMapView` class or something that has all of the APIs that the GoogleMap class has. + private MapView mView; + private GoogleMap map; + + private ReactContext reactContext; + private LatLngBounds boundsToMove; + + private ArrayList features = new ArrayList<>(); + private HashMap markerMap = new HashMap<>(); + private HashMap polylineMap = new HashMap<>(); + private HashMap polygonMap = new HashMap<>(); + private HashMap circleMap = new HashMap<>(); + + private AirMapMarkerManager annotationManager; + private AirMapPolylineManager polylineManager; + private AirMapPolygonManager polygonManager; + private AirMapCircleManager circleManager; + private boolean showUserLocation = false; + + public AirMapManager( + AirMapMarkerManager annotationManager, + AirMapPolylineManager polylineManager, + AirMapPolygonManager polygonManager, + AirMapCircleManager circleManager + ) { + super(); + this.annotationManager = annotationManager; + this.polylineManager = polylineManager; + this.polygonManager = polygonManager; + this.circleManager = circleManager; + } + + @Override + public String getName() { + return REACT_CLASS; + } + + public GoogleMap getMap() { + return map; + } + + @Override + protected MapView createViewInstance(ThemedReactContext context) { + reactContext = context; + mView = new MapView(context); + mView.onCreate(null); + mView.onResume(); + map = mView.getMap(); + + if (map == null) { + emitMapError("Map is null", "map_null"); + } else { + + try { + MapsInitializer.initialize(context.getApplicationContext()); + } catch (Exception e) { + e.printStackTrace(); + emitMapError("Map initialize error", "map_init_error"); + } + } + + map.setInfoWindowAdapter(this); + + // We need to be sure to disable location-tracking when app enters background, in-case some other module + // has acquired a wake-lock and is controlling location-updates, otherwise, location-manager will be left + // updating location constantly, killing the battery, even though some other location-mgmt module may + // desire to shut-down location-services. + LifecycleEventListener listener = new LifecycleEventListener() { + @Override + public void onHostResume() { + map.setMyLocationEnabled(showUserLocation); + } + + @Override + public void onHostPause() { + map.setMyLocationEnabled(false); + } + + @Override + public void onHostDestroy() { + + } + }; + + context.addLifecycleEventListener(listener); + + return mView; + } + + private void emitMapError (String message, String type) { + WritableMap error = Arguments.createMap(); + error.putString("message", message); + error.putString("type", type); + + reactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit("onError", error); + } + + @ReactProp(name="region") + public void setRegion(MapView view, ReadableMap region) { + Double lng = region.getDouble("longitude"); + Double lat = region.getDouble("latitude"); + Double lngDelta = region.getDouble("longitudeDelta"); + Double latDelta = region.getDouble("latitudeDelta"); + LatLngBounds bounds = new LatLngBounds( + new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest + new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast + ); + if (view.getHeight() <= 0 || view.getWidth() <= 0) { + // in this case, our map has not been laid out yet, so we save the bounds in a local + // variable, and make a guess of zoomLevel 10. Not to worry, though: as soon as layout + // occurs, we will move the camera to the saved bounds. Note that if we tried to move + // to the bounds now, it would trigger an exception. + map.moveCamera(CameraUpdateFactory.newLatLngZoom(new LatLng(lat, lng), 10)); + boundsToMove = bounds; + } else { + map.moveCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0)); + boundsToMove = null; + } + } + + @ReactProp(name="mapType") + public void setMapType(MapView view, @Nullable String mapType) { + int typeId = MAP_TYPES.get(mapType); + map.setMapType(typeId); + } + + @ReactProp(name="showsUserLocation", defaultBoolean = false) + public void setShowsUserLocation(MapView view, boolean showUserLocation) { + this.showUserLocation = showUserLocation; // hold onto this for lifecycle handling + map.setMyLocationEnabled(showUserLocation); + } + + @ReactProp(name="showsTraffic", defaultBoolean = false) + public void setShowTraffic(MapView view, boolean showTraffic) { + map.setTrafficEnabled(showTraffic); + } + + @ReactProp(name="showsBuildings", defaultBoolean = false) + public void setShowBuildings(MapView view, boolean showBuildings) { + map.setBuildingsEnabled(showBuildings); + } + + @ReactProp(name="showsIndoors", defaultBoolean = false) + public void setShowIndoors(MapView view, boolean showIndoors) { + map.setIndoorEnabled(showIndoors); + } + + @ReactProp(name="showsCompass", defaultBoolean = false) + public void setShowsCompass(MapView view, boolean showsCompass) { + map.getUiSettings().setCompassEnabled(showsCompass); + } + + @ReactProp(name="scrollEnabled", defaultBoolean = false) + public void setScrollEnabled(MapView view, boolean scrollEnabled) { + map.getUiSettings().setScrollGesturesEnabled(scrollEnabled); + } + + @ReactProp(name="zoomEnabled", defaultBoolean = false) + public void setZoomEnabled(MapView view, boolean zoomEnabled) { + map.getUiSettings().setZoomGesturesEnabled(zoomEnabled); + } + + @Override + public void addView(MapView parent, View child, int index) { + // Our desired API is to pass up annotations/overlays as children to the mapview component. + // This is where we intercept them and do the appropriate underlying mapview action. + if (child instanceof AirMapMarker) { + AirMapMarker annotation = (AirMapMarker) child; + annotation.addToMap(map); + features.add(index, annotation); + Marker marker = (Marker)annotation.getFeature(); + markerMap.put(marker, annotation); + } else if (child instanceof AirMapPolyline) { + AirMapPolyline polylineView = (AirMapPolyline) child; + polylineView.addToMap(map); + features.add(index, polylineView); + Polyline polyline = (Polyline)polylineView.getFeature(); + polylineMap.put(polyline, polylineView); + } else if (child instanceof AirMapPolygon) { + AirMapPolygon polygonView = (AirMapPolygon) child; + polygonView.addToMap(map); + features.add(index, polygonView); + Polygon polygon = (Polygon)polygonView.getFeature(); + polygonMap.put(polygon, polygonView); + } else if (child instanceof AirMapCircle) { + AirMapCircle circleView = (AirMapCircle) child; + circleView.addToMap(map); + features.add(index, circleView); + Circle circle = (Circle)circleView.getFeature(); + circleMap.put(circle, circleView); + } else { + parent.addView(child, index); + } + } + + @Override + public int getChildCount(MapView parent) { + return features.size(); + } + + @Override + public View getChildAt(MapView parent, int index) { + return features.get(index); + } + + @Override + public void removeViewAt(MapView parent, int index) { + AirMapFeature feature = features.remove(index); + feature.removeFromMap(map); + + if (feature instanceof AirMapMarker) { + markerMap.remove(feature.getFeature()); + } else if (feature instanceof AirMapPolyline) { + polylineMap.remove(feature.getFeature()); + } else if (feature instanceof AirMapPolygon) { + polygonMap.remove(feature.getFeature()); + } else if (feature instanceof AirMapCircle) { + circleMap.remove(feature.getFeature()); + } + } + + @Override + public void receiveCommand(MapView view, int commandId, @Nullable ReadableArray args) { + Integer duration; + Double lat; + Double lng; + Double lngDelta; + Double latDelta; + ReadableMap region; + + switch (commandId) { + case ANIMATE_TO_REGION: + region = args.getMap(0); + duration = args.getInt(1); + lng = region.getDouble("longitude"); + lat = region.getDouble("latitude"); + lngDelta = region.getDouble("longitudeDelta"); + latDelta = region.getDouble("latitudeDelta"); + LatLngBounds bounds = new LatLngBounds( + new LatLng(lat - latDelta / 2, lng - lngDelta / 2), // southwest + new LatLng(lat + latDelta / 2, lng + lngDelta / 2) // northeast + ); + map.animateCamera(CameraUpdateFactory.newLatLngBounds(bounds, 0), duration, null); + break; + + case ANIMATE_TO_COORDINATE: + region = args.getMap(0); + duration = args.getInt(1); + lng = region.getDouble("longitude"); + lat = region.getDouble("latitude"); + map.animateCamera(CameraUpdateFactory.newLatLng(new LatLng(lat, lng)), duration, null); + break; + + case FIT_TO_ELEMENTS: + fitToElements(args.getBoolean(0)); + break; + } + } + + + private void fitToElements(boolean animated) { + LatLngBounds.Builder builder = new LatLngBounds.Builder(); + for (AirMapFeature feature : features) { + if (feature instanceof AirMapMarker) { + Marker marker = (Marker)feature.getFeature(); + builder.include(marker.getPosition()); + } + // TODO(lmr): may want to include shapes / etc. + } + LatLngBounds bounds = builder.build(); + CameraUpdate cu = CameraUpdateFactory.newLatLngBounds(bounds, 50); + if (animated) { + map.animateCamera(cu); + } else { + map.moveCamera(cu); + } + } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress"), + "onLongPress", MapBuilder.of("registrationName", "onLongPress"), + "onMarkerPress", MapBuilder.of("registrationName", "onMarkerPress"), + "onMarkerSelect", MapBuilder.of("registrationName", "onMarkerSelect"), + "onMarkerDeselect", MapBuilder.of("registrationName", "onMarkerDeselect"), + "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress") + ); + } + + @Override + public @Nullable Map getCommandsMap() { + return MapBuilder.of( + "animateToRegion", ANIMATE_TO_REGION, + "animateToCoordinate", ANIMATE_TO_COORDINATE, + "fitToElements", FIT_TO_ELEMENTS + ); + } + + @Override + protected void addEventEmitters(ThemedReactContext reactContext, final MapView view) { + final EventDispatcher eventDispatcher = + reactContext.getNativeModule(UIManagerModule.class).getEventDispatcher(); + + map.setOnMarkerClickListener(new GoogleMap.OnMarkerClickListener() { + @Override + public boolean onMarkerClick(Marker marker) { + WritableMap event; + + event = makeClickEventData(marker.getPosition()); + event.putString("action", "marker-press"); + pushEvent(view, "onMarkerPress", event); + + event = makeClickEventData(marker.getPosition()); + event.putString("action", "marker-press"); + pushEvent(markerMap.get(marker), "onPress", event); + + return false; // returning false opens the callout window, if possible + } + }); + + map.setOnInfoWindowClickListener(new GoogleMap.OnInfoWindowClickListener() { + @Override + public void onInfoWindowClick(Marker marker) { + WritableMap event; + + event = makeClickEventData(marker.getPosition()); + event.putString("action", "callout-press"); + pushEvent(view, "onCalloutPress", event); + + event = makeClickEventData(marker.getPosition()); + event.putString("action", "callout-press"); + AirMapMarker markerView = markerMap.get(marker); + pushEvent(markerView, "onCalloutPress", event); + + event = makeClickEventData(marker.getPosition()); + event.putString("action", "callout-press"); + AirMapCallout infoWindow = markerView.getCalloutView(); + if (infoWindow != null) pushEvent(infoWindow, "onPress", event); + } + }); + + map.setOnMapClickListener(new GoogleMap.OnMapClickListener() { + @Override + public void onMapClick(LatLng point) { + WritableMap event = makeClickEventData(point); + event.putString("action", "press"); + pushEvent(view, "onPress", event); + } + }); + + map.setOnMapLongClickListener(new GoogleMap.OnMapLongClickListener() { + @Override + public void onMapLongClick(LatLng point) { + WritableMap event = makeClickEventData(point); + event.putString("action", "long-press"); + pushEvent(view, "onLongPress", makeClickEventData(point)); + } + }); + + map.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { + @Override + public void onCameraChange(CameraPosition position) { + LatLngBounds bounds = map.getProjection().getVisibleRegion().latLngBounds; + eventDispatcher.dispatchEvent(new RegionChangeEvent(view.getId(), bounds)); + } + }); + } + + @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 AirMapShadowNode(); + } + + @Override + public void updateExtraData(MapView view, Object extraData) { + // if boundsToMove is not null, we now have the MapView's width/height, so we can apply + // a proper camera move + if (boundsToMove != null) { + HashMap data = (HashMap)extraData; + float width = data.get("width"); + float height = data.get("height"); + map.moveCamera( + CameraUpdateFactory.newLatLngBounds( + boundsToMove, + (int)width, + (int)height, + 0 + ) + ); + boundsToMove = null; + } + } + + // InfoWindowAdapter interface + + @Override + public View getInfoWindow(Marker marker) { + AirMapMarker annotation = markerMap.get(marker); + return annotation.getCallout(); + } + + @Override + public View getInfoContents(Marker marker) { + AirMapMarker annotation = markerMap.get(marker); + return annotation.getInfoContents(); + } + + + private void pushEvent(View view, String name, WritableMap data) { + ReactContext reactContext = (ReactContext) view.getContext(); + reactContext.getJSModule(RCTEventEmitter.class) + .receiveEvent(view.getId(), name, data); + } + + private WritableMap makeClickEventData(LatLng point) { + WritableMap event = new WritableNativeMap(); + + WritableMap coordinate = new WritableNativeMap(); + coordinate.putDouble("latitude", point.latitude); + coordinate.putDouble("longitude", point.longitude); + event.putMap("coordinate", coordinate); + + Projection projection = map.getProjection(); + Point screenPoint = projection.toScreenLocation(point); + + WritableMap position = new WritableNativeMap(); + position.putDouble("x", screenPoint.x); + position.putDouble("y", screenPoint.y); + event.putMap("position", position); + + return event; + } + +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapMarker.java b/android/app/src/main/java/com/rn_mapview/AirMapMarker.java new file mode 100644 index 0000000000..57f27ac5d0 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapMarker.java @@ -0,0 +1,270 @@ +package com.rn_mapview; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.view.View; +import android.widget.LinearLayout; + +import com.facebook.react.bridge.ReadableMap; +import com.google.android.gms.maps.GoogleMap; +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; + +public class AirMapMarker extends AirMapFeature { + + private MarkerOptions markerOptions; + private Marker marker; + private int width; + private int height; + + private LatLng position; + private String title; + private String snippet; + + private boolean anchorIsSet; + private float anchorX; + private float anchorY; + + private AirMapCallout calloutView; + private View wrappedCalloutView; + private Context context; + + private float markerHue = 0.0f; // should be between 0 and 360 + private Integer iconResourceId; + + private float rotation = 0.0f; + private boolean flat = false; + + private float calloutAnchorX; + private float calloutAnchorY; + private boolean calloutAnchorIsSet; + + private boolean hasCustomMarkerView = false; + + public AirMapMarker(Context context) { + super(context); + this.context = context; + } + + public void setCoordinate(ReadableMap coordinate) { + position = new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude")); + if (marker != null) { + marker.setPosition(position); + } + } + + public void setTitle(String title) { + this.title = title; + if (marker != null) { + marker.setTitle(title); + } + } + + public void setSnippet(String snippet) { + this.snippet = snippet; + if (marker != null) { + marker.setSnippet(snippet); + } + } + + public void setRotation(float rotation) { + this.rotation = rotation; + if (marker != null) { + marker.setRotation(rotation); + } + } + + public void setFlat(boolean flat) { + this.flat = flat; + if (marker != null) { + marker.setFlat(flat); + } + } + + public void setMarkerHue(float markerHue) { + this.markerHue = markerHue; + update(); + } + + public void setAnchor(double x, double y) { + anchorIsSet = true; + anchorX = (float)x; + anchorY = (float)y; + if (marker != null) { + marker.setAnchor(anchorX, anchorY); + } + } + + public void setCalloutAnchor(double x, double y) { + calloutAnchorIsSet = true; + calloutAnchorX = (float)x; + calloutAnchorY = (float)y; + if (marker != null) { + marker.setInfoWindowAnchor(calloutAnchorX, calloutAnchorY); + } + } + + public void setImage(ReadableMap image) { + String name = image.getString("uri"); + name = name.toLowerCase().replace("-", "_"); + iconResourceId = context.getResources().getIdentifier( + name, + "drawable", + context.getPackageName()); + update(); + } + + public MarkerOptions getMarkerOptions() { + if (markerOptions == null) { + markerOptions = createMarkerOptions(); + } + return markerOptions; + } + + @Override + public void addView(View child, int index) { + super.addView(child, index); + // if children are added, it means we are rendering a custom marker + hasCustomMarkerView = true; + update(); + } + + @Override + public Object getFeature() { + return marker; + } + + @Override + public void addToMap(GoogleMap map) { + marker = map.addMarker(getMarkerOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + marker.remove(); + } + + private BitmapDescriptor getIcon() { + if (hasCustomMarkerView) { + // creating a bitmap from an arbitrary view + return BitmapDescriptorFactory.fromBitmap(createDrawable()); + } else if (iconResourceId != null) { + // use local image as a marker + return BitmapDescriptorFactory.fromResource(iconResourceId); + } else { + // render the default marker pin + return BitmapDescriptorFactory.defaultMarker(this.markerHue); + } + } + + private MarkerOptions createMarkerOptions() { + MarkerOptions options = new MarkerOptions().position(position); + if (anchorIsSet) options.anchor(anchorX, anchorY); + if (calloutAnchorIsSet) options.anchor(calloutAnchorX, calloutAnchorY); + options.title(title); + options.snippet(snippet); + options.rotation(rotation); + options.flat(flat); + options.icon(getIcon()); + return options; + } + + public void update() { + if (marker == null) { + return; + } + + marker.setIcon(getIcon()); + } + + public void update(int width, int height) { + this.width = width; + this.height = height; + update(); + } + + private Bitmap createDrawable() { + int width = this.width <= 0 ? 100 : this.width; + int height = this.height <= 0 ? 100 : this.height; + this.buildDrawingCache(); + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + + Canvas canvas = new Canvas(bitmap); + this.draw(canvas); + + return bitmap; + } + + public void setCalloutView(AirMapCallout view) { + hasCustomMarkerView = view != null; + this.calloutView = view; + } + + public AirMapCallout getCalloutView() { + return this.calloutView; + } + + public View getCallout() { + if (this.calloutView == null) return null; + + if (this.wrappedCalloutView == null) { + this.wrapCalloutView(); + } + + if (this.calloutView.getTooltip()) { + return this.wrappedCalloutView; + } else { + return null; + } + } + + public View getInfoContents() { + if (this.calloutView == null) return null; + + if (this.wrappedCalloutView == null) { + this.wrapCalloutView(); + } + + if (this.calloutView.getTooltip()) { + return null; + } else { + return this.wrappedCalloutView; + } + } + + private void wrapCalloutView() { + // some hackery is needed to get the arbitrary infowindow view to render centered, and + // with only the width/height that it needs. + if (this.calloutView == null || this.calloutView.getChildCount() == 0) { + return; + } + + LinearLayout LL = new LinearLayout(context); + LL.setOrientation(LinearLayout.VERTICAL); + LL.setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + )); + + + LinearLayout LL2 = new LinearLayout(context); + LL2.setOrientation(LinearLayout.HORIZONTAL); + LL2.setLayoutParams(new LayoutParams( + LayoutParams.WRAP_CONTENT, + LayoutParams.WRAP_CONTENT + )); + + + View child = this.calloutView.getChildAt(0); + this.calloutView.removeView(child); + + LL.addView(LL2); + LL2.addView(child); + + this.wrappedCalloutView = LL; + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapMarkerManager.java b/android/app/src/main/java/com/rn_mapview/AirMapMarkerManager.java new file mode 100644 index 0000000000..8c25336faa --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapMarkerManager.java @@ -0,0 +1,174 @@ +package com.rn_mapview; + +import android.graphics.Color; +import android.view.View; + +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.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; +import com.google.android.gms.maps.model.Marker; + +import java.util.HashMap; +import java.util.Map; + +import javax.annotation.Nullable; + +public class AirMapMarkerManager extends ViewGroupManager { + + public static final int SHOW_INFO_WINDOW = 1; + public static final int HIDE_INFO_WINDOW = 2; + + private AirMapCalloutManager calloutManager; + + public AirMapMarkerManager(AirMapCalloutManager calloutManager) { + super(); + this.calloutManager = calloutManager; + } + + @Override + public String getName() { + return "AIRMapMarker"; + } + + @Override + public AirMapMarker createViewInstance(ThemedReactContext context) { + return new AirMapMarker(context); + } + + @ReactProp(name = "coordinate") + public void setCoordinate(AirMapMarker view, ReadableMap map) { + view.setCoordinate(map); + } + + @ReactProp(name = "title") + public void setTitle(AirMapMarker view, String title) { + view.setTitle(title); + } + + @ReactProp(name = "description") + public void setDescription(AirMapMarker view, String description) { + view.setSnippet(description); + } + + // NOTE(lmr): + // android uses normalized coordinate systems for this, and is provided through the + // `anchor` property and `calloutAnchor` instead. Perhaps some work could be done + // to normalize iOS and android to use just one of the systems. +// @ReactProp(name = "centerOffset") +// public void setCenterOffset(AirMapMarker view, ReadableMap map) { +// +// } +// +// @ReactProp(name = "calloutOffset") +// public void setCalloutOffset(AirMapMarker view, ReadableMap map) { +// +// } + + @ReactProp(name = "anchor") + public void setAnchor(AirMapMarker view, ReadableMap map) { + // should default to (0.5, 1) (bottom middle) + double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; + double y = map != null && map.hasKey("y") ? map.getDouble("y") : 1.0; + view.setAnchor(x, y); + } + + @ReactProp(name = "calloutAnchor") + public void setCalloutAnchor(AirMapMarker view, ReadableMap map) { + // should default to (0.5, 0) (top middle) + double x = map != null && map.hasKey("x") ? map.getDouble("x") : 0.5; + double y = map != null && map.hasKey("y") ? map.getDouble("y") : 0.0; + view.setCalloutAnchor(x, y); + } + + @ReactProp(name = "image") + public void setImage(AirMapMarker view, ReadableMap image) { + view.setImage(image); + } + + @ReactProp(name = "pinColor", defaultInt = Color.RED, customType = "Color") + public void setPinColor(AirMapMarker view, int pinColor) { + float[] hsv = new float[3]; + Color.colorToHSV(pinColor, hsv); + // NOTE: android only supports a hue + view.setMarkerHue(hsv[0]); + } + + @ReactProp(name = "rotation", defaultFloat = 0.0f) + public void setMarkerRotation(AirMapMarker view, float rotation) { + view.setRotation(rotation); + } + + @ReactProp(name = "flat", defaultBoolean = false) + public void setMarkerRotation(AirMapMarker view, boolean flat) { + view.setFlat(flat); + } + + @Override + public void addView(AirMapMarker parent, View child, int index) { + // if an component is a child, then it is a callout view, NOT part of the + // marker. + if (child instanceof AirMapCallout) { + parent.setCalloutView((AirMapCallout)child); + } else { + super.addView(parent, child, index); + parent.update(); + } + } + + @Override + public void removeViewAt(AirMapMarker parent, int index) { + super.removeViewAt(parent, index); + parent.update(); + } + + @Override + public @Nullable Map getCommandsMap() { + return MapBuilder.of( + "showCallout", SHOW_INFO_WINDOW, + "hideCallout", HIDE_INFO_WINDOW + ); + } + + @Override + public void receiveCommand(AirMapMarker view, int commandId, @Nullable ReadableArray args) { + switch (commandId) { + case SHOW_INFO_WINDOW: + ((Marker)view.getFeature()).showInfoWindow(); + break; + + case HIDE_INFO_WINDOW: + ((Marker)view.getFeature()).hideInfoWindow(); + break; + } + } + + @Override + public @Nullable Map getExportedCustomDirectEventTypeConstants() { + return MapBuilder.of( + "onPress", MapBuilder.of("registrationName", "onPress"), + "onCalloutPress", MapBuilder.of("registrationName", "onCalloutPress") + ); + } + + @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 AirMapMarkerShadowNode(); + } + + @Override + public void updateExtraData(AirMapMarker view, Object extraData) { + // This method is called from the shadow node with the width/height of the rendered + // marker view. + HashMap data = (HashMap)extraData; + float width = data.get("width"); + float height = data.get("height"); + view.update((int) width, (int) height); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapMarkerShadowNode.java b/android/app/src/main/java/com/rn_mapview/AirMapMarkerShadowNode.java new file mode 100644 index 0000000000..87f467781b --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapMarkerShadowNode.java @@ -0,0 +1,29 @@ +/** + * 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.rn_mapview; + +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.UIViewOperationQueue; + +import java.util.HashMap; + +// Custom LayoutShadowNode implementation used in conjunction with the AirMapMarker +// which sends the width/height of the view after layout occurs. +public class AirMapMarkerShadowNode extends LayoutShadowNode { + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + super.onCollectExtraUpdates(uiViewOperationQueue); + HashMap data = new HashMap(); + data.put("width", getLayoutWidth()); + data.put("height", getLayoutHeight()); + + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapPolygon.java b/android/app/src/main/java/com/rn_mapview/AirMapPolygon.java new file mode 100644 index 0000000000..efe6865cac --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapPolygon.java @@ -0,0 +1,110 @@ +package com.rn_mapview; + +import android.content.Context; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Polygon; +import com.google.android.gms.maps.model.PolygonOptions; + +import java.util.ArrayList; + +public class AirMapPolygon extends AirMapFeature { + + private PolygonOptions polygonOptions; + private Polygon polygon; + + private ArrayList coordinates; + private int strokeColor; + private int fillColor; + private float strokeWidth; + private boolean geodesic; + private float zIndex; + + public AirMapPolygon(Context context) { + super(context); + } + + public void setCoordinates(ReadableArray coordinates) { + // it's kind of a bummer that we can't run map() or anything on the ReadableArray + this.coordinates = new ArrayList<>(coordinates.size()); + for (int i = 0; i < coordinates.size(); i++) { + ReadableMap coordinate = coordinates.getMap(i); + this.coordinates.add(i, + new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); + } + if (polygon != null) { + polygon.setPoints(this.coordinates); + } + } + + public void setFillColor(int color) { + this.fillColor = color; + if (polygon != null) { + polygon.setFillColor(color); + } + } + + public void setStrokeColor(int color) { + this.strokeColor = color; + if (polygon != null) { + polygon.setStrokeColor(color); + } + } + + public void setStrokeWidth(float width) { + this.strokeWidth = width; + if (polygon != null) { + polygon.setStrokeWidth(width); + } + } + + public void setGeodesic(boolean geodesic) { + this.geodesic = geodesic; + if (polygon != null) { + polygon.setGeodesic(geodesic); + } + } + + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (polygon != null) { + polygon.setZIndex(zIndex); + } + } + + public PolygonOptions getPolygonOptions() { + if (polygonOptions == null) { + polygonOptions = createPolygonOptions(); + } + return polygonOptions; + } + + private PolygonOptions createPolygonOptions() { + PolygonOptions options = new PolygonOptions(); + options.addAll(coordinates); + options.fillColor(fillColor); + options.strokeColor(strokeColor); + options.strokeWidth(strokeWidth); + options.geodesic(geodesic); + options.zIndex(zIndex); + return options; + } + + @Override + public Object getFeature() { + return polygon; + } + + @Override + public void addToMap(GoogleMap map) { + polygon = map.addPolygon(getPolygonOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + polygon.remove(); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapPolygonManager.java b/android/app/src/main/java/com/rn_mapview/AirMapPolygonManager.java new file mode 100644 index 0000000000..f8b7acc4e5 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapPolygonManager.java @@ -0,0 +1,70 @@ +package com.rn_mapview; + +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; + +public class AirMapPolygonManager extends ViewGroupManager { + private DisplayMetrics metrics; + + public AirMapPolygonManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); + } + } + + @Override + public String getName() { + return "AIRMapPolygon"; + } + + @Override + public AirMapPolygon createViewInstance(ThemedReactContext context) { + return new AirMapPolygon(context); + } + + @ReactProp(name = "coordinates") + public void setCoordinate(AirMapPolygon view, ReadableArray coordinates) { + view.setCoordinates(coordinates); + } + + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapPolygon view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setStrokeWidth(widthInScreenPx); + } + + @ReactProp(name = "fillColor", defaultInt = Color.RED, customType = "Color") + public void setFillColor(AirMapPolygon view, int color) { + view.setFillColor(color); + } + + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapPolygon view, int color) { + view.setStrokeColor(color); + } + + @ReactProp(name = "geodesic", defaultBoolean = false) + public void setGeodesic(AirMapPolygon view, boolean geodesic) { + view.setGeodesic(geodesic); + } + + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapPolygon view, float zIndex) { + view.setZIndex(zIndex); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapPolyline.java b/android/app/src/main/java/com/rn_mapview/AirMapPolyline.java new file mode 100644 index 0000000000..1c548de770 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapPolyline.java @@ -0,0 +1,100 @@ +package com.rn_mapview; + +import android.content.Context; + +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.ReadableMap; +import com.google.android.gms.maps.GoogleMap; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Polyline; +import com.google.android.gms.maps.model.PolylineOptions; + +import java.util.ArrayList; + +public class AirMapPolyline extends AirMapFeature { + + private PolylineOptions polylineOptions; + private Polyline polyline; + + private ArrayList coordinates; + private int color; + private float width; + private boolean geodesic; + private float zIndex; + + public AirMapPolyline(Context context) { + super(context); + } + + public void setCoordinates(ReadableArray coordinates) { + this.coordinates = new ArrayList(coordinates.size()); + for (int i = 0; i < coordinates.size(); i++) { + ReadableMap coordinate = coordinates.getMap(i); + this.coordinates.add(i, + new LatLng(coordinate.getDouble("latitude"), coordinate.getDouble("longitude"))); + } + if (polyline != null) { + polyline.setPoints(this.coordinates); + } + } + + public void setColor(int color) { + this.color = color; + if (polyline != null) { + polyline.setColor(color); + } + } + + public void setWidth(float width) { + this.width = width; + if (polyline != null) { + polyline.setWidth(width); + } + } + + public void setZIndex(float zIndex) { + this.zIndex = zIndex; + if (polyline != null) { + polyline.setZIndex(zIndex); + } + } + + public void setGeodesic(boolean geodesic) { + this.geodesic = geodesic; + if (polyline != null) { + polyline.setGeodesic(geodesic); + } + } + + public PolylineOptions getPolylineOptions() { + if (polylineOptions == null) { + polylineOptions = createPolylineOptions(); + } + return polylineOptions; + } + + private PolylineOptions createPolylineOptions() { + PolylineOptions options = new PolylineOptions(); + options.addAll(coordinates); + options.color(color); + options.width(width); + options.geodesic(geodesic); + options.zIndex(zIndex); + return options; + } + + @Override + public Object getFeature() { + return polyline; + } + + @Override + public void addToMap(GoogleMap map) { + polyline = map.addPolyline(getPolylineOptions()); + } + + @Override + public void removeFromMap(GoogleMap map) { + polyline.remove(); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapPolylineManager.java b/android/app/src/main/java/com/rn_mapview/AirMapPolylineManager.java new file mode 100644 index 0000000000..4f467a14ca --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapPolylineManager.java @@ -0,0 +1,65 @@ +package com.rn_mapview; + +import android.content.Context; +import android.graphics.Color; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.uimanager.ReactProp; +import com.facebook.react.uimanager.ThemedReactContext; +import com.facebook.react.uimanager.ViewGroupManager; + +public class AirMapPolylineManager extends ViewGroupManager { + private DisplayMetrics metrics; + + public AirMapPolylineManager(ReactApplicationContext reactContext) { + super(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + metrics = new DisplayMetrics(); + ((WindowManager) reactContext.getSystemService(Context.WINDOW_SERVICE)) + .getDefaultDisplay() + .getRealMetrics(metrics); + } else { + metrics = reactContext.getResources().getDisplayMetrics(); + } + } + + @Override + public String getName() { + return "AIRMapPolyline"; + } + + @Override + public AirMapPolyline createViewInstance(ThemedReactContext context) { + return new AirMapPolyline(context); + } + + @ReactProp(name = "coordinates") + public void setCoordinate(AirMapPolyline view, ReadableArray coordinates) { + view.setCoordinates(coordinates); + } + + @ReactProp(name = "strokeWidth", defaultFloat = 1f) + public void setStrokeWidth(AirMapPolyline view, float widthInPoints) { + float widthInScreenPx = metrics.density * widthInPoints; // done for parity with iOS + view.setWidth(widthInScreenPx); + } + + @ReactProp(name = "strokeColor", defaultInt = Color.RED, customType = "Color") + public void setStrokeColor(AirMapPolyline view, int color) { + view.setColor(color); + } + + @ReactProp(name = "geodesic", defaultBoolean = false) + public void setGeodesic(AirMapPolyline view, boolean geodesic) { + view.setGeodesic(geodesic); + } + + @ReactProp(name = "zIndex", defaultFloat = 1.0f) + public void setZIndex(AirMapPolyline view, float zIndex) { + view.setZIndex(zIndex); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirMapShadowNode.java b/android/app/src/main/java/com/rn_mapview/AirMapShadowNode.java new file mode 100644 index 0000000000..8d1e895925 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirMapShadowNode.java @@ -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.rn_mapview; + +import com.facebook.react.uimanager.LayoutShadowNode; +import com.facebook.react.uimanager.UIViewOperationQueue; + +import java.util.HashMap; + +// Custom LayoutShadowNode implementation used in conjunction with the AirMapManager +// which sends the width/height of the view after layout occurs. +public class AirMapShadowNode extends LayoutShadowNode { + + @Override + public void onCollectExtraUpdates(UIViewOperationQueue uiViewOperationQueue) { + super.onCollectExtraUpdates(uiViewOperationQueue); + + HashMap data = new HashMap<>(); + data.put("width", getLayoutWidth()); + data.put("height", getLayoutHeight()); + + uiViewOperationQueue.enqueueUpdateExtraData(getReactTag(), data); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/AirPackage.java b/android/app/src/main/java/com/rn_mapview/AirPackage.java new file mode 100644 index 0000000000..4b10523bd3 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/AirPackage.java @@ -0,0 +1,47 @@ +package com.rn_mapview; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.JavaScriptModule; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class AirPackage implements ReactPackage { + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List> createJSModules() { + return Collections.emptyList(); + } + + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + AirMapCalloutManager calloutManager = new AirMapCalloutManager(); + AirMapMarkerManager annotationManager = new AirMapMarkerManager(calloutManager); + AirMapPolylineManager polylineManager = new AirMapPolylineManager(reactContext); + AirMapPolygonManager polygonManager = new AirMapPolygonManager(reactContext); + AirMapCircleManager circleManager = new AirMapCircleManager(reactContext); + AirMapManager mapManager = new AirMapManager( + annotationManager, + polylineManager, + polygonManager, + circleManager + ); + + return Arrays.asList( + calloutManager, + annotationManager, + polylineManager, + polygonManager, + circleManager, + mapManager + ); + } +} diff --git a/android/app/src/main/java/com/rn_mapview/MainActivity.java b/android/app/src/main/java/com/rn_mapview/MainActivity.java new file mode 100644 index 0000000000..28d4c6717b --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/MainActivity.java @@ -0,0 +1,115 @@ +package com.rn_mapview; + +import android.app.Activity; +import android.os.Bundle; +import android.view.KeyEvent; + +import com.facebook.react.LifecycleState; +import com.facebook.react.ReactInstanceManager; +import com.facebook.react.ReactRootView; +import com.facebook.react.modules.core.DefaultHardwareBackBtnHandler; +import com.facebook.react.shell.MainReactPackage; + +import com.google.android.gms.common.api.GoogleApiClient; +import com.google.android.gms.common.api.GoogleApiClient.ConnectionCallbacks; +import com.google.android.gms.common.api.GoogleApiClient.OnConnectionFailedListener; + +public class MainActivity + extends Activity + implements DefaultHardwareBackBtnHandler { + + private ReactInstanceManager reactInstanceManager; + private ReactRootView reactRootView; + private GoogleApiClient mGoogleApiClient; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + reactRootView = new ReactRootView(this); + + reactInstanceManager = ReactInstanceManager.builder() + .setApplication(getApplication()) + .setBundleAssetName("index.android.bundle") + .setJSMainModuleName("index.android") + .addPackage(new MainReactPackage()) + .addPackage(new AirPackage()) + .setUseDeveloperSupport(BuildConfig.DEBUG) + .setInitialLifecycleState(LifecycleState.RESUMED) + .build(); + + reactRootView.startReactApplication(reactInstanceManager, "rn_mapview", null); + + // Create a GoogleApiClient instance +// mGoogleApiClient = new GoogleApiClient.Builder(this) +// .addApi(Drive.API) +// .addScope(Drive.SCOPE_FILE) +// .addConnectionCallbacks(this) +// .addOnConnectionFailedListener(this) +// .build(); + + setContentView(reactRootView); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_MENU && reactInstanceManager != null) { + reactInstanceManager.showDevOptionsDialog(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public void onBackPressed() { + if (reactInstanceManager != null) { + reactInstanceManager.onBackPressed(); + } else { + super.onBackPressed(); + } + } + + @Override + public void invokeDefaultOnBackPressed() { + super.onBackPressed(); + } + + @Override + protected void onPause() { + super.onPause(); + + if (reactInstanceManager != null) { + reactInstanceManager.onPause(); + } + } + + @Override + protected void onResume() { + super.onResume(); + + if (reactInstanceManager != null) { + reactInstanceManager.onResume(this, this); + } + } + +// @Override +// public void onConnected(Bundle connectionHint) { +// // Connected to Google Play services! +// // The good stuff goes here. +// } +// +// @Override +// public void onConnectionSuspended(int cause) { +// // The connection has been interrupted. +// // Disable any UI components that depend on Google APIs +// // until onConnected() is called. +// } +// +// @Override +// public void onConnectionFailed(ConnectionResult result) { +// // This callback is important for handling errors that +// // may occur while attempting to connect with Google. +// // +// // More about this in the 'Handle Connection Failures' section. +// +// } +} diff --git a/android/app/src/main/java/com/rn_mapview/RegionChangeEvent.java b/android/app/src/main/java/com/rn_mapview/RegionChangeEvent.java new file mode 100644 index 0000000000..3120add0b8 --- /dev/null +++ b/android/app/src/main/java/com/rn_mapview/RegionChangeEvent.java @@ -0,0 +1,44 @@ +package com.rn_mapview; + +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 { + private final LatLngBounds bounds; + + public RegionChangeEvent(int id, LatLngBounds bounds) { + super(id, System.currentTimeMillis()); + this.bounds = bounds; + } + + @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", true); + + 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); + + rctEventEmitter.receiveEvent(getViewTag(), getEventName(), event); + } +} diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..dc5ce9097c --- /dev/null +++ b/android/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + rn_mapview + diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000000..319eb0ca10 --- /dev/null +++ b/android/app/src/main/res/values/styles.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000000..bdb0fcc6da --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,23 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:1.3.1' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + mavenLocal() + jcenter() + jcenter { + url "http://dl.bintray.com/mkonicek/maven" + } + } +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000000..1fd964e90b --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# Default value: -Xmx10248m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + +android.useDeprecatedNdk=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..b5166dad4d90021f6a0b45268c0755719f1d5cd4 GIT binary patch literal 52266 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 z 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 literal 0 HcmV?d00001 diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..b9fbfaba0e --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip diff --git a/android/gradlew b/android/gradlew new file mode 100755 index 0000000000..91a7e269e1 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +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" + which java >/dev/null 2>&1 || 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 + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000000..aec99730b4 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,90 @@ +@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 + +@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= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +: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 %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="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! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/rn_mapview.iml b/android/rn_mapview.iml new file mode 100644 index 0000000000..acfbe6f12d --- /dev/null +++ b/android/rn_mapview.iml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000000..29140bd4e0 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,3 @@ +rootProject.name = 'rn_mapview' + +include ':app' diff --git a/components/AnimatedRegion.js b/components/AnimatedRegion.js new file mode 100644 index 0000000000..d2f633a60c --- /dev/null +++ b/components/AnimatedRegion.js @@ -0,0 +1,171 @@ +class AnimatedRegion extends AnimatedWithChildren { + //latitude: AnimatedValue; + //longitude: AnimatedValue; + //latitudeDelta: AnimatedValue; + //longitudeDelta: AnimatedValue; + //_listeners: {[key: string]: { + // latitude: string, + // longitude: string, + // latitudeDelta: string; + // longitudeDelta: string, + //}}; + + constructor(valueIn) { + super(); + var value = valueIn || { // probably want to come up with better defaults + latitude: 0, + longitude: 0, + latitudeDelta: 0, + longitudeDelta: 0, + }; + this.latitude = value.latitude instanceof Animated + ? value.latitude + : new AnimatedValue(value.latitude); + this.longitude = value.longitude instanceof Animated + ? value.longitude + : new AnimatedValue(value.longitude); + this.latitudeDelta = value.latitudeDelta instanceof Animated + ? value.latitudeDelta + : new AnimatedValue(value.latitudeDelta); + this.longitudeDelta = value.longitudeDelta instanceof Animated + ? value.longitudeDelta + : new AnimatedValue(value.longitudeDelta); + this._listeners = {}; + } + + setValue(value) { + //this.latitude.setValue(value.latitude); + //this.longitude.setValue(value.longitude); + //this.latitudeDelta.setValue(value.latitudeDelta); + //this.longitudeDelta.setValue(value.longitudeDelta); + this.latitude._value = value.latitude; + this.longitude._value = value.longitude; + this.latitudeDelta._value = value.latitudeDelta; + this.longitudeDelta._value = value.longitudeDelta; + } + + setOffset(offset) { + this.latitude.setOffset(offset.latitude); + this.longitude.setOffset(offset.longitude); + this.latitudeDelta.setOffset(offset.latitudeDelta); + this.longitudeDelta.setOffset(offset.longitudeDelta); + } + + flattenOffset() { + this.latitude.flattenOffset(); + this.longitude.flattenOffset(); + this.latitudeDelta.flattenOffset(); + this.longitudeDelta.flattenOffset(); + } + + __getValue() { + return { + latitude: this.latitude.__getValue(), + longitude: this.longitude.__getValue(), + latitudeDelta: this.latitudeDelta.__getValue(), + longitudeDelta: this.longitudeDelta.__getValue(), + }; + } + + __attach() { + this.latitude.__addChild(this); + this.longitude.__addChild(this); + this.latitudeDelta.__addChild(this); + this.longitudeDelta.__addChild(this); + } + + __detach() { + this.latitude.__removeChild(this); + this.longitude.__removeChild(this); + this.latitudeDelta.__removeChild(this); + this.longitudeDelta.__removeChild(this); + } + + stopAnimation(callback) { + this.latitude.stopAnimation(); + this.longitude.stopAnimation(); + this.latitudeDelta.stopAnimation(); + this.longitudeDelta.stopAnimation(); + callback && callback(this.__getValue()); + } + + addListener(callback) { + var id = String(_uniqueId++); + var jointCallback = ({value: number}) => { + callback(this.__getValue()); + }; + this._listeners[id] = { + latitude: this.latitude.addListener(jointCallback), + longitude: this.longitude.addListener(jointCallback), + latitudeDelta: this.latitudeDelta.addListener(jointCallback), + longitudeDelta: this.longitudeDelta.addListener(jointCallback), + }; + return id; + } + + removeListener(id) { + this.latitude.removeListener(this._listeners[id].latitude); + this.longitude.removeListener(this._listeners[id].longitude); + this.latitudeDelta.removeListener(this._listeners[id].latitudeDelta); + this.longitudeDelta.removeListener(this._listeners[id].longitudeDelta); + delete this._listeners[id]; + } + + spring(config) { + var animations = []; + config.hasOwnProperty('latitude') && + animations.push(timing(this.latitude, { + ...config, + toValue: config.latitude, + })); + + config.hasOwnProperty('longitude') && + animations.push(timing(this.longitude, { + ...config, + toValue: config.longitude, + })); + + config.hasOwnProperty('latitudeDelta') && + animations.push(timing(this.latitudeDelta, { + ...config, + toValue: config.latitudeDelta, + })); + + config.hasOwnProperty('longitudeDelta') && + animations.push(timing(this.longitudeDelta, { + ...config, + toValue: config.longitudeDelta, + })); + + return parallel(animations); + } + + timing(config) { + var animations = []; + config.hasOwnProperty('latitude') && + animations.push(timing(this.latitude, { + ...config, + toValue: config.latitude, + })); + + config.hasOwnProperty('longitude') && + animations.push(timing(this.longitude, { + ...config, + toValue: config.longitude, + })); + + config.hasOwnProperty('latitudeDelta') && + animations.push(timing(this.latitudeDelta, { + ...config, + toValue: config.latitudeDelta, + })); + + config.hasOwnProperty('longitudeDelta') && + animations.push(timing(this.longitudeDelta, { + ...config, + toValue: config.longitudeDelta, + })); + + return parallel(animations); + } +} diff --git a/components/MapCallout.js b/components/MapCallout.js new file mode 100644 index 0000000000..88fe08b5f2 --- /dev/null +++ b/components/MapCallout.js @@ -0,0 +1,41 @@ + +var React = require('react-native'); +var { + View, + NativeMethodsMixin, + requireNativeComponent, + StyleSheet, + PropTypes, +} = React; + +var MapCallout = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + tooltip: PropTypes.bool, + onPress: PropTypes.func, + }, + + getDefaultProps: function() { + return { + tooltip: false, + }; + }, + + render: function() { + return ; + }, +}); + +var styles = StyleSheet.create({ + callout: { + position: 'absolute', + //flex: 0, + //backgroundColor: 'transparent', + }, +}); + +var AIRMapCallout = requireNativeComponent('AIRMapCallout', MapCallout); + +module.exports = MapCallout; diff --git a/components/MapCircle.js b/components/MapCircle.js new file mode 100644 index 0000000000..79b81c61b0 --- /dev/null +++ b/components/MapCircle.js @@ -0,0 +1,122 @@ + +var React = require('react-native'); +var { + View, + NativeMethodsMixin, + requireNativeComponent, + StyleSheet, + PropTypes, +} = React; + +var MapCircle = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + + /** + * The coordinate of the center of the circle + */ + center: PropTypes.shape({ + /** + * Coordinates for the center of the circle. + */ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + }).isRequired, + + /** + * The radius of the circle to be drawn (in meters) + */ + radius: PropTypes.number.isRequired, + + /** + * Callback that is called when the user presses on the circle + */ + onPress: PropTypes.func, + + /** + * The stroke width to use for the path. + */ + strokeWidth: PropTypes.number, + + /** + * The stroke color to use for the path. + */ + strokeColor: PropTypes.string, + + /** + * The fill color to use for the path. + */ + fillColor: PropTypes.string, + + /** + * The order in which this tile overlay is drawn with respect to other overlays. An overlay + * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays + * with the same z-index is arbitrary. The default zIndex is 0. + * + * @platform android + */ + zIndex: PropTypes.number, + + /** + * The line cap style to apply to the open ends of the path. + * The default style is `round`. + * + * @platform ios + */ + lineCap: PropTypes.oneOf([ + 'butt', + 'round', + 'square', + ]), + + /** + * The line join style to apply to corners of the path. + * The default style is `round`. + * + * @platform ios + */ + lineJoin: PropTypes.oneOf([ + 'miter', + 'round', + 'bevel', + ]), + + /** + * The limiting value that helps avoid spikes at junctions between connected line segments. + * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If + * the ratio of the miter length—that is, the diagonal length of the miter join—to the line + * thickness exceeds the miter limit, the joint is converted to a bevel join. The default + * miter limit is 10, which results in the conversion of miters whose angle at the joint + * is less than 11 degrees. + * + * @platform ios + */ + miterLimit: PropTypes.number, + }, + + getDefaultProps: function() { + return { + strokeColor: '#000', + strokeWidth: 1, + }; + }, + + _onPress: function(e) { + this.props.onPress && this.props.onPress(e); + }, + + render: function() { + return ( + + ); + }, +}); + +var AIRMapCircle = requireNativeComponent('AIRMapCircle', MapCircle); + +module.exports = MapCircle; diff --git a/components/MapMarker.js b/components/MapMarker.js new file mode 100644 index 0000000000..739e6c9faa --- /dev/null +++ b/components/MapMarker.js @@ -0,0 +1,233 @@ + +var React = require('react-native'); +var { + View, + NativeMethodsMixin, + requireNativeComponent, + StyleSheet, + PropTypes, + Platform, + NativeModules, +} = React; + +var MapMarker = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + + // TODO(lmr): get rid of these? + identifier: PropTypes.string, + reuseIdentifier: PropTypes.string, + + /** + * The title of the marker. This is only used if the component has no children that + * are an ``, in which case the default callout behavior will be used, which + * will show both the `title` and the `description`, if provided. + */ + title: PropTypes.string, + + /** + * The description of the marker. This is only used if the component has no children + * that are an ``, in which case the default callout behavior will be used, + * which will show both the `title` and the `description`, if provided. + */ + description: PropTypes.string, + + /** + * A custom image to be used as the marker's icon. Only local image resources are allowed to be + * used. + */ + image: PropTypes.shape({ + uri: PropTypes.string, + }), + + /** + * If no custom marker view or custom image is provided, the platform default pin will be used, + * which can be customized by this color. Ignored if a custom marker is being used. + */ + pinColor: PropTypes.string, + + /** + * The coordinate for the marker. + */ + coordinate: PropTypes.shape({ + /** + * Coordinates for the anchor point of the marker. + */ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + }), + + /** + * The offset (in points) at which to display the view. + * + * By default, the center point of an annotation view is placed at the coordinate point of the + * associated annotation. You can use this property to reposition the annotation view as + * needed. This x and y offset values are measured in points. Positive offset values move the + * annotation view down and to the right, while negative values move it up and to the left. + * + * For android, see the `anchor` prop. + * + * @platform ios + */ + centerOffset: PropTypes.shape({ + /** + * Offset from the anchor point + */ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + + /** + * The offset (in points) at which to place the callout bubble. + * + * This property determines the additional distance by which to move the callout bubble. When + * this property is set to (0, 0), the anchor point of the callout bubble is placed on the + * top-center point of the marker view’s frame. Specifying positive offset values moves the + * callout bubble down and to the right, while specifying negative values moves it up and to + * the left. + * + * For android, see the `calloutAnchor` prop. + * + * @platform ios + */ + calloutOffset: PropTypes.shape({ + /** + * Offset to the callout + */ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + + /** + * Sets the anchor point for the marker. + * + * The anchor specifies the point in the icon image that is anchored to the marker's position + * on the Earth's surface. + * + * The anchor point is specified in the continuous space [0.0, 1.0] x [0.0, 1.0], where (0, 0) + * is the top-left corner of the image, and (1, 1) is the bottom-right corner. The anchoring + * point in a W x H image is the nearest discrete grid point in a (W + 1) x (H + 1) grid, + * obtained by scaling the then rounding. For example, in a 4 x 2 image, the anchor point + * (0.7, 0.6) resolves to the grid point at (3, 1). + * + * For ios, see the `centerOffset` prop. + * + * @platform android + */ + anchor: PropTypes.shape({ + /** + * Offset to the callout + */ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + + /** + * Specifies the point in the marker image at which to anchor the callout when it is displayed. + * This is specified in the same coordinate system as the anchor. See the `andor` prop for more + * details. + * + * The default is the top middle of the image. + * + * For ios, see the `calloutOffset` prop. + * + * @platform android + */ + calloutAnchor: PropTypes.shape({ + /** + * Offset to the callout + */ + x: PropTypes.number.isRequired, + y: PropTypes.number.isRequired, + }), + + /** + * Sets whether this marker should be flat against the map true or a billboard facing the + * camera false. + * + * @platform android + */ + flat: PropTypes.bool, + + /** + * Callback that is called when the user presses on the marker + */ + onPress: PropTypes.func, + + /** + * Callback that is called when the user selects the marker, before the callout is shown. + * + * @platform ios + */ + onSelect: PropTypes.func, + + /** + * Callback that is called when the marker is deselected, before the callout is hidden. + */ + onDeselect: PropTypes.func, + + /** + * Callback that is called when the user taps the callout view. + */ + onCalloutPress: PropTypes.func, + + }, + + showCallout: function() { + this._runCommand('showCallout', []); + }, + + hideCallout: function() { + this._runCommand('hideCallout', []); + }, + + _getHandle: function() { + return React.findNodeHandle(this.refs.map); + }, + + _runCommand: function (name, args) { + switch (Platform.OS) { + case 'android': + NativeModules.UIManager.dispatchViewManagerCommand( + this._getHandle(), + NativeModules.UIManager.AIRMapMarker.Commands[name], + args + ); + break; + + case 'ios': + NativeModules.AIRMapMarkerManager[name].apply( + NativeModules.AIRMapMarkerManager[name], + [this._getHandle(), ...args] + ); + break; + } + }, + + _onPress: function(e) { + this.props.onPress && this.props.onPress(e); + }, + + render: function() { + return ( + + ); + }, +}); + +var styles = StyleSheet.create({ + marker: { + position: 'absolute', + backgroundColor: 'transparent', + }, +}); + +var AIRMapMarker = requireNativeComponent('AIRMapMarker', MapMarker); + +module.exports = MapMarker; diff --git a/components/MapPolygon.js b/components/MapPolygon.js new file mode 100644 index 0000000000..9bb78291f0 --- /dev/null +++ b/components/MapPolygon.js @@ -0,0 +1,125 @@ + +var React = require('react-native'); +var { + View, + NativeMethodsMixin, + requireNativeComponent, + StyleSheet, + PropTypes, +} = React; + +var MapPolygon = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + + /** + * An array of coordinates to describe the polygon + */ + coordinates: PropTypes.arrayOf(PropTypes.shape({ + /** + * Latitude/Longitude coordinates + */ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + })), + + /** + * Callback that is called when the user presses on the polygon + */ + onPress: PropTypes.func, + + /** + * The stroke width to use for the path. + */ + strokeWidth: PropTypes.number, + + /** + * The stroke color to use for the path. + */ + strokeColor: PropTypes.string, + + /** + * The fill color to use for the path. + */ + fillColor: PropTypes.string, + + /** + * The order in which this tile overlay is drawn with respect to other overlays. An overlay + * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays + * with the same z-index is arbitrary. The default zIndex is 0. + * + * @platform android + */ + zIndex: PropTypes.number, + + /** + * The line cap style to apply to the open ends of the path. + * The default style is `round`. + * + * @platform ios + */ + lineCap: PropTypes.oneOf([ + 'butt', + 'round', + 'square', + ]), + + /** + * The line join style to apply to corners of the path. + * The default style is `round`. + * + * @platform ios + */ + lineJoin: PropTypes.oneOf([ + 'miter', + 'round', + 'bevel', + ]), + + /** + * The limiting value that helps avoid spikes at junctions between connected line segments. + * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If + * the ratio of the miter length—that is, the diagonal length of the miter join—to the line + * thickness exceeds the miter limit, the joint is converted to a bevel join. The default + * miter limit is 10, which results in the conversion of miters whose angle at the joint + * is less than 11 degrees. + * + * @platform ios + */ + miterLimit: PropTypes.number, + }, + + getDefaultProps: function() { + return { + strokeColor: '#000', + strokeWidth: 1, + }; + }, + + _onPress: function(e) { + this.props.onPress && this.props.onPress(e); + }, + + render: function() { + return ( + + ); + }, +}); + +var styles = StyleSheet.create({ + polyline: { + position: 'absolute', + width: 0, + height: 0, + }, +}); + +var AIRMapPolygon = requireNativeComponent('AIRMapPolygon', MapPolygon); + +module.exports = MapPolygon; diff --git a/components/MapPolyline.js b/components/MapPolyline.js new file mode 100644 index 0000000000..b1eb664ffe --- /dev/null +++ b/components/MapPolyline.js @@ -0,0 +1,113 @@ + +var React = require('react-native'); +var { + View, + NativeMethodsMixin, + requireNativeComponent, + StyleSheet, + PropTypes, +} = React; + +var MapPolyline = React.createClass({ + mixins: [NativeMethodsMixin], + + propTypes: { + ...View.propTypes, + + /** + * An array of coordinates to describe the polygon + */ + coordinates: PropTypes.arrayOf(PropTypes.shape({ + /** + * Latitude/Longitude coordinates + */ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + })), + + /** + * Callback that is called when the user presses on the polyline + */ + onPress: PropTypes.func, + + /** + * The stroke width to use for the path. + */ + strokeWidth: PropTypes.number, + + /** + * The stroke color to use for the path. + */ + strokeColor: PropTypes.string, + + /** + * The order in which this tile overlay is drawn with respect to other overlays. An overlay + * with a larger z-index is drawn over overlays with smaller z-indices. The order of overlays + * with the same z-index is arbitrary. The default zIndex is 0. + * + * @platform android + */ + zIndex: PropTypes.number, + + /** + * The line cap style to apply to the open ends of the path. + * The default style is `round`. + * + * @platform ios + */ + lineCap: PropTypes.oneOf([ + 'butt', + 'round', + 'square', + ]), + + /** + * The line join style to apply to corners of the path. + * The default style is `round`. + * + * @platform ios + */ + lineJoin: PropTypes.oneOf([ + 'miter', + 'round', + 'bevel', + ]), + + /** + * The limiting value that helps avoid spikes at junctions between connected line segments. + * The miter limit helps you avoid spikes in paths that use the `miter` `lineJoin` style. If + * the ratio of the miter length—that is, the diagonal length of the miter join—to the line + * thickness exceeds the miter limit, the joint is converted to a bevel join. The default + * miter limit is 10, which results in the conversion of miters whose angle at the joint + * is less than 11 degrees. + * + * @platform ios + */ + miterLimit: PropTypes.number, + }, + + getDefaultProps: function() { + return { + strokeColor: '#000', + strokeWidth: 1, + }; + }, + + render: function() { + return ( + + ); + }, +}); + +var styles = StyleSheet.create({ + polyline: { + position: 'absolute', + width: 0, + height: 0, + }, +}); + +var AIRMapPolyline = requireNativeComponent('AIRMapPolyline', MapPolyline); + +module.exports = MapPolyline; diff --git a/components/MapView.js b/components/MapView.js new file mode 100644 index 0000000000..71ad59e12f --- /dev/null +++ b/components/MapView.js @@ -0,0 +1,286 @@ +'use strict'; + +var React = require('react-native'); +var { + EdgeInsetsPropType, + NativeMethodsMixin, + Platform, + ReactNativeViewAttributes, + View, + Animated, + requireNativeComponent, + NativeModules, + PropTypes, +} = React; + +var MapMarker = require('./MapMarker'); +var MapPolyline = require('./MapPolyline'); +var MapPolygon = require('./MapPolygon'); +var MapCircle = require('./MapCircle'); +var MapCallout = require('./MapCallout'); + +var deepDiffer = require('deepDiffer'); +var insetsDiffer = require('insetsDiffer'); +var merge = require('merge'); + +var MapView = React.createClass({ + mixins: [NativeMethodsMixin], + + viewConfig: { + uiViewClassName: 'AIRMap', + validAttributes: { + region: true, + }, + }, + + propTypes: { + ...View.propTypes, + /** + * Used to style and layout the `MapView`. See `StyleSheet.js` and + * `ViewStylePropTypes.js` for more info. + */ + style: View.propTypes.style, + + /** + * If `true` the app will ask for the user's location and focus on it. + * Default value is `false`. + * + * **NOTE**: You need to add NSLocationWhenInUseUsageDescription key in + * Info.plist to enable geolocation, otherwise it is going + * to *fail silently*! + */ + showsUserLocation: PropTypes.bool, + + /** + * If `false` points of interest won't be displayed on the map. + * Default value is `true`. + * + * @platform ios + */ + showsPointsOfInterest: PropTypes.bool, + + /** + * If `false` compass won't be displayed on the map. + * Default value is `true`. + * + * @platform ios + */ + showsCompass: PropTypes.bool, + + /** + * If `false` the user won't be able to pinch/zoom the map. + * Default value is `true`. + * + * @platform ios + */ + zoomEnabled: PropTypes.bool, + + /** + * If `false` the user won't be able to change the map region being displayed. + * Default value is `true`. + * + * @platform ios + */ + scrollEnabled: PropTypes.bool, + + /** + * A Boolean indicating whether the map shows scale information. + * Default value is `false` + * + * @platform ios + */ + showsScale: PropTypes.bool, + + /** + * A Boolean indicating whether the map displays extruded building information. + * Default value is `true`. + */ + showsBuildings: PropTypes.bool, + + /** + * A Boolean value indicating whether the map displays traffic information. + * Default value is `false`. + */ + showsTraffic: PropTypes.bool, + + /** + * A Boolean indicating whether indoor maps should be enabled. + * Default value is `false` + * + * @platform android + */ + showsIndoors: PropTypes.bool, + + /** + * The map type to be displayed. + * + * - standard: standard road map (default) + * - satellite: satellite view + * - hybrid: satellite view with roads and points of interest overlayed + */ + mapType: PropTypes.oneOf([ + 'standard', + 'satellite', + 'hybrid', + ]), + + /** + * The region to be displayed by the map. + * + * The region is defined by the center coordinates and the span of + * coordinates to display. + */ + region: PropTypes.shape({ + /** + * Coordinates for the center of the map. + */ + latitude: PropTypes.number.isRequired, + longitude: PropTypes.number.isRequired, + + /** + * Difference between the minimun and the maximum latitude/longitude + * to be displayed. + */ + latitudeDelta: PropTypes.number.isRequired, + longitudeDelta: PropTypes.number.isRequired, + }), + + /** + * Maximum size of area that can be displayed. + * + * @platform ios + */ + maxDelta: PropTypes.number, + + /** + * Minimum size of area that can be displayed. + * + * @platform ios + */ + minDelta: PropTypes.number, + + /** + * Insets for the map's legal label, originally at bottom left of the map. + * See `EdgeInsetsPropType.js` for more information. + */ + legalLabelInsets: EdgeInsetsPropType, + + /** + * Callback that is called continuously when the user is dragging the map. + */ + onRegionChange: PropTypes.func, + + /** + * Callback that is called once, when the user is done moving the map. + */ + onRegionChangeComplete: PropTypes.func, + + /** + * Callback that is called when user taps on the map. + */ + onPress: PropTypes.func, + + /** + * Callback that is called when user makes a "long press" somewhere on the map. + */ + onLongPress: PropTypes.func, + + /** + * Callback that is called when a marker on the map is tapped by the user. + */ + onMarkerPress: PropTypes.func, + + /** + * Callback that is called when a marker on the map becomes selected. This will be called when + * the callout for that marker is about to be shown. + * + * @platform ios + */ + onMarkerSelect: PropTypes.func, + + /** + * Callback that is called when a marker on the map becomes deselected. This will be called when + * the callout for that marker is about to be hidden. + * + * @platform ios + */ + onMarkerDeselect: PropTypes.func, + + /** + * Callback that is called when a callout is tapped by the user. + */ + onCalloutPress: PropTypes.func, + + }, + + _onChange: function(event: Event) { + if (event.nativeEvent.continuous) { + this.props.onRegionChange && + this.props.onRegionChange(event.nativeEvent.region); + } else { + this.props.onRegionChangeComplete && + this.props.onRegionChangeComplete(event.nativeEvent.region); + } + }, + + animateToRegion: function (region, duration) { + this._runCommand('animateToRegion', [region, duration || 500]); + }, + + animateToCoordinate: function (latLng, duration) { + this._runCommand('animateToCoordinate', [latLng, duration || 500]); + }, + + fitToElements: function(animated) { + this._runCommand('fitToElements', [animated]); + }, + + _getHandle: function() { + return React.findNodeHandle(this.refs.map); + }, + + _runCommand: function (name, args) { + switch (Platform.OS) { + case 'android': + NativeModules.UIManager.dispatchViewManagerCommand( + this._getHandle(), + NativeModules.UIManager.AIRMap.Commands[name], + args + ); + break; + + case 'ios': + NativeModules.AIRMapManager[name].apply( + NativeModules.AIRMapManager[name], + [this._getHandle(), ...args] + ); + break; + } + }, + + render: function() { + return ( + + ); + }, +}); + +var AIRMap = requireNativeComponent('AIRMap', MapView, { + nativeOnly: { + onChange: true, + }, +}); + +MapView.Marker = MapMarker; +MapView.Polyline = MapPolyline; +MapView.Polygon = MapPolygon; +MapView.Circle = MapCircle; +MapView.Callout = MapCallout; + +MapView.Animated = Animated.createAnimatedComponent(MapView); + +module.exports = MapView; diff --git a/components/PriceMarker.js b/components/PriceMarker.js new file mode 100644 index 0000000000..7722596a39 --- /dev/null +++ b/components/PriceMarker.js @@ -0,0 +1,64 @@ +var React = require('react-native'); +var { + StyleSheet, + View, + Text, +} = React; + +var PriceMarker = React.createClass({ + render() { + return ( + + + $ + 99 + + + + + ); + }, +}); + +var styles = StyleSheet.create({ + container: { + flexDirection: 'column', + alignSelf: 'flex-start', + }, + bubble: { + flex: 0, + flexDirection: 'row', + alignSelf: 'flex-start', + backgroundColor: '#FF5A5F', + padding: 2, + borderRadius: 3, + borderColor: '#D23F44', + borderWidth: 0.5, + }, + dollar: { + color: '#FFFFFF', + fontSize: 10, + }, + amount: { + color: '#FFFFFF', + fontSize: 13, + }, + arrow: { + backgroundColor: 'transparent', + borderWidth: 4, + borderColor: 'transparent', + borderTopColor: '#FF5A5F', + alignSelf: 'center', + marginTop: -9, + }, + arrowBorder: { + backgroundColor: 'transparent', + borderWidth: 4, + borderColor: 'transparent', + borderTopColor: '#D23F44', + alignSelf: 'center', + marginTop: -0.5, + }, +}); + +module.exports = PriceMarker; diff --git a/examples/oldandroid.js b/examples/oldandroid.js new file mode 100644 index 0000000000..721e3d3d24 --- /dev/null +++ b/examples/oldandroid.js @@ -0,0 +1,167 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, + Dimensions, + Animated, + TouchableOpacity, + //MapView, + } = React; +//var MapRegionInput = require('./components/MapRegionInput'); +var MapView = require('./components/MapView'); +var PriceMarker = require('./components/PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const LATITUDE = 37.78825; +const LONGITUDE = -122.4324; +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = 0.1218; + +var rn_mapview = React.createClass({ + getInitialState() { + return { + val: new Animated.Value(1), + region: { + latitude: LATITUDE, + longitude: LONGITUDE, + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }, + markers: [], + coord: { + latitude: LATITUDE, + longitude: LONGITUDE, + }, + }; + }, + + onRegionChange(region) { + //this.setState({ region }); + console.log("onRegionChange", region); + }, + + onAnimate() { + var { val } = this.state; + Animated.sequence([ + Animated.timing(val, { toValue: 0, duration: 500 }), + Animated.timing(val, { toValue: 1, duration: 500 }), + ]).start(); + }, + + onAddMarker() { + var coordinate = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }; + var markers = [...this.state.markers, coordinate]; + if (markers.length > 5) { + markers = markers.slice(1); + } + this.setState({ markers }); + }, + + onUpdateCenter() { + var region = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }; + this.setState({ region }); + }, + + render() { + return ( + + + + + + + {this.state.markers.map((coord, i) => ( + + + + ))} + + + + + + Add Marker + + + + + + Update Center + + + + ); + }, +}); + +var styles = StyleSheet.create({ + map: { + height: 350, + margin: 10, + borderWidth: 1, + borderColor: '#000000', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textInput: { + width: 150, + height: 20, + borderWidth: 0.5, + borderColor: '#aaaaaa', + fontSize: 13, + padding: 4, + }, + changeButton: { + alignSelf: 'center', + marginTop: 5, + padding: 3, + borderWidth: 0.5, + borderColor: '#777777', + }, + red: { + width: 50, + height: 50, + backgroundColor: 'red', + }, + blue: { + width: 50, + height: 50, + backgroundColor: 'blue', + } +}); + +AppRegistry.registerComponent('rn_mapview', () => rn_mapview); diff --git a/examples/oldandroid2.js b/examples/oldandroid2.js new file mode 100644 index 0000000000..ab17c86d69 --- /dev/null +++ b/examples/oldandroid2.js @@ -0,0 +1,245 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, + Dimensions, + Animated, + TouchableOpacity, + } = React; +var MapView = require('./components/MapView'); +var PriceMarker = require('./components/PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const LATITUDE = 37.78825; +const LONGITUDE = -122.4324; +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = 0.1218; + +var MapViewTest = React.createClass({ + getInitialState() { + const scale = new Animated.Value(1); + const latitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LATITUDE_DELTA, LATITUDE_DELTA * 0.6], + }); + const longitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LONGITUDE_DELTA, LONGITUDE_DELTA * 0.6], + }); + return { + val: new Animated.Value(1), + scale, + region: new Animated.Region({ + latitude: LATITUDE, + longitude: LONGITUDE, + latitudeDelta: latitudeDelta, + longitudeDelta: longitudeDelta, + }), + markers: [], + coord: { + latitude: LATITUDE, + longitude: LONGITUDE, + }, + }; + }, + + onRegionChange(region) { + //console.log(region); + this.state.region.setValue(region); + //this.setState({ region }); + }, + + onAnimate() { + var { val } = this.state; + Animated.sequence([ + Animated.timing(val, { toValue: 0, duration: 500 }), + Animated.timing(val, { toValue: 1, duration: 500 }), + ]).start(); + }, + + onAddMarker() { + var coordinate = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }; + var markers = [...this.state.markers, coordinate]; + if (markers.length > 5) { + markers = markers.slice(1); + } + this.setState({ markers }); + }, + + onAnimateZoom() { + var { scale, region } = this.state; + var toValue = scale.__getValue() > 1 ? 1 : 2; + Animated.parallel([ + Animated.spring(region.latitude, { + toValue: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(region.longitude, { + toValue: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(scale, { + toValue, + }), + ]).start(); + }, + + onMapPress(e) { + console.log("onMapPress", e); + }, + + onMarkerMove() { + this.setState({ + coord: { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }, + }) + }, + + onAnimateMapRegion() { + this.refs.map.refs.node.animateToRegion({ + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }, 100); + }, + + render() { + + return ( + + console.log("onMarkerPress")} + onCalloutPress={() => console.log("onCalloutPress")} + onMapPress={() => console.log("onMapPress")} + ref="map" + > + console.log("Marker::onPress")} + onCalloutPress={() => console.log("Marker::onCalloutPress")} + > + + console.log("Callout::onPress")} + > + + Well hello there... + + + + {this.state.markers.map((coord, i) => ( + console.log("Marker::onPress")} + onCalloutPress={() => console.log("Marker::onCalloutPress")} + > + + console.log("Callout::onPress")} + > + + Well hello there... + + + + ))} + + + + + Add Marker + + + + + + Animate Zoom + + + + + + Move Marker + + + + + + Animate Map Region + + + + ); + }, +}); + +var styles = StyleSheet.create({ + map: { + height: 350, + margin: 10, + borderWidth: 1, + borderColor: '#000000', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textInput: { + width: 150, + height: 20, + borderWidth: 0.5, + borderColor: '#aaaaaa', + fontSize: 13, + padding: 4, + }, + changeButton: { + alignSelf: 'center', + marginTop: 5, + padding: 3, + borderWidth: 0.5, + borderColor: '#777777', + }, + red: { + width: 50, + height: 50, + backgroundColor: 'red', + }, + blue: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + callout: { + //flex: 0, + //flexDirection: 'column', + position: 'absolute', + //flex: 0, + //backgroundColor: '#fff', + width: 100, + height: 100, + }, +}); + +module.exports = MapViewTest; +AppRegistry.registerComponent('rn_mapview', () => MapViewTest); diff --git a/examples/oldandroid3.js b/examples/oldandroid3.js new file mode 100644 index 0000000000..9bc2a8e42d --- /dev/null +++ b/examples/oldandroid3.js @@ -0,0 +1,259 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, + Dimensions, + Animated, + TouchableOpacity, + } = React; +var MapView = require('./components/MapView'); +var PriceMarker = require('./components/PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const LATITUDE = 38.89399; +const LONGITUDE = -77.03659; +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = 0.1218; + +var MapViewTest = React.createClass({ + getInitialState() { + const scale = new Animated.Value(1); + const latitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LATITUDE_DELTA, LATITUDE_DELTA * 0.6], + }); + const longitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LONGITUDE_DELTA, LONGITUDE_DELTA * 0.6], + }); + return { + val: new Animated.Value(1), + scale, + region: new Animated.Region({ + latitude: LATITUDE, + longitude: LONGITUDE, + latitudeDelta: latitudeDelta, + longitudeDelta: longitudeDelta, + }), + markers: [], + coord: { + latitude: LATITUDE, + longitude: LONGITUDE, + }, + }; + }, + + onRegionChange(region) { + //console.log(region); + this.state.region.setValue(region); + //this.setState({ region }); + }, + + onAnimate() { + var { val } = this.state; + Animated.sequence([ + Animated.timing(val, { toValue: 0, duration: 500 }), + Animated.timing(val, { toValue: 1, duration: 500 }), + ]).start(); + }, + + onAddMarker() { + var coordinate = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }; + var markers = [...this.state.markers, coordinate]; + if (markers.length > 5) { + markers = markers.slice(1); + } + this.setState({ markers }); + }, + + onAnimateZoom() { + var { scale, region } = this.state; + var toValue = scale.__getValue() > 1 ? 1 : 2; + Animated.parallel([ + Animated.spring(region.latitude, { + toValue: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(region.longitude, { + toValue: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(scale, { + toValue, + }), + ]).start(); + }, + + onMapPress(e) { + console.log("onMapPress", e); + }, + + onMarkerMove() { + this.setState({ + coord: { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }, + }) + }, + + onAnimateMapRegion() { + this.refs.map.refs.node.animateToRegion({ + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }, 100); + }, + + render() { + + return ( + + console.log("onMarkerPress")} + onCalloutPress={() => console.log("onCalloutPress")} + onMapPress={() => console.log("onMapPress")} + ref="map" + > + + console.log("Marker::onPress")} + onCalloutPress={() => console.log("Marker::onCalloutPress")} + > + + console.log("Callout::onPress")} + > + + Well hello there... + + + + {this.state.markers.map((coord, i) => ( + console.log("Marker::onPress")} + onCalloutPress={() => console.log("Marker::onCalloutPress")} + > + + console.log("Callout::onPress")} + > + + Well hello there... + + + + ))} + + + + + Add Marker + + + + + + Animate Zoom + + + + + + Move Marker + + + + + + Animate Map Region + + + + ); + }, +}); + +var styles = StyleSheet.create({ + map: { + height: 350, + margin: 10, + borderWidth: 1, + borderColor: '#000000', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textInput: { + width: 150, + height: 20, + borderWidth: 0.5, + borderColor: '#aaaaaa', + fontSize: 13, + padding: 4, + }, + changeButton: { + alignSelf: 'center', + marginTop: 5, + padding: 3, + borderWidth: 0.5, + borderColor: '#777777', + }, + red: { + width: 50, + height: 50, + backgroundColor: 'red', + }, + blue: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + callout: { + //flex: 0, + //flexDirection: 'column', + position: 'absolute', + //flex: 0, + //backgroundColor: '#fff', + width: 100, + height: 100, + }, +}); + +module.exports = MapViewTest; +AppRegistry.registerComponent('rn_mapview', () => MapViewTest); diff --git a/index.android.js b/index.android.js new file mode 100644 index 0000000000..98ec40776e --- /dev/null +++ b/index.android.js @@ -0,0 +1,290 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, + Dimensions, + Animated, + TouchableOpacity, + } = React; +var MapView = require('./components/MapView'); +var PriceMarker = require('./components/PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const LATITUDE = 37.78825; +const LONGITUDE = -122.4324; + +const LATD = LATITUDE - 38.89399; +const LNGD = LONGITUDE + 77.03659; + +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = 0.1218; + +var MapViewTest = React.createClass({ + getInitialState() { + const scale = new Animated.Value(1); + const latitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LATITUDE_DELTA, LATITUDE_DELTA * 0.6], + }); + const longitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LONGITUDE_DELTA, LONGITUDE_DELTA * 0.6], + }); + return { + val: new Animated.Value(1), + scale, + region: new Animated.Region({ + latitude: LATITUDE, + longitude: LONGITUDE, + latitudeDelta: latitudeDelta, + longitudeDelta: longitudeDelta, + }), + markers: [], + coord: { + latitude: LATITUDE, + longitude: LONGITUDE, + }, + }; + }, + + onRegionChange(region) { + //console.log("onRegionChange", region); + this.state.region.setValue(region); + //this.setState({ region }); + }, + + onAnimate() { + var { val } = this.state; + Animated.sequence([ + Animated.timing(val, { toValue: 0, duration: 500 }), + Animated.timing(val, { toValue: 1, duration: 500 }), + ]).start(); + }, + + onAddMarker() { + var coordinate = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }; + var markers = [...this.state.markers, coordinate]; + if (markers.length > 5) { + markers = markers.slice(1); + } + this.setState({ markers }); + }, + + onAnimateZoom() { + var { scale, region } = this.state; + var toValue = scale.__getValue() > 1 ? 1 : 2; + Animated.parallel([ + Animated.spring(region.latitude, { + toValue: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(region.longitude, { + toValue: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(scale, { + toValue, + }), + ]).start(); + }, + + onMarkerMove() { + this.setState({ + coord: { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }, + }) + }, + + onAnimateMapRegion() { + this.refs.map.refs.node.animateToRegion({ + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }, 100); + }, + + onZoomToFit() { + this.refs.map.refs.node.fitToElements(true); + }, + + render() { + + return ( + + console.log("Map::onRegionChangeComplete", e.nativeEvent)} + onPress={(e) => console.log("Map::onPress", e.nativeEvent)} + onLongPress={(e) => console.log("Map::onLongPress", e.nativeEvent)} + onMarkerPress={(e) => console.log("Map::onMarkerPress", e.nativeEvent)} + onMarkerSelect={(e) => console.log("Map::onMarkerSelect", e.nativeEvent)} + onMarkerDeselect={(e) => console.log("Map::onMarkerDeselect", e.nativeEvent)} + onCalloutPress={(e) => console.log("Map::onCalloutPress", e.nativeEvent)} + > + + + + console.log("Marker::onPress", e.nativeEvent)} + onCalloutPress={(e) => console.log("Marker::onCalloutPress", e.nativeEvent)} + > + + console.log("Callout::onPress", e.nativeEvent)} + > + + Well hello there... + + + + {this.state.markers.map((coord, i) => ( + console.log("Marker::onPress", e.nativeEvent)} + onCalloutPress={(e) => console.log("Marker::onCalloutPress", e.nativeEvent)} + > + + console.log("Callout::onPress", e.nativeEvent)} + > + + Well hello there... + + + + ))} + + + + + Add Marker + + + + + + Animate Zoom + + + + + + Move Marker + + + + + + Animate Map Region + + + + + + Fit To Elements + + + + ); + }, +}); + +var styles = StyleSheet.create({ + map: { + height: 350, + margin: 10, + borderWidth: 1, + borderColor: '#000000', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textInput: { + width: 150, + height: 20, + borderWidth: 0.5, + borderColor: '#aaaaaa', + fontSize: 13, + padding: 4, + }, + changeButton: { + alignSelf: 'center', + marginTop: 5, + padding: 3, + borderWidth: 0.5, + borderColor: '#777777', + }, + red: { + width: 50, + height: 50, + backgroundColor: 'red', + }, + blue: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + callout: { + //flex: 0, + //flexDirection: 'column', + //position: 'absolute', + //flex: 0, + //backgroundColor: '#fff', + //width: 100, + //height: 100, + }, +}); + +module.exports = MapViewTest; +AppRegistry.registerComponent('rn_mapview', () => MapViewTest); diff --git a/index.ios.js b/index.ios.js new file mode 100644 index 0000000000..61beffb9d0 --- /dev/null +++ b/index.ios.js @@ -0,0 +1,291 @@ +/** + * Sample React Native App + * https://github.com/facebook/react-native + */ +'use strict'; + +var React = require('react-native'); +var { + AppRegistry, + StyleSheet, + Text, + View, + Dimensions, + Animated, + TouchableOpacity, + } = React; +var MapView = require('./components/MapView'); +var PriceMarker = require('./components/PriceMarker'); + +var { width, height } = Dimensions.get('window'); + +const LATITUDE = 37.78825; +const LONGITUDE = -122.4324; + +const LATD = LATITUDE - 38.89399; +const LNGD = LONGITUDE + 77.03659; + +const LATITUDE_DELTA = 0.0922; +const LONGITUDE_DELTA = 0.1218; + +var MapViewTest = React.createClass({ + getInitialState() { + const scale = new Animated.Value(1); + const latitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LATITUDE_DELTA, LATITUDE_DELTA * 0.6], + }); + const longitudeDelta = scale.interpolate({ + inputRange: [1, 2], + outputRange: [LONGITUDE_DELTA, LONGITUDE_DELTA * 0.6], + }); + return { + val: new Animated.Value(1), + scale, + region: new Animated.Region({ + latitude: LATITUDE, + longitude: LONGITUDE, + latitudeDelta: latitudeDelta, + longitudeDelta: longitudeDelta, + }), + markers: [], + coord: { + latitude: LATITUDE, + longitude: LONGITUDE, + }, + }; + }, + + onRegionChange(region) { + //console.log("onRegionChange", region); + this.state.region.setValue(region); + //this.setState({ region }); + }, + + onAnimate() { + var { val } = this.state; + Animated.sequence([ + Animated.timing(val, { toValue: 0, duration: 500 }), + Animated.timing(val, { toValue: 1, duration: 500 }), + ]).start(); + }, + + onAddMarker() { + var coordinate = { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }; + var markers = [...this.state.markers, coordinate]; + if (markers.length > 5) { + markers = markers.slice(1); + } + this.setState({ markers }); + }, + + onAnimateZoom() { + var { scale, region } = this.state; + var toValue = scale.__getValue() > 1 ? 1 : 2; + Animated.parallel([ + Animated.spring(region.latitude, { + toValue: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(region.longitude, { + toValue: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }), + Animated.spring(scale, { + toValue, + }), + ]).start(); + }, + + onMarkerMove() { + this.setState({ + coord: { + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + }, + }) + }, + + onAnimateMapRegion() { + this.refs.map.refs.node.animateToRegion({ + latitude: LATITUDE + LATITUDE_DELTA * (Math.random() - 0.5), + longitude: LONGITUDE + LONGITUDE_DELTA * (Math.random() - 0.5), + latitudeDelta: LATITUDE_DELTA, + longitudeDelta: LONGITUDE_DELTA, + }, 100); + }, + + onZoomToFit() { + this.refs.map.refs.node.fitToElements(true); + }, + + render() { + + return ( + + console.log("Map::onRegionChangeComplete", e.nativeEvent)} + onPress={(e) => console.log("Map::onPress", e.nativeEvent)} + onLongPress={(e) => console.log("Map::onLongPress", e.nativeEvent)} + onMarkerPress={(e) => console.log("Map::onMarkerPress", e.nativeEvent)} + onMarkerSelect={(e) => console.log("Map::onMarkerSelect", e.nativeEvent)} + onMarkerDeselect={(e) => console.log("Map::onMarkerDeselect", e.nativeEvent)} + onCalloutPress={(e) => console.log("Map::onCalloutPress", e.nativeEvent)} + onMapPress={(e) => console.log("Map::onMapPress", e.nativeEvent)} + > + + + + console.log("Marker::onPress", e.nativeEvent)} + onCalloutPress={(e) => console.log("Marker::onCalloutPress", e.nativeEvent)} + > + + console.log("Callout::onPress", e.nativeEvent)} + > + + Well hello there... + + + + {this.state.markers.map((coord, i) => ( + console.log("Marker::onPress", e.nativeEvent)} + onCalloutPress={(e) => console.log("Marker::onCalloutPress", e.nativeEvent)} + > + + console.log("Callout::onPress", e.nativeEvent)} + > + + Well hello there... + + + + ))} + + + + + Add Marker + + + + + + Animate Zoom + + + + + + Move Marker + + + + + + Animate Map Region + + + + + + Fit To Elements + + + + ); + }, +}); + +var styles = StyleSheet.create({ + map: { + height: 350, + margin: 10, + borderWidth: 1, + borderColor: '#000000', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + }, + textInput: { + width: 150, + height: 20, + borderWidth: 0.5, + borderColor: '#aaaaaa', + fontSize: 13, + padding: 4, + }, + changeButton: { + alignSelf: 'center', + marginTop: 5, + padding: 3, + borderWidth: 0.5, + borderColor: '#777777', + }, + red: { + width: 50, + height: 50, + backgroundColor: 'red', + }, + blue: { + width: 50, + height: 50, + backgroundColor: 'blue', + }, + callout: { + //flex: 0, + //flexDirection: 'column', + //position: 'absolute', + //flex: 0, + //backgroundColor: '#fff', + //width: 100, + //height: 100, + }, +}); + +module.exports = MapViewTest; +AppRegistry.registerComponent('rn_mapview', () => MapViewTest); diff --git a/ios/AIRMap.h b/ios/AIRMap.h new file mode 100644 index 0000000000..7c8a924b07 --- /dev/null +++ b/ios/AIRMap.h @@ -0,0 +1,47 @@ +/** + * 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. + */ + +#import +#import + +#import "RCTConvert+MapKit.h" +#import "RCTComponent.h" +#import "SMCalloutView.h" + +extern const CLLocationDegrees RCTMapDefaultSpan; +extern const NSTimeInterval RCTMapRegionChangeObserveInterval; +extern const CGFloat RCTMapZoomBoundBuffer; + +@interface AIRMap: MKMapView + +@property (nonatomic, strong) SMCalloutView *calloutView; + +@property (nonatomic, assign) BOOL followUserLocation; +@property (nonatomic, assign) BOOL hasStartedRendering; +@property (nonatomic, assign) CGFloat minDelta; +@property (nonatomic, assign) CGFloat maxDelta; +@property (nonatomic, assign) UIEdgeInsets legalLabelInsets; +@property (nonatomic, strong) NSTimer *regionChangeObserveTimer; + +@property (nonatomic, assign) CLLocationCoordinate2D pendingCenter; +@property (nonatomic, assign) MKCoordinateSpan pendingSpan; + + +@property (nonatomic, assign) BOOL ignoreRegionChanges; + +@property (nonatomic, copy) RCTBubblingEventBlock onChange; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; +@property (nonatomic, copy) RCTBubblingEventBlock onLongPress; +@property (nonatomic, copy) RCTDirectEventBlock onMarkerPress; +@property (nonatomic, copy) RCTDirectEventBlock onMarkerSelect; +@property (nonatomic, copy) RCTDirectEventBlock onMarkerDeselect; +@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress; +@property (nonatomic, copy) RCTDirectEventBlock onRegionChange; + +@end diff --git a/ios/AIRMap.m b/ios/AIRMap.m new file mode 100644 index 0000000000..1abf2acc42 --- /dev/null +++ b/ios/AIRMap.m @@ -0,0 +1,184 @@ +/** + * 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. + */ + +#import "AIRMap.h" + +#import "RCTEventDispatcher.h" +#import "AIRMapMarker.h" +#import "UIView+React.h" +#import "AIRMapPolyline.h" +#import "AIRMapPolygon.h" +#import "AIRMapCircle.h" + +const CLLocationDegrees AIRMapDefaultSpan = 0.005; +const NSTimeInterval AIRMapRegionChangeObserveInterval = 0.1; +const CGFloat AIRMapZoomBoundBuffer = 0.01; + + +@interface MKMapView (UIGestureRecognizer) + +// this tells the compiler that MKMapView actually implements this method +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch; + +@end + + +@implementation AIRMap +{ + UIView *_legalLabel; + CLLocationManager *_locationManager; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _hasStartedRendering = NO; + + // Find Apple link label + for (UIView *subview in self.subviews) { + if ([NSStringFromClass(subview.class) isEqualToString:@"MKAttributionLabel"]) { + // This check is super hacky, but the whole premise of moving around + // Apple's internal subviews is super hacky + _legalLabel = subview; + break; + } + } + + // 3rd-party callout view for MapKit that has more options than the built-in. It's painstakingly built to + // be identical to the built-in callout view (which has a private API) + self.calloutView = [SMCalloutView platformCalloutView]; + self.calloutView.delegate = self; + } + return self; +} + +- (void)dealloc +{ + [_regionChangeObserveTimer invalidate]; +} + +- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { + // Our desired API is to pass up markers/overlays as children to the mapview component. + // This is where we intercept them and do the appropriate underlying mapview action. + if ([subview isKindOfClass:[AIRMapMarker class]]) { + [self addAnnotation:(id ) subview]; + } else if ([subview isKindOfClass:[AIRMapPolyline class]]) { + [self addOverlay:(id)subview]; + } else if ([subview isKindOfClass:[AIRMapPolygon class]]) { + [self addOverlay:(id)subview]; + } else if ([subview isKindOfClass:[AIRMapCircle class]]) { + [self addOverlay:(id)subview]; + } else { + [super insertReactSubview:subview atIndex:atIndex]; + } +} + +- (void)removeReactSubview:(id)subview { + // similarly, when the children are being removed we have to do the appropriate + // underlying mapview action here. + if ([subview isKindOfClass:[AIRMapMarker class]]) { + [self removeAnnotation:(id)subview]; + } else if ([subview isKindOfClass:[AIRMapPolyline class]]) { + [self removeOverlay:(id ) subview]; + } else if ([subview isKindOfClass:[AIRMapPolygon class]]) { + [self removeOverlay:(id ) subview]; + } else if ([subview isKindOfClass:[AIRMapCircle class]]) { + [self removeOverlay:(id ) subview]; + } else { + [super removeReactSubview:subview]; + } +} + +#pragma mark Overrides for Callout behavior + +// override UIGestureRecognizer's delegate method so we can prevent MKMapView's recognizer from firing +// when we interact with UIControl subclasses inside our callout view. +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { + if ([touch.view isDescendantOfView:self.calloutView]) + return NO; + else + return [super gestureRecognizer:gestureRecognizer shouldReceiveTouch:touch]; +} + +// Allow touches to be sent to our calloutview. +// See this for some discussion of why we need to override this: https://github.com/nfarina/calloutview/pull/9 +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { + + UIView *calloutMaybe = [self.calloutView hitTest:[self.calloutView convertPoint:point fromView:self] withEvent:event]; + if (calloutMaybe) return calloutMaybe; + + return [super hitTest:point withEvent:event]; +} + +#pragma mark SMCalloutViewDelegate + +- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset { + + // When the callout is being asked to present in a way where it or its target will be partially offscreen, it asks us + // if we'd like to reposition our surface first so the callout is completely visible. Here we scroll the map into view, + // but it takes some math because we have to deal in lon/lat instead of the given offset in pixels. + + CLLocationCoordinate2D coordinate = self.region.center; + + // where's the center coordinate in terms of our view? + CGPoint center = [self convertCoordinate:coordinate toPointToView:self]; + + // move it by the requested offset + center.x -= offset.width; + center.y -= offset.height; + + // and translate it back into map coordinates + coordinate = [self convertPoint:center toCoordinateFromView:self]; + + // move the map! + [self setCenterCoordinate:coordinate animated:YES]; + + // tell the callout to wait for a while while we scroll (we assume the scroll delay for MKMapView matches UIScrollView) + return kSMCalloutViewRepositionDelayForUIScrollView; +} + +#pragma mark Accessors + +- (void)setShowsUserLocation:(BOOL)showsUserLocation +{ + if (self.showsUserLocation != showsUserLocation) { + if (showsUserLocation && !_locationManager) { + _locationManager = [CLLocationManager new]; + if ([_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { + [_locationManager requestWhenInUseAuthorization]; + } + } + super.showsUserLocation = showsUserLocation; + + // If it needs to show user location, force map view centered + // on user's current location on user location updates + _followUserLocation = showsUserLocation; + } +} + +- (void)setRegion:(MKCoordinateRegion)region animated:(BOOL)animated +{ + // If location is invalid, abort + if (!CLLocationCoordinate2DIsValid(region.center)) { + return; + } + + // If new span values are nil, use old values instead + if (!region.span.latitudeDelta) { + region.span.latitudeDelta = self.region.span.latitudeDelta; + } + if (!region.span.longitudeDelta) { + region.span.longitudeDelta = self.region.span.longitudeDelta; + } + + // Animate to new position + [super setRegion:region animated:animated]; +} + +@end diff --git a/ios/AIRMapCallout.h b/ios/AIRMapCallout.h new file mode 100644 index 0000000000..3fce436d9e --- /dev/null +++ b/ios/AIRMapCallout.h @@ -0,0 +1,15 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import +#import "RCTView.h" + + +@interface AIRMapCallout : RCTView + +@property (nonatomic, assign) BOOL tooltip; +@property (nonatomic, copy) RCTBubblingEventBlock onPress; + +@end \ No newline at end of file diff --git a/ios/AIRMapCallout.m b/ios/AIRMapCallout.m new file mode 100644 index 0000000000..2567764c5c --- /dev/null +++ b/ios/AIRMapCallout.m @@ -0,0 +1,12 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "AIRMapCallout.h" + + +@implementation AIRMapCallout { + +} +@end \ No newline at end of file diff --git a/ios/AIRMapCalloutManager.h b/ios/AIRMapCalloutManager.h new file mode 100644 index 0000000000..b5bbadff81 --- /dev/null +++ b/ios/AIRMapCalloutManager.h @@ -0,0 +1,10 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTViewManager.h" + + +@interface AIRMapCalloutManager : RCTViewManager +@end \ No newline at end of file diff --git a/ios/AIRMapCalloutManager.m b/ios/AIRMapCalloutManager.m new file mode 100644 index 0000000000..fbd1804ea5 --- /dev/null +++ b/ios/AIRMapCalloutManager.m @@ -0,0 +1,37 @@ +/** + * 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. + */ + +#import "AIRMapCalloutManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" +#import "RCTViewManager.h" +#import "AIRMapCallout.h" + +@interface AIRMapCalloutManager() + +@end + +@implementation AIRMapCalloutManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + return [AIRMapCallout new]; +} + +RCT_EXPORT_VIEW_PROPERTY(tooltip, BOOL) +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) + +@end diff --git a/ios/AIRMapCircle.h b/ios/AIRMapCircle.h new file mode 100644 index 0000000000..4c009fce4d --- /dev/null +++ b/ios/AIRMapCircle.h @@ -0,0 +1,40 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +#import +#import + +#import "RCTConvert+MapKit.h" +#import "RCTComponent.h" +#import "AIRMapCoordinate.h" +#import "RCTView.h" + + + +@interface AIRMapCircle: MKAnnotationView + +@property (nonatomic, strong) MKCircle *circle; +@property (nonatomic, strong) MKCircleRenderer *renderer; + +@property (nonatomic, assign) CLLocationCoordinate2D centerCoordinate; +@property (nonatomic, assign) CLLocationDistance radius; + +@property (nonatomic, strong) UIColor *fillColor; +@property (nonatomic, strong) UIColor *strokeColor; +@property (nonatomic, assign) CGFloat strokeWidth; +@property (nonatomic, assign) CGFloat miterLimit; +@property (nonatomic, assign) CGLineCap lineCap; +@property (nonatomic, assign) CGLineJoin lineJoin; + +#pragma mark MKOverlay protocol + +@property(nonatomic, readonly) CLLocationCoordinate2D coordinate; +@property(nonatomic, readonly) MKMapRect boundingMapRect; +- (BOOL)intersectsMapRect:(MKMapRect)mapRect; +- (BOOL)canReplaceMapContent; + +@end \ No newline at end of file diff --git a/ios/AIRMapCircle.m b/ios/AIRMapCircle.m new file mode 100644 index 0000000000..4a3d5a12ce --- /dev/null +++ b/ios/AIRMapCircle.m @@ -0,0 +1,155 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "AIRMapCircle.h" +#import "UIView+React.h" + + +@implementation AIRMapCircle { + BOOL _radiusSet; + BOOL _centerSet; +} + +- (void)setFillColor:(UIColor *)fillColor { + _fillColor = fillColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeColor:(UIColor *)strokeColor { + _strokeColor = strokeColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeWidth:(CGFloat)strokeWidth { + _strokeWidth = strokeWidth; + [self applyPropsToRenderer]; +} + +- (void)setLineJoin:(CGLineJoin)lineJoin { + _lineJoin = lineJoin; + [self applyPropsToRenderer]; +} + +- (void)setLineCap:(CGLineCap)lineCap { + _lineCap = lineCap; + [self applyPropsToRenderer]; +} + +- (void)setMiterLimit:(CGFloat)miterLimit { + _miterLimit = miterLimit; + [self applyPropsToRenderer]; +} + +- (void)setRadius:(CLLocationDistance)radius { + _radius = radius; + _radiusSet = YES; + [self createCircleAndRendererIfPossible]; + [self applyPropsToRenderer]; +} + +- (void)setCenterCoordinate:(CLLocationCoordinate2D)centerCoordinate{ + _centerCoordinate = centerCoordinate; + _centerSet = YES; + [self createCircleAndRendererIfPossible]; + [self applyPropsToRenderer]; +} + +- (void) createCircleAndRendererIfPossible +{ + if (!_centerSet || !_radiusSet) return; + self.circle = [MKCircle circleWithCenterCoordinate:_centerCoordinate radius:_radius]; + self.renderer = [[MKCircleRenderer alloc] initWithCircle:self.circle]; +} + +- (void) applyPropsToRenderer +{ + if (!_renderer) { + return; + }; + _renderer.fillColor = _fillColor; + _renderer.strokeColor = _strokeColor; + _renderer.lineWidth = _strokeWidth; + _renderer.lineCap = _lineCap; + _renderer.lineJoin = _lineJoin; + _renderer.miterLimit = _miterLimit; +} + + +#pragma mark MKOverlay implementation + +- (CLLocationCoordinate2D) coordinate +{ + return self.circle.coordinate; +} + +- (MKMapRect) boundingMapRect +{ + return self.circle.boundingMapRect; +} + +- (BOOL)intersectsMapRect:(MKMapRect)mapRect +{ + BOOL answer = [self.circle intersectsMapRect:mapRect]; + return answer; +} + +- (BOOL)canReplaceMapContent +{ + return NO; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@end \ No newline at end of file diff --git a/ios/AIRMapCircleManager.h b/ios/AIRMapCircleManager.h new file mode 100644 index 0000000000..a752c2d3ca --- /dev/null +++ b/ios/AIRMapCircleManager.h @@ -0,0 +1,10 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTViewManager.h" + + +@interface AIRMapCircleManager : RCTViewManager +@end \ No newline at end of file diff --git a/ios/AIRMapCircleManager.m b/ios/AIRMapCircleManager.m new file mode 100644 index 0000000000..f8f85d885f --- /dev/null +++ b/ios/AIRMapCircleManager.m @@ -0,0 +1,49 @@ +/** + * 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. + */ + +#import "AIRMapCircleManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" +#import "RCTViewManager.h" +#import "AIRMapCircle.h" + +@interface AIRMapCircleManager() + +@end + +@implementation AIRMapCircleManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + AIRMapCircle *circle = [AIRMapCircle new]; + return circle; +} + +RCT_REMAP_VIEW_PROPERTY(center, centerCoordinate, CLLocationCoordinate2D) +RCT_EXPORT_VIEW_PROPERTY(radius, CLLocationDistance) +RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap) +RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin) +RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat) + +// NOTE(lmr): +// for now, onPress events for overlays will be left unimplemented. Seems it is possible with some work, but +// it is difficult to achieve in both ios and android so I decided to leave it out. +//RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) + +@end diff --git a/ios/AIRMapCoordinate.h b/ios/AIRMapCoordinate.h new file mode 100644 index 0000000000..71ea0c5951 --- /dev/null +++ b/ios/AIRMapCoordinate.h @@ -0,0 +1,13 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import +#import + +@interface AIRMapCoordinate : NSObject + +@property (nonatomic, assign) CLLocationCoordinate2D coordinate; + +@end \ No newline at end of file diff --git a/ios/AIRMapCoordinate.m b/ios/AIRMapCoordinate.m new file mode 100644 index 0000000000..4a5cfe8222 --- /dev/null +++ b/ios/AIRMapCoordinate.m @@ -0,0 +1,12 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "AIRMapCoordinate.h" + + +@implementation AIRMapCoordinate { + +} +@end \ No newline at end of file diff --git a/ios/AIRMapManager.h b/ios/AIRMapManager.h new file mode 100644 index 0000000000..f3ffd45155 --- /dev/null +++ b/ios/AIRMapManager.h @@ -0,0 +1,14 @@ +/** + * 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. + */ + +#import "RCTViewManager.h" + +@interface AIRMapManager : RCTViewManager + +@end diff --git a/ios/AIRMapManager.m b/ios/AIRMapManager.m new file mode 100644 index 0000000000..fa04960a6f --- /dev/null +++ b/ios/AIRMapManager.m @@ -0,0 +1,370 @@ +/** + * 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. + */ + +#import "RCTViewManager.h" +#import "AIRMapManager.h" + +#import "RCTBridge.h" +#import "RCTUIManager.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTConvert+MapKit.h" +#import "RCTEventDispatcher.h" +#import "AIRMap.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" +#import "RCTViewManager.h" +#import "RCTConvert.h" +#import "AIRMapPolyline.h" +#import "AIRMapPolygon.h" +#import "AIRMapCircle.h" +#import "SMCalloutView.h" + +#import + +static NSString *const RCTMapViewKey = @"MapView"; + + +@interface AIRMapManager() + +@end + +@implementation AIRMapManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + AIRMap *map = [AIRMap new]; + map.delegate = self; + + // MKMapView doesn't report tap events, so we attach gesture recognizers to it + UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapTap:)]; + UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMapLongPress:)]; + // setting this to NO allows the parent MapView to continue receiving marker selection events + tap.cancelsTouchesInView = NO; + longPress.cancelsTouchesInView = NO; + + [map addGestureRecognizer:tap]; + [map addGestureRecognizer:longPress]; + + return map; +} + +RCT_EXPORT_VIEW_PROPERTY(showsUserLocation, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsPointsOfInterest, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsBuildings, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsCompass, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsScale, BOOL) +RCT_EXPORT_VIEW_PROPERTY(showsTraffic, BOOL) +RCT_EXPORT_VIEW_PROPERTY(zoomEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(scrollEnabled, BOOL) +RCT_EXPORT_VIEW_PROPERTY(maxDelta, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(minDelta, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(legalLabelInsets, UIEdgeInsets) +RCT_EXPORT_VIEW_PROPERTY(mapType, MKMapType) +RCT_EXPORT_VIEW_PROPERTY(onChange, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onLongPress, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMarkerPress, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMarkerSelect, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onMarkerDeselect, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock) + +RCT_CUSTOM_VIEW_PROPERTY(region, MKCoordinateRegion, AIRMap) +{ + // don't emit region change events when we are setting the region + BOOL originalIgnore = view.ignoreRegionChanges; + view.ignoreRegionChanges = YES; + [view setRegion:json ? [RCTConvert MKCoordinateRegion:json] : defaultView.region animated:NO]; + view.ignoreRegionChanges = originalIgnore; +} + + +#pragma mark exported MapView methods + +RCT_EXPORT_METHOD(animateToRegion:(nonnull NSNumber *)reactTag + withRegion:(MKCoordinateRegion)region + withDuration:(CGFloat)duration) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + [(AIRMap *)view setRegion:region animated:YES]; + } + }]; +} + +RCT_EXPORT_METHOD(animateToCoordinate:(nonnull NSNumber *)reactTag + withRegion:(CLLocationCoordinate2D)latlng + withDuration:(CGFloat)duration) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMap *mapView = (AIRMap *)view; + MKCoordinateRegion region; + region.span = mapView.region.span; + region.center = latlng; + [mapView setRegion:region animated:YES]; + } + }]; +} + +RCT_EXPORT_METHOD(fitToElements:(nonnull NSNumber *)reactTag + animated:(BOOL)animated) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMap class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMap *mapView = (AIRMap *)view; + // TODO(lmr): we potentially want to include overlays here... and could concat the two arrays together. + [mapView showAnnotations:mapView.annotations animated:animated]; + } + }]; +} + +#pragma mark Gesture Recognizer Handlers + +- (void)handleMapTap:(UITapGestureRecognizer *)recognizer { + AIRMap *map = (AIRMap *)recognizer.view; + if (!map.onPress) return; + + CGPoint touchPoint = [recognizer locationInView:map]; + CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map]; + + map.onPress(@{ + @"coordinate": @{ + @"latitude": @(coord.latitude), + @"longitude": @(coord.longitude), + }, + @"position": @{ + @"x": @(touchPoint.x), + @"y": @(touchPoint.y), + }, + }); + +} + +- (void)handleMapLongPress:(UITapGestureRecognizer *)recognizer { + + // NOTE: android only does the equivalent of "began", so we only send in this case + if (recognizer.state != UIGestureRecognizerStateBegan) return; + + AIRMap *map = (AIRMap *)recognizer.view; + if (!map.onLongPress) return; + + CGPoint touchPoint = [recognizer locationInView:map]; + CLLocationCoordinate2D coord = [map convertPoint:touchPoint toCoordinateFromView:map]; + + map.onLongPress(@{ + @"coordinate": @{ + @"latitude": @(coord.latitude), + @"longitude": @(coord.longitude), + }, + @"position": @{ + @"x": @(touchPoint.x), + @"y": @(touchPoint.y), + }, + }); +} + +#pragma mark MKMapViewDelegate + +#pragma mark Polyline stuff + +- (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id )overlay{ + if ([overlay isKindOfClass:[AIRMapPolyline class]]) { + return ((AIRMapPolyline *)overlay).renderer; + } else if ([overlay isKindOfClass:[AIRMapPolygon class]]) { + return ((AIRMapPolygon *)overlay).renderer; + } else if ([overlay isKindOfClass:[AIRMapCircle class]]) { + return ((AIRMapCircle *)overlay).renderer; + } else { + return nil; + } +} + + +#pragma mark Annotation Stuff + + + +- (void)mapView:(AIRMap *)mapView didSelectAnnotationView:(MKAnnotationView *)view +{ + if (![view.annotation isKindOfClass:[AIRMapMarker class]]) return; + AIRMapMarker *marker = (AIRMapMarker *)view.annotation; + + id event = @{ + @"action": @"marker-select", + @"id": marker.identifier ?: @"unknown", + @"coordinate": @{ + @"latitude": @(marker.coordinate.latitude), + @"longitude": @(marker.coordinate.longitude) + } + }; + + if (mapView.onMarkerSelect) mapView.onMarkerSelect(event); + if (marker.onSelect) marker.onSelect(event); + + if (![marker shouldShowCalloutView]) { + // no callout to show + return; + } + + [marker fillCalloutView:mapView.calloutView]; + + // This is where we present our custom callout view... MapKit's built-in callout doesn't have the flexibility + // we need, but a lot of work was done by Nick Farina to make this identical to MapKit's built-in. + [mapView.calloutView presentCalloutFromRect:marker.bounds + inView:marker + constrainedToView:mapView + animated:YES]; +} + +- (void)mapView:(AIRMap *)mapView didDeselectAnnotationView:(MKAnnotationView *)view { + // hide the callout view + [mapView.calloutView dismissCalloutAnimated:YES]; + + if (![view.annotation isKindOfClass:[AIRMapMarker class]]) return; + AIRMapMarker *marker = (AIRMapMarker *)view.annotation; + + id event = @{ + @"action": @"marker-deselect", + @"id": marker.identifier ?: @"unknown", + @"coordinate": @{ + @"latitude": @(marker.coordinate.latitude), + @"longitude": @(marker.coordinate.longitude) + } + }; + + if (mapView.onMarkerDeselect) mapView.onMarkerDeselect(event); + if (marker.onDeselect) marker.onDeselect(event); + +} + +- (MKAnnotationView *)mapView:(__unused AIRMap *)mapView viewForAnnotation:(AIRMapMarker *)marker +{ + marker.map = mapView; + return [marker getAnnotationView]; +} + +- (void)mapView:(AIRMap *)mapView didUpdateUserLocation:(MKUserLocation *)location +{ + if (mapView.followUserLocation) { + MKCoordinateRegion region; + region.span.latitudeDelta = RCTMapDefaultSpan; + region.span.longitudeDelta = RCTMapDefaultSpan; + region.center = location.coordinate; + [mapView setRegion:region animated:YES]; + + // Move to user location only for the first time it loads up. + mapView.followUserLocation = NO; + } +} + +- (void)mapView:(AIRMap *)mapView regionWillChangeAnimated:(__unused BOOL)animated +{ + [self _regionChanged:mapView]; + + mapView.regionChangeObserveTimer = [NSTimer timerWithTimeInterval:RCTMapRegionChangeObserveInterval + target:self + selector:@selector(_onTick:) + userInfo:@{ RCTMapViewKey: mapView } + repeats:YES]; + + [[NSRunLoop mainRunLoop] addTimer:mapView.regionChangeObserveTimer forMode:NSRunLoopCommonModes]; +} + +- (void)mapView:(AIRMap *)mapView regionDidChangeAnimated:(__unused BOOL)animated +{ + [mapView.regionChangeObserveTimer invalidate]; + mapView.regionChangeObserveTimer = nil; + + [self _regionChanged:mapView]; + + // Don't send region did change events until map has + // started rendering, as these won't represent the final location + if (mapView.hasStartedRendering) { + [self _emitRegionChangeEvent:mapView continuous:NO]; + }; + + mapView.pendingCenter = mapView.region.center; + mapView.pendingSpan = mapView.region.span; +} + +- (void)mapViewWillStartRenderingMap:(AIRMap *)mapView +{ + mapView.hasStartedRendering = YES; + [self _emitRegionChangeEvent:mapView continuous:NO]; +} + +#pragma mark Private + +- (void)_onTick:(NSTimer *)timer +{ + [self _regionChanged:timer.userInfo[RCTMapViewKey]]; +} + +- (void)_regionChanged:(AIRMap *)mapView +{ + BOOL needZoom = NO; + CGFloat newLongitudeDelta = 0.0f; + MKCoordinateRegion region = mapView.region; + // On iOS 7, it's possible that we observe invalid locations during initialization of the map. + // Filter those out. + if (!CLLocationCoordinate2DIsValid(region.center)) { + return; + } + // Calculation on float is not 100% accurate. If user zoom to max/min and then move, it's likely the map will auto zoom to max/min from time to time. + // So let's try to make map zoom back to 99% max or 101% min so that there are some buffer that moving the map won't constantly hitting the max/min bound. + if (mapView.maxDelta > FLT_EPSILON && region.span.longitudeDelta > mapView.maxDelta) { + needZoom = YES; + newLongitudeDelta = mapView.maxDelta * (1 - RCTMapZoomBoundBuffer); + } else if (mapView.minDelta > FLT_EPSILON && region.span.longitudeDelta < mapView.minDelta) { + needZoom = YES; + newLongitudeDelta = mapView.minDelta * (1 + RCTMapZoomBoundBuffer); + } + if (needZoom) { + region.span.latitudeDelta = region.span.latitudeDelta / region.span.longitudeDelta * newLongitudeDelta; + region.span.longitudeDelta = newLongitudeDelta; + mapView.region = region; + } + + // Continously observe region changes + [self _emitRegionChangeEvent:mapView continuous:YES]; +} + +- (void)_emitRegionChangeEvent:(AIRMap *)mapView continuous:(BOOL)continuous +{ + if (!mapView.ignoreRegionChanges && mapView.onChange) { + MKCoordinateRegion region = mapView.region; + if (!CLLocationCoordinate2DIsValid(region.center)) { + return; + } + +#define FLUSH_NAN(value) (isnan(value) ? 0 : value) + mapView.onChange(@{ + @"continuous": @(continuous), + @"region": @{ + @"latitude": @(FLUSH_NAN(region.center.latitude)), + @"longitude": @(FLUSH_NAN(region.center.longitude)), + @"latitudeDelta": @(FLUSH_NAN(region.span.latitudeDelta)), + @"longitudeDelta": @(FLUSH_NAN(region.span.longitudeDelta)), + } + }); + } +} + +@end diff --git a/ios/AIRMapMarker.h b/ios/AIRMapMarker.h new file mode 100644 index 0000000000..4ce1442986 --- /dev/null +++ b/ios/AIRMapMarker.h @@ -0,0 +1,45 @@ +/** + * 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. + */ + +#import "AIRMapMarker.h" +#import "AIRMapCallout.h" + +#import +#import + +#import "RCTConvert+MapKit.h" +#import "RCTComponent.h" +#import "AIRMap.h" +#import "SMCalloutView.h" + +@interface AIRMapMarker : MKAnnotationView + +@property (nonatomic, strong) AIRMapCallout *calloutView; +@property (nonatomic, weak) AIRMap *map; + +@property (nonatomic, strong) NSString *identifier; +@property (nonatomic, copy) NSString *title; +@property (nonatomic, copy) NSString *subtitle; +@property (nonatomic, assign) CLLocationCoordinate2D coordinate; +@property (nonatomic, strong) UIColor *pinColor; + +@property (nonatomic, copy) RCTBubblingEventBlock onPress; +@property (nonatomic, copy) RCTDirectEventBlock onSelect; +@property (nonatomic, copy) RCTDirectEventBlock onDeselect; +@property (nonatomic, copy) RCTDirectEventBlock onCalloutPress; + +- (MKAnnotationView *)getAnnotationView; +- (void)fillCalloutView:(SMCalloutView *)calloutView; +- (BOOL)shouldShowCalloutView; + +@end + + +@interface EmptyCalloutBackgroundView : SMCalloutBackgroundView +@end \ No newline at end of file diff --git a/ios/AIRMapMarker.m b/ios/AIRMapMarker.m new file mode 100644 index 0000000000..a5eb5311dc --- /dev/null +++ b/ios/AIRMapMarker.m @@ -0,0 +1,97 @@ +/** + * 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. + */ + +#import "AIRMapMarker.h" + +#import "RCTEventDispatcher.h" +#import "UIView+React.h" + +@implementation EmptyCalloutBackgroundView +@end + +@implementation AIRMapMarker + +- (void)insertReactSubview:(id)subview atIndex:(NSInteger)atIndex { + if ([subview isKindOfClass:[AIRMapCallout class]]) { + self.calloutView = (AIRMapCallout *)subview; + } else { + [super insertReactSubview:subview atIndex:atIndex]; + } +} + +- (void)removeReactSubview:(id)subview { + if ([subview isKindOfClass:[AIRMapCallout class]] && self.calloutView == subview) { + self.calloutView = nil; + } else { + [super removeReactSubview:subview]; + } +} + +- (MKAnnotationView *)getAnnotationView +{ + if (self.subviews.count > 0 || self.image) { + // If it has subviews, it means we are wanting to render a custom marker with arbitrary react views. + // if it has a non-null image, it means we want to render a custom marker with the image. + // In either case, we want to return the AIRMapMarker since it is both an MKAnnotation and an + // MKAnnotationView all at the same time. + return self; + } else { + // In this case, we want to render a platform "default" marker. + MKPinAnnotationView *view = [MKPinAnnotationView new]; + + // TODO(lmr): Looks like this API was introduces in iOS 8. We may want to handle differently for earlier + // versions. Right now it's just leaving it with the default color. People needing the colors are free to + // use their own custom markers. + if ([view respondsToSelector:@selector(setPinTintColor:)]) { + view.pinTintColor = self.pinColor; + } + + return view; + } +} + +- (void)fillCalloutView:(SMCalloutView *)calloutView { + // Set everything necessary on the calloutView before it becomes visible. + + // Apply the MKAnnotationView's desired calloutOffset (from the top-middle of the view) + calloutView.calloutOffset = self.calloutOffset; + + if (self.calloutView) { + calloutView.title = nil; + calloutView.subtitle = nil; + if (self.calloutView.tooltip) { + // if tooltip is true, then the user wants their react view to be the "tooltip" as wwell, so we set + // the background view to something empty/transparent + calloutView.backgroundView = [EmptyCalloutBackgroundView new]; + } else { + // the default tooltip look is wanted, and the user is just filling the content with their react subviews. + // as a result, we use the default "masked" background view. + calloutView.backgroundView = [SMCalloutMaskedBackgroundView new]; + } + + // when this is set, the callout's content will be whatever react views the user has put as the callout's + // children. + calloutView.contentView = self.calloutView; + + } else { + // if there is no calloutView, it means the user wants to use the default callout behavior with title/subtitle + // pairs. + calloutView.title = self.title; + calloutView.subtitle = self.subtitle; + calloutView.contentView = nil; + calloutView.backgroundView = [SMCalloutMaskedBackgroundView new]; + } +} + +- (BOOL)shouldShowCalloutView { + return self.calloutView != nil || self.title != nil || self.subtitle != nil; +} + + +@end diff --git a/ios/AIRMapMarkerManager.h b/ios/AIRMapMarkerManager.h new file mode 100644 index 0000000000..baea307359 --- /dev/null +++ b/ios/AIRMapMarkerManager.h @@ -0,0 +1,14 @@ +/** + * 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. + */ + +#import "RCTViewManager.h" + +@interface AIRMapMarkerManager : RCTViewManager + +@end diff --git a/ios/AIRMapMarkerManager.m b/ios/AIRMapMarkerManager.m new file mode 100644 index 0000000000..582844e423 --- /dev/null +++ b/ios/AIRMapMarkerManager.m @@ -0,0 +1,110 @@ +/** + * 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. + */ + +#import "AIRMapMarkerManager.h" + +#import "RCTUIManager.h" +#import "RCTConvert+CoreLocation.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" + +@interface AIRMapMarkerManager () + +@end + +@implementation AIRMapMarkerManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + AIRMapMarker *map = [AIRMapMarker 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; + [map addGestureRecognizer:tapGestureRecognizer]; + return map; +} + +//RCT_EXPORT_VIEW_PROPERTY(identifier, NSString) +//RCT_EXPORT_VIEW_PROPERTY(reuseIdentifier, NSString) +RCT_EXPORT_VIEW_PROPERTY(title, NSString) +RCT_REMAP_VIEW_PROPERTY(description, subtitle, NSString) +RCT_EXPORT_VIEW_PROPERTY(coordinate, CLLocationCoordinate2D) +RCT_EXPORT_VIEW_PROPERTY(centerOffset, CGPoint) +RCT_EXPORT_VIEW_PROPERTY(calloutOffset, CGPoint) +RCT_EXPORT_VIEW_PROPERTY(image, UIImage) +RCT_EXPORT_VIEW_PROPERTY(pinColor, UIColor) + +RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onSelect, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onDeselect, RCTDirectEventBlock) +RCT_EXPORT_VIEW_PROPERTY(onCalloutPress, RCTDirectEventBlock) + + +RCT_EXPORT_METHOD(showCallout:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMapMarker class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMapMarker *marker = (AIRMapMarker *)view; + + [marker setSelected:YES animated:YES]; + } + }]; +} + +RCT_EXPORT_METHOD(hideCallout:(nonnull NSNumber *)reactTag) +{ + [self.bridge.uiManager addUIBlock:^(__unused RCTUIManager *uiManager, NSDictionary *viewRegistry) { + id view = viewRegistry[reactTag]; + if (![view isKindOfClass:[AIRMapMarker class]]) { + RCTLogError(@"Invalid view returned from registry, expecting AIRMap, got: %@", view); + } else { + AIRMapMarker *marker = (AIRMapMarker *)view; + + [marker setSelected:NO animated:YES]; + } + }]; +} + +#pragma mark - Events + +- (void)_handleTap:(UITapGestureRecognizer *)recognizer { + AIRMapMarker *marker = (AIRMapMarker *)recognizer.view; + if (!marker) return; + + if (marker.selected) { + CGPoint touchPoint = [recognizer locationInView:marker.map.calloutView]; + if ([marker.map.calloutView hitTest:touchPoint withEvent:nil]) { + + // the callout got clicked, not the marker + id event = @{ + @"action": @"callout-press", + }; + + if (marker.onCalloutPress) marker.onCalloutPress(event); + if (marker.calloutView && marker.calloutView.onPress) marker.calloutView.onPress(event); + if (marker.map.onCalloutPress) marker.map.onCalloutPress(event); + return; + } + } + + // the actual marker got clicked + id event = @{ + @"action": @"marker-press", + }; + + if (marker.onPress) marker.onPress(event); + if (marker.map.onMarkerPress) marker.map.onMarkerPress(event); +} + +@end diff --git a/ios/AIRMapPolygon.h b/ios/AIRMapPolygon.h new file mode 100644 index 0000000000..892ff3fbab --- /dev/null +++ b/ios/AIRMapPolygon.h @@ -0,0 +1,38 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +#import +#import + +#import "RCTConvert+MapKit.h" +#import "RCTComponent.h" +#import "AIRMapCoordinate.h" +#import "RCTView.h" + + + +@interface AIRMapPolygon: MKAnnotationView + +@property (nonatomic, strong) MKPolygon *polygon; +@property (nonatomic, strong) MKPolygonRenderer *renderer; + +@property (nonatomic, strong) NSArray *coordinates; +@property (nonatomic, strong) UIColor *fillColor; +@property (nonatomic, strong) UIColor *strokeColor; +@property (nonatomic, assign) CGFloat strokeWidth; +@property (nonatomic, assign) CGFloat miterLimit; +@property (nonatomic, assign) CGLineCap lineCap; +@property (nonatomic, assign) CGLineJoin lineJoin; + +#pragma mark MKOverlay protocol + +@property(nonatomic, readonly) CLLocationCoordinate2D coordinate; +@property(nonatomic, readonly) MKMapRect boundingMapRect; +- (BOOL)intersectsMapRect:(MKMapRect)mapRect; +- (BOOL)canReplaceMapContent; + +@end \ No newline at end of file diff --git a/ios/AIRMapPolygon.m b/ios/AIRMapPolygon.m new file mode 100644 index 0000000000..d6c1a32246 --- /dev/null +++ b/ios/AIRMapPolygon.m @@ -0,0 +1,143 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "AIRMapPolygon.h" +#import "UIView+React.h" + + +@implementation AIRMapPolygon { + +} + +- (void)setFillColor:(UIColor *)fillColor { + _fillColor = fillColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeColor:(UIColor *)strokeColor { + _strokeColor = strokeColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeWidth:(CGFloat)strokeWidth { + _strokeWidth = strokeWidth; + [self applyPropsToRenderer]; +} + +- (void)setLineJoin:(CGLineJoin)lineJoin { + _lineJoin = lineJoin; + [self applyPropsToRenderer]; +} + +- (void)setLineCap:(CGLineCap)lineCap { + _lineCap = lineCap; + [self applyPropsToRenderer]; +} + +- (void)setMiterLimit:(CGFloat)miterLimit { + _miterLimit = miterLimit; + [self applyPropsToRenderer]; +} + +- (void)setCoordinates:(NSArray *)coordinates { + _coordinates = coordinates; + CLLocationCoordinate2D coords[coordinates.count]; + for(int i = 0; i < coordinates.count; i++) + { + coords[i] = coordinates[i].coordinate; + } + self.polygon = [MKPolygon polygonWithCoordinates:coords count:coordinates.count]; + self.renderer = [[MKPolygonRenderer alloc] initWithPolygon:self.polygon]; + [self applyPropsToRenderer]; +} + +- (void) applyPropsToRenderer +{ + if (!_renderer) return; + _renderer.fillColor = _fillColor; + _renderer.strokeColor = _strokeColor; + _renderer.lineWidth = _strokeWidth; + _renderer.lineCap = _lineCap; + _renderer.lineJoin = _lineJoin; + _renderer.miterLimit = _miterLimit; +} + + +#pragma mark MKOverlay implementation + +- (CLLocationCoordinate2D) coordinate +{ + return self.polygon.coordinate; +} + +- (MKMapRect) boundingMapRect +{ + return self.polygon.boundingMapRect; +} + +- (BOOL)intersectsMapRect:(MKMapRect)mapRect +{ + BOOL answer = [self.polygon intersectsMapRect:mapRect]; + return answer; +} + +- (BOOL)canReplaceMapContent +{ + return NO; +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +@end \ No newline at end of file diff --git a/ios/AIRMapPolygonManager.h b/ios/AIRMapPolygonManager.h new file mode 100644 index 0000000000..6321e9d213 --- /dev/null +++ b/ios/AIRMapPolygonManager.h @@ -0,0 +1,10 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTViewManager.h" + + +@interface AIRMapPolygonManager : RCTViewManager +@end \ No newline at end of file diff --git a/ios/AIRMapPolygonManager.m b/ios/AIRMapPolygonManager.m new file mode 100644 index 0000000000..56997b0976 --- /dev/null +++ b/ios/AIRMapPolygonManager.m @@ -0,0 +1,48 @@ +/** + * 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. + */ + +#import "AIRMapPolygonManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" +#import "RCTViewManager.h" +#import "AIRMapPolygon.h" + +@interface AIRMapPolygonManager() + +@end + +@implementation AIRMapPolygonManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + AIRMapPolygon *polygon = [AIRMapPolygon new]; + return polygon; +} + +RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray) +RCT_EXPORT_VIEW_PROPERTY(fillColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap) +RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin) +RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat) + +// NOTE(lmr): +// for now, onPress events for overlays will be left unimplemented. Seems it is possible with some work, but +// it is difficult to achieve in both ios and android so I decided to leave it out. +//RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) + +@end diff --git a/ios/AIRMapPolyline.h b/ios/AIRMapPolyline.h new file mode 100644 index 0000000000..371a1001a8 --- /dev/null +++ b/ios/AIRMapPolyline.h @@ -0,0 +1,37 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import + +#import +#import + +#import "RCTConvert+MapKit.h" +#import "RCTComponent.h" +#import "AIRMapCoordinate.h" +#import "RCTView.h" + + +@interface AIRMapPolyline: MKAnnotationView + +@property (nonatomic, strong) MKPolyline *polyline; +@property (nonatomic, strong) MKPolylineRenderer *renderer; + +@property (nonatomic, strong) NSArray *coordinates; +@property (nonatomic, strong) UIColor *fillColor; +@property (nonatomic, strong) UIColor *strokeColor; +@property (nonatomic, assign) CGFloat strokeWidth; +@property (nonatomic, assign) CGFloat miterLimit; +@property (nonatomic, assign) CGLineCap lineCap; +@property (nonatomic, assign) CGLineJoin lineJoin; + +#pragma mark MKOverlay protocol + +@property(nonatomic, readonly) CLLocationCoordinate2D coordinate; +@property(nonatomic, readonly) MKMapRect boundingMapRect; +- (BOOL)intersectsMapRect:(MKMapRect)mapRect; +- (BOOL)canReplaceMapContent; + +@end \ No newline at end of file diff --git a/ios/AIRMapPolyline.m b/ios/AIRMapPolyline.m new file mode 100644 index 0000000000..35809db31a --- /dev/null +++ b/ios/AIRMapPolyline.m @@ -0,0 +1,91 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "AIRMapPolyline.h" +#import "UIView+React.h" + + +@implementation AIRMapPolyline { + +} + +- (void)setFillColor:(UIColor *)fillColor { + _fillColor = fillColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeColor:(UIColor *)strokeColor { + _strokeColor = strokeColor; + [self applyPropsToRenderer]; +} + +- (void)setStrokeWidth:(CGFloat)strokeWidth { + _strokeWidth = strokeWidth; + [self applyPropsToRenderer]; +} + +- (void)setLineJoin:(CGLineJoin)lineJoin { + _lineJoin = lineJoin; + [self applyPropsToRenderer]; +} + +- (void)setLineCap:(CGLineCap)lineCap { + _lineCap = lineCap; + [self applyPropsToRenderer]; +} + +- (void)setMiterLimit:(CGFloat)miterLimit { + _miterLimit = miterLimit; + [self applyPropsToRenderer]; +} + +- (void)setCoordinates:(NSArray *)coordinates { + _coordinates = coordinates; + CLLocationCoordinate2D coords[coordinates.count]; + for(int i = 0; i < coordinates.count; i++) + { + coords[i] = coordinates[i].coordinate; + } + self.polyline = [MKPolyline polylineWithCoordinates:coords count:coordinates.count]; + self.renderer = [[MKPolylineRenderer alloc] initWithPolyline:self.polyline]; + [self applyPropsToRenderer]; +} + +- (void) applyPropsToRenderer +{ + if (!_renderer) return; + _renderer.fillColor = _fillColor; + _renderer.strokeColor = _strokeColor; + _renderer.lineWidth = _strokeWidth; + _renderer.lineCap = _lineCap; + _renderer.lineJoin = _lineJoin; + _renderer.miterLimit = _miterLimit; +} + + +#pragma mark MKOverlay implementation + +- (CLLocationCoordinate2D) coordinate +{ + return self.polyline.coordinate; +} + +- (MKMapRect) boundingMapRect +{ + return self.polyline.boundingMapRect; +} + +- (BOOL)intersectsMapRect:(MKMapRect)mapRect +{ + BOOL answer = [self.polyline intersectsMapRect:mapRect]; + return answer; +} + +- (BOOL)canReplaceMapContent +{ + return NO; +} + +@end \ No newline at end of file diff --git a/ios/AIRMapPolylineManager.h b/ios/AIRMapPolylineManager.h new file mode 100644 index 0000000000..a529a837a3 --- /dev/null +++ b/ios/AIRMapPolylineManager.h @@ -0,0 +1,10 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTViewManager.h" + + +@interface AIRMapPolylineManager : RCTViewManager +@end \ No newline at end of file diff --git a/ios/AIRMapPolylineManager.m b/ios/AIRMapPolylineManager.m new file mode 100644 index 0000000000..85140ffa77 --- /dev/null +++ b/ios/AIRMapPolylineManager.m @@ -0,0 +1,48 @@ +/** + * 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. + */ + +#import "AIRMapPolylineManager.h" + +#import "RCTBridge.h" +#import "RCTConvert.h" +#import "RCTConvert+CoreLocation.h" +#import "RCTConvert+MoreMapKit.h" +#import "RCTEventDispatcher.h" +#import "UIView+React.h" +#import "AIRMapMarker.h" +#import "RCTViewManager.h" +#import "AIRMapPolyline.h" + +@interface AIRMapPolylineManager() + +@end + +@implementation AIRMapPolylineManager + +RCT_EXPORT_MODULE() + +- (UIView *)view +{ + AIRMapPolyline *polyline = [AIRMapPolyline new]; + return polyline; +} + +RCT_EXPORT_VIEW_PROPERTY(coordinates, AIRMapCoordinateArray) +RCT_EXPORT_VIEW_PROPERTY(strokeColor, UIColor) +RCT_EXPORT_VIEW_PROPERTY(strokeWidth, CGFloat) +RCT_EXPORT_VIEW_PROPERTY(lineCap, CGLineCap) +RCT_EXPORT_VIEW_PROPERTY(lineJoin, CGLineJoin) +RCT_EXPORT_VIEW_PROPERTY(miterLimit, CGFloat) + +// NOTE(lmr): +// for now, onPress events for overlays will be left unimplemented. Seems it is possible with some work, but +// it is difficult to achieve in both ios and android so I decided to leave it out. +//RCT_EXPORT_VIEW_PROPERTY(onPress, RCTBubblingEventBlock) + +@end diff --git a/ios/Callout/SMCalloutView.h b/ios/Callout/SMCalloutView.h new file mode 100644 index 0000000000..f771825cc9 --- /dev/null +++ b/ios/Callout/SMCalloutView.h @@ -0,0 +1,200 @@ +#import +#import + +/* + +SMCalloutView +------------- +Created by Nick Farina (nfarina@gmail.com) +Version 2.1.2 + +*/ + +/// options for which directions the callout is allowed to "point" in. +typedef NS_OPTIONS(NSUInteger, SMCalloutArrowDirection) { + SMCalloutArrowDirectionUp = 1 << 0, + SMCalloutArrowDirectionDown = 1 << 1, + SMCalloutArrowDirectionAny = SMCalloutArrowDirectionUp | SMCalloutArrowDirectionDown +}; + +/// options for the callout present/dismiss animation +typedef NS_ENUM(NSInteger, SMCalloutAnimation) { + /// the "bounce" animation we all know and love from @c UIAlertView + SMCalloutAnimationBounce, + /// a simple fade in or out + SMCalloutAnimationFade, + /// grow or shrink linearly, like in the iPad Calendar app + SMCalloutAnimationStretch +}; + +NS_ASSUME_NONNULL_BEGIN + +/// when delaying our popup in order to scroll content into view, you can use this amount to match the +/// animation duration of UIScrollView when using @c -setContentOffset:animated. +extern NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView; + +@protocol SMCalloutViewDelegate; +@class SMCalloutBackgroundView; + +// +// Callout view. +// + +@interface SMCalloutView : UIView + +@property (nonatomic, weak, nullable) id delegate; +/// title/titleView relationship mimics UINavigationBar. +@property (nonatomic, copy, nullable) NSString *title; +@property (nonatomic, copy, nullable) NSString *subtitle; + +/// Left accessory view for the call out +@property (nonatomic, strong, nullable) UIView *leftAccessoryView; +/// Right accessoty view for the call out +@property (nonatomic, strong, nullable) UIView *rightAccessoryView; +/// Default @c SMCalloutArrowDirectionDown +@property (nonatomic, assign) SMCalloutArrowDirection permittedArrowDirection; +/// The current arrow direction +@property (nonatomic, readonly) SMCalloutArrowDirection currentArrowDirection; +/// if the @c UIView you're constraining to has portions that are overlapped by nav bar, tab bar, etc. you'll need to tell us those insets. +@property (nonatomic, assign) UIEdgeInsets constrainedInsets; +/// default is @c SMCalloutMaskedBackgroundView, or @c SMCalloutDrawnBackgroundView when using @c SMClassicCalloutView +@property (nonatomic, strong) SMCalloutBackgroundView *backgroundView; + +/** + @brief Custom title view. + + @disucssion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on titleView/subtitleView if defined, so your view + may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized. + + @warning If this is set, the respective @c title property will be ignored. + */ +@property (nonatomic, strong, nullable) UIView *titleView; + +/** + @brief Custom subtitle view. + + @discussion Keep in mind that @c SMCalloutView calls @c -sizeThatFits on subtitleView if defined, so your view + may be resized as a result of that (especially if you're using @c UILabel/UITextField). You may want to subclass and override @c -sizeThatFits, or just wrap your view in a "generic" @c UIView if you do not want it to be auto-sized. + + @warning If this is set, the respective @c subtitle property will be ignored. + */ +@property (nonatomic, strong, nullable) UIView *subtitleView; + +/// Custom "content" view that can be any width/height. If this is set, title/subtitle/titleView/subtitleView are all ignored. +@property (nonatomic, retain) UIView *contentView; + +/// Custom content view margin +@property (nonatomic, assign) UIEdgeInsets contentViewInset; + +/// calloutOffset is the offset in screen points from the top-middle of the target view, where the anchor of the callout should be shown. +@property (nonatomic, assign) CGPoint calloutOffset; + +/// default SMCalloutAnimationBounce, SMCalloutAnimationFade respectively +@property (nonatomic, assign) SMCalloutAnimation presentAnimation, dismissAnimation; + +/// Returns a new instance of SMCalloutView if running on iOS 7 or better, otherwise a new instance of SMClassicCalloutView if available. ++ (SMCalloutView *)platformCalloutView; + +/** + @brief Presents a callout view by adding it to "inView" and pointing at the given rect of inView's bounds. + + @discussion Constrains the callout to the bounds of the given view. Optionally scrolls the given rect into view (plus margins) + if @c -delegate is set and responds to @c -delayForRepositionWithSize. + + @param rect @c CGRect to present the view from + @param view view to 'constrain' the @c constrainedView to + @param constrainedView @c UIView to be constrainted in @c view + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated; + +/** + @brief Present a callout layer in the `layer` and pointing at the given rect of the `layer` bounds + + @discussion Same as the view-based presentation, but inserts the callout into a CALayer hierarchy instead. + @note Be aware that you'll have to direct your own touches to any accessory views, since CALayer doesn't relay touch events. + + @param rect @c CGRect to present the view from + @param layer layer to 'constrain' the @c constrainedLayer to + @param constrainedLayer @c CALayer to be constrained in @c layer + @param animated @c BOOL if presentation should be animated + */ +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated; + +/** + Dismiss the callout view + + @param animated @c BOOL if dismissal should be animated + */ +- (void)dismissCalloutAnimated:(BOOL)animated; + +/// For subclassers. You can override this method to provide your own custom animation for presenting/dismissing the callout. +- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting; + +@end + +// +// Background view - default draws the iOS 7 system background style (translucent white with rounded arrow). +// + +/// Abstract base class +@interface SMCalloutBackgroundView : UIView +/// indicates where the tip of the arrow should be drawn, as a pixel offset +@property (nonatomic, assign) CGPoint arrowPoint; +/// will be set by the callout when the callout is in a highlighted state +@property (nonatomic, assign) BOOL highlighted; +/// returns an optional layer whose contents should mask the callout view's contents (not honored by @c SMClassicCalloutView ) +@property (nonatomic, assign) CALayer *contentMask; +/// height of the callout "arrow" +@property (nonatomic, assign) CGFloat anchorHeight; +/// the smallest possible distance from the edge of our control to the "tip" of the anchor, from either left or right +@property (nonatomic, assign) CGFloat anchorMargin; +@end + +/// Default for iOS 7, this reproduces the "masked" behavior of the iOS 7-style callout view. +/// Accessories are masked by the shape of the callout (including the arrow itself). +@interface SMCalloutMaskedBackgroundView : SMCalloutBackgroundView +@end + +// +// Delegate methods +// + +@protocol SMCalloutViewDelegate +@optional + +/// Controls whether the callout "highlights" when pressed. default YES. You must also respond to @c -calloutViewClicked below. +/// Not honored by @c SMClassicCalloutView. +- (BOOL)calloutViewShouldHighlight:(SMCalloutView *)calloutView; + +/// Called when the callout view is clicked. Not honored by @c SMClassicCalloutView. +- (void)calloutViewClicked:(SMCalloutView *)calloutView; + +/** + Called when the callout view detects that it will be outside the constrained view when it appears, + or if the target rect was already outside the constrained view. You can implement this selector + to respond to this situation by repositioning your content first in order to make everything visible. + The @c CGSize passed is the calculated offset necessary to make everything visible (plus a nice margin). + It expects you to return the amount of time you need to reposition things so the popup can be delayed. + Typically you would return @c kSMCalloutViewRepositionDelayForUIScrollView if you're repositioning by calling @c [UIScrollView @c setContentOffset:animated:]. + + @param calloutView the @c SMCalloutView to reposition + @param offset caluclated offset necessary to make everything visible + @returns @c NSTimeInterval to delay the repositioning + */ +- (NSTimeInterval)calloutView:(SMCalloutView *)calloutView delayForRepositionWithSize:(CGSize)offset; + +/// Called before the callout view appears on screen, or before the appearance animation will start. +- (void)calloutViewWillAppear:(SMCalloutView *)calloutView; + +/// Called after the callout view appears on screen, or after the appearance animation is complete. +- (void)calloutViewDidAppear:(SMCalloutView *)calloutView; + +/// Called before the callout view is removed from the screen, or before the disappearance animation is complete. +- (void)calloutViewWillDisappear:(SMCalloutView *)calloutView; + +/// Called after the callout view is removed from the screen, or after the disappearance animation is complete. +- (void)calloutViewDidDisappear:(SMCalloutView *)calloutView; + +NS_ASSUME_NONNULL_END +@end \ No newline at end of file diff --git a/ios/Callout/SMCalloutView.m b/ios/Callout/SMCalloutView.m new file mode 100644 index 0000000000..9472a70be4 --- /dev/null +++ b/ios/Callout/SMCalloutView.m @@ -0,0 +1,858 @@ +#import "SMCalloutView.h" + +// +// UIView frame helpers - we do a lot of UIView frame fiddling in this class; these functions help keep things readable. +// + +@interface UIView (SMFrameAdditions) +@property (nonatomic, assign) CGPoint frameOrigin; +@property (nonatomic, assign) CGSize frameSize; +@property (nonatomic, assign) CGFloat frameX, frameY, frameWidth, frameHeight; // normal rect properties +@property (nonatomic, assign) CGFloat frameLeft, frameTop, frameRight, frameBottom; // these will stretch/shrink the rect +@end + +// +// Callout View. +// + +#define CALLOUT_DEFAULT_CONTAINER_HEIGHT 44 // height of just the main portion without arrow +#define CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT 52 // height of just the main portion without arrow (when subtitle is present) +#define CALLOUT_MIN_WIDTH 61 // minimum width of system callout +#define TITLE_HMARGIN 12 // the title/subtitle view's normal horizontal margin from the edges of our callout view or from the accessories +#define TITLE_TOP 11 // the top of the title view when no subtitle is present +#define TITLE_SUB_TOP 4 // the top of the title view when a subtitle IS present +#define TITLE_HEIGHT 21 // title height, fixed +#define SUBTITLE_TOP 28 // the top of the subtitle, when present +#define SUBTITLE_HEIGHT 15 // subtitle height, fixed +#define BETWEEN_ACCESSORIES_MARGIN 7 // margin between accessories when no title/subtitle is present +#define TOP_ANCHOR_MARGIN 13 // all the above measurements assume a bottom anchor! if we're pointing "up" we'll need to add this top margin to everything. +#define COMFORTABLE_MARGIN 10 // when we try to reposition content to be visible, we'll consider this margin around your target rect + +NSTimeInterval const kSMCalloutViewRepositionDelayForUIScrollView = 1.0/3.0; + +@interface SMCalloutView () +@property (nonatomic, strong) UIButton *containerView; // for masking and interaction +@property (nonatomic, strong) UILabel *titleLabel, *subtitleLabel; +@property (nonatomic, assign) SMCalloutArrowDirection currentArrowDirection; +@property (nonatomic, assign) BOOL popupCancelled; +@end + +@implementation SMCalloutView + ++ (SMCalloutView *)platformCalloutView { + + // if you haven't compiled SMClassicCalloutView into your app, then we can't possibly create an instance of it! + if (!NSClassFromString(@"SMClassicCalloutView")) + return [SMCalloutView new]; + + // ok we have both - so choose the best one based on current platform + if (floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1) + return [SMCalloutView new]; // iOS 7+ + else + return [NSClassFromString(@"SMClassicCalloutView") new]; +} + +- (id)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + self.permittedArrowDirection = SMCalloutArrowDirectionDown; + self.presentAnimation = SMCalloutAnimationBounce; + self.dismissAnimation = SMCalloutAnimationFade; + self.backgroundColor = [UIColor clearColor]; + self.containerView = [UIButton new]; + self.containerView.isAccessibilityElement = NO; + self.isAccessibilityElement = NO; + self.contentViewInset = UIEdgeInsetsMake(12, 12, 12, 12); + + [self.containerView addTarget:self action:@selector(highlightIfNecessary) forControlEvents:UIControlEventTouchDown | UIControlEventTouchDragInside]; + [self.containerView addTarget:self action:@selector(unhighlightIfNecessary) forControlEvents:UIControlEventTouchDragOutside | UIControlEventTouchCancel | UIControlEventTouchUpOutside | UIControlEventTouchUpInside]; + [self.containerView addTarget:self action:@selector(calloutClicked) forControlEvents:UIControlEventTouchUpInside]; + } + return self; +} + +- (BOOL)supportsHighlighting { + if (![self.delegate respondsToSelector:@selector(calloutViewClicked:)]) + return NO; + if ([self.delegate respondsToSelector:@selector(calloutViewShouldHighlight:)]) + return [self.delegate calloutViewShouldHighlight:self]; + return YES; +} + +- (void)highlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = YES; } +- (void)unhighlightIfNecessary { if (self.supportsHighlighting) self.backgroundView.highlighted = NO; } + +- (void)calloutClicked { + if ([self.delegate respondsToSelector:@selector(calloutViewClicked:)]) + [self.delegate calloutViewClicked:self]; +} + +- (UIView *)titleViewOrDefault { + if (self.titleView) + // if you have a custom title view defined, return that. + return self.titleView; + else { + if (!self.titleLabel) { + // create a default titleView + self.titleLabel = [UILabel new]; + self.titleLabel.frameHeight = TITLE_HEIGHT; + self.titleLabel.opaque = NO; + self.titleLabel.backgroundColor = [UIColor clearColor]; + self.titleLabel.font = [UIFont systemFontOfSize:17]; + self.titleLabel.textColor = [UIColor blackColor]; + } + return self.titleLabel; + } +} + +- (UIView *)subtitleViewOrDefault { + if (self.subtitleView) + // if you have a custom subtitle view defined, return that. + return self.subtitleView; + else { + if (!self.subtitleLabel) { + // create a default subtitleView + self.subtitleLabel = [UILabel new]; + self.subtitleLabel.frameHeight = SUBTITLE_HEIGHT; + self.subtitleLabel.opaque = NO; + self.subtitleLabel.backgroundColor = [UIColor clearColor]; + self.subtitleLabel.font = [UIFont systemFontOfSize:12]; + self.subtitleLabel.textColor = [UIColor blackColor]; + } + return self.subtitleLabel; + } +} + +- (SMCalloutBackgroundView *)backgroundView { + // create our default background on first access only if it's nil, since you might have set your own background anyway. + return _backgroundView ? _backgroundView : (_backgroundView = [self defaultBackgroundView]); +} + +- (SMCalloutBackgroundView *)defaultBackgroundView { + return [SMCalloutMaskedBackgroundView new]; +} + +- (void)rebuildSubviews { + // remove and re-add our appropriate subviews in the appropriate order + [self.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self.containerView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; + [self setNeedsDisplay]; + + [self addSubview:self.backgroundView]; + [self addSubview:self.containerView]; + + if (self.contentView) { + [self.containerView addSubview:self.contentView]; + } + else { + if (self.titleViewOrDefault) [self.containerView addSubview:self.titleViewOrDefault]; + if (self.subtitleViewOrDefault) [self.containerView addSubview:self.subtitleViewOrDefault]; + } + if (self.leftAccessoryView) [self.containerView addSubview:self.leftAccessoryView]; + if (self.rightAccessoryView) [self.containerView addSubview:self.rightAccessoryView]; +} + +// Accessory margins. Accessories are centered vertically when shorter +// than the callout, otherwise they grow from the upper corner. + +- (CGFloat)leftAccessoryVerticalMargin { + if (self.leftAccessoryView.frameHeight < self.calloutContainerHeight) + return roundf((self.calloutContainerHeight - self.leftAccessoryView.frameHeight) / 2); + else + return 0; +} + +- (CGFloat)leftAccessoryHorizontalMargin { + return fminf(self.leftAccessoryVerticalMargin, TITLE_HMARGIN); +} + +- (CGFloat)rightAccessoryVerticalMargin { + if (self.rightAccessoryView.frameHeight < self.calloutContainerHeight) + return roundf((self.calloutContainerHeight - self.rightAccessoryView.frameHeight) / 2); + else + return 0; +} + +- (CGFloat)rightAccessoryHorizontalMargin { + return fminf(self.rightAccessoryVerticalMargin, TITLE_HMARGIN); +} + +- (CGFloat)innerContentMarginLeft { + if (self.leftAccessoryView) + return self.leftAccessoryHorizontalMargin + self.leftAccessoryView.frameWidth + TITLE_HMARGIN; + else + return self.contentViewInset.left; +} + +- (CGFloat)innerContentMarginRight { + if (self.rightAccessoryView) + return self.rightAccessoryHorizontalMargin + self.rightAccessoryView.frameWidth + TITLE_HMARGIN; + else + return self.contentViewInset.right; +} + +- (CGFloat)calloutHeight { + return self.calloutContainerHeight + self.backgroundView.anchorHeight; +} + +- (CGFloat)calloutContainerHeight { + if (self.contentView) + return self.contentView.frameHeight + self.contentViewInset.bottom + self.contentViewInset.top; + else if (self.subtitleView || self.subtitle.length > 0) + return CALLOUT_SUB_DEFAULT_CONTAINER_HEIGHT; + else + return CALLOUT_DEFAULT_CONTAINER_HEIGHT; +} + +- (CGSize)sizeThatFits:(CGSize)size { + + // calculate how much non-negotiable space we need to reserve for margin and accessories + CGFloat margin = self.innerContentMarginLeft + self.innerContentMarginRight; + + // how much room is left for text? + CGFloat availableWidthForText = size.width - margin - 1; + + // no room for text? then we'll have to squeeze into the given size somehow. + if (availableWidthForText < 0) + availableWidthForText = 0; + + CGSize preferredTitleSize = [self.titleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, TITLE_HEIGHT)]; + CGSize preferredSubtitleSize = [self.subtitleViewOrDefault sizeThatFits:CGSizeMake(availableWidthForText, SUBTITLE_HEIGHT)]; + + // total width we'd like + CGFloat preferredWidth; + + if (self.contentView) { + + // if we have a content view, then take our preferred size directly from that + preferredWidth = self.contentView.frameWidth + margin; + } + else if (preferredTitleSize.width >= 0.000001 || preferredSubtitleSize.width >= 0.000001) { + + // if we have a title or subtitle, then our assumed margins are valid, and we can apply them + preferredWidth = fmaxf(preferredTitleSize.width, preferredSubtitleSize.width) + margin; + } + else { + // ok we have no title or subtitle to speak of. In this case, the system callout would actually not display + // at all! But we can handle it. + preferredWidth = self.leftAccessoryView.frameWidth + self.rightAccessoryView.frameWidth + self.leftAccessoryHorizontalMargin + self.rightAccessoryHorizontalMargin; + + if (self.leftAccessoryView && self.rightAccessoryView) + preferredWidth += BETWEEN_ACCESSORIES_MARGIN; + } + + // ensure we're big enough to fit our graphics! + preferredWidth = fmaxf(preferredWidth, CALLOUT_MIN_WIDTH); + + // ask to be smaller if we have space, otherwise we'll fit into what we have by truncating the title/subtitle. + return CGSizeMake(fminf(preferredWidth, size.width), self.calloutHeight); +} + +- (CGSize)offsetToContainRect:(CGRect)innerRect inRect:(CGRect)outerRect { + CGFloat nudgeRight = fmaxf(0, CGRectGetMinX(outerRect) - CGRectGetMinX(innerRect)); + CGFloat nudgeLeft = fminf(0, CGRectGetMaxX(outerRect) - CGRectGetMaxX(innerRect)); + CGFloat nudgeTop = fmaxf(0, CGRectGetMinY(outerRect) - CGRectGetMinY(innerRect)); + CGFloat nudgeBottom = fminf(0, CGRectGetMaxY(outerRect) - CGRectGetMaxY(innerRect)); + return CGSizeMake(nudgeLeft ? nudgeLeft : nudgeRight, nudgeTop ? nudgeTop : nudgeBottom); +} + +- (void)presentCalloutFromRect:(CGRect)rect inView:(UIView *)view constrainedToView:(UIView *)constrainedView animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:view.layer ofView:view constrainedToLayer:constrainedView.layer animated:animated]; +} + +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { + [self presentCalloutFromRect:rect inLayer:layer ofView:nil constrainedToLayer:constrainedLayer animated:animated]; +} + +// this private method handles both CALayer and UIView parents depending on what's passed. +- (void)presentCalloutFromRect:(CGRect)rect inLayer:(CALayer *)layer ofView:(UIView *)view constrainedToLayer:(CALayer *)constrainedLayer animated:(BOOL)animated { + + // Sanity check: dismiss this callout immediately if it's displayed somewhere + if (self.layer.superlayer) [self dismissCalloutAnimated:NO]; + + // cancel any presenting animation that may be in progress + [self.layer removeAnimationForKey:@"present"]; + + // figure out the constrained view's rect in our popup view's coordinate system + CGRect constrainedRect = [constrainedLayer convertRect:constrainedLayer.bounds toLayer:layer]; + + // apply our edge constraints + constrainedRect = UIEdgeInsetsInsetRect(constrainedRect, self.constrainedInsets); + + constrainedRect = CGRectInset(constrainedRect, COMFORTABLE_MARGIN, COMFORTABLE_MARGIN); + + // form our subviews based on our content set so far + [self rebuildSubviews]; + + // apply title/subtitle (if present + self.titleLabel.text = self.title; + self.subtitleLabel.text = self.subtitle; + + // size the callout to fit the width constraint as best as possible + self.frameSize = [self sizeThatFits:CGSizeMake(constrainedRect.size.width, self.calloutHeight)]; + + // how much room do we have in the constraint box, both above and below our target rect? + CGFloat topSpace = CGRectGetMinY(rect) - CGRectGetMinY(constrainedRect); + CGFloat bottomSpace = CGRectGetMaxY(constrainedRect) - CGRectGetMaxY(rect); + + // we prefer to point our arrow down. + SMCalloutArrowDirection bestDirection = SMCalloutArrowDirectionDown; + + // we'll point it up though if that's the only option you gave us. + if (self.permittedArrowDirection == SMCalloutArrowDirectionUp) + bestDirection = SMCalloutArrowDirectionUp; + + // or, if we don't have enough space on the top and have more space on the bottom, and you + // gave us a choice, then pointing up is the better option. + if (self.permittedArrowDirection == SMCalloutArrowDirectionAny && topSpace < self.calloutHeight && bottomSpace > topSpace) + bestDirection = SMCalloutArrowDirectionUp; + + self.currentArrowDirection = bestDirection; + + // we want to point directly at the horizontal center of the given rect. calculate our "anchor point" in terms of our + // target view's coordinate system. make sure to offset the anchor point as requested if necessary. + CGFloat anchorX = self.calloutOffset.x + CGRectGetMidX(rect); + CGFloat anchorY = self.calloutOffset.y + (bestDirection == SMCalloutArrowDirectionDown ? CGRectGetMinY(rect) : CGRectGetMaxY(rect)); + + // we prefer to sit centered directly above our anchor + CGFloat calloutX = roundf(anchorX - self.frameWidth / 2); + + // but not if it's going to get too close to the edge of our constraints + if (calloutX < constrainedRect.origin.x) + calloutX = constrainedRect.origin.x; + + if (calloutX > constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth) + calloutX = constrainedRect.origin.x+constrainedRect.size.width-self.frameWidth; + + // what's the farthest to the left and right that we could point to, given our background image constraints? + CGFloat minPointX = calloutX + self.backgroundView.anchorMargin; + CGFloat maxPointX = calloutX + self.frameWidth - self.backgroundView.anchorMargin; + + // we may need to scoot over to the left or right to point at the correct spot + CGFloat adjustX = 0; + if (anchorX < minPointX) adjustX = anchorX - minPointX; + if (anchorX > maxPointX) adjustX = anchorX - maxPointX; + + // add the callout to the given layer (or view if possible, to receive touch events) + if (view) + [view addSubview:self]; + else + [layer addSublayer:self.layer]; + + CGPoint calloutOrigin = { + .x = calloutX + adjustX, + .y = bestDirection == SMCalloutArrowDirectionDown ? (anchorY - self.calloutHeight) : anchorY + }; + + self.frameOrigin = calloutOrigin; + + // now set the *actual* anchor point for our layer so that our "popup" animation starts from this point. + CGPoint anchorPoint = [layer convertPoint:CGPointMake(anchorX, anchorY) toLayer:self.layer]; + + // pass on the anchor point to our background view so it knows where to draw the arrow + self.backgroundView.arrowPoint = anchorPoint; + + // adjust it to unit coordinates for the actual layer.anchorPoint property + anchorPoint.x /= self.frameWidth; + anchorPoint.y /= self.frameHeight; + self.layer.anchorPoint = anchorPoint; + + // setting the anchor point moves the view a bit, so we need to reset + self.frameOrigin = calloutOrigin; + + // make sure our frame is not on half-pixels or else we may be blurry! + CGFloat scale = [UIScreen mainScreen].scale; + self.frameX = floorf(self.frameX*scale)/scale; + self.frameY = floorf(self.frameY*scale)/scale; + + // layout now so we can immediately start animating to the final position if needed + [self setNeedsLayout]; + [self layoutIfNeeded]; + + // if we're outside the bounds of our constraint rect, we'll give our delegate an opportunity to shift us into position. + // consider both our size and the size of our target rect (which we'll assume to be the size of the content you want to scroll into view. + CGRect contentRect = CGRectUnion(self.frame, rect); + CGSize offset = [self offsetToContainRect:contentRect inRect:constrainedRect]; + + NSTimeInterval delay = 0; + self.popupCancelled = NO; // reset this before calling our delegate below + + if ([self.delegate respondsToSelector:@selector(calloutView:delayForRepositionWithSize:)] && !CGSizeEqualToSize(offset, CGSizeZero)) + delay = [self.delegate calloutView:(id)self delayForRepositionWithSize:offset]; + + // there's a chance that user code in the delegate method may have called -dismissCalloutAnimated to cancel things; if that + // happened then we need to bail! + if (self.popupCancelled) return; + + // now we want to mask our contents to our background view (if requested) to match the iOS 7 style + self.containerView.layer.mask = self.backgroundView.contentMask; + + // if we need to delay, we don't want to be visible while we're delaying, so hide us in preparation for our popup + self.hidden = YES; + + // create the appropriate animation, even if we're not animated + CAAnimation *animation = [self animationWithType:self.presentAnimation presenting:YES]; + + // nuke the duration if no animation requested - we'll still need to "run" the animation to get delays and callbacks + if (!animated) + animation.duration = 0.0000001; // can't be zero or the animation won't "run" + + animation.beginTime = CACurrentMediaTime() + delay; + animation.delegate = self; + + [self.layer addAnimation:animation forKey:@"present"]; +} + +- (void)animationDidStart:(CAAnimation *)anim { + BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; + + if (presenting) { + if ([_delegate respondsToSelector:@selector(calloutViewWillAppear:)]) + [_delegate calloutViewWillAppear:(id)self]; + + // ok, animation is on, let's make ourselves visible! + self.hidden = NO; + } + else if (!presenting) { + if ([_delegate respondsToSelector:@selector(calloutViewWillDisappear:)]) + [_delegate calloutViewWillDisappear:(id)self]; + } +} + +- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)finished { + BOOL presenting = [[anim valueForKey:@"presenting"] boolValue]; + + if (presenting && finished) { + if ([_delegate respondsToSelector:@selector(calloutViewDidAppear:)]) + [_delegate calloutViewDidAppear:(id)self]; + } + else if (!presenting && finished) { + + [self removeFromParent]; + [self.layer removeAnimationForKey:@"dismiss"]; + + if ([_delegate respondsToSelector:@selector(calloutViewDidDisappear:)]) + [_delegate calloutViewDidDisappear:(id)self]; + } +} + +- (void)dismissCalloutAnimated:(BOOL)animated { + + // cancel all animations that may be in progress + [self.layer removeAnimationForKey:@"present"]; + [self.layer removeAnimationForKey:@"dismiss"]; + + self.popupCancelled = YES; + + if (animated) { + CAAnimation *animation = [self animationWithType:self.dismissAnimation presenting:NO]; + animation.delegate = self; + [self.layer addAnimation:animation forKey:@"dismiss"]; + } + else { + [self removeFromParent]; + } +} + +- (void)removeFromParent { + if (self.superview) + [self removeFromSuperview]; + else { + // removing a layer from a superlayer causes an implicit fade-out animation that we wish to disable. + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self.layer removeFromSuperlayer]; + [CATransaction commit]; + } +} + +- (CAAnimation *)animationWithType:(SMCalloutAnimation)type presenting:(BOOL)presenting { + CAAnimation *animation = nil; + + if (type == SMCalloutAnimationBounce) { + + CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fade.duration = 0.23; + fade.fromValue = presenting ? @0.0 : @1.0; + fade.toValue = presenting ? @1.0 : @0.0; + fade.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; + + CABasicAnimation *bounce = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + bounce.duration = 0.23; + bounce.fromValue = presenting ? @0.7 : @1.0; + bounce.toValue = presenting ? @1.0 : @0.7; + bounce.timingFunction = [CAMediaTimingFunction functionWithControlPoints:0.59367:0.12066:0.18878:1.5814]; + + CAAnimationGroup *group = [CAAnimationGroup animation]; + group.animations = @[fade, bounce]; + group.duration = 0.23; + + animation = group; + } + else if (type == SMCalloutAnimationFade) { + CABasicAnimation *fade = [CABasicAnimation animationWithKeyPath:@"opacity"]; + fade.duration = 1.0/3.0; + fade.fromValue = presenting ? @0.0 : @1.0; + fade.toValue = presenting ? @1.0 : @0.0; + animation = fade; + } + else if (type == SMCalloutAnimationStretch) { + CABasicAnimation *stretch = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; + stretch.duration = 0.1; + stretch.fromValue = presenting ? @0.0 : @1.0; + stretch.toValue = presenting ? @1.0 : @0.0; + animation = stretch; + } + + // CAAnimation is KVC compliant, so we can store whether we're presenting for lookup in our delegate methods + [animation setValue:@(presenting) forKey:@"presenting"]; + + animation.fillMode = kCAFillModeForwards; + animation.removedOnCompletion = NO; + return animation; +} + +- (void)layoutSubviews { + + self.containerView.frame = self.bounds; + self.backgroundView.frame = self.bounds; + + // if we're pointing up, we'll need to push almost everything down a bit + CGFloat dy = self.currentArrowDirection == SMCalloutArrowDirectionUp ? TOP_ANCHOR_MARGIN : 0; + + self.titleViewOrDefault.frameX = self.innerContentMarginLeft; + self.titleViewOrDefault.frameY = (self.subtitleView || self.subtitle.length ? TITLE_SUB_TOP : TITLE_TOP) + dy; + self.titleViewOrDefault.frameWidth = self.frameWidth - self.innerContentMarginLeft - self.innerContentMarginRight; + + self.subtitleViewOrDefault.frameX = self.titleViewOrDefault.frameX; + self.subtitleViewOrDefault.frameY = SUBTITLE_TOP + dy; + self.subtitleViewOrDefault.frameWidth = self.titleViewOrDefault.frameWidth; + + self.leftAccessoryView.frameX = self.leftAccessoryHorizontalMargin; + self.leftAccessoryView.frameY = self.leftAccessoryVerticalMargin + dy; + + self.rightAccessoryView.frameX = self.frameWidth - self.rightAccessoryHorizontalMargin - self.rightAccessoryView.frameWidth; + self.rightAccessoryView.frameY = self.rightAccessoryVerticalMargin + dy; + + if (self.contentView) { + self.contentView.frameX = self.innerContentMarginLeft; + self.contentView.frameY = self.contentViewInset.top + dy; + } +} + +#pragma mark - Accessibility + +- (NSInteger)accessibilityElementCount { + return (!!self.leftAccessoryView + !!self.titleViewOrDefault + + !!self.subtitleViewOrDefault + !!self.rightAccessoryView); +} + +- (id)accessibilityElementAtIndex:(NSInteger)index { + if (index == 0) { + return self.leftAccessoryView ? self.leftAccessoryView : self.titleViewOrDefault; + } + if (index == 1) { + return self.leftAccessoryView ? self.titleViewOrDefault : self.subtitleViewOrDefault; + } + if (index == 2) { + return self.leftAccessoryView ? self.subtitleViewOrDefault : self.rightAccessoryView; + } + if (index == 3) { + return self.leftAccessoryView ? self.rightAccessoryView : nil; + } + return nil; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element { + if (element == nil) return NSNotFound; + if (element == self.leftAccessoryView) return 0; + if (element == self.titleViewOrDefault) { + return self.leftAccessoryView ? 1 : 0; + } + if (element == self.subtitleViewOrDefault) { + return self.leftAccessoryView ? 2 : 1; + } + if (element == self.rightAccessoryView) { + return self.leftAccessoryView ? 3 : 2; + } + return NSNotFound; +} + +@end + +// import this known "private API" from SMCalloutBackgroundView +@interface SMCalloutBackgroundView (EmbeddedImages) ++ (UIImage *)embeddedImageNamed:(NSString *)name; +@end + +// +// Callout Background View. +// + +@interface SMCalloutMaskedBackgroundView () +@property (nonatomic, strong) UIView *containerView, *containerBorderView, *arrowView; +@property (nonatomic, strong) UIImageView *arrowImageView, *arrowHighlightedImageView, *arrowBorderView; +@end + +static UIImage *blackArrowImage = nil, *whiteArrowImage = nil, *grayArrowImage = nil; + +@implementation SMCalloutMaskedBackgroundView + +- (id)initWithFrame:(CGRect)frame { + if (self = [super initWithFrame:frame]) { + + // Here we're mimicking the very particular (and odd) structure of the system callout view. + // The hierarchy and view/layer values were discovered by inspecting map kit using Reveal.app + + self.containerView = [UIView new]; + self.containerView.backgroundColor = [UIColor whiteColor]; + self.containerView.alpha = 0.96; + self.containerView.layer.cornerRadius = 8; + self.containerView.layer.shadowRadius = 30; + self.containerView.layer.shadowOpacity = 0.1; + + self.containerBorderView = [UIView new]; + self.containerBorderView.layer.borderColor = [UIColor colorWithWhite:0 alpha:0.1].CGColor; + self.containerBorderView.layer.borderWidth = 0.5; + self.containerBorderView.layer.cornerRadius = 8.5; + + if (!blackArrowImage) { + blackArrowImage = [SMCalloutBackgroundView embeddedImageNamed:@"CalloutArrow"]; + whiteArrowImage = [self image:blackArrowImage withColor:[UIColor whiteColor]]; + grayArrowImage = [self image:blackArrowImage withColor:[UIColor colorWithWhite:0.85 alpha:1]]; + } + + self.anchorHeight = 13; + self.anchorMargin = 27; + + self.arrowView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, blackArrowImage.size.width, blackArrowImage.size.height)]; + self.arrowView.alpha = 0.96; + self.arrowImageView = [[UIImageView alloc] initWithImage:whiteArrowImage]; + self.arrowHighlightedImageView = [[UIImageView alloc] initWithImage:grayArrowImage]; + self.arrowHighlightedImageView.hidden = YES; + self.arrowBorderView = [[UIImageView alloc] initWithImage:blackArrowImage]; + self.arrowBorderView.alpha = 0.1; + self.arrowBorderView.frameY = 0.5; + + [self addSubview:self.containerView]; + [self.containerView addSubview:self.containerBorderView]; + [self addSubview:self.arrowView]; + [self.arrowView addSubview:self.arrowBorderView]; + [self.arrowView addSubview:self.arrowImageView]; + [self.arrowView addSubview:self.arrowHighlightedImageView]; + } + return self; +} + +// Make sure we relayout our images when our arrow point changes! +- (void)setArrowPoint:(CGPoint)arrowPoint { + [super setArrowPoint:arrowPoint]; + [self setNeedsLayout]; +} + +- (void)setHighlighted:(BOOL)highlighted { + [super setHighlighted:highlighted]; + self.containerView.backgroundColor = highlighted ? [UIColor colorWithWhite:0.85 alpha:1] : [UIColor whiteColor]; + self.arrowImageView.hidden = highlighted; + self.arrowHighlightedImageView.hidden = !highlighted; +} + +- (UIImage *)image:(UIImage *)image withColor:(UIColor *)color { + + UIGraphicsBeginImageContextWithOptions(image.size, NO, 0); + CGRect imageRect = (CGRect){.size=image.size}; + CGContextRef c = UIGraphicsGetCurrentContext(); + CGContextTranslateCTM(c, 0, image.size.height); + CGContextScaleCTM(c, 1, -1); + CGContextClipToMask(c, imageRect, image.CGImage); + [color setFill]; + CGContextFillRect(c, imageRect); + UIImage *whiteImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return whiteImage; +} + +- (void)layoutSubviews { + + BOOL pointingUp = self.arrowPoint.y < self.frameHeight/2; + + // if we're pointing up, we'll need to push almost everything down a bit + CGFloat dy = pointingUp ? TOP_ANCHOR_MARGIN : 0; + + self.containerView.frame = CGRectMake(0, dy, self.frameWidth, self.frameHeight - self.arrowView.frameHeight + 0.5); + self.containerBorderView.frame = CGRectInset(self.containerView.bounds, -0.5, -0.5); + + self.arrowView.frameX = roundf(self.arrowPoint.x - self.arrowView.frameWidth / 2); + + if (pointingUp) { + self.arrowView.frameY = 1; + self.arrowView.transform = CGAffineTransformMakeRotation(M_PI); + } + else { + self.arrowView.frameY = self.containerView.frameHeight - 0.5; + self.arrowView.transform = CGAffineTransformIdentity; + } +} + +- (CALayer *)contentMask { + + UIGraphicsBeginImageContextWithOptions(self.bounds.size, NO, 0); + + [self.layer renderInContext:UIGraphicsGetCurrentContext()]; + + UIImage *maskImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + + CALayer *layer = [CALayer layer]; + layer.frame = self.bounds; + layer.contents = (id)maskImage.CGImage; + return layer; +} + +@end + +@implementation SMCalloutBackgroundView + ++ (NSData *)dataWithBase64EncodedString:(NSString *)string { + // + // NSData+Base64.m + // + // Version 1.0.2 + // + // Created by Nick Lockwood on 12/01/2012. + // Copyright (C) 2012 Charcoal Design + // + // Distributed under the permissive zlib License + // Get the latest version from here: + // + // https://github.com/nicklockwood/Base64 + // + // This software is provided 'as-is', without any express or implied + // warranty. In no event will the authors be held liable for any damages + // arising from the use of this software. + // + // Permission is granted to anyone to use this software for any purpose, + // including commercial applications, and to alter it and redistribute it + // freely, subject to the following restrictions: + // + // 1. The origin of this software must not be misrepresented; you must not + // claim that you wrote the original software. If you use this software + // in a product, an acknowledgment in the product documentation would be + // appreciated but is not required. + // + // 2. Altered source versions must be plainly marked as such, and must not be + // misrepresented as being the original software. + // + // 3. This notice may not be removed or altered from any source distribution. + // + const char lookup[] = { + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 62, 99, 99, 99, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 99, 99, 99, 99, 99, 99, + 99, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 99, 99, 99, 99, 99, + 99, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 99, 99, 99, 99, 99 + }; + + NSData *inputData = [string dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES]; + long long inputLength = [inputData length]; + const unsigned char *inputBytes = [inputData bytes]; + + long long maxOutputLength = (inputLength / 4 + 1) * 3; + NSMutableData *outputData = [NSMutableData dataWithLength:(NSUInteger)maxOutputLength]; + unsigned char *outputBytes = (unsigned char *)[outputData mutableBytes]; + + int accumulator = 0; + long long outputLength = 0; + unsigned char accumulated[] = {0, 0, 0, 0}; + for (long long i = 0; i < inputLength; i++) { + unsigned char decoded = lookup[inputBytes[i] & 0x7F]; + if (decoded != 99) { + accumulated[accumulator] = decoded; + if (accumulator == 3) { + outputBytes[outputLength++] = (accumulated[0] << 2) | (accumulated[1] >> 4); + outputBytes[outputLength++] = (accumulated[1] << 4) | (accumulated[2] >> 2); + outputBytes[outputLength++] = (accumulated[2] << 6) | accumulated[3]; + } + accumulator = (accumulator + 1) % 4; + } + } + + //handle left-over data + if (accumulator > 0) outputBytes[outputLength] = (accumulated[0] << 2) | (accumulated[1] >> 4); + if (accumulator > 1) outputBytes[++outputLength] = (accumulated[1] << 4) | (accumulated[2] >> 2); + if (accumulator > 2) outputLength++; + + //truncate data to match actual output length + outputData.length = (NSUInteger)outputLength; + return outputLength? outputData: nil; +} + ++ (UIImage *)embeddedImageNamed:(NSString *)name { + CGFloat screenScale = [UIScreen mainScreen].scale; + if (screenScale > 1.0) { + name = [name stringByAppendingString:@"_2x"]; + screenScale = 2.0; + } + + SEL selector = NSSelectorFromString(name); + + if (![(id)self respondsToSelector:selector]) { + NSLog(@"Could not find an embedded image. Ensure that you've added a class-level method named +%@", name); + return nil; + } + + // We need to hush the compiler here - but we know what we're doing! +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + NSString *base64String = [(id)self performSelector:selector]; +#pragma clang diagnostic pop + + UIImage *rawImage = [UIImage imageWithData:[self dataWithBase64EncodedString:base64String]]; + return [UIImage imageWithCGImage:rawImage.CGImage scale:screenScale orientation:UIImageOrientationUp]; +} + ++ (NSString *)CalloutArrow { return @"iVBORw0KGgoAAAANSUhEUgAAACcAAAANCAYAAAAqlHdlAAAAHGlET1QAAAACAAAAAAAAAAcAAAAoAAAABwAAAAYAAADJEgYpIwAAAJVJREFUOBFiYIAAdn5+fkFOTkE5Dg5eW05O3lJOTr6zQPyfDhhoD28pxF5BOZA7gE5ih7oLN8XJyR8MdNwrGjkQaC5/MG7biZDh4OBXBDruLpUdeBdkLhHWE1bCzs6nAnTcUyo58DnIPMK2kqAC6DALIP5JoQNB+i1IsJZ4pcBEm0iJ40D6ibeNDJVAx00k04ETSbUOAAAA//+SwicfAAAAe0lEQVRjYCAdMHNy8u7l5OT7Tzzm3Qu0hpl0q8jQwcPDIwp02B0iHXeHl5dXhAxryNfCzc2tC3TcJwIO/ARSR74tFOjk4uL1BzruHw4H/gPJU2A85Vq5uPjTgY77g+bAPyBxyk2nggkcHPxOnJz8B4AOfAGiQXwqGMsAACGK1kPPMHNBAAAAAElFTkSuQmCC"; } + ++ (NSString *)CalloutArrow_2x { return @"iVBORw0KGgoAAAANSUhEUgAAAE4AAAAaCAYAAAAZtWr8AAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAAA0AAAAoAAAADQAAAA0AAAFMRh0LGwAAARhJREFUWAnclbENwjAQRZ0mih2fDYgsQEVDxQZMgKjpWYAJkBANI8AGDIEoM0WkzBDRAf8klB44g0OkU1zE3/+9RIpS7VVY730/y/woTWlsjJ9iPcN9pbXfY85auyvm/qcDNmb0e2Z+sk/ZBTthN0oVttX12mJIWeaWEFf+kbySmZQa0msu3nzaGJprTXV3BVLNDG/if7bNOTeAvFP35NGJu39GL7Abb27bFXncVQBZLgJf3jp+ebSWIxZMgrxdvPJoJ4gqHpXgV36ITR46HUGaiNMKB6YQd4lI3gV8qTBjmDhrbQFxVQTyKu4ShjJQap7nE4hrfiiv4Q6B8MLGat1bQNztB/JwZm8Rli5wujFu821xfGZgLPUAAAD//4wvm4gAAAD7SURBVOWXMQ6CMBiFgaFpi6VyBEedXJy4hMQTeBSvRDgJEySegI3EQWOivkZnqUB/k0LyL7R9L++D9G+DwP0TCZGUqCdRlYgUuY9F4JCmqQa0hgBcY7wIItFZMLZYS5l0ruAZbXhs6BIROgmhcoB7OIAHTZUTRqG3wp9xmhqc0aRPQu8YAlwxIbwCEUL6GH9wfDcLXY2HpyvvmkHf9+BcrwCuHQGvNRp9Pl6OY0PPAO42AB7WqMxLKLahpFR7gLv/AA9zPe+gtvAMCIC7WMC7CqEPtrqzmBfHyy3A1V/g1Th27GYBY0BIxrk6Ap65254/VZp30GID9JwteQEZrVMWXqGn8gAAAABJRU5ErkJggg=="; } + +@end + +// +// Our UIView frame helpers implementation +// + +@implementation UIView (SMFrameAdditions) + +- (CGPoint)frameOrigin { return self.frame.origin; } +- (void)setFrameOrigin:(CGPoint)origin { self.frame = (CGRect){ .origin=origin, .size=self.frame.size }; } + +- (CGFloat)frameX { return self.frame.origin.x; } +- (void)setFrameX:(CGFloat)x { self.frame = (CGRect){ .origin.x=x, .origin.y=self.frame.origin.y, .size=self.frame.size }; } + +- (CGFloat)frameY { return self.frame.origin.y; } +- (void)setFrameY:(CGFloat)y { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=y, .size=self.frame.size }; } + +- (CGSize)frameSize { return self.frame.size; } +- (void)setFrameSize:(CGSize)size { self.frame = (CGRect){ .origin=self.frame.origin, .size=size }; } + +- (CGFloat)frameWidth { return self.frame.size.width; } +- (void)setFrameWidth:(CGFloat)width { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=width, .size.height=self.frame.size.height }; } + +- (CGFloat)frameHeight { return self.frame.size.height; } +- (void)setFrameHeight:(CGFloat)height { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=height }; } + +- (CGFloat)frameLeft { return self.frame.origin.x; } +- (void)setFrameLeft:(CGFloat)left { self.frame = (CGRect){ .origin.x=left, .origin.y=self.frame.origin.y, .size.width=fmaxf(self.frame.origin.x+self.frame.size.width-left,0), .size.height=self.frame.size.height }; } + +- (CGFloat)frameTop { return self.frame.origin.y; } +- (void)setFrameTop:(CGFloat)top { self.frame = (CGRect){ .origin.x=self.frame.origin.x, .origin.y=top, .size.width=self.frame.size.width, .size.height=fmaxf(self.frame.origin.y+self.frame.size.height-top,0) }; } + +- (CGFloat)frameRight { return self.frame.origin.x + self.frame.size.width; } +- (void)setFrameRight:(CGFloat)right { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=fmaxf(right-self.frame.origin.x,0), .size.height=self.frame.size.height }; } + +- (CGFloat)frameBottom { return self.frame.origin.y + self.frame.size.height; } +- (void)setFrameBottom:(CGFloat)bottom { self.frame = (CGRect){ .origin=self.frame.origin, .size.width=self.frame.size.width, .size.height=fmaxf(bottom-self.frame.origin.y,0) }; } + +@end \ No newline at end of file diff --git a/ios/RCTConvert+MoreMapKit.h b/ios/RCTConvert+MoreMapKit.h new file mode 100644 index 0000000000..96c99a5994 --- /dev/null +++ b/ios/RCTConvert+MoreMapKit.h @@ -0,0 +1,12 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import +#import +#import "RCTConvert.h" + +@interface RCTConvert (MoreMapKit) + +@end \ No newline at end of file diff --git a/ios/RCTConvert+MoreMapKit.m b/ios/RCTConvert+MoreMapKit.m new file mode 100644 index 0000000000..2292cb8788 --- /dev/null +++ b/ios/RCTConvert+MoreMapKit.m @@ -0,0 +1,27 @@ +// +// Created by Leland Richardson on 12/27/15. +// Copyright (c) 2015 Facebook. All rights reserved. +// + +#import "RCTConvert+MoreMapKit.h" +#import "AIRMapCoordinate.h" +#import "RCTConvert+CoreLocation.h" + + +@implementation RCTConvert (MoreMapKit) + +// NOTE(lmr): +// This is a bit of a hack, but I'm using this class to simply wrap +// around a `CLLocationCoordinate2D`, since I was unable to figure out +// how to handle an array of structs like CLLocationCoordinate2D. Would love +// to get rid of this if someone can show me how... ++ (AIRMapCoordinate *)AIRMapCoordinate:(id)json +{ + AIRMapCoordinate *coord = [AIRMapCoordinate new]; + coord.coordinate = [self CLLocationCoordinate2D:json]; + return coord; +} + +RCT_ARRAY_CONVERTER(AIRMapCoordinate) + +@end \ No newline at end of file diff --git a/ios/rn_mapview.xcodeproj/project.pbxproj b/ios/rn_mapview.xcodeproj/project.pbxproj new file mode 100644 index 0000000000..24f6393b3b --- /dev/null +++ b/ios/rn_mapview.xcodeproj/project.pbxproj @@ -0,0 +1,868 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; + 00E356F31AD99517003FC87E /* rn_mapviewTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* rn_mapviewTests.m */; }; + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; + 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; + 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; + 30BE321827464963924C05E1 /* AIRMapCallout.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE3FE8AE44A4F10A8E3772 /* AIRMapCallout.m */; }; + 30BE336E7A5DFD0C46D53381 /* SMCalloutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE3060FB318CEBDD0FF4CE /* SMCalloutView.m */; }; + 30BE3459CCFDF4EA0DAE6195 /* AIRMapCalloutManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE368A9E17365603F7C9EC /* AIRMapCalloutManager.m */; }; + 30BE359BF58ECE549659E4B1 /* AIRMapCoordinate.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE32AB3F8F320529FE9599 /* AIRMapCoordinate.m */; }; + 30BE35A9D9D07BC7384AD577 /* AIRMapCircle.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE30F862819C6D53F63551 /* AIRMapCircle.m */; }; + 30BE362188C3AEEF72582408 /* AIRMapPolygonManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE38F736781B55E22819C8 /* AIRMapPolygonManager.m */; }; + 30BE374B2CF0430553DF00DD /* AIRMapCircleManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE34D587C0DC3F1C5B2708 /* AIRMapCircleManager.m */; }; + 30BE3834D908CDF15BA071F6 /* RCTConvert+MoreMapKit.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE3DB31CC9DDDA962A49CB /* RCTConvert+MoreMapKit.m */; }; + 30BE39BA519AB3A2F19E8AFD /* AIRMapPolylineManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE3BA49502EB2AF8BCF5E1 /* AIRMapPolylineManager.m */; }; + 30BE3B1430E348929B0C9E52 /* AIRMapPolyline.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE306FD37FAE706A2CEAA4 /* AIRMapPolyline.m */; }; + 30BE3D2300BA633035319921 /* AIRMapPolygon.m in Sources */ = {isa = PBXBuildFile; fileRef = 30BE3D6EF250F9FE1577A441 /* AIRMapPolygon.m */; }; + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; + 84C1E43C1C1CBF87001A48A9 /* AIRMap.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1E43B1C1CBF87001A48A9 /* AIRMap.m */; }; + 84C1E43F1C1CBFB9001A48A9 /* AIRMapMarker.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1E43E1C1CBFB9001A48A9 /* AIRMapMarker.m */; }; + 84C1E4431C1CC104001A48A9 /* AIRMapManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1E4421C1CC104001A48A9 /* AIRMapManager.m */; }; + 84C1E4451C1CC115001A48A9 /* AIRMapMarkerManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C1E4441C1CC115001A48A9 /* AIRMapMarkerManager.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTActionSheet; + }; + 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTGeolocation; + }; + 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5115D1A9E6B3D00147676; + remoteInfo = RCTImage; + }; + 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B511DB1A9E6C8500147676; + remoteInfo = RCTNetwork; + }; + 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; + remoteInfo = RCTVibration; + }; + 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 13B07F861A680F5B00A75B9A; + remoteInfo = rn_mapview; + }; + 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTSettings; + }; + 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 3C86DF461ADF2C930047B81A; + remoteInfo = RCTWebSocket; + }; + 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; + remoteInfo = React; + }; + 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 134814201AA4EA6300B7C361; + remoteInfo = RCTLinking; + }; + 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = 58B5119B1A9E6C1200147676; + remoteInfo = RCTText; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = main.jsbundle; sourceTree = ""; }; + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = "../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj"; sourceTree = ""; }; + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = "../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj"; sourceTree = ""; }; + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = "../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj"; sourceTree = ""; }; + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = "../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj"; sourceTree = ""; }; + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = "../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj"; sourceTree = ""; }; + 00E356EE1AD99517003FC87E /* rn_mapviewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = rn_mapviewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 00E356F21AD99517003FC87E /* rn_mapviewTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = rn_mapviewTests.m; sourceTree = ""; }; + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = "../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj"; sourceTree = ""; }; + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = "../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj"; sourceTree = ""; }; + 13B07F961A680F5B00A75B9A /* rn_mapview.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = rn_mapview.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = rn_mapview/AppDelegate.h; sourceTree = ""; }; + 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = rn_mapview/AppDelegate.m; sourceTree = ""; }; + 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; + 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = rn_mapview/Images.xcassets; sourceTree = ""; }; + 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = rn_mapview/Info.plist; sourceTree = ""; }; + 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = rn_mapview/main.m; sourceTree = ""; }; + 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = "../node_modules/react-native/React/React.xcodeproj"; sourceTree = ""; }; + 30BE30353ADD5C8CDAE6DD47 /* AIRMapCallout.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapCallout.h; sourceTree = ""; }; + 30BE3060FB318CEBDD0FF4CE /* SMCalloutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SMCalloutView.m; sourceTree = ""; }; + 30BE306FD37FAE706A2CEAA4 /* AIRMapPolyline.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapPolyline.m; sourceTree = ""; }; + 30BE30F862819C6D53F63551 /* AIRMapCircle.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapCircle.m; sourceTree = ""; }; + 30BE31A0A9D57A8E926B7AF0 /* SMCalloutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SMCalloutView.h; sourceTree = ""; }; + 30BE3215547586B72D4EF1EE /* AIRMapPolyline.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapPolyline.h; sourceTree = ""; }; + 30BE32AB3F8F320529FE9599 /* AIRMapCoordinate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapCoordinate.m; sourceTree = ""; }; + 30BE34D587C0DC3F1C5B2708 /* AIRMapCircleManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapCircleManager.m; sourceTree = ""; }; + 30BE368A9E17365603F7C9EC /* AIRMapCalloutManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapCalloutManager.m; sourceTree = ""; }; + 30BE37502E6631DAA5369109 /* AIRMapPolygonManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapPolygonManager.h; sourceTree = ""; }; + 30BE376077E1EE4D0BF29A6C /* AIRMapCoordinate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapCoordinate.h; sourceTree = ""; }; + 30BE377457E2273D0535FDC6 /* AIRMapPolygon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapPolygon.h; sourceTree = ""; }; + 30BE38E699024D1B0B64DF97 /* AIRMapCircle.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapCircle.h; sourceTree = ""; }; + 30BE38F736781B55E22819C8 /* AIRMapPolygonManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapPolygonManager.m; sourceTree = ""; }; + 30BE39E37E51F6F345C7468C /* AIRMapCircleManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapCircleManager.h; sourceTree = ""; }; + 30BE3BA49502EB2AF8BCF5E1 /* AIRMapPolylineManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapPolylineManager.m; sourceTree = ""; }; + 30BE3BFE2317BAC4C184525E /* AIRMapCalloutManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapCalloutManager.h; sourceTree = ""; }; + 30BE3CD9D108C654CDF73F09 /* RCTConvert+MoreMapKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "RCTConvert+MoreMapKit.h"; sourceTree = ""; }; + 30BE3D6EF250F9FE1577A441 /* AIRMapPolygon.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapPolygon.m; sourceTree = ""; }; + 30BE3D91BE5DA8805D95FBBF /* AIRMapPolylineManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapPolylineManager.h; sourceTree = ""; }; + 30BE3DB31CC9DDDA962A49CB /* RCTConvert+MoreMapKit.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "RCTConvert+MoreMapKit.m"; sourceTree = ""; }; + 30BE3FE8AE44A4F10A8E3772 /* AIRMapCallout.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapCallout.m; sourceTree = ""; }; + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = "../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj"; sourceTree = ""; }; + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = ""; }; + 84C1E43B1C1CBF87001A48A9 /* AIRMap.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMap.m; sourceTree = ""; }; + 84C1E43D1C1CBF96001A48A9 /* AIRMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMap.h; sourceTree = ""; }; + 84C1E43E1C1CBFB9001A48A9 /* AIRMapMarker.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapMarker.m; sourceTree = ""; }; + 84C1E4401C1CBFC9001A48A9 /* AIRMapMarker.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapMarker.h; sourceTree = ""; }; + 84C1E4411C1CC0F3001A48A9 /* AIRMapManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapManager.h; sourceTree = ""; }; + 84C1E4421C1CC104001A48A9 /* AIRMapManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapManager.m; sourceTree = ""; }; + 84C1E4441C1CC115001A48A9 /* AIRMapMarkerManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AIRMapMarkerManager.m; sourceTree = ""; }; + 84C1E4461C1CC129001A48A9 /* AIRMapMarkerManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AIRMapMarkerManager.h; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 00E356EB1AD99517003FC87E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 146834051AC3E58100842450 /* libReact.a in Frameworks */, + 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, + 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, + 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, + 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, + 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, + 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, + 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, + 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, + 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 00C302A81ABCB8CE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302B61ABCB90400DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302BC1ABCB91800DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302D41ABCB9D200DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, + ); + name = Products; + sourceTree = ""; + }; + 00C302E01ABCB9EE00DB3ED1 /* Products */ = { + isa = PBXGroup; + children = ( + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, + ); + name = Products; + sourceTree = ""; + }; + 00E356EF1AD99517003FC87E /* rn_mapviewTests */ = { + isa = PBXGroup; + children = ( + 00E356F21AD99517003FC87E /* rn_mapviewTests.m */, + 00E356F01AD99517003FC87E /* Supporting Files */, + ); + path = rn_mapviewTests; + sourceTree = ""; + }; + 00E356F01AD99517003FC87E /* Supporting Files */ = { + isa = PBXGroup; + children = ( + 00E356F11AD99517003FC87E /* Info.plist */, + ); + name = "Supporting Files"; + sourceTree = ""; + }; + 139105B71AF99BAD00B5F7CC /* Products */ = { + isa = PBXGroup; + children = ( + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, + ); + name = Products; + sourceTree = ""; + }; + 139FDEE71B06529A00C62182 /* Products */ = { + isa = PBXGroup; + children = ( + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, + ); + name = Products; + sourceTree = ""; + }; + 13B07FAE1A68108700A75B9A /* rn_mapview */ = { + isa = PBXGroup; + children = ( + 008F07F21AC5B25A0029DE68 /* main.jsbundle */, + 13B07FAF1A68108700A75B9A /* AppDelegate.h */, + 13B07FB01A68108700A75B9A /* AppDelegate.m */, + 13B07FB51A68108700A75B9A /* Images.xcassets */, + 13B07FB61A68108700A75B9A /* Info.plist */, + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, + 13B07FB71A68108700A75B9A /* main.m */, + ); + name = rn_mapview; + sourceTree = ""; + }; + 146834001AC3E56700842450 /* Products */ = { + isa = PBXGroup; + children = ( + 146834041AC3E56700842450 /* libReact.a */, + ); + name = Products; + sourceTree = ""; + }; + 30BE3D4A6F339B34F3FA8893 /* Callout */ = { + isa = PBXGroup; + children = ( + 30BE3060FB318CEBDD0FF4CE /* SMCalloutView.m */, + 30BE31A0A9D57A8E926B7AF0 /* SMCalloutView.h */, + ); + path = Callout; + sourceTree = ""; + }; + 78C398B11ACF4ADC00677621 /* Products */ = { + isa = PBXGroup; + children = ( + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, + ); + name = Products; + sourceTree = ""; + }; + 832341AE1AAA6A7D00B99B32 /* Libraries */ = { + isa = PBXGroup; + children = ( + 146833FF1AC3E56700842450 /* React.xcodeproj */, + 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, + 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, + 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, + 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, + 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, + 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, + 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, + 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, + 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, + ); + name = Libraries; + sourceTree = ""; + }; + 832341B11AAA6A8300B99B32 /* Products */ = { + isa = PBXGroup; + children = ( + 832341B51AAA6A8300B99B32 /* libRCTText.a */, + ); + name = Products; + sourceTree = ""; + }; + 83CBB9F61A601CBA00E9B192 = { + isa = PBXGroup; + children = ( + 84C1E4301C1CBF42001A48A9 /* Components */, + 13B07FAE1A68108700A75B9A /* rn_mapview */, + 832341AE1AAA6A7D00B99B32 /* Libraries */, + 00E356EF1AD99517003FC87E /* rn_mapviewTests */, + 83CBBA001A601CBA00E9B192 /* Products */, + 30BE3D4A6F339B34F3FA8893 /* Callout */, + ); + indentWidth = 2; + sourceTree = ""; + tabWidth = 2; + }; + 83CBBA001A601CBA00E9B192 /* Products */ = { + isa = PBXGroup; + children = ( + 13B07F961A680F5B00A75B9A /* rn_mapview.app */, + 00E356EE1AD99517003FC87E /* rn_mapviewTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 84C1E4301C1CBF42001A48A9 /* Components */ = { + isa = PBXGroup; + children = ( + 84C1E43B1C1CBF87001A48A9 /* AIRMap.m */, + 84C1E43D1C1CBF96001A48A9 /* AIRMap.h */, + 84C1E43E1C1CBFB9001A48A9 /* AIRMapMarker.m */, + 84C1E4401C1CBFC9001A48A9 /* AIRMapMarker.h */, + 84C1E4411C1CC0F3001A48A9 /* AIRMapManager.h */, + 84C1E4421C1CC104001A48A9 /* AIRMapManager.m */, + 84C1E4441C1CC115001A48A9 /* AIRMapMarkerManager.m */, + 84C1E4461C1CC129001A48A9 /* AIRMapMarkerManager.h */, + 30BE306FD37FAE706A2CEAA4 /* AIRMapPolyline.m */, + 30BE3215547586B72D4EF1EE /* AIRMapPolyline.h */, + 30BE3BA49502EB2AF8BCF5E1 /* AIRMapPolylineManager.m */, + 30BE3D91BE5DA8805D95FBBF /* AIRMapPolylineManager.h */, + 30BE3DB31CC9DDDA962A49CB /* RCTConvert+MoreMapKit.m */, + 30BE3CD9D108C654CDF73F09 /* RCTConvert+MoreMapKit.h */, + 30BE32AB3F8F320529FE9599 /* AIRMapCoordinate.m */, + 30BE376077E1EE4D0BF29A6C /* AIRMapCoordinate.h */, + 30BE3D6EF250F9FE1577A441 /* AIRMapPolygon.m */, + 30BE377457E2273D0535FDC6 /* AIRMapPolygon.h */, + 30BE38F736781B55E22819C8 /* AIRMapPolygonManager.m */, + 30BE37502E6631DAA5369109 /* AIRMapPolygonManager.h */, + 30BE3FE8AE44A4F10A8E3772 /* AIRMapCallout.m */, + 30BE30353ADD5C8CDAE6DD47 /* AIRMapCallout.h */, + 30BE368A9E17365603F7C9EC /* AIRMapCalloutManager.m */, + 30BE3BFE2317BAC4C184525E /* AIRMapCalloutManager.h */, + 30BE30F862819C6D53F63551 /* AIRMapCircle.m */, + 30BE38E699024D1B0B64DF97 /* AIRMapCircle.h */, + 30BE34D587C0DC3F1C5B2708 /* AIRMapCircleManager.m */, + 30BE39E37E51F6F345C7468C /* AIRMapCircleManager.h */, + ); + name = Components; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 00E356ED1AD99517003FC87E /* rn_mapviewTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "rn_mapviewTests" */; + buildPhases = ( + 00E356EA1AD99517003FC87E /* Sources */, + 00E356EB1AD99517003FC87E /* Frameworks */, + 00E356EC1AD99517003FC87E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 00E356F51AD99517003FC87E /* PBXTargetDependency */, + ); + name = rn_mapviewTests; + productName = rn_mapviewTests; + productReference = 00E356EE1AD99517003FC87E /* rn_mapviewTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 13B07F861A680F5B00A75B9A /* rn_mapview */ = { + isa = PBXNativeTarget; + buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "rn_mapview" */; + buildPhases = ( + 13B07F871A680F5B00A75B9A /* Sources */, + 13B07F8C1A680F5B00A75B9A /* Frameworks */, + 13B07F8E1A680F5B00A75B9A /* Resources */, + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = rn_mapview; + productName = "Hello World"; + productReference = 13B07F961A680F5B00A75B9A /* rn_mapview.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 83CBB9F71A601CBA00E9B192 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0610; + ORGANIZATIONNAME = Facebook; + TargetAttributes = { + 00E356ED1AD99517003FC87E = { + CreatedOnToolsVersion = 6.2; + TestTargetID = 13B07F861A680F5B00A75B9A; + }; + }; + }; + buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "rn_mapview" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 83CBB9F61A601CBA00E9B192; + productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; + ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; + }, + { + ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; + ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; + }, + { + ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; + ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; + }, + { + ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; + ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; + }, + { + ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; + ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; + }, + { + ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; + ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; + }, + { + ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; + ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; + }, + { + ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; + ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; + }, + { + ProductGroup = 139FDEE71B06529A00C62182 /* Products */; + ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; + }, + { + ProductGroup = 146834001AC3E56700842450 /* Products */; + ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 13B07F861A680F5B00A75B9A /* rn_mapview */, + 00E356ED1AD99517003FC87E /* rn_mapviewTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTActionSheet.a; + remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTGeolocation.a; + remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTImage.a; + remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTNetwork.a; + remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTVibration.a; + remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTSettings.a; + remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTWebSocket.a; + remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 146834041AC3E56700842450 /* libReact.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libReact.a; + remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTLinking.a; + remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { + isa = PBXReferenceProxy; + fileType = archive.ar; + path = libRCTText.a; + remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 00E356EC1AD99517003FC87E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F8E1A680F5B00A75B9A /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, + 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Bundle React Native code and images"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "../node_modules/react-native/packager/react-native-xcode.sh"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 00E356EA1AD99517003FC87E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 00E356F31AD99517003FC87E /* rn_mapviewTests.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 13B07F871A680F5B00A75B9A /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 84C1E4451C1CC115001A48A9 /* AIRMapMarkerManager.m in Sources */, + 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, + 84C1E4431C1CC104001A48A9 /* AIRMapManager.m in Sources */, + 13B07FC11A68108700A75B9A /* main.m in Sources */, + 84C1E43F1C1CBFB9001A48A9 /* AIRMapMarker.m in Sources */, + 84C1E43C1C1CBF87001A48A9 /* AIRMap.m in Sources */, + 30BE3B1430E348929B0C9E52 /* AIRMapPolyline.m in Sources */, + 30BE39BA519AB3A2F19E8AFD /* AIRMapPolylineManager.m in Sources */, + 30BE3834D908CDF15BA071F6 /* RCTConvert+MoreMapKit.m in Sources */, + 30BE359BF58ECE549659E4B1 /* AIRMapCoordinate.m in Sources */, + 30BE3D2300BA633035319921 /* AIRMapPolygon.m in Sources */, + 30BE362188C3AEEF72582408 /* AIRMapPolygonManager.m in Sources */, + 30BE321827464963924C05E1 /* AIRMapCallout.m in Sources */, + 30BE3459CCFDF4EA0DAE6195 /* AIRMapCalloutManager.m in Sources */, + 30BE35A9D9D07BC7384AD577 /* AIRMapCircle.m in Sources */, + 30BE374B2CF0430553DF00DD /* AIRMapCircleManager.m in Sources */, + 30BE336E7A5DFD0C46D53381 /* SMCalloutView.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 13B07F861A680F5B00A75B9A /* rn_mapview */; + targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { + isa = PBXVariantGroup; + children = ( + 13B07FB21A68108700A75B9A /* Base */, + ); + name = LaunchScreen.xib; + path = rn_mapview; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 00E356F61AD99517003FC87E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + INFOPLIST_FILE = rn_mapviewTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/rn_mapview.app/rn_mapview"; + }; + name = Debug; + }; + 00E356F71AD99517003FC87E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + COPY_PHASE_STRIP = NO; + FRAMEWORK_SEARCH_PATHS = ( + "$(SDKROOT)/Developer/Library/Frameworks", + "$(inherited)", + ); + INFOPLIST_FILE = rn_mapviewTests/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 8.2; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_NAME = "$(TARGET_NAME)"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/rn_mapview.app/rn_mapview"; + }; + name = Release; + }; + 13B07F941A680F5B00A75B9A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + DEAD_CODE_STRIPPING = NO; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + INFOPLIST_FILE = rn_mapview/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = rn_mapview; + }; + name = Debug; + }; + 13B07F951A680F5B00A75B9A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + INFOPLIST_FILE = rn_mapview/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = rn_mapview; + }; + name = Release; + }; + 83CBBA201A601CBA00E9B192 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 83CBBA211A601CBA00E9B192 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = YES; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + IPHONEOS_DEPLOYMENT_TARGET = 7.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "rn_mapviewTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 00E356F61AD99517003FC87E /* Debug */, + 00E356F71AD99517003FC87E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "rn_mapview" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 13B07F941A680F5B00A75B9A /* Debug */, + 13B07F951A680F5B00A75B9A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "rn_mapview" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 83CBBA201A601CBA00E9B192 /* Debug */, + 83CBBA211A601CBA00E9B192 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; +} diff --git a/ios/rn_mapview.xcodeproj/xcshareddata/xcschemes/rn_mapview.xcscheme b/ios/rn_mapview.xcodeproj/xcshareddata/xcschemes/rn_mapview.xcscheme new file mode 100644 index 0000000000..534107d0ff --- /dev/null +++ b/ios/rn_mapview.xcodeproj/xcshareddata/xcschemes/rn_mapview.xcscheme @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/rn_mapview/AppDelegate.h b/ios/rn_mapview/AppDelegate.h new file mode 100644 index 0000000000..a9654d5e01 --- /dev/null +++ b/ios/rn_mapview/AppDelegate.h @@ -0,0 +1,16 @@ +/** + * 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. + */ + +#import + +@interface AppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; + +@end diff --git a/ios/rn_mapview/AppDelegate.m b/ios/rn_mapview/AppDelegate.m new file mode 100644 index 0000000000..41bd1da802 --- /dev/null +++ b/ios/rn_mapview/AppDelegate.m @@ -0,0 +1,57 @@ +/** + * 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. + */ + +#import "AppDelegate.h" + +#import "RCTRootView.h" + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + NSURL *jsCodeLocation; + + /** + * Loading JavaScript code - uncomment the one you want. + * + * OPTION 1 + * Load from development server. Start the server from the repository root: + * + * $ npm start + * + * To run on device, change `localhost` to the IP address of your computer + * (you can get this by typing `ifconfig` into the terminal and selecting the + * `inet` value under `en0:`) and make sure your computer and iOS device are + * on the same Wi-Fi network. + */ + + jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"]; + + /** + * OPTION 2 + * Load from pre-bundled file on disk. The static bundle is automatically + * generated by "Bundle React Native code and images" build step. + */ + +// jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"]; + + RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation + moduleName:@"rn_mapview" + initialProperties:nil + launchOptions:launchOptions]; + + self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + UIViewController *rootViewController = [UIViewController new]; + rootViewController.view = rootView; + self.window.rootViewController = rootViewController; + [self.window makeKeyAndVisible]; + return YES; +} + +@end diff --git a/ios/rn_mapview/Base.lproj/LaunchScreen.xib b/ios/rn_mapview/Base.lproj/LaunchScreen.xib new file mode 100644 index 0000000000..32d835ff8d --- /dev/null +++ b/ios/rn_mapview/Base.lproj/LaunchScreen.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ios/rn_mapview/Images.xcassets/AppIcon.appiconset/Contents.json b/ios/rn_mapview/Images.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000000..118c98f746 --- /dev/null +++ b/ios/rn_mapview/Images.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,38 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "29x29", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "40x40", + "scale" : "3x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "2x" + }, + { + "idiom" : "iphone", + "size" : "60x60", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/ios/rn_mapview/Info.plist b/ios/rn_mapview/Info.plist new file mode 100644 index 0000000000..cddf0766c9 --- /dev/null +++ b/ios/rn_mapview/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + NSLocationWhenInUseUsageDescription + + NSAppTransportSecurity + + + NSAllowsArbitraryLoads + + + + diff --git a/ios/rn_mapview/main.m b/ios/rn_mapview/main.m new file mode 100644 index 0000000000..3d767fcbb9 --- /dev/null +++ b/ios/rn_mapview/main.m @@ -0,0 +1,18 @@ +/** + * 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. + */ + +#import + +#import "AppDelegate.h" + +int main(int argc, char * argv[]) { + @autoreleasepool { + return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); + } +} diff --git a/ios/rn_mapviewTests/Info.plist b/ios/rn_mapviewTests/Info.plist new file mode 100644 index 0000000000..886825ccc9 --- /dev/null +++ b/ios/rn_mapviewTests/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1 + + diff --git a/ios/rn_mapviewTests/rn_mapviewTests.m b/ios/rn_mapviewTests/rn_mapviewTests.m new file mode 100644 index 0000000000..f155dd06da --- /dev/null +++ b/ios/rn_mapviewTests/rn_mapviewTests.m @@ -0,0 +1,70 @@ +/** + * 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. + */ + +#import +#import + +#import "RCTLog.h" +#import "RCTRootView.h" + +#define TIMEOUT_SECONDS 240 +#define TEXT_TO_LOOK_FOR @"Welcome to React Native!" + +@interface rn_mapviewTests : XCTestCase + +@end + +@implementation rn_mapviewTests + +- (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test +{ + if (test(view)) { + return YES; + } + for (UIView *subview in [view subviews]) { + if ([self findSubviewInView:subview matching:test]) { + return YES; + } + } + return NO; +} + +- (void)testRendersWelcomeScreen +{ + UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; + NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; + BOOL foundElement = NO; + + __block NSString *redboxError = nil; + RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { + if (level >= RCTLogLevelError) { + redboxError = message; + } + }); + + while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { + [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; + + foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { + if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { + return YES; + } + return NO; + }]; + } + + RCTSetLogFunction(RCTDefaultLogFunction); + + XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); + XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); +} + + +@end diff --git a/package.json b/package.json new file mode 100644 index 0000000000..8f94582159 --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "rn_mapview", + "version": "0.0.1", + "private": true, + "scripts": { + "start": "react-native start" + }, + "dependencies": { + "react-native": "^0.16.0" + } +}