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:
@@ -65,6 +65,33 @@ class MLS_Cluster {
|
||||
$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
|
||||
*
|
||||
@@ -225,6 +252,15 @@ class MLS_Cluster {
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
|
||||
$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']) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
@@ -276,8 +312,8 @@ class MLS_Cluster {
|
||||
$total = (int) $wpdb->get_var($count_sql);
|
||||
}
|
||||
|
||||
// If very few properties, always show individual markers (no grouping)
|
||||
if ($total < self::MIN_FOR_GROUPING) {
|
||||
// If few properties, always show individual markers (no grouping)
|
||||
if ($total <= self::MIN_FOR_GROUPING) {
|
||||
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');
|
||||
$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'])) {
|
||||
$where[] = 'standard_status = %s';
|
||||
$values[] = $args['status'];
|
||||
|
||||
+1
-1
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);
|
||||
?>
|
||||
|
||||
<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>
|
||||
<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) : ?>
|
||||
|
||||
@@ -140,9 +140,11 @@
|
||||
densityLayer: null, // Layer group for density dots (zoom 1-11)
|
||||
clusterLayer: null, // Layer group for server clusters (zoom 12-15)
|
||||
markerCluster: null, // MarkerClusterGroup for individual markers (zoom 16+)
|
||||
markerLayer: null, // Plain layer group for unclustered markers (low count)
|
||||
selectedPropertyId: null,
|
||||
isPinClickPan: false, // Flag: true when map pan is caused by pin click (don't clear selection)
|
||||
hoveredPropertyId: null,
|
||||
temporaryHoverMarker: null, // Temporary marker for clustered properties on hover
|
||||
baseZIndex: 400,
|
||||
currentFilters: {},
|
||||
currentMode: null, // Track current visualization mode
|
||||
@@ -173,6 +175,9 @@
|
||||
// Create layer for server-side clusters (zoom 12-15)
|
||||
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)
|
||||
this.markerCluster = L.markerClusterGroup({
|
||||
maxClusterRadius: 50,
|
||||
@@ -287,7 +292,14 @@
|
||||
this.densityLayer.clearLayers();
|
||||
this.clusterLayer.clearLayers();
|
||||
this.markerCluster.clearLayers();
|
||||
this.markerLayer.clearLayers();
|
||||
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
|
||||
this.markerCluster.addLayers(markersToAdd);
|
||||
// Add markers to appropriate layer based on count
|
||||
// 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
|
||||
this.isPinClickPan = false;
|
||||
@@ -622,7 +642,8 @@
|
||||
var self = this;
|
||||
|
||||
$(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
|
||||
if (propertyId === self.selectedPropertyId) {
|
||||
@@ -630,8 +651,33 @@
|
||||
}
|
||||
|
||||
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() {
|
||||
@@ -646,8 +692,17 @@
|
||||
self.hoveredPropertyId = null;
|
||||
}
|
||||
|
||||
self.setMarkerColor(propertyId, 'red');
|
||||
self.resetMarkerZIndex(propertyId);
|
||||
// Remove temporary hover marker if it exists
|
||||
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();
|
||||
}
|
||||
|
||||
// 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
|
||||
if (InfiniteScrollState.isEnabled) {
|
||||
InfiniteScroll.reset();
|
||||
|
||||
Reference in New Issue
Block a user