マップ上のマーカーのサーバークラスタリング。 理論から実践へ

こんにちはHabr。 ストーリーは、ユーザー自身が地図上にラベルを配置できる機能を備えたジオサービスを作成することに決めたという事実から始まります。

そして、データベースに100万個のマーカーを入力することにしたとき、特定の半径でのみマーカーをリクエストしても、すべてが非常に遅くなり、クライアントでのクラスタリングもオプションではないことに気付きました:)



そしてこの森の下のどこかにマンハッタンがあります









クラスタリングは非常に興味深いタスクであり、クライアント、サーバー、およびすべての主要なマップサービスがサポートしています。 多くのタイプのクラスタリングが知られていますが、私の検索ではグリッドクラスタリングを選択しました。 Yandexの従業員は、カンファレンスでのいくつかのレポートで彼女について話をしました。 Yandexマップでクラスターマーカーに使用されます。
Yandexクラスタリング








理論のビット



グリッドクラスタリングはメルカトル図法に基づいています。要するに、それは長方形の平面上の球体の球面の投影です。 これらは、すべてのアトラス、マップなどです。 このクラスタリングで名前が示すように、マップをグリッドでカバーし、このグリッドの各セルのマーカーの数を計算します。



条件:



タイルは基本的に、特定のズームのためにマップを正方形に分割するだけです。 最初のレベルでは、マップはレベル0の1正方形(タイル)、4正方形(タイル)のレベル1、16の2番目のレベル、64の3番目のレベルなどで構成されます。 実際、学位レベルは4です。





Quadkeyは、あらゆるレベルで一意のタイル識別子です。 最初のレベルでは、タイル0、1、2、3に右から左、上から下に番号を付けます。 次に、次のレベルで、最上位からの各タイルに対して、同じ番号を実行しますが、既に親のクワッドキーを追加します。 00.01,02,03,10,11,12,13など 写真からより明確になります。



したがって、4桁の計算システムで数値を取得しました。 各タイルには、任意のズームレベルで固有の識別子があります。 ズーム、緯度、経度から、すでにクワッドキータイル自体を取得できます。 任意のポイントを、対応するズームレベルのクワッドキーに変換できます。 そして、これはquadkeyが特定の座標を探す方法です:

経度-79.377807617187500

緯度43.653785705566406

quadkey 13940830302567(base4:03022313122033033011213)



マイクロソフトの英語によるこのアプローチの説明:

Bing Mapsタイルシステム

この記事では、これらすべての変換の実装はC#で記述されているため、PHPで簡単に書き直しました。

