diff --git a/CapacitorCommunityCapacitorGooglemapsNative.podspec b/CapacitorCommunityCapacitorGooglemapsNative.podspec index bc16cd78..e55853ae 100644 --- a/CapacitorCommunityCapacitorGooglemapsNative.podspec +++ b/CapacitorCommunityCapacitorGooglemapsNative.podspec @@ -14,5 +14,6 @@ Pod::Spec.new do |s| s.ios.deployment_target = '12.0' s.dependency 'Capacitor' s.dependency 'GoogleMaps' + s.dependency 'Google-Maps-iOS-Utils' s.static_framework = true end diff --git a/README.md b/README.md index 6ce2bf0c..47504973 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ - +

@@ -30,20 +30,6 @@ Maps SDK for Android & iOS bring better performance and offline caching compared | -----------| -------| -------| | Hemang Kumar | [hemangsk](https://github.com/hemangsk) | hemangsk@gmail.com | -## Support Development -If you like this plugin and use it on your projects, please consider donating to support the development. Thank you! - - - - - - -
- - - -
- ## Project Status | Features | Android |     iOS     | Current Status | Pending | @@ -80,7 +66,8 @@ You'll have two API keys by the end of this step. Lets proceed: ### Add API key to your App -- [Android](https://developers.google.com/maps/documentation/android-sdk/get-api-key) in AndroidManifest.xml: +#### Android +[Android](https://developers.google.com/maps/documentation/android-sdk/get-api-key) in AndroidManifest.xml: ``` ... @@ -91,6 +78,23 @@ You'll have two API keys by the end of this step. Lets proceed: ... ``` +As of [Capacitor 3](https://capacitorjs.com/docs/updating/3-0), the plugin needs to be registered in MainActivity.java: +```java +import com.hemangkumar.capacitorgooglemaps.CapacitorGoogleMaps; +//... + +public class MainActivity extends BridgeActivity { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // ... + registerPlugin(CapacitorGoogleMaps.class); + } +} + +``` +#### iOS - On iOS, this step is little different and mentioned below. ### Importing & Initializing the plugin @@ -112,13 +116,13 @@ await CapacitorGoogleMaps.initialize({ `component.html` -``` +```html
``` `component.css` -``` +```css #map { margin: 2em 1em; height: 250px; @@ -196,6 +200,12 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
selected-pixel-jameson

πŸ›
chikalio

πŸ›
Javier Gonzalez

πŸ’» +
Shane B.

πŸ“– + + +
Manuel RodrΓ­guez

πŸ’» +
Jamilu Salisu

πŸ’» +
rastafan

πŸ’» diff --git a/android/build.gradle b/android/build.gradle index d8457f83..82e3e850 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,8 +1,4 @@ -ext { - junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' - androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' - androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' -} + buildscript { repositories { @@ -11,11 +7,18 @@ buildscript { } dependencies { classpath 'com.android.tools.build:gradle:3.6.1' + classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:2.0.0" } } apply plugin: 'com.android.library' +ext { + junitVersion = project.hasProperty('junitVersion') ? rootProject.ext.junitVersion : '4.12' + androidxJunitVersion = project.hasProperty('androidxJunitVersion') ? rootProject.ext.androidxJunitVersion : '1.1.1' + androidxEspressoCoreVersion = project.hasProperty('androidxEspressoCoreVersion') ? rootProject.ext.androidxEspressoCoreVersion : '3.2.0' +} + android { compileSdkVersion project.hasProperty('compileSdkVersion') ? rootProject.ext.compileSdkVersion : 29 defaultConfig { @@ -24,16 +27,27 @@ android { versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + manifestPlaceholders = [ MAPS_API_KEY:"debugKey" ] } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + + manifestPlaceholders = [ MAPS_API_KEY:"prodKey" ] } } lintOptions { abortOnError false } + sourceSets { + main { + assets { + srcDirs 'src/main/assets', 'src/main/marker-categories-assets', 'src/main/assets/marker-categories' + } + } + } } repositories { @@ -49,5 +63,11 @@ dependencies { testImplementation "junit:junit:$junitVersion" androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion" androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion" - implementation 'com.google.android.libraries.maps:maps:3.1.0-beta' + + implementation 'com.google.maps.android:android-maps-utils:2.3.0' + implementation 'com.google.android.gms:play-services-maps:18.0.0' + implementation 'com.google.android.gms:play-services-location:16.0.0' + implementation("androidx.appcompat:appcompat:1.3.0") + } + diff --git a/android/gradle.properties b/android/gradle.properties index 2dbcb071..123de31e 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -18,4 +18,4 @@ org.gradle.jvmargs=-Xmx1536m # Supports AndroidX android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index a160a15d..928381e5 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,5 +1,22 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/src/main/assets/marker-categories/category1.png b/android/src/main/assets/marker-categories/category1.png new file mode 100644 index 00000000..7a1f0d8c Binary files /dev/null and b/android/src/main/assets/marker-categories/category1.png differ diff --git a/android/src/main/assets/marker-categories/category2.png b/android/src/main/assets/marker-categories/category2.png new file mode 100644 index 00000000..6766828e Binary files /dev/null and b/android/src/main/assets/marker-categories/category2.png differ diff --git a/android/src/main/assets/marker-categories/category3.png b/android/src/main/assets/marker-categories/category3.png new file mode 100644 index 00000000..6277afb2 Binary files /dev/null and b/android/src/main/assets/marker-categories/category3.png differ diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java index d4b38c69..73097c43 100644 --- a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CapacitorGoogleMaps.java @@ -4,793 +4,929 @@ import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; -import android.content.pm.PackageManager; -import android.location.Address; -import android.location.Geocoder; -import android.location.Location; -import android.util.Log; +import android.content.IntentSender; +import android.content.res.AssetManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.graphics.Rect; +import android.util.Base64; +import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; -import android.widget.FrameLayout; import androidx.annotation.NonNull; -import androidx.core.app.ActivityCompat; -import androidx.core.content.ContextCompat; -import com.getcapacitor.JSArray; import com.getcapacitor.JSObject; -import com.getcapacitor.NativePlugin; import com.getcapacitor.Plugin; import com.getcapacitor.PluginCall; import com.getcapacitor.PluginMethod; import com.getcapacitor.annotation.CapacitorPlugin; import com.getcapacitor.annotation.Permission; -import com.google.android.libraries.maps.CameraUpdateFactory; -import com.google.android.libraries.maps.GoogleMap; -import com.google.android.libraries.maps.GoogleMapOptions; -import com.google.android.libraries.maps.MapView; -import com.google.android.libraries.maps.OnMapReadyCallback; -import com.google.android.libraries.maps.UiSettings; -import com.google.android.libraries.maps.model.CameraPosition; -import com.google.android.libraries.maps.model.CircleOptions; -import com.google.android.libraries.maps.model.LatLng; -import com.google.android.libraries.maps.model.MapStyleOptions; -import com.google.android.libraries.maps.model.Marker; -import com.google.android.libraries.maps.model.MarkerOptions; -import com.google.android.libraries.maps.model.PointOfInterest; -import com.google.android.libraries.maps.model.PolygonOptions; -import com.google.android.libraries.maps.model.PolylineOptions; +import com.google.android.gms.common.api.ApiException; +import com.google.android.gms.common.api.ResolvableApiException; +import com.google.android.gms.location.LocationRequest; +import com.google.android.gms.location.LocationServices; +import com.google.android.gms.location.LocationSettingsRequest; +import com.google.android.gms.location.LocationSettingsResponse; +import com.google.android.gms.location.LocationSettingsStatusCodes; +import com.google.android.gms.maps.model.CameraPosition; + +import com.google.android.gms.tasks.OnCompleteListener; +import com.google.android.gms.tasks.Task; +import com.hemangkumar.capacitorgooglemaps.marker.CustomMarker; +import com.hemangkumar.capacitorgooglemaps.marker.MarkerCategory; +import com.hemangkumar.capacitorgooglemaps.mapsutility.Events; +import com.hemangkumar.capacitorgooglemaps.utils.BoundingRect; + +import org.json.JSONArray; import org.json.JSONException; -import org.json.JSONObject; +import java.io.File; import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.UUID; +import java.util.regex.Matcher; +import java.util.regex.Pattern; @CapacitorPlugin( name = "CapacitorGoogleMaps", permissions = { @Permission( - strings = { Manifest.permission.ACCESS_FINE_LOCATION }, + strings = {Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION }, alias = "geolocation" ), @Permission(strings = { Manifest.permission.INTERNET }, alias = "internet"), } ) -public class CapacitorGoogleMaps extends Plugin implements OnMapReadyCallback, GoogleMap.OnMyLocationClickListener, GoogleMap.OnMyLocationButtonClickListener { +public class CapacitorGoogleMaps extends Plugin implements CustomMapViewEvents { - private MapView mapView; - GoogleMap googleMap; - Integer mapViewParentId; - Integer DEFAULT_WIDTH = 500; - Integer DEFAULT_HEIGHT = 500; - Float DEFAULT_ZOOM = 12.0f; - private HashMap mHashMap = new HashMap<>(); + // -- constants -- + private String MARKER_CATEGORY_DIRECTORY = "marker-categories"; + + // -- private fields -- + private String GOOGLE_MAPS_KEY; + + + public static final HashMap customMapViews = new HashMap<>(); + Float devicePixelRatio; + + private String lastEventChainId; + public List previousEvents = new ArrayList<>(); + private String delegateTouchEventsToMapId; + + + /** + * This method should be called after we requested the WebView through notifyListeners("didRequestElementFromPoint"). + * It should tell us if the exact point that was being touched, is from an element in which a MapView exists. + * Otherwise it is a 'normal' HTML element, and we should thus not delegate touch events. + */ + @PluginMethod() + public void elementFromPointResult(PluginCall call) { + String eventChainId = call.getString("eventChainId"); + if (eventChainId != null && eventChainId.equals(lastEventChainId)) { + Boolean isSameNode = call.getBoolean("isSameNode", false); + if (isSameNode != null && isSameNode) { + // The WebView apparently has decide the touched point belongs to a certain MapView. + // Now we should find out which one exactly. + String mapId = call.getString("mapId"); + if (mapId != null) { + delegateTouchEventsToMapId = mapId; + } + } + } + call.resolve(); + } + + + @SuppressLint("ClickableViewAccessibility") @Override - public void onMapReady(GoogleMap googleMap) { - this.googleMap = googleMap; - notifyListeners("onMapReady", null); + public void load() { + super.load(); + + initMarkerCategories(); + + this.getBridge().getWebView().setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + int touchType = event.getActionMasked(); + + if (touchType == MotionEvent.ACTION_DOWN) { + // Throw away all previous state when starting a new touch gesture. + // The framework may have dropped the up or cancel event for the previous gesture + // due to an app switch, ANR, or some other state change. + delegateTouchEventsToMapId = null; + previousEvents.clear(); + + // Initialize JSObjects for resolve(). + JSObject result = new JSObject(); + JSObject point = new JSObject(); + + // Generate a UUID, and assign it to lastId. + // This way we can make sure we are always referencing the last chain of events. + lastEventChainId = UUID.randomUUID().toString(); + // Then add it to result object, so the WebView can reference the correct events when needed. + result.put("eventChainId", lastEventChainId); + + // Get the touched position. + int x = (int) event.getX(); + int y = (int) event.getY(); + + // Since pixels on a webpage are calculated differently, should convert them first. + // Convert it to 'real' pixels in WebView by using devicePixelRatio. + if (devicePixelRatio != null && devicePixelRatio > 0) { + point.put("x", x / devicePixelRatio); + point.put("y", y / devicePixelRatio); + + // Then add it to result object. + result.put("point", point); + } + + // Then notify the listener that we request to let the WebView determine + // if the element touched is the same node as where some MapView is attached to. + notifyListeners("didRequestElementFromPoint", result); + } + + if (delegateTouchEventsToMapId != null) { + CustomMapView customMapView = customMapViews.get(delegateTouchEventsToMapId); + if (customMapView != null && !customMapView.isHidden()) { + // Apparently, all touch events should be delegated to a specific MapView. + + // If previous events exist, we should execute those first + if (!previousEvents.isEmpty()) { + for (int i = 0; i < previousEvents.size(); i++) { + MotionEvent e = previousEvents.get(i); + if (e != null) { + // Delegate this previous event to the MapView. + dispatchTouchEvent(e, customMapView); + } + } + // Since we delegated all previous events, we can now forget about them. + previousEvents.clear(); + } + + // Finally delegate the current event to the MapView. + dispatchTouchEvent(event, customMapView); + } + } else { + // If delegateTouchEventsToMapId is not set, but it could still be set later! + // So we should save all past events. + // That way we can still execute them later on. + // It is important that we use MotionEvent.obtain() to copy the event first. + // Otherwise the event does not work properly when delegating it later on. + MotionEvent clonedEvent = MotionEvent.obtain(event); + previousEvents.add(clonedEvent); + } + + return false; + } + }); + } + + /** + * dispatching event on selected MapView + */ + private void dispatchTouchEvent(MotionEvent event, CustomMapView customMapView) { + Rect offsetViewBounds = new Rect(); + // returns the visible bounds + customMapView.mapView.getDrawingRect(offsetViewBounds); + // calculates the relative coordinates to the parent + ViewGroup parentViewGroup = (ViewGroup) bridge.getWebView().getParent(); + parentViewGroup.offsetDescendantRectToMyCoords(customMapView.mapView, offsetViewBounds); + + int relativeTop = offsetViewBounds.top; + int relativeLeft = offsetViewBounds.left; + + // Set location with offset points, + // because if a map is positioned with a different top and left point than the WebView, + // that should be accounted for. + event.setLocation(event.getX() - relativeLeft, event.getY() - relativeTop); + + customMapView.mapView.dispatchTouchEvent(event); } @Override protected void handleOnStart() { super.handleOnStart(); - if (mapView != null) { - mapView.onStart(); + for (CustomMapView customMapView : customMapViews.values()) { + if (customMapView != null) { + customMapView.handleOnStart(); + } } } @Override protected void handleOnResume() { super.handleOnResume(); - if (mapView != null) { - mapView.onResume(); + for (CustomMapView customMapView : customMapViews.values()) { + if (customMapView != null) { + customMapView.handleOnResume(); + } } } @Override - protected void handleOnStop() { - super.handleOnStop(); - if (mapView != null) { - mapView.onStop(); + protected void handleOnPause() { + for (CustomMapView customMapView : customMapViews.values()) { + if (customMapView != null) { + customMapView.handleOnPause(); + } } + super.handleOnPause(); } @Override - protected void handleOnPause() { - if (mapView != null) { - mapView.onPause(); + protected void handleOnStop() { + super.handleOnStop(); + for (CustomMapView customMapView : customMapViews.values()) { + if (customMapView != null) { + customMapView.handleOnStop(); + } } - super.handleOnPause(); } @Override protected void handleOnDestroy() { - if (mapView != null) { - mapView.onDestroy(); + for (CustomMapView customMapView : customMapViews.values()) { + if (customMapView != null) { + customMapView.handleOnDestroy(); + } } super.handleOnDestroy(); } + @PluginMethod() public void initialize(PluginCall call) { /* * TODO: Check API key */ + devicePixelRatio = call.getFloat("devicePixelRatio"); call.resolve(); } @PluginMethod() - public void create(PluginCall call) { - final Integer width = call.getInt("width", DEFAULT_WIDTH); - final Integer height = call.getInt("height", DEFAULT_HEIGHT); - final Integer x = call.getInt("x", 0); - final Integer y = call.getInt("y", 0); + public void createMap(final PluginCall call) { + final CapacitorGoogleMaps ctx = this; - final Float zoom = call.getFloat("zoom", DEFAULT_ZOOM); - final Double latitude = call.getDouble("latitude"); - final Double longitude = call.getDouble("longitude"); + bridge.saveCall(call); + final String callbackId = call.getCallbackId(); - final boolean liteMode = call.getBoolean("enabled", false); + final BoundingRect boundingRect = new BoundingRect(); + boundingRect.updateFromJSObject(call.getObject("boundingRect")); - final CapacitorGoogleMaps ctx = this; - getBridge().getActivity().runOnUiThread(new Runnable() { - @Override - public void run() { - LatLng latLng = new LatLng(latitude, longitude); - CameraPosition cameraPosition = new CameraPosition.Builder() - .target(latLng) - .zoom(zoom) - .build(); + final MapCameraPosition mapCameraPosition = new MapCameraPosition(); + mapCameraPosition.updateFromJSObject(call.getObject("cameraPosition"), null); - GoogleMapOptions googleMapOptions = new GoogleMapOptions(); - googleMapOptions.camera(cameraPosition); - googleMapOptions.liteMode(liteMode); + final MapPreferences mapPreferences = new MapPreferences(); + mapPreferences.updateFromJSObject(call.getObject("preferences")); - FrameLayout mapViewParent = new FrameLayout(getBridge().getContext()); - mapViewParentId = View.generateViewId(); - mapViewParent.setId(mapViewParentId); - mapView = new MapView(getBridge().getContext(), googleMapOptions); + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + CustomMapView customMapView = new CustomMapView(getBridge().getContext(), getBridge().getActivity(), ctx); - FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getScaledPixels(width), getScaledPixels(height)); - lp.topMargin = getScaledPixels(y); - lp.leftMargin = getScaledPixels(x); + customMapViews.put(customMapView.getId(), customMapView); - mapView.setLayoutParams(lp); + customMapView.createMap(callbackId, boundingRect, mapCameraPosition, mapPreferences); - mapViewParent.addView(mapView); + customMapView.addToView(((ViewGroup) bridge.getWebView().getParent())); - ((ViewGroup) getBridge().getWebView().getParent()).addView(mapViewParent); + // Bring the WebView in front of the MapView + // This allows us to overlay the MapView in HTML/CSS + bridge.getWebView().bringToFront(); - mapView.onCreate(null); - mapView.onStart(); - mapView.getMapAsync(ctx); + // Hide the background + bridge.getWebView().setBackgroundColor(Color.TRANSPARENT); + bridge.getWebView().loadUrl("javascript:document.documentElement.style.backgroundColor = 'transparent';void(0);"); } }); - call.resolve(); } @PluginMethod() - public void addMarker(final PluginCall call) { - final Double latitude = call.getDouble("latitude", 0d); - final Double longitude = call.getDouble("longitude", 0d); - final Float opacity = call.getFloat("opacity", 1.0f); - final String title = call.getString("title", ""); - final String snippet = call.getString("snippet", ""); - final Boolean isFlat = call.getBoolean("isFlat", true); - final JSObject metadata = call.getObject("metadata"); + public void updateMap(final PluginCall call) { + final String mapId = call.getString("mapId"); getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - LatLng latLng = new LatLng(latitude, longitude); - MarkerOptions markerOptions = new MarkerOptions(); - markerOptions.position(latLng); - markerOptions.alpha(opacity); - markerOptions.title(title); - markerOptions.snippet(snippet); - markerOptions.flat(isFlat); + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + final MapPreferences mapPreferences = customMapView.mapPreferences; + mapPreferences.updateFromJSObject(call.getObject("preferences")); - Marker marker = googleMap.addMarker(markerOptions); + JSObject result = customMapView.invalidateMap(); - // set metadata to marker - marker.setTag(metadata); + call.resolve(result); + } else { + call.reject("map not found"); + } + } + }); + } - // get auto-generated id of the just added marker, - // put this marker into a hashmap with the corresponding id, - // so we can retrieve the marker by id later on - mHashMap.put(marker.getId(), marker); + @PluginMethod() + public void getMap(final PluginCall call) { + final String mapId = call.getString("mapId"); - // initialize JSObject to return when resolving this call - JSObject result = new JSObject(); - JSObject markerResult = new JSObject(); + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + CustomMapView customMapView = customMapViews.get(mapId); - // get marker specific values - markerResult.put("id", marker.getId()); - result.put("marker", markerResult); + if (customMapView != null) { + JSObject result = customMapView.getMap(); - call.resolve(result); + call.resolve(result); + } else { + call.reject("map not found"); + } } }); } - @PluginMethod() - public void removeMarker(final PluginCall call) { + @PluginMethod(returnType = PluginMethod.RETURN_NONE) + public void removeMap(final PluginCall call) { + final String mapId = call.getString("mapId"); + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - final String id = call.getString("id", null); + CustomMapView customMapView = customMapViews.get(mapId); - if (id == null) { - // todo - return; + if (customMapView != null) { + customMapView.removeFromView(((ViewGroup) bridge.getWebView().getParent())); + customMapViews.remove(mapId); + call.resolve(); + } else { + call.reject("map not found"); } + } + }); + } + + @PluginMethod(returnType = PluginMethod.RETURN_NONE) + public void clearMap(final PluginCall call) { + final String mapId = call.getString("mapId"); - Marker marker = mHashMap.get(id); + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + CustomMapView customMapView = customMapViews.get(mapId); - if (marker != null) { - marker.remove(); - mHashMap.remove(id); + if (customMapView != null) { + customMapView.clear(); + call.resolve(); + } else { + call.reject("map not found"); } } }); } @PluginMethod() - public void addPolyline(final PluginCall call) { - final JSArray points = call.getArray("points", new JSArray()); + public void close(PluginCall call) { + final String mapId = call.getString("mapId"); getBridge().executeOnMainThread(new Runnable() { @Override public void run() { - PolylineOptions polylineOptions = new PolylineOptions(); - - for (int i = 0; i < points.length(); i++) { - try { - JSONObject point = points.getJSONObject(i); - LatLng latLng = new LatLng(point.getDouble("latitude"), point.getDouble("longitude")); - polylineOptions.add(latLng); - } catch (JSONException e) { - e.printStackTrace(); + CustomMapView customMapView = customMapViews.get(mapId); + if (customMapView != null){ + View viewToRemove = customMapViews.get(mapId).mapView; + if (viewToRemove != null){ + ((ViewGroup) getBridge().getWebView().getParent()).removeViewInLayout(viewToRemove); + call.resolve(); } } + else { + call.reject("map not found"); + } - googleMap.addPolyline(polylineOptions); - - call.resolve(); } }); } @PluginMethod() - public void addPolygon(final PluginCall call) { - final JSArray points = call.getArray("points", new JSArray()); + public void hide(PluginCall call) { + final String mapId = call.getString("mapId"); getBridge().executeOnMainThread(new Runnable() { @Override public void run() { - PolygonOptions polygonOptions = new PolygonOptions(); - - for (int i = 0; i < points.length(); i++) { - try { - JSONObject point = points.getJSONObject(i); - LatLng latLng = new LatLng(point.getDouble("latitude"), point.getDouble("longitude")); - polygonOptions.add(latLng); - } catch (JSONException e) { - e.printStackTrace(); + CustomMapView customMapView = customMapViews.get(mapId); + if (customMapView != null){ + View viewToHide = customMapViews.get(mapId).mapView; + if (viewToHide != null){ + viewToHide.setVisibility(View.INVISIBLE); + customMapView.setHidden(true); + call.resolve(); } } - - googleMap.addPolygon(polygonOptions); - call.resolve(); + else { + call.reject("map not found"); + } } }); } @PluginMethod() - public void addCircle(final PluginCall call) { - final int radius = call.getInt("radius", 0); - final JSONObject center = call.getObject("center", new JSObject()); + public void show(PluginCall call) { + final String mapId = call.getString("mapId"); getBridge().executeOnMainThread(new Runnable() { + CustomMapView customMapView = customMapViews.get(mapId); @Override public void run() { - CircleOptions circleOptions = new CircleOptions(); - circleOptions.radius(radius); - try { - circleOptions.center(new LatLng(center.getDouble("latitude"), center.getDouble("longitude"))); - } catch (JSONException e) { - e.printStackTrace(); + CustomMapView customMapView = customMapViews.get(mapId); + if (customMapView != null){ + View viewToShow = customMapViews.get(mapId).mapView; + if (viewToShow != null){ + viewToShow.setVisibility(View.VISIBLE); + customMapView.setHidden(false); + call.resolve(); + } + } + else { + call.reject("map not found"); } - - googleMap.addCircle(circleOptions); - - call.resolve(); } }); } - @PluginMethod() - public void setMapType(PluginCall call) { - - String specifiedMapType = call.getString("type", "normal"); - Integer mapType; - - switch (specifiedMapType) { - case "hybrid": - mapType = GoogleMap.MAP_TYPE_HYBRID; - break; - case "satellite": - mapType = GoogleMap.MAP_TYPE_SATELLITE; - break; - case "terrain": - mapType = GoogleMap.MAP_TYPE_TERRAIN; - break; - case "none": - mapType = GoogleMap.MAP_TYPE_NONE; - break; - default: - mapType = GoogleMap.MAP_TYPE_NORMAL; - } - final Integer selectedMapType = mapType; + @PluginMethod + public void moveCamera(final PluginCall call) { + final String mapId = call.getString("mapId"); + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - googleMap.setMapType(selectedMapType); - } + CustomMapView customMapView = customMapViews.get(mapId); - }); + if (customMapView != null) { + final MapCameraPosition mapCameraPosition = customMapView.mapCameraPosition; - call.resolve(); - } + CameraPosition previousCameraPosition = null; + Boolean usePreviousCameraPositionAsBase = call.getBoolean("usePreviousCameraPositionAsBase", false); + if (usePreviousCameraPositionAsBase == null || !usePreviousCameraPositionAsBase) { + // if we should not use the previous one, + // use the current one + previousCameraPosition = customMapView.getCameraPosition(); + } - @PluginMethod() - public void setIndoorEnabled(PluginCall call) { - final Boolean indoorEnabled = call.getBoolean("enabled", false); - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - googleMap.setIndoorEnabled(indoorEnabled); - } - }); + mapCameraPosition.updateFromJSObject(call.getObject("cameraPosition"), previousCameraPosition); - call.resolve(); - } + Integer duration = call.getInt("duration", 0); - @PluginMethod() - public void setTrafficEnabled(PluginCall call) { - final Boolean trafficEnabled = call.getBoolean("enabled", false); - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - googleMap.setTrafficEnabled(trafficEnabled); + // @TODO: add move listeners for movement + JSObject result = customMapView.moveCamera(duration); + + call.resolve(result); + } else { + call.reject("map not found"); + } } }); + } - call.resolve(); + @Override + public void onMapReady(String callbackId, JSObject result) { + PluginCall call = bridge.getSavedCall(callbackId); + call.resolve(result); + bridge.releaseCall(call); } - @PluginMethod() - public void padding(PluginCall call) { - final Integer top = call.getInt("top", 0); - final Integer left = call.getInt("left", 0); - final Integer bottom = call.getInt("bottom", 0); - final Integer right = call.getInt("right", 0); + // -- events of tapping + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapInfoWindow(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_INFO_WINDOW); + } - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - googleMap.setPadding(left, top, right, bottom); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didCloseInfoWindow(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_CLOSE_INFO_WINDOW); + } - call.resolve(); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapMap(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_MAP); } - @PluginMethod() - public void clear(PluginCall call) { - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - googleMap.clear(); - mHashMap.clear(); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didLongPressMap(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_LONG_PRESS_MAP); + } - call.resolve(); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapSingleMarker(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_SINGLE_MARKER); } - @PluginMethod() - public void close(PluginCall call) { - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - View viewToRemove = ((ViewGroup) getBridge().getWebView().getParent()).findViewById(mapViewParentId); - ((ViewGroup) getBridge().getWebView().getParent()).removeViewInLayout(viewToRemove); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapCluster(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_CLUSTER); + } + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapClusterInfoWindow(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_CLUSTER_INFO_WINDOW); } - @PluginMethod() - public void hide(PluginCall call) { - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - View viewToHide = ((ViewGroup) getBridge().getWebView().getParent()).findViewById(mapViewParentId); - viewToHide.setVisibility(View.INVISIBLE); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapMarker(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_MARKER); } - @PluginMethod() - public void show(PluginCall call) { - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - View viewToShow = ((ViewGroup) getBridge().getWebView().getParent()).findViewById(mapViewParentId); - viewToShow.setVisibility(View.VISIBLE); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapClusterItemInfoWindow(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_CLUSTER_ITEM_INFO_WINDOW); } - @PluginMethod() - public void viewBounds(final PluginCall call) { - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - JSObject result = new JSObject(); - JSObject bounds = new JSObject(); - JSObject farLeft = new JSObject(); - JSObject farRight = new JSObject(); - JSObject nearLeft = new JSObject(); - JSObject nearRight = new JSObject(); - - farLeft.put("latitude",googleMap.getProjection().getVisibleRegion().farLeft.latitude); - farLeft.put("longitude",googleMap.getProjection().getVisibleRegion().farLeft.longitude); - farRight.put("latitude",googleMap.getProjection().getVisibleRegion().farRight.latitude); - farRight.put("longitude",googleMap.getProjection().getVisibleRegion().farRight.longitude); - nearLeft.put("latitude",googleMap.getProjection().getVisibleRegion().nearLeft.latitude); - nearLeft.put("longitude",googleMap.getProjection().getVisibleRegion().nearLeft.longitude); - nearRight.put("latitude",googleMap.getProjection().getVisibleRegion().nearRight.latitude); - nearRight.put("longitude",googleMap.getProjection().getVisibleRegion().nearRight.longitude); - - bounds.put("farLeft",farLeft); - bounds.put("farRight",farRight); - bounds.put("nearLeft",nearLeft); - bounds.put("nearRight",nearRight); - result.put("bounds",bounds); - - call.resolve(result); - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didBeginDraggingMarker(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_BEGIN_DRAGGING_MARKER); } - @PluginMethod() - public void reverseGeocodeCoordinate(final PluginCall call) { - final Double latitude = call.getDouble("latitude", 0.0); - final Double longitude = call.getDouble("longitude", 0.0); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didDragMarker(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_DRAG_MARKER); + } - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - /* - * TODO: Check if can be done without adding Places SDK - * - */ + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didEndDraggingMarker(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_END_DRAGGING_MARKER); + } - Geocoder geocoder = new Geocoder(getContext()); - try { - List
addressList = geocoder.getFromLocation(latitude, longitude, 5); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapMyLocationButton(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_MY_LOCATION_BUTTON); + } - JSObject results = new JSObject(); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapMyLocationDot(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_MY_LOCATION_DOT); + } - int index = 0; - for (Address address : addressList) { - JSObject addressObject = new JSObject(); - addressObject.put("administrativeArea", address.getAdminArea()); - addressObject.put("lines", address.getAddressLine(0)); - addressObject.put("country", address.getCountryName()); - addressObject.put("locality", address.getLocality()); - addressObject.put("subLocality", address.getSubLocality()); - addressObject.put("thoroughFare", address.getThoroughfare()); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didTapPoi(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_TAP_POI); + } - results.put(String.valueOf(index++), addressObject); - } - call.resolve(results); - } catch (IOException e) { - call.error("Error in Geocode!"); - } - } - }); + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didBeginMovingCamera(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_BEGIN_MOVING_CAMERA); + } + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didMoveCamera(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_MOVE_CAMERA); + } + @PluginMethod(returnType = PluginMethod.RETURN_CALLBACK) + public void didEndMovingCamera(final PluginCall call) { + setCallbackIdForEvent(call, Events.EVENT_DID_END_MOVING_CAMERA); } - @PluginMethod() - public void settings(final PluginCall call) { - final boolean allowScrollGesturesDuringRotateOrZoom = call.getBoolean("allowScrollGesturesDuringRotateOrZoom", true); + public void setCallbackIdForEvent(final PluginCall call, final String eventName) { + call.setKeepAlive(true); + final String callbackId = call.getCallbackId(); + String mapId = call.getString("mapId"); - final boolean compassButton = call.getBoolean("compassButton", false); - final boolean zoomButton = call.getBoolean("zoomButton", true); - final boolean myLocationButton = call.getBoolean("myLocationButton", false); + final CustomMapView customMapView = customMapViews.get(mapId); - boolean consumesGesturesInView = call.getBoolean("consumesGesturesInView", true); - final boolean indoorPicker = call.getBoolean("indoorPicker", false); + if (customMapView != null) { + final Boolean preventDefault = call.getBoolean("preventDefault", false); - final boolean rotateGestures = call.getBoolean("rotateGestures", true); - final boolean scrollGestures = call.getBoolean("scrollGestures", true); - final boolean tiltGestures = call.getBoolean("tiltGestures", true); - final boolean zoomGestures = call.getBoolean("zoomGestures", true); - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - UiSettings googleMapUISettings = googleMap.getUiSettings(); - googleMapUISettings.setScrollGesturesEnabledDuringRotateOrZoom(allowScrollGesturesDuringRotateOrZoom); - googleMapUISettings.setCompassEnabled(compassButton); - googleMapUISettings.setIndoorLevelPickerEnabled(indoorPicker); - googleMapUISettings.setMyLocationButtonEnabled(myLocationButton); - googleMapUISettings.setRotateGesturesEnabled(rotateGestures); - googleMapUISettings.setScrollGesturesEnabled(scrollGestures); - googleMapUISettings.setTiltGesturesEnabled(tiltGestures); - googleMapUISettings.setZoomGesturesEnabled(zoomGestures); - googleMapUISettings.setZoomControlsEnabled(zoomButton); - googleMapUISettings.setMyLocationButtonEnabled(true); - } - }); + getBridge().getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + customMapView.setCallbackIdForEvent(callbackId, eventName, preventDefault); + } + }); + } + } - call.resolve(); + @Override + public void resultForCallbackId(String callbackId, JSObject result) { + PluginCall call = bridge.getSavedCall(callbackId); + call.resolve(result); } - @PluginMethod() - public void setCamera(PluginCall call) { - final float viewingAngle = call.getFloat("viewingAngle", googleMap.getCameraPosition().tilt); - final float bearing = call.getFloat("bearing", googleMap.getCameraPosition().bearing); - final Float zoom = call.getFloat("zoom", googleMap.getCameraPosition().zoom); - final Double latitude = call.getDouble("latitude", googleMap.getCameraPosition().target.latitude); - final Double longitude = call.getDouble("longitude", googleMap.getCameraPosition().target.longitude); - final Boolean animate = call.getBoolean("animate", false); - Double animationDuration = call.getDouble("animationDuration", 1000.0); + @PluginMethod() + public void addMarker(final PluginCall call) { + final String mapId = call.getString("mapId"); - getBridge().executeOnMainThread(new Runnable() { + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - CameraPosition cameraPosition = new CameraPosition.Builder() - .target(new LatLng(latitude, longitude)) - .zoom(zoom) - .tilt(viewingAngle) - .bearing(bearing) - .build(); - - if (animate) { - /* - * TODO: Implement animationDuration - * */ - googleMap.animateCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + JSObject preferences = JSObjectDefaults.getJSObjectSafe(call, "preferences", new JSObject()); + + CustomMarker customMarker = new CustomMarker(); + customMarker.updateFromJSObject(preferences); + + customMapView.addMarker(customMarker); + + call.resolve(CustomMarker.getResultForMarker(customMarker, mapId)); } else { - googleMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition)); + call.reject("map not found"); } } }); - - call.resolve(); } @PluginMethod() - public void setMapStyle(PluginCall call) { - /* - https://mapstyle.withgoogle.com/ - */ - final String mapStyle = call.getString("jsonString", ""); + public void addMarkers(final PluginCall call) { + final String mapId = call.getString("mapId"); - getBridge().executeOnMainThread(new Runnable() { + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - MapStyleOptions mapStyleOptions = new MapStyleOptions(mapStyle); - googleMap.setMapStyle(mapStyleOptions); + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + JSObject markersObj = call.getObject("markers"); + try { + JSONArray arrayOfMarkers = markersObj.getJSONArray("arrayOfMarkers"); + int length = arrayOfMarkers.length(); + ArrayList customMarkers = new ArrayList<>(length); + for(int i =0; i markerCategoriesNamesAndIcons = fetchMarkersCategoriesFilesFromAssets(getContext()); + List keys = new ArrayList(markerCategoriesNamesAndIcons.keySet()); + Collections.sort(keys); + int i = 1; + for (String nameOfCategory : + keys) { + new MarkerCategory(i, nameOfCategory, markerCategoriesNamesAndIcons.get(nameOfCategory)); + i++; + } - final CapacitorGoogleMaps ctx = this; + } - getBridge().executeOnMainThread(new Runnable() { - @Override - public void run() { - googleMap.setOnPoiClickListener(new GoogleMap.OnPoiClickListener() { - @Override - public void onPoiClick(PointOfInterest pointOfInterest) { - ctx.onPoiClick(pointOfInterest); - } - }); + private HashMap fetchMarkersCategoriesFilesFromAssets(Context context) { + return getImagesFromAsset(MARKER_CATEGORY_DIRECTORY, context); + } + + // method for reading pictures and their names from folder + private HashMap getImagesFromAsset(String path, Context context) { + HashMap stringBitmapHashMap = new HashMap<>(); + + AssetManager assetManager = context.getAssets(); + + // regex for getting full names of picture + // (cool.tank.png) -> cool.tank and .png + // (cool.tank) -> cool.tank and {null} + String textGroups = "(.+?)(\\.[^.]*$|$)"; + Pattern textPattern = Pattern.compile(textGroups); + + try { + // get array of names of files + String[] namesOfFiles = context.getAssets().list(path); + for (String nameOfFile : + namesOfFiles) { + + Matcher textMatcher = textPattern.matcher(nameOfFile); + while(textMatcher.find()) { + File pathToFile = new File(path, nameOfFile); + InputStream istr = assetManager.open(pathToFile.getPath()); + Bitmap bitmap = BitmapFactory.decodeStream(istr); + istr.close(); + stringBitmapHashMap.put(textMatcher.group(1), bitmap); + } } - }); + } catch (IOException e) { + e.printStackTrace(); + } - call.resolve(); + return stringBitmapHashMap; } - @PluginMethod() - public void setOnMapClickListener(PluginCall call) { - final CapacitorGoogleMaps ctx = this; + @PluginMethod + public void zoomInButtonClick(PluginCall call) { + final String mapId = call.getString("mapId"); - getBridge().executeOnMainThread(new Runnable() { + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - googleMap.setOnMapClickListener(new GoogleMap.OnMapClickListener() { - @Override - public void onMapClick(LatLng latLng) { - ctx.onMapClick(latLng); - } - }); + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + customMapView.zoomIn(); + call.resolve(); + } else { + call.reject("map not found"); + } } }); - - call.resolve(); } - @PluginMethod() - public void enableCurrentLocation(final PluginCall call) { + @PluginMethod + public void zoomOutButtonClick(PluginCall call) { + final String mapId = call.getString("mapId"); - final boolean enableLocation = call.getBoolean("enabled", false); - final CapacitorGoogleMaps context = this; - getBridge().executeOnMainThread(new Runnable() { - @SuppressLint("MissingPermission") + getBridge().getActivity().runOnUiThread(new Runnable() { @Override public void run() { - if (ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(getContext(), Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - call.error("Permission for location not granted"); - } else { - googleMap.setMyLocationEnabled(enableLocation); - googleMap.setOnMyLocationClickListener(context); - googleMap.setOnMyLocationButtonClickListener(context); + CustomMapView customMapView = customMapViews.get(mapId); + + if (customMapView != null) { + customMapView.zoomOut(); call.resolve(); + } else { + call.reject("map not found"); } } }); } - public void onPoiClick(PointOfInterest pointOfInterest) { - JSObject result = new JSObject(); - JSObject location = new JSObject(); - JSObject coordinates = new JSObject(); - - coordinates.put("latitude", pointOfInterest.latLng.latitude); - coordinates.put("longitude", pointOfInterest.latLng.longitude); + @PluginMethod + public void myLocationButtonClick(PluginCall call) { + final String mapId = call.getString("mapId"); - location.put("coordinates", coordinates); - - result.put("name", pointOfInterest.name); - result.put("placeID", pointOfInterest.placeId); - result.put("result", location); + getBridge().getActivity().runOnUiThread(new Runnable() { + @SuppressLint("ResourceType") + @Override + public void run() { + CustomMapView customMapView = customMapViews.get(mapId); - notifyListeners("didTapPOIWithPlaceID", result); + if (customMapView != null) { + locationRequest(); + customMapView.locationButton.callOnClick(); + call.resolve(); + } else { + call.reject("map not found"); + } + } + }); } - public void onMapClick(LatLng latLng) { - JSObject result = new JSObject(); - JSObject location = new JSObject(); - JSObject coordinates = new JSObject(); + private void locationRequest() { + LocationRequest locationRequest = LocationRequest.create(); + locationRequest.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); + LocationSettingsRequest.Builder builder = new LocationSettingsRequest.Builder() + .addLocationRequest(locationRequest); - coordinates.put("latitude", latLng.latitude); - coordinates.put("longitude", latLng.longitude); + Task result = + LocationServices.getSettingsClient(getActivity()).checkLocationSettings(builder.build()); - location.put("coordinates", coordinates); - result.put("result", location); - notifyListeners("didTapAt", location); + result.addOnCompleteListener(new OnCompleteListener() { + @Override + public void onComplete(@NonNull Task task) { + try { + LocationSettingsResponse response = task.getResult(ApiException.class); + // All location settings are satisfied. The client can initialize location + // requests here. + } catch (ApiException exception) { + switch (exception.getStatusCode()) { + case LocationSettingsStatusCodes.RESOLUTION_REQUIRED: + // Location settings are not satisfied. But could be fixed by showing the + // user a dialog. + try { + // Cast to a resolvable exception. + ResolvableApiException resolvable = (ResolvableApiException) exception; + // Show the dialog by calling startResolutionForResult(), + // and check the result in onActivityResult(). + resolvable.startResolutionForResult( + getActivity(), + LocationRequest.PRIORITY_HIGH_ACCURACY); + } catch (IntentSender.SendIntentException e) { + // Ignore the error. + } catch (ClassCastException e) { + // Ignore, should be an impossible error. + } + break; + case LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE: + // Location settings are not satisfied. However, we have no way to fix the + // settings so we won't show the dialog. + break; + } + } + } + }); } - public void onMarkerClick(Marker marker) { - JSObject result = new JSObject(); - JSObject location = new JSObject(); - JSObject coordinates = new JSObject(); - JSObject metadata = (JSObject) marker.getTag(); - - coordinates.put("latitude", marker.getPosition().latitude); - coordinates.put("longitude", marker.getPosition().longitude); - - location.put("coordinates", coordinates); - result.put("id", marker.getId()); - result.put("title", marker.getTitle()); - result.put("snippet", marker.getSnippet()); - result.put("result", location); - result.put("metadata", metadata); - notifyListeners("didTap", result); - } - - public boolean onMyLocationButtonClick() { - /* - * TODO: Add handler - */ - return false; - } +} - public void onMyLocationClick(Location location) { - JSObject result = new JSObject(); - result.put("latitude", location.getLatitude()); - result.put("longitude", location.getLongitude()); - notifyListeners("onMyLocationClick", result); - } - public int getScaledPixels(float pixels) { - // Get the screen's density scale - final float scale = getBridge().getActivity().getResources().getDisplayMetrics().density; - // Convert the dps to pixels, based on density scale - return (int) (pixels * scale + 0.5f); - } -} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java new file mode 100644 index 00000000..809fe05a --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapView.java @@ -0,0 +1,804 @@ +package com.hemangkumar.capacitorgooglemaps; + +import android.Manifest; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.pm.PackageManager; +import android.location.Location; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; + +import com.getcapacitor.JSObject; +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.GoogleMapOptions; +import com.google.android.gms.maps.MapView; +import com.google.android.gms.maps.OnMapReadyCallback; +import com.google.android.gms.maps.UiSettings; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.LatLngBounds; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.PointOfInterest; +import com.google.maps.android.clustering.Cluster; +import com.google.maps.android.clustering.ClusterManager; +import com.hemangkumar.capacitorgooglemaps.marker.CustomMarker; +import com.hemangkumar.capacitorgooglemaps.marker.OwnIconRenderer; +import com.hemangkumar.capacitorgooglemaps.mapsutility.Events; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesAppearance; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesControls; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesGestures; +import com.hemangkumar.capacitorgooglemaps.utils.BoundingRect; + +import java.util.Collection; +import java.util.Iterator; +import java.util.UUID; + +public class CustomMapView implements OnMapReadyCallback, + GoogleMap.OnInfoWindowClickListener, + GoogleMap.OnInfoWindowCloseListener, + GoogleMap.OnMapClickListener, + GoogleMap.OnMapLongClickListener, + GoogleMap.OnMarkerClickListener, + GoogleMap.OnMarkerDragListener, + GoogleMap.OnMyLocationClickListener, + GoogleMap.OnMyLocationButtonClickListener, + GoogleMap.OnPoiClickListener, + GoogleMap.OnCameraMoveStartedListener, + GoogleMap.OnCameraMoveListener, + GoogleMap.OnCameraIdleListener, + ClusterManager.OnClusterClickListener, + ClusterManager.OnClusterInfoWindowClickListener, + ClusterManager.OnClusterItemClickListener, + ClusterManager.OnClusterItemInfoWindowClickListener +{ + + // -- constants -- + private final double EPSILON = 0.00001; + + // -- private fields -- + private final Context context; + private final Activity activity; + private final CustomMapViewEvents customMapViewEvents; + private final String id; + private ClusterManager mClusterManager; + + MapView mapView; + private GoogleMap googleMap; + + public View locationButton; + + + // -- public fields + public MapCameraPosition mapCameraPosition; + public MapPreferences mapPreferences; + + // -- callbacks -- + + String savedCallbackIdForCreate; + + String savedCallbackIdForDidTapInfoWindow; + + String savedCallbackIdForDidCloseInfoWindow; + + String savedCallbackIdForDidTapMap; + + String savedCallbackIdForDidLongPressMap; + + String savedCallbackIdForDidTapMarker; + Boolean preventDefaultForDidTapMarker = false; + + String savedCallbackIdForDidBeginDraggingMarker; + + String savedCallbackIdForDidDragMarker; + + String savedCallbackIdForDidEndDraggingMarker; + + String savedCallbackIdForDidTapMyLocationButton; + Boolean preventDefaultForDidTapMyLocationButton = false; + + String savedCallbackIdForDidTapMyLocationDot; + + String savedCallbackIdForDidTapPoi; + + String savedCallbackIdForDidBeginMovingCamera; + String savedCallbackIdForDidMoveCamera; + String savedCallbackIdForDidEndMovingCamera; + + String savedCallbackIdForDidTapCluster; + Boolean preventDefaultForDidTapCluster = true; + String savedCallbackIdForDidTapClusterInfoWindow; + + String savedCallbackIdForDidTapClusterItem; + String savedCallbackIdForDidTapClusterItemInfoWindow; + + + private boolean isHidden = false; + + // values for unclustering animation bounding box + // with - bounding box width in pixels (px) + // height - bounding box height in pixels (px) + private int widthOfTheMapCameraAnimationUnclustering; + private int heightOfTheMapCameraAnimationUnclustering; + + + public boolean isHidden() { + return isHidden; + } + + public void setHidden(boolean hidden) { + isHidden = hidden; + } + + public CustomMapView(@NonNull Context context, @NonNull Activity activity, CustomMapViewEvents customMapViewEvents) { + this.context = context; + this.activity = activity; + this.customMapViewEvents = customMapViewEvents; + this.id = UUID.randomUUID().toString(); + } + + public String getId() { + return id; + } + + private boolean hasPermission() { + return ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED; + } + + @SuppressLint({"MissingPermission", "ResourceType"}) + @Override + public void onMapReady(GoogleMap googleMap) { + // populate `googleMap` variable for other methods to use + this.googleMap = googleMap; + + // set ClusterManager + // Initialize the manager with the context and the map. + this.mClusterManager = new ClusterManager<>(this.context, this.googleMap); + + + // Point the map's listeners at the listeners implemented by the cluster + // manager. + this.googleMap.setOnCameraIdleListener(mClusterManager); + + // using custom renderer here + mClusterManager.setRenderer(new OwnIconRenderer( + this.context, this.activity, this.googleMap, mClusterManager)); + + // set controls + UiSettings googleMapUISettings = this.googleMap.getUiSettings(); + googleMapUISettings.setIndoorLevelPickerEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.INDOOR_LEVEL_PICKER_KEY)); + googleMapUISettings.setMyLocationButtonEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.MY_LOCATION_BUTTON_KEY)); + + + // set appearance + this.googleMap.setMapStyle(this.mapPreferences.appearance.style); + this.googleMap.setBuildingsEnabled(this.mapPreferences.appearance.isBuildingsShown); + this.googleMap.setIndoorEnabled(this.mapPreferences.appearance.isIndoorShown); + if (hasPermission()) { + this.googleMap.setMyLocationEnabled(this.mapPreferences.appearance.isMyLocationDotShown); + locationButton = mapView.findViewById(0x2); + locationButton.setVisibility(View.GONE); + } + this.googleMap.setTrafficEnabled(this.mapPreferences.appearance.isTrafficShown); + + + // execute callback + if (customMapViewEvents != null && savedCallbackIdForCreate != null) { + JSObject result = getResultForMap(); + customMapViewEvents.onMapReady(savedCallbackIdForCreate, result); + } + + } + + protected void handleOnStart() { + if (mapView != null) { + mapView.onStart(); + } + } + + protected void handleOnResume() { + if (mapView != null) { + mapView.onResume(); + } + } + + protected void handleOnPause() { + if (mapView != null) { + mapView.onPause(); + } + } + + protected void handleOnStop() { + if (mapView != null) { + mapView.onStop(); + } + } + + protected void handleOnDestroy() { + if (mapView != null) { + mapView.onDestroy(); + } + } + + + public void setCallbackIdForEvent(String callbackId, String eventName, Boolean preventDefault) { + if (callbackId != null && eventName != null) { + if (eventName.equals(Events.EVENT_DID_TAP_INFO_WINDOW)) { + this.googleMap.setOnInfoWindowClickListener(this); + savedCallbackIdForDidTapInfoWindow = callbackId; + } else if (eventName.equals(Events.EVENT_DID_CLOSE_INFO_WINDOW)) { + this.googleMap.setOnInfoWindowCloseListener(this); + savedCallbackIdForDidCloseInfoWindow = callbackId; + } else if (eventName.equals(Events.EVENT_DID_TAP_MAP)) { + this.googleMap.setOnMapClickListener(this); + savedCallbackIdForDidTapMap = callbackId; + } else if (eventName.equals(Events.EVENT_DID_LONG_PRESS_MAP)) { + this.googleMap.setOnMapLongClickListener(this); + savedCallbackIdForDidLongPressMap = callbackId; + } else if (eventName.equals(Events.EVENT_DID_TAP_SINGLE_MARKER)) { + this.googleMap.setOnMarkerClickListener(mClusterManager); + savedCallbackIdForDidTapMarker = callbackId; + if (preventDefault == null) { + preventDefault = false; + } + preventDefaultForDidTapMarker = preventDefault; + } else if(eventName.equals(Events.EVENT_DID_TAP_CLUSTER_ITEM_INFO_WINDOW)) { + this.mClusterManager.setOnClusterItemInfoWindowClickListener(this); + savedCallbackIdForDidTapClusterItemInfoWindow = callbackId; + } else if (eventName.equals(Events.EVENT_DID_BEGIN_DRAGGING_MARKER)) { + this.googleMap.setOnMarkerDragListener(this); + savedCallbackIdForDidBeginDraggingMarker = callbackId; + } else if (eventName.equals(Events.EVENT_DID_DRAG_MARKER)) { + this.googleMap.setOnMarkerDragListener(this); + savedCallbackIdForDidDragMarker = callbackId; + } else if (eventName.equals(Events.EVENT_DID_END_DRAGGING_MARKER)) { + this.googleMap.setOnMarkerDragListener(this); + savedCallbackIdForDidEndDraggingMarker = callbackId; + } else if (eventName.equals(Events.EVENT_DID_TAP_CLUSTER)) { + this.mClusterManager.setOnClusterClickListener(this); + savedCallbackIdForDidTapCluster = callbackId; + if (preventDefault == null) { + preventDefault = false; + } + preventDefaultForDidTapCluster = preventDefault; + } else if (eventName.equals(Events.EVENT_DID_TAP_CLUSTER_INFO_WINDOW)) { + this.mClusterManager.setOnClusterInfoWindowClickListener(this); + savedCallbackIdForDidTapClusterInfoWindow = callbackId; + } else if (eventName.equals(Events.EVENT_DID_TAP_MARKER)) { + this.mClusterManager.setOnClusterItemClickListener(this); + savedCallbackIdForDidTapClusterItem = callbackId; + + } else if (eventName.equals(Events.EVENT_DID_TAP_MY_LOCATION_BUTTON)) { + this.googleMap.setOnMyLocationButtonClickListener(this); + savedCallbackIdForDidTapMyLocationButton = callbackId; + if (preventDefault == null) { + preventDefault = false; + } + preventDefaultForDidTapMyLocationButton = preventDefault; + } else if (eventName.equals(Events.EVENT_DID_TAP_MY_LOCATION_DOT)) { + this.googleMap.setOnMyLocationClickListener(this); + savedCallbackIdForDidTapMyLocationDot = callbackId; + } else if (eventName.equals((Events.EVENT_DID_TAP_POI))) { + this.googleMap.setOnPoiClickListener(this); + savedCallbackIdForDidTapPoi = callbackId; + } else if (eventName.equals((Events.EVENT_DID_BEGIN_MOVING_CAMERA))) { + this.googleMap.setOnCameraMoveStartedListener(this); + savedCallbackIdForDidBeginMovingCamera = callbackId; + } else if (eventName.equals((Events.EVENT_DID_MOVE_CAMERA))) { + this.googleMap.setOnCameraMoveListener(this); + savedCallbackIdForDidMoveCamera = callbackId; + } else if (eventName.equals((Events.EVENT_DID_END_MOVING_CAMERA))) { + this.googleMap.setOnCameraIdleListener(this); + savedCallbackIdForDidEndMovingCamera = callbackId; + } + } + } + + public void createMap(String callbackId, BoundingRect boundingRect, MapCameraPosition mapCameraPosition, MapPreferences mapPreferences) { + savedCallbackIdForCreate = callbackId; + + this.mapCameraPosition = mapCameraPosition; + this.mapPreferences = mapPreferences; + + GoogleMapOptions googleMapOptions = this.mapPreferences.generateGoogleMapOptions(); + googleMapOptions.camera(this.mapCameraPosition.cameraPosition); + + mapView = new MapView(context, googleMapOptions); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getScaledPixels(boundingRect.width), getScaledPixels(boundingRect.height)); + lp.topMargin = getScaledPixels(boundingRect.y); + lp.leftMargin = getScaledPixels(boundingRect.x); + + mapView.setLayoutParams(lp); + + mapView.onCreate(null); + mapView.onStart(); + mapView.getMapAsync(this); + + widthOfTheMapCameraAnimationUnclustering = getScaledPixels(boundingRect.width) - (getScaledPixels(boundingRect.width) /5); + heightOfTheMapCameraAnimationUnclustering = getScaledPixels(boundingRect.height) - (getScaledPixels(boundingRect.height) /5); + } + + @SuppressLint("MissingPermission") + public JSObject invalidateMap() { + if (this.googleMap == null) { + return null; + } + + UiSettings googleMapUISettings = this.googleMap.getUiSettings(); + + // set gestures + googleMapUISettings.setRotateGesturesEnabled(this.mapPreferences.gestures.getBoolean(MapPreferencesGestures.ROTATE_ALLOWED_KEY)); + googleMapUISettings.setScrollGesturesEnabled(this.mapPreferences.gestures.getBoolean(MapPreferencesGestures.SCROLL_ALLOWED_KEY)); + googleMapUISettings.setScrollGesturesEnabledDuringRotateOrZoom(this.mapPreferences.gestures.getBoolean(MapPreferencesGestures.SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY)); + googleMapUISettings.setTiltGesturesEnabled(this.mapPreferences.gestures.getBoolean(MapPreferencesGestures.TILT_ALLOWED_KEY)); + googleMapUISettings.setZoomGesturesEnabled(this.mapPreferences.gestures.getBoolean(MapPreferencesGestures.ZOOM_ALLOWED_KEY)); + + // set controls + googleMapUISettings.setCompassEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.COMPASS_BUTTON_KEY)); + googleMapUISettings.setIndoorLevelPickerEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.INDOOR_LEVEL_PICKER_KEY)); + googleMapUISettings.setMapToolbarEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.MAP_TOOLBAR_KEY)); + googleMapUISettings.setMyLocationButtonEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.MY_LOCATION_BUTTON_KEY)); + googleMapUISettings.setZoomControlsEnabled(this.mapPreferences.controls.getBoolean(MapPreferencesControls.ZOOM_BUTTONS_KEY)); + + // set appearance + this.googleMap.setMapType(this.mapPreferences.appearance.type); + this.googleMap.setMapStyle(this.mapPreferences.appearance.style); + this.googleMap.setBuildingsEnabled(this.mapPreferences.appearance.isBuildingsShown); + this.googleMap.setIndoorEnabled(this.mapPreferences.appearance.isIndoorShown); + if (hasPermission()) { + this.googleMap.setMyLocationEnabled(this.mapPreferences.appearance.isMyLocationDotShown); + } + this.googleMap.setTrafficEnabled(this.mapPreferences.appearance.isTrafficShown); + + return getResultForMap(); + } + + public JSObject getMap() { + return getResultForMap(); + } + + + private JSObject getResultForMap() { + if (this.mapView != null && this.googleMap != null) { + // initialize JSObjects + JSObject result = new JSObject(); + + JSObject resultGoogleMap = new JSObject(); + result.put("googleMap", resultGoogleMap); + + JSObject resultPreferences = new JSObject(); + resultGoogleMap.put("preferences", resultPreferences); + + JSObject resultGestures = new JSObject(); + resultPreferences.put("gestures", resultGestures); + + JSObject resultControls = new JSObject(); + resultPreferences.put("controls", resultControls); + + JSObject resultAppearance = new JSObject(); + resultPreferences.put("appearance", resultAppearance); + + // get UISettings + UiSettings googleMapUISettings = this.googleMap.getUiSettings(); + + // return mapId + resultGoogleMap.put("mapId", id); + + // return cameraPosition + this.getResultForCameraPosition(resultGoogleMap); + + // return gestures + resultGestures.put(MapPreferencesGestures.ROTATE_ALLOWED_KEY, googleMapUISettings.isRotateGesturesEnabled()); + resultGestures.put(MapPreferencesGestures.SCROLL_ALLOWED_KEY, googleMapUISettings.isScrollGesturesEnabled()); + resultGestures.put(MapPreferencesGestures.SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY, googleMapUISettings.isScrollGesturesEnabledDuringRotateOrZoom()); + resultGestures.put(MapPreferencesGestures.TILT_ALLOWED_KEY, googleMapUISettings.isTiltGesturesEnabled()); + resultGestures.put(MapPreferencesGestures.ZOOM_ALLOWED_KEY, googleMapUISettings.isZoomGesturesEnabled()); + + // return controls + resultControls.put(MapPreferencesControls.COMPASS_BUTTON_KEY, googleMapUISettings.isCompassEnabled()); + resultControls.put(MapPreferencesControls.INDOOR_LEVEL_PICKER_KEY, googleMapUISettings.isIndoorLevelPickerEnabled()); + resultControls.put(MapPreferencesControls.MAP_TOOLBAR_KEY, googleMapUISettings.isMapToolbarEnabled()); + resultControls.put(MapPreferencesControls.MY_LOCATION_BUTTON_KEY, googleMapUISettings.isMyLocationButtonEnabled()); + resultControls.put(MapPreferencesControls.ZOOM_BUTTONS_KEY, googleMapUISettings.isZoomControlsEnabled()); + + // return appearance + resultAppearance.put(MapPreferencesAppearance.TYPE_KEY, this.googleMap.getMapType()); + resultAppearance.put(MapPreferencesAppearance.BUILDINGS_SHOWN_KEY, this.googleMap.isBuildingsEnabled()); + resultAppearance.put(MapPreferencesAppearance.INDOOR_SHOWN_KEY, this.googleMap.isIndoorEnabled()); + resultAppearance.put(MapPreferencesAppearance.MY_LOCATION_DOT_SHOWN_KEY, this.googleMap.isMyLocationEnabled()); + resultAppearance.put(MapPreferencesAppearance.TRAFFIC_SHOWN_KEY, this.googleMap.isTrafficEnabled()); + + return result; + } + return null; + } + + + @Override + public void onCameraIdle() { + if (customMapViewEvents != null && savedCallbackIdForDidEndMovingCamera != null) { + this.mClusterManager.onCameraIdle(); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidEndMovingCamera, this.getResultForCameraPosition(new JSObject())); + } + } + + @Override + public void onCameraMove() { + if (customMapViewEvents != null && savedCallbackIdForDidMoveCamera != null) { + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidMoveCamera, null); + } + } + + + @Override + public void onCameraMoveStarted(int i) { + if (customMapViewEvents != null && savedCallbackIdForDidBeginMovingCamera != null) { + int reason = 2; + if (i == REASON_GESTURE) { + // Camera motion initiated in response to user gestures on the map. + // For example: pan, tilt, pinch to zoom, or rotate. + reason = 1; + } + JSObject result = new JSObject(); + result.put("reason", reason); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidBeginMovingCamera, result); + } + } + + @Override + public void onInfoWindowClick(Marker marker) { + if (customMapViewEvents != null && savedCallbackIdForDidTapInfoWindow != null) { + JSObject result = CustomMarker.getResultForMarker(marker, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapInfoWindow, result); + } + } + + @Override + public void onInfoWindowClose(Marker marker) { + if (customMapViewEvents != null && savedCallbackIdForDidCloseInfoWindow != null) { + JSObject result = CustomMarker.getResultForMarker(marker, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidCloseInfoWindow, result); + } + } + + @Override + public void onMapClick(LatLng latLng) { + if (customMapViewEvents != null && savedCallbackIdForDidTapMap != null) { + JSObject result = getResultForPosition(latLng); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapMap, result); + } + } + + @Override + public void onMapLongClick(LatLng latLng) { + if (customMapViewEvents != null && savedCallbackIdForDidLongPressMap != null) { + JSObject result = getResultForPosition(latLng); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidLongPressMap, result); + } + } + + @Override + public boolean onMarkerClick(Marker marker) { +// if (customMapViewEvents != null && savedCallbackIdForDidTapMarker != null) { +// JSObject result = CustomMarker.getResultForMarker(marker, this.id); +// customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapMarker, result); +// } +// return preventDefaultForDidTapMarker; + return true; + } + + @Override + public void onMarkerDrag(Marker marker) { + if (customMapViewEvents != null && savedCallbackIdForDidDragMarker != null) { + JSObject result = CustomMarker.getResultForMarker(marker, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidDragMarker, result); + } + } + + @Override + public void onMarkerDragEnd(Marker marker) { + if (customMapViewEvents != null && savedCallbackIdForDidEndDraggingMarker != null) { + JSObject result = CustomMarker.getResultForMarker(marker, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidEndDraggingMarker, result); + } + } + + @Override + public void onMarkerDragStart(Marker marker) { + if (customMapViewEvents != null && savedCallbackIdForDidBeginDraggingMarker != null) { + JSObject result = CustomMarker.getResultForMarker(marker, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidBeginDraggingMarker, result); + } + } + + @Override + public boolean onMyLocationButtonClick() { + if (customMapViewEvents != null && savedCallbackIdForDidTapMyLocationButton != null) { + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapMyLocationButton, null); + } + return preventDefaultForDidTapMyLocationButton; + } + + @Override + public void onMyLocationClick(@NonNull Location location) { + if (customMapViewEvents != null && savedCallbackIdForDidTapMyLocationDot != null) { + JSObject result = getResultForPosition(location); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapMyLocationDot, result); + } + } + + @Override + public void onPoiClick(PointOfInterest pointOfInterest) { + if (customMapViewEvents != null && savedCallbackIdForDidTapPoi != null) { + JSObject result = getResultForPoi(pointOfInterest); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapPoi, result); + } + } + + + @Override + public boolean onClusterClick(Cluster cluster) { + LatLngBounds.Builder builder = LatLngBounds.builder(); + for (CustomMarker item : cluster.getItems()) { + builder.include(item.getPosition()); + } + final LatLngBounds bounds = builder.build(); + // if bounds very small than + boolean getMarkersData = unclusterCluster(bounds); + + if (customMapViewEvents != null && savedCallbackIdForDidTapCluster != null) { + JSObject result = getResultForCluster(cluster, this.id, getMarkersData); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapCluster, result); + } + + + return preventDefaultForDidTapCluster; + } + + private boolean unclusterCluster(LatLngBounds bounds) { + // check if cluster can uncluster + if((Math.abs(bounds.northeast.latitude - bounds.southwest.latitude) > EPSILON) && + (Math.abs(bounds.northeast.longitude - bounds.southwest.longitude) > EPSILON)) { + CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngBounds(bounds, + widthOfTheMapCameraAnimationUnclustering, + heightOfTheMapCameraAnimationUnclustering, + 100); + googleMap.animateCamera(cameraUpdate); + return false; + } else { + // if all markers inside cluster is have same position + return true; + } + } + + @Override + public void onClusterInfoWindowClick(Cluster cluster) { + if (customMapViewEvents != null && savedCallbackIdForDidTapClusterInfoWindow != null) { + JSObject result = getResultForCluster(cluster, this.id, false); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapClusterInfoWindow, result); + } + } + + + @Override + public boolean onClusterItemClick(CustomMarker item) { + if (customMapViewEvents != null && savedCallbackIdForDidTapMarker != null) { + JSObject result = CustomMarker.getResultForMarker(item, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapMarker, result); + } + return preventDefaultForDidTapMarker; + } + + @Override + public void onClusterItemInfoWindowClick(CustomMarker item) { + if (customMapViewEvents != null && savedCallbackIdForDidTapInfoWindow != null) { + JSObject result = CustomMarker.getResultForMarker(item, this.id); + customMapViewEvents.resultForCallbackId(savedCallbackIdForDidTapInfoWindow, result); + } + } + + + private static JSObject getResultForCluster(Cluster cluster, + String mapId, + boolean getMarkersData) { + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + + // get map id + positionResult.put("mapId", mapId); + + // get position values + positionResult.put("latitude", cluster.getPosition().latitude); + positionResult.put("longitude", cluster.getPosition().longitude); + + // return result + result.put("position", positionResult); + + if (getMarkersData) { + JSObject markersData = new JSObject(); + for (CustomMarker marker: + cluster.getItems()) { + markersData.put(marker.getId(), CustomMarker.getResultForMarker(marker, mapId)); + + } + result.put("markersData", markersData); + } + return result; + } + + private int getScaledPixels(float pixels) { + // Get the screen's density scale + final float scale = context.getResources().getDisplayMetrics().density; + // Convert the dps to pixels, based on density scale + return (int) (pixels * scale + 0.5f); + } + + public void addToView(ViewGroup parent) { + parent.addView(mapView); + } + + public void removeFromView(ViewGroup parent) { + parent.removeView(mapView); + } + + public void clear() { + googleMap.clear(); + mClusterManager.clearItems(); + } + + public void addMarker(CustomMarker customMarker) { +// customMarker.setProfilePhoto(R.drawable.ic_launcher); + this.mClusterManager.addItem(customMarker); + this.mClusterManager.cluster(); + +// Iterator iterator = this.mClusterManager.getMarkerCollection().getMarkers().iterator(); +// while (iterator.hasNext()) { +// Marker marker = iterator.next(); +// if(marker.getId().equals(customMarker.getMarkerId())) { +// return marker; +// } +// } + } + + public void addMarkers(Collection arrayOfCustomMarkers) { + + this.mClusterManager.addItems(arrayOfCustomMarkers); + this.mClusterManager.cluster(); + + } + + public boolean updateMarker(String markerId, JSObject newPreferences) { + boolean isUpdated = false; + Iterator iterator = this.mClusterManager.getAlgorithm().getItems().iterator(); + while (iterator.hasNext()) { + CustomMarker customMarker = iterator.next(); + if (customMarker.getId().equals(markerId)) { + customMarker.updateFromJSObject(newPreferences); + isUpdated = mClusterManager.updateItem(customMarker); + mClusterManager.cluster(); + } + } + + return isUpdated; + } + + public void removeMarker(String markerId) { + Iterator iterator = this.mClusterManager.getAlgorithm().getItems().iterator(); + while (iterator.hasNext()) { + CustomMarker customMarker = iterator.next(); + if (customMarker.getId().equals(markerId)) { + mClusterManager.removeItem(customMarker); + mClusterManager.cluster(); + } + } + } + + + public CameraPosition getCameraPosition() { + if (this.googleMap != null) { + return this.googleMap.getCameraPosition(); + } + return null; + } + + public JSObject moveCamera(Integer duration) { + CameraUpdate cameraUpdate = CameraUpdateFactory.newCameraPosition(this.mapCameraPosition.cameraPosition); + + if (duration == null || duration <= 0) { + googleMap.moveCamera(cameraUpdate); + } else { + googleMap.animateCamera(cameraUpdate, duration, null); + } + + return getResultForMap(); + } + + + private JSObject getResultForCameraPosition(JSObject resultObjectToExtend) { + JSObject resultCameraPosition = new JSObject(); + resultObjectToExtend.put("cameraPosition", resultCameraPosition); + + JSObject resultCameraPositionTarget = new JSObject(); + resultCameraPosition.put("target", resultCameraPositionTarget); + + // get CameraPosition + CameraPosition cameraPosition = this.getCameraPosition(); + + // return cameraPosition + resultCameraPositionTarget.put("latitude", cameraPosition.target.latitude); + resultCameraPositionTarget.put("longitude", cameraPosition.target.longitude); + resultCameraPosition.put("bearing", cameraPosition.bearing); + resultCameraPosition.put("tilt", cameraPosition.tilt); + resultCameraPosition.put("zoom", cameraPosition.zoom); + + return resultObjectToExtend; + } + + + private JSObject getResultForPosition(Location location) { + + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + result.put("position", positionResult); + + // get position values + positionResult.put("latitude", location.getLatitude()); + positionResult.put("longitude", location.getLongitude()); + + // return result + return result; + } + + private JSObject getResultForPosition(LatLng latLng) { + + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + result.put("position", positionResult); + + // get position values + positionResult.put("latitude", latLng.latitude); + positionResult.put("longitude", latLng.longitude); + + // return result + return result; + } + + private JSObject getResultForPoi(PointOfInterest pointOfInterest) { + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + JSObject poiResult = new JSObject(); + + // get position values + positionResult.put("latitude", pointOfInterest.latLng.latitude); + positionResult.put("longitude", pointOfInterest.latLng.longitude); + + // get other values + poiResult.put("name", pointOfInterest.name); + poiResult.put("placeId", pointOfInterest.placeId); + + // return result + result.put("position", positionResult); + result.put("poi", poiResult); + return result; + } + + public void zoomIn() { + this.googleMap.animateCamera(CameraUpdateFactory.zoomIn()); + } + + public void zoomOut() { + this.googleMap.animateCamera(CameraUpdateFactory.zoomOut()); + } + +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapViewEvents.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapViewEvents.java new file mode 100644 index 00000000..f6871f16 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/CustomMapViewEvents.java @@ -0,0 +1,9 @@ +package com.hemangkumar.capacitorgooglemaps; + +import com.getcapacitor.JSObject; + +public interface CustomMapViewEvents { + void onMapReady(String callbackId, JSObject result); + + void resultForCallbackId(String callbackId, JSObject result); +} \ No newline at end of file diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/JSObjectDefaults.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/JSObjectDefaults.java new file mode 100644 index 00000000..4ef14399 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/JSObjectDefaults.java @@ -0,0 +1,121 @@ +package com.hemangkumar.capacitorgooglemaps; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; +import com.getcapacitor.PluginCall; + +import org.json.JSONException; + +import java.util.HashMap; + +public abstract class JSObjectDefaults { + private final HashMap defaults; + + private final HashMap actualValues = new HashMap(){}; + + public JSObjectDefaults(HashMap defaults) { + this.defaults = defaults; + } + + private Object getObject(String key) { + // get default value + Object defaultObject = this.defaults.get(key); + + // then get actual value + if (this.actualValues.containsKey(key)) { + Object object = this.actualValues.get(key); + if (object != null) { + return object; + } + } + + if (defaultObject != null) { + return defaultObject; + } + + return null; + } + + public boolean getBoolean(String key) { + Object object = getObject(key); + + if (object != null) { + return (boolean) object; + } + + return false; + } + + public void updateFromJSObject(@Nullable JSObject jsObject) { + if (jsObject != null) { + for (HashMap.Entry entry : defaults.entrySet()) { + String key = entry.getKey(); + Object defaultValue = entry.getValue(); + + if (defaultValue instanceof Boolean) { + if (jsObject.has(key)) { + Boolean newValue = jsObject.getBoolean(key, (Boolean) defaultValue); + if (newValue == null) { + newValue = (Boolean) defaultValue; + } + actualValues.put(key, (boolean) newValue); + } + } + } + } + } + + @NonNull + public static JSObject getJSObjectSafe(JSObject jsObject, @NonNull String name, @NonNull JSObject defaultValue) { + JSObject returnedJsObject = jsObject.getJSObject(name); + if (returnedJsObject != null) { + return returnedJsObject; + } + return defaultValue; + } + + @NonNull + public static JSObject getJSObjectSafe(PluginCall call, @NonNull String name, @NonNull JSObject defaultValue) { + if (call != null) { + JSObject jsObject = call.getObject(name, defaultValue); + if (jsObject != null) { + return jsObject; + } + } + return new JSObject(); + } + + @NonNull + public static Double getDoubleSafe(JSObject jsObject, @NonNull String name, @NonNull Double defaultValue) { + try { + return jsObject.getDouble(name); + } catch (JSONException ignored) {} + return defaultValue; + } + + @NonNull + public static Integer getIntegerSafe(JSObject jsObject, @NonNull String name, @NonNull Integer defaultValue) { + try { + return jsObject.getInt(name); + } catch (JSONException ignored) {} + return defaultValue; + } + + @NonNull + public static Float getFloatSafe(JSObject jsObject, @NonNull String name, @NonNull Float defaultValue) { + Double returnedDouble = JSObjectDefaults.getDoubleSafe(jsObject, name, (double) defaultValue); + return returnedDouble.floatValue(); + } + + @NonNull + public static Boolean getBooleanSafe(JSObject jsObject, @NonNull String name, @NonNull Boolean defaultValue) { + Boolean returnedBoolean = jsObject.getBoolean(name, false); + if (returnedBoolean != null) { + return returnedBoolean; + } + return defaultValue; + } +} + diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapCameraPosition.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapCameraPosition.java new file mode 100644 index 00000000..48e11322 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapCameraPosition.java @@ -0,0 +1,95 @@ +package com.hemangkumar.capacitorgooglemaps; + +import android.util.Log; + +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; +import com.google.android.gms.maps.model.CameraPosition; +import com.google.android.gms.maps.model.LatLng; + +import org.json.JSONException; + +public class MapCameraPosition { + public CameraPosition cameraPosition; + + public MapCameraPosition() { + this.cameraPosition = CameraPosition.builder().target(new LatLng(0, 0)).build(); + } + + public void updateFromJSObject(@Nullable JSObject cameraPosition, CameraPosition previousCameraPosition) { + CameraPosition.Builder cameraPositionBuilder; + if (previousCameraPosition != null) { + // use given cameraPosition as the base + cameraPositionBuilder = new CameraPosition.Builder(previousCameraPosition); + } else if (this.cameraPosition != null) { + // use oldCameraPosition as the base + cameraPositionBuilder = new CameraPosition.Builder(this.cameraPosition); + } else { + cameraPositionBuilder = new CameraPosition.Builder(); + } + + LatLng latLng = null; + if (cameraPosition != null) { + JSObject target = cameraPosition.getJSObject("target"); + + if (target != null) { + try { + if (target.has("latitude") && target.has("longitude")) { + double latitude = target.getDouble("latitude"); + double longitude = target.getDouble("longitude"); + + latLng = new LatLng(latitude, longitude); + cameraPositionBuilder.target(latLng); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + if (cameraPosition.has("bearing")) { + try { + float bearing = (float) cameraPosition.getDouble("bearing"); + cameraPositionBuilder.bearing(bearing); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + if (cameraPosition.has("tilt")) { + try { + float tilt = (float) cameraPosition.getDouble("tilt"); + if (tilt < 0.0F || tilt > 90.0F) { + Log.d("GoogleMap", "Tilt needs to be between 0 and 90 inclusive: " + tilt); + } else { + cameraPositionBuilder.tilt(tilt); + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + + if (cameraPosition.has("zoom")) { + try { + float zoom = (float) cameraPosition.getDouble("zoom"); + cameraPositionBuilder.zoom(zoom); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + if (this.cameraPosition == null && latLng == null) { + // at the very least a camera target should be set + Log.d("GoogleMap", "Target of camera has been set to (0,0) automatically."); + latLng = new LatLng(0, 0); + cameraPositionBuilder.target(latLng); + } + + try { + this.cameraPosition = cameraPositionBuilder.build(); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapPreferences.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapPreferences.java new file mode 100644 index 00000000..fa3acfce --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/MapPreferences.java @@ -0,0 +1,72 @@ +package com.hemangkumar.capacitorgooglemaps; + +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; +import com.google.android.gms.maps.GoogleMapOptions; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesAppearance; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesControls; +import com.hemangkumar.capacitorgooglemaps.mapsutility.MapPreferencesGestures; + +public class MapPreferences { + public MapPreferencesGestures gestures; + public MapPreferencesControls controls; + public MapPreferencesAppearance appearance; + + public MapPreferences() { + this.gestures = new MapPreferencesGestures(); + this.controls = new MapPreferencesControls(); + this.appearance = new MapPreferencesAppearance(); + } + + public void updateFromJSObject(@Nullable JSObject preferences) { + if (preferences != null) { + // update gestures + JSObject gesturesObject = preferences.getJSObject("gestures"); + this.gestures.updateFromJSObject(gesturesObject); + // update controls + JSObject controlsObject = preferences.getJSObject("controls"); + this.controls.updateFromJSObject(controlsObject); + // update appearance + JSObject appearanceObject = preferences.getJSObject("appearance"); + this.appearance.updateFromJSObject(appearanceObject); + } + } + + public GoogleMapOptions generateGoogleMapOptions() { + GoogleMapOptions googleMapOptions = new GoogleMapOptions(); + + // set gestures + if (this.gestures != null) { + googleMapOptions.rotateGesturesEnabled(gestures.getBoolean(MapPreferencesGestures.ROTATE_ALLOWED_KEY)); + googleMapOptions.scrollGesturesEnabled(gestures.getBoolean(MapPreferencesGestures.SCROLL_ALLOWED_KEY)); + googleMapOptions.scrollGesturesEnabledDuringRotateOrZoom(gestures.getBoolean(MapPreferencesGestures.SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY)); + googleMapOptions.tiltGesturesEnabled(gestures.getBoolean(MapPreferencesGestures.TILT_ALLOWED_KEY)); + googleMapOptions.zoomGesturesEnabled(gestures.getBoolean(MapPreferencesGestures.ZOOM_ALLOWED_KEY)); + } + + // set controls + if (this.controls != null) { + googleMapOptions.compassEnabled(controls.getBoolean(MapPreferencesControls.COMPASS_BUTTON_KEY)); + googleMapOptions.mapToolbarEnabled(controls.getBoolean(MapPreferencesControls.MAP_TOOLBAR_KEY)); + googleMapOptions.zoomControlsEnabled(controls.getBoolean(MapPreferencesControls.ZOOM_BUTTONS_KEY)); + + // controls.isIndoorLevelPickerEnabled can only be set through `UiSettings` + // controls.isMyLocationButtonEnabled can only be set through `UiSettings` + } + + // set appearance + if (this.appearance != null) { + // set mapType + googleMapOptions.mapType(this.appearance.type); + + // appearance.style can only be set through `GoogleMap` + // appearance.isIndoorShown can only be set through `GoogleMap` + // appearance.isBuildingsShown can only be set through `GoogleMap` + // appearance.isMyLocationDotShown can only be set through `GoogleMap` + // appearance.isTrafficShown can only be set through `GoogleMap` + } + + return googleMapOptions; + } +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/Events.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/Events.java new file mode 100644 index 00000000..38a09692 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/Events.java @@ -0,0 +1,23 @@ +package com.hemangkumar.capacitorgooglemaps.mapsutility; + +public abstract class Events { + + public static final String EVENT_DID_TAP_INFO_WINDOW = "didTapInfoWindow"; + public static final String EVENT_DID_CLOSE_INFO_WINDOW = "didCloseInfoWindow"; + public static final String EVENT_DID_TAP_MAP = "didTapMap"; + public static final String EVENT_DID_LONG_PRESS_MAP = "didLongPressMap"; + public static final String EVENT_DID_TAP_SINGLE_MARKER = "didTapMarker"; + public static final String EVENT_DID_BEGIN_DRAGGING_MARKER = "didBeginDraggingMarker"; + public static final String EVENT_DID_DRAG_MARKER = "didDragMarker"; + public static final String EVENT_DID_END_DRAGGING_MARKER = "didEndDraggingMarker"; + public static final String EVENT_DID_TAP_CLUSTER = "didTapCluster"; + public static final String EVENT_DID_TAP_CLUSTER_INFO_WINDOW = "didTapClusterInfoWindow"; + public static final String EVENT_DID_TAP_MARKER = "didTapClusterItem"; + public static final String EVENT_DID_TAP_CLUSTER_ITEM_INFO_WINDOW = "didTapClusterItemInfoWindow"; + public static final String EVENT_DID_TAP_MY_LOCATION_BUTTON = "didTapMyLocationButton"; + public static final String EVENT_DID_TAP_MY_LOCATION_DOT = "didTapMyLocationDot"; + public static final String EVENT_DID_TAP_POI = "didTapPoi"; + public static final String EVENT_DID_BEGIN_MOVING_CAMERA = "didBeginMovingCamera"; + public static final String EVENT_DID_MOVE_CAMERA = "didMoveCamera"; + public static final String EVENT_DID_END_MOVING_CAMERA = "didEndMovingCamera"; +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesAppearance.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesAppearance.java new file mode 100644 index 00000000..0d6dbc17 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesAppearance.java @@ -0,0 +1,67 @@ +package com.hemangkumar.capacitorgooglemaps.mapsutility; + +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; +import com.google.android.gms.maps.model.MapStyleOptions; + +public class MapPreferencesAppearance { + public Integer type; + public MapStyleOptions style; + public Boolean isBuildingsShown; + public Boolean isIndoorShown; + public Boolean isMyLocationDotShown; + public Boolean isTrafficShown; + + public static final String TYPE_KEY = "type"; + public static final String STYLE_KEY = "style"; + public static final String BUILDINGS_SHOWN_KEY = "isBuildingsShown"; + public static final String INDOOR_SHOWN_KEY = "isIndoorShown"; + public static final String MY_LOCATION_DOT_SHOWN_KEY = "isMyLocationDotShown"; + public static final String TRAFFIC_SHOWN_KEY = "isTrafficShown"; + + public MapPreferencesAppearance() { + this.type = 1; + this.style = null; + this.isBuildingsShown = true; + this.isIndoorShown = true; + this.isMyLocationDotShown = false; + this.isTrafficShown = false; + } + + public void updateFromJSObject(@Nullable JSObject jsObject) { + if (jsObject != null) { + // update mapType + if (jsObject.has(TYPE_KEY)) { + Integer mapType = jsObject.getInteger(TYPE_KEY, 1); + if (mapType != null) { + if (mapType < 0 || mapType > 4) { + mapType = 1; + } + this.type = mapType; + } + } + if (jsObject.has(STYLE_KEY)) { + String mapStyle = jsObject.getString(STYLE_KEY, null); + if (mapStyle == null) { + this.style = null; + } else { + this.style = new MapStyleOptions(mapStyle); + } + } + if (jsObject.has(BUILDINGS_SHOWN_KEY)) { + this.isBuildingsShown = jsObject.getBool(BUILDINGS_SHOWN_KEY); + } + if (jsObject.has(INDOOR_SHOWN_KEY)) { + this.isIndoorShown = jsObject.getBool(INDOOR_SHOWN_KEY); + } + if (jsObject.has(MY_LOCATION_DOT_SHOWN_KEY)) { + this.isMyLocationDotShown = jsObject.getBool(MY_LOCATION_DOT_SHOWN_KEY); + } + if (jsObject.has(TRAFFIC_SHOWN_KEY)) { + this.isTrafficShown = jsObject.getBool(TRAFFIC_SHOWN_KEY); + } + } + } +} + diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesControls.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesControls.java new file mode 100644 index 00000000..b01d45c3 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesControls.java @@ -0,0 +1,25 @@ +package com.hemangkumar.capacitorgooglemaps.mapsutility; + +import com.hemangkumar.capacitorgooglemaps.JSObjectDefaults; + +import java.util.HashMap; + +public class MapPreferencesControls extends JSObjectDefaults { + public static final String COMPASS_BUTTON_KEY = "isCompassButtonEnabled"; + public static final String INDOOR_LEVEL_PICKER_KEY = "isIndoorLevelPickerEnabled"; + public static final String MAP_TOOLBAR_KEY = "isMapToolbarEnabled"; + public static final String MY_LOCATION_BUTTON_KEY = "isMyLocationButtonEnabled"; + public static final String ZOOM_BUTTONS_KEY = "isZoomButtonsEnabled"; + + public MapPreferencesControls() { + super(new HashMap(){{ + put(COMPASS_BUTTON_KEY, Boolean.FALSE); + put(INDOOR_LEVEL_PICKER_KEY, Boolean.FALSE); + put(MAP_TOOLBAR_KEY, Boolean.FALSE); + put(MY_LOCATION_BUTTON_KEY, Boolean.FALSE); + put(ZOOM_BUTTONS_KEY, Boolean.FALSE); + }}); + } + + +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesGestures.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesGestures.java new file mode 100644 index 00000000..a224e491 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/mapsutility/MapPreferencesGestures.java @@ -0,0 +1,23 @@ +package com.hemangkumar.capacitorgooglemaps.mapsutility; + +import com.hemangkumar.capacitorgooglemaps.JSObjectDefaults; + +import java.util.HashMap; + +public class MapPreferencesGestures extends JSObjectDefaults { + public static final String ROTATE_ALLOWED_KEY = "isRotateAllowed"; + public static final String SCROLL_ALLOWED_KEY = "isScrollAllowed"; + public static final String SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY = "isScrollAllowedDuringRotateOrZoom"; + public static final String TILT_ALLOWED_KEY = "isTiltAllowed"; + public static final String ZOOM_ALLOWED_KEY = "isZoomAllowed"; + + public MapPreferencesGestures() { + super(new HashMap(){{ + put(ROTATE_ALLOWED_KEY, Boolean.TRUE); + put(SCROLL_ALLOWED_KEY, Boolean.TRUE); + put(SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY, Boolean.TRUE); + put(TILT_ALLOWED_KEY, Boolean.TRUE); + put(ZOOM_ALLOWED_KEY, Boolean.TRUE); + }}); + } +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/CustomMarker.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/CustomMarker.java new file mode 100644 index 00000000..8a84692b --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/CustomMarker.java @@ -0,0 +1,210 @@ +package com.hemangkumar.capacitorgooglemaps.marker; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; +import com.google.android.gms.maps.model.LatLng; +import com.google.android.gms.maps.model.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.maps.android.clustering.ClusterItem; +import com.hemangkumar.capacitorgooglemaps.JSObjectDefaults; + +import org.json.JSONException; + +import java.util.Objects; +import java.util.UUID; + +public class CustomMarker implements ClusterItem { + // generate id for the just added marker, + // put this marker into a hashmap with the corresponding id, + // so we can retrieve the marker by id later on + private String id = UUID.randomUUID().toString(); + + private final MarkerOptions markerOptions = new MarkerOptions(); + private JSObject tag = new JSObject(); + + private int markerCategoryId; + + public void updateFromJSObject(JSObject preferences) { + final JSObject position = JSObjectDefaults.getJSObjectSafe(preferences, "position", new JSObject()); + final Double latitude = JSObjectDefaults.getDoubleSafe(position, "latitude", 0d); + final Double longitude = JSObjectDefaults.getDoubleSafe(position, "longitude", 0d); + LatLng latLng = new LatLng(latitude, longitude); + + final String markerId = preferences.getString("id", null); + final String title = preferences.getString("title", ""); + final String snippet = preferences.getString("snippet", ""); + final Float opacity = JSObjectDefaults.getFloatSafe(preferences, "opacity", 1f); + final Boolean isFlat = JSObjectDefaults.getBooleanSafe(preferences,"isFlat", false); + final Boolean isDraggable = JSObjectDefaults.getBooleanSafe(preferences,"isDraggable", false); + final Integer markerCategoryId = JSObjectDefaults.getIntegerSafe(preferences, "category", 0); + + // if marker id setted than use it + if(markerId != null) + this.id = markerId; + + this.markerCategoryId = markerCategoryId; + + this.setMetadata(JSObjectDefaults.getJSObjectSafe(preferences, "metadata", new JSObject())); + + this.markerOptions.position(latLng); + this.markerOptions.title(title); + this.markerOptions.snippet(snippet); + this.markerOptions.alpha(opacity); + this.markerOptions.flat(isFlat); + this.markerOptions.draggable(isDraggable); + + + + this.markerOptions.zIndex(21); + } + + + private void setMetadata(@NonNull JSObject jsObject) { + JSObject tag = new JSObject(); + // set id to tag + tag.put("id", this.id); + // set iconId to tag + tag.put("iconId", this.markerCategoryId); + // then set metadata to tag + tag.put("metadata", jsObject); + // save in tag variable + this.tag = tag; + } + + public static JSObject getResultForMarker(Marker marker, String mapId) { + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + JSObject markerResult = new JSObject(); + + // get map id + positionResult.put("mapId", mapId); + + // get position values + positionResult.put("latitude", marker.getPosition().latitude); + positionResult.put("longitude", marker.getPosition().longitude); + + // get marker specific values +// markerResult.put("title", marker.getTitle()); +// markerResult.put("snippet", marker.getSnippet()); +// markerResult.put("opacity", marker.getAlpha()); +// markerResult.put("isFlat", marker.isFlat()); +// markerResult.put("isDraggable", marker.isDraggable()); + markerResult.put("metadata", new JSObject()); + + JSObject tag = (JSObject) marker.getTag(); + if (tag != null) { + // get and set markerId to marker + String markerId = tag.getString("id"); + markerResult.put("id", markerId); + + // get and set iconId to marker + int iconId = JSObjectDefaults.getIntegerSafe(tag, "iconId", -1); + markerResult.put("iconId", iconId); + + // get and set metadata to marker + try { + JSObject metadata = tag.getJSObject("metadata", new JSObject()); + markerResult.put("metadata", metadata); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + // return result + result.put("position", positionResult); + result.put("marker", markerResult); + + return result; + } + + public static JSObject getResultForMarker(CustomMarker marker, String mapId) { + + // initialize JSObjects to return + JSObject result = new JSObject(); + JSObject positionResult = new JSObject(); + JSObject markerResult = new JSObject(); + + // get map id + positionResult.put("mapId", mapId); + + // get position values + positionResult.put("latitude", marker.getPosition().latitude); + positionResult.put("longitude", marker.getPosition().longitude); + + // get marker specific values +// markerResult.put("title", marker.getTitle()); +// markerResult.put("snippet", marker.getSnippet()); +// markerResult.put("opacity", marker.markerOptions.getAlpha()); +// markerResult.put("isFlat", marker.markerOptions.isFlat()); +// markerResult.put("isDraggable", marker.markerOptions.isDraggable()); + markerResult.put("metadata", new JSObject()); + + JSObject tag = (JSObject) marker.tag; + if (tag != null) { + // get and set markerId to marker + String markerId = tag.getString("id"); + markerResult.put("id", markerId); + + // get and set iconId to marker + int iconId = JSObjectDefaults.getIntegerSafe(tag, "iconId", -1); + markerResult.put("iconId", iconId); + + // get and set metadata to marker + try { + JSObject metadata = tag.getJSObject("metadata", new JSObject()); + markerResult.put("metadata", metadata); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + // return result + result.put("position", positionResult); + result.put("marker", markerResult); + + return result; + } + + public String getId() { + return id; + } + + @NonNull + @Override + public LatLng getPosition() { + return this.markerOptions.getPosition(); + } + + @Nullable + @Override + public String getTitle() { + return this.markerOptions.getTitle(); + } + + @Nullable + @Override + public String getSnippet() { + return this.markerOptions.getSnippet(); + } + + public MarkerCategory getMarkerCategory() { + return MarkerCategory.getMarkerCategoryById(this.markerCategoryId); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CustomMarker that = (CustomMarker) o; + return id.equals(that.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/MarkerCategory.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/MarkerCategory.java new file mode 100644 index 00000000..abd9c263 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/MarkerCategory.java @@ -0,0 +1,55 @@ +package com.hemangkumar.capacitorgooglemaps.marker; + +import android.graphics.Bitmap; + +import java.util.HashMap; +import java.util.Map; + +public class MarkerCategory { + + private int id; + private String title; + private Bitmap icon; + + static private final Map markerCategories = new HashMap(); + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Bitmap getIcon() { + return icon; + } + + public void setIcon(Bitmap icon) { + this.icon = icon; + } + + public MarkerCategory(int id, String title, Bitmap icon) { + this.id = id; + this.title = title; + this.icon = icon; + + markerCategories.put(this.id, this); + } + + public static MarkerCategory getMarkerCategoryById(int id) { + if(markerCategories.containsKey(id)) { + return markerCategories.get(id); + } else { + return markerCategories.get(0); + } + } +} diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/OwnIconRenderer.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/OwnIconRenderer.java new file mode 100644 index 00000000..5e7e6291 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/marker/OwnIconRenderer.java @@ -0,0 +1,168 @@ +package com.hemangkumar.capacitorgooglemaps.marker; + + +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; + +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.Marker; +import com.google.android.gms.maps.model.MarkerOptions; +import com.google.maps.android.clustering.Cluster; +import com.google.maps.android.clustering.ClusterManager; +import com.google.maps.android.clustering.view.DefaultClusterRenderer; +import com.google.maps.android.ui.IconGenerator; +import com.hemangkumar.capacitorgooglemaps.capacitorgooglemaps.R; + +import java.util.ArrayList; +import java.util.List; + + +public class OwnIconRenderer extends DefaultClusterRenderer { + + // -- constants -- + public static final int MIN_COUNT_ELEMENTS_IN_CLUSTER = 2; + + + // cluster sizes must be equal buckets count + private static final int[] CLUSTER_SIZES = { 64, 68, 72, 76, 80, 84, 88, 92 }; + private static final int[] BUCKETS = { 5, 10, 15, 20, 25, 30, 35, 40 }; + + + // == fields == + private final IconGenerator mIconGenerator; + private final ImageView mImageView; + private final int mDimension; + + private Context context; + private GoogleMap map; + + private List mClusterIconGenerators = new ArrayList<>(); + + // == constructor == + /** + * Draws profile photos inside markers (using IconGenerator). + * When there are multiple people in the cluster, draw multiple photos (using MultiDrawable). + */ + public OwnIconRenderer(Context context, Activity activity, GoogleMap map, + ClusterManager clusterManager) { + super(context, map, clusterManager); + this.context = context; + this.map = map; + + mIconGenerator = new IconGenerator(context); + + final float scale = context.getResources().getDisplayMetrics().density; + + for (int i = 0; i < BUCKETS.length; i++) { + int dps = CLUSTER_SIZES[i]; + int pixels = (int) (dps * scale + 0.5f); + + View multiProfile = activity.getLayoutInflater().inflate(R.layout.multi_profile, null); + ImageView mClusterImageView = multiProfile.findViewById(R.id.image); + mClusterImageView.setImageResource(R.drawable.cluster_icon); + + multiProfile.setLayoutParams(new ViewGroup.LayoutParams(pixels, pixels)); + + IconGenerator mClusterIconGenerator = new IconGenerator(context); + + mClusterIconGenerator.setContentView(multiProfile); + mClusterIconGenerator.setBackground(null); + + mClusterIconGenerators.add(mClusterIconGenerator); + } + + mImageView = new ImageView(context); + mDimension = (int) context.getResources().getDimension(R.dimen.custom_profile_image); + mImageView.setLayoutParams(new ViewGroup.LayoutParams(mDimension, mDimension)); + int padding = (int) context.getResources().getDimension(R.dimen.custom_profile_padding); + mImageView.setPadding(padding, padding, padding, padding); + mIconGenerator.setContentView(mImageView); + mIconGenerator.setBackground(null); + + } + + @Override + protected void onBeforeClusterItemRendered(@NonNull CustomMarker customMarker, + MarkerOptions markerOptions) { + // Draw a single marker - set the info window to show their name + markerOptions + .icon(getItemIcon(customMarker)) + .title(customMarker.getTitle()); + } + + + @Override + protected void onClusterItemUpdated(@NonNull CustomMarker customMarker, Marker marker) { + // Same implementation as onBeforeClusterItemRendered() (to update cached markers) + marker.setIcon(getItemIcon(customMarker)); + marker.setTitle(customMarker.getTitle()); + } + + + private BitmapDescriptor getItemIcon(CustomMarker customMarker) { + if(customMarker.getMarkerCategory().getIcon() == null) { + return null; + } + + + mImageView.setImageBitmap(customMarker.getMarkerCategory().getIcon()); + Bitmap icon = mIconGenerator.makeIcon(); + return BitmapDescriptorFactory.fromBitmap(icon); + } + + @Override + protected void onBeforeClusterRendered(@NonNull Cluster cluster, MarkerOptions markerOptions) { + // Note: this method runs on the UI thread. Don't spend too much time in here (like in this example). + markerOptions + .zIndex((int) map.getCameraPosition().zoom) + .icon(getClusterIcon(cluster)); + } + + + @Override + protected void onClusterUpdated(@NonNull Cluster cluster, Marker marker) { + // Same implementation as onBeforeClusterRendered() (to update cached markers) + marker.setIcon(getClusterIcon(cluster)); + } + + /** + * Note: this + * method runs on the UI thread. Don't spend too much time in here (like in this example). + * + * @param cluster cluster to draw a BitmapDescriptor for + * @return a BitmapDescriptor representing a cluster + */ + private BitmapDescriptor getClusterIcon(Cluster cluster) { + int index = getBucket(cluster.getSize()); + Bitmap icon = mClusterIconGenerators.get(index).makeIcon(String.valueOf(cluster.getSize())); + return BitmapDescriptorFactory.fromBitmap(icon); + } + + /** + * Gets the "bucket" for a particular cluster. By default, uses the number of + * points within the + * cluster, bucketed to some set points. + */ + private int getBucket(@NonNull int sizeOfCluster) { + int index = 0; + while ((index+1 < BUCKETS.length) && (BUCKETS[index+1] <= sizeOfCluster)) { + index++; + } + return index; + } + + @Override + protected boolean shouldRenderAsCluster(Cluster cluster) { + // Always render clusters. + return cluster.getSize() >= MIN_COUNT_ELEMENTS_IN_CLUSTER; + } + +} \ No newline at end of file diff --git a/android/src/main/java/com/hemangkumar/capacitorgooglemaps/utils/BoundingRect.java b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/utils/BoundingRect.java new file mode 100644 index 00000000..7e317a88 --- /dev/null +++ b/android/src/main/java/com/hemangkumar/capacitorgooglemaps/utils/BoundingRect.java @@ -0,0 +1,37 @@ +package com.hemangkumar.capacitorgooglemaps.utils; + +import androidx.annotation.Nullable; + +import com.getcapacitor.JSObject; + +public class BoundingRect { + public Integer width; + public Integer height; + public Integer x; + public Integer y; + + public BoundingRect() { + this.width = 500; + this.height = 500; + this.x = 0; + this.y = 0; + } + + public void updateFromJSObject(@Nullable JSObject jsObject) { + if (jsObject != null) { + if (jsObject.has("width")) { + this.width = jsObject.getInteger("width"); + } + if (jsObject.has("height")) { + this.height = jsObject.getInteger("height"); + } + if (jsObject.has("x")) { + this.x = jsObject.getInteger("x"); + } + if (jsObject.has("y")) { + this.y = jsObject.getInteger("y"); + } + } + } +} + diff --git a/android/src/main/res/drawable/cluster.png b/android/src/main/res/drawable/cluster.png new file mode 100644 index 00000000..43fcbab0 Binary files /dev/null and b/android/src/main/res/drawable/cluster.png differ diff --git a/android/src/main/res/drawable/cluster_icon.png b/android/src/main/res/drawable/cluster_icon.png new file mode 100644 index 00000000..bdba2e1c Binary files /dev/null and b/android/src/main/res/drawable/cluster_icon.png differ diff --git a/android/src/main/res/drawable/gran.jpg b/android/src/main/res/drawable/gran.jpg new file mode 100644 index 00000000..b2b4fcfc Binary files /dev/null and b/android/src/main/res/drawable/gran.jpg differ diff --git a/android/src/main/res/drawable/ic_launcher.png b/android/src/main/res/drawable/ic_launcher.png new file mode 100644 index 00000000..71ae1e72 Binary files /dev/null and b/android/src/main/res/drawable/ic_launcher.png differ diff --git a/android/src/main/res/drawable/john.jpg b/android/src/main/res/drawable/john.jpg new file mode 100644 index 00000000..131a3e4d Binary files /dev/null and b/android/src/main/res/drawable/john.jpg differ diff --git a/android/src/main/res/drawable/mechanic.jpg b/android/src/main/res/drawable/mechanic.jpg new file mode 100644 index 00000000..c8d26baf Binary files /dev/null and b/android/src/main/res/drawable/mechanic.jpg differ diff --git a/android/src/main/res/drawable/ruth.jpg b/android/src/main/res/drawable/ruth.jpg new file mode 100644 index 00000000..7a91fe2c Binary files /dev/null and b/android/src/main/res/drawable/ruth.jpg differ diff --git a/android/src/main/res/drawable/stefan.jpg b/android/src/main/res/drawable/stefan.jpg new file mode 100644 index 00000000..3926a344 Binary files /dev/null and b/android/src/main/res/drawable/stefan.jpg differ diff --git a/android/src/main/res/drawable/teacher.jpg b/android/src/main/res/drawable/teacher.jpg new file mode 100644 index 00000000..1147f664 Binary files /dev/null and b/android/src/main/res/drawable/teacher.jpg differ diff --git a/android/src/main/res/drawable/turtle.jpg b/android/src/main/res/drawable/turtle.jpg new file mode 100644 index 00000000..15fc2e96 Binary files /dev/null and b/android/src/main/res/drawable/turtle.jpg differ diff --git a/android/src/main/res/drawable/walter.jpg b/android/src/main/res/drawable/walter.jpg new file mode 100644 index 00000000..f1d34ec1 Binary files /dev/null and b/android/src/main/res/drawable/walter.jpg differ diff --git a/android/src/main/res/drawable/yeats.jpg b/android/src/main/res/drawable/yeats.jpg new file mode 100644 index 00000000..45651c31 Binary files /dev/null and b/android/src/main/res/drawable/yeats.jpg differ diff --git a/android/src/main/res/layout/multi_profile.xml b/android/src/main/res/layout/multi_profile.xml new file mode 100644 index 00000000..dd9ce2cd --- /dev/null +++ b/android/src/main/res/layout/multi_profile.xml @@ -0,0 +1,40 @@ + + + + + + + + + + diff --git a/android/src/main/res/values/dimensions.xml b/android/src/main/res/values/dimensions.xml new file mode 100644 index 00000000..b56c8236 --- /dev/null +++ b/android/src/main/res/values/dimensions.xml @@ -0,0 +1,21 @@ + + + + + 64dp + 0dp + diff --git a/ios/Plugin/Controllers/CustomMapViewController.swift b/ios/Plugin/Controllers/CustomMapViewController.swift new file mode 100644 index 00000000..59500841 --- /dev/null +++ b/ios/Plugin/Controllers/CustomMapViewController.swift @@ -0,0 +1,426 @@ +// +// CustomMapViewController.swift +// Capacitor +// +// Created by Admin on 27.12.2021. +// + +import Capacitor +import GoogleMaps +import GoogleMapsUtils +import UIKit + +let kClusterItemCount = 10000 + + +class CustomMapViewController: UIViewController, GMSMapViewDelegate { + + // -- constants -- + private var EPSILON : Double = 0.00001; + + var customMapViewEvents: CustomMapViewEvents!; + + var id: String!; + + var mapView: GMSMapView!; + + var locationButton : UIButton!; + + + + var boundingRect = BoundingRect(); + var mapCameraPosition = MapCameraPosition(); + var mapPreferences = MapPreferences(); + + + private var mClusterManager: GMUClusterManager! + private var mMarkersList: [CustomMarker]! + + + var savedCallbackIdForCreate: String!; + + var savedCallbackIdForDidTapInfoWindow: String!; + + var savedCallbackIdForDidCloseInfoWindow: String!; + + var savedCallbackIdForDidTapMap: String!; + + var savedCallbackIdForDidLongPressMap: String!; + + var savedCallbackIdForDidTapMarker: String!; + var preventDefaultForDidTapMarker: Bool = false; + + var savedCallbackIdForDidTapCluster: String!; + + var savedCallbackIdForDidTapMyLocationButton: String!; + var preventDefaultForDidTapMyLocationButton: Bool = false; + + var savedCallbackIdForDidTapMyLocationDot: String!; + + + private var widthOfTheMapCameraAnimationUnclustering : Double = 0; + private var heightOfTheMapCameraAnimationUnclustering : Double = 0; + + // This allows you to initialise your custom UIViewController without a nib or bundle. + convenience init(customMapViewEvents: CustomMapViewEvents) { + self.init(nibName:nil, bundle:nil) + self.customMapViewEvents = customMapViewEvents + self.id = NSUUID().uuidString.lowercased() + } + + + // This extends the superclass. + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + + override func loadView() { + let frame = CGRect(x: self.boundingRect.x, y: self.boundingRect.y, width: self.boundingRect.width, height: self.boundingRect.height); + + let camera = GMSCameraPosition.camera(withLatitude: self.mapCameraPosition.latitude, longitude: self.mapCameraPosition.longitude, zoom: self.mapCameraPosition.zoom, bearing: self.mapCameraPosition.bearing, viewingAngle: self.mapCameraPosition.tilt); + + self.mapView = GMSMapView.map(withFrame: frame, camera: camera); + + self.view = mapView; + + + widthOfTheMapCameraAnimationUnclustering = boundingRect.width * 0.25; + heightOfTheMapCameraAnimationUnclustering = boundingRect.height * 0.125; + } + + override func viewDidLoad() { + super.viewDidLoad(); + + setupClustering() + + mMarkersList = [CustomMarker](); + + // Register self to listen to GMSMapViewDelegate events. + mClusterManager.setMapDelegate(self) + + self.invalidateMap(); + + self.customMapViewEvents.lastResultForCallbackId(callbackId: savedCallbackIdForCreate, result: [ + "googleMap": [ + "mapId": self.id + ] + ]); + } + + private func setupClustering() { + guard let mapView = self.mapView else { return } + + let iconGenerator = CustomClusterIconGenerator() + let renderer = GMUDefaultClusterRenderer(mapView: mapView, clusterIconGenerator: iconGenerator) + renderer.delegate = self; + renderer.zIndex = 10; + let algorithm = GMUNonHierarchicalDistanceBasedAlgorithm() + self.mClusterManager = GMUClusterManager(map: mapView, algorithm: algorithm, renderer: renderer) + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + print("CustomMapViewController getting touches and event") +// var uir : UIResponder = UIResponder() +// uir.touchesBegan(touches, with: event) + + self.mapView.point(inside: (touches.first?.location(in: self.view))!, with: event) +// self.GMapView.touchesBegan(touches, with: event) +// super.touchesBegan(touches, with: event) + } + + + func invalidateMap() { + if (self.mapView == nil) { + return; + } + + // set gestures + self.mapView.settings.rotateGestures = self.mapPreferences.gestures.isRotateAllowed; + self.mapView.settings.scrollGestures = self.mapPreferences.gestures.isScrollAllowed; + self.mapView.settings.allowScrollGesturesDuringRotateOrZoom = self.mapPreferences.gestures.isScrollAllowedDuringRotateOrZoom; + self.mapView.settings.tiltGestures = self.mapPreferences.gestures.isTiltAllowed; + self.mapView.settings.zoomGestures = self.mapPreferences.gestures.isZoomAllowed; + + // set controls + self.mapView.settings.compassButton = self.mapPreferences.controls.isCompassButtonEnabled; + self.mapView.settings.indoorPicker = self.mapPreferences.controls.isIndoorLevelPickerEnabled; + self.mapView.settings.myLocationButton = self.mapPreferences.controls.isMyLocationButtonEnabled; + + // set appearance + self.mapView.mapType = self.mapPreferences.appearance.type; + self.mapView.mapStyle = self.mapPreferences.appearance.style; + self.mapView.isBuildingsEnabled = self.mapPreferences.appearance.isBuildingsShown; + self.mapView.isIndoorEnabled = self.mapPreferences.appearance.isIndoorShown; + self.mapView.isMyLocationEnabled = self.mapPreferences.appearance.isMyLocationDotShown; + self.mapView.isTrafficEnabled = self.mapPreferences.appearance.isTrafficShown; + + + // if we have location button then + // hide default button + if(self.mapView.isMyLocationEnabled == true) { + locationButton = getLocationButtonFromMapView(); + locationButton.isHidden = true; + } + } + + + public func setCallbackIdForEvent(callbackId: String!, eventName: String!, + preventDefault: Bool!) { + if (callbackId != nil && eventName != nil) { + switch eventName { + case Events.EVENT_DID_TAP_INFO_WINDOW: + savedCallbackIdForDidTapInfoWindow = callbackId; + case Events.EVENT_DID_CLOSE_INFO_WINDOW: + savedCallbackIdForDidCloseInfoWindow = callbackId; + case Events.EVENT_DID_TAP_MAP: + savedCallbackIdForDidTapMap = callbackId; + case Events.EVENT_DID_LONG_PRESS_MAP: + savedCallbackIdForDidLongPressMap = callbackId; + case Events.EVENT_DID_TAP_MARKER: + savedCallbackIdForDidTapMarker = callbackId + preventDefaultForDidTapMarker = preventDefault ?? false; + case Events.EVENT_DID_TAP_CLUSTER: + savedCallbackIdForDidTapCluster = callbackId + case Events.EVENT_DID_TAP_MY_LOCATION_BUTTON: + savedCallbackIdForDidTapMyLocationButton = callbackId; + preventDefaultForDidTapMyLocationButton = preventDefault ?? false; + case Events.EVENT_DID_TAP_MY_LOCATION_DOT: + savedCallbackIdForDidTapMyLocationDot = callbackId + default: + return + } + + } + + } + + public func clear() { + mapView.clear(); + mClusterManager.clearItems(); + } + + public func addMarker(_ customMarker: CustomMarker!) { + // TO-DO: Implement setting of custom icon + self.mClusterManager.add(customMarker); + self.mMarkersList.append(customMarker); + self.mClusterManager.cluster(); + } + + public func addMarkers(_ markerArray: [CustomMarker]!) { + self.mClusterManager.add(markerArray); + self.mMarkersList.append(contentsOf: markerArray); + self.mClusterManager.cluster(); + } + + public func updateMarker(_ markerId: String, _ preferences:JSObject) -> Bool { + for item in self.mMarkersList { + if(item.id == markerId) { + self.mClusterManager.remove(item); + item.updateFromJSObject(preferences: preferences); + self.mClusterManager.add(item); + self.mClusterManager.cluster(); + return true; + } + } + return false; + } + + public func removeMarker(_ markerId: String) { + var index : Int = 0; + for item in self.mMarkersList { + if(item.id == markerId) { + self.mClusterManager.remove(item); + self.mMarkersList.remove(at: index); + self.mClusterManager.cluster(); + return; + } + index += 1; + } + } + + + // MARK: GMSMapViewDelegate + + internal func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { + if (customMapViewEvents != nil && savedCallbackIdForDidTapInfoWindow != nil) { + let result: PluginCallResultData = CustomMarker.getResultForMarker(marker, self.id); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapInfoWindow, result: result); + } + } + + internal func mapView(_ mapView: GMSMapView, didCloseInfoWindowOf marker: GMSMarker) { + if (customMapViewEvents != nil && savedCallbackIdForDidCloseInfoWindow != nil) { + let result: PluginCallResultData = CustomMarker.getResultForMarker(marker, self.id); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidCloseInfoWindow, result: result); + } + } + + internal func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { + if (customMapViewEvents != nil && savedCallbackIdForDidTapMap != nil) { + let result: PluginCallResultData = self.getResultForPosition(coordinate: coordinate); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapMap, result: result); + } + } + + internal func mapView(_ mapView: GMSMapView, didLongPressAt coordinate: CLLocationCoordinate2D) { + if (customMapViewEvents != nil && savedCallbackIdForDidLongPressMap != nil) { + let result: PluginCallResultData = self.getResultForPosition(coordinate: coordinate); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidLongPressMap, result: result); + } + } + + internal func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { + // check if was tapped cluster + if let cluster = marker.userData as? GMUCluster { + NSLog("Did tap cluster") + var bounds : GMSCoordinateBounds = GMSCoordinateBounds() + for item in cluster.items { + bounds = bounds.includingCoordinate(item.position) + } + var getMarkersData: Bool = unclusteringCluster(bounds); + + if(customMapViewEvents != nil && savedCallbackIdForDidTapCluster != nil) { + let result: PluginCallResultData = CustomMapViewController.getResultForCluster(cluster, self.id, getMarkersData); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapCluster, result: result); + } + return true + } + + // if not than was tapped marker + NSLog("Did tap marker") + mapView.animate(toLocation: marker.position) + + if (customMapViewEvents != nil && savedCallbackIdForDidTapMarker != nil) { + let result: PluginCallResultData = CustomMarker.getResultForMarker(marker, self.id); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapMarker, result: result); + } + return preventDefaultForDidTapMarker; + } + + private func unclusteringCluster(_ bounds: GMSCoordinateBounds) -> Bool { + // check if cluster can uncluster + if((fabs(bounds.northEast.latitude - bounds.southWest.latitude) > EPSILON) && + (fabs(bounds.northEast.longitude - bounds.southWest.longitude) > EPSILON)) { + + + print("widthOfTheMapCameraAnimationUnclustering: \(widthOfTheMapCameraAnimationUnclustering)") + print("heightOfTheMapCameraAnimationUnclustering: \(heightOfTheMapCameraAnimationUnclustering)") + var update : GMSCameraUpdate = GMSCameraUpdate.fit(bounds, with: UIEdgeInsets.init( + top: heightOfTheMapCameraAnimationUnclustering, + left: widthOfTheMapCameraAnimationUnclustering/2, + bottom: heightOfTheMapCameraAnimationUnclustering/2, + right: widthOfTheMapCameraAnimationUnclustering)) + + mapView.animate(with: update) + return false; + } else { + // if all markers inside cluster is have same position + return true; + } + } + + internal func mapView(_ mapView: GMSMapView, didTapMyLocation coordinate: CLLocationCoordinate2D) { + if (customMapViewEvents != nil && savedCallbackIdForDidTapMyLocationDot != nil) { + let result: PluginCallResultData = self.getResultForPosition(coordinate: coordinate); + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapMyLocationDot, result: result); + } + } + + internal func didTapMyLocationButton(for mapView: GMSMapView) -> Bool { + if (customMapViewEvents != nil && savedCallbackIdForDidTapMyLocationButton != nil) { + customMapViewEvents.resultForCallbackId(callbackId: savedCallbackIdForDidTapMyLocationButton, result: nil); + } + return preventDefaultForDidTapMyLocationButton; + } + + private func getResultForPosition(coordinate: CLLocationCoordinate2D) -> PluginCallResultData { + return [ + "position": [ + "latitude": coordinate.latitude, + "longitude": coordinate.longitude + ] + ] + } + + private static func getResultForCluster(_ cluster : GMUCluster, _ mapId : String, _ getMarkersData : Bool) -> PluginCallResultData { + let clusterObj : GMUCluster = cluster; + + + // initialize JSObjects to return + var result: JSObject = JSObject(); + var positionResult = JSObject(); + + // get map id + positionResult = [ + "mapId" : mapId, + "latitude" : clusterObj.position.latitude, + "longitude" : clusterObj.position.longitude, + ] + + result["position"] = positionResult; + + if(getMarkersData) { + var markersData : JSObject = JSObject(); + for item in cluster.items { + let marker = item as! CustomMarker; + markersData[marker.id] = CustomMarker.getResultForMarker((item as! GMSMarker), mapId) as! JSObject + } + result["markersData"] = markersData; + } + + return result; + } + + + public func zoomIn() { + self.mapView.animate(toZoom: mapView.camera.zoom + 1); + } + + public func zoomOut() { + self.mapView.animate(toZoom: mapView.camera.zoom - 1); + } + + public func myLocationButtonClick() { + locationButton.sendActions(for: .touchUpInside); + } + + private func getLocationButtonFromMapView() -> UIButton { + for object in self.mapView.subviews { + if(String(describing: type(of: object)) == "GMSUISettingsPaddingView") { + for view in object.subviews { + if(String(describing: type(of: view)) == "GMSUISettingsView") { + for btn in view.subviews { + if(String(describing: type(of: btn)) == "GMSx_MDCFloatingButton") { + if btn is UIButton { + let button = btn as! UIButton; + return button; + } + } + } + } + } + } + } + return UIButton(); + } +} + + +extension CustomMapViewController: GMUClusterRendererDelegate { + func renderer(_ renderer: GMUClusterRenderer, willRenderMarker marker: GMSMarker) { + // if your marker is pointy you can change groundAnchor +// marker.groundAnchor = CGPoint(x: 0.5, y: 1) + if let markerData = (marker.userData as? CustomMarker) { + let icon = MarkerCategory.markerCategories[markerData.markerCategoryId]?.getIcon ?? nil; + marker.icon = icon; + marker.zIndex = 5; + } + } +} + + diff --git a/ios/Plugin/CustomMapViewEvents.swift b/ios/Plugin/CustomMapViewEvents.swift new file mode 100644 index 00000000..bb8afa0a --- /dev/null +++ b/ios/Plugin/CustomMapViewEvents.swift @@ -0,0 +1,7 @@ +import Foundation +import Capacitor + +public class CustomMapViewEvents: CAPPlugin { + func lastResultForCallbackId(callbackId: String, result: PluginCallResultData) {} + func resultForCallbackId(callbackId: String, result: PluginCallResultData?) {} +} diff --git a/ios/Plugin/Plugin.m b/ios/Plugin/Plugin.m index 6ea0e4cf..195936d2 100644 --- a/ios/Plugin/Plugin.m +++ b/ios/Plugin/Plugin.m @@ -5,26 +5,29 @@ // each method the plugin supports using the CAP_PLUGIN_METHOD macro. CAP_PLUGIN(CapacitorGoogleMaps, "CapacitorGoogleMaps", CAP_PLUGIN_METHOD(initialize, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(create, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(createMap, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(updateMap, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(clearMap, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(addMarker, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(setMapType, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(setMapStyle, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(setCamera, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(isIndoorEnabled, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(accessibilityElementsHidden, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(isMyLocationEnabled, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(padding, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(clear, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(isTrafficEnabled, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(close, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(addMarkers, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(updateMarker, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(removeMarker, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(didTapInfoWindow, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didCloseInfoWindow, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didTapMap, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didLongPressMap, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didTapMarker, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didTapCluster, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didTapMyLocationButton, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(didTapMyLocationDot, CAPPluginReturnCallback); + CAP_PLUGIN_METHOD(blockMapViews, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(unblockMapViews, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(getArrayOfHTMLElements, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(setArrayOfHTMLElements, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(zoomInButtonClick, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(zoomOutButtonClick, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(myLocationButtonClick, CAPPluginReturnPromise); + CAP_PLUGIN_METHOD(addMarkerCategory, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(hide, CAPPluginReturnPromise); CAP_PLUGIN_METHOD(show, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(reverseGeocodeCoordinate, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(settings, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(addPolyline, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(addPolygon, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(addCircle, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(enableCurrentLocation, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(myLocation, CAPPluginReturnPromise); - CAP_PLUGIN_METHOD(viewBounds, CAPPluginReturnPromise); ) diff --git a/ios/Plugin/Plugin.swift b/ios/Plugin/Plugin.swift index b60787be..b1e5afc7 100644 --- a/ios/Plugin/Plugin.swift +++ b/ios/Plugin/Plugin.swift @@ -1,720 +1,705 @@ import Foundation import Capacitor import GoogleMaps +import UIKit @objc(CapacitorGoogleMaps) -public class CapacitorGoogleMaps: CAPPlugin, GMSMapViewDelegate, GMSPanoramaViewDelegate { - +public class CapacitorGoogleMaps: CustomMapViewEvents { + + // --constants-- + let TAG_NUMBER_FOR_MAP_UIVIEW : Int = 10; + let TAG_NUMBER_FOR_TOP_OVERLAY_UIVIEW : Int = 20; + let TAG_NUMBER_FOR_DEFAULT_WEBVIEW_SUBVIEW_WITH_HTML_ELEMENTS : Int = 1; + let MARKER_CATEGORY_DIRECTORY : String = "marker-categories.bundle"; + let SIZE_OF_MARKERS = CGSize(width: 64.0, height: 64.0) + + var GOOGLE_MAPS_KEY: String = ""; - var mapViewController: GMViewController!; - var streetViewController: GMStreetViewController!; - var DEFAULT_ZOOM: Double = 12.0; + + var customMapViewControllers = [String : CustomMapViewController](); + + var hasTopView : Bool = false; + + var arrayOfHTMLElements = [BoundingRect](); + + + // class for view that will receive touches and transmit them + class OverlayView: UIView { + + public var mainClass: CapacitorGoogleMaps?; + + override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + + // make html view default off + mainClass?.bridge?.webView?.viewWithTag(mainClass!.TAG_NUMBER_FOR_DEFAULT_WEBVIEW_SUBVIEW_WITH_HTML_ELEMENTS)?.isUserInteractionEnabled = false; + + // checking the hit in the elements + for boundingRect in mainClass?.arrayOfHTMLElements ?? [] { + // checking the hit in the element + if(Bool(point.x > boundingRect.x) && + Bool(point.x < (boundingRect.x + boundingRect.width)) && + Bool(point.y > boundingRect.y) && + Bool(point.y < (boundingRect.y + boundingRect.height))) { + // touch point inside of html element + // then we make this subview is toucheable + mainClass?.bridge?.webView?.viewWithTag(mainClass!.TAG_NUMBER_FOR_DEFAULT_WEBVIEW_SUBVIEW_WITH_HTML_ELEMENTS)?.isUserInteractionEnabled = true + return false + } + } - var hashMap = [Int : GMSMarker](); + // if we here, than html view off + let arrayOfMaps = [CustomMapViewController]((mainClass?.customMapViewControllers.values)!) + + for mapview in arrayOfMaps { + // checking if map exists in this point + if(point.x > mapview.boundingRect.x && + point.x < (mapview.boundingRect.x + mapview.boundingRect.width) && + point.y > mapview.boundingRect.y && + point.y < (mapview.boundingRect.y + mapview.boundingRect.height)) { + // if mapview exist in this point than doing nothing + // just go further + return false + } else { + // if there is no mapView than we on html view + } + } + mainClass?.bridge?.webView?.viewWithTag(mainClass!.TAG_NUMBER_FOR_DEFAULT_WEBVIEW_SUBVIEW_WITH_HTML_ELEMENTS)?.isUserInteractionEnabled = true + return false + } + } + + @objc func initialize(_ call: CAPPluginCall) { - + + initMarkerCategories(); + self.GOOGLE_MAPS_KEY = call.getString("key", "") - + if self.GOOGLE_MAPS_KEY.isEmpty { call.reject("GOOGLE MAPS API key missing!") return } - + GMSServices.provideAPIKey(self.GOOGLE_MAPS_KEY) + call.resolve([ "initialized": true ]) } - - @objc func create(_ call: CAPPluginCall) { - - DispatchQueue.main.async { - self.mapViewController = GMViewController(); - self.mapViewController.mapViewBounds = [ - "width": call.getDouble("width") ?? 500, - "height": call.getDouble("height") ?? 500, - "x": call.getDouble("x") ?? 0, - "y": call.getDouble("y") ?? 0, - ] - self.mapViewController.cameraPosition = [ - "latitude": call.getDouble("latitude") ?? 0.0, - "longitude": call.getDouble("longitude") ?? 0.0, - "zoom": call.getDouble("zoom") ?? (self.DEFAULT_ZOOM) - ] - self.bridge?.viewController?.view.addSubview(self.mapViewController.view) - self.mapViewController.GMapView.delegate = self - self.notifyListeners("onMapReady", data: nil) - } - call.resolve([ - "created": true - ]) - } - - @objc func addMarker(_ call: CAPPluginCall) { - - let latitude = call.getDouble("latitude") ?? 0 - let longitude = call.getDouble("longitude") ?? 0 - let opacity = call.getFloat("opacity") ?? 1 - let title = call.getString("title") ?? "" - let snippet = call.getString("snippet") ?? "" - let isFlat = call.getBool("isFlat") ?? false - let metadata = call.getObject("metadata") ?? [:] - let url = URL(string: call.getString("iconUrl", "")) - var imageData: Data? - - DispatchQueue.global().async { - - if url != nil { - /* https://stackoverflow.com/a/27517280/5056792 */ - imageData = try? Data(contentsOf: url!) + + + + @objc func createMap(_ call: CAPPluginCall) { + + DispatchQueue.main.sync{ + let customMapViewController : CustomMapViewController = CustomMapViewController(customMapViewEvents: self); + + self.bridge?.saveCall(call) + customMapViewController.savedCallbackIdForCreate = call.callbackId; + + let boundingRect = call.getObject("boundingRect", JSObject()); + customMapViewController.boundingRect.updateFromJSObject(boundingRect); + + let mapCameraPosition = call.getObject("cameraPosition", JSObject()); + customMapViewController.mapCameraPosition.updateFromJSObject(mapCameraPosition); + + let preferences = call.getObject("preferences", JSObject()); + customMapViewController.mapPreferences.updateFromJSObject(preferences); + + + self.bridge?.webView?.subviews[0].tag = TAG_NUMBER_FOR_DEFAULT_WEBVIEW_SUBVIEW_WITH_HTML_ELEMENTS; + customMapViewController.view.tag = self.TAG_NUMBER_FOR_MAP_UIVIEW; + + // elements on the top of the mapView + // getting JSOobject + let elementsOnTop = call.getObject("elementsOnTop", JSObject()); + // getting two dimensional array of child elements + let twoDArray = elementsOnTop["innerEles"] as? [[Int]]; + for i in 0.. = [] - for address in response?.results() ?? [] { - let addr = [ - "administrativeArea": address.administrativeArea ?? "", - "lines": address.lines!, - "country": address.country ?? "", - "locality": address.locality ?? "", - "postalCode": address.postalCode ?? "", - "subLocality": address.subLocality ?? "", - "thoroughFare": address.thoroughfare ?? "" - ] as [String : Any] - addressList.append(addr) + guard let customMapViewController: CustomMapViewController = self.customMapViewControllers[mapId] + else { + call.reject("map not found"); + return } - - call.resolve([ - "addresses": addressList - ]) - } + self.locationRequest(); + customMapViewController.myLocationButtonClick(); + call.resolve(); + } } - - @objc func settings(_ call: CAPPluginCall) { - - let allowScrollGesturesDuringRotateOrZoom = call.getBool("allowScrollGesturesDuringRotateOrZoom") ?? true - let compassButton = call.getBool("compassButton") ?? false - let consumesGesturesInView = call.getBool("consumesGesturesInView") ?? true - let indoorPicker = call.getBool("indoorPicker") ?? false - let myLocationButton = call.getBool("myLocationButton") ?? false - let rotateGestures = call.getBool("rotateGestures") ?? true - let scrollGestures = call.getBool("scrollGestures") ?? true - let tiltGestures = call.getBool("tiltGestures") ?? true - let zoomGestures = call.getBool("zoomGestures") ?? true - - - DispatchQueue.main.async { - self.mapViewController.GMapView.settings.allowScrollGesturesDuringRotateOrZoom = allowScrollGesturesDuringRotateOrZoom - self.mapViewController.GMapView.settings.compassButton = compassButton - self.mapViewController.GMapView.settings.consumesGesturesInView = consumesGesturesInView - self.mapViewController.GMapView.settings.indoorPicker = indoorPicker - self.mapViewController.GMapView.settings.myLocationButton = myLocationButton - self.mapViewController.GMapView.settings.rotateGestures = rotateGestures - self.mapViewController.GMapView.settings.scrollGestures = scrollGestures - self.mapViewController.GMapView.settings.tiltGestures = tiltGestures - self.mapViewController.GMapView.settings.zoomGestures = zoomGestures - - call.resolve([ - "settingsApplied": true - ]) + + private func locationRequest() { + // this open app specific settings + func openSettings(alert: UIAlertAction!) { + if let url = URL.init(string: UIApplication.openSettingsURLString) { + UIApplication.shared.open(url, options: [:], completionHandler: nil) + } } - } - - @objc func setCamera(_ call: CAPPluginCall) { - - let viewingAngle = call.getDouble("viewingAngle") ?? 45 - let bearing = call.getDouble("bearing") ?? 270 - let zoom = call.getFloat("zoom") ?? 1 - let latitude = call.getDouble("latitude") ?? 0 - let longitude = call.getDouble("longitude") ?? 0 - - let animate = call.getBool("animate") ?? false - let animationDuration = call.getDouble("animationDuration") ?? 1 - - let coordinates = call.getArray("coordinates", AnyObject.self) - - DispatchQueue.main.async { - let camera = GMSCameraPosition(latitude: latitude, longitude: longitude, zoom: zoom, bearing: bearing, viewingAngle: viewingAngle) - - if animationDuration != 0 && animate { - /* FIXME: Use animation duration */ - if coordinates == nil { - self.mapViewController.GMapView.animate(to: camera) - call.resolve([ - "cameraSet": true - ]) - } else { - var gmsBounds = GMSCoordinateBounds() - for coordinate in coordinates ?? [] { - let coord = CLLocationCoordinate2D(latitude: coordinate["latitude"] as! CLLocationDegrees, longitude: coordinate["longitude"] as! CLLocationDegrees) - let marker = GMSMarker(position: coord) - gmsBounds = gmsBounds.includingCoordinate(marker.position) - } - CATransaction.begin() - CATransaction.setValue(animationDuration, forKey: kCATransactionAnimationDuration) - CATransaction.setCompletionBlock({ - call.resolve([ - "cameraSet": true - ]) - }) - self.mapViewController.GMapView.animate(with: GMSCameraUpdate.fit(gmsBounds, withPadding: 30.0)) - CATransaction.commit() + + + + let status = CLLocationManager.authorizationStatus() + if status == .notDetermined || status == .denied{ + + // present an alert indicating location authorization required + // and offer to take the user to Settings for the app via + // UIApplication -openUrl: and UIApplicationOpenSettingsURLString + DispatchQueue.main.async { + let title : String?; + let message : String?; + let titleOfSecondAction : String; + if(CLLocationManager.locationServicesEnabled() != true) { + title = "Location Services Off" + message = "Turn on Location Services in Settings > Privacy to allow \"" + Bundle.appName() + "\" to determine your current location"; + titleOfSecondAction = "OK"; + } else { + title = "GPS access is restricted. In order to Allow \"" + Bundle.appName() + "\" to Determine Your Location, please give GPS permissions to application"; + message = nil + titleOfSecondAction = "Cancel"; + } + + + let alert = UIAlertController(title: title, + message: message, + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: "Settings", + style: UIAlertAction.Style.default, + handler: openSettings)) + + alert.addAction(UIAlertAction(title: titleOfSecondAction, style: UIAlertAction.Style.default, handler: nil)) + self.bridge?.webView?.window?.rootViewController?.present(alert, animated: true, completion: nil) } - } else { - self.mapViewController.GMapView.camera = camera - call.resolve([ - "cameraSet": true - ]) } - } - } - - @objc func setMapStyle(_ call: CAPPluginCall) { - let styleJsonString = call.getString("jsonString") ?? "" - - DispatchQueue.main.async { - do { - self.mapViewController.GMapView.mapStyle = try GMSMapStyle(jsonString: styleJsonString) - call.resolve([ - "stylesApplied": true - ]) - } catch { - call.reject("One or more of the map styles failed to load. \(error)") - } - + + @objc func addMarkerCategory(_ call: CAPPluginCall) { + let id : Int = call.getInt("id", -1); + let title : String = call.getString("title", ""); + let encodedImage : String = call.getString("base64Data", ""); + + if (id == -1 ) { + call.reject("dont have id for category"); + return; } - } - - @objc func addPolyline(_ call: CAPPluginCall) { - - let points = call.getArray("points", AnyObject.self) - - DispatchQueue.main.async { - - let path = GMSMutablePath() - - for point in points ?? [] { - let coords = CLLocationCoordinate2D(latitude: point["latitude"] as! CLLocationDegrees, longitude: point["longitude"] as! CLLocationDegrees) - path.add(coords) - } - - let polyline = GMSPolyline(path: path) - - polyline.map = self.mapViewController.GMapView - call.resolve([ - "created": true - ]) + + var image : UIImage = UIImage(); + if let decodedData = Data(base64Encoded: encodedImage, options: .ignoreUnknownCharacters) { + image = UIImage(data: decodedData)! + image = image.resized(to: SIZE_OF_MARKERS); } - } - - @objc func addPolygon(_ call: CAPPluginCall) { - - let points = call.getArray("points", AnyObject.self) - - DispatchQueue.main.async { - - let path = GMSMutablePath() - - for point in points ?? [] { - let coords = CLLocationCoordinate2D(latitude: point["latitude"] as! CLLocationDegrees, longitude: point["longitude"] as! CLLocationDegrees) - path.add(coords) - } - - let polygon = GMSPolygon(path: path) - polygon.map = self.mapViewController.GMapView - - call.resolve([ - "created": true - ]) + + MarkerCategory(id, title, image); + + } + + + private func initMarkerCategories() { + // default marker icon for zero category + MarkerCategory(0, "default", nil); + + // getting map of names of categories and icons of this + var markerCategoriesNamesAndIcons = fetchMarkersCategoriesFilesFromAssets(); + // sorting keys in alphabetical order for adding catetories in the same order + var arrayOfKeys = Array(markerCategoriesNamesAndIcons.keys.map{ $0 }) + arrayOfKeys = arrayOfKeys.sorted(by: <) + + var i : Int = 1; + for nameOfCategory in arrayOfKeys { + MarkerCategory(i, nameOfCategory, markerCategoriesNamesAndIcons[nameOfCategory] as? UIImage ?? nil) + i += 1; } - } - - @objc func addCircle(_ call: CAPPluginCall) { - - let radius = call.getDouble("radius") ?? 0.0 - - let center = call.getObject("center") - - let coordinates = CLLocationCoordinate2D(latitude: center?["latitude"] as! CLLocationDegrees, longitude: center?["longitude"] as! CLLocationDegrees) - + + } + + private func fetchMarkersCategoriesFilesFromAssets() -> [String : UIImage?] { + var returnArray = [String : UIImage?](); + + let fm = FileManager.default + let listImageName = fm.getListFileNameInBundle(bundlePath: MARKER_CATEGORY_DIRECTORY) + for imgName in listImageName { + + let pattern = #"(.+?)(\.[^.]*$|$)"# + let regex = try! NSRegularExpression(pattern: pattern) + var result: [String] = [String](); + let stringRange = NSRange(location: 0, length: imgName.utf16.count) + if let firstMatch = regex.firstMatch(in: imgName, range: stringRange) { + result = (1 ..< firstMatch.numberOfRanges).map { (imgName as NSString).substring(with: firstMatch.range(at: $0)) } + } else { + result[0] = imgName; + } + + + + var image = fm.getImageInBundle(bundlePath: MARKER_CATEGORY_DIRECTORY + "/\(result[0])") + image = image?.resized(to: SIZE_OF_MARKERS) + returnArray[imgName] = image; + } + return returnArray; + } + + + @objc func hide(_ call: CAPPluginCall) { + let mapId: String = call.getString("mapId", ""); + DispatchQueue.main.async { - - let circleCenter = coordinates - let circle = GMSCircle(position: circleCenter, radius: radius) - circle.map = self.mapViewController.GMapView - - call.resolve([ - "created": true - ]) + guard let customMapViewController: CustomMapViewController = self.customMapViewControllers[mapId] + else { + call.reject("map not found"); + return + } + customMapViewController.mapView.isHidden = true; + call.resolve(); } } - - - public func mapView(_ mapView: GMSMapView, didTapPOIWithPlaceID placeID: String, name: String, location: CLLocationCoordinate2D) { - self.notifyListeners("didTapPOIWithPlaceID", data: [ - "results": [ - "name": name, - "placeID": placeID, - "location": [ - "latitude": location.latitude, - "longitude": location.longitude - ] - ] - ]) - } - - - public func mapView(_ mapView: GMSMapView, didLongPressAt coordinate: CLLocationCoordinate2D) { - self.notifyListeners("didLongPressAt", data: ["result": [ - "coordinates": [ - "latitude": coordinate.latitude, - "longitude": coordinate.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didTapInfoWindowOf marker: GMSMarker) { - self.notifyListeners("didTapInfoWindowOf", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didLongPressInfoWindowOf marker: GMSMarker) { - self.notifyListeners("didLongPressInfoWindowOf", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didCloseInfoWindowOf marker: GMSMarker) { - self.notifyListeners("didCloseInfoWindowOf", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didTap marker: GMSMarker) -> Bool { - self.notifyListeners("didTap", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ], - "id": marker.hash.hashValue, - "title": marker.title ?? "", - "snippet": marker.snippet ?? "", - "metadata": marker.userData - ]]) - return false - } - - public func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) { - self.notifyListeners("didBeginDragging", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) { - self.notifyListeners("didEndDragging", data: ["result": [ - "coordinates": [ - "latitude": marker.position.latitude, - "longitude": marker.position.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, idleAt position: GMSCameraPosition) { - self.notifyListeners("idleAt", data: ["result": [ - "position": [ - "latitude": position.target.latitude, - "longitude": position.target.longitude - ], - "zoom": position.zoom, - ]]) - } - - - public func didTapMyLocationButton(for mapView: GMSMapView) -> Bool { - self.notifyListeners("didTapMyLocationButton", data: ["value": true]) - /* - TODO: Add animation to user's current location - */ - return false - } - - public func mapView(_ mapView: GMSMapView, didTapAt coordinate: CLLocationCoordinate2D) { - self.notifyListeners("didTapAt", data: ["result": [ - "coordinates": [ - "latitude": coordinate.latitude, - "longitude": coordinate.longitude - ] - ]]) - } - - public func mapView(_ mapView: GMSMapView, didChange position: GMSCameraPosition) { - self.notifyListeners("didChange", data: ["result": [ - "position": [ - "latitude": position.target.latitude, - "longitude": position.target.longitude - ], - "zoom": position.zoom, - ]]) - } - - // Street View - @objc func createStreetView(_ call: CAPPluginCall) { - + + @objc func show(_ call: CAPPluginCall) { + let mapId: String = call.getString("mapId", ""); + DispatchQueue.main.async { - self.streetViewController = GMStreetViewController(); - self.streetViewController.mapViewBounds = [ - "width": call.getDouble("width") ?? 0, - "height": call.getDouble("height") ?? 0, - "x": call.getDouble("x") ?? 0, - "y": call.getDouble("y") ?? 0, - ] - self.mapViewController.cameraPosition = [ - "heading": call.getDouble("heading") ?? 180, - "pitch": call.getDouble("pitch") ?? -10, - "zoom": call.getDouble("zoom") ?? Double(self.DEFAULT_ZOOM) - ] - self.bridge?.viewController?.view.addSubview(self.streetViewController.view) - self.streetViewController.GMapStreetView.delegate = self + guard let customMapViewController: CustomMapViewController = self.customMapViewControllers[mapId] + else { + call.reject("map not found"); + return + } + customMapViewController.mapView.isHidden = false; + call.resolve(); } - call.resolve([ - "created": true - ]) } + +} - @objc func moveNearCoordinate(_ call: CAPPluginCall) { - let latitude = call.getDouble("latitude") ?? 0 - let longitude = call.getDouble("longitude") ?? 0 - - let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - - let radius = UInt(call.getInt("radius") ?? 0) - - DispatchQueue.main.async { - self.streetViewController.GMapStreetView.moveNearCoordinate(coordinate, radius: (radius)) +extension Bundle { + static func appName() -> String { + guard let dictionary = Bundle.main.infoDictionary else {return ""} + if let appName : String = dictionary["CFBundleName"] as? String { + return appName + } else { + return "" } - - call.resolve([ - "movedNearCoordinates": true - ]) } +} - @objc func setStreetViewCamera(_ call: CAPPluginCall) { - let heading = call.getDouble("heading") ?? 180 - let pitch = call.getDouble("pitch") ?? -10 - let zoom = call.getFloat("zoom") ?? 1 - - let animationDuration = call.getDouble("animationDuration") ?? 0 - let camera = GMSPanoramaCamera(heading: heading, pitch: pitch, zoom: zoom) +extension FileManager { + func getListFileNameInBundle(bundlePath: String) -> [String] { - DispatchQueue.main.async { - if animationDuration != 0 { - self.streetViewController.GMapStreetView.animate(to: camera, animationDuration: animationDuration) - } else { - self.streetViewController.GMapStreetView.camera = camera - } + let fileManager = FileManager.default + let bundleURL = Bundle.main.bundleURL + let assetURL = bundleURL.appendingPathComponent(bundlePath) + do { + let contents = try fileManager.contentsOfDirectory(at: assetURL, includingPropertiesForKeys: [URLResourceKey.nameKey, URLResourceKey.isDirectoryKey], options: .skipsHiddenFiles) + return contents.map{$0.lastPathComponent} } - - call.resolve([ - "cameraSet": true - ]) - } - - @objc func addStreetViewMarker(_ call: CAPPluginCall) { - let latitude = call.getDouble("latitude") ?? 0 - let longitude = call.getDouble("longitude") ?? 0 - - DispatchQueue.main.async { - let marker = GMSMarker() - marker.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) - marker.title = call.getString("markerTitle") ?? "" - marker.snippet = call.getString("markerSnippet") ?? "" - marker.panoramaView = self.streetViewController.GMapStreetView + catch { + return [] } - - call.resolve([ - "markerAdded": true - ]) } -} - -class GMViewController: UIViewController { - - var mapViewBounds: [String : Double]! - var GMapView: GMSMapView! - var cameraPosition: [String: Double]! - - var DEFAULT_ZOOM: Float = 12 - - override func viewDidLoad() { - super.viewDidLoad() - let camera = GMSCameraPosition.camera(withLatitude: cameraPosition["latitude"] ?? 0, longitude: cameraPosition["longitude"] ?? 0, zoom: Float(cameraPosition["zoom"] ?? Double(DEFAULT_ZOOM))) - let frame = CGRect(x: mapViewBounds["x"] ?? 0, y: mapViewBounds["y"]!, width: mapViewBounds["width"] ?? 0, height: mapViewBounds["height"] ?? 0) - self.GMapView = GMSMapView.map(withFrame: frame, camera: camera) - self.view = GMapView + func getImageInBundle(bundlePath: String) -> UIImage? { + let bundleURL = Bundle.main.bundleURL + let assetURL = bundleURL.appendingPathComponent(bundlePath) + return UIImage.init(contentsOfFile: assetURL.relativePath) } } -class GMStreetViewController: UIViewController { - - var mapViewBounds: [String : Double]! - var GMapStreetView: GMSPanoramaView! - var cameraPosition: [String: Double]! - - var DEFAULT_HEADING: Double = 180 - var DEFAULT_PITCH: Double = -10 - var DEFAULT_ZOOM: Double = 12 - - override func viewDidLoad() { - super.viewDidLoad() - let camera = GMSPanoramaCamera( - heading: cameraPosition["heading"] ?? (DEFAULT_HEADING), - pitch: cameraPosition["pitch"] ?? (DEFAULT_PITCH), - zoom: Float(cameraPosition["zoom"] ?? (DEFAULT_ZOOM))) - let frame = CGRect(x: mapViewBounds["x"] ?? 0, y: mapViewBounds["y"]!, width: mapViewBounds["width"] ?? 0, height: mapViewBounds["height"] ?? 0) - self.GMapStreetView = GMSPanoramaView(frame: frame) - self.GMapStreetView.camera = camera - self.view = self.GMapStreetView +extension UIImage { + public func resized(to target: CGSize) -> UIImage { + let ratio = min( + target.height / size.height, target.width / size.width + ) + let new = CGSize( + width: size.width * ratio, height: size.height * ratio + ) + let renderer = UIGraphicsImageRenderer(size: new) + return renderer.image { _ in + self.draw(in: CGRect(origin: .zero, size: new)) + } } } diff --git a/ios/Plugin/maps-utils/BoundingRect.swift b/ios/Plugin/maps-utils/BoundingRect.swift new file mode 100644 index 00000000..b1a3dc8d --- /dev/null +++ b/ios/Plugin/maps-utils/BoundingRect.swift @@ -0,0 +1,43 @@ +import Capacitor + +class BoundingRect { + public static let WIDTH_KEY: String! = "width"; + public static let HEIGHT_KEY: String! = "height"; + public static let X_KEY: String! = "x"; + public static let Y_KEY: String! = "y"; + + private static let WIDTH_DEFAULT: Double! = 500.0; + private static let HEIGHT_DEFAULT: Double! = 500.0; + private static let X_DEFAULT: Double! = 0.0; + private static let Y_DEFAULT: Double! = 0.0; + + public var width: Double!; + public var height: Double!; + public var x: Double!; + public var y: Double!; + + public init () { + self.width = BoundingRect.WIDTH_DEFAULT; + self.height = BoundingRect.HEIGHT_DEFAULT; + self.x = BoundingRect.X_DEFAULT; + self.y = BoundingRect.Y_DEFAULT; + } + + func updateFromJSObject(_ object: JSObject) { + self.width = object[BoundingRect.WIDTH_KEY] as? Double ?? BoundingRect.WIDTH_DEFAULT; + self.height = object[BoundingRect.HEIGHT_KEY] as? Double ?? BoundingRect.HEIGHT_DEFAULT; + self.x = object[BoundingRect.X_KEY] as? Double ?? BoundingRect.X_DEFAULT; + self.y = object[BoundingRect.Y_KEY] as? Double ?? BoundingRect.Y_DEFAULT; + } + + func getJSObject() -> JSObject { + + var rectangleAsJSObject = JSObject(); + rectangleAsJSObject.updateValue(self.width, forKey: BoundingRect.WIDTH_KEY); + rectangleAsJSObject.updateValue(self.height, forKey: BoundingRect.HEIGHT_KEY); + rectangleAsJSObject.updateValue(self.x, forKey: BoundingRect.X_KEY); + rectangleAsJSObject.updateValue(self.y, forKey: BoundingRect.Y_KEY); + + return rectangleAsJSObject; + } +} diff --git a/ios/Plugin/maps-utils/MapCameraPosition.swift b/ios/Plugin/maps-utils/MapCameraPosition.swift new file mode 100644 index 00000000..63dd93d1 --- /dev/null +++ b/ios/Plugin/maps-utils/MapCameraPosition.swift @@ -0,0 +1,40 @@ +import Capacitor + +class MapCameraPosition { + public static let TARGET_KEY: String! = "target"; + public static let LATITUDE_KEY: String! = "latitude"; + public static let LONGITUDE_KEY: String! = "longitude"; + public static let BEARING_KEY: String! = "bearing"; + public static let TILT_KEY: String! = "tilt"; + public static let ZOOM_KEY: String! = "zoom"; + + private static let TARGET_DEFAULT: JSObject! = JSObject(); + private static let LATITUDE_DEFAULT: Double! = 0.0; + private static let LONGITUDE_DEFAULT: Double! = 0.0; + private static let BEARING_DEFAULT: Double! = 0.0; + private static let TILT_DEFAULT: Double! = 0.0; + private static let ZOOM_DEFAULT: Float! = 12.0; + + public var latitude: Double!; + public var longitude: Double!; + public var bearing: Double!; + public var tilt: Double!; + public var zoom: Float!; + + public init () { + self.latitude = MapCameraPosition.LATITUDE_DEFAULT; + self.longitude = MapCameraPosition.LONGITUDE_DEFAULT; + self.bearing = MapCameraPosition.BEARING_DEFAULT; + self.tilt = MapCameraPosition.TILT_DEFAULT; + self.zoom = MapCameraPosition.ZOOM_DEFAULT; + } + + func updateFromJSObject(_ object: JSObject) { + let target: JSObject = object[MapCameraPosition.TARGET_KEY] as? JSObject ?? MapCameraPosition.TARGET_DEFAULT; + self.latitude = target[MapCameraPosition.LATITUDE_KEY] as? Double ?? MapCameraPosition.LATITUDE_DEFAULT; + self.longitude = target[MapCameraPosition.LONGITUDE_KEY] as? Double ?? MapCameraPosition.LONGITUDE_DEFAULT; + self.bearing = object[MapCameraPosition.BEARING_KEY] as? Double ?? MapCameraPosition.BEARING_DEFAULT; + self.tilt = object[MapCameraPosition.TILT_KEY] as? Double ?? MapCameraPosition.TILT_DEFAULT; + self.zoom = object[MapCameraPosition.ZOOM_KEY] as? Float ?? MapCameraPosition.ZOOM_DEFAULT; + } +} diff --git a/ios/Plugin/maps-utils/MapPreferences.swift b/ios/Plugin/maps-utils/MapPreferences.swift new file mode 100644 index 00000000..81bf0420 --- /dev/null +++ b/ios/Plugin/maps-utils/MapPreferences.swift @@ -0,0 +1,27 @@ +import Capacitor + +class MapPreferences { + public var gestures: MapPreferencesGestures! + public var controls: MapPreferencesControls! + public var appearance: MapPreferencesAppearance! + + public init() { + self.gestures = MapPreferencesGestures(); + self.controls = MapPreferencesControls(); + self.appearance = MapPreferencesAppearance(); + } + + open func updateFromJSObject(_ preferences: JSObject!) { + if preferences != nil { + // update gestures + let gesturesObject: JSObject! = preferences["gestures"] as? JSObject ?? JSObject(); + self.gestures.updateFromJSObject(object: gesturesObject); + // update controls + let controlsObject: JSObject! = preferences["controls"] as? JSObject ?? JSObject(); + self.controls.updateFromJSObject(object: controlsObject); + // update appearance + let appearanceObject: JSObject! = preferences["appearance"] as? JSObject ?? JSObject(); + self.appearance.updateFromJSObject(object: appearanceObject); + } + } +} diff --git a/ios/Plugin/maps-utils/MapPreferencesAppearance.swift b/ios/Plugin/maps-utils/MapPreferencesAppearance.swift new file mode 100644 index 00000000..3ce5094b --- /dev/null +++ b/ios/Plugin/maps-utils/MapPreferencesAppearance.swift @@ -0,0 +1,94 @@ +import Capacitor +import GoogleMaps + +class MapPreferencesAppearance { + public static let TYPE_KEY: String! = "type" + public static let STYLE_KEY: String! = "style" + public static let BUILDINGS_SHOWN_KEY: String! = "isBuildingsShown" + public static let INDOOR_SHOWN_KEY: String! = "isIndoorShown" + public static let MY_LOCATION_DOT_SHOWN_KEY: String! = "isMyLocationDotShown" + public static let TRAFFIC_SHOWN_KEY: String! = "isTrafficShown" + + var type: GMSMapViewType = GMSMapViewType.normal; + + var style: GMSMapStyle? = nil; + + var _isBuildingsShown: Bool = true + var isBuildingsShown: Bool! { + get { + return _isBuildingsShown + } + set (newVal) { + _isBuildingsShown = newVal ?? true + } + } + + var _isIndoorShown: Bool = true + var isIndoorShown: Bool! { + get { + return _isIndoorShown + } + set (newVal) { + _isIndoorShown = newVal ?? true + } + } + + var _isMyLocationDotShown: Bool = false + var isMyLocationDotShown: Bool! { + get { + return _isMyLocationDotShown + } + set (newVal) { + _isMyLocationDotShown = newVal ?? false + } + } + + var _isTrafficShown: Bool = false + var isTrafficShown: Bool! { + get { + return _isTrafficShown + } + set (newVal) { + _isTrafficShown = newVal ?? false + } + } + + func updateFromJSObject(object: JSObject) { + if (object[MapPreferencesAppearance.TYPE_KEY] != nil) { + let type = object[MapPreferencesAppearance.TYPE_KEY] as? Int; + if (type != nil) { + if (type == 0) { + self.type = GMSMapViewType.none; + } else if (type == 1) { + self.type = GMSMapViewType.normal; + } else if (type == 2) { + self.type = GMSMapViewType.satellite; + } else if (type == 3) { + self.type = GMSMapViewType.terrain; + } else if (type == 4) { + self.type = GMSMapViewType.hybrid; + } else { + self.type = GMSMapViewType.normal; + } + } + } + + if (object[MapPreferencesAppearance.STYLE_KEY] != nil) { + let style = object[MapPreferencesAppearance.STYLE_KEY] as? String; + if (style == nil) { + self.style = nil; + } else { + do { + try self.style = GMSMapStyle(jsonString: style ?? "{}"); + } catch { + print("error parsing style json string"); + } + } + } + + self.isBuildingsShown = object[MapPreferencesAppearance.BUILDINGS_SHOWN_KEY] as? Bool; + self.isIndoorShown = object[MapPreferencesAppearance.INDOOR_SHOWN_KEY] as? Bool; + self.isMyLocationDotShown = object[MapPreferencesAppearance.MY_LOCATION_DOT_SHOWN_KEY] as? Bool; + self.isTrafficShown = object[MapPreferencesAppearance.TRAFFIC_SHOWN_KEY] as? Bool; + } +} diff --git a/ios/Plugin/maps-utils/MapPreferencesControls.swift b/ios/Plugin/maps-utils/MapPreferencesControls.swift new file mode 100644 index 00000000..f18d1ff8 --- /dev/null +++ b/ios/Plugin/maps-utils/MapPreferencesControls.swift @@ -0,0 +1,45 @@ +import Capacitor + +class MapPreferencesControls { + public static let COMPASS_BUTTON_KEY: String! = "isCompassButtonEnabled" + public static let INDOOR_LEVEL_PICKER_KEY: String! = "isIndoorLevelPickerEnabled" + // (android only) public static let MAP_TOOLBAR_KEY: String! = "isMapToolbarEnabled" + public static let MY_LOCATION_BUTTON_KEY: String! = "isMyLocationButtonEnabled" + // (android only) public static let ZOOM_BUTTONS_KEY: String! = "isZoomButtonsEnabled" + + var _isCompassButtonEnabled: Bool = true + var isCompassButtonEnabled: Bool! { + get { + return _isCompassButtonEnabled + } + set (newVal) { + _isCompassButtonEnabled = newVal ?? true + } + } + + var _isIndoorLevelPickerEnabled: Bool = false + var isIndoorLevelPickerEnabled: Bool! { + get { + return _isIndoorLevelPickerEnabled + } + set (newVal) { + _isIndoorLevelPickerEnabled = newVal ?? false + } + } + + var _isMyLocationButtonEnabled: Bool = true + var isMyLocationButtonEnabled: Bool! { + get { + return _isMyLocationButtonEnabled + } + set (newVal) { + _isMyLocationButtonEnabled = newVal ?? true + } + } + + func updateFromJSObject(object: JSObject) { + self.isCompassButtonEnabled = object[MapPreferencesControls.COMPASS_BUTTON_KEY] as? Bool; + self.isIndoorLevelPickerEnabled = object[MapPreferencesControls.INDOOR_LEVEL_PICKER_KEY] as? Bool; + self.isMyLocationButtonEnabled = object[MapPreferencesControls.MY_LOCATION_BUTTON_KEY] as? Bool; + } +} diff --git a/ios/Plugin/maps-utils/MapPreferencesGestures.swift b/ios/Plugin/maps-utils/MapPreferencesGestures.swift new file mode 100644 index 00000000..5a49e86e --- /dev/null +++ b/ios/Plugin/maps-utils/MapPreferencesGestures.swift @@ -0,0 +1,67 @@ +import Capacitor + +class MapPreferencesGestures { + public static let ROTATE_ALLOWED_KEY: String! = "isRotateAllowed" + public static let SCROLL_ALLOWED_KEY: String! = "isScrollAllowed" + public static let SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY: String! = "isScrollAllowedDuringRotateOrZoom" + public static let TILT_ALLOWED_KEY: String! = "isTiltAllowed" + public static let ZOOM_ALLOWED_KEY: String! = "isZoomAllowed" + + var _isRotateAllowed: Bool = true + var isRotateAllowed: Bool! { + get { + return _isRotateAllowed + } + set (newVal) { + _isRotateAllowed = newVal ?? true + } + } + + var _isScrollAllowed: Bool = true + var isScrollAllowed: Bool! { + get { + return _isScrollAllowed + } + set (newVal) { + _isScrollAllowed = newVal ?? true + } + } + + var _isScrollAllowedDuringRotateOrZoom: Bool = true + var isScrollAllowedDuringRotateOrZoom: Bool! { + get { + return _isScrollAllowedDuringRotateOrZoom + } + set (newVal) { + _isScrollAllowedDuringRotateOrZoom = newVal ?? true + } + } + + var _isTiltAllowed: Bool = true + var isTiltAllowed: Bool! { + get { + return _isTiltAllowed + } + set (newVal) { + _isTiltAllowed = newVal ?? true + } + } + + var _isZoomAllowed: Bool = true + var isZoomAllowed: Bool! { + get { + return _isZoomAllowed + } + set (newVal) { + _isZoomAllowed = newVal ?? true + } + } + + func updateFromJSObject(object: JSObject) { + self.isRotateAllowed = object[MapPreferencesGestures.ROTATE_ALLOWED_KEY] as? Bool; + self.isScrollAllowed = object[MapPreferencesGestures.SCROLL_ALLOWED_KEY] as? Bool; + self.isScrollAllowedDuringRotateOrZoom = object[MapPreferencesGestures.SCROLL_ALLOWED_DURING_ROTATE_OR_ZOOM_KEY] as? Bool; + self.isScrollAllowed = object[MapPreferencesGestures.TILT_ALLOWED_KEY] as? Bool; + self.isScrollAllowed = object[MapPreferencesGestures.ZOOM_ALLOWED_KEY] as? Bool; + } +} diff --git a/ios/Plugin/marker/CustomClusterIconGenerator.swift b/ios/Plugin/marker/CustomClusterIconGenerator.swift new file mode 100644 index 00000000..ebaefff4 --- /dev/null +++ b/ios/Plugin/marker/CustomClusterIconGenerator.swift @@ -0,0 +1,74 @@ +// +// MapClusterIconGenerator.swift +// CapacitorCommunityCapacitorGooglemapsNative +// +// Created by Admin on 24.01.2022. +// + +import GoogleMapsUtils +import UIKit + +class CustomClusterIconGenerator: GMUDefaultClusterIconGenerator { + + // clusterSizes count must be equal buckets count + let clusterSizes: [Int] = [64, 68, 72, 76, 80, 84, 88, 92]; + let buckets: [NSNumber] = [5, 10, 15, 20, 25, 30, 35, 40]; + + var backgroundImages : [UIImage]; + + override init() { + + backgroundImages = [UIImage](repeating: UIImage(), count: clusterSizes.count); + var index = 0; + for size in clusterSizes { + backgroundImages[index] = (UIImage(named: "cluster")? + .resized(to: CGSize(width: size, height: size)))!; + index += 1; + } + super.init(); + } + + override func icon(forSize size: UInt) -> UIImage { + let bucketIndex : Int = bucketIndex(forSize: Int(size)); + var text : String = String(size); + + if(backgroundImages != nil) { + let image : UIImage = backgroundImages[bucketIndex] + return textToImage(drawText: text as NSString, inImage: image, font: UIFont.systemFont(ofSize: 14)) + } else { + let image = UIImage(named: "cluster"); + return textToImage(drawText: text as NSString, inImage: image!, font: UIFont.systemFont(ofSize: 14)) + } + } + + private func bucketIndex(forSize size: Int) -> Int { + var index = 0 + while index + 1 < buckets.count && Int(buckets[index + 1].uintValue) <= size { + index += 1 + } + return index + } + + private func textToImage(drawText text: NSString, inImage image: UIImage, font: UIFont) -> UIImage { + + UIGraphicsBeginImageContext(image.size) + image.draw(in: CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)) + + let textStyle = NSMutableParagraphStyle() + textStyle.alignment = NSTextAlignment.center + let textColor = UIColor.white + let attributes = [ + NSAttributedString.Key.font: font, + NSAttributedString.Key.paragraphStyle: textStyle, + NSAttributedString.Key.foregroundColor: textColor] + + // vertically center (depending on font) + let textH = font.lineHeight + let textY = (image.size.height-textH)/2 + let textRect = CGRect(x: 0, y: textY, width: image.size.width, height: textH) + text.draw(in: textRect.integral, withAttributes: attributes) + let result = UIGraphicsGetImageFromCurrentImageContext() + UIGraphicsEndImageContext() + return result! + } +} diff --git a/ios/Plugin/marker/CustomMarker.swift b/ios/Plugin/marker/CustomMarker.swift new file mode 100644 index 00000000..2371caac --- /dev/null +++ b/ios/Plugin/marker/CustomMarker.swift @@ -0,0 +1,83 @@ +import Capacitor +import GoogleMaps + +class CustomMarker: GMSMarker { + private var markerId: String! = NSUUID().uuidString.lowercased() + public var id: String { + get { + return self.markerId; + } + } + + var markerCategoryId : Int = 0; + + public func updateFromJSObject(preferences: JSObject) { + let markerId = preferences["id"] as? String ?? nil; + if(markerId != nil) { + self.markerId = markerId as! String; + } + + let position = preferences["position"] as? JSObject ?? JSObject(); + let latitude = position["latitude"] as? Double ?? 0.0; + let longitude = position["longitude"] as? Double ?? 0.0; + self.position = CLLocationCoordinate2D(latitude: latitude, longitude: longitude); + + self.title = preferences["title"] as? String ?? nil; + self.snippet = preferences["snippet"] as? String ?? nil; + self.opacity = preferences["opacity"] as? Float ?? 1.0; + self.isFlat = preferences["isFlat"] as? Bool ?? false; + self.isDraggable = preferences["isDraggable"] as? Bool ?? false; + self.markerCategoryId = preferences["category"] as? Int ?? 0; + + + let metadata: JSObject = preferences["metadata"] as? JSObject ?? JSObject(); + + var userData = JSObject() + userData["id"] = self.id + userData["iconId"] = self.markerCategoryId; + userData["metadata"] = metadata + + self.userData = userData + } + + public static func getResultForMarker(_ marker: GMSMarker, _ mapId: String) -> PluginCallResultData { + // check marker is marker from map or from clusterManager + let markerObj : GMSMarker; + if marker.userData is GMSMarker { + markerObj = marker.userData as! GMSMarker; + } else { + markerObj = marker as! GMSMarker + } + + let tag: JSObject = markerObj.userData as! JSObject; + + return [ + "position": [ + "mapId": mapId, + "latitude": marker.position.latitude, + "longitude": marker.position.longitude, + ] as! JSObject, + "marker": [ + "metadata": tag["metadata"] ?? JSObject(), + "id": tag["id"] ?? "", + "iconId": tag["iconId"] ?? -1, + ] + ]; + +// return [ +// "marker": [ +// "id": tag["id"] ?? "", +// "title": marker.title ?? "", +// "snippet": marker.snippet ?? "", +// "opacity": marker.opacity, +// "isFlat": marker.isFlat, +// "isDraggable": marker.isDraggable, +// "position": [ +// "latitude": marker.position.latitude, +// "longitude": marker.position.longitude +// ], +// "metadata": tag["metadata"] ?? JSObject() +// ] +// ]; + } +} diff --git a/ios/Plugin/marker/CustomRendererMarkers.swift b/ios/Plugin/marker/CustomRendererMarkers.swift new file mode 100644 index 00000000..3fea4e60 --- /dev/null +++ b/ios/Plugin/marker/CustomRendererMarkers.swift @@ -0,0 +1,59 @@ +// +// CustomRendererMarkers.swift +// CapacitorCommunityCapacitorGooglemapsNative +// +// Created by Admin on 24.01.2022. +// + +import Foundation +import GoogleMapsUtils + +class CustomRendererMarkers: GMUDefaultClusterRenderer { + + // -- constants -- + public static let MIN_COUNT_ELEMENTS_IN_CLUSTER : Int = 2; + +var mapView:GMSMapView? +let kGMUAnimationDuration: Double = 0.5 + +override init(mapView: GMSMapView, clusterIconGenerator iconGenerator: GMUClusterIconGenerator) { + + super.init(mapView: mapView, clusterIconGenerator: iconGenerator) +} + + override func shouldRender(as cluster: GMUCluster, atZoom zoom: Float) -> Bool { + return cluster.count >= CustomRendererMarkers.MIN_COUNT_ELEMENTS_IN_CLUSTER; + } + + +// method dont working and dont override anything +// TODO: rewrite super class for changing animation +func markerWithPosition(position: CLLocationCoordinate2D, from: CLLocationCoordinate2D, userData: AnyObject, clusterIcon: UIImage, animated: Bool) -> GMSMarker { + let initialPosition = animated ? from : position + let marker = GMSMarker(position: initialPosition) + marker.userData! = userData + if clusterIcon.cgImage != nil { + marker.icon = clusterIcon + } + else { + marker.icon = self.getCustomTitleItem(userData: userData) + + } + marker.map = mapView + if animated + { + CATransaction.begin() + CAAnimation.init().duration = kGMUAnimationDuration + marker.layer.latitude = position.latitude + marker.layer.longitude = position.longitude + CATransaction.commit() + } + return marker +} + +func getCustomTitleItem(userData: AnyObject) -> UIImage { + let item = userData as! CustomMarker + return MarkerCategory.markerCategories[item.markerCategoryId]?.getIcon ?? item.icon!; +// return item.icon! +} +} diff --git a/ios/Plugin/marker/MarkerCategory.swift b/ios/Plugin/marker/MarkerCategory.swift new file mode 100644 index 00000000..61c3b77e --- /dev/null +++ b/ios/Plugin/marker/MarkerCategory.swift @@ -0,0 +1,31 @@ +// +// MarkerCategory.swift +// CapacitorCommunityCapacitorGooglemapsNative +// +// Created by Admin on 27.01.2022. +// + +import Foundation + +class MarkerCategory { + + private var id : Int?; + private var title : String?; + private var icon : UIImage?; + + static var markerCategories = [Int : MarkerCategory](); + + init(_ id: Int, _ title: String, _ icon: UIImage?) { + self.id = id; + self.title = title; + self.icon = icon; + + MarkerCategory.markerCategories[self.id ?? 0] = self + } + + var getIcon : UIImage? { + get { + return icon; + } + } +} diff --git a/ios/Plugin/utility/Events.swift b/ios/Plugin/utility/Events.swift new file mode 100644 index 00000000..6f544098 --- /dev/null +++ b/ios/Plugin/utility/Events.swift @@ -0,0 +1,19 @@ +// +// Events.swift +// CapacitorCommunityCapacitorGooglemapsNative +// +// Created by Admin on 27.12.2021. +// + +import Foundation + +class Events { + static var EVENT_DID_TAP_INFO_WINDOW: String = "didTapInfoWindow"; + static var EVENT_DID_CLOSE_INFO_WINDOW: String = "didCloseInfoWindow"; + static var EVENT_DID_TAP_MAP: String = "didTapMap"; + static var EVENT_DID_LONG_PRESS_MAP: String = "didLongPressMap"; + static var EVENT_DID_TAP_MARKER: String = "didTapMarker"; + static var EVENT_DID_TAP_CLUSTER: String = "didTapCluster"; + static var EVENT_DID_TAP_MY_LOCATION_BUTTON: String = "didTapMyLocationButton"; + static var EVENT_DID_TAP_MY_LOCATION_DOT: String = "didTapMyLocationDot"; +} diff --git a/package.json b/package.json index 50b35d32..6c5558ff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@capacitor-community/capacitor-googlemaps-native", - "version": "1.1.0", + "version": "1.1.5", "description": "Plugin using native Maps API for Android and iOS.", "main": "dist/esm/index.js", "module": "dist/esm/index.js", @@ -46,7 +46,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/capacitor-community/capacitor-googlemaps-native" + "url": "https://github.com/vorokami/capacitor-google-maps" }, "bugs": { "url": "https://github.com/capacitor-community/capacitor-googlemaps-native/issues"