diff --git a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php index c4b87289..127d00e2 100644 --- a/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php +++ b/wp-content/plugins/mls-by-hansonxyz/includes/class-mls-cluster.php @@ -23,12 +23,25 @@ class MLS_Cluster { */ const CLUSTER_PIXEL_SPACING = 60; + /** + * Pixel spacing for density dots (smaller, more numerous) + */ + const DENSITY_DOT_SPACING = 40; + /** * Maximum properties to return as individual markers * Above this threshold, return clusters */ const MAX_INDIVIDUAL_MARKERS = 500; + /** + * Zoom thresholds for visualization modes + */ + const ZOOM_HEATMAP_MAX = 7; // 3-7: heatmap only + const ZOOM_DENSITY_MAX = 11; // 8-11: density dots + const ZOOM_CLUSTER_MAX = 15; // 12-15: numbered clusters + // 16+: individual markers + /** * Database instance */ @@ -140,14 +153,16 @@ class MLS_Cluster { * 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. + * corresponds to the target pixel spacing at the given zoom. * * @param int $zoom Map zoom level (1-20) * @param float $lat Center latitude (affects Mercator projection) + * @param int $pixel_spacing Target pixel spacing (defaults to CLUSTER_PIXEL_SPACING) * @return array [lat_step, lng_step] in degrees */ - public function get_grid_step_for_zoom($zoom, $lat = 45.0) { + public function get_grid_step_for_zoom($zoom, $lat = 45.0, $pixel_spacing = null) { $zoom = max(3, min(20, (int) $zoom)); + $pixel_spacing = $pixel_spacing ?: self::CLUSTER_PIXEL_SPACING; // Degrees per pixel at zoom level (longitude) // 360 degrees / (256 pixels * 2^zoom tiles) @@ -159,8 +174,8 @@ class MLS_Cluster { $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; + $lng_step = $pixel_spacing * $degrees_per_pixel_lng; + $lat_step = $pixel_spacing * $degrees_per_pixel_lat; return array($lat_step, $lng_step); } @@ -250,19 +265,128 @@ class MLS_Cluster { $total = (int) $wpdb->get_var($count_sql); } - // If low count or high zoom, return individual markers - 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, $args['zoom'], $center_lat, $total); + $zoom = (int) $args['zoom']; + + // Determine visualization mode based on zoom level + // Zoom 3-7: Heatmap (just return points for client-side heatmap) + if ($zoom <= self::ZOOM_HEATMAP_MAX) { + return $this->get_heatmap_data($where_sql, $values, $total); + } + + // Zoom 8-11: Density dots (small colored circles without numbers) + if ($zoom <= self::ZOOM_DENSITY_MAX) { + return $this->get_density_data($where_sql, $values, $zoom, $center_lat, $total); + } + + // Zoom 12-15: Numbered clusters (or individual if low count) + if ($zoom <= self::ZOOM_CLUSTER_MAX) { + if ($total <= self::MAX_INDIVIDUAL_MARKERS) { + return $this->get_individual_markers($where_sql, $values, $total); + } + return $this->get_cluster_data($where_sql, $values, $zoom, $center_lat, $total); + } + + // Zoom 16+: Individual markers + return $this->get_individual_markers($where_sql, $values, $total); + } + + /** + * Get heatmap data (just coordinates for client-side rendering) + * + * @param string $where_sql WHERE clause + * @param array $values Prepared values + * @param int $total Total count + * @return array + */ + private function get_heatmap_data($where_sql, $values, $total) { + global $wpdb; + + $table = $this->db->properties_table(); + + // Get sampled points for heatmap (limit to prevent overwhelming the client) + // Use grid-based sampling to get representative distribution + $sql = "SELECT latitude, longitude + FROM {$table} + WHERE {$where_sql} + LIMIT 10000"; + + if (!empty($values)) { + $results = $wpdb->get_results($wpdb->prepare($sql, $values)); + } else { + $results = $wpdb->get_results($sql); + } + + $points = array(); + foreach ($results as $row) { + $points[] = array( + (float) $row->latitude, + (float) $row->longitude, + 1.0 // intensity + ); + } + + return array( + 'type' => 'heatmap', + 'total' => $total, + 'point_count' => count($points), + 'points' => $points, + ); + } + + /** + * Get density dot data (clustered points with count for coloring) + * + * @param string $where_sql WHERE clause + * @param array $values Prepared values + * @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_density_data($where_sql, $values, $zoom, $center_lat, $total) { + global $wpdb; + + $table = $this->db->properties_table(); + + // Use smaller grid cells for density dots + list($lat_step, $lng_step) = $this->get_grid_step_for_zoom($zoom, $center_lat, self::DENSITY_DOT_SPACING); + + $sql = "SELECT + FLOOR(latitude / %f) as lat_cell, + FLOOR(longitude / %f) as lng_cell, + COUNT(*) as count, + AVG(latitude) as avg_lat, + AVG(longitude) as avg_lng + FROM {$table} + WHERE {$where_sql} + GROUP BY lat_cell, lng_cell + HAVING count >= 1"; + + $grid_values = array_merge(array($lat_step, $lng_step), $values); + $results = $wpdb->get_results($wpdb->prepare($sql, $grid_values)); + + $dots = array(); + foreach ($results as $row) { + $dots[] = array( + 'lat' => (float) $row->avg_lat, + 'lng' => (float) $row->avg_lng, + 'count' => (int) $row->count, + ); + } + + return array( + 'type' => 'density', + 'total' => $total, + 'dot_count' => count($dots), + 'zoom' => $zoom, + 'dots' => $dots, + ); } /** diff --git a/wp-content/themes/homeproz/archive-property.php b/wp-content/themes/homeproz/archive-property.php index 19c33ed3..578fe4fd 100755 --- a/wp-content/themes/homeproz/archive-property.php +++ b/wp-content/themes/homeproz/archive-property.php @@ -131,6 +131,8 @@ if (function_exists('mls_get_property_count')) { + +