TileSystem.php
class TileSystem { const EarthRadius = 6378137; const MinLatitude = -90; const MaxLatitude = 90; const MinLongitude = -180; const MaxLongitude = 180; const MaxZoom = 23; private static function clip( $n, $minValue, $maxValue) { return min( max( $n, $minValue), $maxValue); } public static function mapSize( $levelOfDetail) { return 256 << $levelOfDetail; } public static function GroundResolution( $latitude, $levelOfDetail) { $latitude = self::clip( $latitude, self::MinLatitude, self::MaxLatitude); return cos( $latitude * pi() / 180) * 2 * pi() * self::EarthRadius / self::mapSize( $levelOfDetail); } public static function mapScale( $latitude, $levelOfDetail, $screenDpi) { return self::groundResolution( $latitude, $levelOfDetail) * $screenDpi / 0.0254; } public static function latToPixelY( $latitude, $levelOfDetail) { $latitude = self::clip( $latitude, self::MinLatitude, self::MaxLatitude); $sinLatitude = sin( $latitude * pi() / 180); $y = 0.5 - log((1 + $sinLatitude) / (1 - $sinLatitude)) / (4 * pi()); $mapSize = self::mapSize( $levelOfDetail); $pixelY = (int) self::clip( $y * $mapSize + 0.5, 0, $mapSize - 1); return $pixelY; } public static function longToPixelX( $longitude, $levelOfDetail) { $longitude = self::clip( $longitude, self::MinLongitude, self::MaxLongitude); $x = ($longitude + 180) / 360; $mapSize = self::mapSize( $levelOfDetail); $pixelX = (int) self::clip( $x * $mapSize + 0.5, 0, $mapSize - 1); return $pixelX; } public static function pixelXToLong( $pixelX, $levelOfDetail) { $mapSize = self::mapSize( $levelOfDetail); $x = ( self::clip( $pixelX, 0, $mapSize - 1) / $mapSize) - 0.5; $longitude = 360 * $x; return $longitude; } public static function pixelYToLat( $pixelY, $levelOfDetail) { $mapSize = self::mapSize( $levelOfDetail); $y = 0.5 - (self::clip( $pixelY, 0, $mapSize - 1) / $mapSize); $latitude = 90 - 360 * atan( exp(-$y * 2 * pi())) / pi(); return $latitude; } public static function pixelYToTileY( $pixelY) { return $pixelY / 256; } public static function pixelXToTileX( $pixelX) { return $pixelX / 256; } public static function tileXYToPixelXY( $tileX, $tileY, &$pixelX, &$pixelY) { $pixelX = $tileX * 256; $pixelY = $tileY * 256; } public static function tileXYToQuadKey( $tileX, $tileY, $levelOfDetail) { if ($levelOfDetail == 0) return "0"; $quadKey = ""; for ($i = $levelOfDetail; $i > 0; $i--) { $digit = 0; $mask = 1 << ($i - 1); if (($tileX & $mask) != 0) { $digit++; } if (($tileY & $mask) != 0) { $digit++; $digit++; } $quadKey .= $digit; } return $quadKey; } public static function latLongToQuadKeyDec( $lat, $long, $zoom) { return self::quadKey4ToDec( self::latLongToQuadKey( $lat, $long, $zoom)); } public static function latLongToQuadKey( $lat, $long, $zoom) { $pixelX = \foci\utils\TileSystem::longToPixelX( $long, $zoom); $pixelY = \foci\utils\TileSystem::latToPixelY( $lat, $zoom); $tileX = \foci\utils\TileSystem::pixelXToTileX( $pixelX); $tileY = \foci\utils\TileSystem::pixelYToTileY( $pixelY); return \foci\utils\TileSystem::tileXYToQuadKey( $tileX, $tileY, $zoom); } public static function pixelXYToQuadKey( $pixelX, $pixelY, $zoom) { $tileX = \foci\utils\TileSystem::pixelXToTileX( $pixelX); $tileY = \foci\utils\TileSystem::pixelYToTileY( $pixelY); return \foci\utils\TileSystem::tileXYToQuadKey( $tileX, $tileY, $zoom); } public static function pixelXYToQuadKeyDec( $pixelX, $pixelY, $zoom) { return base_convert( self::pixelXYToQuadKey( $pixelX, $pixelY, $zoom), 4, 10); } public static function quadKeyToTileXY( $quadKey, &$tileX, &$tileY, &$levelOfDetail) { $tileX = $tileY = 0; $levelOfDetail = strlen( $quadKey); for( $i = $levelOfDetail; $i > 0; $i--) { $mask = 1 << ($i - 1); switch( $quadKey[$levelOfDetail - $i]) { case '0': break; case '1': $tileX |= $mask; break; case '2': $tileY |= $mask; break; case '3': $tileX |= $mask; $tileY |= $mask; break; default:; } } } public static function tileXYToQuadKeyDec( $tileX, $tileY, $levelOfDetail) { return base_convert( self::tileXYToQuadKey( $tileX, $tileY, $levelOfDetail), 4, 10); } public static function quadKey4ToDec( $quadkey) { return base_convert( $quadkey, 4, 10); } }
      
      







練習に移りましょう



範囲



ここで重要な点を考慮する必要があります。特定のズームでマップ上のすべてのクラスターを要求する必要はありませんが、クライアントのスコープ内にあるクラスターのみを取得する必要があります(この場合は、AndroidとWebです)。



有効範囲内のクラスターを取得するためのオプション:



怠zyな顧客を選択しました。 なんで?



サーバーのロジックはどうなりますか?



