Move cluster experiment in samples (#858)

This commit is contained in:
Emux
2021-06-24 15:07:36 +03:00
committed by GitHub
parent e2c901d1e5
commit 5de7070f15
11 changed files with 92 additions and 158 deletions

View File

@@ -0,0 +1,436 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 Izumi Kawashima
* Copyright 2017 Longri
* Copyright 2017-2018 devemux86
* Copyright 2017 nebular
* Copyright 2017 Wolfgang Schramm
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.android.cluster;
import org.oscim.backend.canvas.Bitmap;
import org.oscim.core.MercatorProjection;
import org.oscim.core.PointF;
import org.oscim.core.Tile;
import org.oscim.layers.marker.*;
import org.oscim.layers.marker.utils.SparseIntArray;
import org.oscim.renderer.GLViewport;
import org.oscim.renderer.bucket.SymbolItem;
import org.oscim.utils.FastMath;
import org.oscim.utils.geom.GeometryUtils;
/**
* An extension to the MarkerRenderer with item clustering support.
*/
public class ClusterMarkerRenderer extends MarkerRenderer {
/**
* Max number to display inside a cluster icon
*/
protected static final int CLUSTER_MAXSIZE = 10;
/**
* default color of number inside the icon. Would be super-cool to cook this into the map theme
*/
private static final int CLUSTER_COLORTEXT = 0xff8000c0;
/**
* default color of circle background
*/
private static final int CLUSTER_COLORBACK = 0xffffffff;
/**
* Map Cluster Icon Size. This is the biggest size for clusters of CLUSTER_MAXSIZE elements. Smaller clusters will be slightly smaller
*/
protected static final int MAP_MARKER_CLUSTER_SIZE_DP = 64;
/**
* Clustering grid square size, decrease to cluster more aggresively. Ideally this value is the typical marker size
*/
private static final int MAP_GRID_SIZE_DP = 64;
/**
* cached bitmaps database, we will cache cluster bitmaps from 1 to MAX_SIZE
* and always use same bitmap for efficiency
*/
protected static Bitmap[] mClusterBitmaps = new Bitmap[CLUSTER_MAXSIZE + 1];
protected int mStyleBackground = CLUSTER_COLORBACK, mStyleForeground = CLUSTER_COLORTEXT;
/**
* Discrete scale step, used to trigger reclustering on significant scale change
*/
private int mScalePow = 0;
/**
* Map scale to cluster the marker
*/
private double mClusterScale = 0;
/**
* We use a flat Sparse array to calculate the clusters. The sparse array models a 2D map where every (x,y) denotes
* a grid slot, ie. 64x64dp. For efficiency I use a linear sparsearray with ARRindex = SLOTypos * max_x + SLOTxpos"
*/
private final SparseIntArray mGridMap = new SparseIntArray(200); // initial space for 200 markers, that's not a lot of memory, and in most cases will avoid resizing the array
/**
* Whether to enable clustering or disable the functionality
*/
private final boolean mClusteringEnabled;
/**
* Constructs a clustered marker renderer
*
* @param markerLayer The owner layer
* @param defaultSymbol The default symbol
* @param style The desired style, or NULL to disable clustering
*/
public ClusterMarkerRenderer(MarkerLayer markerLayer, MarkerSymbol defaultSymbol, ClusterMarkerRenderer.ClusterStyle style) {
super(markerLayer, defaultSymbol);
mClusteringEnabled = style != null;
if (mClusteringEnabled) {
setClusterStyle(style.foreground, style.background);
for (int k = 0; k <= CLUSTER_MAXSIZE; k++) {
// cache bitmaps so render thread never creates them
// we create CLUSTER_MAXSIZE bitmaps. Bigger clusters will show like "+"
getClusterBitmap(k);
}
}
}
/**
* Configures the cluster icon style. This is called by the constructor and cannot be made public because
* we pre-cache the icons at construction time so the renderer does not have to create them while rendering
*
* @param backgroundColor Background color
* @param foregroundColor text & border color
*/
private void setClusterStyle(int foregroundColor, int backgroundColor) {
mStyleBackground = backgroundColor;
mStyleForeground = foregroundColor;
}
@Override
protected void populate(int size) {
repopulateCluster(size, mClusterScale);
}
/**
* Repopulates item list clustering close markers. This is triggered from update() when
* a significant change in scale has happened.
*
* @param size Item list size
* @param scale current map scale
*/
private void repopulateCluster(int size, double scale) {
/* the grid slot size in px. increase to group more aggressively. currently set to marker size */
final int GRIDSIZE = ClusterUtils.getPixels(MAP_GRID_SIZE_DP);
/* the factor to map into Grid Coordinates (discrete squares of GRIDSIZE x GRIDSIZE) */
final double factor = (scale / GRIDSIZE);
Clustered[] tmp = new Clustered[size];
// clear grid map to count items that share the same "grid slot"
mGridMap.clear();
for (int i = 0; i < size; i++) {
Clustered it = tmp[i] = new Clustered();
it.item = mMarkerLayer.createItem(i);
/* pre-project points */
MercatorProjection.project(it.item.getPoint(), mMapPoint);
it.px = mMapPoint.x;
it.py = mMapPoint.y;
// items can be declared non-clusterable
if (!(it.item instanceof NonClusterable)) {
final int
absposx = (int) (it.px * factor), // absolute item X position in the grid
absposy = (int) (it.py * factor), // absolute item Y position
maxcols = (int) factor, // Grid number of columns
itemGridIndex = absposx + absposy * maxcols; // Index in the sparsearray map
// we store in the linear sparsearray the index of the marker,
// ie, index = y * maxcols + x; array[index} = markerIndex
// Lets check if there's already an item in the grid slot
final int storedIndexInGridSlot = mGridMap.get(itemGridIndex, -1);
if (storedIndexInGridSlot == -1) {
// no item at that grid position. The grid slot is free so let's
// store this item "i" (we identify every item by its InternalItem index)
mGridMap.put(itemGridIndex, i);
//Log.v(TAG, "UNclustered item at " + itemGridIndex);
} else {
// at that grid position there's already a marker index
// mark this item as clustered out, so it will be skipped in the update() call
it.clusteredOut = true;
// and increment the count on its "parent" that will from now on act as a cluster
tmp[storedIndexInGridSlot].clusterSize++;
//Log.v(TAG, "Clustered item at " + itemGridIndex + ", \'parent\' size " + (tmp[storedIndexInGridSlot].clusterSize));
}
}
}
/* All ready for update. */
synchronized (this) {
mUpdate = true;
mItems = tmp;
}
}
@Override
public synchronized void update(GLViewport v) {
final double scale = Tile.SIZE * v.pos.scale;
if (mClusteringEnabled) {
/*
Clustering check: If clustering is enabled and there's been a significant scale change
trigger repopulation and return. After repopulation, this will be called again
*/
// (int) log of scale gives us adequate steps to trigger clustering
int scalePow = FastMath.log2((int) scale);
if (scalePow != mScalePow) {
mScalePow = scalePow;
mClusterScale = scale;
// post repopulation to the main thread
mMarkerLayer.map().post(new Runnable() {
@Override
public void run() {
repopulateCluster(mItems.length, scale);
}
});
// and get out of here
return;
}
}
if (!v.changed() && !mUpdate)
return;
mUpdate = false;
double mx = v.pos.x;
double my = v.pos.y;
//int changesInvisible = 0;
//int changedVisible = 0;
int numVisible = 0;
// Increase view to show items that are partially visible
mMarkerLayer.map().viewport().getMapExtents(mBox, Tile.SIZE >> 1);
long flip = (long) (Tile.SIZE * v.pos.scale) >> 1;
if (mItems == null) {
if (buckets.get() != null) {
buckets.clear();
compile();
}
return;
}
double angle = Math.toRadians(v.pos.bearing);
float cos = (float) Math.cos(angle);
float sin = (float) Math.sin(angle);
/* check visibility */
for (InternalItem itm : mItems) {
Clustered it = (Clustered) itm;
it.changes = false;
it.x = (float) ((it.px - mx) * scale);
it.y = (float) ((it.py - my) * scale);
if (it.x > flip)
it.x -= (flip << 1);
else if (it.x < -flip)
it.x += (flip << 1);
if ((it.clusteredOut) || (!GeometryUtils.pointInPoly(it.x, it.y, mBox, 8, 0))) {
// either properly invisible, or clustered out. Items marked as clustered out mean there's another item
// on the same-ish position that will be promoted to cluster marker, so this particular item is considered
// invisible
if (it.visible && (!it.clusteredOut)) {
// it was previously visible, but now it won't
it.changes = true;
// changes to invible
//changesInvisible++;
}
continue;
}
// item IS definitely visible
it.dy = sin * it.x + cos * it.y;
if (!it.visible) {
it.visible = true;
//changedVisible++;
}
numVisible++;
}
//log.debug(numVisible + " " + changedVisible + " " + changesInvisible);
/* only update when zoomlevel changed, new items are visible
* or more than 10 of the current items became invisible */
//if ((numVisible == 0) && (changedVisible == 0 && changesInvisible < 10))
// return;
buckets.clear();
if (numVisible == 0) {
compile();
return;
}
/* keep position for current state */
mMapPosition.copy(v.pos);
mMapPosition.bearing = -mMapPosition.bearing;
// why do we sort ? z-index?
sort(mItems, 0, mItems.length);
//log.debug(Arrays.toString(mItems));
for (InternalItem itm : mItems) {
Clustered it = (Clustered) itm;
// skip invisible AND clustered-out
if ((!it.visible) || (it.clusteredOut))
continue;
if (it.changes) {
it.visible = false;
continue;
}
SymbolItem s = SymbolItem.pool.get();
if (it.clusterSize > 0) {
// this item will act as a cluster, just use a proper bitmap
// depending on cluster size, instead of its marker
Bitmap bitmap = getClusterBitmap(it.clusterSize + 1);
s.set(it.x, it.y, bitmap, true);
s.offset = new PointF(0.5f, 0.5f);
s.billboard = true; // could be a parameter
} else {
// normal item, use its marker
MarkerSymbol symbol = it.item.getMarker();
if (symbol == null)
symbol = mDefaultMarker;
s.set(it.x, it.y, symbol.getBitmap(), true);
s.offset = symbol.getHotspot();
s.billboard = symbol.isBillboard();
}
mSymbolLayer.pushSymbol(s);
}
buckets.set(mSymbolLayer);
buckets.prepare();
compile();
}
/**
* Gets a bitmap for a given cluster size
*
* @param size The cluster size. Can be greater than CLUSTER_MAXSIZE.
* @return A somewhat cool bitmap to be used as the cluster marker
*/
protected Bitmap getClusterBitmap(int size) {
final String strValue;
if (size >= CLUSTER_MAXSIZE) {
// restrict cluster indicator size. Bigger clusters will show as "+" instead of ie. "45"
size = CLUSTER_MAXSIZE;
strValue = "+";
} else {
strValue = String.valueOf(size);
}
// return cached bitmap if exists. cache hit !
if (mClusterBitmaps[size] != null)
return mClusterBitmaps[size];
// create and cache bitmap. This is unacceptable inside the GL thread,
// so we'll call this routine at the beginning to pre-cache all bitmaps
ClusterUtils.ClusterDrawable drawable = new ClusterUtils.ClusterDrawable(
MAP_MARKER_CLUSTER_SIZE_DP - CLUSTER_MAXSIZE + size, // make size dependent on cluster size
mStyleForeground,
mStyleBackground,
strValue
);
mClusterBitmaps[size] = drawable.getBitmap();
return mClusterBitmaps[size];
}
/**
* Class to wrap the cluster icon style properties
*/
public static class ClusterStyle {
final int background;
final int foreground;
/**
* Creates the Cluster style
*
* @param fore Foreground (border and text) color
* @param back Background (circle) color
*/
public ClusterStyle(int fore, int back) {
foreground = fore;
background = back;
}
}
/**
* Convenience method for instantiating this renderer via a factory, so the layer construction semantic is more pleasing to the eye
*
* @param defaultSymbol Default symbol to use if the Marker is not assigned a symbol
* @param style Cluster icon style, or NULL to disable clustering functionality
* @return A factory to be passed to the ItemizedLayer constructor in order to enable the cluster functionality
*/
public static MarkerRendererFactory factory(final MarkerSymbol defaultSymbol, final ClusterStyle style) {
return new MarkerRendererFactory() {
@Override
public MarkerRenderer create(MarkerLayer markerLayer) {
return new ClusterMarkerRenderer(markerLayer, defaultSymbol, style);
}
};
}
}

View File

@@ -0,0 +1,105 @@
/*
* Copyright 2017 nebular
* Copyright 2017 devemux86
* Copyright 2017 Wolfgang Schramm
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.android.cluster;
import org.oscim.backend.CanvasAdapter;
import org.oscim.backend.canvas.Bitmap;
import org.oscim.backend.canvas.Canvas;
import org.oscim.backend.canvas.Paint;
/**
* A simple utility class to make clustered markers functionality self-contained.
* Includes a method to translate between DPs and PXs and a circular icon generator.
*/
public class ClusterUtils {
/**
* Get pixels from DPs
*
* @param dp Value in DPs
* @return Value in PX according to screen density
*/
public static int getPixels(float dp) {
return (int) (CanvasAdapter.getScale() * dp);
}
public static class ClusterDrawable {
private final Paint mPaintText = CanvasAdapter.newPaint();
private final Paint mPaintCircle = CanvasAdapter.newPaint();
private final Paint mPaintBorder = CanvasAdapter.newPaint();
private int mSize;
private String mText;
/**
* Generates a circle with a number inside
*
* @param sizedp Size in DPs
* @param foregroundColor Foreground
* @param backgroundColor Background
* @param text Text inside. Will only work for a single character!
*/
public ClusterDrawable(int sizedp, int foregroundColor, int backgroundColor, String text) {
setup(sizedp, foregroundColor, backgroundColor);
setText(text);
}
private void setup(int sizedp, int foregroundColor, int backgroundColor) {
mSize = ClusterUtils.getPixels(sizedp);
mPaintText.setTextSize(ClusterUtils.getPixels((int) (sizedp * 0.6666666)));
mPaintText.setColor(foregroundColor);
mPaintCircle.setColor(backgroundColor);
mPaintCircle.setStyle(Paint.Style.FILL);
mPaintBorder.setColor(foregroundColor);
mPaintBorder.setStyle(Paint.Style.STROKE);
mPaintBorder.setStrokeWidth(2.0f * CanvasAdapter.getScale());
}
private void setText(String text) {
mText = text;
}
private void draw(Canvas canvas) {
int halfsize = mSize >> 1;
final int noneClippingRadius = halfsize - getPixels(2);
// fill
canvas.drawCircle(halfsize, halfsize, noneClippingRadius, mPaintCircle);
// outline
canvas.drawCircle(halfsize, halfsize, noneClippingRadius, mPaintBorder);
// draw the number at the center
canvas.drawText(mText,
(canvas.getWidth() - mPaintText.getTextWidth(mText)) * 0.5f,
(canvas.getHeight() + mPaintText.getTextHeight(mText)) * 0.5f,
mPaintText);
}
public Bitmap getBitmap() {
int width = mSize, height = mSize;
width = width > 0 ? width : 1;
height = height > 0 ? height : 1;
Bitmap bitmap = CanvasAdapter.newBitmap(width, height, 0);
Canvas canvas = CanvasAdapter.newCanvas();
canvas.setBitmap(bitmap);
draw(canvas);
return bitmap;
}
}
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2017 nebular
* Copyright 2017 devemux86
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.android.cluster;
import org.oscim.layers.marker.InternalItem;
/**
* Extension for clustered items.
*/
public class Clustered extends InternalItem {
/**
* If this is >0, this item will be displayed as a cluster circle, with size clusterSize+1.
*/
public int clusterSize;
/**
* If this is true, this item is hidden (because it's represented by another InternalItem acting as cluster.
*/
public boolean clusteredOut;
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2017 nebular
* Copyright 2017 devemux86
*
* This program is free software: you can redistribute it and/or modify it under the
* terms of the GNU Lesser 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.oscim.android.cluster;
import org.oscim.core.GeoPoint;
import org.oscim.layers.marker.MarkerItem;
/**
* If a MarkerItem is created using this convenience class instead of MarkerItem,
* this specific item will not be clusterable.
*/
public class NonClusterable extends MarkerItem {
public NonClusterable(String title, String description, GeoPoint geoPoint) {
super(null, title, description, geoPoint);
}
public NonClusterable(Object uid, String title, String description, GeoPoint geoPoint) {
super(uid, title, description, geoPoint);
}
}

View File

@@ -17,6 +17,7 @@ package org.oscim.android.test;
import android.graphics.BitmapFactory;
import org.oscim.android.canvas.AndroidBitmap;
import org.oscim.android.cluster.ClusterMarkerRenderer;
import org.oscim.backend.canvas.Bitmap;
import org.oscim.backend.canvas.Color;
import org.oscim.core.GeoPoint;