Exemplo n.º 1
0
 /**
  * Add plugin options to Customizer
  * See: https://developer.wordpress.org/themes/advanced-topics/customizer-api/
  */
 public function customizer_options($wp_customize)
 {
     // Customizer Panel
     $wp_customize->add_panel(self::$customizer_panel_id, array('title' => $this->getPluginDisplayName(), 'description' => esc_html__('Plugin options and settings', 'tk-event-weather')));
     // Customizer Section
     $wp_customize->add_section(self::$customizer_section_id, array('title' => __('Core Settings', 'tk-event-weather'), 'description' => __("Set this plugin's default <a href='https://codex.wordpress.org/Shortcode_API' target='_blank'>shortcode</a> arguments (link opens in new window). Since the plugin operates as a shortcode, you may override the defaults you set here on a per-shortcode basis.", 'tk-event-weather'), 'priority' => 1, 'panel' => self::$customizer_panel_id));
     // Dark Sky API Key
     // https://developer.wordpress.org/reference/functions/sanitize_key/ -- Lowercase alphanumeric characters, dashes and underscores are allowed. -- which matches Dark Sky's API Key pattern
     $wp_customize->add_setting(self::$customizer_flag . '[darksky_api_key]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => '', 'sanitize_callback' => 'sanitize_key'));
     $wp_customize->add_control(self::$customizer_flag . '_darksky_api_key_control', array('label' => esc_html__('Dark Sky API Key', 'tk-event-weather'), 'description' => __('Enter your <a href="https://darksky.net/dev/" target="_blank">Dark Sky API Key</a> (link opens in new window)', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[darksky_api_key]', 'type' => 'password'));
     // Google Maps API Key
     // e.g. AIza...URyTxC0w
     $wp_customize->add_setting(self::$customizer_flag . '[google_maps_api_key]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_google_maps_api_key_control', array('label' => esc_html__('Google Maps Geocoding API Key', 'tk-event-weather'), 'description' => __('Most users will NOT need a <a href="https://developers.google.com/maps/documentation/geocoding/get-api-key" target="_blank">Google Maps Geocoding API Key</a> (link opens in new window) to use the <strong>location</strong> shortcode argument. However, if you want or need to use one, enter it here. When creating an API key: for "Where will you be calling the API from?", choose "Web server (e.g. node.js, Tomcat)".<br>Important Terms are documentend in the Tools tab of the plugin settings page.', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[google_maps_api_key]', 'type' => 'password'));
     // Display Template
     $wp_customize->add_setting(self::$customizer_flag . '[display_template]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_display_template_control', array('label' => esc_html__('Default Display Template', 'tk-event-weather'), 'description' => esc_html__('Choose your default Display Template. If left blank, default will be "Hourly Horizontal".', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[display_template]', 'type' => 'select', 'choices' => TkEventWeather__Functions::valid_display_templates('true')));
     // Past cutoff days
     $wp_customize->add_setting(self::$customizer_flag . '[cutoff_past_days]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => '', 'sanitize_callback' => array('TkEventWeather__Functions', 'sanitize_absint_allow_blank')));
     $wp_customize->add_control(self::$customizer_flag . '_cutoff_past_days_control', array('label' => esc_html__('Past cutoff (in days)', 'tk-event-weather'), 'description' => __('If datetime is this far in the past, do not output the forecast. Enter zero for "no limit".<br>Example: "30" would disable weather more than 30 days in the past.<br>Default: 30', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[cutoff_past_days]', 'type' => 'text'));
     // Future cutoff days
     $wp_customize->add_setting(self::$customizer_flag . '[cutoff_future_days]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => '', 'sanitize_callback' => array('TkEventWeather__Functions', 'sanitize_absint_allow_blank')));
     $wp_customize->add_control(self::$customizer_flag . '_cutoff_future_days_control', array('label' => esc_html__('Future cutoff (in days)', 'tk-event-weather'), 'description' => __('If datetime is this far in the future, do not output the forecast. Enter zero for "no limit".<br>Example: "365" would disable weather more than 1 year in the future.<br>Default: 365', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[cutoff_future_days]', 'type' => 'text'));
     // Units
     $wp_customize->add_setting(self::$customizer_flag . '[darksky_units]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_darksky_units_control', array('label' => esc_html__('Units', 'tk-event-weather'), 'description' => __('Although it is recommended to leave this as "Auto", you may choose to force returning the weather data in specific units.<br>Reference: <a href="https://darksky.net/dev/docs/time-machine" target="_blank">Dark Sky API Docs > Options</a> (link opens in new window)', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[darksky_units]', 'type' => 'select', 'choices' => TkEventWeather__Functions::darksky_option_units('true')));
     // UTC Offset Type
     $wp_customize->add_setting(self::$customizer_flag . '[utc_offset_type]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_utc_offset_type_control', array('label' => esc_html__('UTC Offset Type', 'tk-event-weather'), 'description' => __("In which time zone should hourly times be displayed?<br><strong>From API</strong> means times will be displayed per location. For example, if an event on your site is in New York City, the weather times get displayed in New York City time even if your WordPress time zone is set to Honolulu, Hawaii or UTC-10.<br><strong>From Wordpress</strong> means all weather times display in your WordPress time zone. From the example above, the event in New York City would have its weather displayed in Honolulu time.<br>Default: From API", 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[utc_offset_type]', 'type' => 'select', 'choices' => TkEventWeather__Functions::valid_utc_offset_types('true')));
     // Transient expiration in hours
     $wp_customize->add_setting(self::$customizer_flag . '[transients_expiration_hours]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => '', 'sanitize_callback' => array('TkEventWeather__Functions', 'sanitize_absint_allow_blank')));
     $wp_customize->add_control(self::$customizer_flag . '_transients_expiration_hours_control', array('label' => esc_html__('Dark Sky transient expiration (in hours)', 'tk-event-weather'), 'description' => __('If stored Dark Sky API data is older than this many hours, pull fresh weather data from the API.<br>Default: 12<br>Note: Google Maps Geocoding API transients are always set to 30 days.', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[transients_expiration_hours]', 'type' => 'text'));
     // Disable transients
     $wp_customize->add_setting(self::$customizer_flag . '[transients_off]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_transients_off_control', array('label' => esc_html__('Disable Transients', 'tk-event-weather'), 'description' => __('The <a href="https://codex.wordpress.org/Transients_API" target="_blank">WordPress Transients API</a> (link opens in new window) is used to reduce repetitive API calls and improve performance. Check this box if you wish to disable using Transients (suggested only for testing purposes).<br>Note: Applies to both Dark Sky and Google Maps Geocoding API transients.', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[transients_off]', 'type' => 'checkbox', 'choices' => array('true' => __('Disable', 'tk-event-weather'))));
     // Disable Sunrise/Sunset
     $wp_customize->add_setting(self::$customizer_flag . '[sunrise_sunset_off]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_sunrise_sunset_off_control', array('label' => esc_html__('Disable Sunrise/Sunset', 'tk-event-weather'), 'description' => __('Check this box to disable including sunrise and sunset times into the hourly weather views.', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[sunrise_sunset_off]', 'type' => 'checkbox', 'choices' => array('true' => __('Disable', 'tk-event-weather'))));
     // Disable Plugin Credit Link
     $wp_customize->add_setting(self::$customizer_flag . '[plugin_credit_link_on]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_plugin_credit_link_on_control', array('label' => esc_html__('Enable display of the plugin credit link', 'tk-event-weather'), 'description' => __("<strong>Check this box to turn on</strong> linking to the TK Event Weather plugin's home page. <strong>We sure appreciate it!</strong>", 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[plugin_credit_link_on]', 'type' => 'checkbox', 'choices' => array('true' => __('Enable', 'tk-event-weather'))));
     // Disable Dark Sky Credit Link
     $wp_customize->add_setting(self::$customizer_flag . '[darksky_credit_link_off]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_darksky_credit_link_off_control', array('label' => esc_html__('Disable display of the Dark Sky credit link', 'tk-event-weather'), 'description' => __("Check this box to disable linking to Dark Sky<br>You should not check this box without permission from Dark Sky, per their Terms of Use.", 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[darksky_credit_link_off]', 'type' => 'checkbox', 'choices' => array('true' => __('Disable', 'tk-event-weather'))));
     // Enable Debug Mode
     $wp_customize->add_setting(self::$customizer_flag . '[debug_on]', array('type' => 'option', 'capability' => 'edit_theme_options', 'default' => ''));
     $wp_customize->add_control(self::$customizer_flag . '_debug_on_control', array('label' => esc_html__('Enable Debug Mode for this plugin', 'tk-event-weather'), 'description' => __('Prints extra information to the page only for Administrators.<br>Warning: Likely exposes your API key(s) to all Administrators.', 'tk-event-weather'), 'section' => self::$customizer_section_id, 'settings' => self::$customizer_flag . '[debug_on]', 'type' => 'checkbox', 'choices' => array('true' => __('Enable', 'tk-event-weather'))));
     /*
     					// Icons
     					$wp_customize->add_setting( self::$customizer_flag . '[icons]', array(
     						'type'							=> 'option',
     						'capability'				=> 'edit_theme_options',
     						'default'					=> '',
     					));
     					
     					$wp_customize->add_control( self::$customizer_flag . '_icons_control', array(
     						'label'			=> esc_html__( 'Icons settings', 'tk-event-weather' ),
     						'description' => __( '', 'tk-event-weather' ),
     						'section'		=> self::$customizer_section_id,
     						'settings'		=> self::$customizer_flag . '[sunrise_sunset_off]',
     						'type'				=> 'select',
     						'choices'		=> TkEventWeather__Functions::valid_icon_type( 'true' ),
     					));
     */
 }
 public function handleShortcode($atts)
 {
     $output = '';
     $template_data = array();
     // sent to view template to render output
     $plugin_options = TkEventWeather__Functions::plugin_options();
     if (empty($plugin_options)) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please complete the initial setup');
     } else {
         $api_key_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'darksky_api_key');
         $gmaps_api_key_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'google_maps_api_key');
         $display_template_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'display_template');
         $cutoff_past_days_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'cutoff_past_days', 30);
         $cutoff_future_days_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'cutoff_future_days', 365);
         $units_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'darksky_units', 'auto');
         $transients_off_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'transients_off');
         $transients_expiration_hours_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'transients_expiration_hours', 12);
         $utc_offset_type_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'utc_offset_type');
         $sunrise_sunset_off_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'sunrise_sunset_off');
         //$icons_option = TkEventWeather__Functions::array_get_value_by_key ( $plugin_options, 'icons' );
         $plugin_credit_link_on_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'plugin_credit_link_on');
         $darksky_credit_link_off_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'darksky_credit_link_off');
         $debug_on_option = TkEventWeather__Functions::array_get_value_by_key($plugin_options, 'debug_on');
     }
     /*
      * Required:
      * api_key
      * lat/long
      * start time
      */
     // Attributes
     $defaults = array('api_key' => $api_key_option, 'gmaps_api_key' => $gmaps_api_key_option, 'post_id' => get_the_ID(), 'lat_long' => '', 'lat_long_custom_field' => '', 'lat' => '', 'lat_custom_field' => '', 'long' => '', 'long_custom_field' => '', 'location' => '', 'location_custom_field' => '', 'start_time' => '', 'start_time_custom_field' => '', 'end_time' => '', 'end_time_custom_field' => '', 'cutoff_past' => $cutoff_past_days_option, 'cutoff_future' => $cutoff_future_days_option, 'exclude' => '', 'transients_off' => $transients_off_option, 'transients_expiration' => $transients_expiration_hours_option, 'units' => '', 'utc_offset_type' => $utc_offset_type_option, 'sunrise_sunset_off' => $sunrise_sunset_off_option, 'icons' => '', 'plugin_credit_link_on' => $plugin_credit_link_on_option, 'darksky_credit_link_off' => $darksky_credit_link_off_option, 'class' => '', 'template' => $display_template_option, 'debug_on' => $debug_on_option);
     $atts = shortcode_atts($defaults, $atts, 'tk-event-weather');
     // extract( $atts ); // convert each array item to individual variable
     // load CSS file for Administrators so error messages get styled even if there are no valid shortcodes on page
     // if non-Administrator and no valid shortcode, CSS file will not load
     if (current_user_can('edit_theme_options')) {
         TkEventWeather__Functions::register_css();
         wp_enqueue_style(sanitize_html_class(TkEventWeather__FuncSetup::shortcode_name_hyphenated()));
     }
     // Code
     $debug = (bool) $atts['debug_on'];
     // if false === $transients, clear existing and set new transients
     if (!empty($atts['transients_off']) && 'true' == $atts['transients_off']) {
         $transients = false;
     } else {
         $transients = true;
     }
     // @link https://developer.wordpress.org/reference/functions/sanitize_key/
     $api_key = sanitize_key($atts['api_key']);
     if (empty($api_key)) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please enter your Dark Sky API Key');
     }
     // manually entered override custom field
     $post_id = $atts['post_id'];
     // if post does not exist by ID, clear out $post_id variable
     // @link https://developer.wordpress.org/reference/functions/get_post_status/
     if (!empty($post_id)) {
         if (false === get_post_status($post_id)) {
             // The post does not exist
             $post_id = '';
         }
     }
     $template_data['post_id'] = $post_id;
     // the variable to send to Dark Sky API -- to be built via the code below
     $latitude_longitude = '';
     // only used temporarily if separate lat and long need to be combined into $latitude_longitude
     $latitude = '';
     $longitude = '';
     // combined lat,long (manual then custom field)
     if (!empty($atts['lat_long'])) {
         $latitude_longitude = $atts['lat_long'];
     } elseif (!empty($post_id) && !empty($atts['lat_long_custom_field'])) {
         $latitude_longitude = get_post_meta($post_id, $atts['lat_long_custom_field'], true);
     }
     $latitude_longitude = TkEventWeather__Functions::valid_lat_long($latitude_longitude);
     // if no lat,long yet then build via separate lat and long
     if (empty($latitude_longitude)) {
         // latitude
         if (!empty($atts['lat'])) {
             $latitude = $atts['lat'];
         } elseif (!empty($post_id) && !empty($atts['lat_custom_field'])) {
             $latitude = get_post_meta($post_id, $atts['lat_custom_field'], true);
         }
         // longitude
         if (!empty($atts['long'])) {
             $longitude = $atts['long'];
         } elseif (!empty($post_id) && !empty($atts['long_custom_field'])) {
             $longitude = get_post_meta($post_id, $atts['long_custom_field'], true);
         }
         // build comma-separated $latitude_longitude
         $latitude_longitude = sprintf('%F,%F', $latitude, $longitude);
         $latitude_longitude = TkEventWeather__Functions::valid_lat_long($latitude_longitude);
     }
     // Fetch from Google Maps Geocoding API
     $location = '';
     $location_api_data = '';
     if (empty($latitude_longitude)) {
         if (!empty($atts['location'])) {
             $location = $atts['location'];
         } elseif (!empty($post_id) && !empty($atts['location_custom_field'])) {
             $location = get_post_meta($post_id, $atts['location_custom_field'], true);
         }
         $location = trim($location);
     }
     // Google Maps Transient
     if (!empty($location)) {
         // build transient
         $location_transient_name = sprintf('%s_gmaps_%s', TkEventWeather__FuncSetup::$transient_name_prepend, TkEventWeather__Functions::remove_all_whitespace($location));
         $location_transient_name = TkEventWeather__Functions::sanitize_transient_name($location_transient_name);
         $location_transient_value = TkEventWeather__Functions::transient_get_or_delete($location_transient_name, $transients);
         if (!empty($location_transient_value)) {
             if (!is_object($location_transient_value) || is_wp_error($location_transient_value)) {
                 $location_transient_value = '';
                 delete_transient($location_transient_name);
             }
         }
         if (!empty($location_transient_value)) {
             $location_api_data = $location_transient_value;
         } else {
             // make an API call
             $location_request_uri = sprintf('https://maps.googleapis.com/maps/api/geocode/json?address=%s', urlencode($location));
             $location_request_uri_query_args = array();
             $gmaps_api_key = TkEventWeather__Functions::sanitize_key_allow_uppercase($atts['gmaps_api_key']);
             if (!empty($gmaps_api_key)) {
                 $location_request_uri_query_args['key'] = urlencode($gmaps_api_key);
             }
             if (!empty($location_request_uri_query_args)) {
                 $location_request_uri = add_query_arg($location_request_uri_query_args, $location_request_uri);
             }
             $location_request = wp_safe_remote_get(esc_url_raw($location_request_uri));
             if (is_wp_error($location_request)) {
                 return TkEventWeather__Functions::invalid_shortcode_message('Google Maps Geocoding API request sent but resulted in a WordPress Error. Please troubleshoot');
             }
             // @link https://developer.wordpress.org/reference/functions/wp_remote_retrieve_body/
             $location_body = wp_remote_retrieve_body($location_request);
             if (empty($location_body)) {
                 return TkEventWeather__Functions::invalid_shortcode_message('Google Maps Geocoding API request sent but nothing received. Please troubleshoot');
             }
             $location_api_data = json_decode($location_body);
             if (empty($location_api_data)) {
                 return TkEventWeather__Functions::invalid_shortcode_message('Google Maps Geocoding API response received but some sort of data inconsistency. Please troubleshoot');
             }
             // inside here because if using transient, $location_request will not be set
             if (!empty($debug)) {
                 $output .= sprintf('<!--%1$sTK Event Weather -- Google Maps Geocoding API -- Request URI%1$s%2$s%1$s-->%1$s', PHP_EOL, $location_request_uri);
             }
             /* Example Debug Output:
             <!--
             TK Event Weather -- Google Maps Geocoding API -- Request URI
             https://maps.googleapis.com/maps/api/geocode/json?address=The+White+House
             -->
             */
         }
         // do stuff with Google Maps Geocoding API data
         // see https://developers.google.com/maps/documentation/geocoding/intro#StatusCodes
         if ('OK' != $location_api_data->status) {
             return TkEventWeather__Functions::invalid_shortcode_message('The Google Maps Geocoding API resulted in an error: ' . $location_api_data->status . '. See https://developers.google.com/maps/documentation/geocoding/intro#StatusCodes');
         }
         if (!empty($location_api_data->results[0]->geometry->location->lat)) {
             $latitude = $location_api_data->results[0]->geometry->location->lat;
             $longitude = $location_api_data->results[0]->geometry->location->lng;
         }
         // build comma-separated $latitude_longitude
         $latitude_longitude = sprintf('%F,%F', $latitude, $longitude);
         $latitude_longitude = TkEventWeather__Functions::valid_lat_long($latitude_longitude);
         // now $api_data is set for sure (better be to have gotten this far)
         if (!empty($debug)) {
             $output .= sprintf('<!--%1$sTK Event Weather -- Google Maps Geocoding API -- JSON Data%1$s%2$s%1$s-->%1$s', PHP_EOL, json_encode($location_api_data, JSON_PRETTY_PRINT));
             // requires PHP 5.4
         }
         /* Example Debug Output:
         <!--
         TK Event Weather -- Google Maps Geocoding API -- JSON Data
         {
         		"results": [
         				{
         						"address_components": [
         								{
         										"long_name": "The White House",
         										"short_name": "The White House",
         										"types": [
         												"point_of_interest",
         												"establishment"
         										]
         								},
         								{
         										"long_name": "1600",
         										"short_name": "1600",
         										"types": [
         												"street_number"
         										]
         								},
         								{
         										"long_name": "Pennsylvania Avenue Northwest",
         										"short_name": "Pennsylvania Ave NW",
         										"types": [
         												"route"
         										]
         								},
         								{
         										"long_name": "Northwest Washington",
         										"short_name": "Northwest Washington",
         										"types": [
         												"neighborhood",
         												"political"
         										]
         								},
         								{
         										"long_name": "Washington",
         										"short_name": "Washington",
         										"types": [
         												"locality",
         												"political"
         										]
         								},
         								{
         										"long_name": "District of Columbia",
         										"short_name": "DC",
         										"types": [
         												"administrative_area_level_1",
         												"political"
         										]
         								},
         								{
         										"long_name": "United States",
         										"short_name": "US",
         										"types": [
         												"country",
         												"political"
         										]
         								},
         								{
         										"long_name": "20500",
         										"short_name": "20500",
         										"types": [
         												"postal_code"
         										]
         								}
         						],
         						"formatted_address": "The White House, 1600 Pennsylvania Ave NW, Washington, DC 20500, USA",
         						"geometry": {
         								"location": {
         										"lat": 38.8976763,
         										"lng": -77.0365298
         								},
         								"location_type": "APPROXIMATE",
         								"viewport": {
         										"northeast": {
         												"lat": 38.899025280291,
         												"lng": -77.035180819709
         										},
         										"southwest": {
         												"lat": 38.896327319708,
         												"lng": -77.037878780292
         										}
         								}
         						},
         						"partial_match": true,
         						"place_id": "ChIJ37HL3ry3t4kRv3YLbdhpWXE",
         						"types": [
         								"point_of_interest",
         								"establishment"
         						]
         				}
         		],
         		"status": "OK"
         }
         -->
         */
         // set transient if API call resulted in usable data
         if (true === $transients && !empty($latitude_longitude)) {
             set_transient($location_transient_name, $location_api_data, 30 * DAY_IN_SECONDS);
             // allowed to store for up to 30 calendar days, per https://developers.google.com/maps/terms#10-license-restrictions
         }
     }
     if (empty($latitude_longitude)) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please enter valid Latitude and Longitude coordinates (or a Location that Google Maps can get coordinates for)');
     }
     $template_data['latitude_longitude'] = $latitude_longitude;
     // Start Time
     // ISO 8601 datetime or Unix timestamp
     if ('' != $atts['start_time']) {
         $start_time = $atts['start_time'];
     } elseif (!empty($post_id) && !empty($atts['start_time_custom_field'])) {
         $start_time = get_post_meta($post_id, $atts['lat_long_custom_field'], true);
     } else {
         $start_time = '';
     }
     if ('' != $start_time) {
         // check ISO 8601 first because it's stricter
         if (true === TkEventWeather__Functions::valid_iso_8601_date_time($start_time, 'bool')) {
             $start_time = TkEventWeather__Functions::valid_iso_8601_date_time($start_time);
             $start_time_iso_8601 = $start_time;
             $start_time_timestamp = date('U', strtotime($start_time));
         } elseif (true === TkEventWeather__Functions::valid_timestamp($start_time, 'bool')) {
             $start_time = TkEventWeather__Functions::valid_timestamp($start_time);
             $start_time_iso_8601 = date(DateTime::ATOM, $start_time);
             // DateTime::ATOM is same as 'c'
             $start_time_timestamp = $start_time;
         } else {
             $start_time = '';
         }
     }
     $start_time_timestamp = TkEventWeather__Functions::valid_timestamp($start_time_timestamp);
     if (empty($start_time_timestamp)) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please enter a valid Start Time format');
     }
     $template_data['start_time_timestamp'] = $start_time_timestamp;
     $weather_first_hour_timestamp = TkEventWeather__Functions::timestamp_truncate_minutes($start_time_timestamp);
     $template_data['weather_first_hour_timestamp'] = $weather_first_hour_timestamp;
     // cutoff_past
     // strtotime date relative to $weather_first_hour_timestamp
     if (!empty($atts['cutoff_past'])) {
         $cutoff_past = $atts['cutoff_past'];
     } else {
         $cutoff_past = '';
     }
     if (empty($cutoff_past)) {
         // not set or set to zero (which means "no limit" per plugin option)
         $min_timestamp = '';
     } else {
         if (is_int($cutoff_past)) {
             // if is_int, use that number of NEGATIVE (past) days, per plugin option
             $min_timestamp = strtotime(sprintf('-%d days', absint($cutoff_past)));
         } else {
             // else, use raw input, hopefully formatted correctly (e.g. cutoff_past="2 weeks")
             $min_timestamp = strtotime(esc_html($cutoff_past));
             // returns false on bad input
         }
     }
     $min_timestamp = TkEventWeather__Functions::valid_timestamp($min_timestamp);
     if (!empty($min_timestamp) && '' != $weather_first_hour_timestamp) {
         if ($min_timestamp > $weather_first_hour_timestamp) {
             return TkEventWeather__Functions::invalid_shortcode_message('Event Start Time needs to be more recent than the Past Cutoff Time');
         }
     }
     // max 60 years in the past, per API docs
     if (strtotime('-60 years') > $weather_first_hour_timestamp) {
         return TkEventWeather__Functions::invalid_shortcode_message('Event Start Time needs to be more recent than 60 years in the past, per Dark Sky API docs,');
     }
     // End Time
     // ISO 8601 datetime or Unix timestamp
     if ('' != $atts['end_time']) {
         $end_time = $atts['end_time'];
     } elseif (!empty($post_id) && !empty($atts['end_time_custom_field'])) {
         $end_time = get_post_meta($post_id, $atts['lat_long_custom_field'], true);
     } else {
         $end_time = '';
     }
     if ('' != $end_time) {
         // check ISO 8601 first because it's stricter
         if (true === TkEventWeather__Functions::valid_iso_8601_date_time($end_time, 'bool')) {
             $end_time = TkEventWeather__Functions::valid_iso_8601_date_time($end_time);
             $end_time_iso_8601 = $end_time;
             $end_time_timestamp = TkEventWeather__Functions::valid_timestamp(date('U', strtotime($end_time)));
             // date() returns a string
         } elseif (true === TkEventWeather__Functions::valid_timestamp($end_time, 'bool')) {
             $end_time = TkEventWeather__Functions::valid_timestamp($end_time);
             $end_time_iso_8601 = date(DateTime::ATOM, $end_time);
             // DateTime::ATOM is same as 'c'
             $end_time_timestamp = $end_time;
         } else {
             $end_time = '';
         }
     }
     $end_time_timestamp = TkEventWeather__Functions::valid_timestamp($end_time_timestamp);
     if ('' == $end_time_timestamp) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please enter a valid End Time format');
     }
     // if Event Start and End times are the same
     if ($weather_first_hour_timestamp == $end_time_timestamp) {
         return TkEventWeather__Functions::invalid_shortcode_message('Please make sure Event Start Time and Event End Time are not the same');
     }
     // if Event End time is before Start time
     if ($weather_first_hour_timestamp > $end_time_timestamp) {
         return TkEventWeather__Functions::invalid_shortcode_message('Event Start Time must be earlier than Event End Time');
     }
     $template_data['end_time_timestamp'] = $end_time_timestamp;
     /**
      * $weather_last_hour_timestamp helps with setting 'sunset_to_be_inserted'
      * 
      * if event ends at 7:52pm, set $weather_last_hour_timestamp to 8pm
      * if event ends at 7:00:00pm, set $weather_last_hour_timestamp to 7pm
      * 
      **/
     $end_time_hour_timestamp = TkEventWeather__Functions::timestamp_truncate_minutes($end_time_timestamp);
     // e.g. 7pm instead of 7:52pm
     $end_time_hour_timestamp_plus_one_hour = 3600 + $end_time_hour_timestamp;
     // e.g. 8pm
     if ($end_time_timestamp == $end_time_hour_timestamp) {
         // e.g. event ends at 7:00:00
         $weather_last_hour_timestamp = $end_time_hour_timestamp;
     } else {
         $weather_last_hour_timestamp = $end_time_hour_timestamp_plus_one_hour;
     }
     $template_data['weather_last_hour_timestamp'] = TkEventWeather__Functions::valid_timestamp($weather_last_hour_timestamp);
     //
     // cutoff_future
     // strtotime date relative to $end_time_timestamp
     if (!empty($atts['cutoff_future'])) {
         $cutoff_future = $atts['cutoff_future'];
     } else {
         $cutoff_future = '';
     }
     if (empty($cutoff_future)) {
         // not set or set to zero (which means "no limit" per plugin option)
         $max_timestamp = '';
     } else {
         if (is_int($cutoff_future)) {
             // if is_int, use that number of POSITIVE (future) days, per plugin option
             $max_timestamp = strtotime(sprintf('+%d days', absint($cutoff_future)));
         } else {
             // else, use raw input, hopefully formatted correctly (e.g. cutoff_future="2 weeks")
             $max_timestamp = strtotime(esc_html($cutoff_future));
             // returns false on bad input
         }
     }
     $max_timestamp = TkEventWeather__Functions::valid_timestamp($max_timestamp);
     if (!empty($max_timestamp) && '' != $end_time_timestamp) {
         if ($end_time_timestamp > $max_timestamp) {
             return TkEventWeather__Functions::invalid_shortcode_message('Event End Time needs to be more recent than Future Cutoff Time');
         }
     }
     // max 10 years future, per API docs
     if ($end_time_timestamp > strtotime('+10 years')) {
         return TkEventWeather__Functions::invalid_shortcode_message('Event End Time needs to be less than 10 years in the future, per Dark Sky API docs,');
     }
     //
     // $weather_first_hour_timestamp is equal to or greater than $min_timestamp and $end_time_timestamp is less than or equal to $max_timestamp
     // or $min_timestamp and/or $max_timestamp were set to zero (i.e. no limits)
     // so continue...
     //
     // units
     $units = TkEventWeather__Functions::remove_all_whitespace(strtolower($atts['units']));
     $units_default = apply_filters('tk_event_weather_darksky_units_default', $units_option);
     if (!array_key_exists($units, TkEventWeather__Functions::darksky_option_units())) {
         $units = $units_default;
     }
     // exclude
     $exclude = '';
     $exclude_default = apply_filters('tk_event_weather_darksky_exclude_default', 'minutely,alerts');
     // shortcode argument's value
     $exclude_arg = TkEventWeather__Functions::remove_all_whitespace(strtolower($atts['exclude']));
     if (empty($exclude_arg) || $exclude_default == $exclude_arg) {
         $exclude = $exclude_default;
     } else {
         // array of shortcode argument's value
         $exclude_arg_array = explode(',', $exclude_arg);
         if (is_array($exclude_arg_array)) {
             sort($exclude_arg_array);
             $possible_excludes = TkEventWeather__Functions::darksky_option_exclude();
             foreach ($exclude_arg_array as $key => $value) {
                 // if valid 'exclude' then keep it, else ignore it
                 if (array_key_exists($value, $possible_excludes)) {
                     if (empty($exclude)) {
                         // if first to be added to $exclude
                         $exclude .= $value;
                     } else {
                         // if not first one added to $exclude
                         $exclude .= ',';
                         $exclude .= $value;
                     }
                 } else {
                 }
             }
             // foreach()
         }
         // is_array()
     }
     // else
     // Prepare $exclude_for_transient (first letter of each $exclude, not separated by any character)
     $exclude_for_transient = '';
     $exclude_array = explode(',', $exclude);
     if (is_array($exclude_array)) {
         sort($exclude_array);
         foreach ($exclude_array as $key => $value) {
             if (empty($exclude_for_transient)) {
                 $exclude_for_transient .= substr($value, 0, 1);
             } else {
                 // $exclude_for_transient .= '_';
                 $exclude_for_transient .= substr($value, 0, 1);
             }
         }
     }
     // Transients
     // @link https://codex.wordpress.org/Transients_API
     // @link https://codex.wordpress.org/Easier_Expression_of_Time_Constants
     // build transient
     $transient_name = sprintf('%s_fio_%s_%s_%s_%s_%d', TkEventWeather__FuncSetup::$transient_name_prepend, $units, $exclude_for_transient, substr(strstr($latitude_longitude, ',', true), 0, 6), substr(strstr($latitude_longitude, ',', false), 0, 6), substr($weather_first_hour_timestamp, -5, 5));
     $transient_name = TkEventWeather__Functions::sanitize_transient_name($transient_name);
     $transient_value = TkEventWeather__Functions::transient_get_or_delete($transient_name, $transients);
     // Make API call if nothing from Transients
     // e.g. https://api.darksky.net/forecast/APIKEY/LATITUDE,LONGITUDE,TIME
     // if invalid API key, returns 400 Bad Request
     // API does not want any querying wrapped in brackets, as may be shown in the documentation -- brackets indicates OPTIONAL parameters, not to actually wrap in brackets for your request
     //
     // $api_data->currently is either 'right now' if no TIMESTAMP is part of the API or is the weather for the given TIMESTAMP even if in the past or future (yes, it still is called 'currently')
     //
     /*
     *
     *
     * Example:
     * Weather for the White House on Feb 1 at 4:30pm Eastern Time (as of 2016-01-25T03:01:09-06:00)
     * API call in ISO 8601 format
     * https://api.darksky.net/forecast/_______API_KEY_______/36.281445,-75.794662,2016-02-01T16:30:00-05:00?units=auto&exclude=alerts,daily,flags,hourly,minutely
     * API call in Unix Timestamp format (same result)
     * https://api.darksky.net/forecast/_______API_KEY_______/36.281445,-75.794662,1454362200?units=auto&exclude=alerts,daily,flags,hourly,minutely
     * result:
     <!--
     TK Event Weather JSON Data
     {
     		"latitude": 36.281445,
     		"longitude": -75.794662,
     		"timezone": "America\/New_York",
     		"offset": -5,
     		"currently": {
     	"time": 1454362200,
     	"summary": "Clear",
     	"icon": "clear-day",
     	"precipIntensity": 0.0008,
     	"precipProbability": 0.01,
     	"precipType": "rain",
     	"temperature": 56.9,
     	"apparentTemperature": 56.9,
     	"dewPoint": 48.64,
     	"humidity": 0.74,
     	"windSpeed": 1.91,
     	"windBearing": 163,
     	"cloudCover": 0,
     	"pressure": 1016.99,
     	"ozone": 280.52
     		}
     }
     -->
     
     
     
     
     
     
     // if API doesn't TECHNICALLY error out but does return an API error message
     examples:
     {"code":400,"error":"An invalid time was specified."}
     {"code":400,"error":"An invalid units parameter was provided."}
     *
     *
     */
     if (!empty($transient_value)) {
         $api_data = $transient_value;
         if (empty($api_data)) {
             delete_transient($transient_name);
             // return TkEventWeather__Functions::invalid_shortcode_message( 'Data from Transient used but some sort of data inconsistency. Transient deleted. May or may not need to troubleshoot' );
         }
         if (!empty($api_data->error)) {
             delete_transient($transient_name);
             // return TkEventWeather__Functions::invalid_shortcode_message( 'Data from Transient used but an error: ' . $api_data->error . '. Transient deleted. May or may not need to troubleshoot' );
         }
     }
     // $api_data not yet set because $transient_value was bad so just run through new API call as if transient did not exist (got deleted a few lines above)
     if (empty($api_data)) {
         delete_transient($transient_name);
         // delete any expired transient by this name
         $request_uri = sprintf('https://api.darksky.net/forecast/%s/%s,%s', $api_key, $latitude_longitude, $start_time_timestamp);
         $request_uri_query_args = array();
         if (!empty($units)) {
             $request_uri_query_args['units'] = $units;
         }
         if (!empty($exclude)) {
             $request_uri_query_args['exclude'] = $exclude;
         }
         if (!empty($request_uri_query_args)) {
             $request_uri = add_query_arg($request_uri_query_args, $request_uri);
         }
         // GET STUFF FROM API
         // @link https://codex.wordpress.org/Function_Reference/esc_url_raw
         // @link https://developer.wordpress.org/reference/functions/wp_safe_remote_get/
         $request = wp_safe_remote_get(esc_url_raw($request_uri));
         // @link https://developer.wordpress.org/reference/functions/is_wp_error/
         if (is_wp_error($request)) {
             return TkEventWeather__Functions::invalid_shortcode_message('Dark Sky API request sent but resulted in a WordPress Error. Please troubleshoot');
         }
         // @link https://developer.wordpress.org/reference/functions/wp_remote_retrieve_body/
         $body = wp_remote_retrieve_body($request);
         if (empty($body)) {
             return TkEventWeather__Functions::invalid_shortcode_message('Dark Sky API request sent but nothing received. Please troubleshoot');
         }
         $api_data = json_decode($body);
         if (empty($api_data)) {
             return TkEventWeather__Functions::invalid_shortcode_message('Dark Sky API response received but some sort of data inconsistency. Please troubleshoot');
         }
         if (!empty($api_data->error)) {
             return TkEventWeather__Functions::invalid_shortcode_message('Dark Sky API responded with an error: ' . $api_data->error . ' - Please troubleshoot');
         }
         if (empty($api_data->hourly->data)) {
             return TkEventWeather__Functions::invalid_shortcode_message('Dark Sky API responded but without hourly data. Please troubleshoot');
         }
         // inside here because if using transient, $request will not be set
         if (!empty($debug)) {
             $output .= sprintf('<!--%1$sTK Event Weather -- Dark Sky API -- Request URI%1$s%2$s%1$s-->%1$s', PHP_EOL, $request_uri);
         }
         /* Example Debug Output:
         <!--
         TK Event Weather -- Dark Sky API -- Request URI
         https://api.darksky.net/forecast/___API_KEY___/38.897676,-77.036530,1464604200?units=auto&exclude=minutely,alerts
         -->
         */
         if (true === $transients) {
             $transients_expiration_hours = absint($atts['transients_expiration']);
             if (0 >= $transients_expiration_hours) {
                 $transients_expiration_hours = absint($transients_expiration_hours_option);
             }
             set_transient($transient_name, $api_data, $transients_expiration_hours * HOUR_IN_SECONDS);
             // e.g. 12 hours
         }
     }
     /*
     	Example var_dump($request) when bad data, like https://api.darksky.net/forecast/___API_KEY___/0.000000,0.000000,1466199900?exclude=minutely
     object(stdClass)[100]
     public 'latitude' => int 0
     public 'longitude' => int 0
     public 'timezone' => string 'Etc/GMT' (length=7)
     public 'offset' => int 0
     public 'currently' => 
     	object(stdClass)[677]
     		public 'time' => int 1466199900
     public 'flags' => 
     	object(stdClass)[96]
     		public 'sources' => 
     			array (size=0)
     				empty
     		public 'units' => string 'us' (length=2)
     */
     // now $api_data is set for sure (better be to have gotten this far)
     if (!empty($debug)) {
         $output .= sprintf('<!--%1$sTK Event Weather -- Dark Sky API -- JSON Data%1$s%2$s%1$s-->%1$s', PHP_EOL, json_encode($api_data, JSON_PRETTY_PRINT));
         // requires PHP 5.4
     }
     /* Example Debug Output:
     <!--
     TK Event Weather -- Dark Sky API -- JSON Data
     {
     		"latitude": 38.897676,
     		"longitude": -77.03653,
     		"timezone": "America\/New_York",
     		"offset": -4,
     		"currently": {
     				"time": 1464604200,
     				"summary": "Partly Cloudy",
     				"icon": "partly-cloudy-day",
     				"precipType": "rain",
     				"temperature": 65.66,
     				"temperatureError": 6.32,
     				"apparentTemperature": 65.66,
     				"dewPoint": 58.39,
     				"dewPointError": 5,
     				"humidity": 0.77,
     				"humidityError": 0.14,
     				"windSpeed": 4.08,
     				"windSpeedError": 4.12,
     				"windBearing": 234,
     				"windBearingError": 45.29,
     				"visibility": 9.46,
     				"visibilityError": 6.28,
     				"cloudCover": 0.58,
     				"cloudCoverError": 0.4,
     				"pressure": 1015.95,
     				"pressureError": 5.5
     		},
     		"hourly": {
     				"summary": "Mostly cloudy throughout the day.",
     				"icon": "partly-cloudy-day",
     				"data": [
     						{
     								"time": 1464580800,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 69.52,
     								"temperatureError": 4.89,
     								"apparentTemperature": 69.52,
     								"dewPoint": 59.34,
     								"dewPointError": 4.05,
     								"humidity": 0.7,
     								"humidityError": 0.1,
     								"windSpeed": 4.37,
     								"windSpeedError": 3.09,
     								"windBearing": 193,
     								"windBearingError": 35.28,
     								"visibility": 10,
     								"visibilityError": 4.97,
     								"cloudCover": 0.55,
     								"cloudCoverError": 0.28,
     								"pressure": 1015.86,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464584400,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 68.56,
     								"temperatureError": 4.88,
     								"apparentTemperature": 68.56,
     								"dewPoint": 59.29,
     								"dewPointError": 3.85,
     								"humidity": 0.72,
     								"humidityError": 0.1,
     								"windSpeed": 4.62,
     								"windSpeedError": 3.12,
     								"windBearing": 205,
     								"windBearingError": 34.05,
     								"visibility": 10,
     								"visibilityError": 4.87,
     								"cloudCover": 0.54,
     								"cloudCoverError": 0.29,
     								"pressure": 1015.76,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464588000,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 67.67,
     								"temperatureError": 5.04,
     								"apparentTemperature": 67.67,
     								"dewPoint": 59.2,
     								"dewPointError": 3.86,
     								"humidity": 0.74,
     								"humidityError": 0.1,
     								"windSpeed": 4.63,
     								"windSpeedError": 3.24,
     								"windBearing": 218,
     								"windBearingError": 35,
     								"visibility": 10,
     								"visibilityError": 4.95,
     								"cloudCover": 0.54,
     								"cloudCoverError": 0.31,
     								"pressure": 1015.6,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464591600,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 66.85,
     								"temperatureError": 5.31,
     								"apparentTemperature": 66.85,
     								"dewPoint": 59.05,
     								"dewPointError": 4.03,
     								"humidity": 0.76,
     								"humidityError": 0.11,
     								"windSpeed": 4.45,
     								"windSpeedError": 3.43,
     								"windBearing": 226,
     								"windBearingError": 37.58,
     								"visibility": 10,
     								"visibilityError": 5.16,
     								"cloudCover": 0.54,
     								"cloudCoverError": 0.33,
     								"pressure": 1015.47,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464595200,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 66.14,
     								"temperatureError": 5.63,
     								"apparentTemperature": 66.14,
     								"dewPoint": 58.86,
     								"dewPointError": 4.28,
     								"humidity": 0.77,
     								"humidityError": 0.12,
     								"windSpeed": 4.28,
     								"windSpeedError": 3.65,
     								"windBearing": 230,
     								"windBearingError": 40.47,
     								"visibility": 10,
     								"visibilityError": 5.47,
     								"cloudCover": 0.55,
     								"cloudCoverError": 0.36,
     								"pressure": 1015.43,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464598800,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 65.65,
     								"temperatureError": 5.95,
     								"apparentTemperature": 65.65,
     								"dewPoint": 58.65,
     								"dewPointError": 4.57,
     								"humidity": 0.78,
     								"humidityError": 0.13,
     								"windSpeed": 4.13,
     								"windSpeedError": 3.87,
     								"windBearing": 233,
     								"windBearingError": 43.12,
     								"visibility": 9.87,
     								"visibilityError": 5.81,
     								"cloudCover": 0.56,
     								"cloudCoverError": 0.39,
     								"pressure": 1015.54,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464602400,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 65.5,
     								"temperatureError": 6.22,
     								"apparentTemperature": 65.5,
     								"dewPoint": 58.46,
     								"dewPointError": 4.86,
     								"humidity": 0.78,
     								"humidityError": 0.13,
     								"windSpeed": 4.05,
     								"windSpeedError": 4.05,
     								"windBearing": 234,
     								"windBearingError": 45.05,
     								"visibility": 9.56,
     								"visibilityError": 6.14,
     								"cloudCover": 0.57,
     								"cloudCoverError": 0.4,
     								"pressure": 1015.78,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464606000,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 65.82,
     								"temperatureError": 6.42,
     								"apparentTemperature": 65.82,
     								"dewPoint": 58.33,
     								"dewPointError": 5.15,
     								"humidity": 0.77,
     								"humidityError": 0.14,
     								"windSpeed": 4.12,
     								"windSpeedError": 4.2,
     								"windBearing": 234,
     								"windBearingError": 45.53,
     								"visibility": 9.35,
     								"visibilityError": 6.42,
     								"cloudCover": 0.59,
     								"cloudCoverError": 0.4,
     								"pressure": 1016.12,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464609600,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 66.67,
     								"temperatureError": 6.56,
     								"apparentTemperature": 66.67,
     								"dewPoint": 58.27,
     								"dewPointError": 5.48,
     								"humidity": 0.74,
     								"humidityError": 0.14,
     								"windSpeed": 4.58,
     								"windSpeedError": 4.29,
     								"windBearing": 231,
     								"windBearingError": 43.15,
     								"visibility": 9.3,
     								"visibilityError": 6.64,
     								"cloudCover": 0.61,
     								"cloudCoverError": 0.4,
     								"pressure": 1016.48,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464613200,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 68.07,
     								"temperatureError": 6.67,
     								"apparentTemperature": 68.07,
     								"dewPoint": 58.26,
     								"dewPointError": 5.88,
     								"humidity": 0.71,
     								"humidityError": 0.15,
     								"windSpeed": 6.02,
     								"windSpeedError": 4.36,
     								"windBearing": 234,
     								"windBearingError": 35.88,
     								"visibility": 9.48,
     								"visibilityError": 6.81,
     								"cloudCover": 0.62,
     								"cloudCoverError": 0.38,
     								"pressure": 1016.77,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464616800,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 69.93,
     								"temperatureError": 6.78,
     								"apparentTemperature": 69.93,
     								"dewPoint": 58.24,
     								"dewPointError": 6.42,
     								"humidity": 0.66,
     								"humidityError": 0.15,
     								"windSpeed": 6.84,
     								"windSpeedError": 4.41,
     								"windBearing": 241,
     								"windBearingError": 32.79,
     								"visibility": 9.87,
     								"visibilityError": 6.94,
     								"cloudCover": 0.64,
     								"cloudCoverError": 0.37,
     								"pressure": 1016.89,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464620400,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 72.07,
     								"temperatureError": 6.92,
     								"apparentTemperature": 72.07,
     								"dewPoint": 58.16,
     								"dewPointError": 7.14,
     								"humidity": 0.62,
     								"humidityError": 0.15,
     								"windSpeed": 7.48,
     								"windSpeedError": 4.46,
     								"windBearing": 243,
     								"windBearingError": 30.78,
     								"visibility": 10,
     								"visibilityError": 7.07,
     								"cloudCover": 0.65,
     								"cloudCoverError": 0.35,
     								"pressure": 1016.8,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464624000,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 74.26,
     								"temperatureError": 7.11,
     								"apparentTemperature": 74.26,
     								"dewPoint": 57.96,
     								"dewPointError": 8.07,
     								"humidity": 0.57,
     								"humidityError": 0.16,
     								"windSpeed": 8.09,
     								"windSpeedError": 4.52,
     								"windBearing": 244,
     								"windBearingError": 29.16,
     								"visibility": 10,
     								"visibilityError": 7.21,
     								"cloudCover": 0.66,
     								"cloudCoverError": 0.34,
     								"pressure": 1016.51,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464627600,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 76.22,
     								"temperatureError": 7.33,
     								"apparentTemperature": 76.22,
     								"dewPoint": 57.69,
     								"dewPointError": 9.14,
     								"humidity": 0.53,
     								"humidityError": 0.17,
     								"windSpeed": 8.57,
     								"windSpeedError": 4.58,
     								"windBearing": 243,
     								"windBearingError": 28.13,
     								"visibility": 10,
     								"visibilityError": 7.37,
     								"cloudCover": 0.66,
     								"cloudCoverError": 0.33,
     								"pressure": 1016.05,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464631200,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 77.74,
     								"temperatureError": 7.54,
     								"apparentTemperature": 77.74,
     								"dewPoint": 57.46,
     								"dewPointError": 10.19,
     								"humidity": 0.5,
     								"humidityError": 0.18,
     								"windSpeed": 8.84,
     								"windSpeedError": 4.64,
     								"windBearing": 240,
     								"windBearingError": 27.69,
     								"visibility": 10,
     								"visibilityError": 7.52,
     								"cloudCover": 0.66,
     								"cloudCoverError": 0.34,
     								"pressure": 1015.52,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464634800,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 78.65,
     								"temperatureError": 7.71,
     								"apparentTemperature": 78.65,
     								"dewPoint": 57.39,
     								"dewPointError": 10.97,
     								"humidity": 0.48,
     								"humidityError": 0.18,
     								"windSpeed": 8.87,
     								"windSpeedError": 4.67,
     								"windBearing": 235,
     								"windBearingError": 27.78,
     								"visibility": 10,
     								"visibilityError": 7.63,
     								"cloudCover": 0.66,
     								"cloudCoverError": 0.35,
     								"pressure": 1015.01,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464638400,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 78.87,
     								"temperatureError": 7.76,
     								"apparentTemperature": 78.87,
     								"dewPoint": 57.56,
     								"dewPointError": 11.22,
     								"humidity": 0.48,
     								"humidityError": 0.19,
     								"windSpeed": 8.63,
     								"windSpeedError": 4.65,
     								"windBearing": 228,
     								"windBearingError": 28.31,
     								"visibility": 10,
     								"visibilityError": 7.67,
     								"cloudCover": 0.65,
     								"cloudCoverError": 0.35,
     								"pressure": 1014.64,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464642000,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 78.45,
     								"temperatureError": 7.66,
     								"apparentTemperature": 78.45,
     								"dewPoint": 57.93,
     								"dewPointError": 10.84,
     								"humidity": 0.49,
     								"humidityError": 0.18,
     								"windSpeed": 8.13,
     								"windSpeedError": 4.56,
     								"windBearing": 219,
     								"windBearingError": 29.27,
     								"visibility": 10,
     								"visibilityError": 7.6,
     								"cloudCover": 0.64,
     								"cloudCoverError": 0.36,
     								"pressure": 1014.46,
     								"pressureError": 5.5
     						},
     						{
     								"time": 1464645600,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 77.5,
     								"temperatureError": 7.39,
     								"apparentTemperature": 77.5,
     								"dewPoint": 58.4,
     								"dewPointError": 9.94,
     								"humidity": 0.52,
     								"humidityError": 0.18,
     								"windSpeed": 7.42,
     								"windSpeedError": 4.38,
     								"windBearing": 208,
     								"windBearingError": 30.58,
     								"visibility": 10,
     								"visibilityError": 7.39,
     								"cloudCover": 0.63,
     								"cloudCoverError": 0.35,
     								"pressure": 1014.5,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464649200,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 76.21,
     								"temperatureError": 6.98,
     								"apparentTemperature": 76.21,
     								"dewPoint": 58.84,
     								"dewPointError": 8.73,
     								"humidity": 0.55,
     								"humidityError": 0.17,
     								"windSpeed": 6.69,
     								"windSpeedError": 4.15,
     								"windBearing": 198,
     								"windBearingError": 31.77,
     								"visibility": 10,
     								"visibilityError": 7.04,
     								"cloudCover": 0.61,
     								"cloudCoverError": 0.34,
     								"pressure": 1014.72,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464652800,
     								"summary": "Mostly Cloudy",
     								"icon": "partly-cloudy-day",
     								"precipType": "rain",
     								"temperature": 74.76,
     								"temperatureError": 6.46,
     								"apparentTemperature": 74.76,
     								"dewPoint": 59.18,
     								"dewPointError": 7.44,
     								"humidity": 0.58,
     								"humidityError": 0.15,
     								"windSpeed": 6.09,
     								"windSpeedError": 3.87,
     								"windBearing": 191,
     								"windBearingError": 32.41,
     								"visibility": 10,
     								"visibilityError": 6.59,
     								"cloudCover": 0.6,
     								"cloudCoverError": 0.33,
     								"pressure": 1015.06,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464656400,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 73.31,
     								"temperatureError": 5.92,
     								"apparentTemperature": 73.31,
     								"dewPoint": 59.41,
     								"dewPointError": 6.25,
     								"humidity": 0.62,
     								"humidityError": 0.14,
     								"windSpeed": 5.66,
     								"windSpeedError": 3.58,
     								"windBearing": 187,
     								"windBearingError": 32.32,
     								"visibility": 10,
     								"visibilityError": 6.1,
     								"cloudCover": 0.58,
     								"cloudCoverError": 0.31,
     								"pressure": 1015.41,
     								"pressureError": 5.51
     						},
     						{
     								"time": 1464660000,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 71.97,
     								"temperatureError": 5.43,
     								"apparentTemperature": 71.97,
     								"dewPoint": 59.53,
     								"dewPointError": 5.25,
     								"humidity": 0.65,
     								"humidityError": 0.12,
     								"windSpeed": 5.41,
     								"windSpeedError": 3.33,
     								"windBearing": 187,
     								"windBearingError": 31.62,
     								"visibility": 10,
     								"visibilityError": 5.62,
     								"cloudCover": 0.57,
     								"cloudCoverError": 0.29,
     								"pressure": 1015.7,
     								"pressureError": 5.52
     						},
     						{
     								"time": 1464663600,
     								"summary": "Partly Cloudy",
     								"icon": "partly-cloudy-night",
     								"precipType": "rain",
     								"temperature": 70.78,
     								"temperatureError": 5.06,
     								"apparentTemperature": 70.78,
     								"dewPoint": 59.58,
     								"dewPointError": 4.5,
     								"humidity": 0.68,
     								"humidityError": 0.11,
     								"windSpeed": 5.32,
     								"windSpeedError": 3.16,
     								"windBearing": 188,
     								"windBearingError": 30.7,
     								"visibility": 10,
     								"visibilityError": 5.22,
     								"cloudCover": 0.55,
     								"cloudCoverError": 0.28,
     								"pressure": 1015.87,
     								"pressureError": 5.52
     						}
     				]
     		},
     		"daily": {
     				"data": [
     						{
     								"time": 1464580800,
     								"summary": "Mostly cloudy throughout the day.",
     								"icon": "partly-cloudy-day",
     								"sunriseTime": 1464601606,
     								"sunsetTime": 1464654461,
     								"moonPhase": 0.8,
     								"precipType": "rain",
     								"temperatureMin": 65.5,
     								"temperatureMinError": 6.22,
     								"temperatureMinTime": 1464602400,
     								"temperatureMax": 78.87,
     								"temperatureMaxError": 7.76,
     								"temperatureMaxTime": 1464638400,
     								"apparentTemperatureMin": 65.5,
     								"apparentTemperatureMinTime": 1464602400,
     								"apparentTemperatureMax": 78.87,
     								"apparentTemperatureMaxTime": 1464638400,
     								"dewPoint": 58.54,
     								"dewPointError": 7.19,
     								"humidity": 0.64,
     								"humidityError": 0.15,
     								"windSpeed": 5.78,
     								"windSpeedError": 4.05,
     								"windBearing": 222,
     								"windBearingError": 34.98,
     								"visibility": 10,
     								"visibilityError": 6.49,
     								"cloudCover": 0.6,
     								"cloudCoverError": 0.34,
     								"pressure": 1015.67,
     								"pressureError": 5.5
     						}
     				]
     		},
     		"flags": {
     				"sources": [
     						"isd"
     				],
     				"isd-stations": [
     						"724050-13743",
     						"997314-99999",
     						"999999-13710",
     						"999999-13751",
     						"999999-93725"
     				],
     				"units": "us"
     		}
     }
     -->
     */
     // Build Weather data that we'll use
     // https://developer.wordpress.org/reference/functions/wp_list_pluck/
     $api_data_houly_hours = wp_list_pluck($api_data->hourly->data, 'time');
     $api_data_houly_hours_keys = array_keys($api_data_houly_hours);
     // First hour to start pulling for Hourly Data
     foreach ($api_data_houly_hours as $key => $value) {
         if (intval($value) == intval($weather_first_hour_timestamp)) {
             $weather_hourly_start_key = $key;
             // so we know where to start when pulling hourly weather
             break;
         }
     }
     // Protect against odd hourly weather scenarios like location only having data from midnight to 8am and event start time is 9am
     if (!isset($weather_hourly_start_key)) {
         // need to allow for zero due to numeric array
         return TkEventWeather__Functions::invalid_shortcode_message('Event Start Time error. API did not return enough hourly data. Please troubleshoot');
     }
     // End Time Weather
     foreach ($api_data_houly_hours as $key => $value) {
         if (intval($value) >= intval($end_time_timestamp)) {
             $weather_hourly_end_key = $key;
             break;
         }
     }
     // if none, just get last hour of the day (e.g. if Event End Time is next day 2am, just get 11pm same day as Event Start Time (not perfect but may be better than 2nd API call)
     if (!isset($weather_hourly_end_key)) {
         // need to allow for zero due to numeric array
         $weather_hourly_end_key = end($api_data_houly_hours_keys);
     }
     if (!isset($weather_hourly_end_key)) {
         // need to allow for zero due to numeric array
         return TkEventWeather__Functions::invalid_shortcode_message('Event End Time is out of range. Please troubleshoot');
     }
     // UTC Offset -- only use is when displaying a timestamp
     $utc_offset = TkEventWeather__Functions::remove_all_whitespace(strtolower($atts['utc_offset_type']));
     if ('wp' == $utc_offset) {
         $utc_offset = 'wordpress';
     }
     if (!array_key_exists($utc_offset, TkEventWeather__Functions::valid_utc_offset_types())) {
         $utc_offset = 'api';
     }
     $utc_offset_hours = '';
     if ('api' == $utc_offset) {
         $utc_offset_hours = $api_data->offset;
     }
     $template_data['utc_offset_hours'] = $utc_offset_hours;
     $sunrise_sunset = array('on' => false, 'sunrise_timestamp' => false, 'sunrise_hour_timestamp' => false, 'sunset_timestamp' => false, 'sunset_hour_timestamp' => false, 'sunrise_to_be_inserted' => false, 'sunset_to_be_inserted' => false);
     if (empty($atts['sunrise_sunset_off']) || 'true' != $atts['sunrise_sunset_off']) {
         $sunrise_sunset['on'] = true;
     }
     if (true === $sunrise_sunset['on']) {
         $sunrise_sunset['sunrise_timestamp'] = TkEventWeather__Functions::valid_timestamp($api_data->daily->data[0]->sunriseTime);
         $sunrise_sunset['sunrise_hour_timestamp'] = TkEventWeather__Functions::timestamp_truncate_minutes($sunrise_sunset['sunrise_timestamp']);
         if ($sunrise_sunset['sunrise_timestamp'] >= $weather_first_hour_timestamp) {
             $sunrise_sunset['sunrise_to_be_inserted'] = true;
         }
         $sunrise_sunset['sunset_timestamp'] = TkEventWeather__Functions::valid_timestamp($api_data->daily->data[0]->sunsetTime);
         $sunrise_sunset['sunset_hour_timestamp'] = TkEventWeather__Functions::timestamp_truncate_minutes($sunrise_sunset['sunset_timestamp']);
         if ($weather_last_hour_timestamp >= $sunrise_sunset['sunset_timestamp']) {
             $sunrise_sunset['sunset_to_be_inserted'] = true;
         }
     }
     $template_data['sunrise_sunset'] = $sunrise_sunset;
     // Icons
     $icons = $atts['icons'];
     if ('climacons' == $icons) {
         $icons = 'climacons_font';
     }
     if (empty($icons) || !in_array($icons, TkEventWeather__Functions::valid_icon_type())) {
         $icons = 'climacons_font';
     }
     // enqueue CSS file if using Climacons Icon Font
     if ('climacons_font' == $icons) {
         TkEventWeather__Functions::register_climacons_css();
         wp_enqueue_style('tkeventw-climacons');
     }
     // Hourly Weather
     // any internal pointers to reset first?
     $weather_hourly = array();
     $index = $weather_hourly_start_key;
     if (is_integer($index)) {
         foreach ($api_data->hourly->data as $key => $value) {
             if ($key > $weather_hourly_end_key) {
                 break;
             }
             if ($index == $key) {
                 $weather_hourly[$index] = $value;
                 $index++;
             }
         }
     }
     //$weather_hourly = TkEventWeather__Functions::sort_multidim_array_by_sub_key( $weather_hourly, 'time' );
     $template_data['weather_hourly'] = $weather_hourly;
     // Get Low and High from Hourly
     // https://developer.wordpress.org/reference/functions/wp_list_pluck/
     $weather_hourly_temperatures = wp_list_pluck($weather_hourly, 'temperature');
     // if nothing, will be an empty array
     $template_data['weather_hourly_temperatures'] = $weather_hourly_temperatures;
     $weather_hourly_high = '';
     if (!empty($weather_hourly_temperatures) && is_array($weather_hourly_temperatures)) {
         $weather_hourly_high = max($weather_hourly_temperatures);
     }
     $template_data['weather_hourly_high'] = $weather_hourly_high;
     $weather_hourly_low = '';
     if (!empty($weather_hourly_temperatures) && is_array($weather_hourly_temperatures)) {
         $weather_hourly_low = min($weather_hourly_temperatures);
     }
     $template_data['weather_hourly_low'] = $weather_hourly_low;
     $temperature_units = TkEventWeather__Functions::temperature_units($api_data->flags->units);
     $template_data['temperature_units'] = $temperature_units;
     $wind_speed_units = TkEventWeather__Functions::wind_speed_units($api_data->flags->units);
     $template_data['wind_speed_units'] = $wind_speed_units;
     // class
     $class = sanitize_html_class($atts['class']);
     if (!empty($class)) {
         $class = ' ' . $class;
     }
     $display_template = TkEventWeather__Functions::remove_all_whitespace(strtolower($atts['template']));
     if (!array_key_exists($display_template, TkEventWeather__Functions::valid_display_templates())) {
         $display_template = 'hourly_horizontal';
     }
     $template_data['template'] = $display_template;
     $template_class_name = TkEventWeather__Functions::template_class_name($display_template);
     $template_data['template_class_name'] = $template_class_name;
     // if Debug Mode is true, set $debug_vars to true for admins only
     if (!empty($debug) && current_user_can('edit_theme_options')) {
         // var_dump( get_defined_vars() ); // uncomment if you REALLY want to display this information
     }
     TkEventWeather__Functions::register_css();
     wp_enqueue_style(sanitize_html_class(TkEventWeather__FuncSetup::shortcode_name_hyphenated()));
     /**
      * Start Building Output!!!
      * All data should be set by now!!!
      */
     // cannot do <style> tags inside template because it will break any open div (e.g. wrapper div)
     $output .= sprintf('<div class="tk-event-weather__wrapper%s">', $class);
     $output .= PHP_EOL;
     $output .= sprintf('<div class="tk-event-weather-template %s">', $template_data['template_class_name']);
     $output .= PHP_EOL;
     // https://github.com/GaryJones/Gamajo-Template-Loader/issues/13#issuecomment-196046201
     ob_start();
     TkEventWeather__Functions::load_template($display_template, $template_data);
     $output .= ob_get_clean();
     if ('true' == $atts['plugin_credit_link_on']) {
         $output .= TkEventWeather__Functions::plugin_credit_link();
     }
     if (empty($atts['darksky_credit_link_off'])) {
         $output .= TkEventWeather__Functions::darksky_credit_link();
     }
     $output .= '</div>';
     // .tk-event-weather-template
     $output .= PHP_EOL;
     $output .= '</div>';
     // .tk-event-weather--wrapper
     $output .= PHP_EOL;
     return $output;
 }