package com.davidgalindo.rnarcgismapview; import android.annotation.SuppressLint; import android.content.Context; import android.graphics.Color; import android.util.Log; import android.view.MotionEvent; import android.view.View; import android.widget.LinearLayout; import android.widget.TextView; import com.esri.arcgisruntime.ArcGISRuntimeException; import com.esri.arcgisruntime.concurrent.ListenableFuture; import com.esri.arcgisruntime.geometry.Envelope; import com.esri.arcgisruntime.geometry.GeometryEngine; import com.esri.arcgisruntime.geometry.Point; import com.esri.arcgisruntime.geometry.PointCollection; import com.esri.arcgisruntime.geometry.Polygon; import com.esri.arcgisruntime.geometry.Polyline; import com.esri.arcgisruntime.geometry.SpatialReferences; import com.esri.arcgisruntime.mapping.ArcGISMap; import com.esri.arcgisruntime.mapping.Basemap; import com.esri.arcgisruntime.mapping.Viewpoint; import com.esri.arcgisruntime.mapping.view.Callout; import com.esri.arcgisruntime.mapping.view.DefaultMapViewOnTouchListener; import com.esri.arcgisruntime.mapping.view.Graphic; import com.esri.arcgisruntime.mapping.view.GraphicsOverlay; import com.esri.arcgisruntime.mapping.view.IdentifyGraphicsOverlayResult; import com.esri.arcgisruntime.mapping.view.MapView; import com.esri.arcgisruntime.symbology.SimpleLineSymbol; import com.esri.arcgisruntime.tasks.networkanalysis.Route; import com.esri.arcgisruntime.tasks.networkanalysis.RouteResult; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import com.facebook.react.modules.core.DeviceEventManagerModule; import com.facebook.react.uimanager.events.RCTEventEmitter; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutionException; public class RNAGSMapView extends LinearLayout implements LifecycleEventListener { // MARK: Variables/Prop declarations View rootView; public MapView mapView; String basemapUrl = ""; String routeUrl = ""; Boolean recenterIfGraphicTapped = false; HashMap<String, RNAGSGraphicsOverlay> rnGraphicsOverlays = new HashMap<>(); GraphicsOverlay routeGraphicsOverlay; RNAGSRouter router; private Callout callout; Double minZoom = 0.0; Double maxZoom = 0.0; Boolean rotationEnabled = true; // MARK: Initializers public RNAGSMapView(Context context) { super(context); rootView = inflate(context.getApplicationContext(),R.layout.rnags_mapview,this); mapView = rootView.findViewById(R.id.agsMapView); if (context instanceof ReactContext) { ((ReactContext) context).addLifecycleEventListener(this); } setUpMap(); setUpCallout(); } private void setUpCallout() { // We want to create the callout right after the View has finished its layout mapView.post(() -> { callout = mapView.getCallout(); callout.setContent(inflate(getContext().getApplicationContext(),R.layout.rnags_callout_content, null)); Callout.ShowOptions showOptions = new Callout.ShowOptions(); showOptions.setAnimateCallout(true); showOptions.setAnimateRecenter(true); showOptions.setRecenterMap(false); callout.getStyle().setMaxHeight(1000); callout.getStyle().setMaxWidth(1000); callout.getStyle().setMinHeight(100); callout.getStyle().setMinWidth(100); callout.setShowOptions(showOptions); callout.setPassTouchEventsToMapView(false); }); } @SuppressLint("ClickableViewAccessibility") public void setUpMap() { mapView.setMap(new ArcGISMap(Basemap.Type.STREETS_VECTOR, 34.057, -117.196, 17)); mapView.setOnTouchListener(new OnSingleTouchListener(getContext(),mapView)); routeGraphicsOverlay = new GraphicsOverlay(); mapView.getGraphicsOverlays().add(routeGraphicsOverlay); mapView.getMap().addDoneLoadingListener(() -> { ArcGISRuntimeException e = mapView.getMap().getLoadError(); Boolean success = e != null; String errorMessage = !success ? "" : e.getMessage(); WritableMap map = Arguments.createMap(); map.putBoolean("success",success); map.putString("errorMessage",errorMessage); emitEvent("onMapDidLoad",map); }); } // MARK: Prop set methods public void setBasemapUrl(String url) { basemapUrl = url; if(basemapUrl == null || basemapUrl.isEmpty()) { return; } // Set basemap of map if (mapView.getMap() == null) { setUpMap(); } final Basemap basemap = new Basemap(basemapUrl); basemap.addDoneLoadingListener(() -> { if (basemap.getLoadError() != null) { Log.w("AGSMap", "An error occurred: " + basemap.getLoadError().getMessage()); } else { mapView.getMap().setBasemap(basemap); } }); basemap.loadAsync(); } public void setRouteUrl(String url) { routeUrl = url; router = new RNAGSRouter(getContext().getApplicationContext(), routeUrl); } public void setRecenterIfGraphicTapped(boolean value) { recenterIfGraphicTapped = value; } public void setInitialMapCenter(ReadableArray initialCenter) { ArrayList<Point> points = new ArrayList<>(); for (int i = 0; i < initialCenter.size(); i++) { ReadableMap item = initialCenter.getMap(i); if (item == null) { continue; } Double latitude = item.getDouble("latitude"); Double longitude = item.getDouble("longitude"); if (latitude == 0 || longitude == 0) { continue; } Point point = new Point(longitude, latitude, SpatialReferences.getWgs84()); points.add(point); } // If no points exist, add a sample point if (points.size() == 0) { points.add(new Point(36.244797,-94.148060, SpatialReferences.getWgs84())); } if (points.size() == 1) { mapView.getMap().setInitialViewpoint(new Viewpoint(points.get(0),10000)); } else { Polygon polygon = new Polygon(new PointCollection(points)); Viewpoint viewpoint = viewpointFromPolygon(polygon); mapView.getMap().setInitialViewpoint(viewpoint); } } public void setMinZoom(Double value) { minZoom = value; mapView.getMap().setMinScale(minZoom); } public void setMaxZoom(Double value) { maxZoom = value; mapView.getMap().setMaxScale(maxZoom); } public void setRotationEnabled(Boolean value) { rotationEnabled = value; } // MARK: Exposed RN Methods public void showCallout(ReadableMap args) { ReadableMap rawPoint = args.getMap("point"); if (!rawPoint.hasKey("latitude") || !rawPoint.hasKey("longitude")) { return; } Double latitude = rawPoint.getDouble("latitude"); Double longitude = rawPoint.getDouble("longitude"); if (latitude == 0 || longitude == 0) { return; } String title = ""; String text = ""; Boolean shouldRecenter = false; if(args.hasKey("title")) { title = args.getString("title"); } if(args.hasKey("text")) { text = args.getString("text"); } if(args.hasKey("shouldRecenter")) { shouldRecenter = args.getBoolean("shouldRecenter"); } // Displaying the callout Point agsPoint = new Point(longitude, latitude, SpatialReferences.getWgs84()); // Set callout content View calloutContent = callout.getContent(); ((TextView) calloutContent.findViewById(R.id.title)).setText(title); ((TextView) calloutContent.findViewById(R.id.text)).setText(text); callout.setLocation(agsPoint); callout.show(); Log.i("AGS",callout.isShowing()+" " + calloutContent.getWidth() + " " + calloutContent.getHeight()); if (shouldRecenter) { mapView.setViewpointCenterAsync(agsPoint); } } public void centerMap(ReadableArray args) { final ArrayList<Point> points = new ArrayList<>(); for (int i = 0; i < args.size(); i++) { ReadableMap item = args.getMap(i); if (item == null) { continue; } Double latitude = item.getDouble("latitude"); Double longitude = item.getDouble("longitude"); if(latitude == 0 || longitude == 0) { continue; } points.add(new Point(longitude, latitude, SpatialReferences.getWgs84())); } // Perform the recentering if (points.size() == 1) { mapView.setViewpointCenterAsync(points.get(0),60000); } else if (points.size() > 1) { PointCollection pointCollection = new PointCollection(points); Polygon polygon = new Polygon(pointCollection); mapView.setViewpointGeometryAsync(polygon, 50); } } // Layer add/remove public void addGraphicsOverlay(ReadableMap args) { GraphicsOverlay graphicsOverlay = new GraphicsOverlay(); mapView.getGraphicsOverlays().add(graphicsOverlay); RNAGSGraphicsOverlay overlay = new RNAGSGraphicsOverlay(args, graphicsOverlay); rnGraphicsOverlays.put(overlay.getReferenceId(), overlay); } public void removeGraphicsOverlay(String removalId) { RNAGSGraphicsOverlay overlay = rnGraphicsOverlays.get(removalId); if (overlay == null) { Log.w("Warning (AGS)", "No overlay with the associated ID was found."); return; } mapView.getGraphicsOverlays().remove(overlay.getAGSGraphicsOverlay()); rnGraphicsOverlays.remove(removalId); } // Point updates public void updatePointsInGraphicsOverlay(ReadableMap args) { if (!args.hasKey("overlayReferenceId")) { Log.w("Warning (AGS)", "No overlay with the associated ID was found."); return; } Boolean shouldAnimateUpdate = false; if(args.hasKey("animated")) { shouldAnimateUpdate = args.getBoolean("animated"); } String overlayReferenceId = args.getString("overlayReferenceId"); RNAGSGraphicsOverlay overlay = rnGraphicsOverlays.get(overlayReferenceId); if (overlay != null && args.hasKey("updates")){ overlay.setShouldAnimateUpdate(shouldAnimateUpdate); overlay.updateGraphics(args.getArray("updates")); } } public void addPointsToOverlay(ReadableMap args) { if (!args.hasKey("overlayReferenceId")) { Log.w("Warning (AGS)", "No overlay with the associated ID was found."); return; } String overlayReferenceId = args.getString("overlayReferenceId"); RNAGSGraphicsOverlay overlay = rnGraphicsOverlays.get(overlayReferenceId); if (overlay != null && args.hasKey("points")) { overlay.addGraphics(args.getArray("points")); } } public void removePointsFromOverlay(ReadableMap args) { if (!args.hasKey("overlayReferenceId")) { Log.w("Warning (AGS)", "No overlay with the associated ID was found."); return; } String overlayReferenceId = args.getString("overlayReferenceId"); RNAGSGraphicsOverlay overlay = rnGraphicsOverlays.get(overlayReferenceId); if (overlay != null && args.hasKey("referenceIds")) { overlay.removeGraphics(args.getArray("referenceIds")); } } // set/get public void setRouteIsVisible(Boolean isVisible) { routeGraphicsOverlay.setVisible(isVisible); } public Boolean getRouteIsVisible() { return routeGraphicsOverlay.isVisible(); } // Routing public void routeGraphicsOverlay(ReadableMap args) { if (router == null) { Log.w("Warning (AGS)", "Router has not been initialized. Perhaps no route URL was provided? Route URL:" + routeUrl); return; } if (!args.hasKey("overlayReferenceId")) { Log.w("Warning (AGS)", "No overlay with the associated ID was found."); return; } String overlayReferenceId = args.getString("overlayReferenceId"); RNAGSGraphicsOverlay overlay = rnGraphicsOverlays.get(overlayReferenceId); ArrayList<String> removeGraphics = new ArrayList<>(); if(args.hasKey("excludeGraphics")) { ReadableArray rawArray = args.getArray("excludeGraphics"); for (Object item: rawArray.toArrayList()) { removeGraphics.add(((String) item)); } } final String color; if (args.hasKey("routeColor")) { color = args.getString("routeColor"); } else { color = "#FF0000"; } assert overlay != null; ListenableFuture<RouteResult> future = router.createRoute(overlay.getAGSGraphicsOverlay(),removeGraphics); if (future == null) { Log.w("Warning (AGS)", "There was an issue creating the route. Please try again later, or check your routing server."); return; } setIsRouting(true); future.addDoneListener(() -> { try { RouteResult result = future.get(); if (result != null && !result.getRoutes().isEmpty()) { Route route = result.getRoutes().get(0); drawRoute(route, color); } } catch(Exception e) { e.printStackTrace(); } finally { setIsRouting(false); } }); } private void setIsRouting(Boolean value) { ((ReactContext) getContext()).getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit("isRoutingChanged",value); } private void drawRoute(Route route, String color) { if (route == null) { Log.w("Warning (AGS)", "No route result returned."); return; } routeGraphicsOverlay.getGraphics().clear(); SimpleLineSymbol symbol = new SimpleLineSymbol(SimpleLineSymbol.Style.SOLID, Color.parseColor(color),5); Polyline polyline = route.getRouteGeometry(); Graphic routeGraphic = new Graphic(route.getRouteGeometry(),symbol); routeGraphicsOverlay.getGraphics().add(routeGraphic); } // MARK: Event emitting public void emitEvent(String eventName, WritableMap args) { ((ReactContext) getContext()).getJSModule(RCTEventEmitter.class).receiveEvent( getId(), eventName, args ); } // MARK: OnTouchListener public class OnSingleTouchListener extends DefaultMapViewOnTouchListener { OnSingleTouchListener(Context context, MapView mMapView){ super(context,mMapView); } @Override public boolean onRotate(MotionEvent event, double rotationAngle) { if (rotationEnabled) { return super.onRotate(event, rotationAngle); } else { return true; } } @Override public boolean onDown(MotionEvent e) { WritableMap map = createPointMap(e); emitEvent("onMapMoved",map); return true; } private WritableMap createPointMap(MotionEvent e){ android.graphics.Point screenPoint = new android.graphics.Point(((int) e.getX()), ((int) e.getY())); WritableMap screenPointMap = Arguments.createMap(); screenPointMap.putInt("x",screenPoint.x); screenPointMap.putInt("y",screenPoint.y); Point mapPoint = mMapView.screenToLocation(screenPoint); WritableMap mapPointMap = Arguments.createMap(); if (mapPoint != null) { Point latLongPoint = ((Point) GeometryEngine.project(mapPoint, SpatialReferences.getWgs84())); mapPointMap.putDouble("latitude", latLongPoint.getY()); mapPointMap.putDouble("longitude", latLongPoint.getX()); } WritableMap map = Arguments.createMap(); map.putMap("screenPoint", screenPointMap); map.putMap("mapPoint",mapPointMap); return map; } @Override public boolean onSingleTapConfirmed(MotionEvent e) { WritableMap map = createPointMap(e); android.graphics.Point screenPoint = new android.graphics.Point(((int) e.getX()), ((int) e.getY())); ListenableFuture<List<IdentifyGraphicsOverlayResult>> future = mMapView.identifyGraphicsOverlaysAsync(screenPoint,15, false); future.addDoneListener(() -> { try { if (!future.get().isEmpty()) { // We only care about the topmost result IdentifyGraphicsOverlayResult futureResult = future.get().get(0); List<Graphic> graphicResult = futureResult.getGraphics(); // More null checking >.> if (!graphicResult.isEmpty()) { Graphic result = graphicResult.get(0); map.putString("graphicReferenceId", Objects.requireNonNull(result.getAttributes().get("referenceId")).toString()); if (recenterIfGraphicTapped) { mapView.setViewpointCenterAsync(((Point) result.getGeometry())); } } } } catch (InterruptedException | ExecutionException exception) { exception.printStackTrace(); } finally { emitEvent("onSingleTap",map); } }); return true; } } // MARK: Lifecycle Event Listeners @Override public void onHostResume() { mapView.resume(); } @Override public void onHostPause() { mapView.pause(); } @Override public void onHostDestroy() { mapView.dispose(); if (getContext() instanceof ReactContext) { ((ReactContext) getContext()).removeLifecycleEventListener(this); } } // MARK: Misc. public Viewpoint viewpointFromPolygon(Polygon polygon) { Envelope envelope = polygon.getExtent(); Double paddingWidth = envelope.getWidth() * 0.5; Double paddingHeight = envelope.getHeight() * 0.5; return new Viewpoint(new Envelope( envelope.getXMin() - paddingWidth, envelope.getYMax() + paddingHeight, envelope.getXMax() + paddingWidth, envelope.getYMin() - paddingHeight, SpatialReferences.getWgs84()), 0); } }