Fix clustering density with pixel-based grid calculation
Replace geohash precision mapping with dynamic grid sizing based on target pixel spacing (60px between cluster centers). Uses Leaflet/OSM tile math to calculate degrees-per-pixel at each zoom level, adjusted for Mercator projection at the viewport's center latitude. At zoom 7, this gives ~52km cells and ~150 clusters statewide, properly separating Minneapolis from St. Cloud. Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -18,29 +18,10 @@ class MLS_Cluster {
|
||||
const GEOHASH_CHARS = '0123456789bcdefghjkmnpqrstuvwxyz';
|
||||
|
||||
/**
|
||||
* Zoom level to geohash precision mapping
|
||||
* Lower precision = larger geographic area = more clustering
|
||||
* Target pixel spacing between cluster centers
|
||||
* With 30px cluster icons, 45px gap gives 75px total spacing
|
||||
*/
|
||||
const ZOOM_PRECISION = array(
|
||||
3 => 2, // ~630km cells - continent level
|
||||
4 => 2,
|
||||
5 => 2,
|
||||
6 => 3, // ~78km cells - state level
|
||||
7 => 3,
|
||||
8 => 3,
|
||||
9 => 4, // ~20km cells - county level
|
||||
10 => 4,
|
||||
11 => 4,
|
||||
12 => 5, // ~2.4km cells - city level
|
||||
13 => 5,
|
||||
14 => 5,
|
||||
15 => 6, // ~610m cells - neighborhood level
|
||||
16 => 6,
|
||||
17 => 6,
|
||||
18 => 7, // ~76m cells - street level
|
||||
19 => 7,
|
||||
20 => 8, // Individual points
|
||||
);
|
||||
const CLUSTER_PIXEL_SPACING = 60;
|
||||
|
||||
/**
|
||||
* Maximum properties to return as individual markers
|
||||
@@ -156,20 +137,32 @@ class MLS_Cluster {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get precision for zoom level
|
||||
* Calculate grid cell size in degrees for a given zoom level
|
||||
*
|
||||
* Uses Leaflet/OSM tile math to determine what geographic distance
|
||||
* corresponds to CLUSTER_PIXEL_SPACING pixels at the given zoom.
|
||||
*
|
||||
* @param int $zoom Map zoom level (1-20)
|
||||
* @return int Geohash precision
|
||||
* @param float $lat Center latitude (affects Mercator projection)
|
||||
* @return array [lat_step, lng_step] in degrees
|
||||
*/
|
||||
public function get_precision_for_zoom($zoom) {
|
||||
public function get_grid_step_for_zoom($zoom, $lat = 45.0) {
|
||||
$zoom = max(3, min(20, (int) $zoom));
|
||||
|
||||
if (isset(self::ZOOM_PRECISION[$zoom])) {
|
||||
return self::ZOOM_PRECISION[$zoom];
|
||||
}
|
||||
// Degrees per pixel at zoom level (longitude)
|
||||
// 360 degrees / (256 pixels * 2^zoom tiles)
|
||||
$degrees_per_pixel_lng = 360.0 / (256 * pow(2, $zoom));
|
||||
|
||||
// Default to precision 5 for unknown zoom levels
|
||||
return 5;
|
||||
// Latitude degrees per pixel (adjusted for Mercator at given latitude)
|
||||
// At the equator it's the same, at poles it's compressed
|
||||
$lat_rad = deg2rad(abs($lat));
|
||||
$degrees_per_pixel_lat = $degrees_per_pixel_lng * cos($lat_rad);
|
||||
|
||||
// Calculate step size to achieve target pixel spacing
|
||||
$lng_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lng;
|
||||
$lat_step = self::CLUSTER_PIXEL_SPACING * $degrees_per_pixel_lat;
|
||||
|
||||
return array($lat_step, $lng_step);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -258,14 +251,18 @@ class MLS_Cluster {
|
||||
}
|
||||
|
||||
// If low count or high zoom, return individual markers
|
||||
$precision = $this->get_precision_for_zoom($args['zoom']);
|
||||
|
||||
if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 18) {
|
||||
if ($total <= self::MAX_INDIVIDUAL_MARKERS || $args['zoom'] >= 16) {
|
||||
return $this->get_individual_markers($where_sql, $values, $total);
|
||||
}
|
||||
|
||||
// Calculate center latitude for Mercator adjustment
|
||||
$center_lat = 45.0; // Default Minnesota
|
||||
if ($args['bounds'] && count($args['bounds']) === 4) {
|
||||
$center_lat = ($args['bounds'][0] + $args['bounds'][2]) / 2;
|
||||
}
|
||||
|
||||
// Return clusters
|
||||
return $this->get_cluster_data($where_sql, $values, $precision, $total);
|
||||
return $this->get_cluster_data($where_sql, $values, $args['zoom'], $center_lat, $total);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -334,27 +331,22 @@ class MLS_Cluster {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get clustered data using geohash grouping
|
||||
* Get clustered data using grid-based grouping
|
||||
*
|
||||
* @param string $where_sql WHERE clause
|
||||
* @param array $values Prepared values
|
||||
* @param int $precision Geohash precision
|
||||
* @param int $zoom Map zoom level
|
||||
* @param float $center_lat Center latitude for Mercator adjustment
|
||||
* @param int $total Total count
|
||||
* @return array
|
||||
*/
|
||||
private function get_cluster_data($where_sql, $values, $precision, $total) {
|
||||
private function get_cluster_data($where_sql, $values, $zoom, $center_lat, $total) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
// Use MySQL SUBSTR to extract geohash prefix for grouping
|
||||
// We compute geohash on-the-fly using latitude/longitude
|
||||
// For better performance with 30k+ records, we use grid-based clustering
|
||||
|
||||
// Calculate grid cell size based on precision
|
||||
// Approximate degrees per geohash precision level
|
||||
$lat_step = 180 / pow(2, (int)($precision * 2.5));
|
||||
$lng_step = 360 / pow(2, (int)($precision * 2.5));
|
||||
// Calculate grid cell size based on zoom level and target pixel spacing
|
||||
list($lat_step, $lng_step) = $this->get_grid_step_for_zoom($zoom, $center_lat);
|
||||
|
||||
// Use FLOOR to group coordinates into cells
|
||||
$sql = "SELECT
|
||||
@@ -388,7 +380,8 @@ class MLS_Cluster {
|
||||
'type' => 'clusters',
|
||||
'total' => $total,
|
||||
'cluster_count' => count($clusters),
|
||||
'precision' => $precision,
|
||||
'zoom' => $zoom,
|
||||
'grid_size_deg' => array('lat' => $lat_step, 'lng' => $lng_step),
|
||||
'clusters' => $clusters,
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user