diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java deleted file mode 100644 index 38e51da52..000000000 --- a/app/src/fdroid/java/org/meshtastic/app/map/cluster/MarkerClusterer.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.app.map.cluster; - -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Point; -import android.view.MotionEvent; - -import org.meshtastic.app.map.model.MarkerWithLabel; - -import org.osmdroid.util.BoundingBox; -import org.osmdroid.views.MapView; -import org.osmdroid.views.overlay.Overlay; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.ListIterator; - -/** - * An overlay allowing to perform markers clustering. - * Usage: put your markers inside with add(Marker), and add the MarkerClusterer to the map overlays. - * Depending on the zoom level, markers will be displayed separately, or grouped as a single Marker.
- * - * This abstract class provides the framework. Sub-classes have to implement the clustering algorithm, - * and the rendering of a cluster. - * - * @author M.Kergall - * - */ -public abstract class MarkerClusterer extends Overlay { - - /** impossible value for zoom level, to force clustering */ - protected static final int FORCE_CLUSTERING = -1; - - protected ArrayList mItems = new ArrayList(); - protected Point mPoint = new Point(); - protected ArrayList mClusters = new ArrayList(); - protected int mLastZoomLevel; - protected Bitmap mClusterIcon; - protected String mName, mDescription; - - // abstract methods: - - /** clustering algorithm */ - public abstract ArrayList clusterer(MapView mapView); - /** Build the marker for a cluster. */ - public abstract MarkerWithLabel buildClusterMarker(StaticCluster cluster, MapView mapView); - /** build clusters markers to be used at next draw */ - public abstract void renderer(ArrayList clusters, Canvas canvas, MapView mapView); - - public MarkerClusterer() { - super(); - mLastZoomLevel = FORCE_CLUSTERING; - } - - public void setName(String name){ - mName = name; - } - - public String getName(){ - return mName; - } - - public void setDescription(String description){ - mDescription = description; - } - - public String getDescription(){ - return mDescription; - } - - /** Set the cluster icon to be drawn when a cluster contains more than 1 marker. - * If not set, default will be the default osmdroid marker icon (which is really inappropriate as a cluster icon). */ - public void setIcon(Bitmap icon){ - mClusterIcon = icon; - } - - /** Add the Marker. - * Important: Markers added in a MarkerClusterer should not be added in the map overlays. */ - public void add(MarkerWithLabel marker){ - mItems.add(marker); - } - - /** Force a rebuild of clusters at next draw, even without a zooming action. - * Should be done when you changed the content of a MarkerClusterer. */ - public void invalidate(){ - mLastZoomLevel = FORCE_CLUSTERING; - } - - /** @return the Marker at id (starting at 0) */ - public MarkerWithLabel getItem(int id){ - return mItems.get(id); - } - - /** @return the list of Markers. */ - public ArrayList getItems(){ - return mItems; - } - - protected void hideInfoWindows(){ - for (MarkerWithLabel m : mItems){ - if (m.isInfoWindowShown()) - m.closeInfoWindow(); - } - } - - @Override public void draw(Canvas canvas, MapView mapView, boolean shadow) { - if (shadow) - return; - //if zoom has changed and mapView is now stable, rebuild clusters: - int zoomLevel = mapView.getZoomLevel(); - if (zoomLevel != mLastZoomLevel && !mapView.isAnimating()){ - hideInfoWindows(); - mClusters = clusterer(mapView); - renderer(mClusters, canvas, mapView); - mLastZoomLevel = zoomLevel; - } - - for (StaticCluster cluster:mClusters){ - MarkerWithLabel marker = cluster.getMarker(); - marker.draw(canvas, mapView, false); - } - } - - public Iterable reversedClusters() { - return new Iterable() { - @Override - public Iterator iterator() { - final ListIterator i = mClusters.listIterator(mClusters.size()); - return new Iterator() { - @Override - public boolean hasNext() { - return i.hasPrevious(); - } - - @Override - public StaticCluster next() { - return i.previous(); - } - - @Override - public void remove() { - i.remove(); - } - }; - } - }; - } - - @Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){ - for (final StaticCluster cluster : reversedClusters()) { - if (cluster.getMarker().onSingleTapConfirmed(event, mapView)) - return true; - } - return false; - } - - @Override public boolean onLongPress(final MotionEvent event, final MapView mapView) { - for (final StaticCluster cluster : reversedClusters()) { - if (cluster.getMarker().onLongPress(event, mapView)) - return true; - } - return false; - } - - @Override public boolean onTouchEvent(final MotionEvent event, final MapView mapView) { - for (StaticCluster cluster : reversedClusters()) { - if (cluster.getMarker().onTouchEvent(event, mapView)) - return true; - } - return false; - } - - @Override public boolean onDoubleTap(final MotionEvent event, final MapView mapView) { - for (final StaticCluster cluster : reversedClusters()) { - if (cluster.getMarker().onDoubleTap(event, mapView)) - return true; - } - return false; - } - - @Override public BoundingBox getBounds(){ - if (mItems.size() == 0) - return null; - double minLat = Double.MAX_VALUE; - double minLon = Double.MAX_VALUE; - double maxLat = -Double.MAX_VALUE; - double maxLon = -Double.MAX_VALUE; - for (final MarkerWithLabel item : mItems) { - final double latitude = item.getPosition().getLatitude(); - final double longitude = item.getPosition().getLongitude(); - minLat = Math.min(minLat, latitude); - minLon = Math.min(minLon, longitude); - maxLat = Math.max(maxLat, latitude); - maxLon = Math.max(maxLon, longitude); - } - return new BoundingBox(maxLat, maxLon, minLat, minLon); - } - -} diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java deleted file mode 100644 index e2710352a..000000000 --- a/app/src/fdroid/java/org/meshtastic/app/map/cluster/RadiusMarkerClusterer.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.app.map.cluster; - -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Paint; -import android.graphics.Rect; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.view.MotionEvent; - -import org.meshtastic.app.map.model.MarkerWithLabel; - -import org.osmdroid.bonuspack.R; -import org.osmdroid.util.BoundingBox; -import org.osmdroid.util.GeoPoint; -import org.osmdroid.views.MapView; - -import java.util.ArrayList; -import java.util.Iterator; - -/** - * Radius-based Clustering algorithm: - * create a cluster using the first point from the cloned list. - * All points that are found within the neighborhood are added to this cluster. - * Then all the neighbors and the main point are removed from the list of points. - * It continues until the list is empty. - * - * Largely inspired from GridMarkerClusterer by M.Kergall - * - * @author sidorovroman92@gmail.com - */ - -public class RadiusMarkerClusterer extends MarkerClusterer { - - protected int mMaxClusteringZoomLevel = 7; - protected int mRadiusInPixels = 100; - protected double mRadiusInMeters; - protected Paint mTextPaint; - private ArrayList mClonedMarkers; - protected boolean mAnimated; - int mDensityDpi; - - /** cluster icon anchor */ - public float mAnchorU = MarkerWithLabel.ANCHOR_CENTER, mAnchorV = MarkerWithLabel.ANCHOR_CENTER; - /** anchor point to draw the number of markers inside the cluster icon */ - public float mTextAnchorU = MarkerWithLabel.ANCHOR_CENTER, mTextAnchorV = MarkerWithLabel.ANCHOR_CENTER; - - public RadiusMarkerClusterer(Context ctx) { - super(); - mTextPaint = new Paint(); - mTextPaint.setColor(Color.WHITE); - mTextPaint.setTextSize(15 * ctx.getResources().getDisplayMetrics().density); - mTextPaint.setFakeBoldText(true); - mTextPaint.setTextAlign(Paint.Align.CENTER); - mTextPaint.setAntiAlias(true); - Drawable clusterIconD = ctx.getResources().getDrawable(R.drawable.marker_cluster); - Bitmap clusterIcon = ((BitmapDrawable) clusterIconD).getBitmap(); - setIcon(clusterIcon); - mAnimated = true; - mDensityDpi = ctx.getResources().getDisplayMetrics().densityDpi; - } - - /** If you want to change the default text paint (color, size, font) */ - public Paint getTextPaint(){ - return mTextPaint; - } - - /** Set the radius of clustering in pixels. Default is 100px. */ - public void setRadius(int radius){ - mRadiusInPixels = radius; - } - - /** Set max zoom level with clustering. When zoom is higher or equal to this level, clustering is disabled. - * You can put a high value to disable this feature. */ - public void setMaxClusteringZoomLevel(int zoom){ - mMaxClusteringZoomLevel = zoom; - } - - /** Radius-Based clustering algorithm */ - @Override public ArrayList clusterer(MapView mapView) { - - ArrayList clusters = new ArrayList(); - convertRadiusToMeters(mapView); - - mClonedMarkers = new ArrayList(mItems); //shallow copy - while (!mClonedMarkers.isEmpty()) { - MarkerWithLabel m = mClonedMarkers.get(0); - StaticCluster cluster = createCluster(m, mapView); - clusters.add(cluster); - } - return clusters; - } - - private StaticCluster createCluster(MarkerWithLabel m, MapView mapView) { - GeoPoint clusterPosition = m.getPosition(); - - StaticCluster cluster = new StaticCluster(clusterPosition); - cluster.add(m); - - mClonedMarkers.remove(m); - - if (mapView.getZoomLevel() > mMaxClusteringZoomLevel) { - //above max level => block clustering: - return cluster; - } - - Iterator it = mClonedMarkers.iterator(); - while (it.hasNext()) { - MarkerWithLabel neighbor = it.next(); - double distance = clusterPosition.distanceToAsDouble(neighbor.getPosition()); - if (distance <= mRadiusInMeters) { - cluster.add(neighbor); - it.remove(); - } - } - - return cluster; - } - - @Override public MarkerWithLabel buildClusterMarker(StaticCluster cluster, MapView mapView) { - MarkerWithLabel m = new MarkerWithLabel(mapView, "", null); - m.setPosition(cluster.getPosition()); - m.setInfoWindow(null); - m.setAnchor(mAnchorU, mAnchorV); - - Bitmap finalIcon = Bitmap.createBitmap(mClusterIcon.getScaledWidth(mDensityDpi), - mClusterIcon.getScaledHeight(mDensityDpi), mClusterIcon.getConfig()); - Canvas iconCanvas = new Canvas(finalIcon); - iconCanvas.drawBitmap(mClusterIcon, 0, 0, null); - String text = "" + cluster.getSize(); - int textHeight = (int) (mTextPaint.descent() + mTextPaint.ascent()); - iconCanvas.drawText(text, - mTextAnchorU * finalIcon.getWidth(), - mTextAnchorV * finalIcon.getHeight() - textHeight / 2, - mTextPaint); - m.setIcon(new BitmapDrawable(mapView.getContext().getResources(), finalIcon)); - - return m; - } - - @Override public void renderer(ArrayList clusters, Canvas canvas, MapView mapView) { - for (StaticCluster cluster : clusters) { - if (cluster.getSize() == 1) { - //cluster has only 1 marker => use it as it is: - cluster.setMarker(cluster.getItem(0)); - } else { - //only draw 1 Marker at Cluster center, displaying number of Markers contained - MarkerWithLabel m = buildClusterMarker(cluster, mapView); - cluster.setMarker(m); - } - } - } - - private void convertRadiusToMeters(MapView mapView) { - - Rect mScreenRect = mapView.getIntrinsicScreenRect(null); - - int screenWidth = mScreenRect.right - mScreenRect.left; - int screenHeight = mScreenRect.bottom - mScreenRect.top; - - BoundingBox bb = mapView.getBoundingBox(); - - double diagonalInMeters = bb.getDiagonalLengthInMeters(); - double diagonalInPixels = Math.sqrt(screenWidth * screenWidth + screenHeight * screenHeight); - double metersInPixel = diagonalInMeters / diagonalInPixels; - - mRadiusInMeters = mRadiusInPixels * metersInPixel; - } - - public void setAnimation(boolean animate){ - mAnimated = animate; - } - - public void zoomOnCluster(MapView mapView, StaticCluster cluster){ - BoundingBox bb = cluster.getBoundingBox(); - if (bb.getLatNorth()!=bb.getLatSouth() || bb.getLonEast()!=bb.getLonWest()) { - bb = bb.increaseByScale(2.3f); - mapView.zoomToBoundingBox(bb, true); - } else //all points exactly at the same place: - mapView.setExpectedCenter(bb.getCenterWithDateLine()); - } - - @Override public boolean onSingleTapConfirmed(final MotionEvent event, final MapView mapView){ - for (final StaticCluster cluster : reversedClusters()) { - if (cluster.getMarker().onSingleTapConfirmed(event, mapView)) { - if (mAnimated && cluster.getSize() > 1) - zoomOnCluster(mapView, cluster); - return true; - } - } - return false; - } - -} diff --git a/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java b/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java deleted file mode 100644 index 324a34b52..000000000 --- a/app/src/fdroid/java/org/meshtastic/app/map/cluster/StaticCluster.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2025 Meshtastic LLC - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.meshtastic.app.map.cluster; - -import org.meshtastic.app.map.model.MarkerWithLabel; - -import org.osmdroid.util.BoundingBox; -import org.osmdroid.util.GeoPoint; - -import java.util.ArrayList; - -/** - * Cluster of Markers. - * @author M.Kergall - */ -public class StaticCluster { - protected final ArrayList mItems = new ArrayList(); - protected GeoPoint mCenter; - protected MarkerWithLabel mMarker; - - public StaticCluster(GeoPoint center) { - mCenter = center; - } - - public void setPosition(GeoPoint center){ - mCenter = center; - } - - public GeoPoint getPosition() { - return mCenter; - } - - public int getSize() { - return mItems.size(); - } - - public MarkerWithLabel getItem(int index) { - return mItems.get(index); - } - - public boolean add(MarkerWithLabel t) { - return mItems.add(t); - } - - /** set the Marker to be displayed for this cluster */ - public void setMarker(MarkerWithLabel marker){ - mMarker = marker; - } - - /** @return the Marker to be displayed for this cluster */ - public MarkerWithLabel getMarker(){ - return mMarker; - } - - public BoundingBox getBoundingBox(){ - if (getSize()==0) - return null; - GeoPoint p = getItem(0).getPosition(); - BoundingBox bb = new BoundingBox(p.getLatitude(), p.getLongitude(), p.getLatitude(), p.getLongitude()); - for (int i=1; i