parent
84c45e153a
commit
4b30ca3242
@ -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" />
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
|
428
vtm/src/org/oscim/layers/marker/ClusterMarkerRenderer.java
Normal file
428
vtm/src/org/oscim/layers/marker/ClusterMarkerRenderer.java
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
57
vtm/src/org/oscim/layers/marker/InternalItem.java
Normal file
57
vtm/src/org/oscim/layers/marker/InternalItem.java
Normal 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
89
vtm/src/org/oscim/layers/marker/utils/GrowingArrayUtils.java
Normal file
89
vtm/src/org/oscim/layers/marker/utils/GrowingArrayUtils.java
Normal 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() {
|
||||
}
|
||||
}
|
106
vtm/src/org/oscim/layers/marker/utils/ScreenUtils.java
Normal file
106
vtm/src/org/oscim/layers/marker/utils/ScreenUtils.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
264
vtm/src/org/oscim/layers/marker/utils/SparseIntArray.java
Normal file
264
vtm/src/org/oscim/layers/marker/utils/SparseIntArray.java
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user