Skip to content

Instantly share code, notes, and snippets.

@gsarig
Last active November 28, 2022 11:22
Show Gist options
  • Save gsarig/6cb47d81a02688ddcb8df719bb8d8db9 to your computer and use it in GitHub Desktop.
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/)
<?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