保管



クワッドキーを保存するには、ズームレベルを選択する必要があります。23で十分です。 データベースはMySQLを使用します。 トークンストレージテーブルの構造は次のとおりです。

 CREATE TABLE `marker` ( `id` int(11) NOT NULL AUTO_INCREMENT, `latitude` FLOAT(19,15) NOT NULL, `longitude` FLOAT(19,15) NOT NULL, `quadkey` bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (`id`), INDEX `quadkey ` (`quadkey `) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
      
      





クワッドキーインデックスを使用すると、タイル(クラスター)内のマーカーの数を簡単に選択できます。



MySQLにクワッドキーを保存するには、bigint(20)を使用します。4桁のシステムから10進数に変換し、データベースとアプリケーションでは10桁のクワッドキーを使用します。



クラスター検索コード



クラスター検索コードは次のようになります。

  public function getClusters( $quad_key, $zoom) { $q1 = $quad_key . str_repeat("0", TileSystem::MaxZoom - $zoom); $q2 = $quad_key . str_repeat("3", TileSystem::MaxZoom - $zoom); $mask = $q1; $mask_plus = 1; $mask = $quad_key . str_repeat("3", $mask_plus); $mask = $mask . str_repeat("0", TileSystem::MaxZoom - $zoom - $mask_plus); $q1 = TileSystem::quadKey4ToDec( $q1); $q2 = TileSystem::quadKey4ToDec( $q2); $mask = TileSystem::quadKey4ToDec( $mask); return $this-> findBetweenQuadKeys( $q1, $q2, $mask); } public function findBetweenQuadKeys( $quad_key_l, $quad_key_r, $mask) { $entities = []; $sql = "SELECT `quadkey`, MIN(id) AS id, COUNT(id) AS count, AVG(longitude) AS longitude, AVG(latitude) AS latitude "; $sql .= "FROM `marker ` "; $sql .= "WHERE `quadkey` BETWEEN $quad_key_l AND $quad_key_r "; $sql .= "AND `is_deleted`= '0' "; $sql .= "GROUP BY (`quadkey` & $mask) "; if( $stmt = $this->_connection->query( $sql)) { foreach( $stmt as $row) $entities[] = $this->_createEntityFromRow( $row); } return $entities; }
      
      





$ mask変数を使用すると、特定のズームでクワッドキーをグループ化できます。 変数$ quad_key_lおよび$ quad_key_rを使用すると、この親のすべてのマーカーのクワッドキーを考慮することができます。つまり、$ quad_key_l = 03022313122033033000000および$ quad_key_r = 03022313122033033033333の場合、データベース内の030223131220330330330のすべてのマーカーが見つかります。

これは、結果のSQLクエリのようになります。

 SELECT `quadkey`, MIN(id) AS id, COUNT(id) AS count, AVG(longitude) AS longitude, AVG(latitude) AS latitude FROM marker WHERE quadkey between 15499682906112 and 15499683168255 GROUP BY (quadkey & 15499683151872); --   4-  -- WHERE quadkey between "03201203031012000000000" and "03201203031012333333333" -- GROUP BY (quadkey & "03201203031012330000000")
      
      







どうした



残念ながら、100万個のマーカーに基づいたクラスターのスクリーンショットは保存されませんでした(ただし、それは何かでしたが)、現在の小さなベースがあり、次のようになります。

ウェブ



Android





クラスターの座標は、マーカーがタイルの中心ではなく最も集中している場所にあります。これは、SQLクエリの座標の単純な平均化のおかげです。



結果



このクラスタリングを使用するサーバーが稼働しています。 アプローチ自体は、さらなるスケーリングに非常に便利です。

クラスター内のマーカーの数が絶えず再カウントされないように、クラスターのキャッシングが追加されます。

このトピックをさらに検討すると、マップサービスはマップをレンダリングするためにタイルを積極的に使用します。 同様の何かがゲーム開発などにも使用されています。



ソース



www.mapbox.com/guides/how-web-maps-work

msdn.microsoft.com/en-us/library/bb259689.aspx



All Articles