Android-互いの距離に応じて、マップ上のフィルターピン

最近、Googleマップを使用してプロジェクトを行う機会がありました。 ある時点で、私はこの写真を見ました:













これは、近くに表示する必要のある場所が多すぎるときに起こったと思われます。 まあ、何も-問題は一種の標準です-多くの人々はこれに会うべきでした、おそらく開発者はグループ化とフィルタリングのために提供しました...ええ、それはありませんでした! ドキュメントを確認した後(私はすぐに流onlyにしか話せないことを認めました-必要なものを逃したかもしれません)、Googleに尋ねました-驚いたことに、私はすぐに必要なものを見つけることができませんでした。 まあ、何も-神は鍋を燃やしません-それから私たちは自分でそれを書きます。



まず、プロジェクトを作成し、Google Maps APIをそれに接続します。 ここでは、何も説明する必要はないと思います-既に多くのことが書かれていますが、ところで、ハブへのリンクは1、2、3です。



まず、何をどのように機能させるかを決定します。 行こう...



明らかに、すべてのピンが画面上に表示されるわけではなく、互いから一定の距離だけに表示されるアルゴリズムを考え出す必要があります。 また、それらをグループにまとめる方法も学ぶ必要があります。 このすべての経済の再計算は、以下が発生するために必要です。



1)カードのピンの初期ロード時。

2)ズームを変更する場合



最初の段落では、原則として、すべてが明確です-2番目の段落を決めましょう。 インターフェイスを宣言します。



public interface IOnZoomListener { void onZoomChanged(); }
      
      







そして、MapViewを次のように変更します。



 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が変更された場合-インターフェイスのメソッドをプルします)。



次に、ピンをマップに表示する方法と、それらをグループに結合する方法を決定します。 これを行うには、OverlayItemから継承したクラスMyOverlayItemを作成し、次の追加を行います。



 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を出力します-アルゴリズムの動作を明確に示します。



このアルゴリズムは究極の真実ではないことを付け加えます-改善することができ、改善する必要があります-たとえば、グループの最初の要素のサイトではなくピンを描きますが、その内容に応じてその位置を計算します。 しかし、あなたはすでにあなた自身のプロジェクトでこれを自分で実現しています。



それでは、すべてをまとめる方法を考えましょう。

MapActivityから継承するManyPinsProjectActivityを作成して、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は1つの要素を持つ通常のItemizedOverlayであり、PlaceInfoは以下を含む通常のラッパークラスであることを追加する価値があります。



  private String name; private double latitude; private double longitude;
      
      







すべてのこれらの操作の後-これは、ピン付きのマップがどのように見えるようになったかです:













この記事がお役に立てば幸いです。



プロジェクト全体はこちらにあります



All Articles