/, and may have words and a featured image. $pages = ! empty( deltabreen_maps_options()['location_pages'] ); register_post_type( 'deltabreen_location', array( 'labels' => array( 'name' => __( 'Map locations', 'maps-by-deltabreen' ), 'singular_name' => __( 'Map location', 'maps-by-deltabreen' ), 'add_new' => __( 'Add location', 'maps-by-deltabreen' ), 'add_new_item' => __( 'Add location', 'maps-by-deltabreen' ), 'edit_item' => __( 'Edit location', 'maps-by-deltabreen' ), 'new_item' => __( 'New location', 'maps-by-deltabreen' ), 'search_items' => __( 'Search locations', 'maps-by-deltabreen' ), 'not_found' => __( 'No locations yet.', 'maps-by-deltabreen' ), 'not_found_in_trash' => __( 'No locations in the bin.', 'maps-by-deltabreen' ), 'all_items' => __( 'All locations', 'maps-by-deltabreen' ), 'menu_name' => __( 'Map locations', 'maps-by-deltabreen' ), ), 'public' => $pages, 'publicly_queryable' => $pages, 'exclude_from_search' => ! $pages, 'show_in_nav_menus' => $pages, 'has_archive' => false, 'rewrite' => $pages ? array( 'slug' => deltabreen_maps_location_slug(), 'with_front' => false ) : false, 'show_ui' => true, 'show_in_menu' => true, 'menu_icon' => 'dashicons-location-alt', 'supports' => $pages ? array( 'title', 'editor', 'thumbnail', 'excerpt' ) : array( 'title' ), 'show_in_rest' => false, 'capability_type' => 'post', 'map_meta_cap' => true, ) ); register_taxonomy( 'deltabreen_location_cat', 'deltabreen_location', array( 'labels' => array( 'name' => __( 'Location categories', 'maps-by-deltabreen' ), 'singular_name' => __( 'Location category', 'maps-by-deltabreen' ), 'add_new_item' => __( 'Add category', 'maps-by-deltabreen' ), 'edit_item' => __( 'Edit category', 'maps-by-deltabreen' ), 'menu_name' => __( 'Categories', 'maps-by-deltabreen' ), ), 'public' => false, 'show_ui' => true, 'show_admin_column' => true, 'hierarchical' => true, 'show_in_rest' => false, ) ); } ); /** Every location category, empty ones included. */ function deltabreen_maps_categories() { $terms = get_terms( array( 'taxonomy' => 'deltabreen_location_cat', 'hide_empty' => false, ) ); return is_wp_error( $terms ) ? array() : $terms; } /** A category's pin colour, or empty for the default. */ function deltabreen_maps_category_color( $term_id ) { return deltabreen_maps_clean_color( get_term_meta( $term_id, 'deltabreen_color', true ) ); } /** A category's own pin icon (an image URL), or empty for the coloured pin. It draws on Lite and above. */ function deltabreen_maps_category_icon( $term_id ) { return deltabreen_maps_clean_link( get_term_meta( $term_id, 'deltabreen_icon', true ) ); } /** A category's built-in pin symbol, or empty for none. */ function deltabreen_maps_category_glyph( $term_id ) { return deltabreen_maps_clean_glyph( get_term_meta( $term_id, 'deltabreen_glyph', true ) ); } /** The symbol picker's choices: none, then each built-in symbol by its name. */ function deltabreen_maps_glyph_options( $selected, $none ) { $html = ''; foreach ( DELTABREEN_MAPS_GLYPHS as $glyph ) { $html .= ''; } return $html; } /** "all" or empty for every category, else category slugs from a comma-separated list. */ function deltabreen_maps_category_list( $raw ) { $raw = trim( (string) $raw ); if ( '' === $raw || 'all' === $raw ) { return array(); } return array_values( array_filter( array_map( 'sanitize_title', explode( ',', $raw ) ) ) ); } /** A location's address on one line: street, town, postcode, country. */ function deltabreen_maps_location_address( $post_id ) { $parts = array(); foreach ( array( 'address', 'city', 'postcode', 'country' ) as $field ) { $v = trim( (string) get_post_meta( $post_id, '_deltabreen_' . $field, true ) ); if ( '' !== $v ) { $parts[] = $v; } } return implode( ', ', $parts ); } /** One day's hours: ranges such as 09:00-17:30 or 09:00-13:00,14:00-18:00, "off", or empty when not given. */ function deltabreen_maps_clean_day( $v ) { $v = strtolower( trim( is_scalar( $v ) ? (string) $v : '' ) ); if ( '' === $v ) { return ''; } if ( in_array( $v, array( 'closed', 'off', '-', strtolower( __( 'closed', 'maps-by-deltabreen' ) ) ), true ) ) { return 'off'; } $v = preg_replace( '/\s+/', '', $v ); $range = '\d{1,2}:\d{2}-\d{1,2}:\d{2}'; return preg_match( '/^' . $range . '(,' . $range . ')*$/', $v ) ? $v : ''; } /** Hours text the map can read: at most 200 characters, cut after a whole rule rather than inside one. */ function deltabreen_maps_clean_hours_text( $v ) { $v = deltabreen_maps_clean_text( $v, 1000 ); if ( strlen( $v ) <= 200 ) { return $v; } $cut = strrpos( substr( $v, 0, 201 ), ';' ); return false === $cut ? '' : rtrim( substr( $v, 0, $cut ) ); } /** A location's opening hours as the embed reads them: from the seven days if any is filled, else the one-line form. */ function deltabreen_maps_location_hours( $post_id ) { $days = get_post_meta( $post_id, '_deltabreen_hours', true ); $days = is_array( $days ) ? $days : array(); if ( array_filter( $days ) ) { $rules = array(); foreach ( DELTABREEN_MAPS_DAYS as $d ) { $v = isset( $days[ $d ] ) && '' !== $days[ $d ] ? $days[ $d ] : 'off'; $rules[] = $d . ' ' . $v; } return implode( '; ', $rules ); } return (string) get_post_meta( $post_id, '_deltabreen_hours_text', true ); } /** * Check and store a location's details; shared by the edit screen and the import. * * @param int $post_id The location. * @param array $in address, city, postcode, country, lng, lat, phone, url, notes, hours (days), hours_text, * photo, button, button_url, closed_until, notice, glyph. */ function deltabreen_maps_save_location_meta( $post_id, $in ) { $text = array( 'address' => 200, 'city' => 100, 'postcode' => 20, 'country' => 60, 'notes' => 500, ); foreach ( $text as $field => $max ) { if ( array_key_exists( $field, $in ) ) { update_post_meta( $post_id, '_deltabreen_' . $field, deltabreen_maps_clean_text( $in[ $field ], $max ) ); } } if ( array_key_exists( 'lng', $in ) || array_key_exists( 'lat', $in ) ) { $lng = isset( $in['lng'] ) && is_numeric( $in['lng'] ) ? (float) $in['lng'] : null; $lat = isset( $in['lat'] ) && is_numeric( $in['lat'] ) ? (float) $in['lat'] : null; if ( null !== $lng && null !== $lat && abs( $lng ) <= 180 && abs( $lat ) <= 90 ) { update_post_meta( $post_id, '_deltabreen_lng', $lng ); update_post_meta( $post_id, '_deltabreen_lat', $lat ); } else { delete_post_meta( $post_id, '_deltabreen_lng' ); delete_post_meta( $post_id, '_deltabreen_lat' ); } } if ( array_key_exists( 'phone', $in ) ) { update_post_meta( $post_id, '_deltabreen_phone', deltabreen_maps_clean_phone( $in['phone'] ) ); } foreach ( array( 'url', 'photo', 'button_url' ) as $field ) { if ( array_key_exists( $field, $in ) ) { update_post_meta( $post_id, '_deltabreen_' . $field, deltabreen_maps_clean_link( $in[ $field ] ) ); } } if ( array_key_exists( 'button', $in ) ) { update_post_meta( $post_id, '_deltabreen_button', deltabreen_maps_clean_text( $in['button'], 40 ) ); } if ( array_key_exists( 'hours', $in ) ) { $days = array(); $raw = is_array( $in['hours'] ) ? $in['hours'] : array(); foreach ( DELTABREEN_MAPS_DAYS as $d ) { $days[ $d ] = deltabreen_maps_clean_day( $raw[ $d ] ?? '' ); } update_post_meta( $post_id, '_deltabreen_hours', $days ); } if ( array_key_exists( 'hours_text', $in ) ) { update_post_meta( $post_id, '_deltabreen_hours_text', deltabreen_maps_clean_hours_text( $in['hours_text'] ) ); } if ( array_key_exists( 'closed_until', $in ) ) { update_post_meta( $post_id, '_deltabreen_closed_until', deltabreen_maps_clean_date( $in['closed_until'] ) ); } if ( array_key_exists( 'notice', $in ) ) { update_post_meta( $post_id, '_deltabreen_notice', deltabreen_maps_clean_text( $in['notice'], 160 ) ); } if ( array_key_exists( 'glyph', $in ) ) { update_post_meta( $post_id, '_deltabreen_glyph', deltabreen_maps_clean_glyph( $in['glyph'] ) ); } deltabreen_maps_bump_pins(); } /** * One published location's details for a block: name, address, contact, photo, hours, closure and point; null when it * is not a published location. * * @param int $post_id The location. * @return array|null */ function deltabreen_maps_location_view( $post_id ) { $post = get_post( (int) $post_id ); if ( ! $post || 'deltabreen_location' !== $post->post_type || 'publish' !== $post->post_status ) { return null; } $get = function ( $field ) use ( $post ) { return (string) get_post_meta( $post->ID, '_deltabreen_' . $field, true ); }; $lng = $get( 'lng' ); $lat = $get( 'lat' ); return array( 'id' => $post->ID, 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES, 'UTF-8' ), 'address' => deltabreen_maps_location_address( $post->ID ), 'phone' => deltabreen_maps_clean_phone( $get( 'phone' ) ), 'url' => deltabreen_maps_clean_link( $get( 'url' ) ), 'photo' => deltabreen_maps_clean_link( $get( 'photo' ) ), 'hours' => deltabreen_maps_location_hours( $post->ID ), 'closed_until' => deltabreen_maps_clean_date( $get( 'closed_until' ) ), 'notice' => deltabreen_maps_clean_text( $get( 'notice' ), 160 ), 'lng' => is_numeric( $lng ) && is_numeric( $lat ) ? (float) $lng : null, 'lat' => is_numeric( $lng ) && is_numeric( $lat ) ? (float) $lat : null, ); } /** Directions to a point in Google Maps and in Apple Maps. */ function deltabreen_maps_directions_links( $lat, $lng ) { $to = rawurlencode( $lat . ',' . $lng ); return array( 'google' => 'https://www.google.com/maps/dir/?api=1&destination=' . $to, 'apple' => 'https://maps.apple.com/?daddr=' . $to, ); } /** The attributes the blocks' view script reads a place's hours from, escaped. */ function deltabreen_maps_hours_attrs( $place ) { $attrs = array( 'data-hours' => $place['hours'], 'data-timezone' => deltabreen_maps_timezone(), 'data-closed-until' => $place['closed_until'], 'data-locale' => str_replace( '_', '-', determine_locale() ), ); $html = 'data-deltabreen-hours'; foreach ( $attrs as $name => $value ) { if ( '' !== $value ) { $html .= ' ' . $name . '="' . esc_attr( $value ) . '"'; } } return $html; } /** Invalidate every cached list of location pins. */ function deltabreen_maps_bump_pins() { update_option( 'deltabreen_maps_pins_version', (int) get_option( 'deltabreen_maps_pins_version', 1 ) + 1, false ); } /** * The published locations as the embed's pins, in the given categories (all when empty), and each category's colour, * icon and symbol. Cached until a location or a category changes. * * @param string[] $slugs Category slugs. * @return array{pins: array, categories: array, icons: array, glyphs: array} */ function deltabreen_maps_location_pins( $slugs ) { // Per language too, as WPML and Polylang show each language its own locations. $key = 'deltabreen_maps_pins_' . md5( DELTABREEN_MAPS_VERSION . '|' . get_option( 'deltabreen_maps_pins_version', 1 ) . '|' . determine_locale() . '|' . implode( ',', $slugs ) ); $cached = get_transient( $key ); if ( is_array( $cached ) ) { return $cached; } $args = array( 'post_type' => 'deltabreen_location', 'post_status' => 'publish', 'posts_per_page' => DELTABREEN_MAPS_MAX_PINS, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, // Lets WPML and Polylang keep to the current language. 'suppress_filters' => false, ); if ( $slugs ) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- one cached query per change, not per view. $args['tax_query'] = array( array( 'taxonomy' => 'deltabreen_location_cat', 'field' => 'slug', 'terms' => $slugs, ), ); } $pins = array(); $colours = array(); $icons = array(); $glyphs = array(); foreach ( get_posts( $args ) as $post ) { $terms = get_the_terms( $post, 'deltabreen_location_cat' ); $term = ( $terms && ! is_wp_error( $terms ) ) ? $terms[0] : null; $pin = deltabreen_maps_clean_pin( array( 'id' => 'loc-' . $post->ID, 'lng' => get_post_meta( $post->ID, '_deltabreen_lng', true ), 'lat' => get_post_meta( $post->ID, '_deltabreen_lat', true ), 'title' => html_entity_decode( $post->post_title, ENT_QUOTES, 'UTF-8' ), 'address' => deltabreen_maps_location_address( $post->ID ), 'phone' => get_post_meta( $post->ID, '_deltabreen_phone', true ), 'url' => get_post_meta( $post->ID, '_deltabreen_url', true ), 'text' => get_post_meta( $post->ID, '_deltabreen_notes', true ), 'category' => $term ? html_entity_decode( $term->name, ENT_QUOTES, 'UTF-8' ) : '', 'hours' => deltabreen_maps_location_hours( $post->ID ), 'photo' => get_post_meta( $post->ID, '_deltabreen_photo', true ), 'button' => get_post_meta( $post->ID, '_deltabreen_button', true ), 'buttonUrl' => get_post_meta( $post->ID, '_deltabreen_button_url', true ), 'glyph' => get_post_meta( $post->ID, '_deltabreen_glyph', true ), 'closedUntil' => get_post_meta( $post->ID, '_deltabreen_closed_until', true ), 'notice' => get_post_meta( $post->ID, '_deltabreen_notice', true ), ) ); /** * One location's pin, before maps get it: change any field, or return null to leave it off every map. * * @param array|null $pin The pin as the embed takes it, or null when the location has no point yet. * @param WP_Post $post The location. */ // With pages of their own, a pin with no button of its own links to its page. if ( is_array( $pin ) && ! isset( $pin['button'] ) && deltabreen_maps_location_pages() ) { $pin['button'] = __( 'Details', 'maps-by-deltabreen' ); $pin['buttonUrl'] = get_permalink( $post ); } $pin = apply_filters( 'deltabreen_maps_location', $pin, $post ); $pin = is_array( $pin ) ? deltabreen_maps_clean_pin( $pin ) : null; if ( ! $pin ) { continue; } if ( $term ) { $colour = deltabreen_maps_category_color( $term->term_id ); if ( '' !== $colour ) { $colours[ $pin['category'] ] = $colour; } $icon = deltabreen_maps_category_icon( $term->term_id ); if ( '' !== $icon ) { $icons[ $pin['category'] ] = $icon; } $glyph = deltabreen_maps_category_glyph( $term->term_id ); if ( '' !== $glyph ) { $glyphs[ $pin['category'] ] = $glyph; } } $pins[] = $pin; } $out = array( 'pins' => $pins, 'categories' => $colours, 'icons' => $icons, 'glyphs' => $glyphs, ); set_transient( $key, $out, DAY_IN_SECONDS ); return $out; } // Any change to a location or a category invalidates the cached pins. add_action( 'save_post_deltabreen_location', 'deltabreen_maps_bump_pins', 20 ); foreach ( array( 'deleted_post', 'trashed_post', 'untrashed_post' ) as $deltabreen_hook ) { add_action( $deltabreen_hook, function ( $post_id, $post = null ) { // A deleted post is gone by now, so its type comes from the post handed over. $type = $post instanceof WP_Post ? $post->post_type : get_post_type( $post_id ); if ( 'deltabreen_location' === $type ) { deltabreen_maps_bump_pins(); } }, 10, 2 ); } foreach ( array( 'created_deltabreen_location_cat', 'edited_deltabreen_location_cat', 'delete_deltabreen_location_cat' ) as $deltabreen_hook ) { add_action( $deltabreen_hook, 'deltabreen_maps_bump_pins' ); } add_action( 'add_meta_boxes_deltabreen_location', function () { add_meta_box( 'deltabreen-location', __( 'Where and when', 'maps-by-deltabreen' ), 'deltabreen_maps_location_box', 'deltabreen_location', 'normal', 'high' ); } ); /** The location's details: address, point on the map, contact, notes and opening hours. */ function deltabreen_maps_location_box( $post ) { $get = function ( $field ) use ( $post ) { return (string) get_post_meta( $post->ID, '_deltabreen_' . $field, true ); }; wp_nonce_field( 'deltabreen_location_save', 'deltabreen_location_nonce' ); ?> $v ) { $out[ $k ] = $v; if ( 'title' === $k ) { $out['deltabreen_where'] = __( 'Address', 'maps-by-deltabreen' ); } } return $out; } ); add_action( 'manage_deltabreen_location_posts_custom_column', function ( $column, $post_id ) { if ( 'deltabreen_where' !== $column ) { return; } echo esc_html( deltabreen_maps_location_address( $post_id ) ); if ( '' === (string) get_post_meta( $post_id, '_deltabreen_lat', true ) ) { echo '
' . esc_html__( 'Not on the map yet: open it and find the address.', 'maps-by-deltabreen' ) . ''; } }, 10, 2 ); // A pin colour for each category. add_action( 'deltabreen_location_cat_add_form_fields', function () { wp_nonce_field( 'deltabreen_category_color', 'deltabreen_category_nonce' ); ?>

term_id ); ?>

