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:
Hanson.xyz Dev
2025-12-16 01:27:44 -06:00
parent 9337a3cbc7
commit acd606bb03
6 changed files with 151 additions and 37 deletions
@@ -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)) {