/, with its card, a map with
* its pin, its week of hours and anything written about it, and schema.org LocalBusiness data so search engines
* read the address, the point and the hours. /locations/ itself is a page with the store locator, made as a draft
* when the pages are turned on.
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** Whether locations have pages of their own. */
function deltabreen_maps_location_pages() {
return ! empty( deltabreen_maps_options()['location_pages'] );
}
/** The part of a location page's address before its name. */
function deltabreen_maps_location_slug() {
/**
* The part of a location page's address before its name, "locations" unless changed.
*
* @param string $slug The slug.
*/
return sanitize_title( apply_filters( 'deltabreen_maps_location_slug', 'locations' ) );
}
// Turning the pages on or off changes the site's addresses: rebuild them once, on the next request.
add_action(
'update_option_deltabreen_maps',
function ( $old, $new ) {
$was = ! empty( $old['location_pages'] );
$now = ! empty( $new['location_pages'] );
if ( $was === $now ) {
return;
}
update_option( 'deltabreen_maps_flush', 1, false );
deltabreen_maps_bump_pins();
if ( $now ) {
deltabreen_maps_ensure_locations_page();
}
},
10,
2
);
add_action(
'init',
function () {
if ( get_option( 'deltabreen_maps_flush' ) ) {
delete_option( 'deltabreen_maps_flush' );
flush_rewrite_rules();
}
},
99
);
/** A draft page at /locations/ with the store locator, unless the site has a page there already. */
function deltabreen_maps_ensure_locations_page() {
$slug = deltabreen_maps_location_slug();
if ( get_page_by_path( $slug, OBJECT, 'page' ) ) {
return;
}
$patterns = deltabreen_maps_patterns();
$page = wp_insert_post(
array(
'post_type' => 'page',
'post_status' => 'draft',
'post_name' => $slug,
'post_title' => __( 'Our locations', 'maps-by-deltabreen' ),
'post_content' => $patterns['locator']['content'],
)
);
if ( $page && ! is_wp_error( $page ) ) {
set_transient( 'deltabreen_maps_locations_page', (int) $page, DAY_IN_SECONDS );
}
}
add_action(
'admin_notices',
function () {
$page = (int) get_transient( 'deltabreen_maps_locations_page' );
if ( ! $page || ! current_user_can( 'edit_pages' ) || 'draft' !== get_post_status( $page ) ) {
return;
}
printf(
'
',
esc_html__( 'Each location has a page of its own now. A draft page with a store locator is ready to list them all:', 'maps-by-deltabreen' ),
esc_url( (string) get_edit_post_link( $page ) ),
esc_html__( 'open it and publish', 'maps-by-deltabreen' )
);
}
);
// A location's page: its card, a map with its pin and its week, before whatever is written about it.
add_filter(
'the_content',
function ( $content ) {
if ( ! is_singular( 'deltabreen_location' ) || ! in_the_loop() || ! is_main_query() || ! deltabreen_maps_location_pages() ) {
return $content;
}
$id = get_the_ID();
$place = deltabreen_maps_location_view( $id );
if ( ! $place ) {
return $content;
}
$block = function ( $name, $attrs ) {
return render_block(
array(
'blockName' => $name,
'attrs' => $attrs,
'innerBlocks' => array(),
'innerHTML' => '',
'innerContent' => array(),
)
);
};
$map = '';
if ( null !== $place['lat'] ) {
$from = deltabreen_maps_location_pins( array() );
$pins = array_values(
array_filter(
$from['pins'],
function ( $p ) use ( $id ) {
return ( $p['id'] ?? '' ) === 'loc-' . $id;
}
)
);
$map = deltabreen_maps_render(
array(
'pins' => $pins,
'center' => $place['lng'] . ',' . $place['lat'],
'zoom' => '15',
'height' => '360px',
)
);
}
$html = ''
. $block( 'deltabreen/location-card', array( 'location' => $id, 'showPhoto' => true, 'showHours' => true, 'showTitle' => false ) )
. $map
. $block( 'deltabreen/hours', array( 'location' => $id, 'showWeek' => true ) )
. '
';
/**
* What a location's page shows before its own text.
*
* @param string $html The card, the map and the week.
* @param WP_Post $post The location.
*/
return apply_filters( 'deltabreen_maps_location_page', $html, get_post( $id ) ) . $content;
},
20
);
/**
* Hours in the embed's part of OpenStreetMap's syntax as minutes after midnight: each weekday's ranges, Monday first,
* and the dated days; null when unreadable (assets/hours-view.js reads the same).
*
* @param string $text Such as "Mo-Fr 09:00-17:30; Sa 10:00-14:00; Su off; Dec 25 off".
* @return array{week: array>, dated: array}|null
*/
function deltabreen_maps_parse_hours( $text ) {
$text = trim( (string) $text );
if ( '' === $text || false !== strpos( $text, '||' ) ) {
return null;
}
$week = array_fill( 0, 7, array() );
if ( '24/7' === $text ) {
return array(
'week' => array_fill( 0, 7, array( array( 0, 1440 ) ) ),
'dated' => array(),
);
}
$days = array( 'mo', 'tu', 'we', 'th', 'fr', 'sa', 'su' );
$months = array( 'jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec' );
$rest = function ( $s ) {
$s = trim( $s );
if ( preg_match( '/^(off|closed)$/i', $s ) ) {
return array();
}
if ( '24/7' === $s ) {
return array( array( 0, 1440 ) );
}
$out = array();
foreach ( '' === $s ? array( 'x' ) : explode( ',', $s ) as $part ) {
if ( ! preg_match( '/^\s*(\d{1,2}):(\d{2})\s*-\s*(\d{1,2}):(\d{2})\s*$/', $part, $m ) || (int) $m[1] > 24 || (int) $m[3] > 24 ) {
return null;
}
$open = (int) $m[1] * 60 + (int) $m[2];
$close = (int) $m[3] * 60 + (int) $m[4];
$out[] = array( $open, $close <= $open ? $close + 1440 : $close );
}
return $out;
};
$dated = array();
foreach ( array_filter( array_map( 'trim', preg_split( '/[;\n]/', $text ) ) ) as $rule ) {
if ( preg_match( '/^(?:(\d{4})\s+)?(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s+(\d{1,2})(?:\s*-\s*(\d{1,2}))?(?:\s+(.*))?$/i', $rule, $d ) ) {
$ranges = $rest( $d[5] ?? '' );
$from = (int) $d[3];
$to = isset( $d[4] ) && '' !== $d[4] ? (int) $d[4] : $from;
if ( null === $ranges || $from < 1 || $to > 31 || $to < $from ) {
return null;
}
for ( $day = $from; $day <= $to; $day++ ) {
$dated[] = array(
'year' => '' !== $d[1] ? (int) $d[1] : null,
'month' => array_search( strtolower( $d[2] ), $months, true ) + 1,
'day' => $day,
'ranges' => $ranges,
);
}
continue;
}
if ( ! preg_match( '/^((?:Mo|Tu|We|Th|Fr|Sa|Su|PH|SH)(?:\s*[-,]\s*(?:Mo|Tu|We|Th|Fr|Sa|Su|PH|SH))*)?\s*(.*)$/i', $rule, $m ) ) {
return null;
}
$which = array();
if ( '' === ( $m[1] ?? '' ) ) {
$which = range( 0, 6 );
} else {
foreach ( explode( ',', preg_replace( '/\s+/', '', strtolower( $m[1] ) ) ) as $part ) {
$ends = explode( '-', $part );
if ( in_array( $ends[0], array( 'ph', 'sh' ), true ) && 1 === count( $ends ) ) {
continue;
}
$a = array_search( $ends[0], $days, true );
$b = isset( $ends[1] ) ? array_search( $ends[1], $days, true ) : $a;
if ( false === $a || false === $b || count( $ends ) > 2 ) {
return null;
}
for ( $n = $a; ; $n = ( $n + 1 ) % 7 ) {
$which[] = $n;
if ( $n === $b ) {
break;
}
}
}
}
$ranges = $rest( $m[2] ?? '' );
if ( null === $ranges ) {
return null;
}
foreach ( $which as $n ) {
$week[ $n ] = $ranges;
}
}
return array(
'week' => $week,
'dated' => $dated,
);
}
/** A minute after midnight as schema.org writes a time; midnight at the end of a day is 23:59. */
function deltabreen_maps_schema_time( $minute ) {
$minute = $minute >= 1440 ? ( 1440 === $minute ? 1439 : $minute - 1440 ) : $minute;
return sprintf( '%02d:%02d', intdiv( $minute, 60 ), $minute % 60 );
}
/**
* A location as schema.org LocalBusiness data: name, address, point, phone, page, photo and opening hours, with a
* temporary closure and special days as special opening hours.
*
* @param int $post_id The location.
* @return array|null
*/
function deltabreen_maps_location_schema( $post_id ) {
$place = deltabreen_maps_location_view( $post_id );
if ( ! $place ) {
return null;
}
$get = function ( $field ) use ( $post_id ) {
return trim( (string) get_post_meta( $post_id, '_deltabreen_' . $field, true ) );
};
$data = array(
'@context' => 'https://schema.org',
'@type' => 'LocalBusiness',
'name' => $place['title'],
'url' => get_permalink( $post_id ),
);
$street = array_filter(
array(
'@type' => 'PostalAddress',
'streetAddress' => $get( 'address' ),
'addressLocality' => $get( 'city' ),
'postalCode' => $get( 'postcode' ),
'addressCountry' => $get( 'country' ),
)
);
if ( count( $street ) > 1 ) {
$data['address'] = $street;
}
if ( null !== $place['lat'] ) {
$data['geo'] = array(
'@type' => 'GeoCoordinates',
'latitude' => $place['lat'],
'longitude' => $place['lng'],
);
}
if ( '' !== $place['phone'] ) {
$data['telephone'] = $place['phone'];
}
if ( '' !== $place['url'] ) {
$data['sameAs'] = $place['url'];
}
$photo = '' !== $place['photo'] ? $place['photo'] : (string) get_the_post_thumbnail_url( $post_id, 'large' );
if ( '' !== $photo ) {
$data['image'] = $photo;
}
$hours = deltabreen_maps_parse_hours( $place['hours'] );
if ( $hours ) {
$names = array( 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' );
$specs = array();
foreach ( $hours['week'] as $n => $ranges ) {
foreach ( $ranges as $r ) {
$at = deltabreen_maps_schema_time( $r[0] ) . '-' . deltabreen_maps_schema_time( $r[1] );
if ( ! isset( $specs[ $at ] ) ) {
$specs[ $at ] = array(
'@type' => 'OpeningHoursSpecification',
'dayOfWeek' => array(),
'opens' => deltabreen_maps_schema_time( $r[0] ),
'closes' => deltabreen_maps_schema_time( $r[1] ),
);
}
$specs[ $at ]['dayOfWeek'][] = $names[ $n ];
}
}
if ( $specs ) {
$data['openingHoursSpecification'] = array_values( $specs );
}
$special = array();
$year = (int) wp_date( 'Y' );
foreach ( $hours['dated'] as $d ) {
$on = sprintf( '%04d-%02d-%02d', $d['year'] ?? $year, $d['month'], $d['day'] );
$times = $d['ranges'] ? $d['ranges'] : array( array( 0, 0 ) );
foreach ( $times as $r ) {
$special[] = array(
'@type' => 'OpeningHoursSpecification',
'validFrom' => $on,
'validThrough' => $on,
'opens' => deltabreen_maps_schema_time( $r[0] ),
'closes' => deltabreen_maps_schema_time( $r[1] ),
);
}
}
if ( '' !== $place['closed_until'] ) {
$special[] = array(
'@type' => 'OpeningHoursSpecification',
'validFrom' => wp_date( 'Y-m-d' ),
'validThrough' => $place['closed_until'],
'opens' => '00:00',
'closes' => '00:00',
);
}
if ( $special ) {
$data['specialOpeningHoursSpecification'] = $special;
}
}
/**
* A location's schema.org data, before it is printed on its page.
*
* @param array $data The LocalBusiness object.
* @param int $post_id The location.
*/
return apply_filters( 'deltabreen_maps_location_schema', $data, $post_id );
}
add_action(
'wp_head',
function () {
if ( ! is_singular( 'deltabreen_location' ) || ! deltabreen_maps_location_pages() ) {
return;
}
$data = deltabreen_maps_location_schema( get_queried_object_id() );
if ( $data ) {
echo wp_get_inline_script_tag( wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ), array( 'type' => 'application/ld+json' ) ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON with every < and > escaped.
}
}
);