taxonomy; $location = in_array( $hook, array( 'post.php', 'post-new.php' ), true ) && 'deltabreen_location' === $screen->post_type; if ( $terms || $location ) { // The "Choose image" buttons open the Media Library. wp_enqueue_media(); wp_enqueue_script( 'deltabreen-maps-images', plugins_url( 'admin/images.js', __FILE__ ), array( 'wp-i18n' ), DELTABREEN_MAPS_VERSION, true ); deltabreen_maps_script_translations( 'deltabreen-maps-images' ); } if ( $terms ) { wp_enqueue_style( 'wp-color-picker' ); wp_enqueue_script( 'wp-color-picker' ); wp_add_inline_script( 'wp-color-picker', 'jQuery(function ($) { $(".deltabreen-color").wpColorPicker(); });' ); } if ( $location ) { deltabreen_maps_enqueue_admin(); wp_enqueue_script( 'deltabreen-maps-location', plugins_url( 'admin/location.js', __FILE__ ), array( 'deltabreen-maps-admin', 'wp-i18n' ), DELTABREEN_MAPS_VERSION, true ); deltabreen_maps_script_translations( 'deltabreen-maps-location' ); wp_enqueue_script( 'deltabreen-maps-hours', plugins_url( 'admin/hours.js', __FILE__ ), array( 'wp-i18n' ), DELTABREEN_MAPS_VERSION, true ); deltabreen_maps_script_translations( 'deltabreen-maps-hours' ); } } ); add_action( 'rest_api_init', function () { // The blocks' list to choose a location from: every published one by name, with or without a point. register_rest_route( 'deltabreen-maps/v1', '/location-list', array( 'methods' => 'GET', 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, 'callback' => function () { $out = array(); foreach ( get_posts( array( 'post_type' => 'deltabreen_location', 'post_status' => 'publish', 'posts_per_page' => DELTABREEN_MAPS_MAX_PINS, 'orderby' => 'title', 'order' => 'ASC', 'no_found_rows' => true, 'suppress_filters' => false ) ) as $post ) { $out[] = array( 'id' => $post->ID, 'title' => html_entity_decode( get_the_title( $post ), ENT_QUOTES, 'UTF-8' ), ); } return $out; }, ) ); // The block editor's view of the locations: the same pins a page would print, for a preview and a count. register_rest_route( 'deltabreen-maps/v1', '/pins', array( 'methods' => 'GET', 'permission_callback' => function () { return current_user_can( 'edit_posts' ); }, 'args' => array( 'locations' => array( 'type' => 'string', 'default' => 'all', 'sanitize_callback' => 'sanitize_text_field', ), 'posts' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key', ), 'posts_in' => array( 'type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_title', ), ), 'callback' => function ( WP_REST_Request $req ) { $from = '' !== $req['locations'] ? deltabreen_maps_location_pins( deltabreen_maps_category_list( $req['locations'] ) ) : array( 'pins' => array(), 'categories' => array(), 'icons' => array(), 'glyphs' => array() ); if ( '' !== $req['posts'] ) { $from['pins'] = array_merge( $from['pins'], deltabreen_maps_post_pins( $req['posts'], $req['posts_in'] ) ); } return array( 'pins' => $from['pins'], 'categories' => (object) $from['categories'], 'icons' => (object) $from['icons'], 'glyphs' => (object) $from['glyphs'], 'count' => count( $from['pins'] ), ); }, ) ); } );