package net.osmand.plus.views; import android.app.Dialog; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; import android.net.Uri; import android.provider.Settings; import android.support.v4.content.ContextCompat; import android.support.v4.view.MenuItemCompat; import android.support.v7.widget.PopupMenu; import android.support.v7.widget.Toolbar; import android.text.util.Linkify; import android.util.TypedValue; import android.view.Gravity; import android.view.MenuItem; import android.view.MenuItem.OnMenuItemClickListener; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.Button; import android.widget.LinearLayout; import android.widget.LinearLayout.LayoutParams; import android.widget.ScrollView; import android.widget.TextView; import net.osmand.AndroidUtils; import net.osmand.PlatformUtil; import net.osmand.ResultMatcher; import net.osmand.ValueHolder; import net.osmand.data.Amenity; import net.osmand.data.LatLon; import net.osmand.data.PointDescription; import net.osmand.data.QuadRect; import net.osmand.data.QuadTree; import net.osmand.data.RotatedTileBox; import net.osmand.osm.PoiType; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; import net.osmand.plus.activities.MapActivity; import net.osmand.plus.helpers.FileNameTranslationHelper; import net.osmand.plus.poi.PoiUIFilter; import net.osmand.plus.render.RenderingIcons; import net.osmand.plus.routing.RoutingHelper; import net.osmand.plus.routing.RoutingHelper.IRouteInformationListener; import net.osmand.plus.views.MapTextLayer.MapTextProvider; import net.osmand.util.Algorithms; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import static android.util.TypedValue.COMPLEX_UNIT_DIP; public class POIMapLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider, MapTextProvider<Amenity>, IRouteInformationListener { private static final int startZoom = 9; public static final org.apache.commons.logging.Log log = PlatformUtil.getLog(POIMapLayer.class); private Paint paintIcon; private Paint paintIconBackground; private Bitmap poiBackground; private Bitmap poiBackgroundSmall; private OsmandMapTileView view; private RoutingHelper routingHelper; private Set<PoiUIFilter> filters = new TreeSet<>(); private MapTextLayer mapTextLayer; /// cache for displayed POI // Work with cache (for map copied from AmenityIndexRepositoryOdb) private MapLayerData<List<Amenity>> data; private OsmandApplication app; public POIMapLayer(final MapActivity activity) { routingHelper = activity.getRoutingHelper(); routingHelper.addListener(this); app = activity.getMyApplication(); data = new OsmandMapLayer.MapLayerData<List<Amenity>>() { { ZOOM_THRESHOLD = 0; } @Override public boolean isInterrupted() { return super.isInterrupted(); } @Override public void layerOnPostExecute() { activity.getMapView().refreshMap(); } @Override protected List<Amenity> calculateResult(RotatedTileBox tileBox) { QuadRect latLonBounds = tileBox.getLatLonBounds(); if (filters.isEmpty() || latLonBounds == null) { return new ArrayList<>(); } int z = (int) Math.floor(tileBox.getZoom() + Math.log(view.getSettings().MAP_DENSITY.get()) / Math.log(2)); List<Amenity> res = new ArrayList<>(); PoiUIFilter.combineStandardPoiFilters(filters, app); for (PoiUIFilter filter : filters) { res.addAll(filter.searchAmenities(latLonBounds.top, latLonBounds.left, latLonBounds.bottom, latLonBounds.right, z, new ResultMatcher<Amenity>() { @Override public boolean publish(Amenity object) { return true; } @Override public boolean isCancelled() { return isInterrupted(); } })); } Collections.sort(res, new Comparator<Amenity>() { @Override public int compare(Amenity lhs, Amenity rhs) { return lhs.getId() < rhs.getId() ? -1 : (lhs.getId().longValue() == rhs.getId().longValue() ? 0 : 1); } }); return res; } }; } public void getAmenityFromPoint(RotatedTileBox tb, PointF point, List<? super Amenity> am) { List<Amenity> objects = data.getResults(); if (objects != null) { int ex = (int) point.x; int ey = (int) point.y; final int rp = getRadiusPoi(tb); int compare = rp; int radius = rp * 3 / 2; try { for (int i = 0; i < objects.size(); i++) { Amenity n = objects.get(i); int x = (int) tb.getPixXFromLatLon(n.getLocation().getLatitude(), n.getLocation().getLongitude()); int y = (int) tb.getPixYFromLatLon(n.getLocation().getLatitude(), n.getLocation().getLongitude()); if (Math.abs(x - ex) <= compare && Math.abs(y - ey) <= compare) { compare = radius; am.add(n); } } } catch (IndexOutOfBoundsException e) { // that's really rare case, but is much efficient than introduce synchronized block } } } @Override public void initLayer(OsmandMapTileView view) { this.view = view; paintIcon = new Paint(); //paintIcon.setStrokeWidth(1); //paintIcon.setStyle(Style.STROKE); //paintIcon.setColor(Color.BLUE); paintIcon.setColorFilter(new PorterDuffColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)); paintIconBackground = new Paint(); poiBackground = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_white_orange_poi_shield); poiBackgroundSmall = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_white_orange_poi_shield_small); mapTextLayer = view.getLayerByClass(MapTextLayer.class); } public int getRadiusPoi(RotatedTileBox tb) { int r; final double zoom = tb.getZoom(); if (zoom < startZoom) { r = 0; } else if (zoom <= 15) { r = 10; } else if (zoom <= 16) { r = 14; } else if (zoom <= 17) { r = 16; } else { r = 18; } return (int) (r * view.getScaleCoefficient()); } @Override public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { Set<PoiUIFilter> selectedPoiFilters = app.getPoiFilters().getSelectedPoiFilters(); if (!this.filters.equals(selectedPoiFilters)) { this.filters = new TreeSet<>(selectedPoiFilters); data.clearCache(); } List<Amenity> objects = Collections.emptyList(); List<Amenity> fullObjects = new ArrayList<>(); List<LatLon> fullObjectsLatLon = new ArrayList<>(); List<LatLon> smallObjectsLatLon = new ArrayList<>(); if (!filters.isEmpty()) { if (tileBox.getZoom() >= startZoom) { data.queryNewData(tileBox); objects = data.getResults(); if (objects != null) { float iconSize = poiBackground.getWidth() * 3 / 2; QuadTree<QuadRect> boundIntersections = initBoundIntersections(tileBox); for (Amenity o : objects) { float x = tileBox.getPixXFromLatLon(o.getLocation().getLatitude(), o.getLocation() .getLongitude()); float y = tileBox.getPixYFromLatLon(o.getLocation().getLatitude(), o.getLocation() .getLongitude()); if (intersects(boundIntersections, x, y, iconSize, iconSize)) { canvas.drawBitmap(poiBackgroundSmall, x - poiBackgroundSmall.getWidth() / 2, y - poiBackgroundSmall.getHeight() / 2, paintIconBackground); smallObjectsLatLon.add(new LatLon(o.getLocation().getLatitude(), o.getLocation().getLongitude())); } else { fullObjects.add(o); fullObjectsLatLon.add(new LatLon(o.getLocation().getLatitude(), o.getLocation().getLongitude())); } } for (Amenity o : fullObjects) { int x = (int) tileBox.getPixXFromLatLon(o.getLocation().getLatitude(), o.getLocation() .getLongitude()); int y = (int) tileBox.getPixYFromLatLon(o.getLocation().getLatitude(), o.getLocation() .getLongitude()); canvas.drawBitmap(poiBackground, x - poiBackground.getWidth() / 2, y - poiBackground.getHeight() / 2, paintIconBackground); String id = null; PoiType st = o.getType().getPoiTypeByKeyName(o.getSubType()); if (st != null) { if (RenderingIcons.containsSmallIcon(st.getIconKeyName())) { id = st.getIconKeyName(); } else if (RenderingIcons.containsSmallIcon(st.getOsmTag() + "_" + st.getOsmValue())) { id = st.getOsmTag() + "_" + st.getOsmValue(); } } if (id != null) { Bitmap bmp = RenderingIcons.getIcon(view.getContext(), id, false); if (bmp != null) { canvas.drawBitmap(bmp, x - bmp.getWidth() / 2, y - bmp.getHeight() / 2, paintIcon); } } } this.fullObjectsLatLon = fullObjectsLatLon; this.smallObjectsLatLon = smallObjectsLatLon; } } } mapTextLayer.putData(this, objects); } @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { } @Override public void destroyLayer() { routingHelper.removeListener(this); } @Override public boolean drawInScreenPixels() { return true; } public static void showWikipediaDialog(Context ctx, OsmandApplication app, Amenity a) { String lang = app.getSettings().MAP_PREFERRED_LOCALE.get(); if (a.getType().isWiki()) { showWiki(ctx, app, a, lang); } } public static void showDescriptionDialog(Context ctx, OsmandApplication app, String text, String title) { showText(ctx, app, text, title); } static int getResIdFromAttribute(final Context ctx, final int attr) { if (attr == 0) { return 0; } final TypedValue typedvalueattr = new TypedValue(); ctx.getTheme().resolveAttribute(attr, typedvalueattr, true); return typedvalueattr.resourceId; } @SuppressWarnings("deprecation") private static void showWiki(final Context ctx, final OsmandApplication app, final Amenity a, final String lang) { String preferredLang = lang; if (Algorithms.isEmpty(preferredLang)) { preferredLang = app.getLanguage(); } final Dialog dialog = new Dialog(ctx, app.getSettings().isLightContent() ? R.style.OsmandLightTheme : R.style.OsmandDarkTheme); final String title = Algorithms.isEmpty(lang) ? a.getName() : a.getName(lang); LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); final Toolbar topBar = new Toolbar(ctx); topBar.setClickable(true); Drawable back = app.getIconsCache().getIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha); topBar.setNavigationIcon(back); topBar.setNavigationContentDescription(R.string.access_shared_string_navigate_up); topBar.setTitle(title); topBar.setBackgroundColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTabBackground))); topBar.setTitleTextColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTextColor))); String lng = a.getContentLanguage("content", preferredLang, "en"); if (Algorithms.isEmpty(lng)) { lng = "en"; } final String langSelected = lng; String content = a.getDescription(langSelected); final Button bottomBar = new Button(ctx); bottomBar.setText(R.string.read_full_article); bottomBar.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { String article = "https://" + langSelected.toLowerCase() + ".wikipedia.org/wiki/" + title.replace(' ', '_'); Intent i = new Intent(Intent.ACTION_VIEW); i.setData(Uri.parse(article)); ctx.startActivity(i); } }); MenuItem mi = topBar.getMenu().add(langSelected.toUpperCase()).setOnMenuItemClickListener(new OnMenuItemClickListener() { @Override public boolean onMenuItemClick(final MenuItem item) { showPopupLangMenu(ctx, topBar, app, a, dialog, langSelected); return true; } }); MenuItemCompat.setShowAsAction(mi, MenuItem.SHOW_AS_ACTION_ALWAYS); topBar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { dialog.dismiss(); } }); final WebView wv = new WebView(ctx); WebSettings settings = wv.getSettings(); settings.setDefaultTextEncodingName("utf-8"); settings.setBuiltInZoomControls(true); settings.setDisplayZoomControls(false); settings.setSupportZoom(true); //Scale web view font size with system font size float scale = ctx.getResources().getConfiguration().fontScale; if (android.os.Build.VERSION.SDK_INT >= 14) { settings.setTextZoom((int) (scale * 100f)); } else { if (scale <= 0.7f) { settings.setTextSize(WebSettings.TextSize.SMALLEST); } else if (scale <= 0.85f) { settings.setTextSize(WebSettings.TextSize.SMALLER); } else if (scale <= 1.0f) { settings.setTextSize(WebSettings.TextSize.NORMAL); } else if (scale <= 1.15f) { settings.setTextSize(WebSettings.TextSize.LARGER); } else { settings.setTextSize(WebSettings.TextSize.LARGEST); } } wv.loadDataWithBaseURL(null, content, "text/html", "UTF-8", null); // wv.loadUrl(OsMoService.SIGN_IN_URL + app.getSettings().OSMO_DEVICE_KEY.get()); //For pinch zooming to work WebView must not be inside ScrollView //ScrollView scrollView = new ScrollView(ctx); ll.addView(topBar); LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0); lp.weight = 1; //ll.addView(scrollView, lp); //scrollView.addView(wv); ll.addView(wv, lp); ll.addView(bottomBar, new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); dialog.setContentView(ll); wv.setFocusable(true); wv.setFocusableInTouchMode(true); wv.requestFocus(View.FOCUS_DOWN); wv.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: if (!v.hasFocus()) { v.requestFocus(); } break; } return false; } }); dialog.setCancelable(true); dialog.show(); } private static void showText(final Context ctx, final OsmandApplication app, final String text, String title) { final Dialog dialog = new Dialog(ctx, app.getSettings().isLightContent() ? R.style.OsmandLightTheme : R.style.OsmandDarkTheme); LinearLayout ll = new LinearLayout(ctx); ll.setOrientation(LinearLayout.VERTICAL); final Toolbar topBar = new Toolbar(ctx); topBar.setClickable(true); Drawable back = app.getIconsCache().getIcon(R.drawable.abc_ic_ab_back_mtrl_am_alpha); topBar.setNavigationIcon(back); topBar.setNavigationContentDescription(R.string.access_shared_string_navigate_up); topBar.setTitle(title); topBar.setBackgroundColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTabBackground))); topBar.setTitleTextColor(ContextCompat.getColor(ctx, getResIdFromAttribute(ctx, R.attr.pstsTextColor))); topBar.setNavigationOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { dialog.dismiss(); } }); final TextView textView = new TextView(ctx); LinearLayout.LayoutParams llTextParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); int textMargin = dpToPx(app, 10f); boolean light = app.getSettings().isLightContent(); textView.setLayoutParams(llTextParams); textView.setPadding(textMargin, textMargin, textMargin, textMargin); textView.setTextSize(16); textView.setTextColor(ContextCompat.getColor(app, light ? R.color.ctx_menu_info_text_light : R.color.ctx_menu_info_text_dark)); textView.setAutoLinkMask(Linkify.ALL); textView.setLinksClickable(true); textView.setText(text); ScrollView scrollView = new ScrollView(ctx); ll.addView(topBar); LayoutParams lp = new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT, 0); lp.weight = 1; ll.addView(scrollView, lp); scrollView.addView(textView); dialog.setContentView(ll); dialog.setCancelable(true); dialog.show(); } protected static void showPopupLangMenu(final Context ctx, Toolbar tb, final OsmandApplication app, final Amenity a, final Dialog dialog, final String langSelected) { final PopupMenu optionsMenu = new PopupMenu(ctx, tb, Gravity.RIGHT); Set<String> namesSet = new TreeSet<>(); namesSet.addAll(a.getNames("content", "en")); namesSet.addAll(a.getNames("description", "en")); Map<String, String> names = new HashMap<>(); for (String n : namesSet) { names.put(n, FileNameTranslationHelper.getVoiceName(ctx, n)); } String selectedLangName = names.get(langSelected); if (selectedLangName != null) { names.remove(langSelected); } Map<String, String> sortedNames = AndroidUtils.sortByValue(names); if (selectedLangName != null) { MenuItem item = optionsMenu.getMenu().add(selectedLangName); item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { dialog.dismiss(); showWiki(ctx, app, a, langSelected); return true; } }); } for (final Map.Entry<String, String> e : sortedNames.entrySet()) { MenuItem item = optionsMenu.getMenu().add(e.getValue()); item.setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() { @Override public boolean onMenuItemClick(MenuItem item) { dialog.dismiss(); showWiki(ctx, app, a, e.getKey()); return true; } }); } optionsMenu.show(); } @Override public PointDescription getObjectName(Object o) { if (o instanceof Amenity) { return new PointDescription(PointDescription.POINT_TYPE_POI, ((Amenity) o).getName( view.getSettings().MAP_PREFERRED_LOCALE.get(), view.getSettings().MAP_TRANSLITERATE_NAMES.get())); } return null; } @Override public boolean disableSingleTap() { return false; } @Override public boolean disableLongPressOnMap() { return false; } @Override public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List<Object> objects) { if (tileBox.getZoom() >= startZoom) { getAmenityFromPoint(tileBox, point, objects); } } @Override public LatLon getObjectLocation(Object o) { if (o instanceof Amenity) { return ((Amenity) o).getLocation(); } return null; } @Override public boolean isObjectClickable(Object o) { return o instanceof Amenity; } @Override public LatLon getTextLocation(Amenity o) { return o.getLocation(); } @Override public int getTextShift(Amenity o, RotatedTileBox rb) { return getRadiusPoi(rb); } @Override public String getText(Amenity o) { return o.getName(view.getSettings().MAP_PREFERRED_LOCALE.get(), view.getSettings().MAP_TRANSLITERATE_NAMES.get()); } @Override public void newRouteIsCalculated(boolean newRoute, ValueHolder<Boolean> showToast) { } @Override public void routeWasCancelled() { } @Override public void routeWasFinished() { } public static int dpToPx(Context ctx, float dp) { Resources r = ctx.getResources(); return (int) TypedValue.applyDimension( COMPLEX_UNIT_DIP, dp, r.getDisplayMetrics() ); } }