wip
This commit is contained in:
Regular → Executable
Regular → Executable
Regular → Executable
+151
@@ -39,6 +39,7 @@ class MLS_CLI {
|
||||
WP_CLI::add_command('mls cache', array($instance, 'cache'));
|
||||
WP_CLI::add_command('mls recovery', array($instance, 'recovery'));
|
||||
WP_CLI::add_command('mls media', array($instance, 'media'));
|
||||
WP_CLI::add_command('mls geo', array($instance, 'geo'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1078,4 +1079,154 @@ class MLS_CLI {
|
||||
|
||||
rmdir($dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* Geographic coordinate validation commands.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <action>
|
||||
* : Action to perform: validate, stats, or show-invalid
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* # Validate all property coordinates
|
||||
* wp mls geo validate
|
||||
*
|
||||
* # Show validation statistics
|
||||
* wp mls geo stats
|
||||
*
|
||||
* # Show properties with invalid coordinates
|
||||
* wp mls geo show-invalid
|
||||
*
|
||||
* @param array $args Positional arguments
|
||||
* @param array $assoc_args Associative arguments
|
||||
*/
|
||||
public function geo($args, $assoc_args) {
|
||||
if (empty($args[0])) {
|
||||
WP_CLI::error('Please specify an action: validate, stats, or show-invalid');
|
||||
return;
|
||||
}
|
||||
|
||||
$action = $args[0];
|
||||
|
||||
switch ($action) {
|
||||
case 'validate':
|
||||
$this->geo_validate();
|
||||
break;
|
||||
|
||||
case 'stats':
|
||||
$this->geo_stats();
|
||||
break;
|
||||
|
||||
case 'show-invalid':
|
||||
$this->geo_show_invalid();
|
||||
break;
|
||||
|
||||
default:
|
||||
WP_CLI::error("Unknown action: {$action}. Use validate, stats, or show-invalid.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all property coordinates
|
||||
*/
|
||||
private function geo_validate() {
|
||||
WP_CLI::log('Validating property coordinates against state boundaries...');
|
||||
WP_CLI::log('');
|
||||
|
||||
$progress = null;
|
||||
$progress_callback = function($processed, $total, $valid, $invalid) use (&$progress) {
|
||||
if ($progress === null) {
|
||||
$progress = WP_CLI\Utils\make_progress_bar('Validating', $total);
|
||||
}
|
||||
$progress->tick();
|
||||
};
|
||||
|
||||
$results = MLS_Geo_Validator::validate_all_properties($progress_callback);
|
||||
|
||||
if ($progress) {
|
||||
$progress->finish();
|
||||
}
|
||||
|
||||
WP_CLI::log('');
|
||||
WP_CLI::success("Validation complete!");
|
||||
WP_CLI::log(" Total properties: " . number_format($results['total']));
|
||||
WP_CLI::log(" Valid coordinates: " . number_format($results['valid']));
|
||||
WP_CLI::log(" Invalid coordinates: " . number_format($results['invalid']));
|
||||
|
||||
if ($results['invalid'] > 0) {
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log("Properties with invalid coordinates will be excluded from map views.");
|
||||
WP_CLI::log("Run 'wp mls geo show-invalid' to see details.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show coordinate validation statistics
|
||||
*/
|
||||
private function geo_stats() {
|
||||
$stats = MLS_Geo_Validator::get_stats();
|
||||
|
||||
WP_CLI::log('Coordinate Validation Statistics:');
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log(" Total properties: " . number_format($stats['total']));
|
||||
WP_CLI::log(" Valid coordinates: " . number_format($stats['valid']));
|
||||
WP_CLI::log(" Invalid coordinates: " . number_format($stats['invalid']));
|
||||
WP_CLI::log(" Null coordinates: " . number_format($stats['null_coords']));
|
||||
|
||||
if ($stats['total'] > 0) {
|
||||
$pct_valid = round(($stats['valid'] / $stats['total']) * 100, 1);
|
||||
$pct_invalid = round(($stats['invalid'] / $stats['total']) * 100, 1);
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log(" Valid %: {$pct_valid}%");
|
||||
WP_CLI::log(" Invalid %: {$pct_invalid}%");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show properties with invalid coordinates
|
||||
*/
|
||||
private function geo_show_invalid() {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->plugin->get_db()->properties_table();
|
||||
|
||||
$invalid = $wpdb->get_results(
|
||||
"SELECT listing_id, listing_key, street_number, street_name, city, state_or_province, latitude, longitude
|
||||
FROM {$table}
|
||||
WHERE coordinates_invalid = 1
|
||||
ORDER BY city, street_name
|
||||
LIMIT 100"
|
||||
);
|
||||
|
||||
if (empty($invalid)) {
|
||||
WP_CLI::success("No properties with invalid coordinates found.");
|
||||
return;
|
||||
}
|
||||
|
||||
WP_CLI::log("Properties with invalid coordinates (showing up to 100):");
|
||||
WP_CLI::log('');
|
||||
|
||||
$items = array();
|
||||
foreach ($invalid as $row) {
|
||||
$address = trim($row->street_number . ' ' . $row->street_name);
|
||||
$items[] = array(
|
||||
'MLS ID' => $row->listing_id,
|
||||
'Address' => $address ?: '(no address)',
|
||||
'City' => $row->city,
|
||||
'State' => $row->state_or_province,
|
||||
'Lat' => $row->latitude,
|
||||
'Lng' => $row->longitude,
|
||||
);
|
||||
}
|
||||
|
||||
WP_CLI\Utils\format_items('table', $items, array('MLS ID', 'Address', 'City', 'State', 'Lat', 'Lng'));
|
||||
|
||||
$total = $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 1");
|
||||
if ($total > 100) {
|
||||
WP_CLI::log('');
|
||||
WP_CLI::log("... and " . ($total - 100) . " more.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
/**
|
||||
* US State Bounding Boxes
|
||||
*
|
||||
* Approximate rectangular bounds for each US state.
|
||||
* Used to validate that property coordinates fall within their claimed state.
|
||||
*
|
||||
* Format: state_code => [min_lat, max_lat, min_lng, max_lng]
|
||||
*
|
||||
* Data sourced from US Census Bureau state boundaries.
|
||||
* Bounds include a small buffer for edge cases.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
return array(
|
||||
'AL' => array(30.14, 35.01, -88.47, -84.89), // Alabama
|
||||
'AK' => array(51.21, 71.39, -179.15, 179.77), // Alaska (crosses date line)
|
||||
'AZ' => array(31.33, 37.00, -114.82, -109.04), // Arizona
|
||||
'AR' => array(33.00, 36.50, -94.62, -89.64), // Arkansas
|
||||
'CA' => array(32.53, 42.01, -124.42, -114.13), // California
|
||||
'CO' => array(36.99, 41.00, -109.06, -102.04), // Colorado
|
||||
'CT' => array(40.95, 42.05, -73.73, -71.79), // Connecticut
|
||||
'DE' => array(38.45, 39.84, -75.79, -75.05), // Delaware
|
||||
'FL' => array(24.40, 31.00, -87.63, -80.03), // Florida
|
||||
'GA' => array(30.36, 35.00, -85.61, -80.84), // Georgia
|
||||
'HI' => array(18.91, 22.24, -160.25, -154.81), // Hawaii
|
||||
'ID' => array(41.99, 49.00, -117.24, -111.04), // Idaho
|
||||
'IL' => array(36.97, 42.51, -91.51, -87.02), // Illinois
|
||||
'IN' => array(37.77, 41.76, -88.10, -84.78), // Indiana
|
||||
'IA' => array(40.37, 43.50, -96.64, -90.14), // Iowa
|
||||
'KS' => array(36.99, 40.00, -102.05, -94.59), // Kansas
|
||||
'KY' => array(36.50, 39.15, -89.57, -81.96), // Kentucky
|
||||
'LA' => array(28.93, 33.02, -94.04, -88.82), // Louisiana
|
||||
'ME' => array(43.06, 47.46, -71.08, -66.95), // Maine
|
||||
'MD' => array(37.91, 39.72, -79.49, -75.05), // Maryland
|
||||
'MA' => array(41.24, 42.89, -73.51, -69.93), // Massachusetts
|
||||
'MI' => array(41.70, 48.19, -90.42, -82.41), // Michigan
|
||||
'MN' => array(43.50, 49.38, -97.24, -89.49), // Minnesota
|
||||
'MS' => array(30.17, 35.00, -91.66, -88.10), // Mississippi
|
||||
'MO' => array(35.99, 40.61, -95.77, -89.10), // Missouri
|
||||
'MT' => array(44.36, 49.00, -116.05, -104.04), // Montana
|
||||
'NE' => array(40.00, 43.00, -104.05, -95.31), // Nebraska
|
||||
'NV' => array(35.00, 42.00, -120.01, -114.04), // Nevada
|
||||
'NH' => array(42.70, 45.31, -72.56, -70.70), // New Hampshire
|
||||
'NJ' => array(38.93, 41.36, -75.56, -73.89), // New Jersey
|
||||
'NM' => array(31.33, 37.00, -109.05, -103.00), // New Mexico
|
||||
'NY' => array(40.50, 45.02, -79.76, -71.86), // New York
|
||||
'NC' => array(33.84, 36.59, -84.32, -75.46), // North Carolina
|
||||
'ND' => array(45.94, 49.00, -104.05, -96.55), // North Dakota
|
||||
'OH' => array(38.40, 42.33, -84.82, -80.52), // Ohio
|
||||
'OK' => array(33.62, 37.00, -103.00, -94.43), // Oklahoma
|
||||
'OR' => array(41.99, 46.29, -124.57, -116.46), // Oregon
|
||||
'PA' => array(39.72, 42.27, -80.52, -74.69), // Pennsylvania
|
||||
'RI' => array(41.15, 42.02, -71.86, -71.12), // Rhode Island
|
||||
'SC' => array(32.03, 35.22, -83.35, -78.54), // South Carolina
|
||||
'SD' => array(42.48, 45.95, -104.06, -96.44), // South Dakota
|
||||
'TN' => array(34.98, 36.68, -90.31, -81.65), // Tennessee
|
||||
'TX' => array(25.84, 36.50, -106.65, -93.51), // Texas
|
||||
'UT' => array(36.99, 42.00, -114.05, -109.04), // Utah
|
||||
'VT' => array(42.73, 45.02, -73.44, -71.46), // Vermont
|
||||
'VA' => array(36.54, 39.47, -83.68, -75.24), // Virginia
|
||||
'WA' => array(45.54, 49.00, -124.76, -116.92), // Washington
|
||||
'WV' => array(37.20, 40.64, -82.64, -77.72), // West Virginia
|
||||
'WI' => array(42.49, 47.08, -92.89, -86.25), // Wisconsin
|
||||
'WY' => array(40.99, 45.01, -111.06, -104.05), // Wyoming
|
||||
'DC' => array(38.79, 38.99, -77.12, -76.91), // District of Columbia
|
||||
'PR' => array(17.88, 18.52, -67.95, -65.22), // Puerto Rico
|
||||
'VI' => array(17.62, 18.42, -65.08, -64.56), // US Virgin Islands
|
||||
'GU' => array(13.23, 13.65, 144.62, 144.96), // Guam
|
||||
);
|
||||
Regular → Executable
|
Can't render this file because it is too large.
|
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -249,7 +249,8 @@ class MLS_Cluster {
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
// Build WHERE clause
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
|
||||
// Exclude properties with invalid coordinates from map display
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN, IA only)
|
||||
@@ -291,7 +292,7 @@ class MLS_Cluster {
|
||||
$values[] = (int) $args['min_beds'];
|
||||
}
|
||||
|
||||
// Add bounds filter if provided
|
||||
// Add bounds filter (BETWEEN is efficient for rectangular viewport queries)
|
||||
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';
|
||||
@@ -571,7 +572,8 @@ class MLS_Cluster {
|
||||
global $wpdb;
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
|
||||
// Exclude properties with invalid coordinates
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN, IA only)
|
||||
|
||||
Regular → Executable
+60
-2
@@ -13,7 +13,7 @@ class MLS_DB {
|
||||
* Schema version for index migrations
|
||||
* Increment this when adding new indexes or columns
|
||||
*/
|
||||
const SCHEMA_VERSION = 3;
|
||||
const SCHEMA_VERSION = 5;
|
||||
|
||||
/**
|
||||
* Get table name with prefix
|
||||
@@ -415,8 +415,66 @@ class MLS_DB {
|
||||
update_option('mls_schema_version', 3);
|
||||
}
|
||||
|
||||
// Migration to schema version 4: Add spatial POINT column and index
|
||||
if ($current_schema < 4) {
|
||||
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
|
||||
|
||||
// Check if location column exists
|
||||
$column_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'location'",
|
||||
DB_NAME,
|
||||
$table_properties
|
||||
));
|
||||
|
||||
if (!$column_exists) {
|
||||
// Add POINT column (nullable initially for population)
|
||||
$wpdb->query("ALTER TABLE {$table_properties} ADD COLUMN location POINT SRID 4326 DEFAULT NULL AFTER longitude");
|
||||
|
||||
// Populate location from existing lat/lng
|
||||
// Note: SRID 4326 uses axis order (latitude, longitude) in MySQL 8.0+
|
||||
$wpdb->query("UPDATE {$table_properties} SET location = ST_PointFromText(CONCAT('POINT(', latitude, ' ', longitude, ')'), 4326) WHERE latitude IS NOT NULL AND longitude IS NOT NULL");
|
||||
|
||||
// Make column NOT NULL (required for spatial index)
|
||||
$wpdb->query("ALTER TABLE {$table_properties} MODIFY location POINT NOT NULL SRID 4326");
|
||||
|
||||
// Add spatial index
|
||||
$existing_indexes = self::get_existing_indexes($table_properties);
|
||||
if (!isset($existing_indexes['idx_location_spatial'])) {
|
||||
$wpdb->query("ALTER TABLE {$table_properties} ADD SPATIAL INDEX idx_location_spatial (location)");
|
||||
}
|
||||
}
|
||||
|
||||
update_option('mls_schema_version', 4);
|
||||
}
|
||||
|
||||
// Migration to schema version 5: Add coordinates_invalid column for geo validation
|
||||
if ($current_schema < 5) {
|
||||
$table_properties = $wpdb->prefix . MLS_TABLE_PROPERTIES;
|
||||
|
||||
// Check if column exists
|
||||
$column_exists = $wpdb->get_var($wpdb->prepare(
|
||||
"SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = %s AND TABLE_NAME = %s AND COLUMN_NAME = 'coordinates_invalid'",
|
||||
DB_NAME,
|
||||
$table_properties
|
||||
));
|
||||
|
||||
if (!$column_exists) {
|
||||
$wpdb->query("ALTER TABLE {$table_properties} ADD COLUMN coordinates_invalid TINYINT(1) NOT NULL DEFAULT 0 AFTER longitude");
|
||||
}
|
||||
|
||||
// Add index if not exists
|
||||
$existing_indexes = self::get_existing_indexes($table_properties);
|
||||
if (!isset($existing_indexes['idx_coordinates_invalid'])) {
|
||||
$wpdb->query("ALTER TABLE {$table_properties} ADD INDEX idx_coordinates_invalid (coordinates_invalid)");
|
||||
}
|
||||
|
||||
update_option('mls_schema_version', 5);
|
||||
}
|
||||
|
||||
// Future migrations go here:
|
||||
// if ($current_schema < 4) { ... }
|
||||
// if ($current_schema < 6) { ... }
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Regular → Executable
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
/**
|
||||
* Geographic Coordinate Validator
|
||||
*
|
||||
* Validates that property coordinates fall within their claimed state boundaries.
|
||||
* Uses bounding box approximations for fast validation.
|
||||
*/
|
||||
|
||||
if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class MLS_Geo_Validator {
|
||||
|
||||
/**
|
||||
* State bounding boxes
|
||||
* Format: state_code => [min_lat, max_lat, min_lng, max_lng]
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $state_bounds = null;
|
||||
|
||||
/**
|
||||
* Load state bounds data
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function get_state_bounds() {
|
||||
if (self::$state_bounds === null) {
|
||||
$bounds_file = MLS_PLUGIN_DIR . 'data/state-bounds.php';
|
||||
if (file_exists($bounds_file)) {
|
||||
self::$state_bounds = include $bounds_file;
|
||||
} else {
|
||||
self::$state_bounds = array();
|
||||
}
|
||||
}
|
||||
return self::$state_bounds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if coordinates are valid for a given state
|
||||
*
|
||||
* @param float $latitude Latitude
|
||||
* @param float $longitude Longitude
|
||||
* @param string $state_code Two-letter state code (e.g., 'MN', 'IA')
|
||||
* @return bool True if coordinates are within state bounds
|
||||
*/
|
||||
public static function validate_coordinates($latitude, $longitude, $state_code) {
|
||||
// Null or empty coordinates are invalid
|
||||
if ($latitude === null || $longitude === null || $latitude === '' || $longitude === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lat = (float) $latitude;
|
||||
$lng = (float) $longitude;
|
||||
|
||||
// Basic sanity check - valid lat/lng ranges
|
||||
if ($lat < -90 || $lat > 90 || $lng < -180 || $lng > 180) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Continental US rough bounds check (catches obviously wrong data like European coords)
|
||||
// This is a quick filter before state-specific validation
|
||||
if ($lat < 24 || $lat > 50 || $lng < -125 || $lng > -66) {
|
||||
// Allow Alaska, Hawaii, and territories which fall outside continental bounds
|
||||
if (!in_array(strtoupper($state_code), array('AK', 'HI', 'PR', 'VI', 'GU'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Get state-specific bounds
|
||||
$bounds = self::get_state_bounds();
|
||||
$state_code = strtoupper($state_code);
|
||||
|
||||
if (!isset($bounds[$state_code])) {
|
||||
// Unknown state - can't validate, assume valid
|
||||
return true;
|
||||
}
|
||||
|
||||
list($min_lat, $max_lat, $min_lng, $max_lng) = $bounds[$state_code];
|
||||
|
||||
// Check if coordinates fall within state bounding box
|
||||
// Add small buffer (0.1 degrees ~= 7 miles) for edge cases
|
||||
$buffer = 0.1;
|
||||
|
||||
return $lat >= ($min_lat - $buffer) &&
|
||||
$lat <= ($max_lat + $buffer) &&
|
||||
$lng >= ($min_lng - $buffer) &&
|
||||
$lng <= ($max_lng + $buffer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a property record and return whether coordinates are invalid
|
||||
*
|
||||
* @param object|array $property Property data with latitude, longitude, state_or_province
|
||||
* @return int 1 if coordinates are invalid, 0 if valid
|
||||
*/
|
||||
public static function check_property($property) {
|
||||
// Convert to array if object
|
||||
if (is_object($property)) {
|
||||
$lat = isset($property->latitude) ? $property->latitude : null;
|
||||
$lng = isset($property->longitude) ? $property->longitude : null;
|
||||
$state = isset($property->state_or_province) ? $property->state_or_province : null;
|
||||
} else {
|
||||
$lat = isset($property['latitude']) ? $property['latitude'] : null;
|
||||
$lng = isset($property['longitude']) ? $property['longitude'] : null;
|
||||
$state = isset($property['state_or_province']) ? $property['state_or_province'] : null;
|
||||
}
|
||||
|
||||
// If no coordinates, mark as invalid for map purposes
|
||||
if ($lat === null || $lng === null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// If no state, can't validate - assume valid
|
||||
if (empty($state)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Normalize state code (handle full names or codes)
|
||||
$state_code = self::normalize_state_code($state);
|
||||
|
||||
// Validate
|
||||
return self::validate_coordinates($lat, $lng, $state_code) ? 0 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize state name/code to two-letter code
|
||||
*
|
||||
* @param string $state State name or code
|
||||
* @return string Two-letter state code
|
||||
*/
|
||||
private static function normalize_state_code($state) {
|
||||
$state = trim($state);
|
||||
|
||||
// Already a two-letter code
|
||||
if (strlen($state) === 2) {
|
||||
return strtoupper($state);
|
||||
}
|
||||
|
||||
// Common state name to code mappings
|
||||
$name_to_code = array(
|
||||
'minnesota' => 'MN',
|
||||
'iowa' => 'IA',
|
||||
'wisconsin' => 'WI',
|
||||
'north dakota' => 'ND',
|
||||
'south dakota' => 'SD',
|
||||
'nebraska' => 'NE',
|
||||
'missouri' => 'MO',
|
||||
'illinois' => 'IL',
|
||||
// Add more as needed
|
||||
);
|
||||
|
||||
$lower = strtolower($state);
|
||||
if (isset($name_to_code[$lower])) {
|
||||
return $name_to_code[$lower];
|
||||
}
|
||||
|
||||
// Return first two characters as fallback
|
||||
return strtoupper(substr($state, 0, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all existing properties in the database
|
||||
* Updates coordinates_invalid column for each record
|
||||
*
|
||||
* @param callable $progress_callback Optional callback for progress updates
|
||||
* @return array Results with counts
|
||||
*/
|
||||
public static function validate_all_properties($progress_callback = null) {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . MLS_TABLE_PROPERTIES;
|
||||
|
||||
$results = array(
|
||||
'total' => 0,
|
||||
'valid' => 0,
|
||||
'invalid' => 0,
|
||||
'errors' => array(),
|
||||
);
|
||||
|
||||
// Get total count
|
||||
$results['total'] = (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}");
|
||||
|
||||
if ($results['total'] === 0) {
|
||||
return $results;
|
||||
}
|
||||
|
||||
// Process in batches
|
||||
$batch_size = 500;
|
||||
$offset = 0;
|
||||
|
||||
while ($offset < $results['total']) {
|
||||
$properties = $wpdb->get_results($wpdb->prepare(
|
||||
"SELECT id, latitude, longitude, state_or_province FROM {$table} LIMIT %d OFFSET %d",
|
||||
$batch_size,
|
||||
$offset
|
||||
));
|
||||
|
||||
if (empty($properties)) {
|
||||
break;
|
||||
}
|
||||
|
||||
$valid_ids = array();
|
||||
$invalid_ids = array();
|
||||
|
||||
foreach ($properties as $property) {
|
||||
$is_invalid = self::check_property($property);
|
||||
|
||||
if ($is_invalid) {
|
||||
$invalid_ids[] = $property->id;
|
||||
$results['invalid']++;
|
||||
} else {
|
||||
$valid_ids[] = $property->id;
|
||||
$results['valid']++;
|
||||
}
|
||||
}
|
||||
|
||||
// Batch update valid records
|
||||
if (!empty($valid_ids)) {
|
||||
$ids_string = implode(',', array_map('intval', $valid_ids));
|
||||
$wpdb->query("UPDATE {$table} SET coordinates_invalid = 0 WHERE id IN ({$ids_string})");
|
||||
}
|
||||
|
||||
// Batch update invalid records
|
||||
if (!empty($invalid_ids)) {
|
||||
$ids_string = implode(',', array_map('intval', $invalid_ids));
|
||||
$wpdb->query("UPDATE {$table} SET coordinates_invalid = 1 WHERE id IN ({$ids_string})");
|
||||
}
|
||||
|
||||
$offset += $batch_size;
|
||||
|
||||
// Progress callback
|
||||
if ($progress_callback && is_callable($progress_callback)) {
|
||||
$progress_callback($offset, $results['total'], $results['valid'], $results['invalid']);
|
||||
}
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get statistics about coordinate validity
|
||||
*
|
||||
* @return array Statistics
|
||||
*/
|
||||
public static function get_stats() {
|
||||
global $wpdb;
|
||||
|
||||
$table = $wpdb->prefix . MLS_TABLE_PROPERTIES;
|
||||
|
||||
return array(
|
||||
'total' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table}"),
|
||||
'valid' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 0"),
|
||||
'invalid' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE coordinates_invalid = 1"),
|
||||
'null_coords' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$table} WHERE latitude IS NULL OR longitude IS NULL"),
|
||||
);
|
||||
}
|
||||
}
|
||||
Regular → Executable
+5
-3
@@ -375,13 +375,15 @@ class MLS_Image_Endpoint {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Send headers
|
||||
// Send headers - remove any no-cache headers WordPress may have added
|
||||
header_remove('Pragma');
|
||||
header('Pragma: public');
|
||||
header('Content-Type: ' . $mime_type);
|
||||
header('Content-Length: ' . $file_size);
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $last_modified) . ' GMT');
|
||||
header('ETag: "' . $etag . '"');
|
||||
header('Cache-Control: public, max-age=31536000'); // 1 year
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 31536000) . ' GMT');
|
||||
header('Cache-Control: public, max-age=3600'); // 1 hour
|
||||
header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 3600) . ' GMT');
|
||||
|
||||
// Stream file
|
||||
readfile($path);
|
||||
|
||||
Regular → Executable
Regular → Executable
Regular → Executable
@@ -109,8 +109,8 @@ class MLS_Query {
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SQL for distance-based filtering using Haversine formula
|
||||
* Returns properties within specified miles of a center point
|
||||
* Build SQL for distance-based filtering using spatial index
|
||||
* Uses bounding box pre-filter + ST_Distance_Sphere for accuracy
|
||||
*
|
||||
* @param float $lat Center latitude
|
||||
* @param float $lng Center longitude
|
||||
@@ -118,14 +118,46 @@ class MLS_Query {
|
||||
* @return string SQL expression for distance filter
|
||||
*/
|
||||
private function get_distance_filter_sql($lat, $lng, $miles) {
|
||||
// Haversine formula: distance in miles
|
||||
// 3959 is Earth's radius in miles
|
||||
// Convert miles to meters for ST_Distance_Sphere (returns meters)
|
||||
$meters = $miles * 1609.344;
|
||||
|
||||
// Create center point (SRID 4326 uses lat, lng order in MySQL 8.0+)
|
||||
// Use ST_Distance_Sphere with the spatial indexed location column
|
||||
return sprintf(
|
||||
"(3959 * acos(cos(radians(%f)) * cos(radians(latitude)) * cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)))) <= %f",
|
||||
"ST_Distance_Sphere(location, ST_PointFromText('POINT(%f %f)', 4326)) <= %f",
|
||||
$lat,
|
||||
$lng,
|
||||
$lat,
|
||||
$miles
|
||||
$meters
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build SQL for bounding box pre-filter
|
||||
* Uses simple BETWEEN for fast initial filtering before distance calc
|
||||
* This narrows down candidates significantly before the expensive ST_Distance_Sphere
|
||||
*
|
||||
* @param float $lat Center latitude
|
||||
* @param float $lng Center longitude
|
||||
* @param float $miles Radius in miles
|
||||
* @return string SQL expression for bounding box filter
|
||||
*/
|
||||
private function get_bounding_box_filter_sql($lat, $lng, $miles) {
|
||||
// Approximate degrees per mile (varies by latitude, using average)
|
||||
// 1 degree latitude ≈ 69 miles
|
||||
// 1 degree longitude ≈ 69 miles * cos(latitude)
|
||||
$lat_delta = $miles / 69.0;
|
||||
$lng_delta = $miles / (69.0 * cos(deg2rad($lat)));
|
||||
|
||||
$min_lat = $lat - $lat_delta;
|
||||
$max_lat = $lat + $lat_delta;
|
||||
$min_lng = $lng - $lng_delta;
|
||||
$max_lng = $lng + $lng_delta;
|
||||
|
||||
// Use BETWEEN for bounding box - efficient with indexes on lat/lng
|
||||
return sprintf(
|
||||
"(latitude BETWEEN %f AND %f AND longitude BETWEEN %f AND %f)",
|
||||
$min_lat, $max_lat,
|
||||
$min_lng, $max_lng
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,6 +193,7 @@ class MLS_Query {
|
||||
'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)
|
||||
'featured_ids' => null, // Array of listing_id values to prioritize after HomeProz
|
||||
'limit' => 20,
|
||||
'offset' => 0,
|
||||
'orderby' => 'modification_timestamp',
|
||||
@@ -218,12 +251,18 @@ class MLS_Query {
|
||||
$values[] = $args['postal_code'];
|
||||
} elseif ($args['center_lat'] && $args['center_lng']) {
|
||||
// Direct lat/lng radius search (from homepage location search)
|
||||
// Use bounding box pre-filter for spatial index, then exact distance
|
||||
$bbox_filter = $this->get_bounding_box_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
(int) $args['radius']
|
||||
);
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
(int) $args['radius']
|
||||
);
|
||||
$where[] = "({$distance_filter})";
|
||||
$where[] = "({$bbox_filter} AND {$distance_filter})";
|
||||
}
|
||||
|
||||
if ($args['county']) {
|
||||
@@ -311,6 +350,18 @@ class MLS_Query {
|
||||
$sql .= ' WHERE ' . implode(' AND ', $where);
|
||||
|
||||
// ORDER BY
|
||||
// Always prioritize: 1) HomeProz listings, 2) Featured listings, 3) Regular listings
|
||||
// Build featured sort expression if featured_ids provided
|
||||
$featured_sort = '';
|
||||
if (!empty($args['featured_ids']) && is_array($args['featured_ids'])) {
|
||||
$featured_ids = array_map('sanitize_text_field', $args['featured_ids']);
|
||||
$featured_placeholders = implode(',', array_fill(0, count($featured_ids), '%s'));
|
||||
$featured_sort = $wpdb->prepare(
|
||||
", (CASE WHEN listing_id IN ({$featured_placeholders}) THEN 1 ELSE 0 END) DESC",
|
||||
...$featured_ids
|
||||
);
|
||||
}
|
||||
|
||||
// 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'];
|
||||
@@ -318,7 +369,7 @@ class MLS_Query {
|
||||
// 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",
|
||||
" ORDER BY is_homeproz DESC{$featured_sort}, (POW(latitude - %f, 2) + POW((longitude - %f) * %f, 2)) ASC",
|
||||
(float) $center_lat,
|
||||
(float) $center_lng,
|
||||
$lat_factor
|
||||
@@ -338,7 +389,7 @@ class MLS_Query {
|
||||
|
||||
$orderby = in_array($args['orderby'], $allowed_orderby) ? $args['orderby'] : 'modification_timestamp';
|
||||
$order = strtoupper($args['order']) === 'ASC' ? 'ASC' : 'DESC';
|
||||
$sql .= " ORDER BY {$orderby} {$order}";
|
||||
$sql .= " ORDER BY is_homeproz DESC{$featured_sort}, {$orderby} {$order}";
|
||||
}
|
||||
|
||||
// LIMIT/OFFSET
|
||||
@@ -530,13 +581,19 @@ class MLS_Query {
|
||||
$values[] = $args['postal_code'];
|
||||
} elseif (!empty($args['center_lat']) && !empty($args['center_lng'])) {
|
||||
// Direct lat/lng radius search (from homepage location search)
|
||||
// Use bounding box pre-filter for spatial index, then exact distance
|
||||
$radius = !empty($args['radius']) ? (int) $args['radius'] : 30;
|
||||
$bbox_filter = $this->get_bounding_box_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
$radius
|
||||
);
|
||||
$distance_filter = $this->get_distance_filter_sql(
|
||||
(float) $args['center_lat'],
|
||||
(float) $args['center_lng'],
|
||||
$radius
|
||||
);
|
||||
$where[] = "({$distance_filter})";
|
||||
$where[] = "({$bbox_filter} AND {$distance_filter})";
|
||||
}
|
||||
|
||||
if (!empty($args['county'])) {
|
||||
@@ -728,7 +785,8 @@ class MLS_Query {
|
||||
|
||||
$table = $this->db->properties_table();
|
||||
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL');
|
||||
// Exclude properties with invalid coordinates from map bounds
|
||||
$where = array('mlg_can_view = 1', 'latitude IS NOT NULL', 'longitude IS NOT NULL', 'coordinates_invalid = 0');
|
||||
$values = array();
|
||||
|
||||
// Add state filter (MN and IA only)
|
||||
|
||||
Regular → Executable
Regular → Executable
+20
@@ -627,6 +627,18 @@ class MLS_Sync_Engine {
|
||||
$this->emit_progress('property_created', array('listing_key' => $listing_key));
|
||||
}
|
||||
|
||||
// Update spatial location column (wpdb can't handle ST_PointFromText directly)
|
||||
$lat = $property['Latitude'] ?? null;
|
||||
$lng = $property['Longitude'] ?? null;
|
||||
if ($lat !== null && $lng !== null) {
|
||||
$wpdb->query($wpdb->prepare(
|
||||
"UPDATE {$this->db->properties_table()} SET location = ST_PointFromText(CONCAT('POINT(', %f, ' ', %f, ')'), 4326) WHERE listing_key = %s",
|
||||
(float) $lat,
|
||||
(float) $lng,
|
||||
$listing_key
|
||||
));
|
||||
}
|
||||
|
||||
// Process media if present
|
||||
if (isset($property['Media']) && is_array($property['Media'])) {
|
||||
$this->media_handler->sync_property_media($listing_key, $property['Media'], false, $this->progress_callback);
|
||||
@@ -652,6 +664,13 @@ class MLS_Sync_Engine {
|
||||
* @return array Mapped data for database
|
||||
*/
|
||||
private function map_property_data($property) {
|
||||
// Validate coordinates against state boundaries
|
||||
$coordinates_invalid = MLS_Geo_Validator::validate_coordinates(
|
||||
$property['Latitude'] ?? null,
|
||||
$property['Longitude'] ?? null,
|
||||
$property['StateOrProvince'] ?? 'MN'
|
||||
) ? 0 : 1;
|
||||
|
||||
return array(
|
||||
'listing_id' => $property['ListingId'] ?? null,
|
||||
'originating_system' => $property['OriginatingSystemName'] ?? 'northstar',
|
||||
@@ -673,6 +692,7 @@ class MLS_Sync_Engine {
|
||||
'county' => $property['CountyOrParish'] ?? null,
|
||||
'latitude' => $property['Latitude'] ?? null,
|
||||
'longitude' => $property['Longitude'] ?? null,
|
||||
'coordinates_invalid' => $coordinates_invalid,
|
||||
|
||||
'property_type' => $property['PropertyType'] ?? null,
|
||||
'property_sub_type' => $property['PropertySubType'] ?? null,
|
||||
|
||||
@@ -97,6 +97,7 @@ final class MLS_Plugin {
|
||||
require_once MLS_PLUGIN_DIR . 'includes/class-mls-image-endpoint.php';
|
||||
require_once MLS_PLUGIN_DIR . 'includes/class-mls-query.php';
|
||||
require_once MLS_PLUGIN_DIR . 'includes/class-mls-cluster.php';
|
||||
require_once MLS_PLUGIN_DIR . 'includes/class-mls-geo-validator.php';
|
||||
|
||||
// Activation/Deactivation
|
||||
require_once MLS_PLUGIN_DIR . 'includes/class-mls-activator.php';
|
||||
|
||||
Regular → Executable
Reference in New Issue
Block a user