Sync property list with map viewport
Major changes: - Property list now updates when map pans/zooms - Properties sorted by distance from map center (closest first) - Shows "X properties in view" when viewport filtering active - Min 30 properties required before grouping kicks in - Added rule to CLAUDE.md: no commits unless asked Backend: - MLS_Query: Added bounds filtering and distance-based sorting - AJAX handler: Accepts bounds/center, sorts by distance when provided Frontend: - Map move triggers property list refresh with same viewport - Loop prevention flag to avoid map->filter->map recursion - Resets to page 1 when viewport changes Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,12 @@ class MLS_Cluster {
|
||||
*/
|
||||
const MAX_INDIVIDUAL_MARKERS = 500;
|
||||
|
||||
/**
|
||||
* Minimum properties before any grouping kicks in
|
||||
* Below this, always show individual markers
|
||||
*/
|
||||
const MIN_FOR_GROUPING = 30;
|
||||
|
||||
/**
|
||||
* Zoom thresholds for visualization modes
|
||||
*/
|
||||
@@ -270,6 +276,11 @@ 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) {
|
||||
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) {
|
||||
|
||||
@@ -50,6 +50,8 @@ class MLS_Query {
|
||||
'listing_key' => null,
|
||||
'listing_id' => null,
|
||||
'search' => null, // Search in address/remarks
|
||||
'bounds' => null, // Map bounds: array(sw_lat, sw_lng, ne_lat, ne_lng)
|
||||
'center' => null, // Map center for distance sort: array(lat, lng)
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'orderby' => 'modification_timestamp',
|
||||
@@ -166,24 +168,51 @@ class MLS_Query {
|
||||
$values[] = $search_term;
|
||||
}
|
||||
|
||||
// Map bounds filtering
|
||||
if ($args['bounds'] && is_array($args['bounds']) && count($args['bounds']) === 4) {
|
||||
list($sw_lat, $sw_lng, $ne_lat, $ne_lng) = $args['bounds'];
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$where[] = 'latitude IS NOT NULL';
|
||||
$where[] = 'longitude IS NOT NULL';
|
||||
$values[] = (float) $sw_lat;
|
||||
$values[] = (float) $ne_lat;
|
||||
$values[] = (float) $sw_lng;
|
||||
$values[] = (float) $ne_lng;
|
||||
}
|
||||
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
|
||||
// ORDER BY
|
||||
$allowed_orderby = array(
|
||||
'modification_timestamp',
|
||||
'list_price',
|
||||
'bedrooms_total',
|
||||
'bathrooms_total',
|
||||
'living_area',
|
||||
'year_built',
|
||||
'days_on_market',
|
||||
'city',
|
||||
'created_at',
|
||||
);
|
||||
// If center provided, sort by distance from center
|
||||
if ($args['center'] && is_array($args['center']) && count($args['center']) === 2) {
|
||||
list($center_lat, $center_lng) = $args['center'];
|
||||
// Haversine formula approximation for distance (good enough for sorting)
|
||||
// Using squared Euclidean distance with latitude adjustment for speed
|
||||
$lat_factor = cos(deg2rad((float) $center_lat));
|
||||
$sql .= $wpdb->prepare(
|
||||
" ORDER BY (POW(latitude - %f, 2) + POW((longitude - %f) * %f, 2)) ASC",
|
||||
(float) $center_lat,
|
||||
(float) $center_lng,
|
||||
$lat_factor
|
||||
);
|
||||
} else {
|
||||
$allowed_orderby = array(
|
||||
'modification_timestamp',
|
||||
'list_price',
|
||||
'bedrooms_total',
|
||||
'bathrooms_total',
|
||||
'living_area',
|
||||
'year_built',
|
||||
'days_on_market',
|
||||
'city',
|
||||
'created_at',
|
||||
);
|
||||
|
||||
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'modification_timestamp';
|
||||
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
|
||||
$sql .= " ORDER BY {$orderby} {$order}";
|
||||
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'modification_timestamp';
|
||||
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
|
||||
$sql .= " ORDER BY {$orderby} {$order}";
|
||||
}
|
||||
|
||||
// LIMIT/OFFSET
|
||||
$sql .= ' LIMIT %d OFFSET %d';
|
||||
@@ -378,6 +407,19 @@ class MLS_Query {
|
||||
$values[] = (int) $args['min_baths'];
|
||||
}
|
||||
|
||||
// Map bounds filtering
|
||||
if (!empty($args['bounds']) && is_array($args['bounds']) && count($args['bounds']) === 4) {
|
||||
list($sw_lat, $sw_lng, $ne_lat, $ne_lng) = $args['bounds'];
|
||||
$where[] = 'latitude BETWEEN %f AND %f';
|
||||
$where[] = 'longitude BETWEEN %f AND %f';
|
||||
$where[] = 'latitude IS NOT NULL';
|
||||
$where[] = 'longitude IS NOT NULL';
|
||||
$values[] = (float) $sw_lat;
|
||||
$values[] = (float) $ne_lat;
|
||||
$values[] = (float) $sw_lng;
|
||||
$values[] = (float) $ne_lng;
|
||||
}
|
||||
|
||||
$sql = "SELECT COUNT(*) FROM {$table} WHERE " . implode(' AND ', $where);
|
||||
|
||||
if (!empty($values)) {
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -34,6 +34,19 @@ function homeproz_ajax_filter_properties() {
|
||||
$sort = isset($_POST['sort']) ? sanitize_text_field($_POST['sort']) : 'newest';
|
||||
$paged = isset($_POST['paged']) ? intval($_POST['paged']) : 1;
|
||||
|
||||
// Map bounds and center (for map-synced list view)
|
||||
$bounds = null;
|
||||
$center = null;
|
||||
$has_map_bounds = false;
|
||||
|
||||
if (isset($_POST['bounds']) && is_array($_POST['bounds']) && count($_POST['bounds']) === 4) {
|
||||
$bounds = array_map('floatval', $_POST['bounds']);
|
||||
$has_map_bounds = true;
|
||||
}
|
||||
if (isset($_POST['center']) && is_array($_POST['center']) && count($_POST['center']) === 2) {
|
||||
$center = array_map('floatval', $_POST['center']);
|
||||
}
|
||||
|
||||
// Build filter args for count and properties
|
||||
$per_page = 12;
|
||||
$filter_args = array(
|
||||
@@ -55,6 +68,9 @@ function homeproz_ajax_filter_properties() {
|
||||
if ($beds) {
|
||||
$filter_args['min_beds'] = $beds;
|
||||
}
|
||||
if ($bounds) {
|
||||
$filter_args['bounds'] = $bounds;
|
||||
}
|
||||
|
||||
// Get total count efficiently from database
|
||||
$total = mls_get_property_count($filter_args);
|
||||
@@ -64,10 +80,16 @@ function homeproz_ajax_filter_properties() {
|
||||
$mls_args = array_merge($filter_args, array(
|
||||
'limit' => $per_page,
|
||||
'offset' => ($paged - 1) * $per_page,
|
||||
'orderby' => 'modification_timestamp',
|
||||
'order' => 'DESC',
|
||||
));
|
||||
|
||||
// If we have map center, sort by distance; otherwise by date
|
||||
if ($center) {
|
||||
$mls_args['center'] = $center;
|
||||
} else {
|
||||
$mls_args['orderby'] = 'modification_timestamp';
|
||||
$mls_args['order'] = 'DESC';
|
||||
}
|
||||
|
||||
// Fetch only the properties we need for this page
|
||||
$paged_properties = mls_get_properties($mls_args);
|
||||
|
||||
@@ -80,8 +102,11 @@ function homeproz_ajax_filter_properties() {
|
||||
<?php if ($total > 0) : ?>
|
||||
Showing <strong><?php echo esc_html($total); ?></strong>
|
||||
<?php echo $total === 1 ? 'property' : 'properties'; ?>
|
||||
<?php if ($has_map_bounds) : ?>
|
||||
in view
|
||||
<?php endif; ?>
|
||||
<?php else : ?>
|
||||
No properties found
|
||||
No properties in view
|
||||
<?php endif; ?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -113,19 +113,24 @@
|
||||
|
||||
var self = this;
|
||||
var bounds = this.map.getBounds();
|
||||
var center = this.map.getCenter();
|
||||
var zoom = this.map.getZoom();
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
// Bounds array for both map clusters and property list
|
||||
var boundsArray = [
|
||||
bounds.getSouthWest().lat,
|
||||
bounds.getSouthWest().lng,
|
||||
bounds.getNorthEast().lat,
|
||||
bounds.getNorthEast().lng
|
||||
];
|
||||
var centerArray = [center.lat, center.lng];
|
||||
|
||||
var requestData = {
|
||||
action: 'mls_get_clusters',
|
||||
zoom: zoom,
|
||||
bounds: [
|
||||
bounds.getSouthWest().lat,
|
||||
bounds.getSouthWest().lng,
|
||||
bounds.getNorthEast().lat,
|
||||
bounds.getNorthEast().lng
|
||||
],
|
||||
bounds: boundsArray,
|
||||
status: this.currentFilters.status || 'Active',
|
||||
property_type: this.currentFilters.property_type || '',
|
||||
city: this.currentFilters.city || '',
|
||||
@@ -134,6 +139,9 @@
|
||||
min_beds: this.currentFilters.min_beds || ''
|
||||
};
|
||||
|
||||
// Also update the property list with the same viewport
|
||||
PropertyFilters.updateFromMap(boundsArray, centerArray);
|
||||
|
||||
$.ajax({
|
||||
url: homeprozMapData.clusterEndpoint,
|
||||
type: 'GET',
|
||||
@@ -518,6 +526,9 @@
|
||||
// State
|
||||
isFirstLoad: true,
|
||||
isLoading: false,
|
||||
mapBounds: null, // Current map viewport bounds
|
||||
mapCenter: null, // Current map center for distance sorting
|
||||
isMapUpdate: false, // Flag to prevent map->filter->map loop
|
||||
|
||||
/**
|
||||
* Initialize
|
||||
@@ -597,6 +608,18 @@
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Update property list based on map viewport
|
||||
* Called by PropertyMap when map moves/zooms
|
||||
*/
|
||||
updateFromMap: function(bounds, center) {
|
||||
this.mapBounds = bounds;
|
||||
this.mapCenter = center;
|
||||
this.isMapUpdate = true;
|
||||
// Reset to page 1 when map viewport changes
|
||||
this.filterProperties(1, false);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get page number from URL hash
|
||||
*/
|
||||
@@ -629,35 +652,47 @@
|
||||
this.$results.html('<div class="property-results-loading"><div class="spinner"></div></div>');
|
||||
}
|
||||
|
||||
// Build request data
|
||||
var requestData = {
|
||||
action: 'homeproz_filter_properties',
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_location: formData.property_location,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
paged: page
|
||||
};
|
||||
|
||||
// Add map bounds and center if available
|
||||
if (this.mapBounds) {
|
||||
requestData.bounds = this.mapBounds;
|
||||
}
|
||||
if (this.mapCenter) {
|
||||
requestData.center = this.mapCenter;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: homeprozAjax.ajaxUrl,
|
||||
type: 'POST',
|
||||
data: {
|
||||
action: 'homeproz_filter_properties',
|
||||
nonce: homeprozAjax.nonce,
|
||||
property_type: formData.property_type,
|
||||
property_location: formData.property_location,
|
||||
min_price: formData.min_price,
|
||||
max_price: formData.max_price,
|
||||
beds: formData.beds,
|
||||
paged: page
|
||||
},
|
||||
data: requestData,
|
||||
success: function(response) {
|
||||
if (response.success) {
|
||||
self.$results.html(response.data.html);
|
||||
self.isFirstLoad = false;
|
||||
|
||||
// Update map with new filter params
|
||||
if (response.data.filters) {
|
||||
// Update map with new filter params (but not if this was triggered by map move)
|
||||
if (response.data.filters && !self.isMapUpdate) {
|
||||
PropertyMap.updateFilters(response.data.filters);
|
||||
}
|
||||
self.isMapUpdate = false;
|
||||
|
||||
// Recalculate layout after content update
|
||||
if (typeof LayoutCalculator !== 'undefined') {
|
||||
LayoutCalculator.calculate();
|
||||
}
|
||||
|
||||
// Update URL
|
||||
// Update URL (skip when map-triggered)
|
||||
if (updateHistory) {
|
||||
self.updateUrl(formData, page);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user