Android Mapview: объединение перекрывающихся маркеров в новый маркер
Итак, у меня есть MapView с большим количеством маркеров, большинство из которых сосредоточены в кластерах шириной в милю. При увеличении маркеры перекрываются и кажутся только одним. Чего я хочу добиться, так это при определенном уровне масштабирования заменить перекрывающиеся маркеры групповым маркером, который будет отображать плотность маркеров, а onClick будет изменять масштаб, чтобы отобразить все маркеры внутри. Я знаю, что могу сделать это с помощью измерения расстояния грубой силой, но должен быть более эффективный способ. У кого-нибудь есть какое-нибудь решение или умные алгоритмы, как мне этого добиться?
7 ответов
Гм... при условии, что маркеры не сгруппированы, не наслоены или что-то еще: почему - прежде чем показывать их - вы не создаете сетку определенной плотности и просто помещаете маркеры в ячейки вашей сетки?
Если вы посчитаете, что несколько маркеров попадают в одну ячейку (ячейку сетки) - вы можете сгруппировать их. Если вам нужна более умная группировка, вы можете также проверить соседние ячейки.
Может быть, это звучит немного примитивно, но:
- Нет п ^2 алгоритмов
- Нет предположения о порядке ввода
- Нет необходимости дополнительно обрабатывать маркеры, которые не будут показаны
Код для сетки:
Примечание. Я из мира C++ (попал сюда через тег [attribute]), поэтому я буду придерживаться псевдо-C++. Я не знаю API карты. Но я был бы удивлен, если бы это не могло быть эффективно переведено на любой язык / библиотеку, которую вы используете.
Входные данные: - список маркеров - окно просмотра прямоугольника в мировых координатах (часть мира, которую мы сейчас ищем)
В простейшей форме это будет выглядеть примерно так:
void draw(MarkerList mlist, View v) {
//binning:
list<Marker> grid[densityX][densityY]; //2D array with some configurable, fixed density
foreach(Marker m in mlist) {
if (m.within(v)) {
int2 binIdx;
binIdx.x=floor(densityX*(m.coord.x-v.x1)/(v.x2-v.x1));
binIdx.y=floor(densityY*(m.coord.y-v.y1)/(v.y2-v.y1));
grid[binIdx.x][binIdx.y].push(m); //just push the reference
}
//drawing:
for (int i=0; i<densityX; ++i)
for (int j=0; j<densityY; ++j) {
if (grid[i][j].size()>N) {
GroupMarker g;
g.add(grid[i][j]); //process the list of markers belonging to this cell
g.draw();
} else {
foreach (Marker m in grid[i][j])
m.draw()
}
}
}
Проблема, которая может появиться, состоит в том, что нежелательное разделение сетки может появиться в некоторой кластерной группе, образуя два GroupMarkers. Чтобы противостоять этому, вы можете рассмотреть не только одну ячейку сетки, но и ее соседей в разделе "\drawing", и - если они сгруппированы - пометить соседние ячейки как посещенные.
Я преобразовал ответ Cygnus X1 в Java. Поместите этот метод в свой пользовательский оверлей и измените drawSingle() и drawGroup() в соответствии со своими потребностями. Вы также улучшаете производительность, например, конвертируете ArrayLists в примитивные массивы.
@Override
public void draw(Canvas canvas, MapView mapView, boolean shadow) {
// binning:
int densityX = 10;
int densityY = 10;
// 2D array with some configurable, fixed density
List<List<List<OverlayItem>>> grid = new ArrayList<List<List<OverlayItem>>>(
densityX);
for(int i = 0; i<densityX; i++){
ArrayList<List<OverlayItem>> column = new ArrayList<List<OverlayItem>>(densityY);
for(int j = 0; j < densityY; j++){
column.add(new ArrayList<OverlayItem>());
}
grid.add(column);
}
for (OverlayItem m : mOverlays) {
int binX;
int binY;
Projection proj = mapView.getProjection();
Point p = proj.toPixels(m.getPoint(), null);
if (isWithin(p, mapView)) {
double fractionX = ((double)p.x / (double)mapView.getWidth());
binX = (int) (Math.floor(densityX * fractionX));
double fractionY = ((double)p.y / (double)mapView.getHeight());
binY = (int) (Math
.floor(densityX * fractionY));
// Log.w("PointClusterer absolute", p.x+ ", "+p.y);
// Log.w("PointClusterer relative", fractionX+ ", "+fractionY);
// Log.w("PointClusterer portion", "Marker is in portion: " + binX
// + ", " + binY);
grid.get(binX).get(binY).add(m); // just push the reference
}
}
// drawing:
for (int i = 0; i < densityX; i++) {
for (int j = 0; j < densityY; j++) {
List<OverlayItem> markerList = grid.get(i).get(j);
if (markerList.size() > 1) {
drawGroup(canvas, mapView, markerList);
} else {
// draw single marker
drawSingle(canvas, mapView, markerList);
}
}
}
}
private void drawGroup(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
GeoPoint point = markerList.get(0).getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("GROUP", ptScreenCoord.x, ptScreenCoord.y + 30, paint);
}
private void drawSingle(Canvas canvas, MapView mapView,
List<OverlayItem> markerList) {
for (OverlayItem item : markerList) {
GeoPoint point = item.getPoint();
Point ptScreenCoord = new Point();
mapView.getProjection().toPixels(point, ptScreenCoord);
Paint paint = new Paint();
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(30);
paint.setAntiAlias(true);
paint.setARGB(150, 0, 0, 0);
// show text to the right of the icon
canvas.drawText("SINGLE", ptScreenCoord.x, ptScreenCoord.y + 30,
paint);
}
}
public static boolean isWithin(Point p, MapView mapView) {
return (p.x > 0 & p.x < mapView.getWidth() & p.y > 0 & p.y < mapView
.getHeight());
}
}
Следующее прагматическое решение, основанное на расстоянии в пикселях, действительно работает лучше всего для меня:
http://www.appelsiini.net/2008/11/introduction-to-marker-clustering-with-google-maps
То, что вы ищете, обычно называется кластеризацией. Для этого есть общие приемы, вы можете сослаться, например, на этот SO вопрос, который ведет к этому посту.
Основная идея состоит в том, чтобы разделить карту на квадраты на основе текущего уровня масштабирования (вы можете кэшировать вычисления на основе уровня масштабирования, чтобы избежать пересчета, когда пользователь начинает масштабирование), и сгруппировать их на основе того, к какому квадрату они принадлежат. Таким образом, у вас получится группировка, основанная на уровне масштабирования, т.е. для уровня 1-5 просто нарисуйте маркеры, для уровня 5-8 сгруппируйте их в квадраты по 20 миль, по 9-10 в квадраты по 50 миль и т. Д. на.
Вот еще один важный вопрос о SO, на который вы, возможно, захотите взглянуть, хотя и не уверены в его эффективности: Android Maps Point Clustering
Предполагая, что ваши маркеры сгруппированы в ItemizedOverlay, вы можете создать метод, который вызывался при масштабировании карты. Это позволит сравнить координаты пикселей каждого маркера, чтобы увидеть, перекрываются ли они, и установить флаг. Затем в методе рисования вы можете нарисовать сгруппированный маркер или отдельных лиц;
Что-то вроде:
//this would need to be wired to be called when the mapview is zoomed
//it sets the drawgrouped flag if co-ordinates are close together
Boolean drawGrouped=false;
public void onMapZoom(MapView mapView){
//loop thru overlay items
Integer i,l=this.size();
OverlayItem item;
Integer deltaX=null,deltaY=null;
Projection proj = mapView.getProjection();
Point p=new Point();
Integer x=null,y=null;
Integer tolerance = 10; //if co-ordinates less than this draw grouped icon
for(i=0;i<l;i++){
//get the item
item=this.getItem(i);
//convert the overlays position to pixels
proj.toPixels(item.getPoint(), p);
proj.toPixels(item.getPoint(), p);
//compare co-ordinates
if(i==0){
x=p.x;
y=p.y;
continue;
}
deltaX=Math.abs(p.x-x);
deltaY=Math.abs(p.y-y);
//if the co-ordinates are too far apart dont draw grouped
if(deltaX>tolerance || deltaY>tolerance){
drawGrouped=false;
return;
}
x=p.x;
y=p.y;
}
//all co-ords are within the tolerance
drawGrouped=true;
}
public void draw(android.graphics.Canvas canvas, MapView mapView, boolean shadow){
if(drawGrouped==true){
//draw the grouped icon *needs to be optimised to only do it once
drawGrouped(canvas,mapView,shadow);
return;
}
//not grouped do regular drawing
super.draw(canvas, mapView, shadow);
}
Если ваши маркеры сгруппированы, у вас будет четкое представление о том, на каком уровне масштабирования вы должны отображать отдельные маркеры или маркер группы, например, уровень масштабирования> 17, затем отображать отдельные маркеры, в противном случае отображать маркер группы. Я использовал код что-то вроде этого в моем ItemizedOverlay, чтобы изменить мои маркеры:
@Override
public void draw(Canvas canvas, MapView mapv, boolean shadow)
{
int zoom = mapv.getZoomLevel();
switch(zoom)
{
case 19:
setMarkersForZoomLevel19();
break;
case 18:
setMarkersForZoomLevel18();
break;
case 17:
setMarkersForZoomLevel17();
break;
case 16:
setMarkersForZoomLevel16();
break;
default:
// Hide the markers or remove the overlay from the map view.
mapv.getOverlays().clear();
}
area.drawArea(canvas, mapv);
// Putting this call here rather than at the beginning, ensures that
// the Overlay items are drawn over the top of canvas stuff e.g. route lines.
super.draw(canvas, mapv, false);
}
private void setMarkersForZoomLevel19()
{
for (JourneyOverlayItem item : mOverlays)
{
item.setMarker(areaPointIcon48);
}
}
Если возможно иметь отдельные маркеры в коллекции, вы можете легко получить наибольшую и наименьшую широту и долготу, а разница между ними даст вам интервал широты и долготы (затем его можно использовать для увеличения диапазона, чтобы показать группа маркеров). Разделите участки на 2, и у вас должна быть центральная точка для размещения маркера группы.
Это подход, который я использовал. Тем не менее, это O(n^2).
Булавки должны быть отсортированы на основе видных.
Подберите булавку с самым высоким выступом. Посмотрите на все булавки вокруг него. Поглотите контакты рядом с этим контактом.
Затем перейдите к следующему наиболее заметному выводу. Делать то же самое. Повторение.
Просто.
Ситуация усложняется, если вы перемещаете карту, увеличиваете или уменьшаете масштаб и хотите убедиться, что новые контакты не перерисовываются. Таким образом, вы проверяете каждый кластер, если они должны разделиться во время увеличения, а затем вы проверяете каждый кластер, если они должны объединиться во время уменьшения. Затем вы удаляете булавки, которые ушли, и добавляете новые булавки. Для каждого добавляемого вами пин-кода вы проверяете, должны ли они присоединиться к кластеру или сформировать собственный кластер.