Недавно мне представилась возможность реализовать проект с использованием карт Google. В какой-то момент я увидел вот эту картинку:
Это происходило, как вы уже догадались, когда поблизости было слишком много мест, которые нужно было отобразить.
Ну ничего - проблема вроде стандартная - многие должны были с этим столкнуться, разработчики наверняка предусмотрели группировку и фильтрацию.
Да, это не так! Полистав документацию (сразу признаюсь, что я был лишь беглым - возможно, я пропустил то, что мне было нужно) и задав вопрос Гуглу - к своему удивлению, я быстро не смог найти ничего нужного.
Ну да ладно - не боги горшки обжигают - так что сами напишем.
Сначала давайте создадим проект и подключим к нему API Google Maps. Тут думаю объяснять ничего не надо - об этом уже много написано, кстати вот ссылки на хабе - один раз , два , три .
Для начала давайте определимся, что и как должно работать.
Идти… Очевидно, нам нужно придумать какой-то алгоритм, согласно которому на экране будут отображаться не все пины, а только те, которые расположены на определенном расстоянии друг от друга.
А еще вам нужно научиться объединять их в группы.
Перерасчет всей этой экономики должен произойти: 1) При первой загрузке пинов на карту.
2) При изменении масштаба В принципе, с первым пунктом все понятно – давайте определимся со вторым.
Объявим интерфейс:
И мы модифицируем наш MapView следующим образом:public interface IOnZoomListener { void onZoomChanged(); }
public class MyMapView extends MapView {
int oldZoomLevel = -1;
IOnZoomListener onZoomListener;
public MyMapView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public MyMapView(Context context, String apiKey) {
super(context, apiKey);
}
public MyMapView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void setOnZoomListener(IOnZoomListener onZoomListener) {
this.onZoomListener = onZoomListener;
}
@Override
public void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
int newZoom = this.getZoomLevel();
if (newZoom != oldZoomLevel) {
if (oldZoomLevel != -1 && onZoomListener != null) {
onZoomListener.onZoomChanged();
}
oldZoomLevel = getZoomLevel();
}
}
}
Здесь мы просто добавили возможность прописывать наш интерфейс и проверять при рисовании изменение ZoomLevel (если ZoomLevel изменился, мы подтягиваем метод нашего интерфейса).
Теперь определимся, как мы будем отображать пины на карте и как объединять их в группы.
Для этого создадим класс MyOverlayItem, унаследованный от OverlayItem со следующими дополнениями: public class MyOverlayItem extends OverlayItem {
private String name;
private ArrayList<MyOverlayItem> list = new ArrayList<MyOverlayItem>();
public MyOverlayItem(GeoPoint point, String name) {
super(point, "", "");
this.name = name;
}
public String getName() {
if (list.size() > 0) {
return "There are " + (list.size() + 1) + " places.";
} else {
return name;
}
}
public void addList(MyOverlayItem item) {
list.add(item);
}
public ArrayList<MyOverlayItem> getList() {
return list;
}
}
В списке ArrayList будет храниться список сгруппированных пинов, а метод getName вернет либо имя объекта, либо их количество в группе.
Теперь опишем, для чего, собственно, все это и затевалось — нашего модифицированного ItemizedOverlay. Суть алгоритма фильтрации довольно проста: мы просто пробегаем в цикле по всем существующим элементам и проверяем каждый элемент на предмет близости к уже существующей группе элементов.
Если мы находим такую группу, элемент добавляется в нее; если нет, с этим элементом создается новая группа: boolean isImposition;
for (MyOverlayItem itemFromAll : myOverlaysAll) {
isImposition = false;
for (MyOverlayItem item : myOverlays) {
if (itemFromAll == item) {
isImposition = true;
break;
}
if (isImposition(itemFromAll, item)) {
item.addList(itemFromAll);
isImposition = true;
break;
}
}
if (!isImposition) {
myOverlays.add(itemFromAll);
}
}
Сначала для проверки расстояния я хотел просто использовать координаты булавок (ошибкой, возникающей в зависимости от широты, можно пренебречь, так как расстояния не большие), но тогда мне пришлось бы управлять еще и ZoomLevel.
Для моих задач вполне подошел метод mapView.getLatitudeSpan; он возвращает расстояние видимой ширины экрана в нужной нам системе координат. Остается только разделить это расстояние на определенный коэффициент (сколько максимальных пинов должно «поместиться» в экран по ширине) — это и будет минимальное расстояние между пинами: private boolean isImposition(MyOverlayItem item1, MyOverlayItem item2) {
int latspan = mapView.getLatitudeSpan();
int delta = latspan / KOEFF;
int dx = item1.getPoint().
getLatitudeE6() - item2.getPoint().
getLatitudeE6(); int dy = item1.getPoint().
getLongitudeE6() - item2.getPoint().
getLongitudeE6();
double dist = Math.sqrt(dx * dx + dy * dy);
if (dist < delta) {
return true;
} else {
return false;
}
}
На всякий случай вот полный исходный код класса: public class PlaceOverlay extends ItemizedOverlay<MyOverlayItem> {
private static final int KOEFF = 20;
private ArrayList<MyOverlayItem> myOverlaysAll = new ArrayList<MyOverlayItem>();
private ArrayList<MyOverlayItem> myOverlays = new ArrayList<MyOverlayItem>();
private MapView mapView;
public PlaceOverlay(Drawable defaultMarker, MapView mapView) {
super(boundCenterBottom(defaultMarker));
this.mapView = mapView;
populate();
}
public void addOverlay(MyOverlayItem overlay) {
myOverlaysAll.add(overlay);
myOverlays.add(overlay);
}
public void doPopulate() {
populate();
setLastFocusedIndex(-1);
}
@Override
protected MyOverlayItem createItem(int i) {
return myOverlays.get(i);
}
@Override
public int size() {
return myOverlays.size();
}
private boolean isImposition(MyOverlayItem item1, MyOverlayItem item2) {
int latspan = mapView.getLatitudeSpan();
int delta = latspan / KOEFF;
int dx = item1.getPoint().
getLatitudeE6() - item2.getPoint().
getLatitudeE6(); int dy = item1.getPoint().
getLongitudeE6() - item2.getPoint().
getLongitudeE6(); double dist = Math.sqrt(dx * dx + dy * dy); if (dist < delta) { return true; } else { return false; } } public void clear() { myOverlaysAll.clear(); myOverlays.clear(); } public void calculateItems() { myOverlaysClear(); boolean isImposition; for (MyOverlayItem itemFromAll : myOverlaysAll) { isImposition = false; for (MyOverlayItem item : myOverlays) { if (itemFromAll == item) { isImposition = true; break; } if (isImposition(itemFromAll, item)) { item.addList(itemFromAll); isImposition = true; break; } } if (!isImposition) { myOverlays.add(itemFromAll); } } doPopulate(); } private void myOverlaysClear() { for (MyOverlayItem item : myOverlaysAll) { item.getList().
clear(); } myOverlays.clear(); } @Override protected boolean onTap(int index) { Toast.makeText(mapView.getContext(), myOverlays.get(index).
getName(), Toast.LENGTH_SHORT).
show();
return true;
}
}
Ах да — в методе onTap мы отображаем Toast с названием группы — чтобы наглядно продемонстрировать работу алгоритма.
Хочу добавить, что этот алгоритм не является истиной в последней инстанции — его можно и нужно совершенствовать — например, рисовать булавку не на месте первого элемента группы, а рассчитывать ее расположение в зависимости от ее содержимого.
Но вы уже реализуете это сами в своих проектах.
Теперь давайте разберемся, как все это собрать воедино.
Давайте создадим ManyPinsProjectActivity, который будет наследовать от MapActivity и реализуем следующие интерфейсы: LocationListener, IOnZoomListener. Однако я не буду описывать все подробно — за меня все расскажет исходный код: public class ManyPinsProjectActivity extends MapActivity implements LocationListener, IOnZoomListener {
private static final int DEFAULT_ZOOM = 15;
private MyMapView mapView = null;
private Drawable myCurrentMarker = null;
private Drawable placeMarker = null;
private List<Overlay> mapOverlays;
private PlaceOverlay placeOverlay;
private MyCurrentLocationOverlay myCurrentLocationOverlay;
double currentLatitude, currentLongitude;
private MapController mapController;
private LocationManager locationManager;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
mapView = (MyMapView) findViewById(R.id.mapview);
myCurrentMarker = this.getResources().
getDrawable(R.drawable.my_pin_red); placeMarker = this.getResources().
getDrawable(R.drawable.my_pin);
myCurrentLocationOverlay = new MyCurrentLocationOverlay(myCurrentMarker, mapView);
placeOverlay = new PlaceOverlay(placeMarker, mapView);
mapOverlays = mapView.getOverlays();
mapController = mapView.getController();
mapView.setBuiltInZoomControls(true);
mapView.setOnZoomListener(this);
}
private void animateToPlaceOnMap(final GeoPoint geopoint) {
mapView.post(new Runnable() {
@Override
public void run() {
mapView.invalidate();
mapController.animateTo(geopoint);
mapController.setZoom(DEFAULT_ZOOM);
}
});
}
private void setCurrentGeopoint(double myLatitude, double myLongitude) {
currentLatitude = myLatitude;
currentLongitude = myLongitude;
final GeoPoint myCurrentGeoPoint = new GeoPoint((int) (myLatitude * 1E6), (int) (myLongitude * 1E6));
MyOverlayItem myCurrentItem = new MyOverlayItem(myCurrentGeoPoint, "Current Location");
myCurrentLocationOverlay.addOverlay(myCurrentItem);
mapOverlays.add(myCurrentLocationOverlay);
animateToPlaceOnMap(myCurrentGeoPoint);
}
@Override
protected void onPause() {
super.onPause();
locationManager.removeUpdates(this);
}
@Override
protected void onResume() {
super.onResume();
locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, 5000, 100, this);
}
private ArrayList<PlaceInfo> generatePlaces(){
Random random = new Random();
int x, y;
ArrayList<PlaceInfo> places = new ArrayList<PlaceInfo>();
PlaceInfo p;
for(int i = 0; i < 100; i++){
x = random.nextInt(2000);
y = random.nextInt(2000);
p = new PlaceInfo();
p.setLatitude(currentLatitude + x/100000f);
p.setLongitude(currentLongitude - y/100000f);
p.setName("Place № " + i);
places.add(p);
}
return places;
}
private void displayPlacesOnMap() {
ArrayList<PlaceInfo> places = generatePlaces();
mapOverlays.remove(placeOverlay);
GeoPoint point = null;
MyOverlayItem overlayitem = null;
placeOverlay.clear();
for (PlaceInfo place : places) {
point = new GeoPoint((int) (place.getLatitude() * 1E6), (int) (place.getLongitude() * 1E6));
overlayitem = new MyOverlayItem(point, place.getName());
placeOverlay.addOverlay(overlayitem);
}
placeOverlay.calculateItems();
placeOverlay.doPopulate();
if (placeOverlay.size() > 0) {
mapOverlays.add(placeOverlay);
mapView.postInvalidate();
}
}
@Override
public void onLocationChanged(Location location) {
locationManager.removeUpdates(this);
double myLatitude = location.getLatitude();
double myLongitude = location.getLongitude();
setCurrentGeopoint(myLatitude, myLongitude);
displayPlacesOnMap();
}
@Override
public void onStatusChanged(String provider, int status, Bundle extras) {
}
@Override
public void onProviderEnabled(String provider) {
}
@Override
public void onProviderDisabled(String provider) {
}
@Override
protected boolean isRouteDisplayed() {
return false;
}
@Override
public void onZoomChanged() {
if (placeOverlay != null) {
placeOverlay.calculateItems();
}
}
}
Здесь стоит добавить, что MyCurrentLocationOverlay — это обычный ItemizedOverlay с одним элементом, а PlaceInfo — это обычный класс-обертка, содержащий: private String name;
private double latitude;
private double longitude;
После всех этих манипуляций наша карта с булавками стала выглядеть вот так:
Надеюсь, статья окажется для вас полезной.
Весь проект можно найти по адресу связь .
Теги: #Android #API карт Google #Разработка Android
-
Mail.ru Агент 4.10 Избавит Вас От Спама
19 Oct, 24 -
Заглянем В Будущее Ит На 500 Лет
19 Oct, 24 -
Фотографии С Выставки Etarget-2008
19 Oct, 24 -
«Образование» Отстает
19 Oct, 24