package net.osmand.plus.views; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ColorFilter; import android.graphics.Paint; import android.graphics.Paint.Align; import android.graphics.Paint.Style; import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.PorterDuff.Mode; import android.graphics.PorterDuffColorFilter; import android.os.AsyncTask; import android.support.annotation.ColorInt; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v4.content.ContextCompat; 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.plus.GPXDatabase.GpxDataItem; import net.osmand.plus.GPXUtilities; import net.osmand.plus.GPXUtilities.GPXFile; import net.osmand.plus.GPXUtilities.TrkSegment; import net.osmand.plus.GPXUtilities.WptPt; import net.osmand.plus.GpxSelectionHelper; import net.osmand.plus.GpxSelectionHelper.GpxDisplayGroup; import net.osmand.plus.GpxSelectionHelper.GpxDisplayItem; import net.osmand.plus.GpxSelectionHelper.SelectedGpxFile; import net.osmand.plus.OsmandApplication; import net.osmand.plus.OsmandSettings.CommonPreference; import net.osmand.plus.R; import net.osmand.plus.base.FavoriteImageDrawable; import net.osmand.plus.render.OsmandRenderer; import net.osmand.plus.render.OsmandRenderer.RenderingContext; import net.osmand.plus.views.MapTextLayer.MapTextProvider; import net.osmand.render.RenderingRuleProperty; import net.osmand.render.RenderingRuleSearchRequest; import net.osmand.render.RenderingRulesStorage; import java.io.File; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; public class GPXLayer extends OsmandMapLayer implements ContextMenuLayer.IContextMenuProvider, ContextMenuLayer.IMoveObjectProvider, MapTextProvider<WptPt> { private OsmandMapTileView view; private Paint paint; private Paint paint2; private boolean isPaint2; private Paint shadowPaint; private boolean isShadowPaint; private Paint paint_1; private boolean isPaint_1; private int cachedHash; private int cachedColor; private Paint paintIcon; private Bitmap pointSmall; private int currentTrackColor; private Bitmap selectedPoint; private LatLon selectedPointLatLon; private static final int startZoom = 7; private GpxSelectionHelper selectedGpxHelper; private Paint paintBmp; private List<WptPt> cache = new ArrayList<>(); private Map<WptPt, SelectedGpxFile> pointFileMap = new HashMap<>(); private MapTextLayer textLayer; private Paint paintOuter; private Paint paintInnerCircle; private Paint paintTextIcon; private OsmandRenderer osmandRenderer; private List<TrkSegment> points; private GPXFile gpx; private ContextMenuLayer contextMenuLayer; @ColorInt private int visitedColor; @ColorInt private int defPointColor; @Override public void initLayer(OsmandMapTileView view) { this.view = view; selectedGpxHelper = view.getApplication().getSelectedGpxHelper(); osmandRenderer = view.getApplication().getResourceManager().getRenderer().getRenderer(); initUI(); } private void initUI() { paint = new Paint(); paint.setStyle(Style.STROKE); paint.setAntiAlias(true); paint2 = new Paint(); paint2.setStyle(Style.STROKE); paint2.setAntiAlias(true); shadowPaint = new Paint(); shadowPaint.setStyle(Style.STROKE); shadowPaint.setAntiAlias(true); paint_1 = new Paint(); paint_1.setStyle(Style.STROKE); paint_1.setAntiAlias(true); paintBmp = new Paint(); paintBmp.setAntiAlias(true); paintBmp.setFilterBitmap(true); paintBmp.setDither(true); paintTextIcon = new Paint(); paintTextIcon.setTextSize(10 * view.getDensity()); paintTextIcon.setTextAlign(Align.CENTER); paintTextIcon.setFakeBoldText(true); paintTextIcon.setColor(Color.BLACK); paintTextIcon.setAntiAlias(true); textLayer = view.getLayerByClass(MapTextLayer.class); paintOuter = new Paint(); paintOuter.setColor(0x88555555); paintOuter.setAntiAlias(true); paintOuter.setStyle(Style.FILL_AND_STROKE); paintInnerCircle = new Paint(); paintInnerCircle.setStyle(Style.FILL_AND_STROKE); paintInnerCircle.setColor(0xddFFFFFF); paintInnerCircle.setAntiAlias(true); paintIcon = new Paint(); pointSmall = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_white_shield_small); selectedPoint = BitmapFactory.decodeResource(view.getResources(), R.drawable.map_default_location); contextMenuLayer = view.getLayerByClass(ContextMenuLayer.class); visitedColor = ContextCompat.getColor(view.getApplication(), R.color.color_ok); defPointColor = ContextCompat.getColor(view.getApplication(), R.color.gpx_color_point); } @Override public void onDraw(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { if (contextMenuLayer.getMoveableObject() instanceof WptPt) { WptPt objectInMotion = (WptPt) contextMenuLayer.getMoveableObject(); PointF pf = contextMenuLayer.getMovableCenterPoint(tileBox); SelectedGpxFile gpxFile = pointFileMap.get(objectInMotion); if (gpxFile != null) { drawBigPoint(canvas, objectInMotion, getFileColor(gpxFile), pf.x, pf.y); } } } @Override public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSettings settings) { if (points != null) { updatePaints(0, false, false, settings, tileBox); for (TrkSegment ts : points) ts.drawRenderers(view.getZoom(), paint, canvas, tileBox); } else { List<SelectedGpxFile> selectedGPXFiles = selectedGpxHelper.getSelectedGPXFiles(); cache.clear(); currentTrackColor = view.getSettings().CURRENT_TRACK_COLOR.get(); if (!selectedGPXFiles.isEmpty()) { drawSelectedFilesSegments(canvas, tileBox, selectedGPXFiles, settings); canvas.rotate(-tileBox.getRotate(), tileBox.getCenterPixelX(), tileBox.getCenterPixelY()); drawSelectedFilesSplits(canvas, tileBox, selectedGPXFiles, settings); drawSelectedFilesPoints(canvas, tileBox, selectedGPXFiles); } if (textLayer != null && textLayer.isVisible()) { textLayer.putData(this, cache); } } } public void updateLayerStyle() { cachedHash = -1; } private int updatePaints(int color, boolean routePoints, boolean currentTrack, DrawSettings nightMode, RotatedTileBox tileBox) { RenderingRulesStorage rrs = view.getApplication().getRendererRegistry().getCurrentSelectedRenderer(); final boolean isNight = nightMode != null && nightMode.isNightMode(); int hsh = calculateHash(rrs, routePoints, isNight, tileBox.getMapDensity(), tileBox.getZoom()); if (hsh != cachedHash) { cachedHash = hsh; cachedColor = ContextCompat.getColor(view.getApplication(), R.color.gpx_track); if (rrs != null) { RenderingRuleSearchRequest req = new RenderingRuleSearchRequest(rrs); req.setBooleanFilter(rrs.PROPS.R_NIGHT_MODE, isNight); CommonPreference<String> p = view.getSettings().getCustomRenderProperty("currentTrackColor"); if (p != null && p.isSet()) { RenderingRuleProperty ctColor = rrs.PROPS.get("currentTrackColor"); if (ctColor != null) { req.setStringFilter(ctColor, p.get()); } } CommonPreference<String> p2 = view.getSettings().getCustomRenderProperty("currentTrackWidth"); if (p2 != null && p2.isSet()) { RenderingRuleProperty ctWidth = rrs.PROPS.get("currentTrackWidth"); if (ctWidth != null) { req.setStringFilter(ctWidth, p2.get()); } } String additional = ""; if (routePoints) { additional = "routePoints=true"; } if (currentTrack) { additional = (additional.length() == 0 ? "" : ";") + "currentTrack=true"; } req.setIntFilter(rrs.PROPS.R_MINZOOM, tileBox.getZoom()); req.setIntFilter(rrs.PROPS.R_MAXZOOM, tileBox.getZoom()); if (additional.length() > 0) { req.setStringFilter(rrs.PROPS.R_ADDITIONAL, additional); } if (req.searchRenderingAttribute("gpx")) { RenderingContext rc = new OsmandRenderer.RenderingContext(view.getContext()); rc.setDensityValue((float) tileBox.getMapDensity()); cachedColor = req.getIntPropertyValue(rrs.PROPS.R_COLOR); osmandRenderer.updatePaint(req, paint, 0, false, rc); isPaint2 = osmandRenderer.updatePaint(req, paint2, 1, false, rc); isPaint_1 = osmandRenderer.updatePaint(req, paint_1, -1, false, rc); isShadowPaint = req.isSpecified(rrs.PROPS.R_SHADOW_RADIUS); if (isShadowPaint) { ColorFilter cf = new PorterDuffColorFilter(req.getIntPropertyValue(rrs.PROPS.R_SHADOW_COLOR), Mode.SRC_IN); shadowPaint.setColorFilter(cf); shadowPaint.setStrokeWidth(paint.getStrokeWidth() + 2 * rc.getComplexValue(req, rrs.PROPS.R_SHADOW_RADIUS)); } } else { System.err.println("Rendering attribute gpx is not found !"); paint.setStrokeWidth(7 * view.getDensity()); } } } paint.setColor(color == 0 ? cachedColor : color); return cachedColor; } private int calculateHash(Object... o) { return Arrays.hashCode(o); } private void drawSelectedFilesSplits(Canvas canvas, RotatedTileBox tileBox, List<SelectedGpxFile> selectedGPXFiles, DrawSettings settings) { if (tileBox.getZoom() >= startZoom) { // request to load for (SelectedGpxFile g : selectedGPXFiles) { List<GpxDisplayGroup> groups = g.getDisplayGroups(); if (groups != null) { for (GpxDisplayGroup group : groups) { List<GpxDisplayItem> items = group.getModifiableList(); drawSplitItems(canvas, tileBox, items, settings); } } } } } private void drawSplitItems(Canvas canvas, RotatedTileBox tileBox, List<GpxDisplayItem> items, DrawSettings settings) { final QuadRect latLonBounds = tileBox.getLatLonBounds(); int r = (int) (12 * tileBox.getDensity()); int dr = r * 3 / 2; int px = -1; int py = -1; for (int k = 0; k < items.size(); k++) { GpxDisplayItem i = items.get(k); WptPt o = i.locationEnd; if (o != null && o.lat >= latLonBounds.bottom && o.lat <= latLonBounds.top && o.lon >= latLonBounds.left && o.lon <= latLonBounds.right) { int x = (int) tileBox.getPixXFromLatLon(o.lat, o.lon); int y = (int) tileBox.getPixYFromLatLon(o.lat, o.lon); if (px != -1 || py != -1) { if (Math.abs(x - px) <= dr && Math.abs(y - py) <= dr) { continue; } } px = x; py = y; String nm = i.splitName; if (nm != null) { int ind = nm.indexOf(' '); if (ind > 0) { nm = nm.substring(0, ind); } canvas.drawCircle(x, y, r + (float) Math.ceil(tileBox.getDensity()), paintOuter); canvas.drawCircle(x, y, r - (float) Math.ceil(tileBox.getDensity()), paintInnerCircle); paintTextIcon.setTextSize(r); canvas.drawText(nm, x, y + r / 2, paintTextIcon); } } } } private void drawSelectedFilesPoints(Canvas canvas, RotatedTileBox tileBox, List<SelectedGpxFile> selectedGPXFiles) { if (tileBox.getZoom() >= startZoom) { float iconSize = FavoriteImageDrawable.getOrCreate(view.getContext(), 0, true).getIntrinsicWidth() * 3 / 2.5f; QuadTree<QuadRect> boundIntersections = initBoundIntersections(tileBox); List<LatLon> fullObjectsLatLon = new ArrayList<>(); List<LatLon> smallObjectsLatLon = new ArrayList<>(); // request to load final QuadRect latLonBounds = tileBox.getLatLonBounds(); for (SelectedGpxFile g : selectedGPXFiles) { List<WptPt> pts = getListStarPoints(g); List<WptPt> fullObjects = new ArrayList<>(); @ColorInt int fileColor = getFileColor(g); for (WptPt o : pts) { if (o.lat >= latLonBounds.bottom && o.lat <= latLonBounds.top && o.lon >= latLonBounds.left && o.lon <= latLonBounds.right && o != contextMenuLayer.getMoveableObject()) { cache.add(o); float x = tileBox.getPixXFromLatLon(o.lat, o.lon); float y = tileBox.getPixYFromLatLon(o.lat, o.lon); if (intersects(boundIntersections, x, y, iconSize, iconSize)) { @ColorInt int pointColor = getPointColor(o, fileColor); paintIcon.setColorFilter(new PorterDuffColorFilter(pointColor, PorterDuff.Mode.MULTIPLY)); canvas.drawBitmap(pointSmall, x - pointSmall.getWidth() / 2, y - pointSmall.getHeight() / 2, paintIcon); smallObjectsLatLon.add(new LatLon(o.lat, o.lon)); } else { fullObjects.add(o); fullObjectsLatLon.add(new LatLon(o.lat, o.lon)); } } pointFileMap.put(o, g); } for (WptPt o : fullObjects) { float x = tileBox.getPixXFromLatLon(o.lat, o.lon); float y = tileBox.getPixYFromLatLon(o.lat, o.lon); drawBigPoint(canvas, o, fileColor, x, y); } } if (selectedPointLatLon != null && selectedPointLatLon.getLatitude() >= latLonBounds.bottom && selectedPointLatLon.getLatitude() <= latLonBounds.top && selectedPointLatLon.getLongitude() >= latLonBounds.left && selectedPointLatLon.getLongitude() <= latLonBounds.right) { float x = tileBox.getPixXFromLatLon(selectedPointLatLon.getLatitude(), selectedPointLatLon.getLongitude()); float y = tileBox.getPixYFromLatLon(selectedPointLatLon.getLatitude(), selectedPointLatLon.getLongitude()); paintIcon.setColorFilter(null); canvas.drawBitmap(selectedPoint, x - selectedPoint.getWidth() / 2, y - selectedPoint.getHeight() / 2, paintIcon); } this.fullObjectsLatLon = fullObjectsLatLon; this.smallObjectsLatLon = smallObjectsLatLon; } } private int getFileColor(@NonNull SelectedGpxFile g) { return g.getColor() == 0 ? defPointColor : g.getColor(); } private void drawBigPoint(Canvas canvas, WptPt o, int fileColor, float x, float y) { int pointColor = getPointColor(o, fileColor); FavoriteImageDrawable fid = FavoriteImageDrawable.getOrCreate(view.getContext(), pointColor, true); fid.drawBitmapInCenter(canvas, x, y); } @ColorInt private int getPointColor(WptPt o, @ColorInt int fileColor) { boolean visit = isPointVisited(o); return visit ? visitedColor : o.getColor(fileColor); } private void drawSelectedFilesSegments(Canvas canvas, RotatedTileBox tileBox, List<SelectedGpxFile> selectedGPXFiles, DrawSettings settings) { for (SelectedGpxFile g : selectedGPXFiles) { GpxDataItem gpxDataItem = null; if (!g.isShowCurrentTrack()) { gpxDataItem = view.getApplication().getGpxDatabase().getItem(new File(g.getGpxFile().path)); } List<TrkSegment> segments = g.getPointsToDisplay(); for (TrkSegment ts : segments) { int color = gpxDataItem != null ? gpxDataItem.getColor() : 0; if (g.isShowCurrentTrack()) { color = currentTrackColor; } if (color == 0) { color = ts.getColor(cachedColor); } if (ts.renders.isEmpty() // only do once (CODE HERE NEEDS TO BE UI INSTEAD) && !ts.points.isEmpty()) { // hmmm. 0-point tracks happen, but.... how? if (g.isShowCurrentTrack()) { ts.renders.add(new Renderable.CurrentTrack(ts.points)); } else { ts.renders.add(new Renderable.StandardTrack(ts.points, 17.2)); } } updatePaints(color, g.isRoutePoints(), g.isShowCurrentTrack(), settings, tileBox); ts.drawRenderers(view.getZoom(), paint, canvas, tileBox); } } } private boolean isPointVisited(WptPt o) { boolean visit = false; String visited = o.getExtensionsToRead().get("VISITED_KEY"); if (visited != null && !visited.equals("0")) { visit = true; } return visit; } private List<WptPt> getListStarPoints(SelectedGpxFile g) { return g.getGpxFile().points; } public LatLon getSelectedPointLatLon() { return selectedPointLatLon; } public void setSelectedPointLatLon(LatLon selectedPointLatLon) { this.selectedPointLatLon = selectedPointLatLon; } private boolean calculateBelongs(int ex, int ey, int objx, int objy, int radius) { return (Math.abs(objx - ex) <= radius * 2 && Math.abs(objy - ey) <= radius * 2); // return Math.abs(objx - ex) <= radius && (ey - objy) <= radius / 2 && (objy - ey) <= 3 * radius ; } public void getWptFromPoint(RotatedTileBox tb, PointF point, List<? super WptPt> res) { int r = (int) (15 * tb.getDensity()); int ex = (int) point.x; int ey = (int) point.y; for (SelectedGpxFile g : selectedGpxHelper.getSelectedGPXFiles()) { List<WptPt> pts = getListStarPoints(g); // int fcolor = g.getColor() == 0 ? clr : g.getColor(); for (WptPt n : pts) { int x = (int) tb.getPixXFromLatLon(n.lat, n.lon); int y = (int) tb.getPixYFromLatLon(n.lat, n.lon); if (calculateBelongs(ex, ey, x, y, r)) { res.add(n); } } } } @Override public PointDescription getObjectName(Object o) { if (o instanceof WptPt) { return new PointDescription(PointDescription.POINT_TYPE_WPT, ((WptPt) o).name); //$NON-NLS-1$ } return null; } @Override public boolean disableSingleTap() { return false; } @Override public boolean disableLongPressOnMap() { return false; } @Override public boolean isObjectClickable(Object o) { return o instanceof WptPt; } @Override public void collectObjectsFromPoint(PointF point, RotatedTileBox tileBox, List<Object> res) { if (tileBox.getZoom() >= startZoom) { getWptFromPoint(tileBox, point, res); } } @Override public LatLon getObjectLocation(Object o) { if (o instanceof WptPt) { return new LatLon(((WptPt) o).lat, ((WptPt) o).lon); } return null; } @Override public void destroyLayer() { } @Override public boolean drawInScreenPixels() { return false; } @Override public boolean onLongPressEvent(PointF point, RotatedTileBox tileBox) { return false; } @Override public LatLon getTextLocation(WptPt o) { return new LatLon(o.lat, o.lon); } @Override public int getTextShift(WptPt o, RotatedTileBox rb) { return (int) (16 * rb.getDensity()); } @Override public String getText(WptPt o) { return o.name; } public void setGivenGpx(GPXFile gpx) { this.gpx = gpx; this.points = (gpx == null ? null : gpx.proccessPoints()); } @Override public boolean isObjectMovable(Object o) { return o instanceof WptPt; } @Override public void applyNewObjectPosition(@NonNull Object o, @NonNull LatLon position, @Nullable ContextMenuLayer.ApplyMovedObjectCallback callback) { if (o instanceof WptPt) { WptPt objectInMotion = (WptPt) o; GPXFile gpxFile = pointFileMap.get(objectInMotion).getGpxFile(); gpxFile.updateWptPt(objectInMotion, position.getLatitude(), position.getLongitude(), System.currentTimeMillis(), objectInMotion.desc, objectInMotion.name, objectInMotion.category, objectInMotion.getColor()); new SaveGpxFileAsyncTask(view.getApplication(), callback, objectInMotion).execute(gpxFile); } else if (callback != null) { callback.onApplyMovedObject(false, o); } } static class SaveGpxFileAsyncTask extends AsyncTask<GPXFile, Void, String> { private final OsmandApplication app; @Nullable private final ContextMenuLayer.ApplyMovedObjectCallback callback; @Nullable private final WptPt point; SaveGpxFileAsyncTask(OsmandApplication app, @Nullable ContextMenuLayer.ApplyMovedObjectCallback callback, @Nullable WptPt point) { this.app = app; this.callback = callback; this.point = point; } @Override protected String doInBackground(GPXFile... params) { GPXFile gpxFile = params[0]; return GPXUtilities.writeGpxFile(new File(gpxFile.path), gpxFile, app); } @Override protected void onPostExecute(String errorMessage) { if (callback != null) { callback.onApplyMovedObject(errorMessage == null, point); } } } }