Last active
November 28, 2022 11:22
-
-
Save gsarig/6cb47d81a02688ddcb8df719bb8d8db9 to your computer and use it in GitHub Desktop.
Guess the default coordinates for WordPress timezones (read more: https://www.gsarigiannidis.gr/default-coordinates-for-wordpress-timezones/)
This file contains 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 | |
/** | |
* Plugin Name: Timezone Coordinates | |
* Description: Guess the default coordinates for WordPress timezones. | |
* Author: Giorgos Sarigiannidis | |
* Version: 0.1 | |
* Author URI: http://www.gsarigiannidis.gr | |
* License URI: http://www.gnu.org/licenses/gpl-3.0.html | |
* | |
* @package TimezoneCoordinates | |
*/ | |
/** | |
* The output of this class can be found here: https://gist.github.com/gsarig/083861efb8e47abf2f6fb5cc8c65dd9a | |
* | |
* More info about the implementation: https://www.gsarigiannidis.gr/default-coordinates-for-wordpress-timezones/ | |
* | |
* To call the class, paste `new Timezone_Coordinates()` on your theme's functions.php and reload the page. After you are done, make sure to remove it, to prevent it from running again. | |
*/ | |
class Timezone_Coordinates { | |
/** | |
* Get a list of airports from https://davidmegginson.github.io/ourairports-data/ | |
* | |
* @var string | |
*/ | |
public static string $airports_csv = 'https://davidmegginson.github.io/ourairports-data/airports.csv'; | |
/** | |
* Whether the JSON output should be minified or not. | |
* | |
* @var bool | |
*/ | |
public bool $minified; | |
/** | |
* @param bool $minified | |
*/ | |
public function __construct( bool $minified = false ) { | |
$this->minified = $minified; | |
add_action( 'wp_ajax_build_coordinates_json', [ $this, 'build_coordinates_json' ] ); | |
add_action( 'wp_ajax_nopriv_build_coordinates_json', [ $this, 'build_coordinates_json' ] ); | |
add_action( 'wp_footer', [ $this, 'coordinates_from_nominatim' ], 20 ); | |
} | |
/** | |
* Get a list of the WordPress timezones and try to guess the default coordinates for each one. | |
* | |
* @param bool $empties Whether it should return the entries without coordinates or not (defaults to `false`, to return the successful entries). | |
* | |
* @return array | |
*/ | |
public function coordinates_from_airports( bool $empties = false ): array { | |
// Get any existing copy of our transient data. | |
$unique_locations = get_transient( 'cached_coordinates_from_airports' ); | |
$empty_timezones = get_transient( 'cached_empty_timezones' ); | |
if ( false === ( $unique_locations ) || false === ( $empty_timezones ) ) { | |
$csv = file( esc_url( self::$airports_csv ) ); | |
if ( empty( $csv ) ) { | |
return []; | |
} | |
// Convert the CSV to an array. | |
$airports = array_map( 'str_getcsv', $csv ); | |
array_walk( $airports, function ( &$a ) use ( $airports ) { | |
$a = array_combine( $airports[0], $a ); | |
} ); | |
array_shift( $airports ); // remove column header. | |
if ( empty( $airports ) ) { | |
return []; | |
} | |
// Get the WordPress timezones and try to match them with a location. | |
$locations = []; | |
$timezones = timezone_identifiers_list(); | |
foreach ( $timezones as $timezone ) { | |
$places = explode( '/', $timezone ); | |
$timezone_continent = ''; | |
if ( count( $places ) >= 2 ) { | |
$timezone_continent = strtoupper( | |
substr( | |
$places[ array_key_last( $places ) - 1 ], | |
0, | |
2 | |
) | |
); | |
} | |
// WordPress and the CSV organize their entries differently, so we need to make some adjustments to the continents. | |
if ( in_array( $timezone_continent, [ 'PA', 'AU' ], true ) ) { | |
$timezone_continent = 'OC'; | |
} | |
if ( $timezone_continent === 'IN' ) { | |
$timezone_continent = 'AS'; | |
} | |
if ( empty( $timezone_continent ) ) { | |
continue; | |
} | |
// Search the airports to find those that match with the timezone name. | |
$name = str_replace( '_', ' ', $places[ array_key_last( $places ) ] ); // Convert names like "New_York" to "New York". | |
$column = array_column( $airports, 'municipality' ); | |
$entries = array_keys( | |
array_combine( | |
array_keys( $airports ), | |
$column | |
), | |
$name | |
); | |
if ( empty( $entries ) ) { | |
continue; | |
} | |
foreach ( $entries as $entry ) { | |
$continent = $airports[ $entry ]['continent'] ?? ''; | |
$name = $airports[ $entry ]['municipality'] ?? ''; | |
$lat = $airports[ $entry ]['latitude_deg'] ?? ''; | |
$lng = $airports[ $entry ]['longitude_deg'] ?? ''; | |
$america = [ 'NA', 'SA' ]; | |
if ( | |
empty( $continent ) || | |
empty( $name ) || | |
empty( $lat ) || | |
empty( $lng ) || | |
// We need to account for the fact that the airports CSV organize America in NA (North America) and SA (South America), while WordPress has a single entry AM (America). | |
( ! in_array( $continent, $america, true ) && $continent !== $timezone_continent ) || | |
( in_array( $continent, $america, true ) && 'AM' !== $timezone_continent ) | |
) { | |
continue; | |
} | |
$locations[] = [ | |
'name' => $name, | |
'timezone' => $timezone, | |
'lat' => $lat, | |
'lng' => $lng, | |
]; | |
} | |
} | |
// Remove duplicates. | |
$unique_locations = []; | |
$empty_timezones = []; | |
foreach ( $timezones as $timezone ) { | |
$places = explode( '/', $timezone ); | |
$name = str_replace( '_', ' ', $places[ array_key_last( $places ) ] ); | |
$column = array_column( $locations, 'name' ); | |
$entry = array_search( $name, $column ); | |
if ( false === $entry || empty( $locations[ $entry ] ) ) { | |
$empty_timezones[] = $timezone; | |
} else { | |
$unique_locations[] = $locations[ $entry ]; | |
} | |
} | |
// Remove the `name` key, as we don't need it anymore. | |
$timezones_with_coordinates = []; | |
foreach ( $unique_locations as $unique_location ) { | |
unset( $unique_location['name'] ); | |
$timezones_with_coordinates[] = $unique_location; | |
} | |
// Store the data to transients. | |
set_transient( 'cached_empty_timezones', $empty_timezones, 12 * HOUR_IN_SECONDS ); | |
set_transient( 'cached_coordinates_from_airports', $timezones_with_coordinates, 12 * HOUR_IN_SECONDS ); | |
} | |
return ( true === $empties ) ? $empty_timezones : $unique_locations; | |
} | |
/** | |
* Runs an AJAX script to take the empty timezones and search for their coordinates using the Nominatim API. | |
* | |
* @return void | |
*/ | |
function coordinates_from_nominatim() { | |
$timezones = $this->coordinates_from_airports( true ); | |
$ajax_url = esc_url( admin_url( 'admin-ajax.php' ) ); | |
?> | |
<script> | |
(function () { | |
const ajaxUrl = <?php echo wp_json_encode( $ajax_url ); ?>; | |
const timezones = <?php echo wp_json_encode( $timezones ); ?>; | |
if (0 === timezones.length) { | |
return; | |
} | |
let i = 1; | |
const locations = []; | |
const failed = []; | |
const lastTimezone = timezones.at(-1); | |
const nominatimApiUrl = (keyword) => { | |
return 'https://nominatim.openstreetmap.org/search?q=' + keyword + '&format=json&limit=1'; | |
} | |
for (const timezone of timezones) { | |
i++; | |
// Apply a delay to each request, to avoid hitting the Nominatim API limits. | |
setTimeout(() => { | |
fetch(nominatimApiUrl(timezone)) | |
.then(response => { | |
if (200 !== response.status) { | |
console.log('%c Bad request for ' + timezone, 'color: red;'); | |
return; | |
} | |
return response.json(); | |
}).then(data => { | |
if (data[0]) { | |
locations.push( | |
{ | |
timezone: timezone, | |
lat: data[0]['lat'], | |
lng: data[0]['lon'] | |
} | |
); | |
console.log('%c' + timezone + ' added', 'color: green;'); | |
} else { | |
console.log('%c' + timezone + ' failed and retrying with a more specific keyword', 'color: orange;'); | |
// If the search fails, repeat with a more specific keyword. | |
const place = timezone.split('/'); | |
const keyword = (2 === place.length) ? place[1] : place[0]; | |
fetch(nominatimApiUrl(keyword)) | |
.then(response => { | |
if (200 !== response.status) { | |
console.log('%c Bad request for ' + timezone, 'color: red;'); | |
return; | |
} | |
return response.json(); | |
}).then(data => { | |
if (data[0]) { | |
locations.push( | |
{ | |
timezone: timezone, | |
lat: data[0]['lat'], | |
lng: data[0]['lon'] | |
} | |
); | |
console.log('%c' + timezone + ' added', 'color: green;'); | |
} else { | |
failed.push(timezone); | |
console.log('%c' + timezone + ' failed', 'color: red;'); | |
} | |
} | |
); | |
} | |
} | |
); | |
if (timezone === lastTimezone) { | |
console.log('%c Finished processing ' + timezones.length + ' timezones (' + failed.length + ' failures).', 'color: lightblue;'); | |
} | |
let formData = new FormData(); | |
formData.append('action', 'build_coordinates_json'); | |
formData.append('locations', JSON.stringify(locations)); | |
fetch(ajaxUrl, { | |
method: 'POST', | |
body: formData | |
}) | |
.then(response => response.text()); | |
}, i * 2000); | |
} | |
})(); | |
</script> | |
<?php | |
} | |
/** | |
* Build the JSON file. | |
* | |
* @return void | |
*/ | |
function build_coordinates_json() { | |
$from_nominatim = json_decode( str_replace( '\\', '', $_POST['locations'] ) ); | |
$from_airports = $this->coordinates_from_airports(); | |
$merged = array_merge( $from_airports, $from_nominatim ?? [] ); | |
$json = wp_json_encode( $merged, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ); | |
if ( true === $this->minified ) { | |
$json = wp_json_encode( $merged, JSON_UNESCAPED_SLASHES ); | |
} | |
file_put_contents( WPMU_PLUGIN_DIR . '/coordinates-for-wordpress-timezones.json', $json ); | |
wp_die(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment