Add hover pins for clustered properties, disable grouping under 30 markers

- Add data-lat/data-lng attributes to MLS property cards
- Create temporary highlighted pin on card hover when marker is clustered
- Show individual pins (no grouping) when <= 30 properties in viewport
- Add markerLayer for unclustered markers to bypass client-side clustering
- Show loading spinner immediately on map move to abort image loads

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Hanson.xyz Dev
2025-12-17 02:31:23 -06:00
parent 8cd630593d
commit dfad0f57e6
4 changed files with 114 additions and 11 deletions
@@ -65,6 +65,33 @@ class MLS_Cluster {
$this->db = $db; $this->db = $db;
} }
/**
* Get state filter SQL clause
*
* @return string SQL clause or empty string
*/
private function get_state_filter() {
if (!defined('MLS_ALLOWED_STATES') || empty(MLS_ALLOWED_STATES)) {
return '';
}
global $wpdb;
$states = array_map(function($state) use ($wpdb) {
return $wpdb->prepare('%s', $state);
}, MLS_ALLOWED_STATES);
return 'state_or_province IN (' . implode(',', $states) . ')';
}
/**
* Get the TBD address exclusion filter
* Excludes properties with "TBD" as street number
*
* @return string SQL clause
*/
private function get_tbd_exclusion_filter() {
return "(street_number IS NULL OR (street_number != 'TBD' AND street_number NOT LIKE 'TBD %'))";
}
/** /**
* Encode latitude/longitude to geohash * Encode latitude/longitude to geohash
* *
@@ -225,6 +252,15 @@ class MLS_Cluster {
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL'); $where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
$values = array(); $values = array();
// Add state filter (MN, IA only)
$state_filter = $this->get_state_filter();
if ($state_filter) {
$where[] = $state_filter;
}
// Exclude TBD addresses
$where[] = $this->get_tbd_exclusion_filter();
if ($args['status']) { if ($args['status']) {
$where[] = 'standard_status = %s'; $where[] = 'standard_status = %s';
$values[] = $args['status']; $values[] = $args['status'];
@@ -276,8 +312,8 @@ class MLS_Cluster {
$total = (int) $wpdb->get_var($count_sql); $total = (int) $wpdb->get_var($count_sql);
} }
// If very few properties, always show individual markers (no grouping) // If few properties, always show individual markers (no grouping)
if ($total < self::MIN_FOR_GROUPING) { if ($total <= self::MIN_FOR_GROUPING) {
return $this->get_individual_markers($where_sql, $values, $total); return $this->get_individual_markers($where_sql, $values, $total);
} }
@@ -541,6 +577,15 @@ class MLS_Cluster {
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL'); $where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
$values = array(); $values = array();
// Add state filter (MN, IA only)
$state_filter = $this->get_state_filter();
if ($state_filter) {
$where[] = $state_filter;
}
// Exclude TBD addresses
$where[] = $this->get_tbd_exclusion_filter();
if (!empty($args['status'])) { if (!empty($args['status'])) {
$where[] = 'standard_status = %s'; $where[] = 'standard_status = %s';
$values[] = $args['status']; $values[] = $args['status'];
File diff suppressed because one or more lines are too long
@@ -65,7 +65,7 @@ $image_url = function_exists('mls_get_image_url') ? mls_get_image_url($listing_k
$has_image = !empty($image_url); $has_image = !empty($image_url);
?> ?>
<article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>" class="property-card card mls-property"> <article id="property-<?php echo esc_attr($listing_key); ?>" data-property-id="<?php echo esc_attr($listing_key); ?>"<?php if ($property->latitude && $property->longitude) : ?> data-lat="<?php echo esc_attr($property->latitude); ?>" data-lng="<?php echo esc_attr($property->longitude); ?>"<?php endif; ?> class="property-card card mls-property">
<a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a> <a href="<?php echo esc_url($property_url); ?>" class="property-card-link-overlay" aria-hidden="true" tabindex="-1"></a>
<div class="property-card-image<?php echo $has_image ? ' has-image is-loading' : ''; ?>"<?php if ($has_image) : ?> data-bg="<?php echo esc_url($image_url); ?>"<?php endif; ?>> <div class="property-card-image<?php echo $has_image ? ' has-image is-loading' : ''; ?>"<?php if ($has_image) : ?> data-bg="<?php echo esc_url($image_url); ?>"<?php endif; ?>>
<?php if ($has_image) : ?> <?php if ($has_image) : ?>
@@ -140,9 +140,11 @@
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 12-15)
markerCluster: null, // MarkerClusterGroup for individual markers (zoom 16+) markerCluster: null, // MarkerClusterGroup 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,
temporaryHoverMarker: null, // Temporary marker for clustered properties on hover
baseZIndex: 400, baseZIndex: 400,
currentFilters: {}, currentFilters: {},
currentMode: null, // Track current visualization mode currentMode: null, // Track current visualization mode
@@ -173,6 +175,9 @@
// Create layer for server-side clusters (zoom 12-15) // Create layer for server-side clusters (zoom 12-15)
this.clusterLayer = L.layerGroup().addTo(this.map); this.clusterLayer = L.layerGroup().addTo(this.map);
// Create plain layer for unclustered markers (low count)
this.markerLayer = L.layerGroup().addTo(this.map);
// Create marker cluster group for individual markers (when zoomed in) // Create marker cluster group for individual markers (when zoomed in)
this.markerCluster = L.markerClusterGroup({ this.markerCluster = L.markerClusterGroup({
maxClusterRadius: 50, maxClusterRadius: 50,
@@ -287,7 +292,14 @@
this.densityLayer.clearLayers(); this.densityLayer.clearLayers();
this.clusterLayer.clearLayers(); this.clusterLayer.clearLayers();
this.markerCluster.clearLayers(); this.markerCluster.clearLayers();
this.markerLayer.clearLayers();
this.markers = {}; this.markers = {};
// Also remove temporary hover marker if present
if (this.temporaryHoverMarker) {
this.map.removeLayer(this.temporaryHoverMarker);
this.temporaryHoverMarker = null;
}
}, },
/** /**
@@ -477,8 +489,16 @@
} }
}); });
// Bulk add markers for performance // Add markers to appropriate layer based on count
this.markerCluster.addLayers(markersToAdd); // Use plain layer for <= 30 markers (no client-side clustering)
// Use MarkerClusterGroup for > 30 markers (client-side clustering)
if (markersToAdd.length <= 30) {
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;
@@ -622,7 +642,8 @@
var self = this; var self = this;
$(document).on('mouseenter', '.property-card[data-property-id]', function() { $(document).on('mouseenter', '.property-card[data-property-id]', function() {
var propertyId = $(this).data('property-id'); var $card = $(this);
var propertyId = $card.data('property-id');
// Don't change if this is the selected (amber) card // Don't change if this is the selected (amber) card
if (propertyId === self.selectedPropertyId) { if (propertyId === self.selectedPropertyId) {
@@ -630,8 +651,33 @@
} }
self.hoveredPropertyId = propertyId; self.hoveredPropertyId = propertyId;
self.setMarkerColor(propertyId, 'blue');
self.setMarkerZIndex(propertyId, 9000); // Blue below amber but above red // Check if marker exists on map
if (self.markers[propertyId]) {
// Marker exists - highlight it
self.setMarkerColor(propertyId, 'blue');
self.setMarkerZIndex(propertyId, 9000); // Blue below amber but above red
} else {
// Marker is clustered - create temporary pin at property location
var lat = $card.data('lat');
var lng = $card.data('lng');
if (lat && lng && self.map) {
// Remove any existing temporary marker
if (self.temporaryHoverMarker) {
self.map.removeLayer(self.temporaryHoverMarker);
}
// Create temporary marker with blue color (highlighted)
self.temporaryHoverMarker = L.marker([lat, lng], {
icon: self.createIcon('blue'),
zIndexOffset: 15000 // Above everything else
});
// Add directly to map (not to cluster layer)
self.temporaryHoverMarker.addTo(self.map);
}
}
}); });
$(document).on('mouseleave', '.property-card[data-property-id]', function() { $(document).on('mouseleave', '.property-card[data-property-id]', function() {
@@ -646,8 +692,17 @@
self.hoveredPropertyId = null; self.hoveredPropertyId = null;
} }
self.setMarkerColor(propertyId, 'red'); // Remove temporary hover marker if it exists
self.resetMarkerZIndex(propertyId); if (self.temporaryHoverMarker) {
self.map.removeLayer(self.temporaryHoverMarker);
self.temporaryHoverMarker = null;
}
// Reset regular marker if it exists
if (self.markers[propertyId]) {
self.setMarkerColor(propertyId, 'red');
self.resetMarkerZIndex(propertyId);
}
}); });
}, },
@@ -919,6 +974,9 @@
this.clearPinSelection(); this.clearPinSelection();
} }
// Immediately show spinner and clear grid to abort image loading
this.$results.html('<div class="property-results-loading"><div class="spinner"></div></div>');
// Reset infinite scroll state before loading new content // Reset infinite scroll state before loading new content
if (InfiniteScrollState.isEnabled) { if (InfiniteScrollState.isEnabled) {
InfiniteScroll.reset(); InfiniteScroll.reset();