Switch to 100% server-side map clustering

Removes client-side Leaflet MarkerCluster library in favor of
server-side clustering at all zoom levels:
- Zoom 1-8: Density dots
- Zoom 9-15: Server-generated numbered clusters
- Zoom 16+: Individual property markers

This prevents the visual issue where server-returned clusters were
being re-clustered by the client into a single merged marker.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-29 03:16:16 -06:00
parent f2a9b28ac2
commit eed01f2e04
4 changed files with 19 additions and 68 deletions
+2 -5
View File
@@ -336,15 +336,12 @@ class MLS_Cluster {
return $this->get_density_data($where_sql, $values, $zoom, $center_lat, $total, self::DENSITY_DOT_SPACING); return $this->get_density_data($where_sql, $values, $zoom, $center_lat, $total, self::DENSITY_DOT_SPACING);
} }
// Zoom 12-15: Numbered clusters (or individual if low count) // Zoom 9-15: Always use server-side clusters (let server handle grouping)
if ($zoom <= self::ZOOM_CLUSTER_MAX) { 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); return $this->get_cluster_data($where_sql, $values, $zoom, $center_lat, $total);
} }
// Zoom 16+: Individual markers // Zoom 16+: Individual markers (no clustering needed at this zoom)
return $this->get_individual_markers($where_sql, $values, $total); return $this->get_individual_markers($where_sql, $values, $total);
} }
@@ -120,7 +120,7 @@ $view_class = $show_map ? 'is-map-view' : 'is-grid-view';
$initial_filters = array( $initial_filters = array(
'status' => isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : 'Active', 'status' => isset($_GET['property_status']) ? sanitize_text_field($_GET['property_status']) : 'Active',
'property_type' => isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '', 'property_type' => isset($_GET['property_type']) ? sanitize_text_field($_GET['property_type']) : '',
'city' => isset($_GET['property_location']) ? sanitize_text_field($_GET['property_location']) : '', 'city' => isset($_GET['city']) ? sanitize_text_field($_GET['city']) : '',
'min_price' => isset($_GET['min_price']) ? intval($_GET['min_price']) : '', 'min_price' => isset($_GET['min_price']) ? intval($_GET['min_price']) : '',
'max_price' => isset($_GET['max_price']) ? intval($_GET['max_price']) : '', 'max_price' => isset($_GET['max_price']) ? intval($_GET['max_price']) : '',
'min_beds' => isset($_GET['beds']) ? intval($_GET['beds']) : '', 'min_beds' => isset($_GET['beds']) ? intval($_GET['beds']) : '',
@@ -137,13 +137,8 @@ if (function_exists('mls_get_property_count')) {
?> ?>
<!-- Leaflet CSS --> <!-- Leaflet CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/> <link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- Leaflet MarkerCluster CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.css" crossorigin=""/>
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.4.1/dist/MarkerCluster.Default.css" crossorigin=""/>
<!-- Leaflet JS --> <!-- Leaflet JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Leaflet MarkerCluster JS -->
<script src="https://unpkg.com/leaflet.markercluster@1.4.1/dist/leaflet.markercluster.js" crossorigin=""></script>
<script> <script>
var homeprozMapData = { var homeprozMapData = {
isMapView: <?php echo $show_map ? 'true' : 'false'; ?>, isMapView: <?php echo $show_map ? 'true' : 'false'; ?>,
File diff suppressed because one or more lines are too long
@@ -271,9 +271,8 @@
markers: {}, // Object keyed by property ID markers: {}, // Object keyed by property ID
markerData: {}, // Object keyed by property ID -> {lat, lng, ...} markerData: {}, // Object keyed by property ID -> {lat, lng, ...}
densityLayer: null, // Layer group for density dots (zoom 1-11) densityLayer: null, // Layer group for density dots (zoom 1-11)
clusterLayer: null, // Layer group for server clusters (zoom 12-15) clusterLayer: null, // Layer group for server clusters (zoom 9-15)
markerCluster: null, // MarkerClusterGroup for individual markers (zoom 16+) markerLayer: null, // Plain layer group for individual markers (zoom 16+)
markerLayer: null, // Plain layer group for unclustered markers (low count)
selectedPropertyId: null, selectedPropertyId: null,
isPinClickPan: false, // Flag: true when map pan is caused by pin click (don't clear selection) isPinClickPan: false, // Flag: true when map pan is caused by pin click (don't clear selection)
hoveredPropertyId: null, hoveredPropertyId: null,
@@ -304,42 +303,15 @@
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>' attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>'
}).addTo(this.map); }).addTo(this.map);
// Create layer for density dots (zoom 1-11) // Create layer for density dots (zoom 1-8)
this.densityLayer = L.layerGroup().addTo(this.map); this.densityLayer = L.layerGroup().addTo(this.map);
// Create layer for server-side clusters (zoom 12-15) // Create layer for server-side clusters (zoom 9-15)
this.clusterLayer = L.layerGroup().addTo(this.map); this.clusterLayer = L.layerGroup().addTo(this.map);
// Create plain layer for unclustered markers (low count) // Create plain layer for individual markers (zoom 16+ or low count)
this.markerLayer = L.layerGroup().addTo(this.map); this.markerLayer = L.layerGroup().addTo(this.map);
// Create marker cluster group for individual markers (when zoomed in)
this.markerCluster = L.markerClusterGroup({
maxClusterRadius: 50,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
zoomToBoundsOnClick: true,
disableClusteringAtZoom: 18,
chunkedLoading: true,
chunkInterval: 200,
chunkDelay: 50,
iconCreateFunction: function(cluster) {
var count = cluster.getChildCount();
var size = 'small';
if (count >= 100) {
size = 'large';
} else if (count >= 10) {
size = 'medium';
}
return L.divIcon({
html: '<div><span>' + count + '</span></div>',
className: 'marker-cluster marker-cluster-' + size,
iconSize: L.point(40, 40)
});
}
});
this.map.addLayer(this.markerCluster);
// Bind map events for dynamic loading // Bind map events for dynamic loading
var self = this; var self = this;
this.map.on('moveend zoomend', function() { this.map.on('moveend zoomend', function() {
@@ -455,7 +427,6 @@
clearAllLayers: function() { clearAllLayers: function() {
this.densityLayer.clearLayers(); this.densityLayer.clearLayers();
this.clusterLayer.clearLayers(); this.clusterLayer.clearLayers();
this.markerCluster.clearLayers();
this.markerLayer.clearLayers(); this.markerLayer.clearLayers();
this.markers = {}; this.markers = {};
@@ -653,16 +624,11 @@
} }
}); });
// Add markers to appropriate layer based on count // Add all markers to plain layer (server handles clustering)
// Use plain layer for <= 30 markers (no client-side clustering) // Individual markers only come at zoom 16+ where no clustering is needed
// Use MarkerClusterGroup for > 30 markers (client-side clustering) markersToAdd.forEach(function(marker) {
if (markersToAdd.length <= 30) { self.markerLayer.addLayer(marker);
markersToAdd.forEach(function(marker) { });
self.markerLayer.addLayer(marker);
});
} else {
this.markerCluster.addLayers(markersToAdd);
}
// Reset the pin-click-pan flag now that markers are rendered // Reset the pin-click-pan flag now that markers are rendered
this.isPinClickPan = false; this.isPinClickPan = false;
@@ -844,27 +810,20 @@
var needsTemporaryMarker = false; var needsTemporaryMarker = false;
if (marker) { if (marker) {
// Marker exists - check if it's visible (not clustered and in viewport) // Marker exists - check if it's in viewport
var markerLatLng = marker.getLatLng(); var markerLatLng = marker.getLatLng();
var inViewport = self.map.getBounds().contains(markerLatLng); var inViewport = self.map.getBounds().contains(markerLatLng);
// Check if marker is clustered (part of a cluster group) if (inViewport) {
var isClustered = false; // Marker is visible - highlight it
if (self.markerCluster && self.markerCluster.hasLayer(marker)) {
var visibleParent = self.markerCluster.getVisibleParent(marker);
isClustered = visibleParent && visibleParent !== marker;
}
if (!isClustered && inViewport) {
// Marker is visible as individual pin - highlight it
self.setMarkerColor(propertyId, 'blue'); self.setMarkerColor(propertyId, 'blue');
self.setMarkerZIndex(propertyId, 9000); self.setMarkerZIndex(propertyId, 9000);
} else { } else {
// Marker is clustered or outside viewport - need temporary marker // Marker outside viewport - need temporary marker
needsTemporaryMarker = true; needsTemporaryMarker = true;
} }
} else { } else {
// Marker doesn't exist in current dataset - need temporary marker // Marker doesn't exist in current dataset (server-side clustered or not loaded)
needsTemporaryMarker = true; needsTemporaryMarker = true;
} }