Last active
June 9, 2025 03:24
-
-
Save brandonjp/d44decb9ed29bc4fb977404ec923339b to your computer and use it in GitHub Desktop.
Add GPS Coordinates Column to WordPress Media Library [SnipSnip.pro]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Title: Add GPS Coordinates Column to WordPress Media Library (Best Version) | |
* Description: Extracts and displays GPS coordinates from image EXIF data in the media library table view. Robust extraction (multiple fallbacks), developer-configurable UI, admin notice, and copy-to-clipboard functionality. | |
* Version: 1.6.0 | |
* Author: brandonjp.com | |
* Last Updated: 2025-06-08 | |
* Blog URL: http://snipsnip.pro/880 | |
* Requirements: WordPress 5.0+ | |
* License: GPL v2 or later | |
* | |
* Changelog: | |
* 1.6.0 - Added GPS coordinate caching as post meta for major performance improvement | |
* 1.5.2 - Added comprehensive debugging and localhost troubleshooting capabilities | |
* 1.5.1 - Fixed fatal errors on localhost: proper EXIF checks, PHP compatibility, better error handling | |
* 1.5.0 - Fixed click-to-copy functionality: proper script dependencies, event delegation, timing fixes | |
* 1.4.0 - Use proven working copy-to-clipboard JS, fix map icon hover, config-driven feedback, improved reliability | |
* 1.3.1 - Fix click to copy bugs and add text labels to config options | |
* 1.3.0 - Best version: robust extraction, configurable UI, admin notice, modern JS feedback | |
* 1.2.0 - Simplified to reliable methods only: PHP EXIF and WordPress core | |
* 1.1.0 - Added multiple fallback methods | |
* 1.0.0 - Initial release | |
*/ | |
// ===================== | |
// CONFIGURATION SECTION | |
// ===================== | |
$gps_column_config = array( | |
// UI style: 'compact' (two lines, icons) or 'verbose' (labels, WP buttons, two lines) | |
'ui_style' => 'verbose', | |
// Show admin notice about available extraction methods | |
'show_admin_notice' => true, | |
// Extraction methods order (array of method keys) | |
'extraction_order' => array('wordpress', 'exif', 'getimagesize', 'exiftool'), | |
// Show dash or text if no GPS: 'dash' or 'text' | |
'no_gps_display' => 'dash', | |
// Enable error logging for extraction failures | |
'log_errors' => true, | |
// Debug mode - shows extraction attempts in admin notices | |
'debug_mode' => true, | |
// Test only exiftool (useful for debugging) - set to true to skip other methods | |
'test_exiftool_only' => false, | |
// Cache GPS coordinates as post meta for performance (recommended: true) | |
// This dramatically improves page load times by avoiding repeated file EXIF extraction | |
// GPS data is extracted once and stored as post meta, cleared when attachment is updated | |
'cache_gps_meta' => true, | |
// Meta key for storing GPS coordinates | |
'meta_key_gps' => '_gps_coordinates', | |
// Meta key for storing "no GPS" flag to avoid repeated failed extractions | |
'meta_key_no_gps' => '_gps_no_data', | |
// UI text and emoji config | |
'text_map_icon' => 'πΊοΈ', | |
'text_map_icon_hover' => 'π', | |
'text_map_label' => 'Map', | |
'text_copy_icon' => 'π', | |
'text_copy_icon_success' => 'β', | |
'text_copy_label' => 'Copy', | |
'text_copied_label' => 'β Copied!', | |
'text_no_gps' => 'No GPS', | |
'text_no_gps_verbose' => 'No GPS data', | |
'text_dash' => 'β', | |
); | |
if (!class_exists('WP_Media_Library_GPS_Coordinates_Column')): | |
class WP_Media_Library_GPS_Coordinates_Column { | |
const VERSION = '1.6.0'; | |
const COLUMN_ID = 'gps_coordinates'; | |
private $config; | |
public function __construct($config = array()) { | |
$this->config = $config; | |
add_action('init', array($this, 'init')); | |
} | |
public function init() { | |
add_filter('manage_media_columns', array($this, 'add_gps_column')); | |
add_action('manage_media_custom_column', array($this, 'display_gps_column'), 10, 2); | |
add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); | |
// Cache management hooks | |
if (!empty($this->config['cache_gps_meta'])) { | |
add_action('attachment_updated', array($this, 'clear_gps_cache'), 10, 3); | |
add_action('edit_attachment', array($this, 'clear_gps_cache_simple')); | |
} | |
if (!empty($this->config['show_admin_notice'])) { | |
add_action('admin_notices', array($this, 'show_extraction_methods_info')); | |
} | |
} | |
public function add_gps_column($columns) { | |
$new_columns = array(); | |
foreach ($columns as $key => $value) { | |
if ($key === 'date') { | |
$new_columns[self::COLUMN_ID] = ($this->config['ui_style'] === 'verbose') ? 'GPS Coordinates' : 'GPS'; | |
} | |
$new_columns[$key] = $value; | |
} | |
return $new_columns; | |
} | |
public function display_gps_column($column_name, $attachment_id) { | |
if ($column_name !== self::COLUMN_ID) return; | |
try { | |
$coordinates = $this->get_gps_coordinates($attachment_id); | |
$ui = $this->config['ui_style']; | |
$no_gps = $this->config['no_gps_display']; | |
$cfg = $this->config; | |
if ($coordinates) { | |
$lat = $coordinates['latitude']; | |
$lng = $coordinates['longitude']; | |
$maps_url = "https://maps.google.com/?q={$lat},{$lng}"; | |
if ($ui === 'verbose') { | |
echo '<div class="gps-coordinates-wrapper">'; | |
echo '<div class="gps-coords" data-lat="' . esc_attr($lat) . '" data-lng="' . esc_attr($lng) . '">'; | |
echo '<strong>Lat:</strong> ' . esc_html($lat) . '<br>'; | |
echo '<strong>Lng:</strong> ' . esc_html($lng) . '</div>'; | |
echo '<div class="gps-actions">'; | |
echo '<a href="' . esc_url($maps_url) . '" target="_blank" class="button button-small gps-map-link" title="Open in Google Maps" data-map-icon="' . esc_attr($cfg['text_map_icon']) . '" data-map-icon-hover="' . esc_attr($cfg['text_map_icon_hover']) . '">' . esc_html($cfg['text_map_icon']) . ' ' . esc_html($cfg['text_map_label']) . '</a> '; | |
echo '<button type="button" class="button button-small copy-gps" data-coords="' . esc_attr($lat . ',' . $lng) . '" title="Copy coordinates" data-copy-icon="' . esc_attr($cfg['text_copy_icon']) . '" data-copy-icon-success="' . esc_attr($cfg['text_copy_icon_success']) . '">' . esc_html($cfg['text_copy_icon']) . ' ' . esc_html($cfg['text_copy_label']) . '</button>'; | |
echo '</div></div>'; | |
} else { // compact | |
echo '<div class="gps-wrapper">'; | |
echo '<div class="gps-coords">' . esc_html($lat) . '<br>' . esc_html($lng) . '</div>'; | |
echo '<div class="gps-actions">'; | |
echo '<a href="' . esc_url($maps_url) . '" target="_blank" class="gps-map-link" title="View on map" data-map-icon="' . esc_attr($cfg['text_map_icon']) . '" data-map-icon-hover="' . esc_attr($cfg['text_map_icon_hover']) . '">' . esc_html($cfg['text_map_icon']) . '</a> '; | |
echo '<button type="button" class="copy-gps" data-coords="' . esc_attr($lat . ',' . $lng) . '" title="Copy coordinates" data-copy-icon="' . esc_attr($cfg['text_copy_icon']) . '" data-copy-icon-success="' . esc_attr($cfg['text_copy_icon_success']) . '">' . esc_html($cfg['text_copy_icon']) . '</button>'; | |
echo '</div></div>'; | |
} | |
} else { | |
if ($no_gps === 'text') { | |
echo ($ui === 'verbose') ? '<span class="description">' . esc_html($cfg['text_no_gps_verbose']) . '</span>' : '<span class="no-gps">' . esc_html($cfg['text_no_gps']) . '</span>'; | |
} else { | |
echo '<span class="no-gps">' . esc_html($cfg['text_dash']) . '</span>'; | |
} | |
} | |
} catch (Exception $e) { | |
// Fail silently to prevent breaking the media library | |
if (!empty($this->config['log_errors'])) { | |
error_log('GPS Coordinates: display_gps_column failed - ' . $e->getMessage()); | |
} | |
echo '<span class="no-gps">' . esc_html($this->config['text_dash']) . '</span>'; | |
} | |
} | |
/** | |
* Extract GPS coordinates using multiple fallback methods with caching support | |
* @param int $attachment_id | |
* @return array|false | |
*/ | |
private function get_gps_coordinates($attachment_id) { | |
// If caching is enabled, check cache first | |
if (!empty($this->config['cache_gps_meta'])) { | |
return $this->get_gps_coordinates_with_cache($attachment_id); | |
} | |
// If caching is disabled, use direct extraction | |
return $this->extract_gps_coordinates($attachment_id); | |
} | |
/** | |
* Get GPS coordinates with caching support | |
* @param int $attachment_id | |
* @return array|false | |
*/ | |
private function get_gps_coordinates_with_cache($attachment_id) { | |
$meta_key_gps = $this->config['meta_key_gps']; | |
$meta_key_no_gps = $this->config['meta_key_no_gps']; | |
// Check if GPS coordinates are already cached | |
$cached_gps = get_post_meta($attachment_id, $meta_key_gps, true); | |
if (!empty($cached_gps) && is_array($cached_gps) && isset($cached_gps['latitude']) && isset($cached_gps['longitude'])) { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Using cached coordinates for attachment $attachment_id: " . json_encode($cached_gps)); | |
} | |
return $cached_gps; | |
} | |
// Check if we've already determined there's no GPS data | |
$no_gps_flag = get_post_meta($attachment_id, $meta_key_no_gps, true); | |
if ($no_gps_flag === '1') { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Using cached 'no GPS' flag for attachment $attachment_id"); | |
} | |
return false; | |
} | |
// Extract GPS coordinates | |
$coordinates = $this->extract_gps_coordinates($attachment_id); | |
// Cache the result | |
if ($coordinates !== false) { | |
update_post_meta($attachment_id, $meta_key_gps, $coordinates); | |
// Remove any existing "no GPS" flag | |
delete_post_meta($attachment_id, $meta_key_no_gps); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Cached GPS coordinates for attachment $attachment_id: " . json_encode($coordinates)); | |
} | |
} else { | |
// Store "no GPS" flag to avoid repeated failed extractions | |
update_post_meta($attachment_id, $meta_key_no_gps, '1'); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Cached 'no GPS' flag for attachment $attachment_id"); | |
} | |
} | |
return $coordinates; | |
} | |
/** | |
* Extract GPS coordinates using multiple fallback methods (without caching) | |
* @param int $attachment_id | |
* @return array|false | |
*/ | |
private function extract_gps_coordinates($attachment_id) { | |
$file_path = get_attached_file($attachment_id); | |
if (!$file_path || !file_exists($file_path)) return false; | |
$mime_type = get_post_mime_type($attachment_id); | |
// PHP 8.0+ compatibility - use substr instead of str_starts_with | |
if (!$mime_type || substr($mime_type, 0, 6) !== 'image/') return false; | |
$order = isset($this->config['extraction_order']) ? $this->config['extraction_order'] : array('wordpress', 'exif', 'getimagesize', 'exiftool'); | |
// Override for testing exiftool only | |
if (!empty($this->config['test_exiftool_only'])) { | |
$order = array('exiftool'); | |
} | |
$debug_info = array(); | |
foreach ($order as $method) { | |
try { | |
$coordinates = false; | |
switch ($method) { | |
case 'wordpress': | |
$coordinates = $this->extract_gps_with_wordpress($file_path); | |
break; | |
case 'exif': | |
$coordinates = $this->extract_gps_with_exif($file_path); | |
break; | |
case 'getimagesize': | |
$coordinates = $this->extract_gps_with_getimagesize($file_path); | |
break; | |
case 'exiftool': | |
$coordinates = $this->extract_gps_with_exiftool($file_path); | |
break; | |
} | |
if (!empty($this->config['debug_mode'])) { | |
$debug_info[$method] = $coordinates !== false ? 'SUCCESS' : 'NO_GPS_DATA'; | |
} | |
if ($coordinates !== false) { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Method '$method' succeeded for attachment $attachment_id: " . json_encode($coordinates)); | |
} | |
return $coordinates; | |
} | |
} catch (Exception $e) { | |
if (!empty($this->config['debug_mode'])) { | |
$debug_info[$method] = 'EXCEPTION: ' . $e->getMessage(); | |
} | |
if (!empty($this->config['log_errors'])) { | |
error_log('GPS Coordinates: ' . get_class($this) . " $method extraction failed - " . $e->getMessage()); | |
} | |
continue; | |
} catch (Error $e) { | |
if (!empty($this->config['debug_mode'])) { | |
$debug_info[$method] = 'ERROR: ' . $e->getMessage(); | |
} | |
// Handle fatal errors like missing functions | |
if (!empty($this->config['log_errors'])) { | |
error_log('GPS Coordinates: ' . get_class($this) . " $method fatal error - " . $e->getMessage()); | |
} | |
continue; | |
} | |
} | |
if (!empty($this->config['debug_mode']) && !empty($debug_info)) { | |
error_log("GPS Debug: All methods failed for attachment $attachment_id: " . json_encode($debug_info)); | |
} | |
return false; | |
} | |
/** Extraction methods **/ | |
private function extract_gps_with_wordpress($file_path) { | |
if (!function_exists('wp_read_image_metadata')) return false; | |
$metadata = wp_read_image_metadata($file_path); | |
if (!$metadata || empty($metadata['latitude']) || empty($metadata['longitude'])) return false; | |
return array( | |
'latitude' => round((float) $metadata['latitude'], 6), | |
'longitude' => round((float) $metadata['longitude'], 6) | |
); | |
} | |
private function extract_gps_with_exif($file_path) { | |
if (!extension_loaded('exif') || !function_exists('exif_read_data')) return false; | |
$exif = @exif_read_data($file_path); | |
if (!$exif || !isset($exif['GPS'])) return false; | |
return $this->parse_gps_from_exif($exif['GPS']); | |
} | |
private function extract_gps_with_getimagesize($file_path) { | |
// Check for both required functions | |
if (!function_exists('getimagesize') || !extension_loaded('exif') || !function_exists('exif_read_data')) return false; | |
$imageinfo = array(); | |
$size = @getimagesize($file_path, $imageinfo); | |
if (!$size || empty($imageinfo['APP1'])) return false; | |
$exif = @exif_read_data('data://image/jpeg;base64,' . base64_encode($imageinfo['APP1'])); | |
if (!$exif || !isset($exif['GPS'])) return false; | |
return $this->parse_gps_from_exif($exif['GPS']); | |
} | |
private function extract_gps_with_exiftool($file_path) { | |
if (!$this->is_exiftool_available()) { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: ExifTool not available"); | |
} | |
return false; | |
} | |
$escaped_path = escapeshellarg($file_path); | |
$command = "exiftool -GPS:GPSLatitude -GPS:GPSLongitude -GPS:GPSLatitudeRef -GPS:GPSLongitudeRef -n -T {$escaped_path} 2>/dev/null"; | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Running exiftool command: $command"); | |
} | |
$output = @shell_exec($command); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: ExifTool raw output: " . var_export($output, true)); | |
} | |
if (!$output) return false; | |
$values = explode("\t", trim($output)); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: ExifTool parsed values: " . json_encode($values)); | |
} | |
if (count($values) < 4 || $values[0] === '-' || $values[1] === '-') return false; | |
$latitude = (float) $values[0]; | |
$longitude = (float) $values[1]; | |
$lat_ref = trim($values[2]); | |
$lng_ref = trim($values[3]); | |
if (strtoupper($lat_ref) === 'S') $latitude = -$latitude; | |
if (strtoupper($lng_ref) === 'W') $longitude = -$longitude; | |
$result = array( | |
'latitude' => round($latitude, 6), | |
'longitude' => round($longitude, 6) | |
); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: ExifTool final result: " . json_encode($result)); | |
} | |
return $result; | |
} | |
private function is_exiftool_available() { | |
if (!function_exists('shell_exec')) { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: shell_exec function not available"); | |
} | |
return false; | |
} | |
$disabled_functions = explode(',', ini_get('disable_functions')); | |
$disabled_functions = array_map('trim', $disabled_functions); | |
if (in_array('shell_exec', $disabled_functions) || in_array('exec', $disabled_functions)) { | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: shell_exec or exec is disabled"); | |
} | |
return false; | |
} | |
$output = @shell_exec('exiftool -ver 2>/dev/null'); | |
$available = !empty($output) && is_numeric(trim($output)); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: ExifTool version check output: " . var_export($output, true) . " | Available: " . ($available ? 'YES' : 'NO')); | |
} | |
return $available; | |
} | |
/** | |
* Cache management methods | |
*/ | |
/** | |
* Clear GPS cache when attachment is updated | |
* @param int $post_ID | |
* @param WP_Post $post_after | |
* @param WP_Post $post_before | |
*/ | |
public function clear_gps_cache($post_ID, $post_after, $post_before) { | |
if ($post_after->post_type === 'attachment') { | |
$this->clear_gps_cache_for_attachment($post_ID); | |
} | |
} | |
/** | |
* Clear GPS cache when attachment is edited (simpler hook) | |
* @param int $attachment_id | |
*/ | |
public function clear_gps_cache_simple($attachment_id) { | |
$this->clear_gps_cache_for_attachment($attachment_id); | |
} | |
/** | |
* Clear GPS cache for specific attachment | |
* @param int $attachment_id | |
*/ | |
private function clear_gps_cache_for_attachment($attachment_id) { | |
if (empty($this->config['cache_gps_meta'])) return; | |
$meta_key_gps = $this->config['meta_key_gps']; | |
$meta_key_no_gps = $this->config['meta_key_no_gps']; | |
delete_post_meta($attachment_id, $meta_key_gps); | |
delete_post_meta($attachment_id, $meta_key_no_gps); | |
if (!empty($this->config['debug_mode'])) { | |
error_log("GPS Debug: Cleared GPS cache for attachment $attachment_id"); | |
} | |
} | |
/** | |
* Manually refresh GPS cache for attachment (useful for admin interfaces) | |
* @param int $attachment_id | |
* @return array|false | |
*/ | |
public function refresh_gps_cache($attachment_id) { | |
$this->clear_gps_cache_for_attachment($attachment_id); | |
return $this->get_gps_coordinates($attachment_id); | |
} | |
private function parse_gps_from_exif($gps) { | |
if (!isset($gps['GPSLatitude']) || !isset($gps['GPSLongitude']) || | |
!isset($gps['GPSLatitudeRef']) || !isset($gps['GPSLongitudeRef'])) return false; | |
$latitude = $this->convert_gps_to_decimal($gps['GPSLatitude'], $gps['GPSLatitudeRef']); | |
$longitude = $this->convert_gps_to_decimal($gps['GPSLongitude'], $gps['GPSLongitudeRef']); | |
if ($latitude === false || $longitude === false) return false; | |
return array( | |
'latitude' => round($latitude, 6), | |
'longitude' => round($longitude, 6) | |
); | |
} | |
private function convert_gps_to_decimal($coordinate, $hemisphere) { | |
if (!is_array($coordinate) || count($coordinate) < 3) return false; | |
$degrees = $this->fraction_to_decimal($coordinate[0]); | |
$minutes = $this->fraction_to_decimal($coordinate[1]); | |
$seconds = $this->fraction_to_decimal($coordinate[2]); | |
if ($degrees === false || $minutes === false || $seconds === false) return false; | |
$decimal = $degrees + ($minutes / 60) + ($seconds / 3600); | |
if (in_array(strtoupper($hemisphere), array('S', 'W'))) $decimal = -$decimal; | |
return $decimal; | |
} | |
private function fraction_to_decimal($fraction) { | |
if (is_numeric($fraction)) return (float) $fraction; | |
if (is_string($fraction) && strpos($fraction, '/') !== false) { | |
$parts = explode('/', $fraction); | |
if (count($parts) === 2 && is_numeric($parts[0]) && is_numeric($parts[1]) && $parts[1] != 0) { | |
return (float) $parts[0] / (float) $parts[1]; | |
} | |
} | |
return false; | |
} | |
/** | |
* Show available extraction methods info (dismissible) | |
*/ | |
public function show_extraction_methods_info() { | |
if (get_current_screen()->id !== 'upload') return; | |
$user_id = get_current_user_id(); | |
$dismissed = get_user_meta($user_id, 'gps_extraction_methods_dismissed', true); | |
if ($dismissed) return; | |
$methods = $this->get_available_extraction_methods(); | |
$method_names = array(); | |
foreach ($methods as $method) { | |
$status_indicator = $method['status'] === 'available' ? 'β' : 'β'; | |
$method_names[] = $status_indicator . ' ' . $method['name']; | |
} | |
$debug_info = ''; | |
if (!empty($this->config['debug_mode'])) { | |
$debug_info = '<br><small><strong>Debug mode enabled:</strong> Check error logs for detailed extraction attempts.</small>'; | |
} | |
$cache_info = ''; | |
if (!empty($this->config['cache_gps_meta'])) { | |
$cache_info = '<br><small><strong>GPS Caching:</strong> β Enabled (coordinates stored as post meta for better performance)</small>'; | |
} else { | |
$cache_info = '<br><small><strong>GPS Caching:</strong> β Disabled (extracting from files on every page load)</small>'; | |
} | |
echo '<div class="notice notice-info is-dismissible" data-dismiss-key="gps_extraction_methods">'; | |
echo '<p><strong>GPS Coordinates Column:</strong> Available extraction methods: ' . esc_html(implode(', ', $method_names)) . $cache_info . $debug_info . '</p>'; | |
echo '</div>'; | |
$this->add_dismiss_handler(); | |
} | |
private function add_dismiss_handler() { | |
$js = <<<'JAVASCRIPT' | |
jQuery(document).on('click', '.notice[data-dismiss-key] .notice-dismiss', function() { | |
var dismissKey = jQuery(this).parent().data('dismiss-key'); | |
if (dismissKey) { | |
jQuery.post(ajaxurl, { | |
action: 'dismiss_gps_notice', | |
key: dismissKey, | |
nonce: (typeof wpApiSettings !== 'undefined' && wpApiSettings.nonce) ? wpApiSettings.nonce : jQuery('#_wpnonce').val() | |
}); | |
} | |
}); | |
JAVASCRIPT; | |
wp_add_inline_script('jquery', $js); | |
add_action('wp_ajax_dismiss_gps_notice', array($this, 'handle_dismiss_notice')); | |
} | |
public function handle_dismiss_notice() { | |
// Use standard nonce check instead of REST API nonce | |
if (!current_user_can('manage_options')) { | |
wp_die('Unauthorized'); | |
} | |
$key = sanitize_text_field($_POST['key']); | |
$user_id = get_current_user_id(); | |
if ($key === 'gps_extraction_methods') { | |
update_user_meta($user_id, 'gps_extraction_methods_dismissed', true); | |
} | |
wp_die(); | |
} | |
private function get_available_extraction_methods() { | |
$methods = array(); | |
if (function_exists('wp_read_image_metadata')) { | |
$methods[] = array('name' => 'WordPress Core', 'status' => 'available'); | |
} else { | |
$methods[] = array('name' => 'WordPress Core', 'status' => 'unavailable'); | |
} | |
if (extension_loaded('exif') && function_exists('exif_read_data')) { | |
$methods[] = array('name' => 'PHP EXIF', 'status' => 'available'); | |
} else { | |
$methods[] = array('name' => 'PHP EXIF', 'status' => 'unavailable'); | |
} | |
if (function_exists('getimagesize') && extension_loaded('exif') && function_exists('exif_read_data')) { | |
$methods[] = array('name' => 'getimagesize()', 'status' => 'available'); | |
} else { | |
$methods[] = array('name' => 'getimagesize()', 'status' => 'unavailable'); | |
} | |
if ($this->is_exiftool_available()) { | |
$methods[] = array('name' => 'ExifTool', 'status' => 'available'); | |
} else { | |
$methods[] = array('name' => 'ExifTool', 'status' => 'unavailable'); | |
} | |
return $methods; | |
} | |
/** | |
* Enqueue admin styles and scripts | |
*/ | |
public function enqueue_admin_assets() { | |
$screen = get_current_screen(); | |
if ($screen && $screen->id === 'upload') { | |
$this->add_admin_styles(); | |
$this->add_admin_scripts(); | |
} | |
} | |
private function add_admin_styles() { | |
$ui = $this->config['ui_style']; | |
if ($ui === 'verbose') { | |
$css = <<<'STYLES' | |
.gps-coordinates-wrapper { | |
font-size: 12px; | |
line-height: 1.4; | |
} | |
.gps-coords { | |
margin-bottom: 5px; | |
color: #666; | |
} | |
.gps-actions { | |
margin-top: 5px; | |
} | |
.gps-actions .button { | |
font-size: 11px; | |
padding: 2px 6px; | |
line-height: 1.2; | |
height: auto; | |
margin-right: 3px; | |
} | |
.copy-gps.copied { | |
background-color: #00a32a; | |
border-color: #00a32a; | |
color: white; | |
} | |
.column-gps_coordinates { | |
width: 150px; | |
} | |
@media screen and (max-width: 782px) { | |
.column-gps_coordinates { | |
display: none; | |
} | |
} | |
STYLES; | |
} else { | |
$css = <<<'CSS' | |
.gps-wrapper { | |
font-size: 11px; | |
line-height: 1.3; | |
} | |
.gps-coords { | |
color: #666; | |
margin-bottom: 2px; | |
word-break: break-all; | |
} | |
.gps-actions a, | |
.gps-actions button { | |
background: none; | |
border: none; | |
cursor: pointer; | |
font-size: 14px; | |
text-decoration: none; | |
padding: 0; | |
margin-right: 4px; | |
} | |
.gps-actions button:hover { | |
opacity: 0.7; | |
} | |
.copy-gps.copied { | |
opacity: 0.5; | |
} | |
.column-gps_coordinates { | |
width: 110px; | |
} | |
.no-gps { | |
color: #ccc; | |
} | |
@media screen and (max-width: 782px) { | |
.column-gps_coordinates { | |
display: none; | |
} | |
} | |
CSS; | |
} | |
wp_add_inline_style('wp-admin', $css); | |
} | |
private function add_admin_scripts() { | |
$cfg = $this->config; | |
$js = <<<JAVASCRIPT | |
window.addEventListener("load", function() { | |
(function createGPSCoordinatesScope() { | |
"use strict"; | |
var config = { | |
copiedLabel: '{$cfg['text_copied_label']}', | |
mapIcon: '{$cfg['text_map_icon']}', | |
mapIconHover: '{$cfg['text_map_icon_hover']}' | |
}; | |
// Copy to clipboard functionality | |
function handleCopyClick(event) { | |
event.preventDefault(); | |
event.stopPropagation(); | |
var button = event.target.closest('.copy-gps'); | |
if (!button) return; | |
var coords = button.getAttribute('data-coords'); | |
if (!coords) { | |
showFeedback(button, 'β Failed'); | |
return; | |
} | |
copyToClipboard(coords).then(function() { | |
showFeedback(button, config.copiedLabel); | |
}).catch(function() { | |
showFeedback(button, 'β Failed'); | |
}); | |
} | |
function copyToClipboard(text) { | |
return new Promise(function(resolve, reject) { | |
// Try modern clipboard API first | |
if (navigator.clipboard && window.isSecureContext) { | |
navigator.clipboard.writeText(text).then(resolve).catch(function() { | |
fallbackCopy(text, resolve, reject); | |
}); | |
} else { | |
fallbackCopy(text, resolve, reject); | |
} | |
}); | |
} | |
function fallbackCopy(text, resolve, reject) { | |
try { | |
var textArea = document.createElement('textarea'); | |
textArea.value = text; | |
textArea.style.position = 'fixed'; | |
textArea.style.left = '-999999px'; | |
textArea.style.top = '-999999px'; | |
textArea.style.opacity = '0'; | |
document.body.appendChild(textArea); | |
textArea.focus(); | |
textArea.select(); | |
var successful = document.execCommand('copy'); | |
document.body.removeChild(textArea); | |
if (successful) { | |
resolve(); | |
} else { | |
reject(new Error('execCommand failed')); | |
} | |
} catch (err) { | |
if (document.body.contains(textArea)) { | |
document.body.removeChild(textArea); | |
} | |
reject(err); | |
} | |
} | |
function showFeedback(button, message) { | |
var originalText = button.textContent; | |
var originalBg = button.style.backgroundColor; | |
var originalColor = button.style.color; | |
button.textContent = message; | |
if (message.includes('β')) { | |
button.style.backgroundColor = '#46b450'; | |
button.style.color = 'white'; | |
button.classList.add('copied'); | |
} else { | |
button.style.backgroundColor = '#dc3232'; | |
button.style.color = 'white'; | |
} | |
setTimeout(function() { | |
button.textContent = originalText; | |
button.style.backgroundColor = originalBg; | |
button.style.color = originalColor; | |
button.classList.remove('copied'); | |
}, 1500); | |
} | |
// Map icon hover functionality | |
function initMapHover() { | |
var mapLinks = document.querySelectorAll('.gps-map-link'); | |
mapLinks.forEach(function(link) { | |
var icon = link.getAttribute('data-map-icon') || config.mapIcon; | |
var iconHover = link.getAttribute('data-map-icon-hover') || config.mapIconHover; | |
var textContent = link.textContent; | |
var label = textContent.replace(icon, '').trim(); | |
link.addEventListener('mouseenter', function() { | |
link.innerHTML = iconHover + (label ? ' ' + label : ''); | |
}); | |
link.addEventListener('mouseleave', function() { | |
link.innerHTML = icon + (label ? ' ' + label : ''); | |
}); | |
}); | |
} | |
// Initialize all functionality | |
function initGPSColumnFeatures() { | |
// Use event delegation for copy buttons | |
document.body.addEventListener('click', function(event) { | |
if (event.target.closest('.copy-gps')) { | |
handleCopyClick(event); | |
} | |
}); | |
// Initialize map hover | |
initMapHover(); | |
} | |
// Setup mutation observer for dynamic content | |
function setupDynamicContentHandler() { | |
var observer = new MutationObserver(function(mutations) { | |
var shouldReinit = false; | |
mutations.forEach(function(mutation) { | |
mutation.addedNodes.forEach(function(node) { | |
if (node.nodeType === 1 && ( | |
node.classList.contains('gps-coordinates-wrapper') || | |
node.classList.contains('gps-wrapper') || | |
node.querySelector('.gps-map-link') | |
)) { | |
shouldReinit = true; | |
} | |
}); | |
}); | |
if (shouldReinit) { | |
setTimeout(function() { | |
initMapHover(); | |
}, 100); | |
} | |
}); | |
observer.observe(document.body, { | |
childList: true, | |
subtree: true | |
}); | |
} | |
// Initialize everything | |
initGPSColumnFeatures(); | |
setupDynamicContentHandler(); | |
})(); | |
}); | |
JAVASCRIPT; | |
wp_add_inline_script('jquery', $js); | |
} | |
} | |
endif; | |
// Initialize the plugin with config | |
if (class_exists('WP_Media_Library_GPS_Coordinates_Column')): | |
new WP_Media_Library_GPS_Coordinates_Column($gps_column_config); | |
endif; |
v1.6.0 - feat: Add GPS coordinate caching system for major performance improvement
- Implement post meta caching for GPS coordinates to eliminate repeated EXIF file parsing
- Add cache management hooks to automatically clear cache when attachments are updated
- Store "no GPS" flags to prevent repeated failed extractions on images without GPS data
- Add configurable cache settings with sensible defaults (cache_gps_meta, meta_key_gps, meta_key_no_gps)
- Update admin notices to display cache status and performance benefits
- Maintain backward compatibility with option to disable caching
Performance impact: Dramatically reduces page load times for media library pages
with 50+ images by reading cached coordinates from database instead of parsing
files on every page load.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
feat: Add comprehensive debugging and localhost troubleshooting capabilities
Version 1.5.0 β 1.5.2
π Bug Fixes
str_starts_with()
withsubstr()
for broader PHP version supportdisplay_gps_column()
to prevent media library breakagewp_read_image_metadata()
in WordPress extraction methodexif_read_data()
in EXIF extraction methodsgetimagesize()
in getimagesize extraction methodshell_exec()
in ExifTool availability check$mime_type
before string operationsException
andError
classes to handle fatal errors gracefully⨠New Features
debug_mode
config option)test_exiftool_only
config option for isolated ExifTool testingπ§ Configuration Changes
log_errors: true
)debug_mode: true
)π₯ Error Handling Improvements
π Code Quality
This update primarily focuses on making the plugin more robust in development environments and providing better diagnostic capabilities for troubleshooting GPS extraction issues.