Marker clustering (#296) (#310) #312

This commit is contained in:
rodo 2017-02-19 15:09:09 +01:00 committed by Emux
parent 84c45e153a
commit 4b30ca3242
10 changed files with 1055 additions and 21 deletions

View File

@ -35,6 +35,9 @@
<activity
android:name=".BitmapTileMapActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".ClusterMarkerOverlayActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />
<activity
android:name=".JeoIndoorMapActivity"
android:configChanges="keyboardHidden|orientation|screenSize" />

View File

@ -0,0 +1,84 @@
/*
* Copyright 2016-2017 devemux86
* Copyright 2017 nebular
*
* 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.test;
import android.graphics.drawable.BitmapDrawable;
import android.os.Bundle;
import org.oscim.android.canvas.AndroidBitmap;
import org.oscim.core.GeoPoint;
import org.oscim.core.MapPosition;
import org.oscim.layers.marker.ClusterMarkerRenderer;
import org.oscim.layers.marker.ItemizedLayer;
import org.oscim.layers.marker.MarkerItem;
import org.oscim.layers.marker.MarkerSymbol;
import org.oscim.theme.VtmThemes;
import java.util.ArrayList;
public class ClusterMarkerOverlayActivity extends BaseMapActivity {
private static final int COUNT = 5;
private static final float STEP = 100f / 110000f; // roughly 100 meters
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mMap.setTheme(VtmThemes.DEFAULT);
MapPosition pos = new MapPosition();
mMap.getMapPosition(pos);
pos.setZoomLevel(2);
mMap.setMapPosition(pos);
ItemizedLayer<MarkerItem> layer = new ItemizedLayer<>(
mMap,
ClusterMarkerRenderer.factory(null, new ClusterMarkerRenderer.ClusterStyle(0xffffffff, 0xff123456))
);
// add it top the map
mMap.layers().add(layer);
// create a symbol, for simplicity we will use this symbol for all created markers
MarkerSymbol symbol = new MarkerSymbol(
new AndroidBitmap(((BitmapDrawable) (getResources().getDrawable(R.drawable.marker_poi))).getBitmap()),
MarkerSymbol.HotspotPlace.CENTER
);
// create some markers spaced STEP degrees
GeoPoint center = pos.getGeoPoint();
ArrayList<MarkerItem> list = new ArrayList<>();
for (int x = -COUNT; x < COUNT; x++) {
for (int y = -COUNT; y < COUNT; y++) {
double random = STEP * Math.random() * 2;
MarkerItem item = new MarkerItem(
"Demo Marker " + ((x * COUNT) + y),
"Your typical marker in your typical map",
new GeoPoint(center.getLatitude() + y * STEP + random, center.getLongitude() + x * STEP + random)
);
item.setMarker(symbol);
list.add(item);
}
}
// add'em all at once
layer.addItems(list);
}
}

View File

@ -3,6 +3,7 @@
* Copyright 2013 Hannes Janetzek
* Copyright 2016-2017 devemux86
* Copyright 2017 Longri
* Copyright 2017 nebular
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
@ -54,6 +55,7 @@ public class Samples extends Activity {
linearLayout.addView(createButton(RotateMarkerOverlayActivity.class));
linearLayout.addView(createButton(AtlasMarkerOverlayActivity.class));
linearLayout.addView(createButton(AtlasMultiTextureActivity.class));
linearLayout.addView(createButton(ClusterMarkerOverlayActivity.class));
linearLayout.addView(createButton(PathOverlayActivity.class));
linearLayout.addView(createButton(LineTexActivity.class));
linearLayout.addView(createButton(VectorLayerMapActivity.class));

View File

@ -0,0 +1,428 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 Izumi Kawashima
* Copyright 2017 Longri
* Copyright 2017 devemux86
* Copyright 2017 nebular
*
* 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.layers.marker;
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.utils.ScreenUtils;
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
*/
private 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 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
*/
private 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
*/
private static Bitmap[] mClusterBitmaps = new Bitmap[CLUSTER_MAXSIZE + 1];
private int mStyleBackground = CLUSTER_COLORBACK, mStyleForeground = CLUSTER_COLORTEXT;
/**
* Discrete scale step, used to trigger reclustering on significant scale change
*/
private int mScaleSaved = 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 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 boolean mClusteringEnabled = false;
/**
* 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<MarkerInterface> 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, mScaleSaved);
}
/**
* 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 = ScreenUtils.getPixels(MAP_GRID_SIZE_DP);
/* the factor to map into Grid Coordinates (discrete squares of GRIDSIZE x GRIDSIZE) */
final double factor = (scale / GRIDSIZE);
InternalItem.Clustered[] tmp = new InternalItem.Clustered[size];
// clear grid map to count items that share the same "grid slot"
mGridMap.clear();
for (int i = 0; i < size; i++) {
InternalItem.Clustered it = tmp[i] = new InternalItem.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 MarkerItem.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 != mScaleSaved) {
mScaleSaved = scalepow;
// 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;
mMarkerLayer.map().viewport().getMapExtents(mBox, mExtents);
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) {
InternalItem.Clustered it = (InternalItem.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) {
InternalItem.Clustered it = (InternalItem.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
*/
private 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
ScreenUtils.ClusterDrawable drawable = new ScreenUtils.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,57 @@
/*
* Copyright 2013 Hannes Janetzek
* Copyright 2016 Izumi Kawashima
* Copyright 2017 Longri
* Copyright 2017 devemux86
* Copyright 2017 nebular
*
* 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.layers.marker;
/**
* The internal representation of a marker.
*/
class InternalItem {
MarkerInterface item;
boolean visible;
boolean changes;
float x, y;
double px, py;
float dy;
@Override
public String toString() {
return "\n" + x + ":" + y + " / " + dy + " " + visible;
}
/**
* Extension to the above class for clustered items. This could be a separate 1st level class,
* but it is included here not to pollute the source tree with tiny new files.
* It only adds a couple properties to InternalItem, and the semantics "InternalItem.Clustered"
* are not bad.
*/
static class Clustered extends InternalItem {
/**
* If this is >0, this item will be displayed as a cluster circle, with size clusterSize+1.
*/
int clusterSize;
/**
* If this is true, this item is hidden (because it's represented by another InternalItem acting as cluster.
*/
boolean clusteredOut;
}
}

View File

@ -8,6 +8,7 @@
* Copyright 2016 devemux86
* Copyright 2016 Erik Duisters
* Copyright 2017 Longri
* Copyright 2017 nebular
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
@ -81,4 +82,18 @@ public class MarkerItem implements MarkerInterface {
if (mMarker != null)
mMarker.setRotation(rotation);
}
/**
* If a MarkerItem is created using this convenience class instead of MarkerItem,
* this specific item will not be clusterable.
*/
public static 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

@ -3,6 +3,7 @@
* Copyright 2016 Izumi Kawashima
* Copyright 2017 Longri
* Copyright 2017 devemux86
* Copyright 2017 nebular
*
* This file is part of the OpenScienceMap project (http://www.opensciencemap.org).
*
@ -17,7 +18,6 @@
* 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.layers.marker;
import org.oscim.core.MercatorProjection;
@ -36,10 +36,10 @@ public class MarkerRenderer extends BucketRenderer {
protected final MarkerSymbol mDefaultMarker;
private final SymbolBucket mSymbolLayer;
private final float[] mBox = new float[8];
private final MarkerLayer<MarkerInterface> mMarkerLayer;
private final Point mMapPoint = new Point();
protected final SymbolBucket mSymbolLayer;
protected final float[] mBox = new float[8];
protected final MarkerLayer<MarkerInterface> mMarkerLayer;
protected final Point mMapPoint = new Point();
/**
* increase view to show items that are partially visible
@ -49,23 +49,9 @@ public class MarkerRenderer extends BucketRenderer {
/**
* flag to force update of markers
*/
private boolean mUpdate;
protected boolean mUpdate;
private InternalItem[] mItems;
static class InternalItem {
MarkerInterface item;
boolean visible;
boolean changes;
float x, y;
double px, py;
float dy;
@Override
public String toString() {
return "\n" + x + ":" + y + " / " + dy + " " + visible;
}
}
protected InternalItem[] mItems;
public MarkerRenderer(MarkerLayer<MarkerInterface> markerLayer, MarkerSymbol defaultSymbol) {
mSymbolLayer = new SymbolBucket();

View File

@ -0,0 +1,89 @@
/*
* Copyright 2017 nebular
*
* 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.layers.marker.utils;
/**
* This comes for Android API. It is used by the INT SPARSEARRAY.
* The Android version uses some native routines to create the arrays, I suppose for performance.
* However our use of this class is very basic, and we will avoid resizing the arrays as much
* as possible.
* <p>
* A helper class that aims to provide comparable growth performance to ArrayList, but on primitive
* arrays. Common array operations are implemented for efficient use in dynamic containers.
* <p>
* All methods in this class assume that the length of an array is equivalent to its capacity and
* NOT the number of elements in the array. The current size of the array is always passed in as a
* parameter.
*/
public final class GrowingArrayUtils {
/**
* Appends an element to the end of the array, growing the array if there is no more room.
*
* @param array The array to which to append the element. This must NOT be null.
* @param currentSize The number of elements in the array. Must be less than or equal to
* array.length.
* @param element The element to append.
* @return the array to which the element was appended. This may be different than the given
* array.
*/
public static int[] append(int[] array, int currentSize, int element) {
assert currentSize <= array.length;
if (currentSize + 1 > array.length) {
int[] newArray = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, currentSize);
array = newArray;
}
array[currentSize] = element;
return array;
}
/**
* Inserts an element into the array at the specified index, growing the array if there is no
* more room.
*
* @param array The array to which to append the element. Must NOT be null.
* @param currentSize The number of elements in the array. Must be less than or equal to
* array.length.
* @param element The element to insert.
* @return the array to which the element was appended. This may be different than the given
* array.
*/
public static int[] insert(int[] array, int currentSize, int index, int element) {
assert currentSize <= array.length;
if (currentSize + 1 <= array.length) {
System.arraycopy(array, index, array, index + 1, currentSize - index);
array[index] = element;
return array;
}
int[] newArray = new int[growSize(currentSize)];
System.arraycopy(array, 0, newArray, 0, index);
newArray[index] = element;
System.arraycopy(array, index, newArray, index + 1, array.length - index);
return newArray;
}
/**
* Given the current size of an array, returns an ideal size to which the array should grow.
* This is typically double the given size, but should not be relied upon to do so in the
* future.
*/
public static int growSize(int currentSize) {
return currentSize <= 4 ? 8 : currentSize * 2;
}
private GrowingArrayUtils() {
}
}

View File

@ -0,0 +1,106 @@
/*
* Copyright 2017 nebular
*
* 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.layers.marker.utils;
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 ScreenUtils {
/**
* https://developer.android.com/reference/android/util/DisplayMetrics.html#DENSITY_DEFAULT
*/
private static final float REFERENCE_DPI = 160;
/**
* 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.dpi / REFERENCE_DPI * dp);
}
public static class ClusterDrawable {
private Paint mPaintText = CanvasAdapter.newPaint();
private Paint mPaintCircle = CanvasAdapter.newPaint(), 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 = ScreenUtils.getPixels(sizedp);
mPaintText.setTextSize(ScreenUtils.getPixels((int) (sizedp * 0.6666666)));
mPaintText.setColor(foregroundColor);
// NOT SUPPORTED on current backends (Feb 2017)
// mPaintText.setTextAlign(Paint.Align.CENTER);
mPaintCircle.setColor(backgroundColor);
mPaintCircle.setStyle(Paint.Style.FILL);
mPaintBorder.setColor(foregroundColor);
mPaintBorder.setStyle(Paint.Style.STROKE);
mPaintBorder.setStrokeWidth(2.0f);
}
private void setText(String text) {
mText = text;
}
private void draw(Canvas canvas) {
int halfsize = mSize >> 1;
// outline
canvas.drawCircle(halfsize, halfsize, halfsize, mPaintCircle);
// fill
canvas.drawCircle(halfsize, halfsize, halfsize, mPaintBorder);
// draw the number, the centering is not perfect without a measureText or alignment
canvas.drawText(mText, halfsize * 0.6f, halfsize + (halfsize >> 1), 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,264 @@
/*
* Copyright (C) 2006 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.oscim.layers.marker.utils;
/**
* Cloned from Android API: SparseInt Array
*/
public class SparseIntArray implements Cloneable {
private int[] mKeys;
private int[] mValues;
private int mSize;
/**
* Creates a new SparseIntArray containing no mappings that will not
* require any additional memory allocation to store the specified
* number of mappings. If you supply an initial capacity of 0, the
* sparse array will be initialized with a light-weight representation
* not requiring any additional array allocations.
*/
public SparseIntArray(int initialCapacity) {
if (initialCapacity == 0)
initialCapacity = 100;
/** Android original code used native functions to create this array */
mKeys = new int[initialCapacity];
mValues = new int[mKeys.length];
mSize = 0;
}
@Override
public SparseIntArray clone() {
SparseIntArray clone = null;
try {
clone = (SparseIntArray) super.clone();
clone.mKeys = mKeys.clone();
clone.mValues = mValues.clone();
} catch (CloneNotSupportedException cnse) {
/* ignore */
}
return clone;
}
/**
* Gets the int mapped from the specified key, or <code>0</code>
* if no such mapping has been made.
*/
public int get(int key) {
return get(key, 0);
}
/**
* Gets the int mapped from the specified key, or the specified value
* if no such mapping has been made.
*/
public int get(int key, int valueIfKeyNotFound) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i < 0) {
return valueIfKeyNotFound;
} else {
return mValues[i];
}
}
/**
* Removes the mapping from the specified key, if there was any.
*/
public void delete(int key) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
removeAt(i);
}
}
/**
* Removes the mapping at the given index.
*/
public void removeAt(int index) {
System.arraycopy(mKeys, index + 1, mKeys, index, mSize - (index + 1));
System.arraycopy(mValues, index + 1, mValues, index, mSize - (index + 1));
mSize--;
}
/**
* Adds a mapping from the specified key to the specified value,
* replacing the previous mapping from the specified key if there
* was one.
*/
public void put(int key, int value) {
int i = ContainerHelpers.binarySearch(mKeys, mSize, key);
if (i >= 0) {
mValues[i] = value;
} else {
i = ~i;
mKeys = GrowingArrayUtils.insert(mKeys, mSize, i, key);
mValues = GrowingArrayUtils.insert(mValues, mSize, i, value);
mSize++;
}
}
/**
* Returns the number of key-value mappings that this SparseIntArray
* currently stores.
*/
public int size() {
return mSize;
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseIntArray stores.
* <p>
* <p>The keys corresponding to indices in ascending order are guaranteed to
* be in ascending order, e.g., <code>keyAt(0)</code> will return the
* smallest key and <code>keyAt(size()-1)</code> will return the largest
* key.</p>
*/
public int keyAt(int index) {
return mKeys[index];
}
/**
* Given an index in the range <code>0...size()-1</code>, returns
* the value from the <code>index</code>th key-value mapping that this
* SparseIntArray stores.
* <p>
* <p>The values corresponding to indices in ascending order are guaranteed
* to be associated with keys in ascending order, e.g.,
* <code>valueAt(0)</code> will return the value associated with the
* smallest key and <code>valueAt(size()-1)</code> will return the value
* associated with the largest key.</p>
*/
public int valueAt(int index) {
return mValues[index];
}
/**
* Directly set the value at a particular index.
*/
public void setValueAt(int index, int value) {
mValues[index] = value;
}
/**
* Returns the index for which {@link #keyAt} would return the
* specified key, or a negative number if the specified
* key is not mapped.
*/
public int indexOfKey(int key) {
return ContainerHelpers.binarySearch(mKeys, mSize, key);
}
/**
* Returns an index for which {@link #valueAt} would return the
* specified key, or a negative number if no keys map to the
* specified value.
* Beware that this is a linear search, unlike lookups by key,
* and that multiple keys can map to the same value and this will
* find only one of them.
*/
public int indexOfValue(int value) {
for (int i = 0; i < mSize; i++)
if (mValues[i] == value)
return i;
return -1;
}
/**
* Removes all key-value mappings from this SparseIntArray.
*/
public void clear() {
mSize = 0;
}
/**
* Puts a key/value pair into the array, optimizing for the case where
* the key is greater than all existing keys in the array.
*/
public void append(int key, int value) {
if (mSize != 0 && key <= mKeys[mSize - 1]) {
put(key, value);
return;
}
mKeys = GrowingArrayUtils.append(mKeys, mSize, key);
mValues = GrowingArrayUtils.append(mValues, mSize, value);
mSize++;
}
/**
* {@inheritDoc}
* <p>
* <p>This implementation composes a string by iterating over its mappings.
*/
@Override
public String toString() {
if (size() <= 0) {
return "{}";
}
StringBuilder buffer = new StringBuilder(mSize * 28);
buffer.append('{');
for (int i = 0; i < mSize; i++) {
if (i > 0) {
buffer.append(", ");
}
int key = keyAt(i);
buffer.append(key);
buffer.append('=');
int value = valueAt(i);
buffer.append(value);
}
buffer.append('}');
return buffer.toString();
}
static class ContainerHelpers {
// This is Arrays.binarySearch(), but doesn't do any argument validation.
static int binarySearch(int[] array, int size, int value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final int midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
static int binarySearch(long[] array, int size, long value) {
int lo = 0;
int hi = size - 1;
while (lo <= hi) {
final int mid = (lo + hi) >>> 1;
final long midVal = array[mid];
if (midVal < value) {
lo = mid + 1;
} else if (midVal > value) {
hi = mid - 1;
} else {
return mid; // value found
}
}
return ~lo; // value not present
}
}
}