diff --git a/OsmAnd-api/build.gradle b/OsmAnd-api/build.gradle index e2b703b4177..03859996bf3 100644 --- a/OsmAnd-api/build.gradle +++ b/OsmAnd-api/build.gradle @@ -2,8 +2,8 @@ apply plugin: 'com.android.library' apply plugin: 'ivy-publish' android { - compileSdk 34 - buildToolsVersion = "34.0.0" + compileSdk 35 + buildToolsVersion = "35.0.0" defaultConfig { minSdkVersion 24 diff --git a/OsmAnd-shared/build.gradle.kts b/OsmAnd-shared/build.gradle.kts index 8ed7c9582c9..42b0b12b39b 100644 --- a/OsmAnd-shared/build.gradle.kts +++ b/OsmAnd-shared/build.gradle.kts @@ -51,6 +51,7 @@ kotlin { val commonLoggingVersion = "1.2" val coroutinesVersion = "1.8.1" val statelyVersion = "2.1.0" + val coilVersion = "3.1.0" sourceSets { commonMain.dependencies { @@ -62,6 +63,7 @@ kotlin { implementation("com.squareup.okio:okio:$okioVersion") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") implementation("co.touchlab:stately-concurrent-collections:$statelyVersion") + implementation("io.coil-kt.coil3:coil-core:$coilVersion") } jvmMain.dependencies { //implementation(kotlin("stdlib-jdk8")) @@ -73,6 +75,7 @@ kotlin { implementation("androidx.sqlite:sqlite:$sqliteVersion") implementation("androidx.sqlite:sqlite-framework:$sqliteVersion") implementation("net.sf.kxml:kxml2:$kxml2Version") + implementation("io.coil-kt.coil3:coil-network-okhttp:$coilVersion") } iosMain.dependencies { implementation("co.touchlab:sqliter-driver:$sqliterVersion") @@ -86,7 +89,7 @@ kotlin { android { namespace = "net.osmand.shared" - compileSdk = 34 + compileSdk = 35 compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/OsmAnd-shared/src/androidMain/kotlin/net/osmand/shared/util/NetworkImageLoader.kt b/OsmAnd-shared/src/androidMain/kotlin/net/osmand/shared/util/NetworkImageLoader.kt new file mode 100644 index 00000000000..b507c97b344 --- /dev/null +++ b/OsmAnd-shared/src/androidMain/kotlin/net/osmand/shared/util/NetworkImageLoader.kt @@ -0,0 +1,76 @@ +package net.osmand.shared.util + +import android.content.Context +import coil3.Bitmap +import coil3.ImageLoader +import coil3.disk.DiskCache +import coil3.disk.directory +import coil3.memory.MemoryCache +import coil3.request.Disposable +import coil3.request.ImageRequest +import coil3.request.allowHardware +import coil3.toBitmap + +class NetworkImageLoader(private val context: Context, useDiskCache: Boolean = false) { + + companion object { + private const val DISK_CACHE_SIZE = 1024 * 1024 * 100L // 100MB + private const val DISK_IMAGES_CACHE_DIR = "net_images_cache" + } + + private var imageLoader: ImageLoader = ImageLoader.Builder(context) + .allowHardware(false) + .memoryCache { + MemoryCache.Builder() + .maxSizePercent(context, 0.25) + .build() + } + .apply { + if (useDiskCache) { + diskCache { + DiskCache.Builder() + .directory(context.cacheDir.resolve(DISK_IMAGES_CACHE_DIR)) + .maxSizeBytes(DISK_CACHE_SIZE) + .build() + } + } + } + .build() + + fun loadImage( + url: String, callback: ImageLoaderCallback, handlePlaceholder: Boolean = false + ): LoadingImage { + val request = ImageRequest.Builder(context) + .data(url) + .target( + onStart = { placeholder -> + callback.onStart(placeholder?.takeIf { handlePlaceholder }?.toBitmap()) + }, + onSuccess = { result -> + callback.onSuccess(result.toBitmap()) + }, + onError = { _ -> + callback.onError() + }) + .build() + + return LoadingImage(url, imageLoader.enqueue(request)) + } +} + +interface ImageLoaderCallback { + + fun onStart(bitmap: Bitmap?) + + fun onSuccess(bitmap: Bitmap) + + fun onError() +} + +class LoadingImage(val url: String, private val disposable: Disposable) { + + fun cancel(): Boolean { + disposable.dispose() + return true + } +} \ No newline at end of file diff --git a/OsmAnd/build-common.gradle b/OsmAnd/build-common.gradle index 81cb48fa97d..81cf69ff32b 100644 --- a/OsmAnd/build-common.gradle +++ b/OsmAnd/build-common.gradle @@ -3,8 +3,8 @@ tasks.register('printc') { } android { - compileSdk 34 - buildToolsVersion = "34.0.0" + compileSdk 35 + buildToolsVersion = "35.0.0" // compileNdkVersion "android-ndk-r17b" namespace = "net.osmand.plus" defaultConfig { @@ -404,6 +404,9 @@ dependencies { // turn off for now //implementation 'com.atilika.kuromoji:kuromoji-ipadic:0.9.0' implementation 'com.squareup.picasso:picasso:2.71828' + //implementation("io.coil-kt.coil3:coil-core:3.1.0") + //implementation("io.coil-kt.coil3:coil-network-okhttp:3.1.0") + implementation 'me.zhanghai.android.materialprogressbar:library:1.4.2' implementation "net.osmand:antpluginlib:3.8.0@aar" // JS core diff --git a/OsmAnd/build.gradle b/OsmAnd/build.gradle index 55eb4b708dd..c9457a9030b 100644 --- a/OsmAnd/build.gradle +++ b/OsmAnd/build.gradle @@ -211,6 +211,11 @@ android { println(osmandTask.getName() + " merge resources") osmandTask.dependsOn(collectExternalResources) } + def generateResources= "generate${taskName}Resources" + tasks.named(generateResources).configure { osmandTask -> + println(osmandTask.getName() + " merge resources") + osmandTask.dependsOn(collectExternalResources) + } } } diff --git a/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProvider.java b/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProvider.java index d1847f96a7b..eea853dfdfb 100644 --- a/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProvider.java +++ b/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProvider.java @@ -1,5 +1,7 @@ package net.osmand.plus.exploreplaces; +import androidx.annotation.NonNull; + import net.osmand.data.Amenity; import net.osmand.data.LatLon; import net.osmand.data.ExploreTopPlacePoint; @@ -17,7 +19,7 @@ public interface ExplorePlacesProvider { @NotNull List getDataCollection(QuadRect mapRect, int limit); - void showPointInContextMenu(@NotNull MapActivity it, @NotNull ExploreTopPlacePoint item); + void showPointInContextMenu(@NotNull MapActivity mapActivity, @NotNull ExploreTopPlacePoint item); void addListener(ExplorePlacesListener listener); @@ -30,6 +32,8 @@ public interface ExplorePlacesProvider { // data version is increased once new data is downloaded int getDataVersion(); + boolean isLoadingRect(@NonNull QuadRect rect); + interface ExplorePlacesListener { // once new data is downloaded data version is increased void onNewExplorePlacesDownloaded(); diff --git a/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProviderJava.java b/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProviderJava.java index abe53b5b85a..7e286ac2917 100644 --- a/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProviderJava.java +++ b/OsmAnd/src/net/osmand/plus/exploreplaces/ExplorePlacesProviderJava.java @@ -23,8 +23,10 @@ import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; // TODO use gzip in loading @@ -41,18 +43,18 @@ // Extra: display new categories from web public class ExplorePlacesProviderJava implements ExplorePlacesProvider { - private static final int DEFAULT_LIMIT_POINTS = 200; + public static final int DEFAULT_LIMIT_POINTS = 200; private static final int NEARBY_MIN_RADIUS = 50; private static final int MAX_TILES_PER_QUAD_RECT = 12; private static final double LOAD_ALL_TINY_RECT = 0.5; - private OsmandApplication app; + private final OsmandApplication app; private volatile int startedTasks = 0; private volatile int finishedTasks = 0; - private final Set loadingTiles = new HashSet<>(); // Track tiles being loaded + private final Map loadingTiles = new HashMap<>(); // Track tiles being loaded public ExplorePlacesProviderJava(OsmandApplication app) { this.app = app; @@ -98,11 +100,13 @@ private String getLang() { return preferredLang; } + @NonNull public List getDataCollection(QuadRect rect) { return getDataCollection(rect, DEFAULT_LIMIT_POINTS); } - public List getDataCollection(QuadRect rect, int limit) { + @NonNull + public List getDataCollection(QuadRect rect, int limit) { if (rect == null) { return Collections.emptyList(); } @@ -170,17 +174,22 @@ public List getDataCollection(QuadRect rect, int limit) { @SuppressLint("DefaultLocale") private void loadTile(int zoom, int tileX, int tileY, String queryLang, PlacesDatabaseHelper dbHelper) { + double left; + double right; + double top; + double bottom; + synchronized (loadingTiles) { String tileKey = zoom + "_" + tileX + "_" + tileY; - if (loadingTiles.contains(tileKey)) { + if (loadingTiles.containsKey(tileKey)) { return; } - loadingTiles.add(tileKey); + left = MapUtils.getLongitudeFromTile(zoom, tileX); + right = MapUtils.getLongitudeFromTile(zoom, tileX + 1); + top = MapUtils.getLatitudeFromTile(zoom, tileY); + bottom = MapUtils.getLatitudeFromTile(zoom, tileY + 1); + loadingTiles.put(tileKey, new QuadRect(left, top, right, bottom)); } - double left = MapUtils.getLongitudeFromTile(zoom, tileX); - double right = MapUtils.getLongitudeFromTile(zoom, tileX + 1); - double top = MapUtils.getLatitudeFromTile(zoom, tileY); - double bottom = MapUtils.getLatitudeFromTile(zoom, tileY + 1); KQuadRect tileRect = new KQuadRect(left, top, right, bottom); // Increment the task counter @@ -204,11 +213,9 @@ public void onFinish(@NonNull List result) { finishedTasks++; // Increment the finished task counter notifyListeners(startedTasks != finishedTasks); } - if (result != null) { - // Store the data in the database for the current tile - dbHelper.insertPlaces(zoom, ftileX, ftileY, queryLang, result); - } - // Remove the tile from the loading set + // Store the data in the database for the current tile + dbHelper.insertPlaces(zoom, ftileX, ftileY, queryLang, result); + // Remove the tile from the loading set String tileKey = zoom + "_" + ftileX + "_" + ftileY; synchronized (loadingTiles) { loadingTiles.remove(tileKey); @@ -217,7 +224,7 @@ public void onFinish(@NonNull List result) { }).execute(); } - public void showPointInContextMenu(MapActivity mapActivity, ExploreTopPlacePoint point) { + public void showPointInContextMenu(@NonNull MapActivity mapActivity, @NonNull ExploreTopPlacePoint point) { double latitude = point.getLatitude(); double longitude = point.getLongitude(); app.getSettings().setMapLocationToShow( @@ -267,4 +274,15 @@ public int getDataVersion() { // data version is increased once new data is downloaded return finishedTasks; } + + public boolean isLoadingRect(@NonNull QuadRect rect) { + synchronized (loadingTiles) { + for (QuadRect loadingRect : loadingTiles.values()) { + if (loadingRect.contains(rect)) { + return true; + } + } + return false; + } + } } \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/views/layers/ExploreTopPlacesLayer.java b/OsmAnd/src/net/osmand/plus/views/layers/ExploreTopPlacesLayer.java index 018b7814bc4..2b865636f87 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/ExploreTopPlacesLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/ExploreTopPlacesLayer.java @@ -1,23 +1,25 @@ package net.osmand.plus.views.layers; +import static net.osmand.plus.exploreplaces.ExplorePlacesProviderJava.DEFAULT_LIMIT_POINTS; + import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.PointF; -import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Target; - import net.osmand.PlatformUtil; import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.MapMarker; +import net.osmand.core.jni.MapMarkerBuilder; +import net.osmand.core.jni.MapMarkersCollection; import net.osmand.core.jni.PointI; -import net.osmand.data.LatLon; +import net.osmand.core.jni.QListMapMarker; import net.osmand.data.ExploreTopPlacePoint; +import net.osmand.data.LatLon; import net.osmand.data.PointDescription; import net.osmand.data.QuadRect; import net.osmand.data.QuadTree; @@ -30,12 +32,17 @@ import net.osmand.plus.views.layers.ContextMenuLayer.IContextMenuProvider; import net.osmand.plus.views.layers.base.OsmandMapLayer; import net.osmand.plus.views.layers.core.ExploreTopPlacesTileProvider; +import net.osmand.shared.util.ImageLoaderCallback; +import net.osmand.shared.util.LoadingImage; +import net.osmand.shared.util.NetworkImageLoader; import net.osmand.util.Algorithms; import org.apache.commons.logging.Log; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; public class ExploreTopPlacesLayer extends OsmandMapLayer implements IContextMenuProvider, ExplorePlacesProvider.ExplorePlacesListener { @@ -44,6 +51,7 @@ public class ExploreTopPlacesLayer extends OsmandMapLayer implements IContextMen private boolean nightMode; private ExploreTopPlacePoint selectedObject; + private static final int SELECTED_MARKER_ID = -1; private Bitmap cachedSmallIconBitmap; @@ -51,26 +59,40 @@ public class ExploreTopPlacesLayer extends OsmandMapLayer implements IContextMen private int requestZoom = 0; // null means disabled private ExploreTopPlacesTileProvider topPlacesMapLayerProvider; - private ExploreTopPlacesTileProvider selectedTopPlacesMapLayerProvider; private ExplorePlacesProvider explorePlacesProvider; private int cachedExploreDataVersion; private List places; // To refresh images - public static final String LOAD_NEARBY_IMAGES_TAG = "load_nearby_images"; private static final int TOP_LOAD_PHOTOS = 25; - private static final long DEBOUNCE_IMAGE_REFRESH = 5000; - + private static final long DEBOUNCE_IMAGE_REFRESH = 1000; private RotatedTileBox imagesDisplayedBox = null; private int imagesUpdatedVersion; private int imagesCachedVersion; private long lastImageCacheRefreshed = 0; + private final NetworkImageLoader imageLoader; + private final List loadingImages = new ArrayList<>(); + + private static class MapPoint { + private final PointI position; + private final boolean alreadyExists; + @Nullable + private final Bitmap imageBitmap; + + public MapPoint(PointI position, @Nullable Bitmap imageBitmap, boolean alreadyExists) { + this.position = position; + this.imageBitmap = imageBitmap; + this.alreadyExists = alreadyExists; + } + } public ExploreTopPlacesLayer(@NonNull Context ctx) { super(ctx); + + imageLoader = new NetworkImageLoader(ctx, false); } @Override @@ -103,9 +125,7 @@ protected void updateResources() { protected void cleanupResources() { super.cleanupResources(); deleteProvider(topPlacesMapLayerProvider); - deleteProvider(selectedTopPlacesMapLayerProvider); topPlacesMapLayerProvider = null; - selectedTopPlacesMapLayerProvider = null; explorePlacesProvider.removeListener(this); } @@ -140,7 +160,7 @@ public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSett places = null; } } - placesUpdated = placesUpdated || scheduleImageRefreshes(places, tileBox); + placesUpdated = placesUpdated || scheduleImageRefreshes(places, tileBox, placesUpdated); ExploreTopPlacePoint selectedObject = getSelectedNearbyPlace(); long selectedObjectId = selectedObject == null ? 0 : selectedObject.getId(); @@ -148,9 +168,9 @@ public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSett boolean selectedObjectChanged = selectedObjectId != lastSelectedObjectId; this.selectedObject = selectedObject; if (hasMapRenderer()) { - if ((mapActivityInvalidated || mapRendererChanged - || nightModeChanged || placesUpdated)) { - initProviderWithPoints(places); + if ((mapActivityInvalidated || mapRendererChanged || nightModeChanged || placesUpdated)) { + updateTopPlacesTileProvider(places != null); + updateTopPlacesCollection(places, tileBox); mapRendererChanged = false; } if (selectedObjectChanged) { @@ -163,7 +183,7 @@ public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSett QuadRect latLonBounds = tileBox.getLatLonBounds(); List fullObjectsLatLon = new ArrayList<>(); List smallObjectsLatLon = new ArrayList<>(); - drawPoints(places, latLonBounds, false, tileBox, boundIntersections, iconSize, canvas, + drawPoints(places, latLonBounds, tileBox, boundIntersections, iconSize, canvas, fullObjectsLatLon, smallObjectsLatLon); this.fullObjectsLatLon = fullObjectsLatLon; this.smallObjectsLatLon = smallObjectsLatLon; @@ -172,7 +192,7 @@ public void onPrepareBufferImage(Canvas canvas, RotatedTileBox tileBox, DrawSett mapActivityInvalidated = false; } - private void drawPoints(List pointsToDraw, QuadRect latLonBounds, boolean synced, RotatedTileBox tileBox, + private void drawPoints(List pointsToDraw, QuadRect latLonBounds, RotatedTileBox tileBox, QuadTree boundIntersections, float iconSize, Canvas canvas, List fullObjectsLatLon, List smallObjectsLatLon) { List fullObjects = new ArrayList<>(); @@ -202,7 +222,7 @@ private void drawPoints(List pointsToDraw, QuadRect latLon Bitmap bigBitmap = ExploreTopPlacesTileProvider.createBigBitmap(getApplication(), bitmap, point.getId() == getSelectedObjectId()); float x = tileBox.getPixXFromLatLon(point.getLatitude(), point.getLongitude()); float y = tileBox.getPixYFromLatLon(point.getLatitude(), point.getLongitude()); - canvas.drawBitmap(bigBitmap, x - bigBitmap.getWidth() / 2, y - bigBitmap.getHeight() / 2, pointPaint); + canvas.drawBitmap(bigBitmap, x - bigBitmap.getWidth() / 2f, y - bigBitmap.getHeight() / 2f, pointPaint); } } } @@ -211,24 +231,94 @@ private long getSelectedObjectId() { return selectedObject == null ? 0 : selectedObject.getId(); } - private void initProviderWithPoints(List points) { + private void updateTopPlacesTileProvider(boolean show) { MapRendererView mapRenderer = getMapRenderer(); if (mapRenderer == null) { return; } - deleteProvider(topPlacesMapLayerProvider); - if (points == null) { + if (show) { + if (topPlacesMapLayerProvider == null) { + topPlacesMapLayerProvider = new ExploreTopPlacesTileProvider(getApplication(), + getPointsOrder() + 100, DEFAULT_LIMIT_POINTS / 4); + topPlacesMapLayerProvider.initProvider(mapRenderer); + } + } else { + deleteProvider(topPlacesMapLayerProvider); topPlacesMapLayerProvider = null; + } + } + + private void updateTopPlacesCollection(@Nullable List points, @NonNull RotatedTileBox tileBox) { + MapRendererView mapRenderer = getMapRenderer(); + if (mapRenderer == null) { + return; + } + if (points == null) { + clearMapMarkersCollections(); return; } - topPlacesMapLayerProvider = new ExploreTopPlacesTileProvider(getApplication(), - getPointsOrder(), - view.getDensity(), - getSelectedObjectId()); - for (ExploreTopPlacePoint nearbyPlacePoint : points) { - topPlacesMapLayerProvider.addToData(nearbyPlacePoint); + if (mapMarkersCollection == null) { + mapMarkersCollection = new MapMarkersCollection(); + } + QListMapMarker existingMapPoints = mapMarkersCollection.getMarkers(); + int[] existingX = new int[(int)existingMapPoints.size()]; + int[] existingY = new int[(int)existingMapPoints.size()]; + for (int i = 0; i < existingMapPoints.size(); i++) { + MapMarker mapPoint = existingMapPoints.get(i); + PointI pos = mapPoint.getPosition(); + existingX[i] = pos.getX(); + existingY[i] = pos.getY(); } - topPlacesMapLayerProvider.initProvider(mapRenderer); + List newPoints = new ArrayList<>(); + float iconSize = ExploreTopPlacesTileProvider.getBigIconSize(view.getApplication()); + QuadTree boundIntersections = initBoundIntersections(tileBox); + for (int j = 0; j < Math.min(points.size(), TOP_LOAD_PHOTOS); j++) { + ExploreTopPlacePoint point = points.get(j); + double lat = point.getLatitude(); + double lon = point.getLongitude(); + + PointI position = NativeUtilities.getPoint31FromLatLon(lat, lon); + int x = position.getX(); + int y = position.getY(); + PointF pixel = NativeUtilities.getElevatedPixelFromLatLon(mapRenderer, tileBox, lat, lon); + if (intersects(boundIntersections, pixel.x, pixel.y, iconSize, iconSize)) { + continue; + } + boolean alreadyExists = false; + for (int i = 0; i < existingX.length; i++) { + if (x == existingX[i] && y == existingY[i]) { + existingX[i] = 0; + existingY[i] = 0; + alreadyExists = true; + break; + } + } + newPoints.add(new MapPoint(position, point.getImageBitmap(), alreadyExists)); + } + for (MapPoint point : newPoints) { + Bitmap imageBitmap = point.imageBitmap; + if (point.alreadyExists || imageBitmap == null) { + continue; + } + + Bitmap imageMapBitmap = ExploreTopPlacesTileProvider + .createBigBitmap(getApplication(), imageBitmap, false); + + MapMarkerBuilder mapMarkerBuilder = new MapMarkerBuilder(); + mapMarkerBuilder.setIsAccuracyCircleSupported(false) + .setBaseOrder(getPointsOrder()) + .setPinIcon(NativeUtilities.createSkImageFromBitmap(imageMapBitmap)) + .setPosition(point.position) + .setPinIconVerticalAlignment(MapMarker.PinIconVerticalAlignment.CenterVertical) + .setPinIconHorisontalAlignment(MapMarker.PinIconHorisontalAlignment.CenterHorizontal) + .buildAndAddToCollection(mapMarkersCollection); + } + for (int i = 0; i < existingX.length; i++) { + if (existingX[i] != 0 && existingY[i] != 0) { + mapMarkersCollection.removeMarker(existingMapPoints.get(i)); + } + } + mapRenderer.addSymbolsProvider(mapMarkersCollection); } public synchronized void showSelectedNearbyPoint() { @@ -236,18 +326,39 @@ public synchronized void showSelectedNearbyPoint() { if (mapRenderer == null) { return; } - deleteProvider(selectedTopPlacesMapLayerProvider); - if (selectedObject != null) { - selectedTopPlacesMapLayerProvider = new ExploreTopPlacesTileProvider(getApplication(), - getPointsOrder() - 1, - view.getDensity(), - getSelectedObjectId()); - - selectedTopPlacesMapLayerProvider.addToData(selectedObject); - selectedTopPlacesMapLayerProvider.initProvider(mapRenderer); + if (mapMarkersCollection == null) { + mapMarkersCollection = new MapMarkersCollection(); } - } + MapMarker previousSelectedMarker = null; + QListMapMarker existingMapPoints = mapMarkersCollection.getMarkers(); + for (int i = 0; i < existingMapPoints.size(); i++) { + MapMarker mapPoint = existingMapPoints.get(i); + if (mapPoint.getMarkerId() == SELECTED_MARKER_ID) { + previousSelectedMarker = mapPoint; + break; + } + } + Bitmap imageBitmap = selectedObject != null ? selectedObject.getImageBitmap() : null; + if (imageBitmap != null) { + Bitmap imageMapBitmap = ExploreTopPlacesTileProvider + .createBigBitmap(getApplication(), imageBitmap, true); + + MapMarkerBuilder mapMarkerBuilder = new MapMarkerBuilder(); + mapMarkerBuilder.setIsAccuracyCircleSupported(false) + .setMarkerId(SELECTED_MARKER_ID) + .setBaseOrder(getPointsOrder() - 1) + .setPinIcon(NativeUtilities.createSkImageFromBitmap(imageMapBitmap)) + .setPosition(NativeUtilities.getPoint31FromLatLon(selectedObject.getLatitude(), selectedObject.getLongitude())) + .setPinIconVerticalAlignment(MapMarker.PinIconVerticalAlignment.CenterVertical) + .setPinIconHorisontalAlignment(MapMarker.PinIconHorisontalAlignment.CenterHorizontal) + .buildAndAddToCollection(mapMarkersCollection); + mapRenderer.addSymbolsProvider(mapMarkersCollection); + } + if (previousSelectedMarker != null) { + mapMarkersCollection.removeMarker(previousSelectedMarker); + } + } public void deleteProvider(@Nullable ExploreTopPlacesTileProvider provider) { MapRendererView mapRenderer = getMapRenderer(); @@ -257,12 +368,7 @@ public void deleteProvider(@Nullable ExploreTopPlacesTileProvider provider) { provider.deleteProvider(mapRenderer); } - @Override - public boolean onLongPressEvent(@NonNull PointF point, @NonNull RotatedTileBox tileBox) { - return false; - } - - @Override + @Override public PointDescription getObjectName(Object o) { if (o instanceof ExploreTopPlacePoint) { return ((ExploreTopPlacePoint) o).getPointDescription(getContext()); @@ -323,17 +429,16 @@ public LatLon getObjectLocation(Object o) { return null; } - private boolean scheduleImageRefreshes(List nearbyPlacePoints, RotatedTileBox tileBox) { + private boolean scheduleImageRefreshes(List nearbyPlacePoints, RotatedTileBox tileBox, boolean forceRefresh) { if (places == null) { if (imagesDisplayedBox != null) { - LOG.info(String.format("Picasso cancel loading")); - Picasso.get().cancelTag(LOAD_NEARBY_IMAGES_TAG); + cancelLoadingImages(); imagesDisplayedBox = null; } return false; } - if (imagesDisplayedBox == null || imagesDisplayedBox.getZoom() != tileBox.getZoom() || - !imagesDisplayedBox.containsTileBox(tileBox)) { + if (forceRefresh || imagesDisplayedBox == null || imagesDisplayedBox.getZoom() != tileBox.getZoom() + || !imagesDisplayedBox.containsTileBox(tileBox)) { imagesDisplayedBox = tileBox.copy(); imagesDisplayedBox.increasePixelDimensions(tileBox.getPixWidth() / 2, tileBox.getPixHeight() / 2); imagesUpdatedVersion++; @@ -347,41 +452,35 @@ private boolean scheduleImageRefreshes(List nearbyPlacePoi if (point.getImageBitmap() == null) { missingPhoto = true; } - if (placesToDisplayWithPhotos.size() > TOP_LOAD_PHOTOS) { - break; - } } } if (missingPhoto) { - Picasso.get().cancelTag(LOAD_NEARBY_IMAGES_TAG); -// LOG.info(String.format("Picasso cancel loading")); + Set imagesToLoad = placesToDisplayWithPhotos.stream() + .map(ExploreTopPlacePoint::getIconUrl).collect(Collectors.toSet()); + loadingImages.removeIf(image -> !imagesToLoad.contains(image.getUrl()) && image.cancel()); for (ExploreTopPlacePoint point : placesToDisplayWithPhotos) { if (point.getImageBitmap() != null) { continue; } - Target imgLoadTarget = new Target() { + + String url = point.getIconUrl(); + loadingImages.add(imageLoader.loadImage(url, new ImageLoaderCallback() { @Override - public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) { - point.setImageBitmap(bitmap); -// LOG.info(String.format("Picasso loaded %s", point.getIconUrl())); - imagesUpdatedVersion++; + public void onStart(@Nullable Bitmap bitmap) { } @Override - public void onBitmapFailed(Exception e, Drawable errorDrawable) { - LOG.error(String.format("Picasso failed to load %s", point.getIconUrl()), e); + public void onSuccess(@NonNull Bitmap bitmap) { + point.setImageBitmap(bitmap); + imagesUpdatedVersion++; } @Override - public void onPrepareLoad(Drawable placeHolderDrawable) { + public void onError() { + LOG.error(String.format("Coil failed to load %s", url)); } - }; -// LOG.info(String.format("Picasso schedule %s", point.getIconUrl())); - Picasso.get() - .load(point.getIconUrl()) - .tag(LOAD_NEARBY_IMAGES_TAG) - .into(imgLoadTarget); + }, false)); } } } @@ -392,13 +491,15 @@ public void onPrepareLoad(Drawable placeHolderDrawable) { return true; } return false; + } - + private void cancelLoadingImages() { + loadingImages.forEach(LoadingImage::cancel); + loadingImages.clear(); } public void enableLayer(boolean enable) { - requestQuadRect = enable ? - new QuadRect() : null; + requestQuadRect = enable ? new QuadRect() : null; } @Override diff --git a/OsmAnd/src/net/osmand/plus/views/layers/base/OsmandMapLayer.java b/OsmAnd/src/net/osmand/plus/views/layers/base/OsmandMapLayer.java index 12a8a0f4d8f..8029b7d2b99 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/base/OsmandMapLayer.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/base/OsmandMapLayer.java @@ -527,6 +527,11 @@ protected void cancelMovableObject() { } } + /** OpenGL */ + public static boolean isMapRendererLost(@NonNull Context ctx) { + return !((OsmandApplication) ctx.getApplicationContext()).getOsmandMap().getMapView().hasMapRenderer(); + } + public static class TileBoxRequest { private final int left; private final int top; diff --git a/OsmAnd/src/net/osmand/plus/views/layers/core/ExploreTopPlacesTileProvider.java b/OsmAnd/src/net/osmand/plus/views/layers/core/ExploreTopPlacesTileProvider.java index 4b12fb9aba4..6ad24056d63 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/core/ExploreTopPlacesTileProvider.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/core/ExploreTopPlacesTileProvider.java @@ -13,6 +13,7 @@ import androidx.annotation.NonNull; import net.osmand.core.android.MapRendererView; +import net.osmand.core.jni.AreaI; import net.osmand.core.jni.MapMarker; import net.osmand.core.jni.MapTiledCollectionProvider; import net.osmand.core.jni.PointI; @@ -22,36 +23,32 @@ import net.osmand.core.jni.SwigUtilities; import net.osmand.core.jni.TextRasterizer; import net.osmand.core.jni.TileId; +import net.osmand.core.jni.Utilities; import net.osmand.core.jni.ZoomLevel; +import net.osmand.core.jni.interface_MapTiledCollectionPoint; import net.osmand.core.jni.interface_MapTiledCollectionProvider; import net.osmand.data.ExploreTopPlacePoint; -import net.osmand.data.PointDescription; +import net.osmand.data.QuadRect; import net.osmand.plus.OsmandApplication; import net.osmand.plus.R; +import net.osmand.plus.exploreplaces.ExplorePlacesProvider; import net.osmand.plus.render.RenderingIcons; import net.osmand.plus.utils.AndroidUtils; import net.osmand.plus.utils.ColorUtilities; import net.osmand.plus.utils.NativeUtilities; +import net.osmand.plus.views.layers.base.OsmandMapLayer; import net.osmand.util.MapUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import java.util.ArrayList; import java.util.List; public class ExploreTopPlacesTileProvider extends interface_MapTiledCollectionProvider { - private static final Log log = LogFactory.getLog(ExploreTopPlacesTileProvider.class); - private final QListPointI points31 = new QListPointI(); - private final List mapLayerDataList = new ArrayList<>(); - private Bitmap cachedSmallBitmap; - private final OsmandApplication app; - private final float density; - private final PointI offset; + private final Context ctx; + private final ExplorePlacesProvider explorePlacesProvider; private MapTiledCollectionProvider providerInstance; - private int baseOrder; - private long selectedObjectId; + + private static final int TILE_LOADING_TIMEOUT = 2000; + private static final int SLEEP_INTERVAL = 100; private static final int SMALL_ICON_BORDER_DP = 1; private static final int BIG_ICON_BORDER_DP = 2; @@ -59,30 +56,49 @@ public class ExploreTopPlacesTileProvider extends interface_MapTiledCollectionPr private static final int BIG_ICON_SIZE_DP = 40; private static final int POINT_OUTER_COLOR = 0xffffffff; + private final int baseOrder; + private final int tilePointsLimit; + private Bitmap cachedSmallBitmap; + private final PointI pinIconOffset; private static Bitmap circleBitmap; - private static Bitmap getCircle(@NonNull Context ctx) { - if (circleBitmap == null) { - circleBitmap = RenderingIcons.getBitmapFromVectorDrawable(ctx, R.drawable.bg_point_circle); + private class TopPlaceCollectionPoint extends interface_MapTiledCollectionPoint { + + private final PointI point31; + + public TopPlaceCollectionPoint(@NonNull ExploreTopPlacePoint point) { + int x = MapUtils.get31TileNumberX(point.getLongitude()); + int y = MapUtils.get31TileNumberY(point.getLatitude()); + this.point31 = new PointI(x, y); } - return circleBitmap; - } - private static Paint createBitmapPaint() { - Paint bitmapPaint = new Paint(); - bitmapPaint.setAntiAlias(true); - bitmapPaint.setDither(true); - bitmapPaint.setFilterBitmap(true); - return bitmapPaint; - } + @Override + public PointI getPoint31() { + return point31; + } + + @Override + public SingleSkImage getImageBitmap(boolean isFullSize) { + Bitmap bitmap; + if (cachedSmallBitmap == null) { + cachedSmallBitmap = createSmallPointBitmap(ctx); + } + bitmap = cachedSmallBitmap; + return NativeUtilities.createSkImageFromBitmap(bitmap); + } + @Override + public String getCaption() { + return ""; + } + } - public ExploreTopPlacesTileProvider(@NonNull OsmandApplication context, int baseOrder, float density, long selectedObjectId) { - this.app = context; + public ExploreTopPlacesTileProvider(@NonNull Context ctx, int baseOrder, int tilePointsLimit) { + this.ctx = ctx; this.baseOrder = baseOrder; - this.density = density; - this.offset = new PointI(0, 0); - this.selectedObjectId = selectedObjectId; + this.tilePointsLimit = tilePointsLimit; + this.pinIconOffset = new PointI(0, 0); + this.explorePlacesProvider = ((OsmandApplication) ctx.getApplicationContext()).getExplorePlacesProvider(); } public void initProvider(@NonNull MapRendererView mapRenderer) { @@ -106,7 +122,7 @@ public int getBaseOrder() { @Override public QListPointI getPoints31() { - return points31; + return new QListPointI(); } @Override @@ -126,7 +142,7 @@ public TextRasterizer.Style getCaptionStyle() { @Override public double getCaptionTopSpace() { - return -4.0 * density; + return 0.0; } @Override @@ -139,33 +155,64 @@ public double getScale() { return 1.0d; } + private static Bitmap getCircle(@NonNull Context ctx) { + if (circleBitmap == null) { + circleBitmap = RenderingIcons.getBitmapFromVectorDrawable(ctx, R.drawable.bg_point_circle); + } + return circleBitmap; + } + + private static Paint createBitmapPaint() { + Paint bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setDither(true); + bitmapPaint.setFilterBitmap(true); + return bitmapPaint; + } + @Override public SingleSkImage getImageBitmap(int index, boolean isFullSize) { - ExploreTopPlacesTileProvider.MapLayerData data = index < mapLayerDataList.size() ? mapLayerDataList.get(index) : null; - if (data == null) { - return SwigUtilities.nullSkImage(); - } - Bitmap bitmap; - if ((isFullSize || data.nearbyPlace.getId() == selectedObjectId) && data.nearbyPlace.getImageBitmap() != null) { - bitmap = createBigBitmap(app, data.nearbyPlace.getImageBitmap(), data.nearbyPlace.getId() == selectedObjectId); - } else { - if (cachedSmallBitmap == null) { - cachedSmallBitmap = createSmallPointBitmap(app); - } - bitmap = cachedSmallBitmap; - } - return NativeUtilities.createSkImageFromBitmap(bitmap); + return SwigUtilities.nullSkImage(); } @Override public String getCaption(int index) { - MapLayerData data = index < mapLayerDataList.size() ? mapLayerDataList.get(index) : null; - return data != null ? PointDescription.getSimpleName(data.nearbyPlace, app) : ""; + return ""; } @Override public QListMapTiledCollectionPoint getTilePoints(TileId tileId, ZoomLevel zoom) { - return new QListMapTiledCollectionPoint(); + if (OsmandMapLayer.isMapRendererLost(ctx)) { + return new QListMapTiledCollectionPoint(); + } + + AreaI tileBBox31 = Utilities.tileBoundingBox31(tileId, zoom); + double l = MapUtils.get31LongitudeX(tileBBox31.getTopLeft().getX()); + double t = MapUtils.get31LatitudeY(tileBBox31.getTopLeft().getY()); + double r = MapUtils.get31LongitudeX(tileBBox31.getBottomRight().getX()); + double b = MapUtils.get31LatitudeY(tileBBox31.getBottomRight().getY()); + + QuadRect tileRect = new QuadRect(l, t, r, b); + List places = explorePlacesProvider.getDataCollection(tileRect, tilePointsLimit); + int i = 0; + while (explorePlacesProvider.isLoadingRect(tileRect) && i++ * SLEEP_INTERVAL < TILE_LOADING_TIMEOUT) { + try { + Thread.sleep(SLEEP_INTERVAL); + } catch (InterruptedException ignore) { + } + } + places = explorePlacesProvider.getDataCollection(tileRect, tilePointsLimit); + if (places.isEmpty() || OsmandMapLayer.isMapRendererLost(ctx)) { + return new QListMapTiledCollectionPoint(); + } + + QListMapTiledCollectionPoint res = new QListMapTiledCollectionPoint(); + for (ExploreTopPlacePoint place : places) { + TopPlaceCollectionPoint point = new TopPlaceCollectionPoint(place); + res.add(point.instantiateProxy(true)); + point.swigReleaseOwnership(); + } + return res; } @Override @@ -180,7 +227,7 @@ public ZoomLevel getMaxZoom() { @Override public boolean supportsNaturalObtainDataAsync() { - return false; + return true; } @Override @@ -195,25 +242,7 @@ public MapMarker.PinIconHorisontalAlignment getPinIconHorisontalAlignment() { @Override public PointI getPinIconOffset() { - return offset; - } - - public void addToData(@NonNull ExploreTopPlacePoint nearbyPlacePoint) throws IllegalStateException { - if (providerInstance != null) { - throw new IllegalStateException("Provider already instantiated. Data cannot be modified at this stage."); - } - int x31 = MapUtils.get31TileNumberX(nearbyPlacePoint.getLongitude()); - int y31 = MapUtils.get31TileNumberY(nearbyPlacePoint.getLatitude()); - points31.add(new PointI(x31, y31)); - mapLayerDataList.add(new MapLayerData(nearbyPlacePoint)); - } - - private static class MapLayerData { - ExploreTopPlacePoint nearbyPlace; - - MapLayerData(@NonNull ExploreTopPlacePoint nearbyPlace) { - this.nearbyPlace = nearbyPlace; - } + return pinIconOffset; } public static Bitmap createSmallPointBitmap(@NonNull Context ctx) { @@ -243,7 +272,7 @@ public static Bitmap createBigBitmap(@NonNull OsmandApplication app, Bitmap load boolean nightMode = app.getDaynightHelper().isNightModeForMapControls(); int borderWidth = AndroidUtils.dpToPxAuto(app, BIG_ICON_BORDER_DP); Bitmap circle = getCircle(app); - int bigIconSize = AndroidUtils.dpToPxAuto(app, BIG_ICON_SIZE_DP); + int bigIconSize = getBigIconSize(app); Bitmap bitmapResult = Bitmap.createBitmap(bigIconSize, bigIconSize, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmapResult); Paint bitmapPaint = createBitmapPaint(); @@ -266,5 +295,7 @@ public static Bitmap createBigBitmap(@NonNull OsmandApplication app, Bitmap load return bitmapResult; } - + public static int getBigIconSize(@NonNull OsmandApplication app) { + return AndroidUtils.dpToPxAuto(app, BIG_ICON_SIZE_DP); + } } \ No newline at end of file diff --git a/OsmAnd/src/net/osmand/plus/views/layers/core/OsmBugsTileProvider.java b/OsmAnd/src/net/osmand/plus/views/layers/core/OsmBugsTileProvider.java index 1a95002b034..46fa49c2e05 100644 --- a/OsmAnd/src/net/osmand/plus/views/layers/core/OsmBugsTileProvider.java +++ b/OsmAnd/src/net/osmand/plus/views/layers/core/OsmBugsTileProvider.java @@ -80,7 +80,7 @@ public PointI getPoint31() { @Override public SingleSkImage getImageBitmap(boolean isFullSize) { - Bitmap bitmap = null; + Bitmap bitmap; if (!osmNote.isOpened() && !showClosed) { return SwigUtilities.nullSkImage(); } @@ -189,7 +189,7 @@ public double getScale() { @Override public QListMapTiledCollectionPoint getTilePoints(TileId tileId, ZoomLevel zoom) { - if (isMapRendererLost()) { + if (OsmandMapLayer.isMapRendererLost(ctx)) { return new QListMapTiledCollectionPoint(); } @@ -204,7 +204,7 @@ public QListMapTiledCollectionPoint getTilePoints(TileId tileId, ZoomLevel zoom) start[0] = System.currentTimeMillis(); }); while (System.currentTimeMillis() - start[0] < layerData.DATA_REQUEST_TIMEOUT) { - if (isMapRendererLost()) { + if (OsmandMapLayer.isMapRendererLost(ctx)) { return new QListMapTiledCollectionPoint(); } synchronized (dataReadyCallback.getSync()) { @@ -219,7 +219,7 @@ public QListMapTiledCollectionPoint getTilePoints(TileId tileId, ZoomLevel zoom) } layerData.removeDataReadyCallback(dataReadyCallback); - if (isMapRendererLost()) { + if (OsmandMapLayer.isMapRendererLost(ctx)) { return new QListMapTiledCollectionPoint(); } @@ -284,8 +284,4 @@ public MapMarker.PinIconHorisontalAlignment getPinIconHorisontalAlignment() { public PointI getPinIconOffset() { return offset; } - - private boolean isMapRendererLost() { - return !((OsmandApplication) ctx.getApplicationContext()).getOsmandMap().getMapView().hasMapRenderer(); - } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index ff286975d8f..ecd6dead5ab 100644 --- a/build.gradle +++ b/build.gradle @@ -8,7 +8,7 @@ buildscript { } } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.7.3' classpath 'com.google.gms:google-services:4.4.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index a10a1b58323..baf031e6e99 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Jul 15 15:30:46 EEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists