package com.mapbox.mapboxsdk.maps; import android.graphics.Bitmap; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.util.LongSparseArray; import android.view.View; import com.mapbox.mapboxsdk.annotations.Annotation; import com.mapbox.mapboxsdk.annotations.BaseMarkerOptions; import com.mapbox.mapboxsdk.annotations.BaseMarkerViewOptions; import com.mapbox.mapboxsdk.annotations.Marker; import com.mapbox.mapboxsdk.annotations.MarkerView; import com.mapbox.mapboxsdk.annotations.MarkerViewManager; import com.mapbox.mapboxsdk.annotations.Polygon; import com.mapbox.mapboxsdk.annotations.PolygonOptions; import com.mapbox.mapboxsdk.annotations.Polyline; import com.mapbox.mapboxsdk.annotations.PolylineOptions; import java.util.ArrayList; import java.util.List; /** * Responsible for managing and tracking state of Annotations linked to Map. All events related to * annotations that occur on {@link MapboxMap} are forwarded to this class. *

* Responsible for referencing {@link InfoWindowManager} and {@link MarkerViewManager}. *

*

* Exposes convenience methods to add/remove/update all subtypes of annotations found in * com.mapbox.mapboxsdk.annotations. *

*/ class AnnotationManager { private final MapView mapView; private final IconManager iconManager; private final InfoWindowManager infoWindowManager = new InfoWindowManager(); private final MarkerViewManager markerViewManager; private final LongSparseArray annotationsArray; private final List selectedMarkers = new ArrayList<>(); private MapboxMap mapboxMap; private MapboxMap.OnMarkerClickListener onMarkerClickListener; private Annotations annotations; private Markers markers; private Polygons polygons; private Polylines polylines; AnnotationManager(NativeMapView view, MapView mapView, LongSparseArray annotationsArray, MarkerViewManager markerViewManager, IconManager iconManager, Annotations annotations, Markers markers, Polygons polygons, Polylines polylines) { this.mapView = mapView; this.annotationsArray = annotationsArray; this.markerViewManager = markerViewManager; this.iconManager = iconManager; this.annotations = annotations; this.markers = markers; this.polygons = polygons; this.polylines = polylines; if (view != null) { // null checking needed for unit tests view.addOnMapChangedListener(markerViewManager); } } // TODO refactor MapboxMap out for Projection and Transform // Requires removing MapboxMap from Annotations by using Peer model from #6912 AnnotationManager bind(MapboxMap mapboxMap) { this.mapboxMap = mapboxMap; this.markerViewManager.bind(mapboxMap); return this; } void update() { markerViewManager.update(); infoWindowManager.update(); } // // Annotations // Annotation getAnnotation(long id) { return annotations.obtainBy(id); } List getAnnotations() { return annotations.obtainAll(); } void removeAnnotation(long id) { annotations.removeBy(id); } void removeAnnotation(@NonNull Annotation annotation) { if (annotation instanceof Marker) { Marker marker = (Marker) annotation; marker.hideInfoWindow(); if (selectedMarkers.contains(marker)) { selectedMarkers.remove(marker); } if (marker instanceof MarkerView) { markerViewManager.removeMarkerView((MarkerView) marker); } } annotations.removeBy(annotation); } void removeAnnotations(@NonNull List annotationList) { for (Annotation annotation : annotationList) { if (annotation instanceof Marker) { Marker marker = (Marker) annotation; marker.hideInfoWindow(); if (selectedMarkers.contains(marker)) { selectedMarkers.remove(marker); } if (marker instanceof MarkerView) { markerViewManager.removeMarkerView((MarkerView) marker); } } } annotations.removeBy(annotationList); } void removeAnnotations() { Annotation annotation; int count = annotationsArray.size(); long[] ids = new long[count]; selectedMarkers.clear(); for (int i = 0; i < count; i++) { ids[i] = annotationsArray.keyAt(i); annotation = annotationsArray.get(ids[i]); if (annotation instanceof Marker) { Marker marker = (Marker) annotation; marker.hideInfoWindow(); if (marker instanceof MarkerView) { markerViewManager.removeMarkerView((MarkerView) marker); } } } annotations.removeAll(); } // // Markers // Marker addMarker(@NonNull BaseMarkerOptions markerOptions, @NonNull MapboxMap mapboxMap) { return markers.addBy(markerOptions, mapboxMap); } List addMarkers(@NonNull List markerOptionsList, @NonNull MapboxMap mapboxMap) { return markers.addBy(markerOptionsList, mapboxMap); } void updateMarker(@NonNull Marker updatedMarker, @NonNull MapboxMap mapboxMap) { markers.update(updatedMarker, mapboxMap); } List getMarkers() { return markers.obtainAll(); } @NonNull List getMarkersInRect(@NonNull RectF rectangle) { return markers.obtainAllIn(rectangle); } MarkerView addMarker(@NonNull BaseMarkerViewOptions markerOptions, @NonNull MapboxMap mapboxMap, @Nullable MarkerViewManager.OnMarkerViewAddedListener onMarkerViewAddedListener) { return markers.addViewBy(markerOptions, mapboxMap, onMarkerViewAddedListener); } List addMarkerViews(@NonNull List markerViewOptions, @NonNull MapboxMap mapboxMap) { return markers.addViewsBy(markerViewOptions, mapboxMap); } List getMarkerViewsInRect(@NonNull RectF rectangle) { return markers.obtainViewsIn(rectangle); } void reloadMarkers() { markers.reload(); } // // Polygons // Polygon addPolygon(@NonNull PolygonOptions polygonOptions, @NonNull MapboxMap mapboxMap) { return polygons.addBy(polygonOptions, mapboxMap); } List addPolygons(@NonNull List polygonOptionsList, @NonNull MapboxMap mapboxMap) { return polygons.addBy(polygonOptionsList, mapboxMap); } void updatePolygon(Polygon polygon) { polygons.update(polygon); } List getPolygons() { return polygons.obtainAll(); } // // Polylines // Polyline addPolyline(@NonNull PolylineOptions polylineOptions, @NonNull MapboxMap mapboxMap) { return polylines.addBy(polylineOptions, mapboxMap); } List addPolylines(@NonNull List polylineOptionsList, @NonNull MapboxMap mapboxMap) { return polylines.addBy(polylineOptionsList, mapboxMap); } void updatePolyline(Polyline polyline) { polylines.update(polyline); } List getPolylines() { return polylines.obtainAll(); } // TODO Refactor from here still in progress void setOnMarkerClickListener(@Nullable MapboxMap.OnMarkerClickListener listener) { onMarkerClickListener = listener; } void selectMarker(@NonNull Marker marker) { if (selectedMarkers.contains(marker)) { return; } // Need to deselect any currently selected annotation first if (!infoWindowManager.isAllowConcurrentMultipleOpenInfoWindows()) { deselectMarkers(); } if (marker instanceof MarkerView) { markerViewManager.select((MarkerView) marker, false); markerViewManager.ensureInfoWindowOffset((MarkerView) marker); } if (infoWindowManager.isInfoWindowValidForMarker(marker) || infoWindowManager.getInfoWindowAdapter() != null) { infoWindowManager.add(marker.showInfoWindow(mapboxMap, mapView)); } // only add to selected markers if user didn't handle the click event themselves #3176 selectedMarkers.add(marker); } void deselectMarkers() { if (selectedMarkers.isEmpty()) { return; } for (Marker marker : selectedMarkers) { if (marker.isInfoWindowShown()) { marker.hideInfoWindow(); } if (marker instanceof MarkerView) { markerViewManager.deselect((MarkerView) marker, false); } } // Removes all selected markers from the list selectedMarkers.clear(); } void deselectMarker(@NonNull Marker marker) { if (!selectedMarkers.contains(marker)) { return; } if (marker.isInfoWindowShown()) { marker.hideInfoWindow(); } if (marker instanceof MarkerView) { markerViewManager.deselect((MarkerView) marker, false); } selectedMarkers.remove(marker); } List getSelectedMarkers() { return selectedMarkers; } InfoWindowManager getInfoWindowManager() { return infoWindowManager; } MarkerViewManager getMarkerViewManager() { return markerViewManager; } void adjustTopOffsetPixels(MapboxMap mapboxMap) { int count = annotationsArray.size(); for (int i = 0; i < count; i++) { Annotation annotation = annotationsArray.get(i); if (annotation instanceof Marker) { Marker marker = (Marker) annotation; marker.setTopOffsetPixels( iconManager.getTopOffsetPixelsForIcon(marker.getIcon())); } } for (Marker marker : selectedMarkers) { if (marker.isInfoWindowShown()) { marker.hideInfoWindow(); marker.showInfoWindow(mapboxMap, mapView); } } } // // Click event // boolean onTap(PointF tapPoint) { MarkerHit markerHit = getMarkerHitFromTouchArea(tapPoint); long markerId = new MarkerHitResolver(markerViewManager, mapboxMap.getProjection()).execute(markerHit); return markerId >= 0 && isClickHandledForMarker(markerId); } private MarkerHit getMarkerHitFromTouchArea(PointF tapPoint) { int averageIconWidthOffset = iconManager.getAverageIconWidth() / 2; int averageIconHeightOffset = iconManager.getAverageIconHeight() / 2; final RectF tapRect = new RectF(tapPoint.x - averageIconWidthOffset, tapPoint.y - averageIconHeightOffset, tapPoint.x + averageIconWidthOffset, tapPoint.y + averageIconHeightOffset ); return new MarkerHit(tapRect, getMarkersInRect(tapRect)); } private boolean isClickHandledForMarker(long markerId) { boolean handledDefaultClick; Marker marker = (Marker) getAnnotation(markerId); if (marker instanceof MarkerView) { handledDefaultClick = markerViewManager.onClickMarkerView((MarkerView) marker); } else { handledDefaultClick = onClickMarker(marker); } if (!handledDefaultClick) { setMarkerSelectionState(marker); } return true; } private boolean onClickMarker(Marker marker) { return onMarkerClickListener != null && onMarkerClickListener.onMarkerClick(marker); } private void setMarkerSelectionState(Marker marker) { if (!selectedMarkers.contains(marker)) { selectMarker(marker); } else { deselectMarker(marker); } } private static class MarkerHitResolver { private final MarkerViewManager markerViewManager; private final Projection projection; private View view; private Bitmap bitmap; private PointF markerLocation; private Rect hitRectView = new Rect(); private RectF hitRectMarker = new RectF(); private RectF highestSurfaceIntersection = new RectF(); private long closestMarkerId = -1; MarkerHitResolver(@NonNull MarkerViewManager markerViewManager, @NonNull Projection projection) { this.markerViewManager = markerViewManager; this.projection = projection; } public long execute(MarkerHit markerHit) { resolveForMarkers(markerHit); return closestMarkerId; } private void resolveForMarkers(MarkerHit markerHit) { for (Marker marker : markerHit.markers) { if (marker instanceof MarkerView) { resolveForMarkerView(markerHit, (MarkerView) marker); } else { resolveForMarker(markerHit, marker); } } } private void resolveForMarkerView(MarkerHit markerHit, MarkerView markerView) { view = markerViewManager.getView(markerView); if (view != null) { view.getHitRect(hitRectView); hitRectMarker = new RectF(hitRectView); hitTestMarker(markerHit, markerView, hitRectMarker); } } private void resolveForMarker(MarkerHit markerHit, Marker marker) { markerLocation = projection.toScreenLocation(marker.getPosition()); bitmap = marker.getIcon().getBitmap(); hitRectMarker.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); hitRectMarker.offsetTo( markerLocation.x - bitmap.getWidth() / 2, markerLocation.y - bitmap.getHeight() / 2 ); hitTestMarker(markerHit, marker, hitRectMarker); } private void hitTestMarker(MarkerHit markerHit, Marker marker, RectF hitRectMarker) { if (hitRectMarker.contains(markerHit.getTapPointX(), markerHit.getTapPointY())) { hitRectMarker.intersect(markerHit.tapRect); if (isRectangleHighestSurfaceIntersection(hitRectMarker)) { highestSurfaceIntersection = new RectF(hitRectMarker); closestMarkerId = marker.getId(); } } } private boolean isRectangleHighestSurfaceIntersection(RectF rectF) { return rectF.width() * rectF.height() > highestSurfaceIntersection.width() * highestSurfaceIntersection.height(); } } private static class MarkerHit { private final RectF tapRect; private final List markers; MarkerHit(RectF tapRect, List markers) { this.tapRect = tapRect; this.markers = markers; } float getTapPointX() { return tapRect.centerX(); } float getTapPointY() { return tapRect.centerY(); } } }